在Linux应用程序中,我通过fork
/ execvp
生成多个程序,并将标准IO流重定向到IPC的管道。我生成一个子进程,将一些数据写入子stdin管道,关闭stdin,然后从stdout管道读取子响应。这很好,直到我同时执行多个子进程,每个子进程使用独立的线程。
一旦我增加线程数,我经常会发现子进程在从stdin读取时挂起 - 尽管read
应该立即退出EOF,因为stdin管道已被父进程关闭。
我已成功在以下测试程序中重现此行为。在我的系统上(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内核中的一个错误,因为一旦我杀死任何挂起的子进程,程序就会正确地继续。
答案 0 :(得分:1)
这个问题是由fork
和管道如何在Unix中工作的两个基本原则引起的。 a)管道描述是参考计数。如果所有指向其另一端的管道文件描述符(引用描述)都已关闭,则管道仅关闭。 b)fork
复制进程的所有打开文件描述符。
在上面的代码中,可能会发生以下竞争条件:如果发生线程切换并且在pipe
和fork
系统调用之间调用fork,则管道文件描述符会重复,从而导致写入/ read结束多次打开。请记住,必须关闭所有重复项才能生成EOF - 如果存在另一个重复的错误,则不会发生这种情况。
最佳解决方案是使用带有pipe2
标志的O_CLOEXEC
系统调用,并在使用{创建文件描述符的受控副本后立即调用子进程中的exec
{1}}:
dup2
请注意,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(自Linux 2.6.23起) 为新文件描述符启用close-on-exec标志。 指定此标志允许程序避免其他操作 fcntl(2)F_SETFD操作,用于设置FD_CLOEXEC标志。
请注意,在某些情况下使用此标志至关重要 多线程程序,因为使用单独的fcntl(2) 设置FD_CLOEXEC标志的F_SETFD操作是不够的 避免一个线程打开文件的竞争条件 描述符并尝试使用设置其close-on-exec标志 fcntl(2)同时另一个线程做fork(2) 加上execve(2)。根据执行的顺序,比赛 可能导致open()返回的文件描述符 无意中泄露给孩子执行的程序 fork(2)创建的进程。 (这种比赛是在 任何创建文件的系统调用都可能的原则 应该设置close-on-exec标志的描述符,以及各种各样的 其他Linux系统调用提供相当于 O_CLOEXEC标志来处理这个问题。)
当一个孩子的过程被杀死时,所有儿童过程突然退出的现象可以通过将这个问题与餐饮哲学家的问题进行比较来解释。与杀死其中一个哲学家将解决死锁的方式相同,杀死其中一个进程将关闭其中一个重复的文件描述符,在另一个子进程中触发EOF,该进程将退出,从而释放其中一个重复的文件描述符。 ..
感谢David Schwartz指出这一点。