2015-05-26 48 views
2

在研究通过使用vfork()而不是fork()来改进Recoll性能的可能性时,我遇到了fork()问题,这是我无法解释的。在fork和exec块之间运行的线程其他线程读取

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

以下问题不是“真正的”问题,因为实际的程序不会触发问题。我只是偶然看到fork()/ vfork()和exec()之间的线程是停止还是停止。

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

这个问题有一个随机的方面,但示例程序大部分时间“成功”。我测试了Linux内核3.2.0(Debian),3.13.0(Ubuntu)和3.19(Ubuntu)。在虚拟机上工作,但你至少需要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); 
     } 
    } 
} 
+0

我喜欢解决你的难题:) – Celada

回答

2

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

你想要做的是:

  1. 创建管与2结束
  2. fork(),孩子继承了管
  3. 子进程关闭读端的两端,父进程关闭写端

...这样孩子最终只有一个管道的一端,这就是dup2()'stdout'。

但你的线程的比赛对方,所以会发生什么是这样的:

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

后来,线程永远等待0,因为它从来没有得到的,因为管道写端仍持有线程1的孩子打开它在读取管道的EOF。

您需要定义pipe()开始之前的一个关键部分,包围fork(),并在父close()结束后,使用互斥在同一时间进入只从一个线程的关键部分。

+0

哇,辉煌。 Unix的30年,我看不到这个...... – medoc

+0

只是一个简单的评论:临界区需要在父关闭后结束,而不是在fork之后结束,否则另一个孩子仍然可以继承fd。 – medoc

+0

你对'close()'是对的,我编辑了我的答案。 – Celada