accept 迴圈與 SO_REUSEPORT
/ accept -> ak-SEPT; SO_REUSEPORT -> ess-oh REUSE-port /
一個接受連線的伺服器有一道門與一位迎賓。門是監聽通訊端——你把它 bind() 到一個連接埠並對它 listen()。迎賓就是 accept 迴圈:一個反覆呼叫 accept() 的循環,每當有客戶端抵達,它就遞回一個全新的、已連線的通訊端,而監聽通訊端本身保持開著以迎接下一個。一個多連線伺服器所做的一切,都始於這個迴圈把新到者轉成一個個的逐連線描述符。
基本迴圈是:for(;;) { int conn = accept(listen_fd, ...); handle(conn); }。在非阻塞的事件迴圈伺服器裡,你改為盯著 listen_fd 的可讀性、反覆呼叫 accept() 直到它回傳 EAGAIN,把所有待處理的連線抽乾。擴展上的問題是:當一個監聽通訊端被許多工作執行緒共享時,它們在它上面競爭、並承受驚群喚醒。SO_REUSEPORT 是解法:它是一個通訊端選項,讓「多個」通訊端同時 bind() 到完全相同的連接埠。你給每個工作執行緒自己的、位於這個共享連接埠上的監聽通訊端,核心則把每條進來的連線雜湊到其中恰好一個。如今工作執行緒從不在單一接受點上競爭——每個都有一個由核心填充的私有佇列。
為何重要:SO_REUSEPORT 把接受路徑從一個被競爭的瓶頸,變成乾淨的、由核心驅動的跨核心負載分配,並且允許優雅重啟(新行程可在舊行程退出前就綁定連接埠,於是沒有連線被拒)。誠實的提醒:核心的分配是依雜湊、而非依哪個工作執行緒最閒,所以一個卡在慢請求上的工作執行緒,仍可能有新連線排在它後面;而一個監聽通訊端關閉時,可能短暫擾動核心先前已導入其佇列的連線,這在重啟時值得在意。
int s = socket(AF_INET, SOCK_STREAM, 0); int one = 1; setsockopt(s, SOL_SOCKET, SO_REUSEPORT, &one, sizeof one); bind(s, addr, addrlen); listen(s, 1024); /* 每個工作執行緒對同一連接埠都這麼做 */
設了 SO_REUSEPORT,每個工作執行緒在共享連接埠上綁定自己的監聽通訊端,核心把連線分散到它們之間。
別把 SO_REUSEPORT 與 SO_REUSEADDR 搞混:SO_REUSEADDR 主要讓你能在重啟後重新綁定一個卡在 TIME_WAIT 的連接埠,而 SO_REUSEPORT 讓多個存活的通訊端共享一個連接埠以做負載分配。核心是依雜湊分散連線,所以這與「導向最不忙的工作執行緒」並不相同。