首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> 關於C

網絡編程實戰:基於 fork 的並發 Server 端實現與殭屍進程處理

     編輯:關於C

背景與挑戰

Unix/Linux 傳統並發模型使用 fork() 為每個客戶端創建子進程。但子進程退出後若父進程未回收,將變為「殭屍進程」佔用 PID 與進程表項。本文展示正確使用 waitpid() + sigaction() 實現非阻塞回收。

關鍵系統調用

  • waitpid(-1, NULL, WNOHANG):非阻塞回收任意已退出子進程。WNOHANG 確保無退出子進程時立即返回 0,不卡住主循環。
  • sigaction(SIGCHLD, &sa, NULL):註冊信號處理函數。SIGCHLD 在子進程停止/退出時發送給父進程。
  • SA_RESTART:系統調用被信號中斷後自動重啟,避免 accept() 等返回 EINTR 錯誤。

完整 Server 代碼


#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;
}

關鍵設計細節解析

  1. 雙棧支援:AF_UNSPEC + sockaddr_storage 同時兼容 IPv4/IPv6。
  2. 信號處理器極簡原則:sigchild_handler 僅調用 waitpid 循環,避免在信號上下文執行非異步安全函數 (如 printf, malloc)。
  3. 文件描述符分離:fork() 複製描述符表,**子進程必須關閉 sockfd,父進程必須關閉 connFd**,否則引用計數不歸零導致連接無法正常斷開或監聽套接字洩漏。
  4. SA_RESTART 重要性:無此標誌時,accept() 被信號中斷返回 -1/EINTR,需手動重試;有此標誌內核自動重啟,代碼更簡潔健壯。

總結

這是經典的「每進程一連接」並發模型。雖 fork() 開銷大於執行緒,但進程隔離性強、崩潰不影響主進程、無鎖競爭,適合連接數較少、邏輯複雜、穩定性要求高的場景。現代高併發常用 epoll/IOCP + 線程池,但理解 fork 模型是網絡編程基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved