深入理解高级IO:从模型到实战,实现高性能并发服务器

在网络编程的演进之路中,我们从单进程只能处理一个客户端连接,逐步迭代到多进程、多线程模型。但随着并发需求的激增,多进程的资源开销、多线程的调度成本逐渐成为性能瓶颈。此时,"高级IO" 应运而生,它通过优化IO的等待与数据拷贝过程,让单个进程/线程就能高效处理成千上万的并发连接,成为高性能服务器的核心技术基石。

本文将结合高级IO的核心知识点,从本质出发,详解五大IO模型,重点拆解IO多路转接技术(select/poll/EPOLL),补充核心函数及参数说明,并附上可落地的伪代码,帮助你彻底掌握高级IO的设计思想与实践方法。

一、重新理解IO:不止于文件读写

在学习高级IO之前,我们首先要打破一个误区:"IO≠文件操作"。

1. IO的本质定义

IO(Input/Output)本质是"输入/输出"的抽象,指程序与外部设备(磁盘、网卡、管道等)之间的数据交互。常见的IO场景包括:

  • 磁盘文件的读写(read/write);

  • 进程间通信(管道、消息队列的读写);

  • 网络数据的收发(网卡接收/发送数据包)。

2. IO的核心痛点:低效的根源

所有IO操作都包含两个核心阶段,这也是其低效的根本原因:

  • 等待阶段:程序等待外部设备准备好数据(比如read网络数据时,等待客户端发送数据到达内核缓冲区);

  • 拷贝阶段:数据从内核态缓冲区拷贝到用户态程序缓冲区(内核态是操作系统核心运行区域,用户态是应用程序运行区域,两者的切换与数据拷贝存在开销)。

以阻塞read为例:当内核缓冲区没有数据时,程序会一直阻塞在等待阶段;直到数据到达后,再执行拷贝操作,整个过程程序无法做其他事情------这就是传统IO的低效所在。

高级IO的核心目标,就是"优化这两个阶段的耗时占比",减少等待时间、避免无效拷贝,从而提升并发处理能力。

二、五大IO模型深度解析(含伪代码)

根据程序在"等待阶段"和"拷贝阶段"的参与方式,IO模型分为五大类。我会结合生动例子,逐一拆解其原理、优缺点与实践伪代码。

1. 阻塞IO(BIO:Blocking IO)------"守株待兔"式IO

原理

阻塞IO是最基础、默认的IO模型。在数据准备好之前,系统调用(如read)会一直阻塞,直到数据准备完成并拷贝到用户态后才返回。

你可以类比成:"像一个人拿一根鱼竿钓鱼,必须时刻盯着水面,鱼不上钩就一直等,什么也做不了"。

核心特点

  • 等待阶段会阻塞进程/线程,期间无法处理其他任务;

  • 所有套接字(socket)默认都是阻塞模式;

  • 实现简单,但并发能力极差(一个连接占用一个进程/线程)。

伪代码实现(网络读取示例)

cpp 复制代码
// 创建socket(默认阻塞模式)
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);

// 阻塞等待客户端连接(accept是阻塞调用)
int clientfd = accept(sockfd, NULL, NULL);

// 阻塞等待数据(read是阻塞调用)
char buf[1024];
ssize_t n = read(clientfd, buf, sizeof(buf)); // 无数据时阻塞在这里

// 处理数据
process_data(buf, n);
close(clientfd);

2. 非阻塞IO(NIO:Non-blocking IO)------"频繁追问"式IO

原理

非阻塞IO通过设置文件描述符(FD)为非阻塞模式,让系统调用(如read)无论数据是否准备好,都会立即返回:

  • 数据未准备好时,返回错误码(如Linux的EAGAIN);

  • 数据准备好时,立即执行拷贝并返回数据长度。

程序需要通过"轮询(循环)" 不断调用系统调用,直到获取数据------相当于"钓鱼时每隔1秒就提一次鱼竿,不管有没有鱼"。

核心特点

  • 等待阶段不阻塞,但需要轮询检查数据是否就绪;

  • 轮询会占用大量CPU资源,适合短连接、数据频繁到达的场景;

  • 并发能力比阻塞IO强,但CPU开销是硬伤。

