深入拆解 Linux Socket 五大 I/O 模型:从底层机制到性能适配

深入拆解 Linux Socket 五大 I/O 模型

引言:I/O 模型对 Linux 网络服务性能的核心影响

在 Linux 网络编程中,I/O 模型的宏观意义是 "数据传输的调度策略",直接决定服务在高并发场景(如 MMO 游戏多玩家实时走位同步、跨服战场 1000 人同步战斗指令、游戏内限时活动道具领取、玩家组队副本的实时数据交互)下的稳定性与运行效率。若模型选择与业务场景不匹配,易引发三类典型问题:

  • 进程阻塞导致响应超时:大量玩家操作指令因进程等待数据排队,出现角色走位卡顿、技能释放延迟;
  • CPU 空闲却无法处理新请求:进程空等玩家数据时释放 CPU,但模型不支持多任务并行,导致部分玩家请求被 "遗漏",出现 "操作无响应";
  • 资源过载崩溃:内存被空等进程占用、CPU 被无效轮询消耗,最终导致游戏服务器宕机,玩家集体掉线。

需先明确:所有 Socket I/O 操作均包含 "等待数据就绪 "(内核等待数据满足传输条件)与"数据拷贝"(内核将数据在用户 / 内核空间移动)两个核心阶段,I/O 模型的调度策略正是围绕这两个阶段设计,通过两大维度发挥作用:

  • 进程等待数据的方式:决定进程在 "等待数据就绪" 阶段是否需暂停(阻塞),或可并行处理其他任务(如同时处理多玩家的走位与技能指令),直接影响 CPU 利用率;
  • 数据拷贝的效率:决定 "数据拷贝" 阶段的资源消耗(如 CPU 占用、耗时),直接影响吞吐量与响应延迟(如跨服战斗中指令同步的实时性)。

基于这两个维度,I/O 模型进一步关联三项关键性能指标:

  • 吞吐量:单位时间内可处理的请求数,高效模型(如 I/O 多路复用、异步 I/O)可支持万级甚至十万级并发(如同时承载 5000 人在线的游戏服务器);
  • 响应延迟:从请求发起至结果返回的总时间,核心优化 "等待数据就绪" 阶段可显著降低延迟(如玩家释放技能后,0.1 秒内同步到队友客户端);
  • 资源利用率:避免 "进程空等占 CPU""内存缓冲区闲置",提升 CPU 算力、网络带宽、内存的实际使用效率(如减少服务器资源浪费,支持更多玩家同时在线)。

什么是 IO 模型?:从宏观策略到协作机制

IO 模型的本质,是 Linux 内核为用户进程提供的「IO 协作机制」,也是 "数据传输调度策略" 的底层实现 ------ 调度策略的性能影响,本质源于协作机制对 "两个核心阶段" 的规则设计。

模型的核心规则维度:协作机制的具体体现

IO 模型通过三个关键规则维度落地协作机制,且直接对应调度策略的两大核心维度(围绕 "两个核心阶段" 展开):

  • 通知规则:用户进程告知内核 "需要执行 IO 操作" 的方式(如 read/write 系统调用、信号注册),是 "数据传输调度" 的起点,决定数据交互的触发效率(如玩家走位指令的快速接收);
  • 反馈规则:内核在 "两个核心阶段"(等待数据就绪、数据拷贝)向用户进程反馈状态的方式(如返回结果码、触发信号),可优化 "进程等待调度",减少进程无效轮询(如避免服务器反复查询 "是否有新的玩家技能指令");
  • 阻塞规则:"两个核心阶段" 中,用户进程是否暂停执行(阻塞)并释放 CPU,是 "进程等待调度" 的核心,直接影响 CPU 利用率(如阻塞时可腾出 CPU 处理其他玩家的组队请求)。

Linux 经典 IO 模型分类:协作机制的不同实现

Linux 系统中,遵循 POSIX 标准的经典 IO 模型共 5 种,是协作机制的不同实现版本,核心差异体现在对 "两个核心阶段" 的规则设计(直接决定调度策略的性能表现):

  • 阻塞 IO(Blocking IO) :两个阶段均阻塞,通知规则为 "系统调用触发",反馈规则为 "拷贝完成后返回结果";
  • 非阻塞 IO(Non-Blocking IO) :等待数据就绪阶段不阻塞、数据拷贝阶段阻塞,通知规则为 "轮询式系统调用",反馈规则为 "未就绪时返回错误码";
  • IO 多路复用(IO Multiplexing) :两个阶段均阻塞(通过单进程监听多 IO 事件优化并发),通知规则为 "注册 IO 事件",反馈规则为 "有事件就绪时返回";
  • 信号驱动 IO(Signal-Driven IO) :等待数据就绪阶段不阻塞、数据拷贝阶段阻塞,通知规则为 "注册信号",反馈规则为 "就绪时触发信号";
  • 异步 IO(Asynchronous IO) :两个阶段均不阻塞,通知规则为 "异步系统调用",反馈规则为 "拷贝完成后触发信号"。

规则差异与性能的关联:回归宏观调度策略

五种模型的规则差异,本质是对 "两个核心阶段" 的处理方式不同,直接决定调度策略的性能表现与适用场景:

  • 阻塞 IO 因 "两个阶段均阻塞",调度效率低,仅适用于低并发、对延迟不敏感的场景(如单人离线游戏的本地数据存档读取);
  • 非阻塞 IO 因 "等待阶段不阻塞但需轮询",轮询会持续占用 CPU 资源(无数据时仍需频繁调用系统调用),仅适用于 "数据就绪频率高、单次数据量小" 的场景(如游戏中玩家高频微操作指令监听,开发中较少单独使用,需搭配线程池缓解 CPU 消耗);
  • IO 多路复用因 "单进程监听多 IO 事件",并发调度能力强,无需为每个请求创建进程 / 线程,适用于高并发服务(如支持 5000 人同时在线的 MMO 游戏服务器);
  • 信号驱动 IO 因 "等待阶段靠信号通知",虽避免轮询,但存在 "信号风暴" 风险(大量数据同时就绪时高频信号打断进程)、信号丢失 / 重入问题,处理逻辑复杂,仅适用于低并发、低延迟的小众场景(如游戏 GM 后台实时指令响应,开发中极少使用);
  • 异步 IO 因 "两个阶段均不阻塞",内核完成拷贝后再通知进程,资源利用率最高,适用于对延迟敏感、需极致性能的场景(如跨服 1000 人实时战斗的指令同步服务器)。

前置知识:I/O 的本质与核心阶段

前文提到的 "IO 模型协作机制",其底层逻辑依赖于 I/O 操作的本质(数据在用户空间与内核空间的交互),以及 "两个核心阶段" 的具体流程。理解这部分基础原理,才能更清晰地掌握协作机制中 "通知、反馈、阻塞" 规则的设计依据。

I/O 的本质:用户空间与内核空间的数据交互

Socket 通信的所有 I/O 操作,本质是数据在 "用户空间"(用户进程的专属内存)与 "内核空间"(操作系统内核的管理内存)之间的定向传输------ 这一过程必须由内核主导,应用程序无法直接操作硬件,只能通过系统调用间接触发。这也是 IO 模型协作机制的设计基础:所有 "通知、反馈" 规则,都需围绕 "数据跨空间传输" 的需求展开。

用户空间与内核空间的隔离:内存划分的底层逻辑

现代操作系统通过 "虚拟内存" 机制,将物理内存划分为两个独立区域,既保障系统稳定(避免应用程序破坏内核资源),也为数据传输提供安全边界,同时决定了 "任何 I/O 操作必须经内核中转":

  • 用户空间(User Space) :应用程序(如游戏服务器进程)的运行区域,权限低,无法直接访问网卡、磁盘等硬件。例如游戏服务器需读取玩家走位指令时,只能通过 read 系统调用请求内核,等待内核将数据拷贝到用户空间的 "用户缓冲区";
  • 内核空间(Kernel Space) :操作系统内核的运行区域,权限高,可直接操作硬件。例如玩家客户端发送技能指令时,内核先通过网卡驱动接收数据,暂存到内核空间的 "内核缓冲区",再根据协作机制的规则,决定何时将数据拷贝到用户空间。
数据跨空间移动:I/O 操作的核心流程

所有 I/O 操作(读玩家指令、写战斗日志、接收组队请求)的核心,都是 "数据在两个空间的定向移动",而这一过程必须通过 "等待数据就绪"、"数据拷贝" 两个阶段完成 ------ 这也是 IO 模型协作机制中 "阻塞、反馈" 规则的直接作用对象:

  • 读操作流程:内核先等待网卡接收玩家指令(如技能 ID、走位坐标),待数据存入 "内核缓冲区"(等待数据就绪阶段);再将数据从内核缓冲区拷贝到用户缓冲区(数据拷贝阶段),游戏服务器进程才能读取并处理;
  • 写操作流程:游戏服务器先将处理结果(如战斗伤害值、道具发放通知)写入 "用户缓冲区";内核等待 "内核缓冲区" 有空闲空间(等待数据就绪阶段),再将数据拷贝到内核缓冲区(数据拷贝阶段),最终通过网卡发送至玩家客户端。

I/O 操作的两个核心阶段:底层流程拆解

"等待数据就绪" 与 "数据拷贝" 是所有 I/O 操作的必经阶段,也是 IO 模型协作机制的核心作用对象 ------ 不同模型的规则差异,本质是对这两个阶段的处理方式不同:

"等待数据就绪" 阶段:I/O 操作的前置条件

此阶段由内核独立完成,核心是 "等待数据满足传输条件",进程需等待该阶段完成后,才能进入 "数据拷贝" 阶段,是 I/O 延迟的主要来源:

  • 读操作场景:内核等待网卡接收玩家数据(如等待玩家释放技能的指令包传输完成),或等待本地文件数据从磁盘读取到内核缓冲区;
  • 写操作场景:内核等待 "内核缓冲区" 释放空闲空间(如前一次发送的玩家状态数据已通过网卡传输,缓冲区腾出空间),或等待磁盘准备好接收写入数据。

以游戏服务器为例:当 1000 名玩家同时在跨服战场释放技能时,内核需逐个等待每个玩家的技能指令包接收完成(等待数据就绪阶段),再根据协作机制的规则,决定是否让服务器进程阻塞等待拷贝。

"数据拷贝" 阶段:资源占用的关键环节

数据就绪后,内核主导数据在两个空间的拷贝,此阶段会占用 CPU 资源(CPU 需持续搬运内存数据),是大数据量场景的性能瓶颈:

  • 读操作场景:内核将 "内核缓冲区" 中的玩家指令数据(如走位坐标)拷贝到 "用户缓冲区",游戏服务器进程可据此更新玩家在地图中的位置、判定技能命中效果;
  • 写操作场景:内核将 "用户缓冲区" 中的服务器处理结果(如玩家经验值增加、战斗胜利通知)拷贝到 "内核缓冲区",再通过网卡发送至玩家客户端,完成画面与状态同步。

例如游戏内限时活动开启时,大量玩家同时领取道具,服务器需向每个玩家发送 "道具到账" 通知 ------ 此时 "数据拷贝" 阶段的 CPU 占用会显著上升,若 IO 模型的协作机制未优化拷贝效率,可能导致通知发送延迟,出现玩家 "领取成功但道具未到账" 的视觉偏差。

Linux Socket 五种 I/O 模型:回应反射式服务器极简实现

以下将以回声服务器(客户端发送数据,服务器原封不动返回) 为案例,逐一讲解各模型的实现逻辑,配套完整可运行代码,帮助理解 Linux 网络编程的核心思路。

通用客户端代码:所有服务器模型通用

以下客户端可与所有服务器模型通信,用于测试服务器功能(为确保代码简洁易懂,仅保留逻辑核心实现,以下同):

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define SERVER_IP "127.0.0.1"
#define PORT 5150
#define BUF_SIZE 1024  // 简化缓冲区大小

