Bootstrap

2.5.1 io_uring

2.5.1 io_uring

1. 对比

1. select、poll、epoll 对比表格

维度selectpollepoll
最大描述符数默认 1024(硬编码)无限制(受系统资源约束)无限制(内核红黑树管理)
时间复杂度O(n)(每次遍历全部 fd)O(n)(遍历动态数组)O(1)(仅处理就绪事件)
数据拷贝方式全量拷贝 fd_set 到内核全量拷贝 pollfd 数组到内核共享内存(mmap 实现零拷贝)
触发模式仅水平触发(LT)仅水平触发(LT)支持 LT 和边缘触发(ET)
内核通知机制轮询检查所有 fd 状态轮询检查所有 fd 状态事件回调(就绪事件链表)
适用场景低并发(<1k)、跨平台中等并发(1k~10k)高并发(>10k)、Linux 平台、长连接低活跃场景
数据结构位图(fd_set动态数组(struct pollfd红黑树(注册 fd)+ 就绪链表(事件回调)
编程复杂度需手动重置 fd_set,易出错无需重置 fd,结构更灵活通过 epoll_ctl 动态管理 fd,接口简洁
性能瓶颈高并发时遍历效率低,内存拷贝开销大中等并发下可扩展,但拷贝和遍历仍为瓶颈高并发下高效,仅处理活跃 fd

2. 关键特性说明:

  1. 水平触发(LT)​:只要 fd 就绪,持续通知(如未读取完数据会反复触发)
  2. 边缘触发(ET)​:仅在状态变化时通知一次,需一次性处理完数据(适合高性能场景)
  3. 零拷贝:epoll 通过 mmap 共享内核与用户空间内存,避免重复数据拷贝
  4. 事件驱动:epoll 使用回调机制直接通知就绪事件,无需轮询所有

3. 应用场景

  • select:嵌入式设备、跨平台工具(旧版 FTP)
  • poll:传统 Web 服务器(早期 Apache)
  • epoll:Nginx、Redis、实时通信系统(直播平台)

2. 异步io

同步IO是指程序发起IO操作后,必须等待操作完成并获取结果后才能继续执行后续代码。整个过程会阻塞当前线程或进程
异步IO允许程序发起IO操作后立即返回,无需等待结果。内核在操作完成后通过回调、事件通知等方式告知程序,期间线程可执行其他任务

在这里插入图片描述

1. 频繁copy

内存共享 mmap

2. 如何做到线程安全

  1. 无锁队列:通过原子操作实现线程安全,适合高并发场景,但实现复杂。
  2. ​环形队列:通过循环缓冲区和同步机制实现线程安全,适合固定大小的队列场景。
  3. ​无锁环形队列:结合无锁编程和环形缓冲区的优点,提供高性能的线程安全队列。

3. io_uring

io_uring 是 Linux 内核提供的一种高效异步 I/O 框架,自 Linux 5.1 版本引入,旨在提升大规模并发 I/O 操作的性能,通过共享内存和环形缓冲区的方式,实现了高效的异步 I/O 操作
应用程序将 I/O 请求提交到 io_uring 的提交队列(SQ),内核异步处理这些请求,并将结果放入完成队列(CQ)。应用程序无需等待 I/O 操作完成,可以继续执行其他任务,之前仨本质还是同步io

  1. io_uring_setup
    io_uring_setup 是用于创建和初始化 io_uring 实例的系统调用。它的主要功能包括:

分配和配置提交队列(SQ)和完成队列(CQ)。
返回一个文件描述符(fd),用于标识 io_uring 实例。
支持通过 struct io_uring_params 参数配置额外的功能,如轮询模式(SQPOLL)或 I/O 轮询(IOPOLL)。

struct io_uring_params params;
int fd = io_uring_setup(entries, &params);
  1. io_uring_register
    io_uring_register 用于将文件描述符、缓冲区或其他资源预先注册到 io_uring 实例中。这样可以减少每次 I/O 操作时的开销,提高效率。常见的注册操作包括:注册文件描述符集合(IORING_REGISTER_FILES),注册内存缓冲区(IORING_REGISTER_BUFFERS),注册事件文件描述符(IORING_REGISTER_EVENTFD)
io_uring_register(fd, IORING_REGISTER_BUFFERS, buffers, buffer_count);
  1. io_uring_enter
    io_uring_enter 是用于提交和处理 I/O 操作的系统调用。它的主要功能包括:

将提交队列(SQ)中的 I/O 请求提交给内核。
等待完成队列(CQ)中的 I/O 操作结果。
支持批量提交和批量获取结果,减少系统调用次数。

io_uring_enter(fd, to_submit, min_complete, flags, NULL, 0);
  1. liburing库

1. 实现

#include <stdio.h>
#include <liburing.h>    // io_uring 异步IO库
#include <netinet/in.h>  // 套接字相关结构体
#include <string.h>      // memset/memcpy
#include <unistd.h>      // close

/* 事件类型枚举 */
#define EVENT_ACCEPT   0  // 接受连接事件
#define EVENT_READ    1  // 读取数据事件
#define EVENT_WRITE   2  // 写入数据事件

/* 连接信息结构体(存储于user_data) */
struct conn_info {
    int fd;     // 关联的文件描述符
    int event;  // 事件类型
};

/* 初始化TCP服务器 */
int init_server(unsigned short port) {
    // 创建TCP套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    
    // 配置服务器地址
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); // 监听所有接口
    serveraddr.sin_port = htons(port);              // 指定端口
    
    // 绑定地址
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
        perror("bind failed");
        return -1;
    }
    
    // 开始监听(等待队列长度10)
    listen(sockfd, 10);
    return sockfd;
}

