多进程并发写入文件异常:程序执行结果不符预期的问题排查
多进程并发写入文件异常排查
需求与问题现象
需要实现一个包含1个主进程+9个子进程的程序,并发写入output.txt文件:
- 主进程写入5个0:
"00000" - 第1个子进程写入5个1:
"11111" - 第2个子进程写入5个2:
"22222" - ...以此类推,最终文件内容应为
00000111112222233333444445555566666777778888899999
但连续执行10次后,结果频繁出现数字缺失、顺序混乱的情况。
原程序代码(numbers.c)
int main(void) { int fd1,fd2,i,pos; char c; char buffer[6]; fd1 = open("output.txt", O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR); write(fd1, "00000", 5); for (i=1; i < 10; i++) { pos = lseek(fd1, 0, SEEK_CUR); if (fork() == 0) { /* Child */ sprintf(buffer, "%d", i*11111); lseek(fd1, pos, SEEK_SET); write(fd1, buffer, 5); close(fd1); exit(0); } else { /* Parent */ lseek(fd1, 5, SEEK_CUR); } } //wait for all childs to finish while (wait(NULL) != -1); lseek(fd1, 0, SEEK_SET); printf("File contents are:\n"); while (read(fd1, &c, 1) > 0) printf("%c", (char) c); printf("\n"); close(fd1); exit(0); }
10次执行结果
$ for i in $(seq 10); do ./numbers ; done File contents are: 0000011111222223333355555666668888899999 File contents are: 00000111112222255555666668888899999 File contents are: 0000011111222223333355555666668888899999 File contents are: 00000111112222244444666667777799999 File contents are: 00000444447777755555666668888899999 File contents are: 00000222224444455555777778888899999 File contents are: 0000011111222224444455555777778888899999 File contents are: 0000011111222225555544444888887777799999 File contents are: 0000011111222224444455555777778888899999 File contents are: 0000011111222225555544444888887777799999
代码中的核心问题
1. 多进程共享文件偏移量的竞态条件
fork()后,子进程会复制父进程的文件描述符,但文件偏移量是内核中打开文件对象的公共属性,并非进程私有。父进程和所有子进程的文件描述符指向同一个内核文件对象,共享同一个偏移量:
- 父进程在循环中先获取当前偏移量
pos,随后fork创建子进程,紧接着就执行lseek(fd1,5,SEEK_CUR)修改偏移量,此时子进程还未执行自己的lseek,导致子进程后续要使用的pos对应的偏移量已经被篡改。 - 多个子进程之间也会互相干扰:比如子进程A执行
lseek设置好写入位置后,CPU切换到子进程B执行lseek修改偏移量,当CPU切回子进程A时,write会从子进程B设置的偏移量开始写入,完全偏离目标位置。
2. lseek与write操作非原子性
子进程中lseek定位位置和write写入的两步操作不是原子的,中间可能被其他进程的操作打断,导致写入位置错误、内容被覆盖。
3. 无进程同步机制
多个进程并发操作文件时,没有使用锁机制保证各自写入操作的独立性,进程间的操作完全无序,进一步加剧了内容混乱的问题。
修复方案
方案1:固定写入位置+记录锁
每个进程直接计算自己的固定写入起始位置(主进程写0-4,子进程i写i*5到i*5+4),同时使用fcntl记录锁保证lseek+write操作的原子性:
#include <fcntl.h> #include <sys/wait.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { int fd1,i; char c; char buffer[6]; fd1 = open("output.txt", O_CREAT | O_TRUNC | O_RDWR, S_IRUSR | S_IWUSR); // 主进程写入固定位置0-4 lseek(fd1, 0, SEEK_SET); write(fd1, "00000", 5); for (i=1; i < 10; i++) { if (fork() == 0) { /* Child */ sprintf(buffer, "%d", i*11111); off_t target_pos = i * 5; // 申请写入区域的独占锁 struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET, .l_start = target_pos, .l_len = 5, .l_pid = getpid() }; fcntl(fd1, F_SETLKW, &lock); // 阻塞等待锁 lseek(fd1, target_pos, SEEK_SET); write(fd1, buffer, 5); // 释放锁 lock.l_type = F_UNLCK; fcntl(fd1, F_SETLK, &lock); close(fd1); exit(0); } } // 等待所有子进程完成 while (wait(NULL) != -1); // 打印结果 lseek(fd1, 0, SEEK_SET); printf("File contents are:\n"); while (read(fd1, &c, 1) > 0) printf("%c", (char) c); printf("\n"); close(fd1); exit(0); }
方案2:父进程预分配位置(无需锁)
由于每个进程的写入区域完全不重叠,父进程可以提前计算好每个子进程的写入位置,在fork前传递给子进程,子进程直接使用固定位置写入,无需锁机制即可避免冲突。
内容的提问来源于stack exchange,提问作者Tempname52
相关产品推荐
相关产品推荐

