在网络编程的演进之路中,我们从单进程只能处理一个客户端连接,逐步迭代到多进程、多线程模型。但随着并发需求的激增,多进程的资源开销、多线程的调度成本逐渐成为性能瓶颈。此时,"高级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通过"信号通知"替代轮询:
-
程序先注册一个信号处理函数,并发起信号驱动IO调用(如sigaction);
-
内核在数据准备好后,向进程发送一个信号(如SIGIO);
-
进程收到信号后,在信号处理函数中执行数据拷贝操作。
你可以类比成:"像钓鱼时用一个鱼漂传感器,鱼上钩时传感器发出警报,你不需要一直盯着,期间可以玩手机、吃饭,收到警报再提竿"。
核心特点
-
等待阶段不阻塞,也无需轮询,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实现一个支持多客户端并发查询的简易服务器,核心逻辑如下:
-
服务器启动后,加载字典数据(如单词-翻译映射);
-
用select监听监听FD和所有客户端FD;
-
新客户端连接时,将其FD加入select集合;
-
客户端发送单词时,服务器查询字典并返回翻译;
-
客户端断开时,移除其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:cppstruct 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:事件结构体:cppstruct 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的知识脉络,在面试或开发高性能服务时,能对你有帮助,如果有帮助到你的话,点点赞和收藏。如果有不同意见或看法,欢迎在评论区留言。