伪代码实现(网络读取示例)

cpp 复制代码
// 创建socket并设置为非阻塞模式
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
fcntl(sockfd, F_SETFL, O_NONBLOCK); // 设置非阻塞
bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sockfd, 5);

int clientfd = accept(sockfd, NULL, NULL);
fcntl(clientfd, F_SETFL, O_NONBLOCK); // 客户端FD也设为非阻塞

char buf[1024];
while (1) {
    // 非阻塞read,立即返回
    ssize_t n = read(clientfd, buf, sizeof(buf));
    if (n > 0) {
        // 数据就绪,处理数据
        process_data(buf, n);
        break;
    } else if (n == -1 && errno == EAGAIN) {
        // 数据未就绪,继续轮询(此处可加入短暂延迟减少CPU占用)
        usleep(1000); // 休眠1毫秒
        continue;
    } else {
        // 其他错误或客户端关闭连接
        close(clientfd);
        break;
    }
}

3. 信号驱动IO(SIO:Signal-driven IO)------"通知到位"式IO

原理

信号驱动IO通过"信号通知"替代轮询:

  1. 程序先注册一个信号处理函数,并发起信号驱动IO调用(如sigaction);

  2. 内核在数据准备好后,向进程发送一个信号(如SIGIO);

  3. 进程收到信号后,在信号处理函数中执行数据拷贝操作。

你可以类比成:"像钓鱼时用一个鱼漂传感器,鱼上钩时传感器发出警报,你不需要一直盯着,期间可以玩手机、吃饭,收到警报再提竿"。

核心特点

  • 等待阶段不阻塞,也无需轮询,CPU利用率高;

  • 依赖信号机制,信号处理逻辑复杂(如信号丢失、并发信号处理);

  • 适合数据到达不频繁、对延迟不敏感的场景。

伪代码实现(网络读取示例)

cpp 复制代码
// 信号处理函数:数据就绪后内核触发此函数,执行拷贝
void sigio_handler(int sig) {
    char buf[1024];
    ssize_t n = read(clientfd, buf, sizeof(buf)); // 拷贝数据
    process_data(buf, n);
}

