文件写操作的线程/进程安全性
需求:
如果有多个线程/进程同时写一个文件,会不会出现写乱的情况,例如:
- 一个按行写 :111111111111
- 另一个按行写 : 222222222222
最后输出会不会出现1和2混杂在同一行的情况,像111111222211这种情况。
结论是:
- 如果使用系统调用write写,那么不会出现内容写乱的情况。
- 如果使用libc库函数fwrite写,则会出现内容写乱的情况。
原因是,系统调用write能够保证操作的原子性,一个写操作只有完成才能返回,下一个写操作才能进入。而libc库函数fwrite不是一个系统调用,无法保证操作的原始性;事实上fwrite还有缓存的功能,能够让多个fwrite的操作缓存成一个write操作,测试我们会发现fwrite的性能要比write高很多,当然代价是fwrite无法保证写的原子性,会导致数据杂乱了。
使用write的例子:
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
int main(int argc, char * argv[])
{
char * filename = "datafile";
int fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
if(fd == -1) {
printf("Failed to open file:%s, errno:%d,%s\n", filename, errno, strerror(errno));
return -1;
}
// Suppose argv[1] buffer size >= 12
argv[1][10]='\n';
argv[1][11]='\0';
int i = 0;
for (; i < 10000000; i++) {
write(fd, argv[1], strlen(argv[1]));
}
close(fd);
return 0;
}
编译运行,在两个窗口同时起两个命令:
$ time ./a.out 11111111111111
real 1m20.910s
user 0m0.576s
sys 0m13.176s
在另一终端:
$ time ./a.out 222222222222
real 1m20.356s
user 0m0.590s
sys 0m13.061s
检查输出文件:
$ sed -n '/^1.*$/p' datafile | grep 2
$ sed -n '/^2.*$/p' datafile | grep 1
因为我们都是按行输出的,内容都是111111111111和2222222222,上面的规则表达式检查所有以1开头的行是否包含字符2,以及所有以2开头的行是否包含1。
可见都没有,即每一行要么都是1要么都是2。
使用fwrite的例子
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char * argv[])
{
char * filename = "datafile";
FILE * fp = fopen(filename , "a");
if(fp == NULL) {
printf("Failed to open file:%s, errno:%d,%s\n", filename, errno, strerror(errno));
return -1;
}
// Suppose argv[1] buffer size >= 12
argv[1][10]='\n';
argv[1][11]='\0';
int i = 0;
for (; i < 10000000; i++) {
fwrite(argv[1], 1 , strlen(argv[1]) , fp);
}
fclose(fp);
return 0;
}
编译运行:
$ time ./a.out 111111111111
real 0m1.522s
user 0m0.268s
sys 0m0.108s
同时在另一个终端启动:
$ time ./a.out 222222222222
real 0m1.388s
user 0m0.271s
sys 0m0.101s
查看结果:
$ sed -n '/^1.*$/p' datafile | grep 2
111122
1111111122222
12
1111122222222
1111111112222
1111112222222
1111111111222
1112222222222
1111111222222
1111222222222
1111111122222
12
1111111112222
1111112222222
...
可以看到很多杂乱的行,一行里面既有1,又有2说明fwrite并不是原子的行为。
另外还可以观察到两者的性能差异,同样写入10000000条数据。
- write耗时1分20秒
- fwrite耗时1.5秒
这个差距可不是一般的小。
总结:
事实上很多的log机制都是直接使用APPEND模式+write来实现写日志行为,而不需要外部行为来保证日志的同步,操作系统本身的write系统调用就能保证不同的logger写入log文件的原子性。
APPEND保证文件每次都是从文件的结果处写入;如果不指定APPEND,只要不移动文件指针也能达到同步的目的,因为lseek和write一样都是系统调用保证操作原子性,但是lseek和write之间的行为不是原子性的,不同的写入者可能会移动读写指针,导致数据写乱。
而如果指定了APPEND模式,那么就保证无法使用lseek来移动读写指正,每回都是写入文件末尾。