繁体   English   中英

在 c 中执行不带任何选项的命令“cat”

[英]executing the command "cat" with no options in c

如果没有给出 arguments 或重定向使用,则 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);
}

Output:

cat: stdin: Input/output error

我尝试在没有 arguments 的情况下运行它,但它返回错误。

假设:您正在从终端中运行的 shell 运行此程序。 (否则你不会看到这种行为。)

注意:如果您尝试重现此内容,由于父子之间的竞争条件,您可能会看到不同的效果。 问题中的行为是最有可能的,你可以用下面的child_sleeps变体强制它,但是如果孩子在父母退出之前获得大量 CPU 时间,你可能会观察到不同的行为 - 我将在下面用parent_sleeps解释变种。

不带参数的cat尝试从其标准输入(即文件描述符 0)读取。 由于没有任何东西重定向标准输入,它仍然是终端。 尝试从终端读取时 go 会出现什么错误? 让我们查阅一些有关read的文档,例如OpenBSD 手册页1Linux 手册页POSIX 规范 引用 POSIX(其他人的措辞或多或少地复制):

该进程是试图从其控制终端读取的后台进程组的成员,并且调用线程正在阻塞 SIGTTIN 或进程正在忽略 SIGTTIN 或进程的进程组是孤立的。

要理解这一点,您需要了解进程组的基础知识以及它们如何与终端交互。

进程组的基本思想是它由一个进程、它的子进程、它的孙进程等组成,除了已经移动到它们自己的进程组中的子(sub...)进程。 当您从 shell 运行命令时,shell 会将其放入自己的进程组中。 所以有一个进程组,其中包含程序的原始进程和调用fork创建的子进程,没有其他进程。

现在关于终端的部分。 基本思想是一次只有一个程序可以访问终端。 否则,哪个程序会接收输入? 有些程序使用子进程,因此终端的所有权属于进程组,而不仅仅是进程。 拥有终端的进程组称为前台进程组,其他进程组为后台进程组 shell命令fg使一个进程组成为前台进程组。

当进程尝试从终端读取时,kernel 检查它是否“拥有”终端。 更准确地说,进程应该在前台进程组中。 不相关的进程也可以从终端读取(只要它有权打开它,这是公平的游戏,即使这是一件不寻常的事情)。 但是不允许属于后台进程组的进程从终端读取。 正常情况下,kernel给进程发送SIGTTIN信号,默认效果是挂起进程2,3 但是,如果进程忽略或阻止 SIGTTIN,则还有一个进一步的步骤可以防止进程读取: read系统调用因EIO而出错。 这是为了避免不相关的后台程序意外“窃取”前台程序的某些输入的情况。

现在我们可以将它与cat发生的事情联系起来。 cat运行时,它的父级已经退出。 (原则上,如果cat启动足够快,父进程可能还没有退出,但这不太可能。我将在下面用parent_sleeps变体讨论这个问题。)所以子cat进程在它的进程组中是单独的。 当父进程退出时,shell收回终端的所有权,所以cat的进程组是后台进程组,kernel会试图阻止它从终端读取。

但是我们还没有完成: cat没有尝试处理 SIGTTIN,那么为什么 kernel 不发送这个信号呢? 这是EIO的另一种情况: 孤立的进程组 一旦父进程退出(和 shell 通知), cat的父进程就不再退出。 但是进程必须有一个父进程,因此 init 进程(PID 1)“采用”孤儿进程:如果进程的原始父进程消失,则进程的父进程设置为 1。由于cat在其进程组中是单独的,并且父进程是1 不是同一个 session 的一部分,进程组是一个孤儿进程组,kernel 使read返回EIO

顺便说一句,对孤立进程组进行不同处理的原因是,在正常情况下,如果用户在 shell 中运行fg命令,则后台进程组可能 go 回到前台。所以如果后台程序尝试读取从终端,它被暂停,直到它重新获得对终端的访问权限。 但是,如果进程组是孤立的,则不再有用户可以放在前台的 shell 作业,因此没有“正常”的方式让进程返回到 state 允许读取的位置终点站。 所以暂停它是没有意义的:阅读是并且将仍然是一个错误。

要让cat在后台运行,请保持其父进程运行。 您可以运行以下变体parent_waits ,其中父进程等待进程退出。

/* parent_waits.c */
#include <unistd.h>
#include <fcntl.h>

int main(int ac, char **av, char **env)
{
    char *args[] = {"/bin/cat",NULL};
    int ps = fork();
    if (ps) {
        int status;
        wait(&status);
    } else {
        execve("/bin/cat",args, env);
    }
}

我在上面提到过存在竞争条件。 如果您不能可靠地重现问题中的行为,请使用下面的child_sleeps变体,其中孩子睡足够长的时间以使父母完成退出。

/* child_sleeps.c */
#include <unistd.h>
#include <fcntl.h>

int main(int ac, char **av, char **env)
{
    char *args[] = {"/bin/cat",NULL};
    int ps = fork();
    if (!ps) {
        usleep(100000);
        execve("/bin/cat",args, env);
    }
}

如果父母退出的速度很慢,则cat可能会在父母退出之前读取。 您可以通过在启动cat之前添加延迟来强制执行此行为,使用以下parent_sleeps变体:

/* parent_sleeps.c */
#include <unistd.h>
#include <fcntl.h>

int main(int ac, char **av, char **env)
{
    char *args[] = {"/bin/cat",NULL};
    int ps = fork();
    if (ps) {
        sleep(1);
    } else {
        execve("/bin/cat",args, env);
    }
}

使用此变体,直到sleep结束(上面代码中的 1 秒,根据需要调整), cat正常工作。 然后父母退出,你得到一个 shell 提示。 之后,当cat再次尝试读取时,它会收到EIO

$ ./parent_sleeps
one
one
$ two
two
/bin/cat: -: Input/output error

最后一点:您可能想通过查看调试器或跟踪系统调用来观察正在发生的事情。 但是你需要注意不要改变进程组的情况。 比如在Linux下,如果尝试用strace, the strace`进程也在进程组中,一直在前台。

strace -o strace_in_foreground.strace -f ./a.out

要观察导致 EIO 案例的系统调用,请告诉strace分离被跟踪的程序。

strace -o program_in_background.strace -D -f ./a.out

1令人失望的是, FreeBSD 手册省略了相关案例。

2如果这发生在作为 shell 中的作业的进程组中,则 shell 会打印一条消息,例如“已暂停(tty 输入)”(zsh)或“已停止(SIGTTIN)”(ksh)或“已停止”(bash)) .

3尝试写入和 SIGTTOU 信号也会发生同样的情况。 然而,对于 output,进程可以忽略 SIGTTOU 并且写入将通过 go。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM