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 操作通常包括两个阶段:

  1. 等待数据准备阶段:内核等待 I/O 设备准备好数据
  2. 数据传输阶段:数据从内核空间复制到用户空间

不同 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 工作原理

  1. 进程调用 recvfrom 系统调用
  2. 内核开始准备数据(等待数据从网络到达)
  3. 数据准备好后,内核将数据从内核空间复制到用户空间
  4. 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 工作原理

  1. 进程将 socket 设置为非阻塞模式
  2. 调用 recvfrom 系统调用
  3. 如果数据未准备好,返回EAGAIN 或 EWOULDBLOCK 错误
  4. 进程可以执行其他任务,稍后再次调用 recvfrom
  5. 当数据准备好后,内核将数据复制到用户空间
  6. 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 工作原理

  1. 进程将需要监控的文件描述符添加到 select/poll/epoll 集合中
  2. 调用 select/poll/epoll 系统调用,阻塞等待就绪的文件描述符
  3. 当有文件描述符就绪时,系统调用返回就绪的文件描述符集合
  4. 进程对就绪的文件描述符进行 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 工作原理

  1. 进程设置 SIGIO 信号处理函数
  2. 启用 socket 的信号驱动 I/O 功能
  3. 当数据准备好时,内核发送 SIGIO 信号给进程
  4. 进程在信号处理函数中执行 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 工作原理

  1. 进程调用 aio_read 系统调用,指定数据缓冲区、大小和偏移量
  2. 系统调用立即返回,进程可以继续执行其他任务
  3. 内核负责等待数据准备和将数据复制到用户空间
  4. 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 工作原理

  1. 提交队列(Submission Queue):用户空间向内核提交 I/O 请求
  2. 完成队列(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(&params, 0, sizeof(params));
    if (io_uring_queue_init_params(ENTRIES, &ring, &params) < 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 进行了多项重要改进:

  1. 动态调整环形缓冲区大小:通过 IORING_REGISTER_RING_RESIZE 命令允许动态调整环形缓冲区大小,提高内存使用效率

  2. 改进的文件系统支持:增强了对各种文件系统的支持,包括网络文件系统

  3. 性能优化:减少了锁竞争,提高了并行处理能力

  4. 新的操作类型:增加了对更多操作的支持,如 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 操作提供更好的支持。了解并掌握这些技术,将帮助我们构建更高效、更可靠的应用系统。"]}

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