使用管道而不是pipe2时阻止在fork之后读取管道

时间:2017-10-11 09:53:13

标签: linux multithreading fork

read(fds[0]...)中分支特定流程时,以下代码有时会在spawn() spawn()上阻止。

#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;
}

请注意;在这里创建管道有点无用。它只是为了证明僵局。 read(fds[0], ...)中的spawn()永远不会阻止。调用read后,管道的所有写入端都已关闭,这会导致read立即返回。父进程中管道的写端显式关闭,子进程中的write-end由于文件描述符上设置FD_CLOEXEC标志而隐式关闭,这将立即关闭文件描述符当execvp成功时(在这种情况下它总是如此)。

这里的问题是我确实偶尔会看到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()表现出相同的行为。
  • 产生的过程很重要。在这种情况下,&#39; null&#39; X服务器进程在特定显示器上生成。分叉&#39; ls&#39;这里例如没有阻止,或阻止发生的可能性显着降低,我不确定。
  • 在Linux 2.6.18上可重现,最高可达4.12.8,所以这不是我认为的某种Linux内核问题。
  • 使用GCC 4.8.2和GCC 7.2.0重现。

1 个答案:

答案 0 :(得分: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();