SIGPIPE/管線中斷的危險(broken pipe hazard)
想像你正往水管裡灌水,而在你沒注意時,有人把遠端從排水口拔出來、走掉了。水往哪去?沒有地方可落。當一個行程寫入一條讀取端已全部關閉的管線、FIFO 或通訊端——讀取方結束了、當機了,或掛斷了——同樣的事發生了:沒有人接收這些位元組。系統必須對此做出明確處置,而它的處置可能讓你大吃一驚。
預設情況下,核心的回應是送給寫入方 SIGPIPE 信號,而 SIGPIPE 的預設動作是終止行程。所以一個持續寫入已消失讀取方的程式,得到的不是客氣的錯誤碼——它被無聲地殺掉。這正是為什麼把一個長時間執行的命令接給 head 會讓生產者消失:head 讀幾行就結束、關掉讀取端;生產者的下一次寫入觸發 SIGPIPE,於是它死了。這是刻意的、通常也方便(它停掉一個沒有觀眾的生產者),但若你沒料到,它看起來就像一次莫名其妙、毫無訊息的當機。
解法是掌控處置方式。你忽略 SIGPIPE——signal(SIGPIPE, SIG_IG) 或透過 sigaction——這樣 write() 就不再把你殺掉;它改為回傳 -1 並把 errno 設成 EPIPE,一個你能檢查並處理的正常錯誤(關閉連線、記錄它、繼續)。網路伺服器普遍這麼做,因為客戶端在回應途中斷線絕不該讓伺服器當機。精確的心智模型:「broken pipe(管線中斷)」是讀取方不見了;SIGPIPE 是它的非同步通知;EPIPE 是你要求忽略信號後改而得到的同步錯誤。在任何讀取方可能先離開的通道上,永遠要處理這兩者之一。
$ yes | head -n 3 # head 讀 3 行就結束;yes 收到 SIGPIPE 並安靜地停下 在程式裡,要處理它而非死掉: signal(SIGPIPE, SIG_IGN); ssize_t n = write(fd, buf, len); if (n < 0 && errno == EPIPE) { /* 讀取方走了 */ }
預設:寫入已死的讀取方會透過 SIGPIPE 把你殺掉。忽略這個信號,你改而得到可處理的 EPIPE。
未處理時,SIGPIPE 會無聲地終止寫入方、不留任何錯誤訊息——典型的「客戶端一斷線我的伺服器就消失」錯誤。忽略 SIGPIPE(或在通訊端上用 MSG_NOSIGNAL),讓 broken pipe 改成可檢查的 EPIPE。