int main() {
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(sockfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(sockfd, 5);

    int clientfd = accept(sockfd, NULL, NULL);

    // 注册信号处理函数
    struct sigaction sa;
    sa.sa_handler = sigio_handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sigaction(SIGIO, &sa, NULL);

    // 设置客户端FD的所有者为当前进程,让内核知道向哪个进程发信号
    fcntl(clientfd, F_SETOWN, getpid());
    // 启用信号驱动IO
    int flags = fcntl(clientfd, F_GETFL);
    fcntl(clientfd, F_SETFL, flags | O_ASYNC);

    // 等待阶段:进程可以做其他事情
    while (1) {
        printf("等待数据中,期间可处理其他任务...\n");
        sleep(1);
    }

    close(clientfd);
    close(sockfd);
    return 0;
}

4. IO多路转接(IO Multiplexing)------"一人多竿"式IO

原理

IO多路转接是高级IO的核心,也是高性能服务器的首选方案。它允许"单个进程/线程同时监听多个文件描述符(FD)",通过一个系统调用(如select/poll/EPOLL)等待多个FD中的任意一个就绪,再集中处理就绪的FD。

你可以类比成:"像一个人同时拿着100根鱼竿钓鱼,通过一个'鱼情监控器'观察所有鱼竿的状态,只要有鱼竿上鱼(FD就绪),就去处理那根鱼竿"------既避免了阻塞IO的并发不足,又解决了非阻塞IO的CPU浪费。

核心特点

  • 单个进程/线程处理多个并发连接,资源开销极低;

  • 等待阶段由内核监听FD,无需程序轮询,CPU利用率高;

  • 支持海量并发(EPOLL可支持数十万连接);

  • 核心系统调用:select(基础)→ poll(优化)→ EPOLL(高性能)。

三种实现对比

|-----------|---------------|-------------------|-------------------|
| 特性 | select | poll | EPOLL(Linux专属) |
| FD数量限制 | 最大1024(内核宏定义) | 无上限(用户自定义数组) | 无上限(动态扩容) |
| 数据结构 | 位图(fd_set) | 数组(struct pollfd) | 红黑树+就绪链表 |
| 用户态→内核态拷贝 | 每次调用拷贝整个集合 | 每次调用拷贝整个数组 | 仅拷贝就绪FD(零拷贝) |
| 就绪FD遍历 | 遍历所有FD(O(n)) | 遍历所有FD(O(n)) | 仅遍历就绪FD(O(1)) |
| 触发模式 | 水平触发(LT) | 水平触发(LT) | 水平触发(LT)+边缘触发(ET) |

伪代码实现(重点:select与EPOLL)

(1)select实现:基础版多路转接

cpp 复制代码
#define MAX_FD 1024

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listenfd, 5);

    // 定义两个集合:master(全局FD集合)、readfds(临时集合,传给select)
    fd_set master_set, read_set;
    FD_ZERO(&master_set); // 清空集合
    FD_SET(listenfd, &master_set); // 添加监听FD到集合
    int max_fd = listenfd; // 记录最大FD(select参数需要)

    while (1) {
        // 每次调用select前,复制master集合到read_set(select会修改传入的集合)
        read_set = master_set;

        // 阻塞等待FD就绪(超时设为NULL表示永久阻塞)
        int ready = select(max_fd + 1, &read_set, NULL, NULL, NULL);
        if (ready == -1) {
            perror("select error");
            break;
        }

        // 遍历所有FD,判断是否就绪
        for (int i = 0; i <= max_fd; i++) {
            if (FD_ISSET(i, &read_set)) { // 该FD就绪
                if (i == listenfd) {
                    // 监听FD就绪:有新客户端连接
                    int clientfd = accept(listenfd, NULL, NULL);
                    FD_SET(clientfd, &master_set); // 添加客户端FD到集合
                    if (clientfd > max_fd) {
                        max_fd = clientfd; // 更新最大FD
                    }
                    printf("新客户端连接:clientfd=%d\n", clientfd);
                } else {
                    // 客户端FD就绪:有数据可读
                    char buf[1024];
                    ssize_t n = read(i, buf, sizeof(buf));
                    if (n > 0) {
                        process_data(buf, n);
                    } else {
                        // 客户端关闭连接,移除FD
                        FD_CLR(i, &master_set);
                        close(i);
                        printf("客户端断开:clientfd=%d\n", i);
                    }
                }
            }
        }
    }

    close(listenfd);
    return 0;
}

(2)EPOLL实现:高性能多路转接(推荐)

EPOLL是Linux专属的高性能IO多路转接技术,通过红黑树管理FD集合,就绪链表存储就绪FD,实现O(1)的就绪FD查询,支持水平触发(LT)和边缘触发(ET)。

cpp 复制代码
#define MAX_EVENTS 1024

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listenfd, 5);

    // 1. 创建EPOLL实例(返回EPOLL文件描述符)
    int epollfd = epoll_create1(0);
    if (epollfd == -1) {
        perror("epoll_create1 error");
        exit(1);
    }

    // 2. 注册监听FD的读事件
    struct epoll_event ev;
    ev.events = EPOLLIN; // 监听读事件
    ev.data.fd = listenfd; // 绑定FD到事件
    epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); // 添加事件

    // 3. 等待事件就绪
    struct epoll_event events[MAX_EVENTS];
    while (1) {
        // 阻塞等待就绪事件(返回就绪事件数量)
        int ready = epoll_wait(epollfd, events, MAX_EVENTS, -1);
        if (ready == -1) {
            perror("epoll_wait error");
            break;
        }

        // 4. 处理就绪事件
        for (int i = 0; i < ready; i++) {
            int fd = events[i].data.fd;
            if (fd == listenfd) {
                // 新客户端连接
                int clientfd = accept(listenfd, NULL, NULL);
                // 设置客户端FD为非阻塞(配合ET模式)
                fcntl(clientfd, F_SETFL, O_NONBLOCK);
                // 注册客户端FD的读事件(ET模式:EPOLLIN | EPOLLET)
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = clientfd;
                epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &ev);
                printf("新客户端连接:clientfd=%d\n", clientfd);
            } else {
                // 客户端数据可读
                char buf[1024];
                ssize_t n;
                // ET模式:一次性读取完所有数据
                while ((n = read(fd, buf, sizeof(buf))) > 0) {
                    process_data(buf, n);
                }
                if (n == -1 && errno != EAGAIN) {
                    // 读取错误或客户端关闭
                    epoll_ctl(epollfd, EPOLL_CTL_DEL, fd, NULL); // 移除事件
                    close(fd);
                    printf("客户端断开:clientfd=%d\n", fd);
                }
            }
        }
    }

    close(epollfd);
    close(listenfd);
    return 0;
}

