linux write函数为原子操作,多进程,多线程假如APPEND,写入不被打断

您所在的位置:网站首页 write函数会覆盖吗 linux write函数为原子操作,多进程,多线程假如APPEND,写入不被打断

linux write函数为原子操作,多进程,多线程假如APPEND,写入不被打断

2024-07-10 15:02| 来源: 网络整理| 查看: 265

当一个文件被多个进程或者多个线程同时操作时,会不会出现内容交错的现象。例如一个进程向文件写入“AAAA” ,使用语句(write( fd,  "AAAA",  4);),另一个进程向文件写入“BBBB”,语句为(write ( fd,  "BBBB",  4);)。那么最终文件的内容会不会出现“AABBBBAA” 的情况呢?这就涉及到write函数是否是原子操作的问题了。

如果write函数是原子操作,也即写入期间不允许进程或者线程的切换,那么就不会出现上面的情况,最终文件里的内容只可能为“AAAABBBB”, “BBBBAAAA”,“BBBB”, “AAAA”,这四种情况。你可能会感到奇怪,前面两种输出还好理解,后面是什么情况?这就与write函数的写入过程有关了,write分为定位和写入两个阶段,定位操作指定内容写入文件的位置(如 “起始”,“末尾”,或是中间某一位置 ,还记得lseek这个函数吧,它就是完成定位操作的)。试想这样一种情况,第一个进程定位完成后(pos=0),时间片结束,OS切换到第二个写入进程,该进程完成了所有操作后(pos=4)再还给第一个进程操作,由于第一个进程已经完成定位操作,现在开始写入,这样就会覆盖掉前面写的内容,导致出现上面的情况。那么如何解决这个问题呢?......嗯,还记得O_APPEND这个参数吗?它就是用来解决这个问题的,它使得定位与写入成为原子操作,也即每次写入的时候都定位到文件的末尾,然后完成写操作,中间不允许打断。这在打印log日志中可是非常好用哩!!!

如果write不是原子操作,那情况就非常复杂了,因为write可能会随时被打断,写入文件的内容就千奇百怪了。这样我们也就只能借助文件锁(多进程情形)或互斥锁(多线程情形),来解决文件写入问题了。当然文件锁和互斥锁都非常耗资源,而且效率较低,不推荐用这种锁机制。好在大多数的unix和linux都将write设计为原子操作,但这只限于文件,对于管道(pipe),套接字(socket),FIFO 又应当别论了。详情,请参考下面的这篇博文。http://os.51cto.com/art/201108/285324.htm

好了,一番没有例证的空谈都是无力的,下面我们就用代码测试write在多进程和多线程下是否是原子操作,以及在以上环境下,Log日志操作方法该如何设计。我的环境为:Linux CentOS 6.3  内核版本:2.6.32-279.el6.i686

#include #include #include #include #include #define FILE_NAME "demo.txt" #define CONSOLE "/dev/tty" #define err_exit(m) {perror(m); exit(1);} int main(){ int fd; /************************参数说明如下:*********************************** *O_RDWR 对文件的操作权限,可读可写操作 *O_CREAT 如果文件不存在就新建该文件,用于记录写入内容;后面的0644设定文件的进入权限 *O_TRUNC 每次打开文件的时候都清空文件原先的内容 *O_APPEND 设定定位与写入操作的原子性,每次写入都追加到文件末尾 ***********************************************************************/ fd = open(FILE_NAME, O_RDWR | O_CREAT | O_TRUNC | O_APPEND , 0644); if(fd == -1) err_exit( "open error"); int pid; char buf[ 4194304]; //设定一个很大的数组4096*1024,测试其他进程是否可以打断该操作。 memset(buf, 'a', 4194303); //将该数组的内容设定为"aaaaaaa...\n",方便我们观测。 memset(buf+ 4194303, '\n', 1); if((pid = fork()) < 0) err_exit( "fork error"); //利用fork产生子进程,共享同一个文件句柄,可以实现多进程的情形。 if(pid == 0){ int i = 0; for(; i < 10; i++){ write(fd, buf, 4194304); } } else if(pid > 0){ int i = 0; for(; i < 100; i++){ write(fd, "bbb\n", 4); } wait( -1); //等待子进程结束,防止僵尸进程的出现; } if(close(fd) == -1) err_exit( "close error"); return 0; } 然后我们用vi编辑器打开看一下内容,是否有“aaaaaaaaa......"中夹杂着“bbb”字符的情况,你会发现,这两个写入的过程是分开的,不会出现交叉的情况。当然如果你将buf的内容设定的非常大,超过了内核的缓存,则可能出现非原子操作的情况,当然这种情况我们应该避免发生。那么对于多线程的情况,该如何测试呢,请看下面的代码:

#include #include #include #include #include #define FILE_NAME "demo.txt" #define CONSOLE "/dev/tty" #define err_exit(m) {perror(m); exit(1);} int fd; //设置成全局变量,方便下面的程序访问 int main(){ /************************参数说明如下:*********************************** *O_RDWR 对文件的操作权限,可读可写操作 *O_CREAT 如果文件不存在就新建该文件,用于记录写入内容;后面的0644设定文件的进入权限 *O_TRUNC 每次打开文件的时候都清空文件原先的内容 *O_APPEND 设定定位与写入操作的原子性,每次写入都追加到文件末尾 ***********************************************************************/ fd = open(FILE_NAME, O_RDWR | O_CREAT | O_TRUNC | O_APPEND , 0644); pthread_t pth1, pth2, pth3, pth4; char buf[ 4194304]; void write_block(void *); memset(buf, 'a', 4194302); //设定一个很大的数组4096*1024,测试其他进程是否可以打断该操作。 memset(buf+ 4194302, '\n', 1); //将该数组的内容设定为"aaaaaaa...\n",方便我们观测。 memset(buf+ 4194303, '\0', 1); //当然最后以0结尾,因为下面的函数用到strlen方法。 pthread_create(&pth1, NULL, write_block, buf); pthread_create(&pth2, NULL, write_block, "bbbb\n"); pthread_create(&pth3, NULL, write_block, "cccc\n"); pthread_create(&pth4, NULL, write_block, "dddd\n"); pthread_join(pth1, NULL); pthread_join(pth2, NULL); pthread_join(pth3, NULL); pthread_join(pth4, NULL); return 0; } void write_block(void* buf){ int i = 0; int len = strlen(buf); for( ; i < 10; i++){ write(fd, buf,len); } } 同样的,我们用vi编辑器打开文件,发现并没有交错的情况出新,可以说明,write系统调用在buf大小不超过内核缓存的时候,他是原子操作,这样我们就证明了write的确时原子操作。上面的例子我们可以多做几遍,防止偶然事件的发生。

最后如果你想完成打印log日志的操作,可以将要打印的内容放入到一个buf中,最后一次调用write方法,这样就可以让输出的内容不会交错,便于查看内容。如果你想达到格式化输出的效果,可以使用sprintf函数,它与printf的用法是一样的,只是多个(char* buf )参数,最后将内容打印到buf中而不是屏幕上。我不建议分几次调用write方法,这样会怎加系统开销,因为系统会在内核和用户程序间来回切换。



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3