在 C 中使用 fgets returns 重复行读取输入
Reading input using fgets returns duplicate lines in C
我正在尝试使用一些 C 代码来实现 shell 并发现 fgets() returns 在我 fork 一个进程后重复的行,我无法理解,我将不胜感激任何帮助。
我的问题是:分叉是否会改变父进程中任何打开文件的偏移量?这似乎发生在我的程序中。
来自下面@Vadim Ponomarev 的回答和我的理解:
fgets() 不是线程安全的(或者严格来说,它是,但是分叉一个进程会导致 stdin 以某种方式被初始化,从而导致共享文件偏移量的变化)。
代码如下:
int main() {
char buf[200];
int r;
pid_t pid = 0;
while(getcmd(buf, 200, pid) >= 0) {
fprintf(stderr, "current pid: %d\n", getpid());
pid = fork();
// Without forking the fgets() reads all lines normally
if(pid == 0)
exit(0);
wait(&r);
}
return 0;
}
getcmd() 函数只是一个包装器:
int
getcmd(char *buf, int nbuf, pid_t pid)
{
memset(buf, 0, nbuf);
if (fgets(buf, nbuf, stdin) == NULL) {
fprintf(stderr, "EOF !!!\n");
return -1;
}
fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
return 0;
}
我还有一个输入文件 temp,其中包含一些随机文本:
line 1
line 2
line 3
编译后,我运行 a.out < temp,输出显示打印了6行,通常有些行是重复的。但是如果我删除行
pid = fork()
...
然后输出就正常了(只是把所有的行都一行一行的显示出来,也就是说fgets()被调用了3次)
知道出了什么问题吗?
输出(这就是得到的):
pid: 10361 -- getcmd buf ======= --> line1
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
EOF !!!
我希望看到这个:
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line1
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
EOF
可供参考的编译版本:
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <zconf.h>
#include <unistd.h>
#include <memory.h>
int
getcmd(char *buf, int nbuf, pid_t pid)
{
memset(buf, 0, nbuf);
if (fgets(buf, nbuf, stdin) == NULL) {
fprintf(stderr, "EOF !!!\n");
return -1;
}
fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
return 0;
}
int main() {
char buf[200];
int r;
pid_t pid = 0;
while(getcmd(buf, 200, pid) >= 0) {
fprintf(stderr, "current pid: %d\n", getpid());
pid = fork();
// Without forking the fgets() reads all lines normally
if(pid == 0)
exit(0);
wait(&r);
}
return 0;
}
谢谢!
- 已经提到父子正在共享文件描述符 0 (stdin) 的当前位置
似乎 libc 运行流(stdin、stdout、stderr)的时间初始化包含一些改变当前 stdin 位置的东西:
> strace -f ./a.out < temp 2>&1 | less
....
write(2, "pid: 29487 -- getcmd buf ======="..., 45pid: 29487 -- getcmd buf ======= --> line 1
clone(child_stack=0,flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,child_tidptr=0x7f34940f19d0) = 29488
Process 29488 attached
[pid 29487] wait4(-1, <unfinished ...>
[pid 29488] lseek(0, -14, SEEK_CUR) = 7
[pid 29488] exit_group(0) = ?
[pid 29488] +++ exited with 0 +++
<... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29488
请注意 child (pid 29488) 中的 lseek(0, -14, SEEK_CUR)
结果在我的环境下(openSUSE Leap 42.2,glibc-2.22-4.3.1)程序无限循环,根本没有EOF
将示例中的 fgets() 更改为 read()
....
if (read(0, buf, nbuf) == 0) {
....
while(getcmd(buf, 7, pid) >= 0) {
....
并按预期编写 运行s(三行和 EOF)
和 运行 strace -f 再次 - child 中不再有 lseek()!
结论 - 似乎流函数(在 stdio.h 中声明)在多进程环境中必须非常谨慎地使用,因为有很多副作用(如本例中)
我从 this thread 中找到了使用 fgets()
的解决方案,它讨论了同样的问题,tldr:
exit flushes the stdio buffers in the child.
...
For more details here is the link corresponding to the POSIX
reference, chapter 2.5.1:
http://pubs.opengroup.org/onlinepubs/007904875/functions/xsh_chap02_05.html
The behaviour is therefore undefined, and thus is allowed to change
between glibc 2.19 and 2.24.
修复:
As written the above link, two solutions are possible to fix the code:
if(fork() == 0) { fclose(fd); exit(1); }
or
if(fork() == 0) { _exit(1); }
我正在尝试使用一些 C 代码来实现 shell 并发现 fgets() returns 在我 fork 一个进程后重复的行,我无法理解,我将不胜感激任何帮助。
我的问题是:分叉是否会改变父进程中任何打开文件的偏移量?这似乎发生在我的程序中。
来自下面@Vadim Ponomarev 的回答和我的理解: fgets() 不是线程安全的(或者严格来说,它是,但是分叉一个进程会导致 stdin 以某种方式被初始化,从而导致共享文件偏移量的变化)。
代码如下:
int main() {
char buf[200];
int r;
pid_t pid = 0;
while(getcmd(buf, 200, pid) >= 0) {
fprintf(stderr, "current pid: %d\n", getpid());
pid = fork();
// Without forking the fgets() reads all lines normally
if(pid == 0)
exit(0);
wait(&r);
}
return 0;
}
getcmd() 函数只是一个包装器:
int
getcmd(char *buf, int nbuf, pid_t pid)
{
memset(buf, 0, nbuf);
if (fgets(buf, nbuf, stdin) == NULL) {
fprintf(stderr, "EOF !!!\n");
return -1;
}
fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
return 0;
}
我还有一个输入文件 temp,其中包含一些随机文本:
line 1
line 2
line 3
编译后,我运行 a.out < temp,输出显示打印了6行,通常有些行是重复的。但是如果我删除行
pid = fork()
...
然后输出就正常了(只是把所有的行都一行一行的显示出来,也就是说fgets()被调用了3次)
知道出了什么问题吗?
输出(这就是得到的):
pid: 10361 -- getcmd buf ======= --> line1
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
current pid: 10361
EOF !!!
我希望看到这个:
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line1
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line2
current pid: 10361
pid: 10361 -- getcmd buf ======= --> line3
EOF
可供参考的编译版本:
#include <stdio.h>
#include <stdlib.h>
#include <wait.h>
#include <zconf.h>
#include <unistd.h>
#include <memory.h>
int
getcmd(char *buf, int nbuf, pid_t pid)
{
memset(buf, 0, nbuf);
if (fgets(buf, nbuf, stdin) == NULL) {
fprintf(stderr, "EOF !!!\n");
return -1;
}
fprintf(stderr, "pid: %d -- getcmd buf ======= --> %s\n", getpid(), buf);
return 0;
}
int main() {
char buf[200];
int r;
pid_t pid = 0;
while(getcmd(buf, 200, pid) >= 0) {
fprintf(stderr, "current pid: %d\n", getpid());
pid = fork();
// Without forking the fgets() reads all lines normally
if(pid == 0)
exit(0);
wait(&r);
}
return 0;
}
谢谢!
- 已经提到父子正在共享文件描述符 0 (stdin) 的当前位置
似乎 libc 运行流(stdin、stdout、stderr)的时间初始化包含一些改变当前 stdin 位置的东西:
> strace -f ./a.out < temp 2>&1 | less .... write(2, "pid: 29487 -- getcmd buf ======="..., 45pid: 29487 -- getcmd buf ======= --> line 1 clone(child_stack=0,flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD,child_tidptr=0x7f34940f19d0) = 29488 Process 29488 attached [pid 29487] wait4(-1, <unfinished ...> [pid 29488] lseek(0, -14, SEEK_CUR) = 7 [pid 29488] exit_group(0) = ? [pid 29488] +++ exited with 0 +++ <... wait4 resumed> [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 29488
请注意 child (pid 29488) 中的 lseek(0, -14, SEEK_CUR)
结果在我的环境下(openSUSE Leap 42.2,glibc-2.22-4.3.1)程序无限循环,根本没有EOF
将示例中的 fgets() 更改为 read()
.... if (read(0, buf, nbuf) == 0) { .... while(getcmd(buf, 7, pid) >= 0) { ....
并按预期编写 运行s(三行和 EOF)
和 运行 strace -f 再次 - child 中不再有 lseek()!
结论 - 似乎流函数(在 stdio.h 中声明)在多进程环境中必须非常谨慎地使用,因为有很多副作用(如本例中)
我从 this thread 中找到了使用 fgets()
的解决方案,它讨论了同样的问题,tldr:
exit flushes the stdio buffers in the child. ... For more details here is the link corresponding to the POSIX reference, chapter 2.5.1:
http://pubs.opengroup.org/onlinepubs/007904875/functions/xsh_chap02_05.html
The behaviour is therefore undefined, and thus is allowed to change between glibc 2.19 and 2.24.
修复:
As written the above link, two solutions are possible to fix the code:
if(fork() == 0) { fclose(fd); exit(1); }
or
if(fork() == 0) { _exit(1); }