网络学习-异步IO(八)

一、异步io

1.1、什么是I/O?

Linux中,一切皆文件fd,定时器,文件,网络,驱动等;就是通信、读取文件的通道(fd)

1.2、处理I/O的方式

  • 阻塞IO:主线程发起io请求,然后阻塞等待结果返回,效率极低
  • 非阻塞IO + 多路复用:典型的epoll,同时监视多个文件描述符,一旦某个io请求就绪,则通知主线程处理。但这个过程还是有阻塞的,只是阻塞在epoll_wait上
  • 真正的异步I/O: 主线程发起io请求,立即返回,完全不用等待;操作系统会在后台完成整个I/O操作,然后通过回调函数通知主线程。

1.3、io_uring

io_uring是2019年加入Linux内核的特性,由Linux内核大师Jens Axboe设计,旨在提供一个统一、高效且真正的异步I/O解决方案。

核心思想 :通过共享内存,原子操作,无锁环形队列实现用户态与内核态之间的零拷贝

核心组件

  • 提交队列:一个环形队列,应用程序将需要执行的I/O操作放入此队列
  • 完成队列:也是一个环形队列,内核 将已完成的I/O操作结果放入此队列.

1.4、核心系统调用

  • io_uring_setup:初始化
c 复制代码
/*
* Desc: 初始化io_uring实例,创建提交队列和完成队列
* @entries: 提交队列的大小,必须是2的幂
* @params: 用于输入输出参数
* @return: 返回io_uring实例的fd
*/
int io_uring_setup(unsigned int entries, struct io_uring_params *params);
  • io_uring_register: 注册资源
c 复制代码
/*
* Desc: 注册资源,减少每次I/O操作的重复验证与映射开销
* @fd: 需注册的资源fd
* @opcode: 注册类型
* @arg: 资源参数
* @nr_args: 资源参数个数
*/
int io_uring_register(unsigned int fd, unsigned int opcode, void *arg, unsigned int nr_args);

常用的opcode

操作码 功能描述
IORING_REGISTER_BUFFERS 注册用户缓冲区数组(struct iovec),用于固定缓冲区 I/O。内核会锁定这些内存。
IORING_REGISTER_FILES 注册文件描述符数组。之后可在 SQE 中通过索引和 IOSQE_FIXED_FILE 标志来引用,避免每次查找。
IORING_REGISTER_EVENTFD 注册一个 eventfd 描述符,用于异步接收 I/O 完成事件的通知。
IORING_REGISTER_PROBE 查询内核,检查当前支持哪些 io_uring 操作码(opcodes),用于兼容性检查。
IORING_UNREGISTER_BUFFERS IORING_UNREGISTER_FILES 注销之前注册的缓冲区或文件描述符。
  • io_uring_enter:行动与等待------关键步骤
c 复制代码
/*
* Desc: 提交I/O请求并等待完成
* @fd: io_uring实例的fd
* @to_submit: 提交的SQE数量
* @min_complete: 最小完成数量
* @flags: 控制函数行为的标志位
* @sig: 信号集,可用于在等待期间原子性地替换和恢复进程的信号掩码,避免信号竞争
*/
int io_uring_enter(unsigned int fd, unsigned int to_submit, unsigned int min_complete, unsigned int flags, sigset_t *sig);

1.5、liburing库

io_uring_setupio_uring_registerio_uring_enter 这些系统调用底层是用C语言实现的,不方便直接使用。因此,提供了liburing库,封装了这些系统调用的复杂性,并提供了一个更高级的API。

二、利用io_uring实现tcp server

2.1、环境准备

linux内核版本需要大于5.1,否则不能完美支持io_uring

  • 查看linux内核版本:uname -r
    如果内核版本低于5.1,则需要升级内核。

    ***注意:***CentOs已经停服了,所以不建议在CentOs上使用io_uring。

  • 安装liburing库:

bash 复制代码
sudo apt install liburing-dev

2.2、服务器初始化

步骤依旧还是原来的那样