int main() {
    int client_fd;
    struct sockaddr_in server_addr;
    char buf[BUF_SIZE];
    int ret;

    // 1. 创建TCP套接字
    client_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (client_fd < 0) { perror("socket fail"); return -1; }

    // 2. 初始化服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    inet_pton(AF_INET, SERVER_IP, &server_addr.sin_addr);

    // 3. 连接服务器
    if (connect(client_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("connect fail"); close(client_fd); return -1;
    }
    printf("已连接服务器,输入数据发送(输入'quit'退出):\n");

    // 4. 发送-接收循环
    while (1) {
        printf("客户端发送:");
        fgets(buf, BUF_SIZE, stdin);
        // 去掉换行符
        int len = strlen(buf);
        if (buf[len-1] == '\n') buf[--len] = '\0';
        if (strcmp(buf, "quit") == 0) break;

        // 发送数据
        ret = send(client_fd, buf, len, 0);
        if (ret <= 0) { perror("send fail"); break; }
        printf("发送字节数:%d\n", ret);

        // 接收服务器返回(反射数据)
        memset(buf, 0, BUF_SIZE);
        ret = recv(client_fd, buf, BUF_SIZE, 0);
        if (ret <= 0) { perror("recv fail"); break; }
        printf("服务器返回:%s(接收字节数:%d)\n\n", buf, ret);
    }

    // 5. 清理
    close(client_fd);
    printf("已断开连接\n");
    return 0;
}

编译运行gcc -o client client.c && ./client

客户端的逻辑处理并不复杂,创建套接字,连接服务器,然后便是不停地收发数据。

阻塞 I/O 模型:最简单的模型

阻塞 I/O 是 Linux Socket I/O 中最直观的模型,每个 I/O 操作(连接建立、数据读写)会让进程 / 线程 "停等" 直到操作完成 ,期间不做任何其他任务。在阻塞 I/O 模型服务器中,典型逻辑是 "主线程阻塞等连接,新线程 / 进程阻塞处理单个客户端",整体流程简单且易理解。仅适合 "并发连接少、对性能要求不高" 的场景(如单机工具、小流量服务)

核心原理:锚定 I/O 的两个核心阶段:阻塞 I/O 的 "阻塞" 特性贯穿 I/O 操作的全流程,且直接对应 I/O 的两个核心阶段,这是理解其本质的关键:

连接建立阶段 :主线程调用 accept() 后进入阻塞态,直到有客户端发起 TCP 连接(三次握手完成),函数才返回新的客户端套接字(client_fd);

数据读写阶段 :处理客户端的线程 / 进程调用 recv()/send() 后同样阻塞:

  • 调用 recv() 时,阻塞等待 "内核空间有数据可读"(等待数据就绪阶段),数据就绪后再阻塞等待 "数据从内核空间拷贝到用户空间"(数据拷贝阶段),两阶段完成后才返回读取字节数;
  • 调用 send() 时,阻塞等待 "内核空间有空闲空间"(等待数据就绪阶段),空间就绪后再阻塞等待 "数据从用户空间拷贝到内核空间"(数据拷贝阶段),拷贝完成后才返回发送字节数。

需注意:阻塞时进程 / 线程会主动释放 CPU 资源(进入内核的 "等待队列"),而非占用 CPU 空等;直到 I/O 操作完成,内核才会唤醒进程 / 线程,让其重新抢占 CPU 继续执行。

c 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <arpa/inet.h>

#define PORT 5150
#define BUF_SIZE 1024

// 处理单个客户端连接(修改参数类型为void*)
void *handle_client(void *arg) {
    int client_fd = *(int*)arg;  // 转换参数类型
    free(arg);  // 释放动态分配的内存
    char buf[BUF_SIZE];
    int ret;
    
    while (1) {
        // 阻塞等待接收数据
        memset(buf, 0, BUF_SIZE);
        ret = recv(client_fd, buf, BUF_SIZE, 0);
        if (ret <= 0) {
            printf("客户端断开连接\n");
            break;
        }
        
        printf("接收:%s(%d字节)\n", buf, ret);
        
        // 阻塞发送数据
        ret = send(client_fd, buf, ret, 0);
        if (ret <= 0) {
            perror("send fail");
            break;
        }
    }
    
    close(client_fd);
    return NULL;  // 线程函数需要返回值
}

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pthread_t tid;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket fail"); return -1; }

    // 端口复用+绑定
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind fail"); close(listen_fd); return -1;
    }

    // 监听
    listen(listen_fd, 5);
    printf("阻塞IO服务器启动:端口=%d\n", PORT);

    // 循环接受客户端连接
    while (1) {
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_fd < 0) { perror("accept fail"); continue; }

        // 打印客户端信息
        char client_ip[INET_ADDRSTRLEN];
        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
        printf("新客户端连接:%s:%d\n", client_ip, ntohs(client_addr.sin_port));

        // 创建线程处理客户端(使用动态分配传递参数)
        int *client_fd_ptr = malloc(sizeof(int));
        *client_fd_ptr = client_fd;
        if (pthread_create(&tid, NULL, handle_client, client_fd_ptr) != 0) {
            perror("pthread_create fail");
            close(client_fd);
            free(client_fd_ptr);  // 失败时释放内存
        }
        pthread_detach(tid);
    }

    close(listen_fd);
    return 0;
}

编译运行gcc -o server_block server_block.c -lpthread && ./server_block

非阻塞 IO(Non-Blocking IO)模型:轮询式处理

非阻塞 IO 是为解决 "阻塞 IO 停等浪费" 问题设计的模型,通过主动设置套接字为 "非阻塞模式",让所有 I/O 操作(连接建立、数据读写)不再 "卡着等结果",而是无论能否完成都立即返回。若操作暂时无法执行(如无数据可读、无连接可接),会返回特定错误码(EAGAIN/EWOULDBLOCK),由程序通过 "定期轮询" 重试检查状态;期间可处理其他任务,无需空等。适配 "对响应时间敏感、需在等待 I/O 时并行处理其他逻辑" 的场景(如实时监控、高频交互工具),但轮询会额外消耗 CPU 资源。

核心原理:用 "立即返回 + 轮询" 替代 "阻塞等待"

非阻塞模式开启 :通过 fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) 给套接字(Socket)添加 "非阻塞标志(O_NONBLOCK)",此后,该套接字的所有 I/O 操作(accept/recv/send)都会遵循非阻塞规则,是核心前提;

连接建立阶段 :主线程调用 accept() 后不再阻塞等待,若暂无客户端连接,accept() 不阻塞,立即返回 -1errno 设为 EAGAINEWOULDBLOCK(意为 "当前无连接可接,稍后再试"),否则正常执行后续处理,此外程序需 "轮询" 重试(如循环调用 accept(),或搭配定时器间隔重试),以至获取到有效 client_fd

数据读写阶段 :处理客户端的线程 / 进程调用 recv()/send() 后同样立即返回,仅在 "数据拷贝阶段" 短暂阻塞:

  • 调用 recv() 时,等待数据就绪阶段(非阻塞),若内核空间无数据可读,recv() 立即返回 -1errno 设为 EAGAIN/EWOULDBLOCK;若有数据可读,进入数据拷贝阶段(阻塞),阻塞等待 "数据从内核空间拷贝到用户空间"(内存到内存操作,微秒级,耗时极短),拷贝完成后返回读取的字节数;
  • 调用 send() 时,等待空间就绪阶段(非阻塞),若内核发送缓冲区已满(无空闲空间),send() 立即返回 -1errno 设为 EAGAIN/EWOULDBLOCK;若有空闲空间,进入数据拷贝阶段(阻塞),阻塞等待 "数据从用户缓冲区拷贝到内核发送缓冲区",拷贝完成后返回发送的字节数。

需注意关键差异(对比阻塞 IO):非阻塞 IO 的 "轮询" 是主动行为,程序需反复调用 I/O 函数检查状态(即使无操作可执行),会持续占用 CPU 资源(阻塞 IO 阻塞时会释放 CPU,进入内核等待队列),需设计合理的轮询间隔;此外阻塞 IO 由内核唤醒进程 / 线程,非阻塞 IO 需程序自己处理 "EAGAIN/EWOULDBLOCK" 错误。

scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <stdlib.h>
#include <arpa/inet.h> 

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 10  // 最大客户端数量

// 设置套接字为非阻塞模式
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

// 处理单个客户端(非阻塞IO轮询)
void *handle_client(void *arg) {
    int client_fd = *(int*)arg;
    free(arg);
    char buf[BUF_SIZE];
    int ret;
    
    // 设置客户端套接字为非阻塞
    set_nonblocking(client_fd);
    
    printf("开始处理客户端 FD=%d\n", client_fd);
    
    while (1) {
        // 非阻塞接收数据(等待数据阶段不阻塞)
        memset(buf, 0, BUF_SIZE);
        ret = recv(client_fd, buf, BUF_SIZE, 0);
        
        if (ret > 0) {
            // 成功接收数据
            printf("FD=%d 接收:%s(%d字节)\n", client_fd, buf, ret);
            
            // 发送数据(数据拷贝阶段仍阻塞)
            send(client_fd, buf, ret, 0);
        }
        else if (ret == 0) {
            // 客户端断开连接
            printf("FD=%d 客户端断开连接\n", client_fd);
            break;
        }
        else {
            // 非阻塞错误处理
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("recv error");
                break;
            }
            // 没有数据,短暂休眠后继续轮询
            usleep(10000);  // 10ms
        }
    }
    
    close(client_fd);
    return NULL;
}

int main() {
    int listen_fd, client_fd, *client_fd_ptr;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    pthread_t tid;

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket fail"); return -1; }

    // 设置监听套接字为非阻塞
    set_nonblocking(listen_fd);

    // 端口复用+绑定
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind fail"); close(listen_fd); return -1;
    }

    // 监听
    listen(listen_fd, 5);
    printf("非阻塞IO服务器启动:端口=%d\n", PORT);

    // 循环接受客户端连接
    while (1) {
        // 非阻塞接受连接
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        
        if (client_fd > 0) {
            // 成功接受连接
            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
            printf("新客户端连接:%s:%d(FD=%d)\n", 
                   client_ip, ntohs(client_addr.sin_port), client_fd);
            
            // 创建线程处理客户端
            client_fd_ptr = malloc(sizeof(int));
            *client_fd_ptr = client_fd;
            if (pthread_create(&tid, NULL, handle_client, client_fd_ptr) != 0) {
                perror("pthread_create fail");
                close(client_fd);
                free(client_fd_ptr);
            }
            pthread_detach(tid);
        }
        else {
            // 非阻塞错误处理
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("accept error");
            }
            // 短暂休眠减少CPU占用
            usleep(10000);  // 10ms
        }
    }

    close(listen_fd);
    return 0;
}

编译运行gcc -o server_nonblock server_nonblock.c -lpthread && ./server_nonblock

IO 多路复用(IO Multiplexing)模型:单线程管理多连接的 "高效模型"

I/O 多路复用的核心是用一个进程 / 线程 "同时监听多个套接字" ,内核在多个套接字中 "等待数据就绪",避免阻塞 I/O 的线程膨胀和非阻塞 I/O 的轮询浪费,是高并发服务的主流选择。Linux 提供 selectpollepoll 三种实现,核心差异在于 "套接字管理方式" 和 "效率"。

select 实现:基于 "位集合" 的监听

select 是最早的 I/O 多路复用接口,核心是用 "二进制位集合"(fd_set)管理待监听的套接字,本质是 "让内核一次性检查多个套接字,有就绪的再通知用户进程"。适配并发连接少或需跨平台的场景。

核心原理:"位集合标记监听 + 内核批量检查 + 用户轮询确认" 的三步流程

管理方式:用 "位集合" 当 "监听清单"

使用 fd_set 结构体存储待监听的文件描述符(Socket FD),每个二进制位索引对应一个FD(如第 4 位对应 FD=4),通过 "位的 0/1 状态" 标记 FD 是否需要监听对应事件。

其中,select 依赖的三个独立的 fd_set 位集合,分别对应的 "读、写、异常" 三类事件需明确:

  • 读事件集合(readfds) :监听 FD 的 "数据可读" 场景(如客户端发送数据、监听套接字有新连接请求、FD 接收缓冲区残留数据);
  • 写事件集合(writefds) :监听 FD 的 "数据可写" 场景(如 FD 发送缓冲区有空闲空间,可写入数据发送给客户端);
  • 异常事件集合(exceptfds) :监听 FD 的 "异常状态" 场景(如客户端连接异常断开、FD 发生底层错误)。

例如,若需监听 "监听套接字(listen_fd)的新连接(读事件)" 和 "客户端套接字(client_fd)的异常",需将 listen_fd 加入读事件集合,client_fd 加入异常事件集合。

并且,其基于位集合的标准操作流程需依赖系统宏函数,确保监听状态准确:

  • 初始化:调用 FD_ZERO(fd_set *set) 分别清空读、写、异常三个 fd_set,将所有位重置为 0,避免旧监听状态干扰新轮次检查;
  • 加监听:调用 FD_SET(int fd, fd_set *set) 将目标 FD 加入对应事件集合,使该 FD 对应位设为 1。需根据事件类型选择集合,同一 FD 可加入多个集合(如同时监听读和异常);
  • 调用 select:传入 int nfds(待监听 FD 的最大值 + 1)、三个 fd_set 地址、timeout(超时时间),触发内核批量检查(将三个 fd_set 从用户空间完整拷贝到内核空间,遍历 0~nfds-1 范围的 FD,检查其在对应集合中的事件是否就绪,未就绪的 FD 则在其对应集合中的位设为 0,就绪则 FD 的位为 1,再将修改后的 fd_set 拷贝回用户空间,唤醒进程并返回 "就绪 FD 的总数";
  • 查结果:select 返回后,遍历 0~nfds-1 范围的 FD,调用 FD_ISSET(int fd, fd_set *set) 检查 FD 在对应集合中的位是否为 1(位值为 1 则表示该 FD 就绪,可执行后续操作)。

效率瓶颈:"拷贝 + 轮询" 导致性能随规模下降select 的效率瓶颈集中在两点:

  • "位集合"拷贝开销 :用户进程要把整个 fd_set 从用户空间拷贝到内核空间(因为内核要检查这些 FD),FD 越多,拷贝的数据量越大,开销越高;
  • 全量轮询确认开销 :内核仅反馈 "有 FD 就绪",但不明确具体 FD 列表。用户进程必须从 0 遍历到 "当前最大 FD",逐个用 FD_ISSET 检查,哪怕只有 1 个 FD 就绪,也要轮询所有位,时间复杂度为 O (n)。

硬限制:默认最多监听 1024 个 FDselect 的最大 FD 数量由系统宏 FD_SETSIZE 固定(默认 1024),且该值在编译时确定(在 <sys/select.h> 中)。若需突破限制(如监听 2048 个 FD),需重新编译内核修改宏值,鉴于该操作会影响系统稳定性,且不符合生产环境运维规范,因此 select 天然无法适配万级以上高并发场景。

scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/select.h>
#include <stdlib.h>
#include <arpa/inet.h> 

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_FD 1024  // select默认最大支持的文件描述符数量

// 暂存每个客户端的待发送数据(避免直接send阻塞)
char send_buf[MAX_FD][BUF_SIZE];  
// 记录每个客户端待发送数据的长度(0表示无待发数据)
int send_len[MAX_FD] = {0};       

int main() {
    int listen_fd, client_fd, max_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    // 读、写、异常三个事件集合
    fd_set read_fds, write_fds, except_fds;
    fd_set tmp_read, tmp_write, tmp_except;  // 临时集合(select会修改原集合)
    char recv_buf[BUF_SIZE];  // 接收数据缓冲区
    int ret, i;

    // 初始化所有事件集合(读、写、异常均需清空)
    FD_ZERO(&read_fds);
    FD_ZERO(&write_fds);
    FD_ZERO(&except_fds);

    // 创建监听套接字
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket fail"); return -1; }
    // 监听FD加入读集合(新连接)和异常集合(自身异常)
    FD_SET(listen_fd, &read_fds);
    FD_SET(listen_fd, &except_fds);
    max_fd = listen_fd;

    // 端口复用+绑定+监听
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, 5);
    printf("select服务器(IO多路复用)启动:端口=%d\n", PORT);

    // select事件循环(核心修改)
    while (1) {
        // 复制所有临时集合(读、写、异常均需备份)
        tmp_read = read_fds;
        tmp_write = write_fds;
        tmp_except = except_fds;
        
        // select同时监听三个事件
        ret = select(max_fd + 1, &tmp_read, &tmp_write, &tmp_except, NULL);
        if (ret < 0) { perror("select fail"); continue; }

        // 遍历所有FD,检查事件
        for (i = 0; i <= max_fd; i++) {
            if (!FD_ISSET(i, &tmp_read)) continue;  // 无读事件则跳过

            if (i == listen_fd) {
                // 监听FD有事件:新客户端连接
                client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
                if (client_fd < 0) { perror("accept fail"); continue; }
                if (client_fd >= MAX_FD) {  // 超过select最大FD限制
                    printf("客户端FD=%d 超出select最大限制(%d),拒绝连接\n", client_fd, MAX_FD);
                    close(client_fd);
                    continue;
                }
                
                // 客户端FD加入读集合(接收数据)和异常集合(捕获强制断连)
                FD_SET(client_fd, &read_fds);
                FD_SET(client_fd, &except_fds);
                send_len[client_fd] = 0;  // 初始化:新客户端无待发数据
                if (client_fd > max_fd) max_fd = client_fd;
                
                // 打印客户端信息
                char client_ip[INET_ADDRSTRLEN];
                inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
                printf("新客户端连接:%s:%d(FD=%d)\n", 
                       client_ip, ntohs(client_addr.sin_port), client_fd);
            } else {
                // 客户端FD有事件:接收数据
                memset(recv_buf, 0, BUF_SIZE);
                ret = recv(i, recv_buf, BUF_SIZE - 1, 0);  // 留1字节存字符串结束符
                if (ret <= 0) {
                    // 客户端正常断开(recv返回0),清理所有集合
                    printf("客户端FD=%d 正常断开\n", i);
                    FD_CLR(i, &read_fds);
                    FD_CLR(i, &write_fds);  // 从写集合移除
                    FD_CLR(i, &except_fds); // 从异常集合移除
                    send_len[i] = 0;        // 清空待发数据
                    close(i);
                    continue;
                }
                
                // 接收数据后暂存,将客户端FD加入写集合(等待发送缓冲区空闲)
                printf("FD=%d 接收:%s(%d字节)→ 暂存待发送\n", i, recv_buf, ret);
                strncpy(send_buf[i], recv_buf, BUF_SIZE - 1);  // 暂存数据
                send_len[i] = ret;                              // 记录数据长度
                FD_SET(i, &write_fds);                          // 加入写集合,等待发送
            }
        }

        // 处理写事件(发送暂存的数据,避免直接send阻塞)
        for (i = 0; i <= max_fd; i++) {
            // 仅处理"有写事件"且"有暂发数据"的客户端FD
            if (!FD_ISSET(i, &tmp_write) || send_len[i] <= 0) continue;

            // 发送暂存的数据
            ret = send(i, send_buf[i], send_len[i], 0);
            if (ret < 0) {
                // 发送失败(如客户端已断开),清理资源
                perror("send fail");
                FD_CLR(i, &read_fds);
                FD_CLR(i, &write_fds);
                FD_CLR(i, &except_fds);
                send_len[i] = 0;
                close(i);
            } else {
                // 发送成功:打印信息,从写集合移除(避免重复发送)
                printf("FD=%d 发送:%s(%d字节)→ 发送完成\n", i, send_buf[i], ret);
                send_len[i] = 0;        // 清空待发数据长度
                FD_CLR(i, &write_fds);  // 移除写集合,防止无数据时触发
            }
        }

        // 处理异常事件(捕获客户端强制断连,如直接关窗口)
        for (i = 0; i <= max_fd; i++) {
            // 跳过监听FD
            if (!FD_ISSET(i, &tmp_except) || i == listen_fd) continue;

            // 异常事件触发:客户端未正常挥手断开
            printf("客户端FD=%d 异常断开(如强制关窗口、网络断连)\n", i);
            // 清理所有集合和资源
            FD_CLR(i, &read_fds);
            FD_CLR(i, &write_fds);
            FD_CLR(i, &except_fds);
            send_len[i] = 0;
            close(i);

            // 优化:若异常FD是当前最大FD,更新max_fd(避免后续无效遍历)
            if (i == max_fd) {
                while (max_fd > 0 && !FD_ISSET(max_fd, &read_fds) && !FD_ISSET(max_fd, &write_fds)) {
                    max_fd--;
                }
            }
        }
    }

    close(listen_fd);
    return 0;
}

编译运行gcc -o server_select server_select.c && ./server_select

poll 实现:基于 "结构体数组" 的监听

poll 是对 select 的优化实现,核心通过 struct pollfd 结构体数组 管理待监听的套接字(FD),解决了 select 中 "位集合(fd_set)的 FD 数量硬限制" 问题,同时保留了 "内核批量检查 + 用户轮询确认" 的核心逻辑,适配中等规模并发场景(如数千连接)。

核心原理:"结构体数组管控 + 内核批量检查 + 用户轮询确认" 的优化流程

管理方式:用 struct pollfd 精准描述监听需求

poll 摒弃了 select 的位集合,改用动态数组存储监听信息,单个 struct pollfd 元素包含三部分核心字段,实现 "一对一" 的 FD 与监听规则映射:

arduino 复制代码
struct pollfd {
    int    fd;      // 待监听的文件描述符(Socket FD,-1 表示该元素无效)
    short  events;  // 期望监听的事件(如 POLLIN=读事件、POLLOUT=写事件,可组合)
    short  revents; // 实际发生的事件(内核填充,用于判断 FD 是否就绪)
};

其中,eventsrevents 支持的常见事件类型需明确:

  • POLLIN:读事件(如客户端发送数据到 Socket、监听套接字有新连接请求、Socket 接收缓冲区有残留数据);
  • POLLOUT:写事件(如 Socket 发送缓冲区有空闲空间,可写入数据发送给客户端);
  • POLLERR:异常事件(如客户端连接异常断开、Socket 发生错误);
  • POLLPRI:紧急数据事件(如 TCP 紧急指针指向的数据到达,较少见)。

例如,若需同时监听某个客户端 FD 的 "读事件" 和 "异常事件",只需设置 events = POLLIN | POLLERR,内核会同时检查这两类事件。

并且,其基于数组的标准操作流程更灵活,无需依赖宏函数,逻辑更直观:

  • 初始化:创建 struct pollfd 数组,按预期并发规模设置长度(如 struct pollfd fds[5000] 支持 5000 个 FD),若后续连接数超出初始长度,可通过 realloc 动态扩容;
  • 加监听:对需要监听的每个 FD,在数组中找到"无效位置"(fd=-1 的元素),赋值 fd (目标 FD)和 events (监听事件,如 POLLIN | POLLERR,监听读事件和异常事件),无效元素设 fd=-1 即可,内核会自动跳过该元素。
  • 调用 poll :传入数组地址、数组长度、超时时间,内核批量检查所有有效 FD 的事件状态(将struct pollfd数组从用户空间拷贝至内核空间,遍历fd≠-1的元素检查events是否就绪(就绪则写入revents,未就绪则revents为 0);待有 FD 就绪或超时,将数组拷贝回用户空间,唤醒进程并返回就绪 FD 数量);
  • 查结果:poll 返回后,遍历数组检查 revents,当 revents & POLLIN 为真时,表示该 FD 有读事件就绪(比如监听 FD 有新连接到来或客户端 FD 有数据可读取),可执行 accept () 或 recv () 操作;当 revents & POLLOUT为真时,表示该 FD 有写事件就绪(通常是发送缓冲区处于空闲状态),可执行 send () 操作;当 revents & POLLERR 为真时,表示该 FD 发生了异常,此时需要关闭该 FD 并将其标记为 fd=-1。

效率特性:突破硬限制,但仍存轮询开销 :poll 相比 select 的核心改进是 解决 FD 数量硬限制,但未完全消除效率瓶颈:

  • 突破数量约束 :数组大小仅受系统内存和 FD 总数(ulimit -n)限制,无需修改内核即可支持上万 FD 监听;
  • 减少冗余操作 :无需像 select 那样每次调用前重置监听集合(events 字段不被内核修改),也无需跟踪 "最大 FD"(poll 直接用数组长度确定检查范围);
  • 仍有轮询成本:内核仅返回 "就绪 FD 的数量",不明确具体 FD 列表,用户进程需遍历整个数组确认就绪 FD,时间复杂度仍为 O (n)(n = 数组长度);
  • 拷贝开销优化有限struct pollfd 数组仍需从用户态拷贝到内核态,数组越大(FD 越多),拷贝耗时越长。
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <poll.h>
#include <stdlib.h>
#include <arpa/inet.h> 

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CONN 2048  // 最大支持连接数

struct pollfd fds[MAX_CONN];  // pollfd数组
int fd_count = 0;             // 数组中有效FD数量
char send_buf[MAX_CONN][BUF_SIZE];  // 发送缓冲区(暂存待发送数据)
int send_len[MAX_CONN] = {0};       // 记录每个客户端的待发送数据长度

// 添加FD到pollfd数组
void add_fd(int fd) {
    if (fd_count >= MAX_CONN) {
        printf("连接数已满(%d/%d)\n", fd_count, MAX_CONN);
        close(fd);
        return;
    }
    fds[fd_count].fd = fd;
    fds[fd_count].events = POLLIN;  // 初始只关注读事件
    send_len[fd_count] = 0;         // 初始化发送长度为0
    fd_count++;
}

// 从pollfd数组移除FD
void remove_fd(int idx) {
    if (idx < 0 || idx >= fd_count) return;
    
    // 关闭文件描述符
    close(fds[idx].fd);
    
    // 用最后一个元素覆盖当前元素(减少数组移动开销)
    if (idx != fd_count - 1) {
        fds[idx] = fds[fd_count - 1];
        // 同步复制发送缓冲区和长度(保持数据一致性)
        memcpy(send_buf[idx], send_buf[fd_count - 1], BUF_SIZE);
        send_len[idx] = send_len[fd_count - 1];
    }
    
    fd_count--;  // 有效数量减1
}

