Linux x86-64 fork 系统调用针对 C 标准 libc 文件的奇怪行为 I/O(关键字:fork、fclose、linux)
Linux x86-64 fork syscall strange behavior against C standard libc FILE I/O (keywords: fork, fclose, linux)
故事
我试图在 Linux 上诊断一个用 C 编写的应用程序中的错误。原来这个bug是由于父进程中FILE *
句柄仍然打开时子进程中忘记fclose
造成的
文件操作只有read
。没有写操作。
案例一
该应用在 Linux 5.4.0-58-generic
上 运行。在这种情况下,错误发生了。
案例二
该应用在 Linux 5.10.0-051000-generic
上 运行。在这种情况下,没有错误,这正是我所期望的。
错误是什么?
如果子进程中没有 fclose
,则父进程执行随机数 fork
系统调用。
案例2肯定
我完全知道忘记 fclose
会导致内存泄漏,但是:
- 我认为,只是在这种情况下,并不是绝对必要的,因为子进程要尽快退出,而我使用的退出是
exit(3)
不是 _exit(2)
.
- 奇怪的是,为什么子进程中的遗忘
fclose
会影响到父进程?
我目前的猜测:
这是一个 Linux 内核错误,已在 5.4
之后的版本中修复。然而我没有证据,但我的测试证明了这一点。
问题
我已经能够通过在子进程退出前调用 fclose
来修复这个应用程序错误。但是,我想知道在这种情况下实际发生了什么。所以我的问题是 为什么在子进程中忘记 fclose
会影响父进程?
重现问题的代码非常简单(附加了 3 个文件)。
注意:test1.c和test2.c的区别只在子进程的fclose
处。 test2.c 不在子进程中调用 fclose
。
文件test.txt
123123123
123123123
123123123
123123123
123123123
123123123
文件test1.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);
}
文件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 执行(错误),它导致随机数的 fork 系统调用。
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
运行 on Linux 5.10.0-051000-generic(相同的代码,完全没有错误)
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
总结
- 在
Linux 5.4.0-58-generic
的子进程中忘记 fclose
会导致父进程中的 fork 系统调用变得奇怪。
Linux 5.10.0-051000-generic
上似乎不存在该错误。
感谢@Jonathan Leffler!
此问题与
重复
遗漏的知识,为什么bug没有出现在Linux 5.10.0-051000-generic
原来是跟内核无关
结果是 parent 进程与 child 进程竞争(与内核无关)。
- 注意:从 child process 中更改文件句柄的偏移量也会更改 parent process[=52 中的偏移量=] 如果 句柄是由 parent.
创建的
- 如果 child 中没有
fclose(3)
,child 进程将在调用 exit(3)
后立即调用 lseek(2)
。这将导致 parent re-read 相同的偏移量,因为 childs 调用 lseek(2)
具有负偏移量 + SEEK_CUR
.
(我不知道为什么要在退出前调用lseek(2)
,可能在@Jonathan Leffler 的回答中有解释,我没有仔细阅读整个回答)。
- 如果 parent 在 child 调用
lseek(2)
之前完成读取整个文件。那么就完全没有问题了
此外,正如@iBug 提到的那样但是请记住,进程调度可能会产生结果non-predictable,除非您实施某种“同步”。
我使用的 Linux 5.10.0-051000-generic
机器上的 parent 进程只是一个 幸运进程 总是在 [=65] 之前先读取整个文件=] 打电话给 lseek(2)
.
我尝试向文件中添加更多行(150 行),因此 parent 大部分比读取 6 行要慢,而且 未定义的行为 发生了。
故事
我试图在 Linux 上诊断一个用 C 编写的应用程序中的错误。原来这个bug是由于父进程中FILE *
句柄仍然打开时子进程中忘记fclose
造成的
文件操作只有read
。没有写操作。
案例一
该应用在 Linux 5.4.0-58-generic
上 运行。在这种情况下,错误发生了。
案例二
该应用在 Linux 5.10.0-051000-generic
上 运行。在这种情况下,没有错误,这正是我所期望的。
错误是什么?
如果子进程中没有 fclose
,则父进程执行随机数 fork
系统调用。
案例2肯定
我完全知道忘记 fclose
会导致内存泄漏,但是:
- 我认为,只是在这种情况下,并不是绝对必要的,因为子进程要尽快退出,而我使用的退出是
exit(3)
不是_exit(2)
. - 奇怪的是,为什么子进程中的遗忘
fclose
会影响到父进程?
我目前的猜测:
这是一个 Linux 内核错误,已在 5.4
之后的版本中修复。然而我没有证据,但我的测试证明了这一点。
问题
我已经能够通过在子进程退出前调用 fclose
来修复这个应用程序错误。但是,我想知道在这种情况下实际发生了什么。所以我的问题是 为什么在子进程中忘记 fclose
会影响父进程?
重现问题的代码非常简单(附加了 3 个文件)。
注意:test1.c和test2.c的区别只在子进程的fclose
处。 test2.c 不在子进程中调用 fclose
。
文件test.txt
123123123
123123123
123123123
123123123
123123123
123123123
文件test1.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);
}
文件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 执行(错误),它导致随机数的 fork 系统调用。
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
运行 on Linux 5.10.0-051000-generic(相同的代码,完全没有错误)
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
总结
- 在
Linux 5.4.0-58-generic
的子进程中忘记fclose
会导致父进程中的 fork 系统调用变得奇怪。 Linux 5.10.0-051000-generic
上似乎不存在该错误。
感谢@Jonathan Leffler!
此问题与
遗漏的知识,为什么bug没有出现在Linux 5.10.0-051000-generic
原来是跟内核无关
结果是 parent 进程与 child 进程竞争(与内核无关)。
- 注意:从 child process 中更改文件句柄的偏移量也会更改 parent process[=52 中的偏移量=] 如果 句柄是由 parent. 创建的
- 如果 child 中没有
fclose(3)
,child 进程将在调用exit(3)
后立即调用lseek(2)
。这将导致 parent re-read 相同的偏移量,因为 childs 调用lseek(2)
具有负偏移量 +SEEK_CUR
.
(我不知道为什么要在退出前调用lseek(2)
,可能在@Jonathan Leffler 的回答中有解释,我没有仔细阅读整个回答)。
- 如果 parent 在 child 调用
lseek(2)
之前完成读取整个文件。那么就完全没有问题了
此外,正如@iBug 提到的那样但是请记住,进程调度可能会产生结果non-predictable,除非您实施某种“同步”。
我使用的 Linux 5.10.0-051000-generic
机器上的 parent 进程只是一个 幸运进程 总是在 [=65] 之前先读取整个文件=] 打电话给 lseek(2)
.
我尝试向文件中添加更多行(150 行),因此 parent 大部分比读取 6 行要慢,而且 未定义的行为 发生了。