当一个文件被多个进程或者多个线程同时操作时,会不会出现内容交错的现象。例如一个进程向文件写入“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
-
-
-
-
-
-
-
-
-
-
-
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;
-
}
-
-
-
-
-
-
-
-
-
-
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);
-
}
-
}
最后如果你想完成打印log日志的操作,可以将要打印的内容放入到一个buf中,最后一次调用write方法,这样就可以让输出的内容不会交错,便于查看内容。如果你想达到格式化输出的效果,可以使用sprintf函数,它与printf的用法是一样的,只是多个(char* buf )参数,最后将内容打印到buf中而不是屏幕上。我不建议分几次调用write方法,这样会怎加系统开销,因为系统会在内核和用户程序间来回切换。