为何vfork()后关闭同个文件描述符不会触发错误?
我编写了如下代码片段:打开文件filename.txt,通过vfork()+exec()创建子进程,在子进程中尝试用grep查找文件内容。
FILE *file = fopen("filename.txt", "r"); int pipefds[2]; pipe(pipefds); pid_t pid = vfork(); if (pid == 0) { // Child process close(pipefds[0]); // child doesn't need read end of the pipe dup2(fileno(file), STDIN_FILENO); pclose(file); dup2(pipefds[1], STDOUT_FILENO); close(pipefds[1]); execlp("grep", "grep", "pattern", NULL); } if (close(pipefds[1]) == -1) { // Why is this successful perror("close"); return 1; } char buffer[1024]; // Why I can read from pipefds[0] if I closed it before exec() function while (read(pipefds[0], buffer, sizeof(buffer) - 1) > 0) { std::cout << buffer << std::endl; } pclose(file); // Segmentation Fault here, as expected
我的疑问:
由于vfork()到execlp()期间子进程与父进程共享地址空间,子进程调用close(pipefds[1])理应修改父进程的文件描述符,但父进程在exec()后调用close(pipefds[1])并未报错,而调用pclose(file)却触发了预期的段错误。此外,为何子进程在exec()前关闭pipefds[0]后,父进程仍能从pipefds[0]读取数据?
核心前提:区分地址空间与文件描述符表
vfork()确实让子进程共享父进程的用户态地址空间,但文件描述符是进程级的独立资源——每个进程有自己专属的文件描述符表,地址空间共享不代表这个表会被共享。这是所有问题的关键。
逐个解答疑问
1. 父进程close(pipefds[1])为何成功?
子进程里调用的close(pipefds[1])操作的是子进程自己的文件描述符表,和父进程的表完全无关。父进程的pipefds[1]从pipe创建后就一直处于有效状态,直到父进程自身调用close,所以这个操作自然不会报错。
2. 子进程close(pipefds[0])后,父进程还能读取pipefds[0]?
同样的逻辑:子进程关闭的是自己持有的管道读端描述符,父进程的管道读端(pipefds[0])完全不受影响。管道的可用性取决于当前有多少进程持有对应端的文件描述符——只要父进程还持有pipefds[0],就能正常读取子进程中grep写入管道的数据。
3. pclose(file)触发段错误的原因
FILE*是用户态的结构体,存储在进程的地址空间中。子进程在vfork共享地址空间期间调用pclose(file),会直接销毁或置空这个结构体。父进程后续再使用已经被破坏的FILE*调用pclose,必然触发段错误——因为地址空间共享,子进程对结构体的修改会直接作用于父进程的内存。
内容的提问来源于stack exchange,提问作者Vahe Margaryan