5. 异步IO(AIO:Asynchronous IO)------"全程托管"式IO

原理

异步IO是最彻底的"非阻塞"模型:程序发起IO调用后,直接返回,内核会"全程托管"等待阶段和拷贝阶段,直到数据完全拷贝到用户态缓冲区后,通过信号或回调通知程序"操作完成"。

与信号驱动IO的区别:信号驱动IO的信号通知"数据就绪"(需要程序自己拷贝),异步IO的信号通知"操作完成"(内核已完成拷贝)------相当于"钓鱼时直接委托渔童,渔童钓上鱼后处理干净再给你,你全程不用管"。

核心特点

  • 程序不参与等待和拷贝,完全由内核处理,并发能力最强;

  • 实现复杂,依赖内核异步支持(如Linux的io_uring、Windows的IOCP);

  • 适合对延迟敏感、高并发的场景(如分布式存储、高性能数据库)。

伪代码实现(Linux io_uring示例)

cpp 复制代码
#include <liburing.h>

#define BUF_SIZE 1024

int main() {
    struct io_uring ring;
    // 初始化io_uring实例
    io_uring_queue_init(32, &ring, 0);

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listenfd, 5);

    // 发起异步accept请求
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_accept(sqe, listenfd, NULL, NULL, 0);
    io_uring_submit(&ring);

    while (1) {
        struct io_uring_cqe *cqe;
        // 等待异步操作完成
        io_uring_wait_cqe(&ring, &cqe);

        if (cqe->res >= 0) {
            int clientfd = cqe->res;
            printf("新客户端连接:clientfd=%d\n", clientfd);

            // 发起异步read请求
            char *buf = malloc(BUF_SIZE);
            sqe = io_uring_get_sqe(&ring);
            io_uring_prep_read(sqe, clientfd, buf, BUF_SIZE, 0);
            // 将缓冲区地址绑定到请求,方便后续处理
            io_uring_sqe_set_data(sqe, buf);
            io_uring_submit(&ring);
        } else if (cqe->res == -EINVAL) {
            // 异步read完成
            char *buf = io_uring_cqe_get_data(cqe);
            ssize_t n = -cqe->res;
            if (n > 0) {
                process_data(buf, n);
            }
            free(buf);
            close(cqe->fd);
        }

        // 标记完成的请求
        io_uring_cqe_seen(&ring, cqe);
    }

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

三、实战:用select实现简易字典服务器

以"字典服务器"为案例,我们用select实现一个支持多客户端并发查询的简易服务器,核心逻辑如下:

  1. 服务器启动后,加载字典数据(如单词-翻译映射);

  2. 用select监听监听FD和所有客户端FD;

  3. 新客户端连接时,将其FD加入select集合;

  4. 客户端发送单词时,服务器查询字典并返回翻译;

  5. 客户端断开时,移除其FD并关闭连接。

核心伪代码

cpp 复制代码
// 加载字典数据(简化为哈希表)
typedef struct {
    char word[32];
    char trans[128];
} Dict;
Dict dict[] = {{"hello", "你好"}, {"world", "世界"}, {"io", "输入输出"}};
int dict_size = sizeof(dict) / sizeof(Dict);