c 复制代码
int init_server(unsigned short port) {
    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        return -1;
    }

    int opt = 1;
    // 2. 设置SO_REUSEADDR选项,允许地址重用
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt error");
        close(sockfd);
        return -1;
    }

    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);

    // 3. 绑定套接字
    if (bind(sockfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) < 0) {
        perror("bind error");
        close(sockfd);
        return -1;
    }
    // 4. 开始监听连接请求
    if (listen(sockfd, 10) < 0) {
        perror("listen error");
        close(sockfd);
        return -1;
    }

    printf("Server listening on port %d\n", port);
    return sockfd;
}

2.3、io_uring初始化

使用liburing库初始化io_uring

c 复制代码
struct io_uring ring;
if (io_uring_queue_init(1024, &ring, 0) < 0) {
    perror("io_uring_queue_init");
    close(sockfd);
    exit(1);
}

2.4、提交accept请求

c 复制代码
/**
 * @brief 设置接受连接事件到io_uring队列
 * @param ring io_uring实例指针
 * @param sockfd 监听套接字文件描述符
 * @param addr 客户端地址结构体指针,用于存储接受的客户端地址信息
 * @param len 客户端地址结构体长度指针
 * 
 * @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
 */
int set_event_accept(struct io_uring* ring, int sockfd, struct sockaddr* addr, socklen_t* len) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = sockfd;
    info->event_type = EVENT_ACCEPT;

    io_uring_prep_accept(sqe, sockfd, addr, len, 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

2.5、为接受的连接提交读请求

c 复制代码
/**
 * @brief 设置接收数据事件到io_uring队列
 * @param ring io_uring实例指针
 * @param fd 要接收数据的套接字文件描述符
 * 
 * @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
 */
int set_event_recv(struct io_uring* ring, int fd) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = fd;
    info->event_type = EVENT_READ;

    io_uring_prep_recv(sqe, fd, info->buffer, sizeof(info->buffer), 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

2.6、回写数据到客户端

c 复制代码
/**
 * @brief 设置io_uring发送事件
 * 
 * @param ring io_uring实例指针
 * @param fd 目标套接字文件描述符
 * @param data 要发送的数据指针
 * @param len 数据长度
 * @return int 成功返回0,失败返回-1
 */
int set_event_send(struct io_uring* ring, int fd, const char* data, int len) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = fd;
    info->event_type = EVENT_WRITE;
    memcpy(info->buffer, data, len);
    info->buffer[len] = '\0';

    io_uring_prep_send(sqe, fd, info->buffer, len, 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

2.7、事件处理循环

c 复制代码
    while (1) {
        // 提交所有准备好的SQE到内核
        io_uring_submit(&ring);

        // 等待完成事件
        struct io_uring_cqe* cqe;
        int ret = io_uring_wait_cqe(&ring, &cqe);
        if (ret < 0) {
            perror("io_uring_wait_cqe");
            break;
        }

        // 获取用户数据(conn_info结构体)
        struct conn_info* info = (struct conn_info*)io_uring_cqe_get_data(cqe);
        int res = cqe->res;  
        if (res < 0) {
            printf("Error on fd %d: %s\n", info->fd, strerror(-res));
            if (info->fd != sockfd) {
                close(info->fd);
            }
            free(info);
            io_uring_cqe_seen(&ring, cqe);
            continue;
        }

        // 根据事件类型进行不同处理
        switch (info->event_type) {
            case EVENT_ACCEPT: {
                printf("New connection accepted, fd: %d\n", res);
                
                // 为新连接提交读请求,开始接收数据
                set_event_recv(&ring, res);
                
                // 重新提交accept请求,继续监听新连接
                set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);
                break;
            }
            
            case EVENT_READ: {
                if (res == 0) {
                    // 客户端关闭连接(收到EOF)
                    printf("Client fd %d disconnected\n", info->fd);
                    close(info->fd);
                } else {
                    // 成功读取数据,打印接收内容
                    printf("Received %d bytes from fd %d: %.*s\n", 
                           res, info->fd, res, info->buffer);
                    
                    // 回写相同数据给客户端(echo服务器)
                    set_event_send(&ring, info->fd, info->buffer, res);
                }
                break;
            }
            
            case EVENT_WRITE: {
                printf("Sent %d bytes to fd %d\n", res, info->fd);
                
                // 写完成后,重新提交读请求等待下一轮数据
                set_event_recv(&ring, info->fd);
                break;
            }
        }

        // 释放连接信息内存并标记CQE已处理
        free(info);
        io_uring_cqe_seen(&ring, cqe);
    }

执行结果:


三、拓展

3.1、C++中的async也是异步,那和io_uring有什么区别?

特性 async io_uring
抽象层次 C++标准库层面 Linux内核层面
主要目标 异步计算任务 异步IO操作
实现机制 线程池+future/promise 内核环形队列+共享内存
阻塞位置 阻塞在future.get() 几乎无阻塞
适用场景 CPU密集型任务 IO密集型应用(如网络服务器)

3.2、io_uring与epoll的区别

特性 epoll io_uring
模型 同步非阻塞 异步非阻塞
阻塞位置 epoll_wait() 几乎无阻塞
批处理能力 弱,需要遍历列表并逐个调用read/write 强,可以批量提交多个IO请求
内存开销 较低 较高(需要维护环形队列和缓冲区)
实现复杂度 相对简单 比较复杂

3.3、io_uring会替代epoll吗?

不会,之间是互补关系。

  1. 对于纯网络I/O,io_uring在极限场景下的性能优于epoll,但在实际中,哪有那么多极限场景
  2. 对于非网络I/O,存储I/O,那么io_uring毫不疑问的赢家.

附上代码:

c 复制代码
#include <cstdio>
#include <liburing.h>
#include <netinet/in.h>
#include <cstring>
#include <unistd.h>
#include <cstdlib>

#define EVENT_ACCEPT    0
#define EVENT_READ      1
#define EVENT_WRITE     2

struct conn_info {
    int fd;
    int event_type;
    char buffer[1024];  // 每个连接独立的缓冲区
};

int init_server(unsigned short port)
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0) {
        perror("socket error");
        return -1;
    }

    // 设置 SO_REUSEADDR
    int opt = 1;
    if (setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
        perror("setsockopt error");
        close(sockfd);
        return -1;
    }

    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));
    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(struct sockaddr_in)) == -1)
    {
        perror("bind error");
        close(sockfd);
        return -1;
    }

    if(listen(sockfd, 10) < 0)
    {
        perror("listen error");
        close(sockfd);
        return -1;
    }

    printf("Server listening on port %d\n", port);
    return sockfd;
}