int main() {
    int listen_fd, client_fd, ret, i;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char recv_buf[BUF_SIZE];  // 接收缓冲区

    // 初始化pollfd数组(先加入监听FD)
    listen_fd = socket(AF_INET, SOCK_STREAM, 0);
    if (listen_fd < 0) { perror("socket fail"); return -1; }
    add_fd(listen_fd);

    // 端口复用+绑定+监听
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind fail"); return -1;
    }
    listen(listen_fd, 5);
    printf("poll服务器(IO多路复用)启动:端口=%d,最大连接=%d\n", PORT, MAX_CONN);

    // poll事件循环(核心逻辑)
    while (1) {
        // 阻塞等待事件(无超时,-1表示无限等待)
        ret = poll(fds, fd_count, -1);
        if (ret < 0) { perror("poll fail"); continue; }

        // 遍历pollfd数组,处理就绪事件
        for (i = 0; i < fd_count; i++) {
            // 处理读事件(新连接或客户端发数据)
            if (fds[i].revents & POLLIN) {
                if (fds[i].fd == listen_fd) {
                    // 监听FD有读事件:新客户端连接
                    client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
                    if (client_fd < 0) { perror("accept fail"); continue; }
                    add_fd(client_fd);  // 将新客户端FD加入数组
                    
                    // 打印客户端信息
                    char client_ip[INET_ADDRSTRLEN];
                    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
                    printf("新客户端连接:%s:%d(FD=%d)\n", 
                           client_ip, ntohs(client_addr.sin_port), client_fd);
                } else {
                    // 客户端FD有读事件:接收数据
                    memset(recv_buf, 0, BUF_SIZE);
                    ret = recv(fds[i].fd, recv_buf, BUF_SIZE - 1, 0);  // 留1字节存结束符
                    if (ret <= 0) {
                        // 客户端断开(正常断开ret=0,异常ret<0)
                        printf("客户端FD=%d 断开连接\n", fds[i].fd);
                        remove_fd(i);
                        i--;  // 移除后需回退索引(避免跳过下一个元素)
                        continue;
                    }
                    
                    // 接收数据后暂存,注册写事件(等待发送缓冲区空闲)
                    printf("FD=%d 接收:%s(%d字节)→ 暂存待发送\n", fds[i].fd, recv_buf, ret);
                    strncpy(send_buf[i], recv_buf, BUF_SIZE - 1);  // 暂存数据到发送缓冲区
                    send_len[i] = ret;                              // 记录数据长度
                    fds[i].events |= POLLOUT;                       // 添加写事件监听
                }
            }
            
            // 处理写事件(发送缓冲区空闲,可发送数据)
            if (fds[i].revents & POLLOUT) {
                if (send_len[i] > 0) {
                    // 发送暂存的数据
                    ret = send(fds[i].fd, send_buf[i], send_len[i], 0);
                    if (ret < 0) {
                        // 发送失败(如客户端已断开)
                        perror("send fail");
                        remove_fd(i);
                        i--;  // 移除后回退索引
                    } else {
                        // 发送成功:清理缓冲区,移除写事件监听
                        printf("FD=%d 发送:%s(%d字节)→ 发送完成\n", fds[i].fd, send_buf[i], ret);
                        send_len[i] = 0;                          // 清空待发送长度
                        fds[i].events &= ~POLLOUT;                // 移除写事件(避免重复触发)
                    }
                } else {
                    // 无待发送数据时,主动移除写事件监听
                    fds[i].events &= ~POLLOUT;
                }
            }
            
            // 处理异常事件(如客户端强制断连、套接字错误)
            if (fds[i].revents & (POLLERR | POLLHUP | POLLNVAL)) {
                printf("客户端FD=%d 异常断开(如强制关闭、网络中断)\n", fds[i].fd);
                remove_fd(i);
                i--;  // 移除后回退索引
            }
        }
    }

    close(listen_fd);
    return 0;
}

编译运行gcc -o server_poll server_poll.c && ./server_poll

epoll 实现:基于 "内核事件表" 的高效监听(含 LT/ET 两种触发模式)

epoll 是 Linux 内核专为高并发场景设计的 IO 多路复用机制,核心优势是 "高效管理大量连接" ,相比 select/poll 的全量遍历,epoll 通过内核维护的两个关键数据结构,将 IO 事件检测效率从 O (n) 降至 O (1),是万级以上并发服务的首选方案。

epoll 高效的核心:内核级数据结构设计:epoll 高效处理大量连接的本质是内核封装了两个各司其职的数据结构,由此替代 select/poll 中 "用户态维护 FD 集合" 的低效方式:

  • 红黑树(内核事件表) :用于存储用户主动注册的 "待监听 FD 及对应事件"(如读事件、写事件)。红黑树的特性确保 FD 的添加、删除、查询操作均为 O (log n) 高效执行 ,解决select/poll中 "修改 FD 集合需全量拷贝" 的开销。
  • 就绪链表 :仅存储 "已就绪的 FD"(如 FD 有数据可读、发送缓冲区空闲)。当 FD 状态从 "非就绪" 变为 "就绪" 时,内核会主动将其移入就绪链表;epoll_wait 调用可直接从链表中读取就绪 FD,无需轮询所有 FD,达到"按需返回" 的 O (1) 效率。

epoll 的两种触发模式:核心差异在 "事件通知时机" :epoll 的灵活性体现在支持两种事件触发模式,二者的核心区别是 "内核何时向用户进程通知 FD 就绪",直接决定了数据处理逻辑的复杂度、容错性和性能,需根据业务场景选择。

  • 水平触发(Level Triggered, LT) :LT 是 epoll 的默认工作模式,只要 FD 处于 "就绪状态" (如可读、可写)时,epoll就会持续向用户进程通知该事件,直到 FD 就绪状态消失 。其特点是无需特殊配置(注册事件时不设置 EPOLLET 标志);编码时无需严格保证 "一次性处理完所有数据",即使"数据未处理完",epoll 会反复通知,直到数据处理完成;支持阻塞或非阻塞 IO,适配大多数通用场景。
  • 边缘触发(Edge Triggered, ET) : ET 模式是 epoll 的高性能模式,仅在 FD 的状态从 "非就绪" 变为 "就绪" 的 "瞬间" 触发一次事件 ,即使 FD 仍有未处理的数据 / 空间,后续也不会再通知,必须依赖开发者手动确保 "一次性处理完所有就绪数据"。其特点是需通过 EPOLL_IN | EPOLLET 标志启用;由于事件仅通知一次,必须用非阻塞 IO 循环读取 / 写入数据(直到 recv/send 返回 EAGAIN/EWOULDBLOCK,表示 "当前无更多数据 / 空间"),避免阻塞;同一 FD 的同一事件仅触发一次,大幅减少内核空间与用户空间的交互开销,必须循环读写直到返回特定错误码,否则会导致数据丢失,适合对性能要求极高的场景。
水平触发(Level Triggered, LT)
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_EVENTS 1024
#define MAX_CLIENTS 1024  // 最大客户端数量

// 客户端发送缓冲区结构
typedef struct {
    char send_buf[BUF_SIZE];
    int send_len;
} ClientData;
ClientData client_data[MAX_CLIENTS];

// 设置非阻塞
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl设置非阻塞失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
}

int main() {
    int epoll_fd, listen_fd, client_fd, ret, i;
    struct epoll_event ev, events[MAX_EVENTS];
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char recv_buf[BUF_SIZE];

    // 初始化客户端数据
    memset(client_data, 0, sizeof(client_data));

    // 创建epoll实例
    if ((epoll_fd = epoll_create1(0)) < 0) {
        perror("epoll_create1失败");
        return -1;
    }

    // 创建监听套接字
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket创建失败");
        return -1;
    }
    set_nonblocking(listen_fd);

    // 监听FD加入epoll
    ev.events = EPOLLIN;
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        perror("epoll_ctl添加监听FD失败");
        close(listen_fd);
        close(epoll_fd);
        return -1;
    }

    // 端口复用+绑定+监听
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    listen(listen_fd, 5);
    printf("epoll服务器(LT模式)启动:端口=%d\n", PORT);

    // 事件循环
    while (1) {
        ret = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (ret < 0) {
            perror("epoll_wait失败");
            continue;
        }

        for (i = 0; i < ret; i++) {
            int fd = events[i].data.fd;

            // 处理读事件(修正:将revents改为events)
            if (events[i].events & EPOLLIN) {
                if (fd == listen_fd) {
                    // 新客户端连接
                    client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
                    if (client_fd < 0) {
                        perror("accept失败");
                        continue;
                    }
                    if (client_fd >= MAX_CLIENTS) {
                        printf("客户端FD=%d 超出最大限制(%d)\n", client_fd, MAX_CLIENTS);
                        close(client_fd);
                        continue;
                    }

                    set_nonblocking(client_fd);
                    client_data[client_fd].send_len = 0;
                    ev.events = EPOLLIN;
                    ev.data.fd = client_fd;
                    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) < 0) {
                        perror("epoll_ctl添加客户端FD失败");
                        close(client_fd);
                        continue;
                    }

                    // 打印客户端信息
                    char client_ip[INET_ADDRSTRLEN];
                    inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
                    printf("新客户端连接:%s:%d(FD=%d)\n", 
                           client_ip, ntohs(client_addr.sin_port), client_fd);
                } else {
                    // 接收客户端数据
                    memset(recv_buf, 0, BUF_SIZE);
                    ret = recv(fd, recv_buf, BUF_SIZE - 1, 0);
                    if (ret <= 0) {
                        printf("客户端FD=%d 断开连接\n", fd);
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        client_data[fd].send_len = 0;
                        continue;
                    }

                    // 暂存数据并注册写事件
                    printf("FD=%d 接收:%s(%d字节)→ 暂存待发送\n", fd, recv_buf, ret);
                    strncpy(client_data[fd].send_buf, recv_buf, BUF_SIZE - 1);
                    client_data[fd].send_len = ret;
                    ev.events = EPOLLIN | EPOLLOUT;  // 同时监听读写
                    ev.data.fd = fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
                }
            }

            // 处理写事件(修正:将revents改为events)
            if (events[i].events & EPOLLOUT) {
                int fd = events[i].data.fd;
                ClientData* data = &client_data[fd];

                if (data->send_len <= 0) {
                    // 无数据则取消写事件
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
                    continue;
                }

                // 发送数据(修正缓冲区偏移计算)
                int sent = send(fd, data->send_buf, data->send_len, 0);
                if (sent < 0) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                        perror("send错误");
                        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                        close(fd);
                        data->send_len = 0;
                    }
                    continue;
                }

                // 更新待发送长度
                data->send_len -= sent;
                if (data->send_len == 0) {
                    printf("FD=%d 发送完成:%s(%d字节)\n", fd, data->send_buf, sent);
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
                } else {
                    printf("FD=%d 部分发送:%d字节,剩余%d字节\n", fd, sent, data->send_len);
                    memmove(data->send_buf, data->send_buf + sent, data->send_len);
                }
            }

            // 处理异常事件(修正:将revents改为events,移除EPOLLNVAL)
            if (events[i].events & (EPOLLERR | EPOLLHUP)) {
                printf("客户端FD=%d 异常断开\n", fd);
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
                client_data[fd].send_len = 0;
            }
        }
    }

    close(listen_fd);
    close(epoll_fd);
    return 0;
}

编译运行gcc -o server_epoll_lt server_epoll_lt.c && ./server_epoll_lt

边缘触发(ET)实现(高性能模式)
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <stdlib.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <errno.h>

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_EVENTS 1024
#define MAX_CLIENTS 1024  // 最大客户端数量(匹配发送缓冲区数组大小)

// 客户端数据结构体:存储发送缓冲区和待发长度(ET模式必需,避免数据丢失)
typedef struct {
    char send_buf[BUF_SIZE];  // 暂存待发送数据
    int send_len;             // 待发送数据长度(0表示无数据)
} ClientData;
ClientData client_data[MAX_CLIENTS];  // 用FD作为索引,管理每个客户端的发送数据

// 强制设置套接字为非阻塞(ET模式必需,避免阻塞循环)
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl设置非阻塞失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
}

