Kaz*_*aIB 2 c unix posix process execve
如果没有给出任何参数或重定向使用,cat 命令将从标准输入中读取。但是当我用它执行它时,execve()它的行为并不像在 bash 中那样。
代码:
#include <unistd.h>
#include <fcntl.h>
int main(int ac, char **av, char **env)
{
char *args[] = {"/bin/cat",NULL};
int ps = fork();
if (!ps)
execve("/bin/cat",args, env);
}
Run Code Online (Sandbox Code Playgroud)
输出 :
cat: stdin: Input/output error
Run Code Online (Sandbox Code Playgroud)
我尝试不带参数运行它,但它返回一个错误。
假设:您正在从终端中运行的 shell 运行该程序。(否则你不会看到这种行为。)
\n注意:如果您尝试重现此情况,由于父级和子级之间的竞争条件,您可能会看到不同的效果。问题中的行为是最有可能的行为,您可以使用child_sleeps下面的变体强制它,但是如果子级在父级退出 \xe2\x80\x94 之前获得大量 CPU 时间,您可能会观察到不同的行为下面用parent_sleeps变体来解释。
cat不带参数尝试从其标准输入(即文件描述符 0)读取。由于没有任何东西重定向标准输入,它仍然是终端。尝试从终端读取数据时可能会出现什么问题?让我们查阅一些相关文档read,例如OpenBSD 手册页1或Linux 手册页或POSIX 规范。引用 POSIX(其他人或多或少复制了其措辞):
\n\n该进程是尝试从其控制终端读取数据的后台进程组的成员,并且调用线程正在阻止 SIGTTIN,或者该进程正在忽略 SIGTTIN,或者该进程的进程组是孤立的。
\n
要理解这一点,您需要了解进程组的基础知识以及它们如何与终端交互。
\n进程组的基本思想是,它由一个进程、它的子进程、它的孙子进程等组成,除了已经移动到自己的进程组中的子(sub\xe2\x80\xa6)进程之外。当您从 shell 运行命令时,shell 会将其放入自己的进程组中。因此,存在一个进程组,其中包含程序的原始进程和通过调用创建的子进程fork,而没有其他进程。
现在是关于终端的部分。基本思想是一次只能有一个程序访问终端。否则哪个程序会接收输入?有些程序使用子进程,因此终端的所有权属于进程组,而不仅仅是进程。拥有终端的进程组称为前台进程组,其他进程组称为后台进程组。shell命令fg使进程组成为前台进程组。
当进程尝试从终端读取数据时,内核会检查它是否 \xe2\x80\x9cowns\xe2\x80\x9d 终端。更准确地说,该进程应该位于前台进程组中。不相关的进程也可以从终端读取(只要它有权打开它,那就是公平的游戏,即使这是一件不寻常的事情)。但是属于后台进程组的进程不允许从终端读取。通常情况下,内核会向进程发送SIGTTIN信号,默认效果是挂起进程2,3。但是,如果进程忽略或阻止 SIGTTIN,则需要采取进一步的步骤来阻止进程读取:系统read调用错误EIO。这是为了避免不相关的后台程序意外 \xe2\x80\x9csteal\xe2\x80\x9d 一些来自前台程序的输入。
现在我们可以将其与 发生的情况联系起来cat。当时间cat运行时,其父级已经退出。(原则上,如果启动足够快,父进程可能还没有退出cat,但这不太可能。我将在下面与变体讨论这一点parent_sleeps。)因此,子cat进程在其进程组中是单独的。当父进程退出时,shell收回终端的所有权,因此该进程组cat是后台进程组,内核会尝试阻止其从终端读取。
但我们还没有完全做到这一点:cat不尝试处理 SIGTTIN,那么为什么内核不发送这个信号呢?这是另一种情况EIO:孤立进程组。一旦父进程退出(并且 shell 通知),cat\ 的父进程就不再退出。但进程必须有父进程,因此 init 进程 (PID 1) \xe2\x80\x9cadopts\xe2\x80\x9d 孤儿进程:如果进程的原始父进程消失,则该进程的父进程设置为 1 . 由于cat在其进程组中是单独的,且父进程为 1,不属于同一会话,因此该进程组是孤儿进程组,内核read返回EIO。
fg顺便说一句,对孤立进程组进行不同处理的原因是,在正常情况下,如果用户在 shell 中运行命令,后台进程组可能会返回前台。因此,如果后台程序尝试从终端读取数据,它将被挂起,直到有希望重新获得对终端的访问权限。但是,如果进程组是孤立的,则用户不再可以将 shell 作业放入前台,因此没有 \xe2\x80\x9cnormal\xe2\x80\x9d 方式让进程返回到说明允许从终端读取数据的位置。因此,暂停它是没有意义的:读取现在是、将来仍然是一个错误。
要允许cat在后台运行,请保持其父进程运行。您可以运行以下变体parent_waits,其中父进程等待进程退出。
/* parent_waits.c */\n#include <unistd.h>\n#include <fcntl.h>\n\nint main(int ac, char **av, char **env)\n{\n char *args[] = {"/bin/cat",NULL};\n int ps = fork();\n if (ps) {\n int status;\n wait(&status);\n } else {\n execve("/bin/cat",args, env);\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n我上面提到了存在竞争条件。如果您无法可靠地重现问题中的行为,请使用child_sleeps下面的变体,其中孩子睡眠足够长的时间,以便父母完成退出。
/* child_sleeps.c */\n#include <unistd.h>\n#include <fcntl.h>\n\nint main(int ac, char **av, char **env)\n{\n char *args[] = {"/bin/cat",NULL};\n int ps = fork();\n if (!ps) {\n usleep(100000);\n execve("/bin/cat",args, env);\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n如果父级退出速度很慢,则有可能cat在父级退出之前能够读取。您可以通过在启动之前添加延迟来强制执行此行为cat,使用以下parent_sleeps变体:
/* parent_sleeps.c */\n#include <unistd.h>\n#include <fcntl.h>\n\nint main(int ac, char **av, char **env)\n{\n char *args[] = {"/bin/cat",NULL};\n int ps = fork();\n if (ps) {\n sleep(1);\n } else {\n execve("/bin/cat",args, env);\n }\n}\nRun Code Online (Sandbox Code Playgroud)\n使用此变体,直到sleep结束(上面代码中的 1 秒,根据需要调整),都cat可以正常工作。然后父进程退出,您会收到 shell 提示符。之后,当cat尝试再次读取时,它会收到EIO.
$ ./parent_sleeps\none\none\n$ two\ntwo\n/bin/cat: -: Input/output error\nRun Code Online (Sandbox Code Playgroud)\n最后一点:您可能会想通过查看调试器或跟踪系统调用来观察正在发生的情况。但您需要小心,不要改变进程组的情况。例如,在Linux下,如果您尝试使用strace, the strace正常跟踪程序,进程也在进程组中并保持在前台。
strace -o strace_in_foreground.strace -f ./a.out\nRun Code Online (Sandbox Code Playgroud)\n要观察导致 EIO 情况的系统调用,请strace告诉分离跟踪的程序。
strace -o program_in_background.strace -D -f ./a.out\nRun Code Online (Sandbox Code Playgroud)\n1 令人失望的是,FreeBSD 手册省略了相关案例。
\n2 如果这种情况发生在 shell 中作为作业的进程组中,则 shell 会打印类似 \xe2\x80\x9csuspended (tty input)\xe2\x80\x9d (zsh) 或 \xe2\x80\x9cStopped (SIGTTIN) 的消息)\xe2\x80\x9d (ksh) 或 \xe2\x80\x9cStopped\xe2\x80\x9d (bash))。
\n3 尝试写入和 SIGTTOU 信号时也会发生同样的情况。然而,对于输出,该进程可以忽略 SIGTTOU 并且写入将完成。
\n