Linux 五种 IO 模型技术文章
io_uring 是 Linux 内核 4.18 引入的新一代异步 I/O 框架,旨在提供比传统 AIO 更好的性能和易用性。它使用共享环形缓冲区在用户空间和内核空间之间传递 I/O 请求和完成事件。本文详细介绍了 Linux 系统中的五种 I/O 模型:阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。每种模型都有其优缺点和适用场景,选择合适的 I/O 模型对于构
1. 引言
在 Linux 系统中,I/O 操作是程序与外部设备交互的基础。理解并正确使用不同的 I/O 模型对于构建高性能应用至关重要。本文将详细介绍 Linux 系统中的五种 I/O 模型:阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O,并深入分析它们的工作原理、性能特点和适用场景。此外,我们还将探讨最新的 io_uring 技术及其在现代 Linux 系统中的应用。
2. I/O 模型基础概念
2.1 I/O 操作的本质
I/O 操作本质上是数据在用户空间和内核空间之间的传输过程。一个完整的 I/O 操作通常包括两个阶段:
- 等待数据准备阶段:内核等待 I/O 设备准备好数据
- 数据传输阶段:数据从内核空间复制到用户空间
不同 I/O 模型的主要区别在于如何处理这两个阶段。
2.2 同步与异步 I/O
- 同步 I/O:进程主动等待 I/O 操作完成
- 异步 I/O:进程不等待 I/O 操作完成,由内核在操作完成后通知进程
2.3 阻塞与非阻塞 I/O
- 阻塞 I/O:I/O 操作期间进程被挂起,无法执行其他任务
- 非阻塞 I/O:I/O 操作立即返回,进程可以继续执行其他任务
3. 五种 I/O 模型详解
3.1 阻塞 I/O 模型
阻塞 I/O 是最简单也是最常用的 I/O 模型。在这种模型中,当进程调用 I/O 操作时,会一直阻塞直到数据传输完成。
3.1.1 工作原理
- 进程调用 recvfrom 系统调用
- 内核开始准备数据(等待数据从网络到达)
- 数据准备好后,内核将数据从内核空间复制到用户空间
- recvfrom 返回,进程继续执行
3.1.2 代码示例
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 接受连接(阻塞)
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 接收数据(阻塞)
ssize_t valread = recv(new_socket, buffer, 1024, 0);
printf("%s\n", buffer);
return 0;
}
3.1.3 优缺点分析
优点:
- 实现简单,易于理解
- 资源消耗低,在连接数较少时效率高
缺点:
- 在高并发场景下性能较差
- 一个线程只能处理一个连接
3.2 非阻塞 I/O 模型
非阻塞 I/O 模型中,当进程调用 I/O 操作时,如果数据未准备好,系统调用会立即返回一个错误,而不是阻塞进程。
3.2.1 工作原理
- 进程将 socket 设置为非阻塞模式
- 调用 recvfrom 系统调用
- 如果数据未准备好,返回EAGAIN 或 EWOULDBLOCK 错误
- 进程可以执行其他任务,稍后再次调用 recvfrom
- 当数据准备好后,内核将数据复制到用户空间
- recvfrom 返回接收的字节数
3.2.2 代码示例
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[1024] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 设置socket为非阻塞模式
int flags = fcntl(server_fd, F_GETFL, 0);
fcntl(server_fd, F_SETFL, flags | O_NONBLOCK);
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
while (1) {
// 接受连接(非阻塞)
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
// 如果没有连接请求,继续轮询
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 可以执行其他任务
printf("No connection request, doing other tasks...\n");
sleep(1);
continue;
} else {
perror("accept");
exit(EXIT_FAILURE);
}
}
// 设置新连接为非阻塞模式
flags = fcntl(new_socket, F_GETFL, 0);
fcntl(new_socket, F_SETFL, flags | O_NONBLOCK);
// 接收数据(非阻塞)
ssize_t valread;
while (1) {
valread = recv(new_socket, buffer, 1024, 0);
if (valread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据未准备好,稍后再试
printf("Data not ready, trying again...\n");
sleep(1);
continue;
} else {
perror("recv");
break;
}
} else if (valread == 0) {
// 连接关闭
printf("Connection closed\n");
break;
} else {
// 处理数据
printf("Received: %s\n", buffer);
break;
}
}
close(new_socket);
}
return 0;
}
3.2.3 优缺点分析
优点:
- 进程可以同时处理多个 I/O 操作
- 不会长时间阻塞,响应性较好
缺点:
- 需要轮询检查,浪费 CPU 资源
- 实现复杂,容易出错
3.3 I/O 多路复用模型
I/O 多路复用模型允许单个进程同时监控多个文件描述符,通过select、poll 或 epoll 等系统调用来等待一个或多个文件描述符就绪。
3.3.1 工作原理
- 进程将需要监控的文件描述符添加到 select/poll/epoll 集合中
- 调用 select/poll/epoll 系统调用,阻塞等待就绪的文件描述符
- 当有文件描述符就绪时,系统调用返回就绪的文件描述符集合
- 进程对就绪的文件描述符进行 I/O 操作
3.3.2 select 模型
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/select.h>
#include <unistd.h>
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
fd_set readfds;
int client_socket[MAX_CLIENTS] = {0};
int max_sd;
while (1) {
// 清空文件描述符集合
FD_ZERO(&readfds);
// 添加服务器socket到集合
FD_SET(server_fd, &readfds);
max_sd = server_fd;
// 添加客户端socket到集合
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_socket[i];
if (sd > 0)
FD_SET(sd, &readfds);
if (sd > max_sd)
max_sd = sd;
}
// 等待活动的文件描述符
int activity = select(max_sd + 1, &readfds, NULL, NULL, NULL);
if ((activity < 0) && (errno != EINTR)) {
perror("select error");
}
// 检查服务器socket是否有活动(新连接)
if (FD_ISSET(server_fd, &readfds)) {
if ((new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
exit(EXIT_FAILURE);
}
// 通知新连接
printf("New connection, socket fd: %d\n", new_socket);
// 添加新连接到客户端数组
for (int i = 0; i < MAX_CLIENTS; i++) {
if (client_socket[i] == 0) {
client_socket[i] = new_socket;
printf("Adding to list of sockets as %d\n", i);
break;
}
}
}
// 检查客户端socket活动
for (int i = 0; i < MAX_CLIENTS; i++) {
int sd = client_socket[i];
if (FD_ISSET(sd, &readfds)) {
// 接收数据
ssize_t valread = recv(sd, buffer, BUFFER_SIZE, 0);
if (valread == 0) {
// 客户端关闭连接
getpeername(sd, (struct sockaddr*)&address, (socklen_t*)&addrlen);
printf("Host disconnected, ip: %s, port: %d\n",
inet_ntoa(address.sin_addr), ntohs(address.sin_port));
close(sd);
client_socket[i] = 0;
} else if (valread > 0) {
// 处理数据
buffer[valread] = '\0';
printf("Received: %s\n", buffer);
}
}
}
}
return 0;
}
3.3.3 epoll 模型
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#define MAX_EVENTS 10
#define BUFFER_SIZE 1024
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
char buffer[BUFFER_SIZE] = {0};
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 创建epoll实例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1) {
perror("epoll_create1");
exit(EXIT_FAILURE);
}
// 添加服务器socket到epoll
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = server_fd;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &event) == -1) {
perror("epoll_ctl: server_fd");
exit(EXIT_FAILURE);
}
struct epoll_event events[MAX_EVENTS];
while (1) {
// 等待事件
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_wait");
exit(EXIT_FAILURE);
}
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == server_fd) {
// 新连接
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket == -1) {
perror("accept");
continue;
}
printf("New connection, socket fd: %d\n", new_socket);
// 设置socket为非阻塞模式
int flags = fcntl(new_socket, F_GETFL, 0);
fcntl(new_socket, F_SETFL, flags | O_NONBLOCK);
// 添加新连接到epoll
event.events = EPOLLIN | EPOLLET; // 边缘触发模式
event.data.fd = new_socket;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_socket, &event) == -1) {
perror("epoll_ctl: new_socket");
close(new_socket);
continue;
}
} else {
// 客户端数据
int sd = events[i].data.fd;
ssize_t valread;
while (1) {
valread = recv(sd, buffer, BUFFER_SIZE, 0);
if (valread < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
// 数据已读完
break;
} else {
perror("recv");
close(sd);
break;
}
} else if (valread == 0) {
// 连接关闭
printf("Connection closed, socket fd: %d\n", sd);
close(sd);
break;
} else {
// 处理数据
buffer[valread] = '\0';
printf("Received: %s\n", buffer);
}
}
}
}
}
close(epoll_fd);
return 0;
}
3.3.4 优缺点分析
优点:
- 单个进程可以处理成千上万的连接
- 效率高,适合高并发场景
- 不需要创建大量线程,资源消耗低
缺点:
- 实现相对复杂
- 仍然存在阻塞(在 select/poll/epoll 调用处)
3.4 信号驱动 I/O 模型
信号驱动 I/O 模型使用 SIGIO 信号来通知进程数据已准备好,允许进程在等待数据时执行其他任务。
3.4.1 工作原理
- 进程设置 SIGIO 信号处理函数
- 启用 socket 的信号驱动 I/O 功能
- 当数据准备好时,内核发送 SIGIO 信号给进程
- 进程在信号处理函数中执行 I/O 操作
3.4.2 代码示例
c
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#define BUFFER_SIZE 1024
int server_fd, new_socket;
char buffer[BUFFER_SIZE];
// SIGIO信号处理函数
void sigio_handler(int signo) {
ssize_t valread;
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
if (signo == SIGIO) {
// 接受连接
new_socket = accept(server_fd, (struct sockaddr *)&client_addr, &client_len);
if (new_socket < 0) {
perror("accept");
return;
}
printf("New connection from %s:%d\n",
inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
// 读取数据
valread = recv(new_socket, buffer, BUFFER_SIZE, 0);
if (valread < 0) {
perror("recv");
} else if (valread == 0) {
printf("Connection closed\n");
} else {
buffer[valread] = '\0';
printf("Received: %s\n", buffer);
}
close(new_socket);
}
}
int main() {
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
// 设置信号处理函数
signal(SIGIO, sigio_handler);
// 设置socket属主
if (fcntl(server_fd, F_SETOWN, getpid()) == -1) {
perror("fcntl F_SETOWN");
exit(EXIT_FAILURE);
}
// 设置非阻塞模式并启用信号驱动I/O
int flags = fcntl(server_fd, F_GETFL, 0);
if (fcntl(server_fd, F_SETFL, flags | O_NONBLOCK | FASYNC) == -1) {
perror("fcntl F_SETFL");
exit(EXIT_FAILURE);
}
printf("Server started, waiting for connections...\n");
// 主循环,执行其他任务
while (1) {
printf("Doing other tasks...\n");
sleep(5);
}
return 0;
}
3.4.3 优缺点分析
优点:
- 进程不需要阻塞等待
- 可以处理多个 I/O 事件
缺点:
- 信号处理复杂,容易遗漏
- 不适合处理大量 I/O 事件
- 数据传输阶段仍需阻塞
3.5 异步 I/O 模型
异步 I/O 模型是最先进的 I/O 模型,进程发起 I/O 操作后立即返回,由内核完成整个 I/O 操作(包括数据准备和数据传输),完成后通知进程。
3.5.1 工作原理
- 进程调用 aio_read 系统调用,指定数据缓冲区、大小和偏移量
- 系统调用立即返回,进程可以继续执行其他任务
- 内核负责等待数据准备和将数据复制到用户空间
- I/O 操作完成后,内核通知进程(通过信号或回调函数)
3.5.2 代码示例(使用 POSIX AIO)
c
#include <stdio.h>
#include <stdlib.h>
#include <aio.h>
#include <unistd.h>
#include <string.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define BUFFER_SIZE 1024
#define MAX_CONNECTIONS 5
struct aiocb aiocb_list[MAX_CONNECTIONS];
int connection_count = 0;
// AIO完成回调函数
void aio_completion_handler(sigval_t sigval) {
struct aiocb *req = (struct aiocb*)sigval.sival_ptr;
ssize_t ret = aio_return(req);
if (ret > 0) {
printf("Received: %.*s\n", (int)ret, (char*)req->aio_buf);
} else if (ret == 0) {
printf("Connection closed\n");
} else {
perror("aio_return");
}
close(req->aio_fildes);
free(req->aio_buf);
connection_count--;
}
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server started, waiting for connections...\n");
while (1) {
// 接受连接
if (connection_count < MAX_CONNECTIONS) {
new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);
if (new_socket < 0) {
perror("accept");
sleep(1);
continue;
}
printf("New connection, socket fd: %d\n", new_socket);
// 初始化AIO控制块
struct aiocb *aiocb = &aiocb_list[connection_count];
memset(aiocb, 0, sizeof(struct aiocb));
// 分配缓冲区
char *buffer = malloc(BUFFER_SIZE);
if (!buffer) {
perror("malloc");
close(new_socket);
continue;
}
// 设置AIO参数
aiocb->aio_fildes = new_socket;
aiocb->aio_buf = buffer;
aiocb->aio_nbytes = BUFFER_SIZE;
aiocb->aio_offset = 0;
aiocb->aio_sigevent.sigev_notify = SIGEV_THREAD;
aiocb->aio_sigevent.sigev_value.sival_ptr = aiocb;
aiocb->aio_sigevent.sigev_notify_function = aio_completion_handler;
aiocb->aio_sigevent.sigev_notify_attributes = NULL;
// 发起异步读操作
if (aio_read(aiocb) < 0) {
perror("aio_read");
free(buffer);
close(new_socket);
continue;
}
connection_count++;
} else {
printf("Max connections reached, waiting...\n");
sleep(1);
}
}
return 0;
}
3.5.3 优缺点分析
优点:
- 完全异步,进程效率最高
- 可以处理大量并发 I/O 操作
- 资源消耗低,扩展性好
缺点:
- 实现复杂
- 对内核版本有要求
- 调试困难
4. 五种 I/O 模型性能对比
4.1 性能指标对比
I/O 模型 | 阻塞情况 | 并发能力 | CPU 利用率 | 延迟 | 复杂度 |
---|---|---|---|---|---|
阻塞 I/O | 完全阻塞 | 低 | 低 | 高 | 低 |
非阻塞 I/O | 部分阻塞 | 中 | 高 | 中 | 中 |
I/O 多路复用 | 选择时阻塞 | 高 | 中 | 中 | 中高 |
信号驱动 I/O | 信号处理阻塞 | 中高 | 中 | 中低 | 高 |
异步 I/O | 无阻塞 | 最高 | 低 | 低 | 最高 |
4.2 适用场景分析
-
阻塞 I/O:适用于连接数少、操作简单的场景,如简单的命令行工具
-
非阻塞 I/O:适用于连接数不多,但需要较好响应性的场景,如交互式应用
-
I/O 多路复用:适用于高并发服务,如 Web 服务器、数据库服务器
-
信号驱动 I/O:适用于对响应时间要求高,但连接数不是特别多的场景
-
异步 I/O:适用于高并发、高性能要求的场景,如高性能服务器、实时数据处理系统
5. io_uring:新一代异步 I/O 技术
5.1 io_uring 简介
io_uring 是 Linux 内核 4.18 引入的新一代异步 I/O 框架,旨在提供比传统 AIO 更好的性能和易用性。它使用共享环形缓冲区在用户空间和内核空间之间传递 I/O 请求和完成事件。
5.2 io_uring 工作原理
- 提交队列(Submission Queue):用户空间向内核提交 I/O 请求
- 完成队列(Completion Queue):内核通知用户空间 I/O 操作完成
io_uring 的主要优势在于:
- 减少系统调用次数
- 降低内存拷贝开销
- 支持多种 I/O 操作
- 可扩展性好
5.3 io_uring 代码示例
c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <liburing.h>
#define ENTRIES 16
#define BUFFER_SIZE 1024
struct conn_info {
int fd;
char buffer[BUFFER_SIZE];
};
int main() {
int server_fd, new_socket;
struct sockaddr_in address;
int addrlen = sizeof(address);
struct io_uring ring;
struct io_uring_params params;
// 初始化io_uring参数
memset(¶ms, 0, sizeof(params));
if (io_uring_queue_init_params(ENTRIES, &ring, ¶ms) < 0) {
perror("io_uring_queue_init_params");
exit(EXIT_FAILURE);
}
// 创建socket文件描述符
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(8080);
// 绑定socket到端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 监听连接
if (listen(server_fd, 3) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Server started, waiting for connections...\n");
// 准备接受连接的请求
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
if (!sqe) {
perror("io_uring_get_sqe"); exit(EXIT_FAILURE);
}
struct conn_info *info = malloc(sizeof(struct conn_info));
info->fd = server_fd;
io_uring_prep_accept(sqe, server_fd, (struct sockaddr *)&address, &addrlen, 0);
io_uring_sqe_set_data(sqe, info);
io_uring_submit(&ring);
while (1) {
struct io_uring_cqe *cqe;
int ret = io_uring_wait_cqe(&ring, &cqe);
if (ret < 0) {
perror("io_uring_wait_cqe");
break;
}
if (cqe->res < 0) {
perror("cqe res");
}
struct conn_info *info = io_uring_cqe_get_data(cqe);
if (info->fd == server_fd) {
// 新连接
new_socket = cqe->res;
printf("New connection, socket fd: %d\n", new_socket);
// 准备读取数据
struct io_uring_sqe *read_sqe = io_uring_get_sqe(&ring);
if (!read_sqe) {
perror("io_uring_get_sqe");
close(new_socket);
} else {
struct conn_info *read_info = malloc(sizeof(struct conn_info));
read_info->fd = new_socket;
io_uring_prep_recv(read_sqe, new_socket, read_info->buffer, BUFFER_SIZE, 0);
io_uring_sqe_set_data(read_sqe, read_info);
io_uring_submit(&ring);
}
// 继续接受新连接
struct io_uring_sqe *accept_sqe = io_uring_get_sqe(&ring);
if (!accept_sqe) {
perror("io_uring_get_sqe");
} else {
struct conn_info *accept_info = malloc(sizeof(struct conn_info));
accept_info->fd = server_fd;
io_uring_prep_accept(accept_sqe, server_fd, (struct sockaddr *)&address, &addrlen, 0);
io_uring_sqe_set_data(accept_sqe, accept_info);
io_uring_submit(&ring);
}
} else {
// 读取数据完成
ssize_t nbytes = cqe->res;
if (nbytes > 0) {
printf("Received: %.*s\n", (int)nbytes, info->buffer);
} else if (nbytes == 0) {
printf("Connection closed, socket fd: %d\n", info->fd);
} else {
perror("recv");
}
close(info->fd);
free(info);
}
io_uring_cqe_seen(&ring, cqe);
}
io_uring_queue_exit(&ring);
return 0;
}
5.3 Linux 6.x 中的 io_uring 新特性
Linux 6.x 内核对 io_uring 进行了多项重要改进:
-
动态调整环形缓冲区大小:通过 IORING_REGISTER_RING_RESIZE 命令允许动态调整环形缓冲区大小,提高内存使用效率
-
改进的文件系统支持:增强了对各种文件系统的支持,包括网络文件系统
-
性能优化:减少了锁竞争,提高了并行处理能力
-
新的操作类型:增加了对更多操作的支持,如 IORING_OP_TIMEOUT
6. I/O 模型在实际应用中的选择
6.1 Nginx 中的 I/O 模型选择
Nginx 支持多种 I/O 模型,根据操作系统自动选择最优模型:
- Linux:epoll
- FreeBSD:kqueue
- Solaris:/dev/poll
- Windows:IOCP
Nginx 的事件模块设计允许在不同 I/O 模型之间无缝切换,保证了其在各种平台上的高性能。
6.2 Redis 中的 I/O 模型选择
Redis 使用单线程的 I/O 多路复用模型,主要基于 epoll 实现。这种设计使得 Redis 能够高效处理大量并发连接,同时保持实现简单。
6.3 高性能服务器的 I/O 模型选择
现代高性能服务器通常采用以下 I/O 模型组合:
- 主 acceptor 线程:阻塞 I/O 接受新连接
- 工作线程池:I/O 多路复用处理已建立连接
- 关键路径:异步 I/O 处理高优先级操作
7. 总结
本文详细介绍了 Linux 系统中的五种 I/O 模型:阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。每种模型都有其优缺点和适用场景,选择合适的 I/O 模型对于构建高性能应用至关重要。
随着硬件和软件技术的发展,异步 I/O 模型,特别是 io_uring 技术,正在成为高性能应用的首选。然而,简单的阻塞 I/O 模型在某些场景下仍然是最佳选择。作为开发者,我们需要根据具体应用场景和性能需求,选择最适合的 I/O 模型。
未来,随着 Linux 内核的不断发展,io_uring 等新技术将进一步成熟,为高性能 I/O 操作提供更好的支持。了解并掌握这些技术,将帮助我们构建更高效、更可靠的应用系统。"]}
更多推荐
所有评论(0)