int main() {
    int epoll_fd, listen_fd, client_fd, ret, i;
    struct epoll_event ev, events[MAX_EVENTS];  // ev:注册事件;events:就绪事件
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    char recv_buf[BUF_SIZE];  // 接收缓冲区

    // 1. 初始化所有客户端的发送数据(清空缓冲区,待发长度设为0)
    memset(client_data, 0, sizeof(client_data));

    // 2. 创建epoll实例(参数0:已废弃,传0即可)
    if ((epoll_fd = epoll_create1(0)) < 0) {
        perror("epoll_create1失败");
        return -1;
    }

    // 3. 创建监听套接字并设为非阻塞(ET模式必需,避免accept阻塞)
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket创建失败");
        return -1;
    }
    set_nonblocking(listen_fd);

    // 4. 监听FD加入epoll(ET模式:修正宏名 EPOLL_IN→EPOLLIN)
    ev.events = EPOLLIN | EPOLLET;  // 关键:启用边缘触发(正确宏名)
    ev.data.fd = listen_fd;
    if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev) < 0) {
        perror("epoll_ctl添加监听FD失败");
        close(listen_fd);
        close(epoll_fd);
        return -1;
    }

    // 5. 端口复用+绑定+监听(基础网络配置)
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);  // 监听所有网卡IP
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind失败");
        close(listen_fd);
        close(epoll_fd);
        return -1;
    }
    listen(listen_fd, 5);
    printf("epoll服务器(边缘触发ET模式)启动:端口=%d\n", PORT);

    // 6. epoll事件循环(ET模式核心逻辑)
    while (1) {
        // 阻塞等待就绪事件(返回就绪事件数量)
        ret = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
        if (ret < 0) {
            perror("epoll_wait失败");
            continue;
        }

        // 遍历所有就绪事件(ET模式:一次事件需处理完所有数据)
        for (i = 0; i < ret; i++) {
            int fd = events[i].data.fd;
            ClientData* cli_data = &client_data[fd];  // 当前客户端的发送数据

            // -------------------------- 处理读事件(新连接/客户端发数据)--------------------------
            if (events[i].events & EPOLLIN) {  // 修正:EPOLL_IN→EPOLLIN
                if (fd == listen_fd) {
                    // 监听FD就绪:ET模式需循环accept,避免遗漏新连接(仅触发一次状态变化)
                    while (1) {
                        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
                        if (client_fd < 0) {
                            // 无更多连接时,非阻塞accept返回EAGAIN,退出循环(ET模式关键)
                            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                break;
                            } else {
                                perror("accept失败");
                                break;
                            }
                        }

                        // 检查客户端FD是否超出最大限制(避免数组越界)
                        if (client_fd >= MAX_CLIENTS) {
                            printf("客户端FD=%d 超出最大限制(%d),拒绝连接\n", client_fd, MAX_CLIENTS);
                            close(client_fd);
                            continue;
                        }

                        // 新连接必须设为非阻塞(ET模式必需)
                        set_nonblocking(client_fd);
                        // 初始化该客户端的发送数据(清空缓冲区)
                        client_data[client_fd].send_len = 0;

                        // 新客户端FD加入epoll(ET模式:修正宏名 EPOLL_IN→EPOLLIN)
                        ev.events = EPOLLIN | EPOLLET;  // 仅监听读事件+边缘触发
                        ev.data.fd = client_fd;
                        if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &ev) < 0) {
                            perror("epoll_ctl添加客户端FD失败");
                            close(client_fd);
                            continue;
                        }

                        // 打印客户端连接信息
                        char client_ip[INET_ADDRSTRLEN];
                        inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
                        printf("新客户端连接:%s:%d(FD=%d)\n", 
                               client_ip, ntohs(client_addr.sin_port), client_fd);
                    }
                } else {
                    // 客户端FD就绪:ET模式需循环recv,直到无数据(仅触发一次状态变化)
                    while (1) {
                        memset(recv_buf, 0, BUF_SIZE);
                        ssize_t read_len = recv(fd, recv_buf, BUF_SIZE - 1, 0);

                        if (read_len > 0) {
                            // 接收成功:暂存数据到发送缓冲区,注册写事件(等待发送)
                            printf("FD=%d 接收:%s(%d字节)→ 暂存待发送\n", fd, recv_buf, (int)read_len);
                            // 确保不超出缓冲区(简化处理:若数据过长,此处可扩展为动态缓冲区)
                            if (cli_data->send_len + read_len <= BUF_SIZE) {
                                memcpy(cli_data->send_buf + cli_data->send_len, recv_buf, read_len);
                                cli_data->send_len += read_len;
                                // 注册写事件(ET模式:修正宏名 EPOLL_IN→EPOLLIN)
                                ev.events = EPOLLIN | EPOLLOUT | EPOLLET;  // 读+写+边缘触发
                                ev.data.fd = fd;
                                epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
                            } else {
                                printf("FD=%d 发送缓冲区已满,丢弃数据(%d字节)\n", fd, (int)read_len);
                            }
                        } else if (read_len == 0) {
                            // 客户端主动断开:清理资源
                            printf("客户端FD=%d 正常断开\n", fd);
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                            close(fd);
                            cli_data->send_len = 0;  // 清空缓冲区
                            break;
                        } else {
                            // 无更多数据:非阻塞recv返回EAGAIN,退出循环(ET模式关键)
                            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                                break;
                            } else {
                                // 其他错误:关闭连接
                                perror("recv错误");
                                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                                close(fd);
                                cli_data->send_len = 0;
                                break;
                            }
                        }
                    }
                }
            }

            // -------------------------- 处理写事件(发送缓冲区有空间)--------------------------
            if (events[i].events & EPOLLOUT) {
                // ET模式需循环send,直到数据发完或缓冲区满(仅触发一次状态变化)
                while (cli_data->send_len > 0) {
                    ssize_t sent_len = send(fd, cli_data->send_buf, cli_data->send_len, 0);
                    if (sent_len < 0) {
                        // 缓冲区满:非阻塞send返回EAGAIN,退出循环(下次有空间再发)
                        if (errno == EAGAIN || errno == EWOULDBLOCK) {
                            break;
                        } else {
                            // 其他错误:关闭连接
                            perror("send错误");
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                            close(fd);
                            cli_data->send_len = 0;
                            break;
                        }
                    }

                    // 更新待发长度:减去已发送字节
                    cli_data->send_len -= (int)sent_len;
                    if (cli_data->send_len == 0) {
                        // 数据全部发完:取消写事件监听(修正宏名 EPOLL_IN→EPOLLIN)
                        printf("FD=%d 发送完成:%s(%d字节)\n", fd, cli_data->send_buf, (int)sent_len);
                        ev.events = EPOLLIN | EPOLLET;  // 仅保留读事件+边缘触发
                        ev.data.fd = fd;
                        epoll_ctl(epoll_fd, EPOLL_CTL_MOD, fd, &ev);
                        break;
                    } else {
                        // 部分发送:移动剩余数据到缓冲区开头(下次继续发)
                        memmove(cli_data->send_buf, cli_data->send_buf + sent_len, cli_data->send_len);
                        printf("FD=%d 部分发送:%d字节,剩余%d字节\n", fd, (int)sent_len, cli_data->send_len);
                    }
                }
            }

            // -------------------------- 处理异常事件(客户端强制断连)--------------------------
            if (events[i].events & (EPOLLERR | EPOLLHUP)) {
                printf("客户端FD=%d 异常断开(如强制关闭、网络中断)\n", fd);
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
                cli_data->send_len = 0;  // 清空缓冲区,释放资源
            }
        }
    }

    // 理论上不会执行到(循环是无限的)
    close(listen_fd);
    close(epoll_fd);
    return 0;
}

编译运行gcc -o server_epoll_et server_epoll_et.c && ./server_epoll_et

信号驱动 IO(Signal-Driven IO)模型:高效的信号通知机制

信号驱动 I/O 模型是衔接 "非阻塞 I/O" 与 "纯异步 I/O" 的过渡方案,通过内核主动发送信号 通知 I/O 操作就绪,核心优势在于彻底避免轮询的开销,实现 "数据就绪即通知" 的按需响应模式,适配低频次、高响应要求的场景,(如网络监控工具 tcpdump、嵌入式设备传感器交互),无法支撑高并发服务(如万级在线的游戏服务器)。

核心原理:"信号注册 - 异步等待 - 信号触发 - 数据拷贝" 四步流程 :信号驱动 I/O 的核心逻辑围绕 "内核与进程的信号协作" 展开,关键特征是 "等待 I/O 就绪阶段非阻塞,数据拷贝阶段阻塞",具体流程可拆解为以下四步:

提前注册:约定 I/O 就绪的 "通知规则":进程需先完成两项核心配置,与内核约定 I/O 就绪的信号通知方式:

  • 注册 SIGIO 信号处理函数 :通过系统调用(如 sigaction)向内核注册一个自定义函数,该函数是 "内核检测到 I/O 就绪后,进程需执行的读写逻辑",且需确保函数仅调用 "信号安全函数"(如 read/write),避免非安全函数(如 printf)导致的进程状态混乱。
  • 绑定文件描述符(FD)与进程 :通过文件控制调用(如 fcntl)的 F_SETOWN 命令将目标文件描述符(FD,如网络 Socket、设备文件)与当前进程 ID 绑定,然后用 F_SETFL 命令为 FD 添加 O_ASYNC 标志,开启该 FD 的 "信号驱动模式";同时,将 FD 设为非阻塞模式,避免后续数据拷贝时阻塞信号处理流程,导致其他 I/O 事件无法响应。

异步等待:进程自由执行其他任务 :完成注册后,进程无需像 "阻塞 I/O" 那样卡在 read()/write() 调用上等待,也无需像 "非阻塞 I/O + 轮询" 那样反复检查 FD 状态。相反,进程可以立即返回,继续执行其他业务逻辑(比如处理已就绪的任务、进行数据计算、或管理其他资源);监听 FD 状态的工作完全交给内核,内核会持续监控该 FD 是否有数据到达(如 Socket 接收缓冲区有新数据),进程与内核在此阶段是 "异步并行" 的,CPU 资源不会因 "等待 I/O" 而闲置。

信号触发:内核主动 "通知" I/O 就绪 :当内核检测到目标 FD 的 I/O 操作就绪(例如客户端向 Socket 发送了数据、设备缓冲区有数据可读),会立即向绑定的进程发送 SIGIO 信号,进程原本在执行的任务会被暂停,自动跳转到之前注册的 "信号处理函数",标志着 "I/O 就绪" 的消息已成功送达。

数据拷贝:阻塞完成 "数据搬运" :在信号处理函数中,进程需要调用 read()/write() 等 I/O 函数,完成数据在 "内核缓冲区" 与 "用户缓冲区" 之间的拷贝:

  • 读操作:将内核空间中的数据(如客户端发送的网络数据)拷贝到用户空间;
  • 写操作:将用户空间中的数据(如服务器的响应数据)拷贝到内核空间。 在此阶段进程会处于阻塞状态,直到数据拷贝完成,read()/write() 才会返回,进程才能继续执行被暂停的任务。这是信号驱动 I/O 与 "纯异步 I/O"(内核全程完成拷贝,进程无阻塞)的核心区别。
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#include <stdarg.h>  // 必需:支持可变参数函数(safe_printf)

// 配置参数
#define PORT 5150          // 服务器监听端口
#define BUF_SIZE 1024      // 数据缓冲区大小(接收/发送共用)
#define MAX_CLIENTS 10     // 最大支持客户端数量(避免数组越界)

// 客户端结构体:结构化管理连接资源(替代单一FD数组,优化复用)
typedef struct {
    int fd;                  // 客户端文件描述符(-1表示槽位空闲)
    char send_buf[BUF_SIZE]; // 发送缓冲区:暂存未发完的数据(避免send阻塞)
    int send_len;            // 待发送数据长度(0=无数据需发)
    char ip[INET_ADDRSTRLEN];// 客户端IP地址(日志展示,便于排查)
    int port;                // 客户端端口(日志展示,便于排查)
} Client;

// 全局客户端列表:管理所有连接(槽位复用,节省资源)
Client clients[MAX_CLIENTS];
int client_count = 0;        // 当前活跃客户端数量(非总槽位数量)

// -------------------------- 补全:函数前置声明(解决隐式声明警告)--------------------------
void safe_printf(const char *fmt, ...);
void set_nonblocking(int fd);
void init_client(Client *cli);
int find_free_client_slot(void);  // 关键:补充该函数声明(main中先调用后定义)
void remove_client(int fd);
void handle_send(Client *cli);
void sigio_handler(int signo);
void setup_sigio(int fd, pid_t pid);
// --------------------------------------------------------------------------------

/**
 * 信号安全的日志打印函数
 * 作用:避免信号处理函数中调用printf(非信号安全)导致的重入崩溃
 * 逻辑:打印前屏蔽SIGIO,打印后恢复,确保日志完整性
 */
void safe_printf(const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);  // 初始化可变参数列表

    // 屏蔽SIGIO信号:防止打印过程中被新的IO信号中断
    sigset_t mask, old_mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGIO);
    sigprocmask(SIG_BLOCK, &mask, &old_mask);

    // 打印日志(vprintf适配可变参数,与printf格式兼容)
    vprintf(fmt, args);

    // 恢复信号掩码:允许后续SIGIO信号正常触发
    sigprocmask(SIG_SETMASK, &old_mask, NULL);

    va_end(args);  // 清理可变参数列表,避免内存泄漏
}

