Unix/Linux 傳統並發模型使用 fork() 為每個客戶端創建子進程。但子進程退出後若父進程未回收,將變為「殭屍進程」佔用 PID 與進程表項。本文展示正確使用 waitpid() + sigaction() 實現非阻塞回收。
WNOHANG 確保無退出子進程時立即返回 0,不卡住主循環。SIGCHLD 在子進程停止/退出時發送給父進程。accept() 等返回 EINTR 錯誤。
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
#define SERVPORT "2349"
// 信號處理函數:非阻塞循環回收所有殭屍子進程
void sigchild_handler(int signo)
{
while (waitpid(-1, NULL, WNOHANG) > 0);
}
int main(int argc, char *argv[])
{
struct addrinfo hints, *res;
int status, sockfd, connFd;
struct sockaddr_storage clientAddr; // 支援 IPv4/IPv6
struct sigaction sa;
char *msg = "The server simply displays a message!";
if (argc != 2) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; }
// 1. getaddrinfo 解析地址 (AI_PASSIVE 綁定所有介面)
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC; // IPv4/IPv6 雙棧
hints.ai_socktype = SOCK_STREAM; // TCP
hints.ai_flags = AI_PASSIVE; // 綁定通配地址
status = getaddrinfo(NULL, SERVPORT, &hints, &res);
if (status != 0) { fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(status)); return 2; }
// 2. 建立 Socket、綁定、監聽
sockfd = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
bind(sockfd, res->ai_addr, res->ai_addrlen);
listen(sockfd, 5);
printf("======== Please Wait Client =========\n");
// 3. 設置 SIGCHLD 信號處理
sa.sa_handler = sigchild_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) { perror("sigaction"); exit(3); }
// 4. 主循環:Accept -> Fork -> 處理
while (1)
{
socklen_t len = sizeof(clientAddr);
connFd = accept(sockfd, (struct sockaddr *)&clientAddr, &len);
// 顯示客戶端 IP (僅演示 IPv4)
char ipstr[INET_ADDRSTRLEN];
struct sockaddr_in *ipv4 = (struct sockaddr_in *)&clientAddr;
inet_ntop(AF_INET, &(ipv4->sin_addr), ipstr, sizeof(ipstr));
printf("client: %s\n", ipstr);
if (fork() == 0) // 子進程
{
close(sockfd); // 子進程不需要監聽套接字
send(connFd, msg, strlen(msg), 0);
close(connFd);
exit(0);
}
close(connFd); // 父進程關閉已連接套接字,繼續監聽
}
close(sockfd);
return 0;
}
AF_UNSPEC + sockaddr_storage 同時兼容 IPv4/IPv6。sigchild_handler 僅調用 waitpid 循環,避免在信號上下文執行非異步安全函數 (如 printf, malloc)。fork() 複製描述符表,**子進程必須關閉 sockfd,父進程必須關閉 connFd**,否則引用計數不歸零導致連接無法正常斷開或監聽套接字洩漏。accept() 被信號中斷返回 -1/EINTR,需手動重試;有此標誌內核自動重啟,代碼更簡潔健壯。這是經典的「每進程一連接」並發模型。雖 fork() 開銷大於執行緒,但進程隔離性強、崩潰不影響主進程、無鎖競爭,適合連接數較少、邏輯複雜、穩定性要求高的場景。現代高併發常用 epoll/IOCP + 線程池,但理解 fork 模型是網絡編程基石。