// 查询字典
char* dict_query(const char* word) {
    for (int i = 0; i < dict_size; i++) {
        if (strcmp(word, dict[i].word) == 0) {
            return dict[i].trans;
        }
    }
    return "未找到该单词";
}

int main() {
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in addr = {AF_INET, htons(8080), INADDR_ANY};
    bind(listenfd, (struct sockaddr*)&addr, sizeof(addr));
    listen(listenfd, 5);

    fd_set master_set, read_set;
    FD_ZERO(&master_set);
    FD_SET(listenfd, &master_set);
    int max_fd = listenfd;

    while (1) {
        read_set = master_set;
        int ready = select(max_fd + 1, &read_set, NULL, NULL, NULL);
        if (ready == -1) break;

        for (int i = 0; i <= max_fd; i++) {
            if (FD_ISSET(i, &read_set)) {
                if (i == listenfd) {
                    // 新客户端连接
                    int clientfd = accept(listenfd, NULL, NULL);
                    FD_SET(clientfd, &master_set);
                    max_fd = clientfd > max_fd ? clientfd : max_fd;
                    printf("客户端连接:clientfd=%d\n", clientfd);
                } else {
                    // 读取客户端单词
                    char buf[32];
                    ssize_t n = read(i, buf, sizeof(buf)-1);
                    if (n <= 0) {
                        // 客户端断开
                        FD_CLR(i, &master_set);
                        close(i);
                        printf("客户端断开:clientfd=%d\n", i);
                        continue;
                    }
                    buf[n] = '\0'; // 字符串结束符
                    printf("收到查询:%s\n", buf);

                    // 查询并返回结果
                    char* trans = dict_query(buf);
                    write(i, trans, strlen(trans));
                }
            }
        }
    }

    close(listenfd);
    return 0;
}

四、高级IO核心函数及参数详解

1. 基础IO与套接字函数

(1)socket():创建套接字

int socket(int domain, int type, int protocol);

  • 参数说明

    • domain:地址族,如AF_INET(IPv4)、AF_INET6(IPv6);

    • type:套接字类型,SOCK_STREAM(TCP)、SOCK_DGRAM(UDP);

    • protocol:协议类型,通常设为0(自动匹配type对应的默认协议);

  • 返回值:成功返回套接字文件描述符(FD),失败返回-1;

  • 注意:默认创建的套接字为阻塞模式。

(2)bind():绑定地址与端口

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

  • 参数说明

    • sockfd:socket()返回的FD;

    • addr:指向sockaddr结构体的指针(需强转为对应类型,如sockaddr_in);

    • addrlen:addr结构体的长度;

  • 返回值:成功返回0,失败返回-1。

(3)listen():监听套接字

int listen(int sockfd, int backlog);

  • 参数说明

    • sockfd:已绑定的套接字FD;

    • backlog:半连接队列最大长度(内核会根据系统调整,如Linux默认为128);

  • 返回值:成功返回0,失败返回-1。

(4)accept():接受客户端连接

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

  • 参数说明

    • sockfd:监听套接字FD;

    • addr:输出参数,存储客户端的地址信息(可设为NULL);

    • addrlen:输入输出参数,传入addr的长度,返回实际地址长度(可设为NULL);

  • 返回值:成功返回客户端套接字FD,失败返回-1;

  • 注意:阻塞模式下会等待客户端连接,非阻塞模式下无连接时返回-1且errno=EAGAIN。

(5)read()/write():读写数据

ssize_t read(int fd, void *buf, size_t count); ssize_t write(int fd, const void *buf, size_t count);

  • 参数说明

    • fd:文件/套接字FD;

    • buf:数据缓冲区;

    • count:要读取/写入的字节数;

  • 返回值

    • read:成功返回读取的字节数,0表示EOF(客户端断开),失败返回-1;

    • write:成功返回写入的字节数,失败返回-1;

  • 非阻塞模式:无数据可读时,read返回-1且errno=EAGAIN。

2. 非阻塞模式设置函数:fcntl()