/**
 * 设置套接字为非阻塞模式
 * 必要性:信号驱动IO中,recv/send若阻塞会卡住信号处理函数,导致所有IO停滞
 * 异常处理:设置失败直接退出(非阻塞是信号驱动IO的基础)
 */
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);  // 获取当前FD的状态标志
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {  // 添加非阻塞标志
        perror("fcntl设置非阻塞失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
}

/**
 * 初始化客户端结构体(重置为空闲状态)
 * 作用:客户端断开后,清理其缓存和状态,使槽位可复用(避免资源浪费)
 */
void init_client(Client *cli) {
    cli->fd = -1;                      // 标记槽位空闲
    cli->send_len = 0;                 // 清空待发送长度
    memset(cli->send_buf, 0, BUF_SIZE); // 清空发送缓冲区
    memset(cli->ip, 0, INET_ADDRSTRLEN); // 清空IP记录
    cli->port = 0;                     // 清空端口记录
}

/**
 * 查找空闲的客户端槽位
 * 优化点:遍历数组找到第一个空闲槽位(fd=-1),避免新建连接时浪费槽位
 * 返回值:空闲槽位索引(-1表示无空闲槽位,需拒绝新连接)
 */
int find_free_client_slot() {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i].fd == -1) {
            return i;
        }
    }
    return -1;
}

/**
 * 移除客户端连接(完整资源清理)
 * 作用:客户端断开时,关闭FD、重置槽位、更新活跃数量,避免资源泄漏
 * 参数:fd - 待移除的客户端文件描述符
 */
void remove_client(int fd) {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (clients[i].fd == fd) {
            // 打印断开日志(含IP/端口,便于排查)
            safe_printf("客户端FD=%d(%s:%d)断开连接\n", 
                   fd, clients[i].ip, clients[i].port);
            close(fd);                      // 关闭客户端FD
            init_client(&clients[i]);       // 重置槽位为空闲
            client_count--;                 // 减少活跃客户端数量
            break;
        }
    }
}

/**
 * 处理发送缓冲区数据(非阻塞发送逻辑)
 * 核心:避免直接send阻塞,通过循环发送确保数据不丢失(信号驱动IO需手动控发送)
 * 参数:cli - 指向当前客户端结构体的指针
 */
void handle_send(Client *cli) {
    if (cli->send_len <= 0 || cli->fd == -1) {
        return;  // 无数据或客户端已断开,直接返回
    }

    // 循环发送:直到数据发完或缓冲区满(非阻塞特性)
    while (cli->send_len > 0) {
        ssize_t sent = send(cli->fd, cli->send_buf, cli->send_len, 0);
        if (sent < 0) {
            // 情况1:发送缓冲区满(EAGAIN/EWOULDBLOCK)→ 下次信号触发再发
            if (errno == EAGAIN || errno == EWOULDBLOCK) {
                break;
            } 
            // 情况2:其他错误(如客户端强制断连)→ 清理连接
            else {
                perror("send错误");
                remove_client(cli->fd);
                break;
            }
        } else if (sent == 0) {
            // 情况3:客户端主动关闭连接(发送FIN包)→ 清理连接
            remove_client(cli->fd);
            break;
        } else {
            // 情况4:成功发送部分/全部数据→ 更新待发长度
            cli->send_len -= (int)sent;
            if (cli->send_len > 0) {
                // 部分发送:将剩余数据移到缓冲区开头(下次继续发)
                memmove(cli->send_buf, cli->send_buf + sent, cli->send_len);
                safe_printf("FD=%d 部分发送:%d字节,剩余%d字节\n", 
                       cli->fd, (int)sent, cli->send_len);
            } else {
                // 全部发送:清空缓冲区,打印完成日志
                safe_printf("FD=%d 发送完成:%s(%d字节)\n", 
                       cli->fd, cli->send_buf, (int)sent);
                memset(cli->send_buf, 0, BUF_SIZE);
            }
        }
    }
}

/**
 * SIGIO信号处理函数(信号驱动IO核心逻辑)
 * 触发时机:内核检测到客户端IO就绪(有数据可读/发送缓冲区有空间)
 * 注意:信号处理函数需简洁,避免调用非信号安全函数(此处用safe_printf规避)
 */
void sigio_handler(int signo) {
    if (signo != SIGIO) {
        return;  // 忽略非SIGIO信号(防止误触发)
    }

    // 遍历所有客户端槽位,处理IO事件
    for (int i = 0; i < MAX_CLIENTS; i++) {
        Client *cli = &clients[i];
        if (cli->fd == -1) {
            continue;  // 跳过空闲槽位,减少无效操作
        }

        // 处理读事件→ 读取所有就绪数据(信号驱动IO仅通知一次,需手动读完)
        int has_data = 0;
        char recv_buf[BUF_SIZE];
        do {
            memset(recv_buf, 0, BUF_SIZE);
            ssize_t recv_len = recv(cli->fd, recv_buf, BUF_SIZE - 1, 0);  // 留1字节存结束符

            if (recv_len > 0) {
                has_data = 1;
                // 打印接收日志,暂存数据到发送缓冲区(不直接send,避免阻塞)
                safe_printf("FD=%d(%s:%d)接收:%s(%d字节)→ 暂存待发送\n", 
                       cli->fd, cli->ip, cli->port, recv_buf, (int)recv_len);

                // 检查发送缓冲区容量:避免数据溢出(简化处理,实际可扩展动态缓冲区)
                if (cli->send_len + recv_len <= BUF_SIZE) {
                    memcpy(cli->send_buf + cli->send_len, recv_buf, recv_len);
                    cli->send_len += (int)recv_len;
                } else {
                    safe_printf("FD=%d 发送缓冲区已满,丢弃数据(%d字节)\n", 
                           cli->fd, (int)recv_len);
                }
            } else if (recv_len == 0) {
                // 客户端主动断开(发送FIN包)→ 清理连接
                remove_client(cli->fd);
                break;
            } else {
                // 无更多数据(EAGAIN/EWOULDBLOCK)或错误→ 退出循环
                if (errno != EAGAIN && errno != EWOULDBLOCK) {
                    perror("recv错误");
                    remove_client(cli->fd);
                }
                break;
            }
        } while (1);

        // 处理写事件→ 发送暂存的数据(有新数据或残留数据时触发)
        if (has_data || cli->send_len > 0) {
            handle_send(cli);
        }
    }
}

/**
 * 配置信号驱动IO(客户端连接初始化关键步骤)
 * 核心逻辑:1. 设置FD所有者(内核向该进程发SIGIO);2. 启用异步IO标志
 * 参数:fd - 待配置的客户端FD;pid - 当前进程ID(FD所有者)
 */
void setup_sigio(int fd, pid_t pid) {
    // 设置FD的所有者进程:内核检测到IO就绪时,向该进程发送SIGIO信号
    if (fcntl(fd, F_SETOWN, pid) < 0) {
        perror("fcntl设置FD所有者失败");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 启用异步IO标志(O_ASYNC):告知内核"该FD需异步通知IO就绪"
    int flags = fcntl(fd, F_GETFL, 0);
    if (fcntl(fd, F_SETFL, flags | O_ASYNC) < 0) {
        perror("fcntl启用异步IO失败");
        close(fd);
        exit(EXIT_FAILURE);
    }

    // 强制设置为非阻塞:信号驱动IO必需(避免recv/send阻塞信号处理函数)
    set_nonblocking(fd);
}

int main() {
    int listen_fd, client_fd;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    struct sigaction sa;
    pid_t pid = getpid();  // 当前进程ID(用于设置客户端FD的所有者)

    // 初始化客户端列表:所有槽位设为空闲状态(避免野值)
    for (int i = 0; i < MAX_CLIENTS; i++) {
        init_client(&clients[i]);
    }

    // 创建监听套接字(TCP协议)
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("创建监听套接字失败");
        return -1;
    }

    // 基础网络配置:端口复用+绑定+监听(确保服务稳定启动)
    int reuse = 1;
    // 端口复用:避免服务器重启时端口因TIME_WAIT状态被占用
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
        perror("设置端口复用失败");
        close(listen_fd);
        return -1;
    }

    // 绑定地址:监听所有网卡的PORT端口(INADDR_ANY表示所有IP)
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;         // IPv4协议
    server_addr.sin_port = htons(PORT);        // 端口转换为网络字节序
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("绑定地址失败");
        close(listen_fd);
        return -1;
    }

    // 开始监听:backlog=5(半连接队列大小,根据需求调整)
    if (listen(listen_fd, 5) < 0) {
        perror("监听失败");
        close(listen_fd);
        return -1;
    }

    // 设置监听套接字为非阻塞:避免accept阻塞主循环(新连接需即时响应)
    set_nonblocking(listen_fd);

    // 配置SIGIO信号:注册信号处理函数(内核通知IO就绪的入口)
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = sigio_handler;  // 信号触发时执行的函数
    sigemptyset(&sa.sa_mask);       // 信号处理期间不屏蔽其他信号(简化逻辑)
    sa.sa_flags = 0;                // 禁用SA_RESTART:避免系统调用被中断后自动重启
    if (sigaction(SIGIO, &sa, NULL) < 0) {
        perror("注册SIGIO信号处理失败");
        close(listen_fd);
        return -1;
    }

    // 打印启动日志:告知服务就绪状态
    safe_printf("信号驱动IO服务器启动:端口=%d,最大客户端=%d\n", PORT, MAX_CLIENTS);

    // 主循环:非阻塞接受新连接(核心:不阻塞,无不必要休眠,低延迟)
    while (1) {
        // 非阻塞accept:无新连接时返回-1,errno=EAGAIN/EWOULDBLOCK(正常)
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_fd > 0) {
            // 查找空闲槽位:无空闲则拒绝新连接(避免数组越界)
            int slot = find_free_client_slot();  // 此处调用前已声明,无编译警告
            if (slot == -1) {
                safe_printf("客户端数量已达上限(%d),拒绝连接(FD=%d)\n", 
                       MAX_CLIENTS, client_fd);
                close(client_fd);
                continue;
            }

            // 配置新客户端为信号驱动IO(关键步骤,确保内核能通知IO就绪)
            setup_sigio(client_fd, pid);

            // 填充客户端信息:IP、端口、FD(日志用,便于排查)
            Client *new_cli = &clients[slot];
            new_cli->fd = client_fd;
            // 将客户端IP从网络字节序转换为字符串(inet_ntop适配IPv4/IPv6)
            inet_ntop(AF_INET, &client_addr.sin_addr, new_cli->ip, sizeof(new_cli->ip));
            new_cli->port = ntohs(client_addr.sin_port);  // 端口转换为主机字节序
            client_count++;  // 更新活跃客户端数量

            // 打印新连接日志:含IP、端口、FD、当前活跃数
            safe_printf("新客户端连接:%s:%d(FD=%d,当前活跃数=%d)\n", 
                   new_cli->ip, new_cli->port, client_fd, client_count);
        } else {
            // 仅处理非"无新连接"的错误(EAGAIN/EWOULDBLOCK是正常情况)
            if (errno != EAGAIN && errno != EWOULDBLOCK) {
                perror("accept错误");
                usleep(10000);  // 错误时短暂休眠,避免CPU空转(仅错误场景)
            }
        }
    }

    // 理论上不会执行到(主循环为无限循环)
    close(listen_fd);
    return 0;
}

编译运行gcc -o server_sigio server_sigio.c && ./server_sigio

异步 IO(Asynchronous IO)模型:完全非阻塞

异步 I/O 模型是唯一实现 "等待数据就绪" 与 "数据拷贝" 两阶段全程非阻塞 的模型,通过 "进程发起请求后彻底脱离 I/O 流程,内核全程处理并在完成后通知" 的机制,将 CPU 资源从 "等待 I/O" 中完全解放,适配对性能有极致需求的场景(如百万级并发网关、高性能数据库),但需承担更高的开发复杂度与内核兼容性成本。

核心原理:"进程发起请求→内核全程处理→完成后通知" 三步流程 :异步 I/O 的核心逻辑围绕 "进程与内核的'委托 - 通知'关系" 展开,全程无任何阻塞环节,关键特征是 "仅在'发起请求'和'处理结果'时占用进程,中间流程完全交给内核" ,具体流程可拆解为以下三步:

进程发起异步 I/O 请求,立即返回: 进程通过专门的异步 I/O 接口向内核 "委托" I/O 任务,无需等待任何结果,直接回归业务逻辑,核心操作包括:

  • 指定 I/O 任务参数:明确 I/O 类型(读 / 写)、目标文件描述符(FD,如 Socket、磁盘文件)、用户空间数据缓冲区地址、数据长度等,让内核清楚 "要处理什么 I/O";
  • 设置完成通知方式 :告知内核 "I/O 全程完成后,如何通知我",常见两种方式,信号通知(如 POSIX 标准 aio_read/aio_write 绑定 SIGIO 信号);完成队列通知(如 Linux 现代方案 io_uring 的 CQ 队列,内核将结果写入队列,进程轮询队列即可,无信号竞态问题);
  • 非阻塞返回:请求提交后,内核仅记录任务信息(如缓冲区地址、FD 状态),进程无需等待 "是否就绪" 或 "是否拷贝",立即返回执行其他业务(如处理已完成的 I/O 结果、计算任务)。

