2017-10-11 14 views
1

的下面的代碼有時塊上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可重現。

回答

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

謝謝,當然。現在感到有點慚愧:)當然,這裏的互斥體並不能保證只有一個線程在設置文件描述符標誌時會實際運行,這是我出於某種原因而產生的印象。 –

+0

@TonvandenHeuvel不需要感到尷尬,併發代碼往往是皮塔考慮得當的;) – Ctx

相關問題