int fcntl(int fd, int cmd, ... /* arg */ );

  • 核心用途:修改文件描述符的属性(如设置非阻塞、信号驱动IO);

  • 常用参数

    • fd:目标FD;

    • cmd:操作指令,核心有:

      • F_GETFL:获取FD的当前属性;

      • F_SETFL:设置FD的属性;

      • F_SETOWN:设置FD的所有者进程/线程(信号驱动IO用);

    • arg:配合cmd的参数,如O_NONBLOCK(非阻塞)、O_ASYNC(信号驱动IO);

  • 示例:设置FD为非阻塞

    int flags = fcntl(fd, F_GETFL); // 获取当前属性 fcntl(fd, F_SETFL, flags | O_NONBLOCK); // 追加非阻塞属性

  • 返回值:成功返回对应结果(如F_GETFL返回属性值),失败返回-1。

3. IO多路转接核心函数

(1)select()

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

  • 参数说明

    • nfds:监听的最大FD+1(内核仅遍历0~nfds-1的FD);

    • readfds:监听读事件的FD集合(如客户端连接、数据到达);

    • writefds:监听写事件的FD集合(如缓冲区可写);

    • exceptfds:监听异常事件的FD集合;

    • timeout:超时时间,NULL表示永久阻塞,struct timeval{tv_sec, tv_usec}表示超时秒/微秒;

  • 辅助宏

    • FD_ZERO(fd_set *set):清空集合;

    • FD_SET(int fd, fd_set *set):将FD加入集合;

    • FD_CLR(int fd, fd_set *set):将FD移出集合;

    • FD_ISSET(int fd, fd_set *set):判断FD是否在就绪集合中;

  • 返回值:成功返回就绪的FD数量,超时返回0,失败返回-1;

  • 注意:select会修改传入的fd_set,每次调用前需重新拷贝集合。

(2)poll()

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

  • 参数说明

    • fds:pollfd结构体数组,每个元素对应一个监听的FD:

      cpp 复制代码
      struct pollfd {
          int fd;         // 目标FD
          short events;   // 要监听的事件(如POLLIN:读事件、POLLOUT:写事件)
          short revents;  // 内核返回的就绪事件(输出参数)
      };
    • nfds:fds数组的长度;

    • timeout:超时时间(毫秒),-1表示永久阻塞,0表示立即返回;

  • 返回值:成功返回就绪的FD数量,超时返回0,失败返回-1。

(3)epoll核心函数(Linux专属)

① epoll_create1():创建epoll实例

int epoll_create1(int flags);

  • 参数说明

    • flags:创建标志,0表示默认,EPOLL_CLOEXEC表示进程执行exec时关闭epoll FD;
  • 返回值:成功返回epoll FD,失败返回-1;

  • 注意:旧版epoll_create(int size)已废弃,size参数无实际意义。

② epoll_ctl():管理epoll事件

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

  • 参数说明

    • epfd:epoll_create1()返回的FD;

    • op:操作类型:

      • EPOLL_CTL_ADD:添加FD及事件到epoll;

      • EPOLL_CTL_MOD:修改已注册FD的事件;

      • EPOLL_CTL_DEL:从epoll中移除FD;

    • fd:要管理的目标FD;

    • event:事件结构体:

      cpp 复制代码
      struct epoll_event {
          uint32_t events; // 监听的事件(如EPOLLIN、EPOLLET)
          epoll_data_t data; // 自定义数据(通常存FD)
      };
      typedef union epoll_data {
          void *ptr;
          int fd;
          uint32_t u32;
          uint64_t u64;
      } epoll_data_t;
  • 核心事件标志

    • EPOLLIN:读事件就绪;

    • EPOLLOUT:写事件就绪;

    • EPOLLET:边缘触发(ET)模式;

    • EPOLLONESHOT:仅触发一次事件,需重新注册;

  • 返回值:成功返回0,失败返回-1。

③ epoll_wait():等待事件就绪

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

  • 参数说明

    • epfd:epoll FD;

    • events:输出参数,存储就绪的事件数组;

    • maxevents:events数组的最大长度;

    • timeout:超时时间(毫秒),-1表示永久阻塞;

  • 返回值:成功返回就绪的事件数量,超时返回0,失败返回-1。

4. 异步IO函数(io_uring)