内核全程处理 I/O 流程,进程无感知:进程发起请求后,I/O 的两个核心阶段完全由内核接管,进程无需任何干预,实现 "CPU 与 I/O 并行":

  • 阶段 1:内核等待 I/O 就绪:内核持续监控目标 FD 的状态(如 Socket 接收缓冲区是否有数据、磁盘是否准备好读写),此阶段与进程完全并行,无需进程轮询或阻塞;
  • 阶段 2:内核完成数据拷贝:当 FD 就绪后,内核直接将数据在 "内核空间" 与 "用户空间" 之间搬运(读操作:内核→用户;写操作:用户→内核),整个拷贝过程无需进程参与,也不会阻塞进程,内核可通过硬件 DMA(直接内存访问)或高效内存拷贝机制完成,避免占用进程 CPU(区别于信号驱动 I/O 的核心,信号驱动 I/O 仅让内核通知 "就绪",拷贝仍需进程阻塞处理,而异步 I/O 让内核全程完成 "就绪等待 + 数据拷贝",进程全程无感知)。

内核通知进程 I/O 完成,进程处理结果:当内核完成 "等待就绪 + 数据拷贝" 全程后,通过进程预设的方式发送 "完成通知",进程仅需处理结果,无需参与 I/O 执行:

  • 触发通知 :若为 "信号通知",内核发送预设信号(如 SIGIO);若为 "完成队列通知",内核将 I/O 结果(成功 / 失败、处理字节数)写入队列(如 io_uring 的 CQ 队列);
  • 进程处理结果:进程收到通知后,无需执行任何 I/O 操作,仅需读取 "完成结果" 并处理业务逻辑:读操作完成:直接使用用户缓冲区中已就绪的数据(如数据库将加载的磁盘数据返回给客户端); 写操作完成:确认数据已发送(如网关记录 "请求已成功转发" 日志);失败处理:针对错误码(如 FD 关闭、磁盘空间不足)执行重试或告警逻辑。此阶段仍为非阻塞 ------ 即使大量 I/O 同时完成,进程也可批量处理结果(如轮询完成队列),无需逐个等待。
基于 POSIX 标准 aio_* 接口:传统异步 I/O
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <aio.h>
#include <signal.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <arpa/inet.h>

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 1024  // 最大客户端数量(匹配缓冲区数组大小)

// 客户端数据结构体:存储aio控制块、缓冲区与连接信息(异步I/O必需)
typedef struct {
    int fd;                  // 客户端FD(-1表示空闲)
    struct aiocb read_aio;   // 读操作aio控制块(内核用)
    struct aiocb write_aio;  // 写操作aio控制块(内核用)
    char read_buf[BUF_SIZE]; // 读缓冲区(用户空间,内核直接写入)
    char write_buf[BUF_SIZE];// 写缓冲区(用户空间,内核直接读取)
    struct sockaddr_in addr; // 客户端地址(日志用)
} AsyncClient;
AsyncClient client_data[MAX_CLIENTS]; // 用FD作为索引,管理每个客户端
int client_count = 0;                 // 当前活跃客户端数量

// 初始化客户端结构体(标记为空闲状态)
void init_client(AsyncClient *cli) {
    cli->fd = -1;
    memset(&cli->read_aio, 0, sizeof(struct aiocb));
    memset(&cli->write_aio, 0, sizeof(struct aiocb));
    memset(cli->read_buf, 0, BUF_SIZE);
    memset(cli->write_buf, 0, BUF_SIZE);
    memset(&cli->addr, 0, sizeof(struct sockaddr_in));
}

// 查找空闲客户端槽位
int find_free_client() {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (client_data[i].fd == -1) {
            return i;
        }
    }
    return -1; // 无空闲槽位
}

// 强制设置套接字为非阻塞(避免accept阻塞主循环)
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl设置非阻塞失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
}

// aio完成信号处理函数(内核完成I/O后发送SIGIO触发)
void aio_completion_handler(int signo, siginfo_t *info, void *context) {
    if (signo != SIGIO || info->si_signo != SIGIO) {
        return; // 忽略非SIGIO信号
    }

    // 遍历客户端,匹配触发信号的aio控制块
    for (int i = 0; i < MAX_CLIENTS; i++) {
        AsyncClient *cli = &client_data[i];
        if (cli->fd == -1) {
            continue;
        }

        // -------------------------- 处理读操作完成(内核已完成数据拷贝)--------------------------
        if (&cli->read_aio == info->si_value.sival_ptr) {
            ssize_t ret = aio_return(&cli->read_aio); // 获取读结果(字节数)
            if (ret <= 0) {
                // 核心修改1:区分正常断开(ret=0)和异常断开(ret<0)
                if (ret == 0) {
                    // 正常断开:客户端主动调用close()
                    printf("客户端FD=%d 正常断开\n", cli->fd);
                } else {
                    // 异常断开:如强制关闭、网络中断(错误码对应具体场景)
                    const char *err_msg = strerror(-ret); // 转换aio错误码为可读信息
                    printf("客户端FD=%d 异常断开(%s,如强制关闭、网络中断)\n", cli->fd, err_msg);
                }
                // 原有清理逻辑保留
                close(cli->fd);
                init_client(cli);
                client_count--;
                return;
            }

            // 接收成功:暂存数据到写缓冲区,发起异步写请求(原有逻辑保留)
            printf("FD=%d 接收:%s(%d字节)→ 发起异步写\n", cli->fd, cli->read_buf, (int)ret);
            memcpy(cli->write_buf, cli->read_buf, ret);

            // 初始化写操作aio控制块(原有逻辑保留)
            memset(&cli->write_aio, 0, sizeof(struct aiocb));
            cli->write_aio.aio_fildes = cli->fd;          // 目标FD
            cli->write_aio.aio_buf = cli->write_buf;       // 写缓冲区(用户空间)
            cli->write_aio.aio_nbytes = ret;              // 写数据长度
            cli->write_aio.aio_offset = 0;                 // Socket忽略偏移量
            // 设置完成通知:发送SIGIO,携带write_aio地址(用于识别操作)
            cli->write_aio.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
            cli->write_aio.aio_sigevent.sigev_signo = SIGIO;
            cli->write_aio.aio_sigevent.sigev_value.sival_ptr = &cli->write_aio;

            // 发起异步写请求(原有逻辑保留)
            if (aio_write(&cli->write_aio) < 0) {
                perror("aio_write失败");
                close(cli->fd);
                init_client(cli);
                client_count--;
            }
            return;
        }

        // -------------------------- 处理写操作完成(内核已完成数据拷贝)--------------------------
        if (&cli->write_aio == info->si_value.sival_ptr) {
            ssize_t ret = aio_return(&cli->write_aio); // 获取写结果(字节数)
            if (ret <= 0) {
                // 核心修改2:区分写失败类型,识别客户端异常断开
                const char *err_msg = strerror(-ret);
                // 常见异常断开错误码:EPIPE(管道破裂)、ECONNRESET(连接重置)、ENOTCONN(未连接)
                if (-ret == EPIPE || -ret == ECONNRESET || -ret == ENOTCONN) {
                    printf("客户端FD=%d 异常断开(发送失败:%s,如强制关闭、网络中断)\n", cli->fd, err_msg);
                } else {
                    // 其他写错误(如缓冲区满):保留原有perror逻辑
                    perror("aio_write失败");
                }
                // 原有清理逻辑保留
                close(cli->fd);
                init_client(cli);
                client_count--;
                return;
            }

            // 写完成:重新发起异步读(原有逻辑保留)
            printf("FD=%d 发送完成:%s(%d字节)→ 重新发起异步读\n", cli->fd, cli->write_buf, (int)ret);
            memset(&cli->read_aio, 0, sizeof(struct aiocb));
            cli->read_aio.aio_fildes = cli->fd;          // 目标FD
            cli->read_aio.aio_buf = cli->read_buf;       // 读缓冲区(用户空间)
            cli->read_aio.aio_nbytes = BUF_SIZE - 1;     // 读长度(留1字节存结束符)
            cli->read_aio.aio_offset = 0;                 // Socket忽略偏移量
            // 设置完成通知:发送SIGIO,携带read_aio地址
            cli->read_aio.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
            cli->read_aio.aio_sigevent.sigev_signo = SIGIO;
            cli->read_aio.aio_sigevent.sigev_value.sival_ptr = &cli->read_aio;

            // 发起异步读请求(原有逻辑保留)
            if (aio_read(&cli->read_aio) < 0) {
                perror("aio_read失败");
                close(cli->fd);
                init_client(cli);
                client_count--;
            }
            return;
        }
    }
}

int main() {
    int listen_fd, client_fd, slot;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_addr_len = sizeof(client_addr);
    struct sigaction sa;

    // 1. 初始化所有客户端数据(清空缓冲区,标记为空闲)
    for (int i = 0; i < MAX_CLIENTS; i++) {
        init_client(&client_data[i]);
    }

    // 2. 创建监听套接字并设为非阻塞(避免accept阻塞主循环)
    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket创建失败");
        return -1;
    }
    set_nonblocking(listen_fd);

    // 3. 配置aio完成信号(SIGIO)处理
    memset(&sa, 0, sizeof(sa));
    sa.sa_sigaction = aio_completion_handler; // 信号处理函数(带参数)
    sa.sa_flags = SA_SIGINFO;                 // 启用siginfo_t参数(获取aio信息)
    sigemptyset(&sa.sa_mask);                 // 信号处理期间不屏蔽其他信号
    if (sigaction(SIGIO, &sa, NULL) < 0) {
        perror("sigaction注册SIGIO失败");
        close(listen_fd);
        return -1;
    }

    // 4. 端口复用+绑定+监听(基础网络配置)
    int reuse = 1;
    setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有网卡IP
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind失败");
        close(listen_fd);
        return -1;
    }
    listen(listen_fd, 5);
    printf("异步I/O服务器(POSIX aio_*)启动:端口=%d\n", PORT);

    // 5. 主循环:非阻塞accept新连接(全程无I/O阻塞)
    while (1) {
        // 非阻塞accept:无新连接时返回-1,errno=EAGAIN(正常)
        client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
        if (client_fd > 0) {
            // 查找空闲客户端槽位
            slot = find_free_client();
            if (slot == -1) {
                printf("客户端FD=%d 超出最大限制(%d),拒绝连接\n", client_fd, MAX_CLIENTS);
                close(client_fd);
                continue;
            }

            // 初始化新客户端数据
            AsyncClient *new_cli = &client_data[slot];
            new_cli->fd = client_fd;
            new_cli->addr = client_addr;
            client_count++;

            // 发起第一个异步读请求(委托内核处理"等待就绪+数据拷贝")
            memset(&new_cli->read_aio, 0, sizeof(struct aiocb));
            new_cli->read_aio.aio_fildes = client_fd;
            new_cli->read_aio.aio_buf = new_cli->read_buf;
            new_cli->read_aio.aio_nbytes = BUF_SIZE - 1;
            new_cli->read_aio.aio_offset = 0;
            new_cli->read_aio.aio_sigevent.sigev_notify = SIGEV_SIGNAL;
            new_cli->read_aio.aio_sigevent.sigev_signo = SIGIO;
            new_cli->read_aio.aio_sigevent.sigev_value.sival_ptr = &new_cli->read_aio;

            if (aio_read(&new_cli->read_aio) < 0) {
                perror("aio_read发起失败");
                close(client_fd);
                init_client(new_cli);
                client_count--;
                continue;
            }

            // 打印新连接日志(进程未阻塞,直接返回)
            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
            printf("新客户端连接:%s:%d(FD=%d,当前活跃数=%d)\n", 
                   client_ip, ntohs(client_addr.sin_port), client_fd, client_count);
        } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
            // 处理非"无新连接"的错误
            perror("accept错误");
            usleep(10000); // 错误时短暂休眠,避免CPU空转
        }

        // 主循环可执行其他业务逻辑(如统计、日志清理),无需等待I/O
        // ...
    }

    // 理论上不会执行到(循环是无限的)
    close(listen_fd);
    return 0;
}

编译运行gcc -o server_aio server_aio.c -lrt && ./server_aio

基于 Linux 现代 io_uring 接口:高性能异步 I/O
scss 复制代码
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <liburing.h> // 新版 liburing 头文件(≥2.0)

#define PORT 5150
#define BUF_SIZE 1024
#define MAX_CLIENTS 1024  // 最大客户端数量(匹配缓冲区数组大小)
#define RING_ENTRIES 256  // io_uring 队列大小(SQ/CQ 最大条目数,建议 256~1024)

// 客户端数据结构体:存储缓冲区、连接信息
typedef struct {
    int fd;                  // 客户端 FD(-1 表示空闲)
    char read_buf[BUF_SIZE]; // 读缓冲区
    char write_buf[BUF_SIZE];// 写缓冲区
    int read_len;            // 已读数据长度
    struct sockaddr_in addr; // 客户端地址(日志打印用)
} IOUringClient;

