线程和分叉:从 popen()-pipe 读取时 fgetc() 阻塞

Posted

技术标签:

【中文标题】线程和分叉:从 popen()-pipe 读取时 fgetc() 阻塞【英文标题】:Threads and fork: fgetc() blocks when reading from popen()-pipe 【发布时间】:2016-08-17 13:17:41 【问题描述】:

在一个多线程程序(在 ARM 上运行)我有

一个主线程,除其他外,它定期检查 popen( "pidof -s prog" ) 是否有另一个程序正在运行。我对文件描述符使用O_CLOEXEC 标志并检查fgetc() 是否从管道接收到任何内容。 将文件描述符设置为“非阻塞”不会导致读取任何内容,也无济于事。来自 shell 命令的相同 pidof 命令运行良好。

在另一个线程中,子进程中带有立即execl()fork() 用于在发生特定事件时启动rsync 操作。父母使用信号处理程序来观察孩子的状态,并可以选择在另一个特定事件中杀死孩子。不管我是用rsync 还是sleep 调用exec(),结果都是一样的。

问题是主线程中的fgetc()一直阻塞,直到子进程终止。

我将尝试通过fork()ing 及早解决这个问题(在应用程序是单线程的某个时候,正如我开始的another post 所假设的那样)。

但无论如何:

我想了解从管道读取时导致fgetc() 阻塞的原因。

到目前为止我尝试过的一些事情:

我尝试使用一个小型示例应用程序来重现该问题,该应用程序执行上述操作,并希望它会显示相同的错误行为,但不幸它工作正常,这就是我这样做的原因此处暂不提供任何代码。也许我错过了相关点。 通过system() 使用相同的rsync 调用不会导致任何问题

我查看了system()implementation,可以看到信号在fork()ing 之前被操纵:

SIGCHLD 被阻止 SIGINT 和 SIGQUIT 被忽略

我需要 SIGCHLD 的信号处理程序,但出于好奇,我尝试在上面的代码中执行相同的操作(我将 sigprocmask() 替换为 pthread_sigmask() ) - 没有任何成功,行为保持不变。

我在 BSP 提供的源代码中找不到 system() 的任何实现。

程序通过fstream打开其他文件-并且没有O_CLOEXEC(会有点cumbersome to change that)

【问题讨论】:

system 与内核源代码无关。它在你的 libc 中。 没错,我已经改了 您的问题是什么?假设它是“从管道读取时导致fgetc() 阻塞的原因”,您的编辑现在解决了吗?如果是这样,最好将其移出答案并将其标记为已接受。 我已经发布了编辑,所以人们现在不要再浪费时间来回答这个问题了。它似乎已修复,但我想在将其发布为答案之前进行一些测试以确保。 【参考方案1】:

错误修正和意外行为解释

确实,我错过了相关点。在将示例程序更多地改编为原始代码示例后,我发现信号处理程序(在测试程序中工作)是问题所在。摘录:

void MyClass::sig_handler(int sig) 
    if( m_pid < 1 ) // not the child we're waiting for
        return;

    pid_t pid;
    int wstatus;

    while ((pid = waitpid( -1, &wstatus, WNOHANG )) != -1 ) 
        // error: this returns 0 as long as any children are alive
        // -> check for "> 0" to ignore active child processes
        if( pid != m_pid )
            return;
        // handle stuff here...
    

我不得不替换以下行

while ((pid = waitpid( -1, &wstatus, WNOHANG )) != -1 )

while ((pid = waitpid( -1, &wstatus, WNOHANG )) > 0 )

因为程序的其他线程 fork() 子级(例如 popen())。如果这些终止,则也会调用信号处理程序(静态类函数)。

据我了解:

在我调用fork() 的线程中,我使用一个成员m_pid,默认值和重置值-1。它从fork() 获取pid。如果 m_pid 为 -1,则 sig 处理程序立即返回。

程序在popen() 阻止fork()s(可能是fork()s 的任何其他调用)。因此,当popen() 返回时,将输入 SIGCHLD 的信号处理程序。 m_pid 的检查已通过,因为 m_pid = fork() 已被调用。 waitpid() 不返回 -1,而是返回 popen() 孩子的 pid,然后继续检查返回值 = 0,直到所有孩子都终止 - 我正在等待的孩子还活着!只有这样waitpid() 返回-1,主线程才能继续读取fgetc()

来自waitpid的手册页:

如果指定了 WNOHANG 并且 pid 指定了一个或多个子项 存在,但尚未改变状态,则返回 0。出错时,-1 被退回

因为 sig 处理程序会检查 m_pid != -1,所以只有当我在 MyClass 中使用 fork() 设置 m_pid 时才会出现问题。

这就是为什么使用system() 不会导致问题的原因。 m_pid 未设置为值!= -1,因此 sig 处理程序会立即返回,例如一个孩子在主线程中是popen()ed。

system() 调用的模仿失败,因为我将m_pid 设置为fork(),因此sig 处理程序没有立即返回。

我猜因为 sig 处理程序是 static member function,所以处理程序会阻塞 fork()ed 子进程的线程。

【讨论】:

以上是关于线程和分叉:从 popen()-pipe 读取时 fgetc() 阻塞的主要内容,如果未能解决你的问题,请参考以下文章

在 linux 中,使用 pipe() 从分叉进程调用 system()

实时 subprocess.Popen 通过 stdout 和 PIPE

Python subprocess.Popen PIPE和SIGPIPE

subprocess.Popen与CLI提示交互

为啥在并行子进程之间分叉两次后 pipe() 不工作?

从 PIPE 写入和读取结构时出错