(1)io_uring_queue_init():初始化io_uring实例

int io_uring_queue_init(unsigned int entries, struct io_uring *ring, unsigned int flags);

  • 参数说明

    • entries:提交队列(SQ)的容量;

    • ring:io_uring结构体指针;

    • flags:初始化标志,0为默认;

  • 返回值:成功返回0,失败返回-1。

(2)io_uring_get_sqe():获取提交队列项

struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);

  • 参数ring:io_uring实例;

  • 返回值:成功返回SQE指针,失败返回NULL。

(3)io_uring_submit():提交请求到内核

int io_uring_submit(struct io_uring *ring);

  • 返回值:成功返回提交的请求数,失败返回-1。

(4)io_uring_wait_cqe():等待完成队列项

int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);

  • 参数cqe_ptr:输出参数,指向完成的请求;

  • 返回值:成功返回0,失败返回-1。

五、IO模型选型与面试重点

1. 选型建议

|-------------------|---------------|
| 场景 | 推荐IO模型 |
| 简单服务、并发量低(<1000) | 阻塞IO(BIO) |
| 短连接、数据频繁到达 | 非阻塞IO(NIO) |
| 数据到达不频繁、CPU敏感 | 信号驱动IO(SIO) |
| 高并发服务器(>10000连接) | IO多路转接(EPOLL) |
| 极致性能、延迟敏感 | 异步IO(AIO) |

2. 面试高频考点

  • IO的两个阶段(等待+拷贝)及高级IO的优化方向;

  • select/poll/EPOLL的区别(FD限制、拷贝开销、遍历效率);

  • EPOLL的水平触发(LT)与边缘触发(ET)的区别及使用场景;

    • LT:FD就绪后,只要数据未处理完,每次epoll_wait都会通知(容错高,易实现);

    • ET:FD就绪后仅通知一次,需一次性处理完所有数据(效率高,需配合非阻塞IO);

  • 为什么EPOLL比select高效?(红黑树管理FD、就绪链表减少遍历、仅拷贝就绪FD);

  • 异步IO与信号驱动IO的核心区别(信号驱动通知"就绪",异步IO通知"完成")。

六、总结

高级IO的核心是"优化IO的等待与拷贝成本",而IO多路转接(尤其是EPOLL)凭借"单进程处理海量并发"的优势,成为高性能服务器的标配技术(如Nginx、Redis均基于EPOLL实现)。

掌握高级IO不仅要理解模型原理,更要吃透核心函数的参数与使用场景------本文补充的函数详解可作为实战手册,结合伪代码落地练习,快速熟悉这些原理。

希望文章能帮助你理清高级IO的知识脉络,在面试或开发高性能服务时,能对你有帮助,如果有帮助到你的话,点点赞和收藏。如果有不同意见或看法,欢迎在评论区留言。

相关推荐
blasit5 小时前
笔记:Qt C++建立子线程做一个socket TCP常连接通信
c++·qt·tcp/ip
YuMiao10 小时前
gstatic连接问题导致Google Gemini / Studio页面乱码或图标缺失问题
服务器·网络协议
chlk1231 天前
Linux文件权限完全图解:读懂 ls -l 和 chmod 755 背后的秘密
linux·操作系统
舒一笑1 天前
Ubuntu系统安装CodeX出现问题
linux·后端
改一下配置文件1 天前
Ubuntu24.04安装NVIDIA驱动完整指南(含Secure Boot解决方案)
linux
BingoGo1 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php
JaguarJack2 天前
当你的 PHP 应用的 API 没有限流时会发生什么?
后端·php·服务端
深紫色的三北六号2 天前
Linux 服务器磁盘扩容与目录迁移:rsync + bind mount 实现服务无感迁移(无需修改配置)
linux·扩容·服务迁移
SudosuBash2 天前
[CS:APP 3e] 关于对 第 12 章 读/写者的一点思考和题解 (作业 12.19,12.20,12.21)
linux·并发·操作系统(os)
哈基咪怎么可能是AI2 天前
为什么我就想要「线性历史 + Signed Commits」GitHub 却把我当猴耍 🤬🎙️
linux·github