在fork和exec之间运行的线程阻塞其他线程读取

在研究通过使用vfork()而不是fork()来改进recoll性能的可能性时,我遇到了fork()问题,这是我无法解释的。

Recoll重复执行外部命令来翻译文件,这就是示例程序的作用:它启动重复执行“ls”并读回输出的线程。

下面的问题不是“真正的”问题,因为实际的程序不会触发问题。 我只是无意中看到fork()/ vfork()和exec()之间停止了什么线程。

当我有fork()和exec()之间的忙线循环时,另一个线程永远不会完成数据读取:最后一个read()应该指示eof,永远被阻塞或者直到另一个线程的循环结束在这一点上,一切恢复正常,你可以通过replace一个完成的无限循环来看到)。 当read()被阻塞时,“ls”命令已经退出(ps显示<defunct>,一个僵尸)。

这个问题有一个随机的方面,但是大部分时间样本程序“成功”。 我testing了Linux内核3.2.0(Debian),3.13.0(Ubuntu)和3.19(Ubuntu)。 在虚拟机上工作,但你至less需要2个处理器,我不能使用一个处理器。

下面是示例程序,我看不出我做错了什么。

#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <errno.h> #include <memory.h> #include <sys/types.h> #include <sys/wait.h> #include <pthread.h> #include <iostream> using namespace std; struct thread_arg { int tnum; int loopcount; const char *cmd; }; void* task(void *rarg) { struct thread_arg *arg = (struct thread_arg *)rarg; const char *cmd = arg->cmd; for (int i = 0; i < arg->loopcount; i++) { pid_t pid; int pipefd[2]; if (pipe(pipefd)) { perror("pipe"); exit(1); } pid = fork(); if (pid) { cerr << "Thread " << arg->tnum << " parent " << endl; if (pid < 0) { perror("fork"); exit(1); } } else { // Child code. Either exec ls or loop (thread 1) if (arg->tnum == 1) { cerr << "Thread " << arg->tnum << " looping" <<endl; for (;;); //for (int cc = 0; cc < 1000 * 1000 * 1000; cc++); } else { cerr << "Thread " << arg->tnum << " child" <<endl; } close(pipefd[0]); if (pipefd[1] != 1) { dup2(pipefd[1], 1); close(pipefd[1]); } cerr << "Thread " << arg->tnum << " child calling exec" << endl; execlp(cmd, cmd, NULL); perror("execlp"); _exit(255); } // Parent closes write side of pipe close(pipefd[1]); int ntot = 0, nread; char buf[1000]; while ((nread = read(pipefd[0], buf, 1000)) > 0) { ntot += nread; cerr << "Thread " << arg->tnum << " nread " << nread << endl; } cerr << "Total " << ntot << endl; close(pipefd[0]); int status; cerr << "Thread " << arg->tnum << " waiting for process " << pid << endl; if (waitpid(pid, &status, 0) != -1) { if (status) { cerr << "Child exited with status " << status << endl; } } else { perror("waitpid"); } } return 0; } int main(int, char **) { int loopcount = 5; const char *cmd = "ls"; cerr << "cmd [" << cmd << "]" << " loopcount " << loopcount << endl; const int nthreads = 2; pthread_t threads[nthreads]; for (int i = 0; i < nthreads; i++) { struct thread_arg *arg = new struct thread_arg; arg->tnum = i; arg->loopcount = loopcount; arg->cmd = cmd; int err; if ((err = pthread_create(&threads[i], 0, task, arg))) { cerr << "pthread_create failed, err " << err << endl; exit(1); } } void *status; for (int i = 0; i < nthreads; i++) { pthread_join(threads[i], &status); if (status) { cerr << "pthread_join: " << status << endl; exit(1); } } } 

发生什么事是你的管道是由两个子进程而不是一个继承。

你想要做的是:

  1. 创建两端的管道
  2. fork() ,子继承管道的两端
  3. 子关闭读端,父关闭写端

…因此,孩子只是一个管道的一端,这是dup2() 'stdout。

但是你的线程相互竞争,所以会发生什么呢?

  1. 线程1创建两端的管道
  2. 线程0创建两端的管道
  3. 线程1 fork() s。 子进程继承了4个文件描述符,而不是2!
  4. 线程1的子关闭了线程1打开的管道的读端,但是它也保持对线程0管道的读端和写端的引用。

之后,线程0将永远等待,因为它永远不会读取正在读取的管道上的EOF,因为管道的写入结束仍然由线程1的子进程保持打开状态。

您将需要定义一个在pipe()之前开始的关键部分,包含fork() ,并在父级中的close()之后close() ,并使用互斥锁一次只从一个线程进入该关键部分。