【问题标题】:Blocking read on pipe after fork when using pipe instead of pipe2使用管道而不是管道2时,在fork后阻止读取管道
【发布时间】:2017-10-11 09:53:12
【问题描述】:

当在spawn() 中派生特定进程时,以下代码有时会阻塞spawn() 中的read(fds[0]...)

#include <fcntl.h>
#include <unistd.h>

#include <atomic>
#include <mutex>
#include <thread>
#include <vector>

void spawn()
{
  static std::mutex m;
  static std::atomic<int> displayNumber{30000};

  std::string display{":" + std::to_string(displayNumber++)};
  const char* const args[] = {"NullXServer", display.c_str(), nullptr};

  int fds[2];

  m.lock();
  pipe(fds);
  int oldFlags = fcntl(fds[0], F_GETFD);
  fcntl(fds[0], F_SETFD, oldFlags | FD_CLOEXEC);
  oldFlags = fcntl(fds[1], F_GETFD);
  fcntl(fds[1], F_SETFD, oldFlags | FD_CLOEXEC);
  m.unlock();

  if (vfork() == 0) {
    execvp("NullXServer", const_cast<char**>(args));
    _exit(0);
  }

  close(fds[1]);
  int i;
  read(fds[0], &i, sizeof(int));
  close(fds[0]);
}

int main()
{
  std::vector<std::thread> threads;
  for (int i = 0; i < 100; ++i) {
    threads.emplace_back(spawn);
  }

  for (auto& t : threads) {
    t.join();
  }

  return 0;
}

注意;在这里创建管道有点没用。这样做只是为了演示死锁。 spawn() 中的 read(fds[0], ...) 不应该阻塞。一旦调用read,管道的所有写端都已关闭,这将导致read 立即返回。由于文件描述符上设置了FD_CLOEXEC标志,父进程中管道的写端是显式关闭的,子进程中的写端是隐式关闭的,它会在@时立即关闭文件描述符987654330@ 成功(在这种情况下总是如此)。

这里的问题是我确实看到 read() 偶尔阻塞。

全部替换:

m.lock();
pipe(fds);
int oldFlags = fcntl(fds[0], F_GETFD);
fcntl(fds[0], F_SETFD, oldFlags | FD_CLOEXEC);
oldFlags = fcntl(fds[1], F_GETFD);
fcntl(fds[1], F_SETFD, oldFlags | FD_CLOEXEC);
m.unlock();

作者:

pipe2(fds, O_CLOEXEC);

修复了阻塞读取,即使这两段代码至少应该导致 FD_CLOEXEC 被自动设置为管道文件描述符。

很遗憾,我在我们部署的所有平台上都没有pipe2

谁能解释为什么read 会使用pipe 方法阻塞上述代码?

更多观察:

  • 扩展互斥锁以覆盖vfork() 块可解决阻塞读取问题。
  • 没有一个系统调用失败。
  • 使用fork() 代替vfork() 表现出相同的行为。
  • 产生的进程很重要。在这种情况下,会在特定显示器上生成一个“空”X 服务器进程。例如,在这里分叉 'ls' 不会阻塞,或者发生阻塞的可能性要低得多,我不确定。
  • 可在 Linux 2.6.18 到 4.12.8 上重现,所以我认为这不是某种 Linux 内核问题。
  • 可使用 GCC 4.8.2 和 GCC 7.2.0 重现。

【问题讨论】:

    标签: linux multithreading fork


    【解决方案1】:

    原因是,你在这里创建了管道

    // Thread A
    int fds[2];
    
    m.lock();
    pipe(fds);
    

    另一个线程可能只是 vfork() 和 exec

    // Thread B
    if (vfork() == 0) {
       execvp("NullXServer", const_cast<char**>(args));
       _exit(0);
    }
    

    就在你设置文件描述符标志之前:

    // Thread A again...
    int oldFlags = fcntl(fds[0], F_GETFD);
    fcntl(fds[0], F_SETFD, oldFlags | FD_CLOEXEC);
    oldFlags = fcntl(fds[1], F_GETFD);
    fcntl(fds[1], F_SETFD, oldFlags | FD_CLOEXEC);
    m.unlock();
    

    所以 B 的子进程将继承线程 A 创建的文件描述符。

    应该有助于扩展互斥体以包含vfork()/execvp() 以减轻这种影响。

    m.lock();
    pipe(fds);
    int oldFlags = fcntl(fds[0], F_GETFD);
    fcntl(fds[0], F_SETFD, oldFlags | FD_CLOEXEC);
    oldFlags = fcntl(fds[1], F_GETFD);
    fcntl(fds[1], F_SETFD, oldFlags | FD_CLOEXEC);
    
    if (vfork() == 0) {
        execvp("NullXServer", const_cast<char**>(args));
        _exit(0);
    }
    m.unlock();
    

    【讨论】:

    • 谢谢,当然。现在有点惭愧:) 当然,这里的互斥锁并不能保证在设置文件描述符标志时实际上只有一个线程会运行,这是我出于某种原因的印象。
    • @TonvandenHeuvel 不必感到尴尬,并发代码往往是一个无法正确思考的皮塔饼;)
    猜你喜欢
    • 2022-01-03
    • 2010-09-15
    • 2013-04-07
    • 1970-01-01
    • 1970-01-01
    • 2022-11-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多