網路服務器架構最基礎的分野在於迭代與並發。本文以偽代碼對比兩種模型,重點剖析 fork() 並發模型中文件描述符引用計數導致的經典陷阱:父進程必須關閉 connfd、子進程必須關閉 listenfd,否則將導致連接洩漏、描述符耗盡、服務不可用。
while (1) {
int connfd = accept(listenfd, ...); // 阻塞等待連接
handle_request(connfd); // 處理完才能接下一個
close(connfd);
}
while (1) {
int connfd = accept(listenfd, ...);
if (fork() == 0) { // 子進程
close(listenfd); // 【關鍵】子進程不需監聽
handle_request(connfd);
close(connfd);
exit(0);
}
close(connfd); // 【關鍵】父進程不持有連接 fd
}
fork() 會複製父進程文件描述符表,子進程獲得的 fd 與父進程指向同一個 struct file 內核對象。struct file 內含 f_count 引用計數,fork() 時 +1,close() 時 -1,歸零才真正釋放內核資源(斷開 TCP 連接、釋放 socket 緩衝區)。| 錯誤做法 | 後果 |
|---|---|
父進程不 close(connfd) | f_count 始終 ≥1,連接永不釋放 → 客戶端 FIN 後服務端仍保持 ESTABLISHED → 描述符耗盡、新連接被拒 |
子進程不 close(listenfd) | 浪費 fd;更嚴重:若父進程意外退出,子進程仍持有 listenfd → 端口被佔用、重啟失敗;多子進程同時持有 listenfd 可能引發 驚群問題(內核 4.5+ 已優化) |
父進程若不 wait(),子進程退出後變為殭屍(Z 狀態),佔用 task_struct 與 PID。解決方案:
signal(SIGCHLD, SIG_IGN); // Linux 內核自動回收(推薦)
// 或
void sigchld_handler(int sig) {
while (waitpid(-1, NULL, WNOHANG) > 0);
}
signal(SIGCHLD, sigchld_handler);
| 模型 | 特點 | 適用場景 |
|---|---|---|
| prefork / worker 池 | 主進程 fork 固定數量 worker,共享 listenfd,accept 序列化 | Apache、Nginx、高併發短連接 |
| 線程池 | pthread + 共享內存,輕量上下文切換 | 計算密集、需共享大量狀態 |
| 事件驅動 + 非阻塞 IO | 單線程 epoll/kqueue/IOCP,零切換、百萬併發 | Nginx、Redis、Node.js、高併發長連接 |
| IO_uring | Linux 5.1+,真異步、零拷貝、批量提交 | 極致性能存儲/網路 |
並發服務器 fork() 模型雖經典,但父關 connfd、子關 listenfd 是鐵律,源於 struct file 引用計數機制。理解此機制,不僅能避免「描述符洩漏、端口佔用、驚群」等經典坑,更能舉一反三應對 dup()、線程共享 fd、exec() 繼承 fd 等場景。現代高併發架構多已轉向事件驅動或混合模型,但文件描述符生命週期管理的核心邏輯始終不變。
Rsync 核心演算法原理與實戰指南:高效檔案同步的終極解析
Linux 驅動開發核心同步機制:等待隊列、睡眠/喚醒與互斥實戰模式
Linux 環境下備份策略全景:磁帶裝置存取、tar/cpio/dump 工具鏈、完全/增量/差分備份模式、自動化腳本與驗證機制
Linux net_device 結構體深度解析:核心字段佈局與功能語義
CentOS 6 快速構建企業級 Web 代理伺服器:Squid + ClamAV + ICAP 完整部署指南
CentOS 虛擬機網卡名稱 eth1 改為 eth0 完整修改方法