// 全局变量:客户端数据管理、io_uring 实例
IOUringClient client_data[MAX_CLIENTS];
struct io_uring ring;


// 客户端初始化
void init_client(IOUringClient *cli) {
    cli->fd = -1;
    cli->read_len = 0;
    memset(cli->read_buf, 0, BUF_SIZE);
    memset(cli->write_buf, 0, BUF_SIZE);
    memset(&cli->addr, 0, sizeof(struct sockaddr_in));
}


// 查找空闲客户端槽位
int find_free_client() {
    for (int i = 0; i < MAX_CLIENTS; i++) {
        if (client_data[i].fd == -1) {
            return i;
        }
    }
    return -1;
}


// 设置套接字为非阻塞
void set_nonblocking(int fd) {
    int flags = fcntl(fd, F_GETFL, 0);
    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
        perror("fcntl设置非阻塞失败");
        close(fd);
        exit(EXIT_FAILURE);
    }
}


// 提交 accept 请求到 io_uring
void submit_accept_request(int listen_fd) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    if (!sqe) {
        perror("io_uring_get_sqe失败(accept请求)");
        return;
    }

    io_uring_prep_accept(
        sqe,                // 空闲 SQ 条目
        listen_fd,          // 监听 FD
        NULL, NULL,         // 客户端地址(后续补充)
        0                   // 标志
    );

    sqe->user_data = (uint64_t)-1; // 标记为 accept 请求
}


// 提交 read 请求到 io_uring
void submit_read_request(int client_fd, int client_idx) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    if (!sqe) {
        perror("io_uring_get_sqe失败(read请求)");
        return;
    }

    IOUringClient *cli = &client_data[client_idx];
    io_uring_prep_read(
        sqe,                // 空闲 SQ 条目
        client_fd,          // 客户端 FD
        cli->read_buf,      // 读缓冲区
        BUF_SIZE - 1,       // 读数据长度
        0                   // 偏移量
    );

    sqe->user_data = (uint64_t)client_idx; // 标记为 read 请求
}


// 提交 write 请求到 io_uring
void submit_write_request(int client_fd, int client_idx) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    if (!sqe) {
        perror("io_uring_get_sqe失败(write请求)");
        return;
    }

    IOUringClient *cli = &client_data[client_idx];
    io_uring_prep_write(
        sqe,                // 空闲 SQ 条目
        client_fd,          // 客户端 FD
        cli->write_buf,     // 写缓冲区
        cli->read_len,      // 写数据长度
        0                   // 偏移量
    );

    // 标记为 write 请求(高位标记)
    sqe->user_data = (uint64_t)client_idx | (1ULL << 30);
}


// 核心函数:处理 io_uring 完成队列(CQ)
void process_completion_queue(int listen_fd) {
    struct io_uring_cqe *cqe; // 完成队列条目
    int ret;                  // I/O 操作结果

    while (io_uring_peek_cqe(&ring, &cqe) == 0) {
        // 处理 accept 请求完成
        if (cqe->user_data == (uint64_t)-1) {
            ret = cqe->res;
            if (ret < 0) {
                printf("accept失败:%s(errno=%d)\n", strerror(-ret), -ret);
                io_uring_cqe_seen(&ring, cqe);
                submit_accept_request(listen_fd);
                continue;
            }
            int client_fd = ret;

            if (client_fd >= MAX_CLIENTS) {
                printf("客户端FD=%d 超出最大限制(%d),拒绝连接\n", client_fd, MAX_CLIENTS);
                close(client_fd);
                io_uring_cqe_seen(&ring, cqe);
                submit_accept_request(listen_fd);
                continue;
            }

            int slot = find_free_client();
            if (slot == -1) {
                printf("客户端数量达上限(%d),拒绝连接(FD=%d)\n", MAX_CLIENTS, client_fd);
                close(client_fd);
                io_uring_cqe_seen(&ring, cqe);
                submit_accept_request(listen_fd);
                continue;
            }

            IOUringClient *new_cli = &client_data[slot];
            new_cli->fd = client_fd;
            struct sockaddr_in client_addr;
            socklen_t addr_len = sizeof(client_addr);
            getsockname(client_fd, (struct sockaddr*)&client_addr, &addr_len);
            new_cli->addr = client_addr;

            submit_read_request(client_fd, slot);

            char client_ip[INET_ADDRSTRLEN];
            inet_ntop(AF_INET, &client_addr.sin_addr, client_ip, sizeof(client_ip));
            printf("新客户端连接:%s:%d(FD=%d)\n", 
                   client_ip, ntohs(client_addr.sin_port), client_fd);

            submit_accept_request(listen_fd);
        }

        // 处理 read 请求完成(无高位标记)
        else if (!(cqe->user_data & (1ULL << 30))) {
            int client_idx = (int)cqe->user_data;
            IOUringClient *cli = &client_data[client_idx];

            if (cli->fd == -1) {
                io_uring_cqe_seen(&ring, cqe);
                continue;
            }

            ret = cqe->res;
            if (ret <= 0) {
                // 区分正常断开和异常断开(与epoll版本日志一致)
                if (ret == 0) {
                    // ret=0:客户端正常断开(主动调用close)
                    printf("客户端FD=%d 正常断开\n", cli->fd);
                } else {
                    // ret<0:异常断开(如强制关闭、网络中断,错误码如ECONNRESET)
                    printf("客户端FD=%d 异常断开(%s)\n", cli->fd, strerror(-ret));
                }
                close(cli->fd);
                init_client(cli);
                io_uring_cqe_seen(&ring, cqe);
                continue;
            }

            cli->read_len = ret;
            memcpy(cli->write_buf, cli->read_buf, ret);
            printf("FD=%d 接收:%s(%d字节)→ 提交异步写\n", cli->fd, cli->read_buf, ret);
            submit_write_request(cli->fd, client_idx);

        }

        // 处理 write 请求完成(有高位标记)
        else {
            int client_idx = (int)(cqe->user_data & ~(1ULL << 30));
            IOUringClient *cli = &client_data[client_idx];

            if (cli->fd == -1) {
                io_uring_cqe_seen(&ring, cqe);
                continue;
            }

            ret = cqe->res;
            if (ret < 0) {
                // 写操作失败:通常是客户端已异常断开(如EPIPE)
                printf("客户端FD=%d 异常断开(发送失败:%s)\n", cli->fd, strerror(-ret));
                close(cli->fd);
                init_client(cli);
                io_uring_cqe_seen(&ring, cqe);
                continue;
            }

            printf("FD=%d 发送完成:%s(%d字节)→ 提交异步读\n", cli->fd, cli->write_buf, ret);
            submit_read_request(cli->fd, client_idx);
        }

        io_uring_cqe_seen(&ring, cqe);
    }
}


// 主函数:程序入口
int main() {
    int listen_fd;
    struct sockaddr_in server_addr;

    for (int i = 0; i < MAX_CLIENTS; i++) {
        init_client(&client_data[i]);
    }

    if (io_uring_queue_init(RING_ENTRIES, &ring, 0) < 0) {
        perror("io_uring_queue_init失败");
        return -1;
    }

    if ((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket创建失败");
        io_uring_queue_exit(&ring);
        return -1;
    }
    set_nonblocking(listen_fd);

    int reuse = 1;
    if (setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
        perror("setsockopt端口复用失败");
        close(listen_fd);
        io_uring_queue_exit(&ring);
        return -1;
    }

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if (bind(listen_fd, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("bind失败");
        close(listen_fd);
        io_uring_queue_exit(&ring);
        return -1;
    }

    if (listen(listen_fd, 5) < 0) {
        perror("listen失败");
        close(listen_fd);
        io_uring_queue_exit(&ring);
        return -1;
    }

    printf("异步I/O服务器(Linux io_uring)启动:端口=%d,队列大小=%d\n", PORT, RING_ENTRIES);

    submit_accept_request(listen_fd);

    while (1) {
        io_uring_submit(&ring);
        process_completion_queue(listen_fd);
        usleep(1000);
    }

    close(listen_fd);
    io_uring_queue_exit(&ring);
    return 0;
}

编译运行 (源码安装的 liburing 头文件在 /usr/local/include,库文件在 /usr/local/lib,编译时需显式指定路径): gcc -o server_io_uring server_io_uring.c -I/usr/local/include -L/usr/local/lib -luring && ./server_io_uring

编译与运行步骤:基于CentOS 8

确保已安装 liburing :若此前未安装或安装失败,根据以下命令源码安装(兼容所有 CentOS 8 版本):

bash 复制代码
# 1. 安装编译依赖(gcc、make、wget)
dnf install -y gcc make wget

# 2. 下载 liburing 2.5 源码(稳定版,兼容代码)
cd /tmp
wget https://github.com/axboe/liburing/archive/refs/tags/liburing-2.5.tar.gz

# 3. 解压并编译安装
tar -zxvf liburing-2.5.tar.gz
cd liburing-liburing-2.5
./configure
make -j4  # 4线程编译,可根据 CPU 核心数调整(如 -j8)
make install  # 安装到 /usr/local/include 和 /usr/local/lib
CentOS 8 内核升级:5.1+

io_uring 是 Linux 内核 5.1 版本才正式引入 的特性,而 CentOS 8 默认内核版本是 4.18.x,完全不支持 io_uring!即使编译成功,运行时也会报 "设备不支持" 或 "无效参数" 错误。至此需先升级 CentOS 8 内核到 5.1+,具体步骤如下

更新仓库缓存,搜索可用的内核版本

ini 复制代码
# 1. 清理仓库缓存,避免旧数据干扰
yum clean all && yum makecache

# 2. 搜索 ELRepo 仓库中的主线内核(kernel-ml,ml=mainline)
yum --enablerepo=elrepo-kernel search kernel-ml

安装最新的主线内核

ini 复制代码
yum --enablerepo=elrepo-kernel install -y kernel-ml

设置新内核为默认启动项(与之前一致)

bash 复制代码
# 1. 查看所有内核启动项,找到新安装的 kernel-ml(通常在最前面,序号为 0)
awk -F\' '$1=="menuentry " {print i++ " : " $2}' /etc/grub2.cfg

# 2. 设置默认启动项为新内核(序号 0,根据实际输出调整)
grub2-set-default 0

# 3. 生成新的 grub 配置
grub2-mkconfig -o /boot/grub2/grub.cfg

# 4. 重启服务器(必须重启才能加载新内核)
reboot

重启后确认内核版本(需 ≥5.1)

bash 复制代码
uname -r # 输出示例:5.15.102-elrepo.x86_64(确认是 5.1+ 版本)

总结:Linux Socket 五大 I/O 模型核心要点

Linux Socket 五大 I/O 模型(阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O、异步 I/O)的本质差异,均源于对 "等待数据就绪" 和 "数据拷贝" 两个核心阶段的阻塞策略与通知机制设计不同。

阻塞 I/O 因两阶段均阻塞,仅适用于单人测试服等低并发场景,代码最简单但无并发能力;非阻塞 I/O 等待阶段不阻塞却需轮询,CPU 消耗高,仅适合 10-20 人小规模高频微操作测试;I/O 多路复用(select/poll/epoll)通过单线程监听多 FD 突破并发瓶颈,其中 select 兼容旧系统但 FD 限 100,poll 无 FD 限但效率一般,epoll 依托红黑树与就绪链表实现 O (1) 事件处理,支持万人级并发,是游戏正式服(如 MMO 跨服战场)的主流选择,LT 模式简单不易出错,ET 模式效率更高但需配合非阻塞 FD 处理数据完整性;信号驱动 I/O 靠 SIGIO 信号通知,避免轮询却有信号风暴风险,游戏开发中极少用;异步 I/O 两阶段均不阻塞,资源利用率最高但兼容性差、调试难,仅适合极致性能需求且有成熟运维支持的场景。

相关推荐
expect7g2 小时前
COW、MOR、MOW
大数据·数据库·后端
程序员小假2 小时前
我们来说说当一个线程两次调用 start() 方法会出现什么情况?
java·后端
bobz9652 小时前
I/O复用 select、poll、epoll
后端
无限大62 小时前
一文读懂HTTP 1.1/2.0/3.0:从原理到应用的通俗解析
后端·面试
SimonKing3 小时前
Archery:开源、一站式的数据库 SQL 审核与运维平台
java·后端·程序员
AI小智3 小时前
为了帮我搞定旅行清单:我的小白老婆报名了30万奖金的黑客松!
后端
双向333 小时前
RTX 4090助力深度学习:从PyTorch到生产环境的完整实践指南
后端
shengjk13 小时前
Java vs Python Web 服务器深度对比:从传统到现代的演进之路
后端
绝无仅有3 小时前
某辅导教育大厂真实面试过程与经验总结
后端·面试·架构