阻断spawn()分叉的具体处理当在spawn()当使用管代替pipe2

问题描述:

的下面的代码有时块上read(fds[0]...)叉后读取上管。阻断<code>spawn()</code>分叉的具体处理当在<code>spawn()</code>当使用管代替pipe2

#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], ...) in spawn()不应该阻塞。一旦调用read,管道的所有写入端都会关闭,这将导致read立即返回。父进程中管道的写端被显式关闭,并且由于文件描述符上设置了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()表现出相同的行为。
  • 产生的过程很重要。在这种情况下,在特定的显示器上会生成一个'空'X服务器进程。例如,在这里分叉'ls'不会阻止,或发生阻塞的可能性显着降低,我不确定。
  • 可以在Linux 2.6.18上重现到4.12.8,所以这不是我假设的某种Linux内核问题。
  • 使用GCC 4.8.2和GCC 7.2.0可重现。

这样做的原因是,你在这里创建

// 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(); 
+0

谢谢,当然。现在感到有点惭愧:)当然,这里的互斥体并不能保证只有一个线程在设置文件描述符标志时会实际运行,这是我出于某种原因而产生的印象。 –

+0

@TonvandenHeuvel不需要感到尴尬,并发代码往往是皮塔考虑得当的;) – Ctx