在fork()之后,寻求关于“文件描述符”的简单说明。

39
在《Unix环境高级编程》第二版的第8.3节“fork函数”中,有这样一段描述:
“父进程和子进程必须共享相同的文件偏移量。考虑一个进程,它派生了一个子进程,然后等待子进程完成。假设两个进程在正常处理中都会写入标准输出。如果父进程将其标准输出重定向到某个文件(比如通过 shell),那么当子进程写入标准输出时,子进程必须更新父进程的文件偏移量。”
你的问题及回答如下:
{1} 这是什么意思?如果父进程的标准输出被重定向到例如“file1”,那么子进程写入后应该更新什么?是父进程原始标准输出的偏移量,还是重定向后的输出(即“file1”)的偏移量?不可能是后者,对吗?
答:是的,子进程要更新父进程原始标准输出的偏移量。
{2} 怎么更新呢?是由子进程显式地更新,还是由操作系统隐式地更新,或者由文件描述符本身更新?我以为 fork 后,父进程和子进程各自走各自的路,拥有自己的文件描述符副本。那么子进程如何将偏移量更新到父进程的一侧呢?
答:在 fork 后,父进程和子进程确实有各自独立的文件描述符副本,但是它们指向同一个打开的文件。当子进程从这个文件读取或写入数据时,文件偏移量会随之改变,并在子进程终止时更新到父进程的文件描述符副本中。
{3} 当调用 fork() 时,我的理解是子进程获得父进程的副本,例如文件描述符,在这种情况下,如果父进程和子进程共享的文件描述符发生了偏移量更改,只能是因为描述符本身记住了偏移量。我的理解正确吗?
答:是的,当父进程调用 fork() 创建子进程时,操作系统会复制父进程的文件描述符表到子进程中,包括文件描述符的所有状态,例如文件偏移量、文件状态标志等。因此,当子进程修改共享的文件描述符时,它也会影响父进程的文件描述符。
2个回答

95

重要的是要区分 文件描述符文件描述符表项,文件描述符是进程在读写调用中用于标识文件的小整数,而文件描述符表项则是内核中的结构。 文件偏移量是文件描述符表项的一部分,它存储在内核中。

让我们以这个程序为例:

#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(void)
{
    int fd;

    fd = open("output", O_CREAT|O_TRUNC|O_WRONLY, 0666);

    if(!fork()) {
        /* child */
        write(fd, "hello ", 6);
        _exit(0);
    } else {
        /* parent */
        int status;

        wait(&status);
        write(fd, "world\n", 6);
    }
}

(省略了所有的错误检查)

如果我们编译这个程序,把它叫做hello,并像这样运行它:

./hello

以下是发生的情况:

程序打开名为output的文件,如果它不存在,则创建该文件;如果已存在,则将其截断为零大小。内核会创建一个文件描述符(在Linux内核中这是一个struct file结构),并将其与调用进程的文件描述符关联起来(即在该进程的文件描述符表中没被使用的最小非负整数)。文件描述符将被返回并分配给程序中的fd。为了方便起见,假设fd为3。

程序进行一次fork()。新的子进程会获得其父进程的文件描述符表的副本,但是文件描述符并不会被复制。两个进程文件表中的第3个条目指向相同的struct file

父进程等待子进程写入完成。子进程的写操作将"hello world\n"的前半部分存储到文件中,并将文件偏移量增加6。文件偏移量位于struct file中!

子进程退出后,父进程的wait()函数结束,并且父进程使用仍然与文件描述符3相关联的相同文件描述符进行写入,该文件描述符的文件偏移量已经被子进程的write()更新。因此消息的后半部分被存储在第一部分的后面,而不是覆盖它,如果父进程具有零的文件偏移量,则会覆盖第一部分(即如果文件描述符未共享,则为该情况)。

最后,父进程退出,内核发现struct file不再使用并释放它。


3
Alan,解释得非常好!感谢你花时间为新手讲解如此详细的内容。 - user1559625
1
@AlanCurry 如果在父进程程序中不使用wait(),那么文件描述符会在父进程退出时被释放吗? - Nmzzz
获取其父进程的文件描述符表的副本,但文件描述符并未被复制。这是文件描述符特定的行为吗(内核描述符共享的事实)?因为例如全局变量是被复制而不是共享的,据我所知。 - Joel Blum
@Allan Curry,为什么你调用 _exit 而不是 exit?你能解释一下你的答案与它们之间的关系吗? - jyz

5
在同一章节中,有一张图显示了打开文件时存在的三个表格。
这些表格包括用户文件描述符表(进程表项的一部分),文件表和inode表(v-node表)。现在,文件描述符(它是文件描述符表的索引)条目指向一个文件表条目,该文件表条目又指向一个inode表条目。文件偏移量(下一次读/写发生的位置)现在在文件表中。
假设您在父进程中打开了一个文件,那么这意味着它具有描述符、文件表条目和inode引用。现在,当您创建一个子进程时,文件描述符表会被复制到子进程中。因此,文件表条目中的引用计数(针对该打开描述符)增加了,这意味着现在有两个引用指向相同的文件表条目。
此描述符现在在父进程和子进程中都可用,指向相同的文件表条目,因此共享偏移量。现在有了这个背景,让我们看看您的问题,
1. 如果将父进程的标准输出重定向到“file1”,那么子进程在写入后应该更新什么?父进程原始的标准输出偏移量还是重定向的输出(即file1)偏移量?不能是后者,对吧?
子进程不需要显式更新任何内容。书的作者试图说明,假设父进程的标准输出被重定向到一个文件,然后进行了fork调用。在此之后,父进程正在等待。因此,描述符现在被复制,即文件偏移量也被共享。现在,无论子进程何时写入任何内容到标准输出,所写的数据都会保存在重定向的文件中。偏移量会自动通过写调用递增。
现在假设子进程退出。那么父进程从等待中出来,并在标准输出上写入一些东西(这是重定向的)。现在父进程的写调用输出将放置在子进程写入的数据之后。为什么呢?因为偏移量的当前值现在已经在子进程写入后更改了。
 Parent ( )
  {
    open a file for writing, that is get the 
    descriptor( say fd);
    close(1);//Closing stdout
    dup(fd); //Now writing to stdout  means writing to the file
    close(fd)
        //Create a child that is do a  fork call.
    ret = fork();
    if ( 0 == ret )
    {
        write(1, "Child", strlen("Child");
        exit ..
    }
        wait(); //Parent waits till child exit.

         write(1, "Parent", strlen("Parent");
    exit ..
}

请看上述伪代码,打开的文件包含的最终数据将是ChildParent。因此,当子进程写入时,文件偏移量会发生改变,并且这可供父进程的写操作使用,因为偏移量是共享的。
2.更新是如何进行的?由子进程显式地,由操作系统隐式地,还是由文件描述符本身完成?在fork之后,我认为父进程和子进程各自拥有自己的文件描述符副本,并且它们分别运行各自的方式。那么子进程如何将偏移量更新到父进程侧?
Now I think the answer is clear-> by the system call that is by the OS.

当调用fork()时,所有我所理解的就是子进程会得到父进程拥有的文件描述符的副本,并进行自己的操作。如果父进程和子进程共享的文件描述符发生任何偏移量的变化,那么这只能是因为描述符自身记住了偏移量。我说得对吗?
还有,用户文件表的条目指向文件表条目(其中包含偏移量),这也应该很清楚。换句话说,系统调用可以从描述符中获取偏移量。

“文件表项中的引用计数(针对该打开描述符)已增加。”您能指出一个参考文献吗?这意味着fork复制父进程的内存不是浅拷贝。 - dashesy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接