/**
 * @brief 设置接受连接事件到io_uring队列
 * @param ring io_uring实例指针
 * @param sockfd 监听套接字文件描述符
 * @param addr 客户端地址结构体指针,用于存储接受的客户端地址信息
 * @param len 客户端地址结构体长度指针
 * 
 * @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
 */
int set_event_accept(struct io_uring* ring, int sockfd, struct sockaddr* addr, socklen_t* len) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = sockfd;
    info->event_type = EVENT_ACCEPT;

    io_uring_prep_accept(sqe, sockfd, addr, len, 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

/**
 * @brief 设置接收数据事件到io_uring队列
 * @param ring io_uring实例指针
 * @param fd 要接收数据的套接字文件描述符
 * 
 * @return int 执行结果:0表示成功,-1表示失败(无法获取SQE)
 */
int set_event_recv(struct io_uring* ring, int fd) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = fd;
    info->event_type = EVENT_READ;

    io_uring_prep_recv(sqe, fd, info->buffer, sizeof(info->buffer), 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

/**
 * @brief 设置io_uring发送事件
 * 
 * @param ring io_uring实例指针
 * @param fd 目标套接字文件描述符
 * @param data 要发送的数据指针
 * @param len 数据长度
 * @return int 成功返回0,失败返回-1
 */
int set_event_send(struct io_uring* ring, int fd, const char* data, int len) 
{
    struct io_uring_sqe* sqe = io_uring_get_sqe(ring);
    if (!sqe) return -1;

    struct conn_info* info = (struct conn_info*)malloc(sizeof(struct conn_info));
    info->fd = fd;
    info->event_type = EVENT_WRITE;
    memcpy(info->buffer, data, len);
    info->buffer[len] = '\0';

    io_uring_prep_send(sqe, fd, info->buffer, len, 0);
    io_uring_sqe_set_data(sqe, info);
    return 0;
}

int main()
{
    unsigned short port = 8080;
    int sockfd = init_server(port);
    if (sockfd < 0) {
        exit(1);
    }

    struct io_uring ring;
    if (io_uring_queue_init(1024, &ring, 0) < 0) {
        perror("io_uring_queue_init");
        close(sockfd);
        exit(1);
    }

    struct sockaddr_in clientaddr;
    socklen_t len = sizeof(clientaddr);
    
    // 初始提交accept请求
    set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);

    printf("Server started, waiting for connections...\n");

    while (1) {
        // 提交所有准备好的SQE到内核
        io_uring_submit(&ring);

        // 等待完成事件
        struct io_uring_cqe* cqe;
        int ret = io_uring_wait_cqe(&ring, &cqe);
        if (ret < 0) {
            perror("io_uring_wait_cqe");
            break;
        }

        // 获取用户数据(conn_info结构体)
        struct conn_info* info = (struct conn_info*)io_uring_cqe_get_data(cqe);
        int res = cqe->res;  
        if (res < 0) {
            printf("Error on fd %d: %s\n", info->fd, strerror(-res));
            if (info->fd != sockfd) {
                close(info->fd);
            }
            free(info);
            io_uring_cqe_seen(&ring, cqe);
            continue;
        }

        // 根据事件类型进行不同处理
        switch (info->event_type) {
            case EVENT_ACCEPT: {
                printf("New connection accepted, fd: %d\n", res);
                
                // 为新连接提交读请求,开始接收数据
                set_event_recv(&ring, res);
                
                // 重新提交accept请求,继续监听新连接
                set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len);
                break;
            }
            
            case EVENT_READ: {
                if (res == 0) {
                    // 客户端关闭连接(收到EOF)
                    printf("Client fd %d disconnected\n", info->fd);
                    close(info->fd);
                } else {
                    // 成功读取数据,打印接收内容
                    printf("Received %d bytes from fd %d: %.*s\n", 
                           res, info->fd, res, info->buffer);
                    
                    // 回写相同数据给客户端(echo服务器)
                    set_event_send(&ring, info->fd, info->buffer, res);
                }
                break;
            }
            
            case EVENT_WRITE: {
                printf("Sent %d bytes to fd %d\n", res, info->fd);
                
                // 写完成后,重新提交读请求等待下一轮数据
                set_event_recv(&ring, info->fd);
                break;
            }
        }

        // 释放连接信息内存并标记CQE已处理
        free(info);
        io_uring_cqe_seen(&ring, cqe);
    }

    // 清理资源:释放io_uring实例并关闭监听套接字
    io_uring_queue_exit(&ring);
    close(sockfd);
    return 0;
}
//gcc -o exe io_uring_.cpp -luring
相关推荐
三次拒绝王俊凯4 小时前
java求职学习day47
java·开发语言·学习
杜子不疼.5 小时前
【Linux】信号机制详解:进程间通信的核心
linux·运维·服务器
DarkBule_5 小时前
0成本get可信域名:dpdns.org公益域名获取全攻略
css·学习·html·github·html5
YJlio5 小时前
ProcDump 学习笔记(6.14):在调试器中查看转储(WinDbg / Visual Studio 快速上手)
笔记·学习·visual studio
檀越剑指大厂6 小时前
从被动查询到主动贡献:Answer的知识社区进化论
网络
知花实央l6 小时前
【Web应用安全】SQLmap实战DVWA SQL注入(从环境搭建到爆库,完整步骤+命令解读)
前端·经验分享·sql·学习·安全·1024程序员节
广然6 小时前
跨厂商(华为 & H3C)防火墙 GRE 隧道部署
网络·华为·防火墙·h3c
Hello.Reader6 小时前
Flink DataStream 从 WindowStrategy 到 WindowProcessFunction 的全链路
网络·数据库·flink
交换机路由器测试之路6 小时前
交换机路由器基础(一)基础概念
网络·智能路由器·路由器·交换机·网络基础·通信基础