当一个文件被多个进程或者多个线程同时操作时,会不会出现内容交错的现象。例如一个进程向文件写入“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

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

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

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

Logo

更多推荐