/* io_uring 参数 */
#define ENTRIES_LENGTH 1024  // 环队列大小
#define BUFFER_LENGTH  1024  // 读写缓冲区大小

/* 设置接收事件到io_uring */
int set_event_recv(struct io_uring *ring, int sockfd, void *buf, size_t len, int flags) {
    // 获取提交队列项(SQE)
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    
    // 构造连接信息(存储到user_data)
    struct conn_info info = {
        .fd = sockfd,
        .event = EVENT_READ
    };
    
    // 准备RECV操作(异步接收数据)
    io_uring_prep_recv(sqe, sockfd, buf, len, flags);
    
    // 将连接信息存入user_data(用于后续识别事件)
    memcpy(&sqe->user_data, &info, sizeof(info));
    return 0;
}

/* 设置发送事件到io_uring */
int set_event_send(struct io_uring *ring, int sockfd, void *buf, size_t len, int flags) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    struct conn_info info = {
        .fd = sockfd,
        .event = EVENT_WRITE
    };
    
    // 准备SEND操作(异步发送数据)
    io_uring_prep_send(sqe, sockfd, buf, len, flags);
    memcpy(&sqe->user_data, &info, sizeof(info));
    return 0;
}

/* 设置接受连接事件到io_uring */
int set_event_accept(struct io_uring *ring, int sockfd, struct sockaddr *addr,
                    socklen_t *addrlen, int flags) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
    struct conn_info info = {
        .fd = sockfd,
        .event = EVENT_ACCEPT
    };
    
    // 准备ACCEPT操作(异步接受连接)
    io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags);
    memcpy(&sqe->user_data, &info, sizeof(info));
    return 0;
}

int main(int argc, char *argv[]) {
    // 初始化服务器套接字
    unsigned short port = 9999;
    int sockfd = init_server(port);
    
    // 初始化io_uring参数
    struct io_uring_params params;
    memset(&params, 0, sizeof(params));
    
    // 创建io_uring实例
    struct io_uring ring;
    io_uring_queue_init_params(ENTRIES_LENGTH, &ring, &params);
    
    // 准备接受连接的异步操作
    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
    
    // 共享缓冲区(注意:多连接时存在竞争风险)
    char buffer[BUFFER_LENGTH] = {0};

    // 主事件循环
    while (1) {
        // 提交所有准备好的SQE到内核
        io_uring_submit(&ring);
        
        // 等待至少一个完成事件(阻塞等待)
        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);
        
        // 批量获取完成事件(最多128个)
        struct io_uring_cqe *cqes[128];
        int nready = io_uring_peek_batch_cqe(&ring, cqes, 128);//epoll_wait
        
        // 处理每个完成事件
        for (int i = 0; i < nready; i++) {
            struct io_uring_cqe *entry = cqes[i];
            struct conn_info result;
            memcpy(&result, &entry->user_data, sizeof(result));
            
            if (result.event == EVENT_ACCEPT) {
                // 处理新连接
                int connfd = entry->res; // accept返回的新fd
                
                // 重新注册ACCEPT事件(持续监听新连接)
                set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
                
                // 注册新连接的读事件
                set_event_recv(&ring, connfd, buffer, BUFFER_LENGTH, 0);
                
            } else if (result.event == EVENT_READ) {
                // 处理读完成
                int ret = entry->res;
                
                if (ret <= 0) { // 连接关闭或错误
                    close(result.fd);
                } else {        // 收到数据,准备回写
                    set_event_send(&ring, result.fd, buffer, ret, 0);
                }
                
            } else if (result.event == EVENT_WRITE) {
                // 写完成,重新注册读事件
                set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);
            }
        }
        
        // 推进完成队列(标记已处理的事件)处理完就清空
        io_uring_cq_advance(&ring, nready);
    }
}
开始
  ↓
