多进程写同一文件-write原子性-log日志
# 参考
https://www.jianshu.com/p/b5a731940ff9 (opens new window)
# append
在打开文件时使用append追加模式,则写入文件时不会因为文件指针位置错误而覆盖已写入的内容(linux内系统调用都是原子性的,多线程安全)
使用setvbuf设置文件写入模式为行缓冲,即每次遇到换行或缓冲满了时写入文件。(测试不要它文件也不会乱)
实测在多线程或多进程模式下,写同一个文件时,比如log,可以多进程各自按行写入,不会出现多行错乱的情形(比如一行内出现两个线程要写的东西,printf就会乱),也不会出现写入数据被其他线程覆盖的情形。
# setvbuf
- int setvbuf(FILE *stream, char *buffer, int mode, size_t size)
# 测试多进程用append模式写同一个文件
main.cpp
#include <unistd.h>
#include <stdio.h>
#include <thread>
#include <sys/types.h>
#include <fcntl.h>
void fun1() {
printf("func1 start, write aaaa\n");
int fd = open("log", O_CREAT | O_RDWR | O_APPEND);
for(int i=0; i<100000; ++i) {
write(fd, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\n", 33);
usleep(10);
}
close(fd);
printf("func1 end\n");
}
void fun2() {
printf("func2 start, write bbbb\n");
int fd = open("log", O_RDWR | O_APPEND);
for(int i=0; i<100000; ++i) {
write(fd, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\n", 33);
usleep(10);
}
close(fd);
printf("func2 end\n");
}
int main(int argc, char *argv[]) {
std::thread t1(fun1);
usleep(1000);
std::thread t2(fun2);
if(t1.joinable()) {
t1.join();
}
if(t2.joinable()) {
t2.join();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
main2.cpp, 将上面的aaaa bbbb改成cccc dddd即可
编译:
set -x
g++ -o main -pthread main.cpp
g++ -o main2 -pthread main2.cpp
2
3
开两个窗口分别执行main和main2,然后查看log文件,会发现没有数据错乱。
# 注
write操作是原子性的,配合append模式时多进程写文件不会混乱。但是要保证write写完,即write的返回值与要写的长度一样,而不是写不完就返回了。通常也都是写完了才返回。
对于socket的多线程write,阻塞模式下, 正常都会写完再返回, 不需要加锁。 非阻塞模式下,如果一次没写完(缓冲区满了),则有可能发生数据混乱。
普通文件的写入,一般write会一次性写完,不会写一半返回,不像socket。
# 底层原理
参考 https://blog.csdn.net/weixin_33959449/article/details/112632457 (opens new window)
- APPEND模式,同一文件的write原子性的,系统通过锁inode,保证每次写操作均在inode中获取的文件size后追加数据,写完后释放锁;
- 非APPEND模式, 只有共享fd时write是原子性的,系统通过锁file结构体后获取file结构体的pos字段,并将数据追加到pos后,写完更新pos字段后释放锁。
值得一再重申的是,由于write调用只是在inode或者file层面上保证一次写操作的原子性,但无法保证用户需要写入的数据的一次肯定被写完,所以在多线程多进程文件共享情况下就需要用户态程序自己来应对short write问题,比如设计一个锁保护一个循环,直到写完成或者写出错,不然循环不退出,锁不释放