2

我正在编写一个处理管道的简单外壳。我有工作代码,但我不太明白它是如何在幕后工作的。这是我需要帮助理解的修改后的代码片段(我删除了错误检查以缩短它):

int fd[2];
pipe(fd);

if (fork()) { /* parent code */
    close(fd[1]);
    dup2(fd[0], 0);

    /* call to execve() here */

} else { /* child code */
    close(fd[0]);
    dup2(fd[1], 1);
}

我对我的问题有猜测,但仅此而已 - 猜测。以下是我的问题:

  1. 阻塞在哪里执行?在我见过的所有示例代码中,read()write()提供了阻塞,但我不需要在这里使用它们。我只是复制 STDIN 指向管道的读取端和 STDOUT 指向管道的写入端。我猜正在发生的是 STDIN 在执行后dup2(fd[0], 0)正在执行阻塞。这个对吗?
  2. 据我了解,每个正在运行的进程都有一个描述符表,指向文件表中打开的文件。当进程重定向 STDIN、STDOUT 或 STDERR 时会发生什么?这些文件描述符是否在所有进程的描述符表中共享?还是每个流程都有副本?重定向一个是否会导致更改在所有这些中反映出来?
  3. 在调用pipe()和随后的调用之后fork(),管道有 4 个“端”打开:父级访问的读取和写入端以及子级访问的读取和写入端。在我的代码中,我关闭了父母的写入端和孩子的读取端。但是,在完成管道后,我不会关闭剩余的两端。代码工作正常,所以我假设某种隐式关闭已经完成,但这都是猜测工作。我应该像这样添加显式调用来关闭剩余的两端吗?

    int fd[2];
    pipe(fd);
    
    if (fork()) { /* parent code */
        close(fd[1]);
        dup2(fd[0], 0);
    
        /* call to execve() here */
    
        close(fd[0]);
    
    } else { /* child code */
        close(fd[0]);
        dup2(fd[1], 1);
        close(fd[1]);
    }
    
  4. 这更多是关于管道过程如何工作的概念性问题。有管道的读取端,由文件句柄引用,fd[0]管道的写入端,由文件句柄引用fd[1]。管道本身只是一个字节流表示的抽象。文件句柄代表打开的文件,对吗?那么这是否意味着在系统的某个地方,有一个文件(由 指向fd[1]),其中包含我们想要通过管道发送的所有信息?并且在通过字节流推送该信息之后,有一个文件(由 指向fd[0]),其中也写入了所有信息,从而创建了管道的抽象?

4

2 回答 2

3
  1. 您提供的代码中没有任何内容块。fork, dup2, 和close所有立即操作。该代码不会在您打印的行中的任何地方暂停执行。如果您观察到任何等待或挂起,则它位于代码中的其他位置(例如,在对waitpidorselect或的调用中read)。

  2. 每个进程都有自己的文件描述符表。文件对象在所有进程之间是全局的(文件系统中的文件可能会被多次打开,不同的文件对象代表它),但文件描述符是每个进程的,这是每个进程引用文件对象的一种方式。因此,像“1”或“2”这样的文件描述符仅在您的进程中有意义——“文件编号 1”和“文件编号 2”可能与另一个进程的含义不同。但是进程可以引用同一个文件对象(尽管每个进程可能有不同的编号)。

    因此,从技术上讲,这就是为什么您可以在文件描述符上设置两组标志的原因,即不在进程之间共享的文件描述符标志 (F_CLOEXEC),以及甚至在进程之间共享的文件对象标志 (例如 O_NONBLOCK) .

    除非你在 stdin/stdout/stderr 上做一些奇怪的事情,比如 freopen(很少见),否则它们只是 fds 0、1、2 的同义词。当您要写入原始字节时,请write使用文件描述符编号进行调用;如果您想编写漂亮的字符串,请fprintf使用 stdin/stdout/stderr 调用——它们位于同一个地方。

  3. 没有完成任何隐式关闭,你只是逃避它。是的,你应该在完成后关闭文件描述符——从技术上讲,我写if (fd[0] != 0) close(fd[0]);只是为了确保!

  4. 不,没有任何内容写入磁盘。这是一个内存支持的文件,这意味着缓冲区不会存储在任何地方。当您写入磁盘上的“常规”文件时,写入的数据由内核存储在缓冲区中,然后尽快传递到磁盘以提交。当您写入管道时,它会同样进入内核管理的缓冲区,但通常不会进入磁盘。它只是坐在那里,直到它被管道的读取端读取,此时内核丢弃它而不是保存它。

    管道有一个读写端,所以写入的数据总是在缓冲区的末尾,读出的数据从缓冲区的头部取出,然后被删除。因此,流动有严格的顺序,就像在物理管道中一样:进入一端的水滴首先从另一端流出。如果远端的水龙头已关闭(进程未读取),则您无法将更多数据推送(写入)到管道的末端。如果没有写入数据并且管道清空,则必须在读取时等待,直到有更多数据通过。

于 2014-01-09T23:50:46.317 回答
2

首先,您通常在execve进程中调用或其姐妹调用之一,而不是在父进程中。请记住,父母知道自己的孩子是谁,但反之亦然。

管道下面实际上是一个由操作系统处理的缓冲区,这样可以保证如果缓冲区已满,则尝试写入它会阻塞,如果没有可读取的内容,则可以保证对它的读取阻塞。这就是您遇到的阻塞的来源。

在过去的美好时光,当缓冲区很小且计算机运行缓慢时,您实际上可以依靠间歇性唤醒读取过程,即使是少量数据,例如几十千字节的数量级也是如此。现在,在许多情况下,阅读过程可以一次性获得输入。

于 2014-01-10T00:02:02.657 回答