问题描述
故事
我尝试在 Linux 上诊断用 C 编写的应用程序中的错误。事实证明,该错误是由于 fclose
句柄在父进程中仍处于打开状态时在子进程中忘记 FILE *
引起的。
文件操作只有read
。没有写操作。
案例 1
应用程序正在 Linux 5.4.0-58-generic
上运行。在这种情况下,发生了错误。
案例 2
应用程序正在 Linux 5.10.0-051000-generic
上运行。在这种情况下,没有错误,这正是我所期望的。
什么是错误?
如果子进程中没有 fork
,父进程会随机执行 fclose
次系统调用。
案例2确认
我完全知道忘记 fclose
会导致内存泄漏,但是:
我目前的猜测:
这是一个 Linux 内核错误,已在 fclose
之后的版本中修复。然而我没有证据,但我的测试证明了这一点。
问题
我已经能够通过在子进程退出之前调用 5.4
来修复此应用程序错误。但是,我想知道在这种情况下实际发生了什么。所以我的问题是为什么在子进程中忘记 fclose
会影响父进程?
重现问题的非常简单的代码(附上 3 个文件)。
注意:test1.c 和 test2.c 的区别仅在于子进程中的 fclose
。 test2.c 不会在子进程中调用 fclose
。
文件 test.txt
fclose
文件 test1.c
123123123
123123123
123123123
123123123
123123123
123123123
文件 test2.c
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define TICK do { putchar('.'); fflush(stdout); } while(0)
int main() {
char buff[1024] = {0};
FILE *handle = fopen("test.txt","r");
uint32_t num_of_forks = 0;
while (fgets(buff,1024,handle) != NULL) {
TICK;
num_of_forks++;
pid_t pid = fork();
if (pid == -1) {
printf("Fork error: %s\n",strerror(errno));
continue;
}
if (pid == 0) {
fclose(handle);
exit(0);
}
}
fclose(handle);
putchar('\n');
printf("Number of forks: %d\n",num_of_forks);
wait(NULL);
}
运行程序
在 Linux 5.4.0-58-generic 上运行(发生错误的地方)
看看test2执行(bug),它导致fork syscall的随机数。
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define TICK do { putchar('.'); fflush(stdout); } while(0)
int main() {
char buff[1024] = {0};
FILE *handle = fopen("test.txt",strerror(errno));
continue;
}
if (pid == 0) {
// fclose(handle);
exit(0);
}
}
fclose(handle);
putchar('\n');
printf("Number of forks: %d\n",num_of_forks);
wait(NULL);
}
在 Linux 5.10.0-051000-generic 上运行(相同的代码,完全没有错误)
ammarfaizi2@integral:/tmp$ uname -r
5.4.0-58-generic
ammarfaizi2@integral:/tmp$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
copyright (C) 2019 Free Software Foundation,Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or fitness FOR A PARTIculaR PURPOSE.
ammarfaizi2@integral:/tmp$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.1) 2.31
copyright (C) 2020 Free Software Foundation,Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or fitness FOR A PARTIculaR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
ammarfaizi2@integral:/tmp$ cat test.txt
123123123
123123123
123123123
123123123
123123123
123123123
ammarfaizi2@integral:/tmp$ diff test1.c test2.c
27c27
< fclose(handle);
---
> // fclose(handle);
ammarfaizi2@integral:/tmp$ gcc test1.c -o test1 && gcc test2.c -o test2
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test2
..................................................................................................................................................................................
Number of forks: 178
ammarfaizi2@integral:/tmp$ ./test2
............................................................................................................................................................................................................................................................................................................................................................
Number of forks: 348
ammarfaizi2@integral:/tmp$ ./test2
...........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
Number of forks: 475
ammarfaizi2@integral:/tmp$ md5sum test1 test2
c32d03916b9b72546b966223837fd115 test1
f314d2135092362288a66f53b37ffa4d test2
总结
- 忘记
root@esteh:/tmp# uname -r 5.10.0-051000-generic root@esteh:/tmp# gcc --version gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0 copyright (C) 2019 Free Software Foundation,Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or fitness FOR A PARTIculaR PURPOSE. root@esteh:/tmp# ldd --version ldd (Ubuntu GLIBC 2.31-0ubuntu9.1) 2.31 copyright (C) 2020 Free Software Foundation,Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or fitness FOR A PARTIculaR PURPOSE. Written by Roland McGrath and Ulrich Drepper. root@esteh:/tmp# cat test.txt 123123123 123123123 123123123 123123123 123123123 123123123 root@esteh:/tmp# diff test1.c test2.c 27c27 < fclose(handle); --- > // fclose(handle); root@esteh:/tmp# gcc test1.c -o test1 && gcc test2.c -o test2 root@esteh:/tmp# ./test1 ...... Number of forks: 6 root@esteh:/tmp# ./test1 ...... Number of forks: 6 root@esteh:/tmp# ./test1 ...... Number of forks: 6 root@esteh:/tmp# ./test2 ...... Number of forks: 6 root@esteh:/tmp# ./test2 ...... Number of forks: 6 root@esteh:/tmp# ./test2 ...... Number of forks: 6 root@esteh:/tmp# md5sum test1 test2 # Make sure the files are identical with case 1 c32d03916b9b72546b966223837fd115 test1 f314d2135092362288a66f53b37ffa4d test2
上的子进程中的fclose
会导致父进程中的 fork 系统调用变得奇怪。 - 该错误似乎不存在于
Linux 5.4.0-58-generic
。
解决方法
感谢@Jonathan Leffler!
此问题与 Why does forking my process cause the file to be read infinitely
重复缺失的知识,为什么在Linux 5.10.0-051000-generic
上没有出现这个bug,原来与内核无关。
原来是父进程与子进程竞争(与内核无关)。
- 注意:如果句柄是由父进程创建的,那么从子进程更改文件句柄的偏移量也会改变父进程中的偏移量 >.
- 如果子进程中没有
fclose(3)
,子进程会在调用lseek(2)
后立即调用exit(3)
。这将导致父级重新读取相同的偏移量,因为子级使用负偏移量 +lseek(2)
调用SEEK_CUR
。
(我不知道为什么要在退出前调用 lseek(2)
,可能在@Jonathan Leffler 的回答中已经解释过了,我没有仔细阅读整个答案)。
- 如果父级在子级调用
lseek(2)
之前完成读取整个文件。那么就完全没有问题了。
此外,正如@iBug 所提到的但请记住,除非您实施某种“同步”,否则进程调度可能会使结果不可预测。
我使用的 Linux 5.10.0-051000-generic
机器上的父进程只是一个幸运进程,它总是在孩子调用 lseek(2)
之前先读取整个文件。
我尝试向文件中添加更多行(为 150 行),因此父级通常会比读取 6 行慢,并且会发生未定义行为。