澄清管道()和dup2()如何在C中工作

时间:2014-01-09 23:23:15

标签: c fork pipe dup2 execve

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

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]指向),它也写入了所有信息,从而创建了一个管道的抽象?

2 个答案:

答案 0 :(得分:3)

  1. 您提供的代码中没有任何内容。 forkdup2close都会立即运行。代码不会在您打印的行中的任何位置暂停执行。如果您正在观察任何等待或挂起,则会在您的代码中的其他位置(例如,在致电waitpidselectread时)。

  2. 每个进程都有自己的文件描述符表。文件对象在所有进程之间是全局的(文件系统中的文件可能多次打开,不同的文件对象代表它),但文件描述符是每个进程,这是每个进程引用文件对象的一种方式。所以像“1”或“2”这样的文件描述符只对你的过程有意义 - “文件号1”和“文件号2”可能意味着与另一个进程不同。但是进程可能引用相同的文件对象(尽管每个进程可能有不同的数字)。

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

    除非你像stdin / stdout / stderr(罕见)上的freopen那样奇怪,否则它们只是fds 0,1,2的同义词。如果要编写原始字节,请使用文件描述符编号调用write;如果你想写漂亮的字符串,用stdin / stdout / stderr调用fprintf - 它们会去同一个地方。

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

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

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

答案 1 :(得分:2)

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

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

在过去的好日子里,当缓冲区较小且计算机速度很慢时,您实际上可以依赖于间歇性唤醒的读取过程,即使对于少量数据,例如大约数十千字节。现在,在许多情况下,阅读过程一次性获得输入。