初始化 TCP 服务器
  ↓
初始化 io_uring 实例
  ↓
注册 ACCEPT 事件到 io_uring
  ↓
进入主事件循环
  ↓
提交 I/O 请求到内核
  ↓
等待完成事件
  ↓
处理完成事件
  ├── 如果是 ACCEPT 事件
  │     ↓
  │    接受新连接
  │     ↓
  │    重新注册 ACCEPT 事件
  │     ↓
  │    注册 READ 事件
  ├── 如果是 READ 事件
  │     ↓
  │    读取数据
  │     ↓
  │    如果数据有效,注册 WRITE 事件
  │    否则关闭连接
  └── 如果是 WRITE 事件
        ↓
        注册 READ 事件
  ↓
推进完成队列
  ↓
继续主事件循环

2. 关键点:

​异步衔接:每个操作完成后立即注册下一个操作(ACCEPT→READ→WRITE→READ循环)
​共享缓冲区:所有连接共用buffer变量,高并发时需改为连接独立缓冲区
​错误处理:当entry->res <= 0时直接关闭连接

3. 问题

1. Reactor 与 Proactor 的三点不同

Reactor 和 Proactor 是两种基于事件分发的网络编程模式,它们的核心区别在于事件处理流程和 I/O 操作的责任分配:

  1. ​事件处理流程

​Reactor:基于同步 I/O,主线程监听事件就绪后,由工作线程执行实际的 I/O 操作(如读/写)和业务处理。Reactor 感知的是「待完成」的 I/O 事件,应用程序需要主动调用系统调用(如 read/write)来获取数据。
​Proactor:基于异步 I/O,主线程直接处理 I/O 操作完成后的事件通知,工作线程仅处理业务逻辑。Proactor 感知的是「已完成」的 I/O 事件,操作系统负责完成数据搬运,应用程序只需处理准备好的数据。
2. ​I/O 操作责任

​Reactor:应用程序负责 I/O 调度和处理,需要主动调用系统调用来读取或写入数据,可能遇到部分读取/写入的情况,需自行管理缓冲区状态。
​Proactor:操作系统全权负责 I/O 操作,应用程序只需处理完成的事件,无需关注中间状态,内置错误处理机制。
3. ​平台实现

​Reactor:常见于 Linux 平台,典型实现包括 epoll 和 Java NIO。
​Proactor:常见于 Windows 平台,典型实现包括 IOCP(I/O 完成端口)和 Boost.Asio。

2. epoll 与 io_uring 的区别

epoll 和 io_uring 都是 Linux 中用于高效处理 I/O 操作的机制,但它们在设计思想和实现方式上有显著差异:

  1. ​设置与使用

epoll:设置好后,应用程序只需等待事件通知,无需频繁修改。epoll 通过事件驱动的方式,避免了频繁遍历文件描述符的性能开销,适合处理大量连接。
io_uring:每次 I/O 操作都需要通过提交队列(SQ)和完成队列(CQ)进行设置。应用程序需要将 I/O 请求放入提交队列,内核处理完成后将结果放入完成队列,应用程序再从完成队列中获取结果。

  1. ​设计思想

epoll:本质上是同步 I/O 的事件通知机制,应用程序在处理 I/O 事件时仍然需要进行实际的 I/O 操作,可能会导致线程阻塞。
io_uring:通过用户态和内核态共享提交队列和完成队列,减少了系统调用的次数和上下文切换的开销,支持真正的异步 I/O 操作,适合高并发场景。

  1. ​性能与灵活性
    epoll:适合处理大量连接,但在高并发场景下可能存在性能瓶颈。
    io_uring:通过批量提交和批量获取结果,减少了系统调用开销,支持多种 I/O 操作,具有更高的灵活性和扩展性

  2. 四种的对比
    select:简单但效率低,适合低并发或跨平台场景。
    poll:改进 select 的描述符数量限制,适合中等并发。
    epoll:事件驱动,适合高并发场景,是 Linux 下高性能服务的首选。
    io_uring:异步 I/O,性能最优,适合大规模高并发场景。

;