深入理解高级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的知识脉络,在面试或开发高性能服务时,能对你有帮助,如果有帮助到你的话,点点赞和收藏。如果有不同意见或看法,欢迎在评论区留言。

相关推荐
广东大榕树信息科技有限公司1 小时前
国产化动环监控系统在数据中心安全中的作用
网络·物联网·国产动环监控系统·动环监控系统
zhouyunjian1 小时前
10-ScheduledThreadPool应用与源码分析
运维·服务器·数据库
Lyre丶1 小时前
ginan入门初探
linux·经验分享·学习·ubuntu
热爱编程的OP1 小时前
Linux进程信号
linux
小兔薯了1 小时前
10.VSFTPD 服务器
运维·服务器
Zeku1 小时前
20251125 - 韦东山Linux第三篇笔记【下】
linux·驱动开发·嵌入式硬件
小yu爱学习1 小时前
2026大专区块链技术应用专业考什么证?
运维·服务器·区块链
XH-hui1 小时前
【打靶日记】VluNyx 之 Hat
linux·网络安全·vulnyx
Boilermaker19921 小时前
[网络编程] TCP/IP 模型概览
网络