【问题标题】:Closing pipe does not interrupt read() in child process spawned from thread关闭管道不会中断从线程产生的子进程中的 read()
【发布时间】:2016-10-29 19:42:53
【问题描述】:

在 Linux 应用程序中,我通过 fork/execvp 生成多个程序,并将标准 IO 流重定向到 IPC 管道。我生成了一个子进程,将一些数据写入子标准输入管道,关闭标准输入,然后从标准输出管道读取子响应。这工作得很好,直到我同时执行了多个子进程,每个子进程使用独立的线程。

当我增加线程数时,我经常发现子进程在从标准输入读取时挂起——尽管read 应该立即以 EOF 退出,因为标准输入管道已被父进程关闭。

我已设法在以下测试程序中重现此行为。在我的系统(Fedora 23、Ubuntu 14.04;g++ 4.9、5、6 和 clang 3.7)上,程序通常会在退出三四个子进程后挂起。尚未退出的子进程挂在read()。杀死任何尚未退出的子进程会导致所有其他子进程神奇地从read() 唤醒,程序继续正常运行。

#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <vector>

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

#define HANDLE_ERR(CODE)     \
    {                        \
        if ((CODE) < 0) {    \
            perror("error"); \
            quick_exit(1);   \
        }                    \
    }

int main()
{
    std::mutex stdout_mtx;
    std::vector<std::thread> threads;
    for (size_t i = 0; i < 8; i++) {
        threads.emplace_back([&stdout_mtx] {
            int pfd[2]; // Create the communication pipe
            HANDLE_ERR(pipe(pfd));

            pid_t pid; // Fork this process
            HANDLE_ERR(pid = fork());
            if (pid == 0) {
                HANDLE_ERR(close(pfd[1])); // Child, close write end of pipe
                for (;;) { // Read data from pfd[0] until EOF or other error
                    char buffer;
                    ssize_t bytes;
                    HANDLE_ERR(bytes = read(pfd[0], &buffer, 1));
                    if (bytes < 1) {
                        break;
                    }

                    // Allow time for thread switching
                    std::this_thread::sleep_for(std::chrono::milliseconds(
                        100));  // This sleep is crucial for the bug to occur
                }
                quick_exit(0); // Exit, do not call C++ destructors
            }
            else {
                { // Some debug info
                    std::lock_guard<std::mutex> lock(stdout_mtx);
                    std::cout << "Created child " << pid << std::endl;
                }

                // Close the read end of the pipe
                HANDLE_ERR(close(pfd[0]));

                // Send some data to the child process
                HANDLE_ERR(write(pfd[1], "abcdef\n", 7));

                // Close the write end of the pipe, wait for the process to exit
                int status;
                HANDLE_ERR(close(pfd[1]));
                HANDLE_ERR(waitpid(pid, &status, 0));

                { // Some debug info
                    std::lock_guard<std::mutex> lock(stdout_mtx);
                    std::cout << "Child " << pid << " exited with status "
                              << status << std::endl;
                }
            }
        });
    }

    // Wait for all threads to complete
    for (auto &thread : threads) {
        thread.join();
    }

    return 0;
}

编译使用

g++ test.cpp -o test -lpthread --std=c++11

请注意,我非常清楚将 fork 和线程混合使用可能很危险,但请记住,在原始代码中,我在分叉后立即调用 execvp,而我没有任何子子进程和主程序之间的共享状态,专门为 IPC 创建的管道除外。我的原始代码(没有线程部分)可以在here找到。

在我看来,这几乎像是 Linux 内核中的一个错误,因为只要我杀死任何挂起的子进程,程序就会继续正确运行。

【问题讨论】:

  • forked 关闭的其他进程可能也打开了该管道。只有在关闭对另一端的最后一个引用时,管道才会关闭。
  • 我检查了管道 fd,它们在所有进程/线程中都是唯一的。在上面的代码中,一个管道究竟是如何在多个进程之间共享的?
  • 我想我现在明白了:如果代码在 pipe() 和 fork() 之间中断,多个进程可能拥有同一个管道...

标签: c++ c linux multithreading pipe


【解决方案1】:

这个问题是由fork 和管道在 Unix 中如何工作的两个基本原则引起的。 a) 管道描述是引用计数的。仅当所有指向其另一端的管道文件描述符(参考描述)都关闭时,管道才关闭。 b) fork 复制一个进程的所有打开的文件描述符。

在上面的代码中,可能会发生以下竞态条件:如果发生线程切换并在pipefork系统调用之间调用fork,则管道文件描述符重复,导致写入/读取结束多次打开。请记住,必须关闭所有重复项才能生成 EOF - 如果有另一个重复项误入不相关的进程,则不会发生这种情况。

最好的解决方案是使用带有O_CLOEXEC 标志的pipe2 系统调用,并在使用dup2 创建文件描述符的受控副本后立即在子进程中调用exec

HANDLE_ERR(pipe2(pfd, O_CLOEXEC));
HANDLE_ERR(pid = fork());
if (pid == 0) {
    HANDLE_ERR(close(pfd[1])); // Child, close write end of pipe
    HANDLE_ERR(dup2(pfd[0], STDIN_FILENO));
    HANDLE_ERR(execlp("cat", "cat"));
}

注意FD_CLOEXEC 标志不会被dup2 系统调用复制。这样一来,所有子进程都会在到达exec系统调用时自动关闭它们不应该接收的所有文件描述符。

来自open O_CLOEXEC 的手册页:

O_CLOEXEC(自 Linux 2.6.23 起) 为新文件描述符启用 close-on-exec 标志。 指定此标志允许程序避免额外的 fcntl(2) 设置 FD_CLOEXEC 标志的 F_SETFD 操作。

请注意,此标志的使用在某些情况下是必不可少的 多线程程序,因为使用单独的 fcntl(2) 设置 FD_CLOEXEC 标志的 F_SETFD 操作不够 避免一个线程打开文件的竞争条件 描述符并尝试使用 fcntl(2) 与另一个线程同时执行 fork(2) 加上 execve(2)。根据执行顺序,比赛 可能导致 open() 返回的文件描述符被 无意中泄露给孩子执行的程序 fork(2) 创建的进程。 (这种比赛在 任何创建文件的系统调用都可能遵循的原则 应设置其 close-on-exec 标志的描述符,以及各种 其他 Linux 系统调用提供了等效的 O_CLOEXEC 标志来处理这个问题。)

当一个子进程被杀死时,所有子进程突然退出的现象可以通过将此问题与哲学家就餐问题进行比较来解释。就像杀死一个哲学家将解决死锁一样,杀死一个进程将关闭一个重复的文件描述符,在另一个子进程中触发一个 EOF,该子进程将退出作为回报,释放一个重复的文件描述符。 ..

感谢 David Schwartz 指出这一点。

【讨论】:

  • 为什么你必须close(pfd[1]) 不会因为O_CLOEXEC 标志而在exec 上关闭?
猜你喜欢
  • 1970-01-01
  • 2015-07-22
  • 2013-02-04
  • 1970-01-01
  • 1970-01-01
  • 2014-01-20
  • 2016-01-02
  • 2015-08-03
  • 2016-05-28
相关资源
最近更新 更多