异步IO框架io_uring实现TCP服务器

一、io_uring介绍

io_uring是 Linux 于 2019 年加入到内核的一种新型异步 I/O 模型,io_uring 主要为了解决 原生AIO(Native AIO) 存在的一些不足之处。下面介绍一下原生 AIO 的不足之处:

  • 系统调用开销大:提交 I/O 操作和获取 I/O 操作的结果都需要通过系统调用完成,而触发系统调用时,需求进行上下文切换。在高 IOPS(Input/Output Per Second)的情况下,进行上下文切换也会消耗大量的CPU时间。

  • 仅支持 Direct I/O(直接I/O):在使用原生 AIO 的时候,只能指定 O_DIRECT 标识位(直接 I/O),不能借助文件系统的页缓存(page cache)来缓存当前的 I/O 请求。

  • 对数据有大小对齐限制:所有写操作的数据大小必须是文件系统块大小(一般为4KB)的倍数,而且要与内存页大小对齐。

  • 数据拷贝开销大:每个 I/O 提交需要拷贝 64+8 字节,每个 I/O 完成结果需要拷贝 32 字节,总共 104 字节的拷贝。这个拷贝开销是否可以承受,和单次 I/O 大小有关:如果需要发送的 I/O 本身就很大,相较之下,这点消耗可以忽略。而在大量小 I/O 的场景下,这样的拷贝影响比较大。

对应io_uring就有他的优势:

  • 减少系统调用:io_uring通过内核态和用户态共享内存的方式进行通信。如下图:

用户态和内核态之间通过共享内存维护了三部分:提交队列、完成队列、提交队列表项数组。

提交队列是一整块连续的内存空间存储的环形队列,用于存放将要执行I/O操作的数据。

完成队列也是一整块连续的内存空间存储的环形队列,其中存放了I/O操作完成返回的结果。

提交队列表项数组是以数组形式将要执行的I/O操作组织在一起,提交队列完成队列分别通过指针指向对应表项(内部通过偏移量实现)完成对应操作。如下图所示:

提交队列具体实现为:io_uring_sq结构体。

struct io_uring_sq {
    unsigned *khead;
    unsigned *ktail;
    
    unsigned *kflags;
    unsigned *kdropped;
    unsigned *array;
    struct io_uring_sqe *sqes;
​
    unsigned sqe_head;
    unsigned sqe_tail;
​
    size_t ring_sz;
    void *ring_ptr;
​
    unsigned ring_mask;
    unsigned ring_entries;
​
    unsigned pad[2];
};

提交队列通过struct io_uring_sqe *sqes提交队列表项数组,使用khead、ktail指向对应队头和对尾完成应用层提交的IO任务的处理。同理完成队列也是如此,不过是由内核态将对应IO事件执行完成后将结果写入到对应表项。应用层通过完成队列表项即可获取到最终结果。

整体流程为:

请求时:1、应用创建SQE,更新SQ tail 2、内核消费SQE,更新SQ head。内核开始处理任务,处理完成后:1、内核为完成的一个或多个请求创建CQE,更新CQ tail 2、应用层消费CQE,更新CQ head。

二、liburing安装

上文介绍的io_uring均为内核层支持,在内核层提供了三个API:

  • io_uring_setup(2)

  • io_uring_register(2)

  • io_uring_enter(2)

    为方便使用直接安装liburing即可在应用层使用io_uring。liburing为作者Axboe封装好的用户态库。

实验环境:vmware 17安装ubuntu22.04 、内核版本:6.8.0-60-generic

选用源码安装方式,安装liburing。

1、获取源码:git clone https://github.com/axboe/liburing.git

2、编译:

cd liburing-master
./configure
make -j
sudo make install

三、代码实现

#include <stdio.h>
#include <liburing.h>
#include <netinet/in.h>
#include <string.h>
#include <unistd.h>
​
#define BUFFER_LENGTH   1024
#define ENTRIES_LENGTH  1024
​
#define EVENT_ACCEPT    0
#define EVENT_READ      1
#define EVENT_WRITE     2
​
// io事件信息
struct req_info{
    int fd;
    int event;
};
​
// 初始化tcp fd
int init_server(unsigned int port){
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serveraddr;
    memset(&serveraddr, 0, sizeof(struct sockaddr_in));
    serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_port = htons(port);
    if(-1 == bind(socket_fd, (struct sockaddr *)&serveraddr, sizeof(struct sockaddr))){
        perror("bind error!\n");
    }
    listen(socket_fd, 10);
    return socket_fd;
}
​
// 设置accept事件
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 req_info accept_info = {
        .fd = sockfd,
        .event = EVENT_ACCEPT
    };
​
    memcpy(&sqe->user_data, &accept_info, sizeof(struct req_info));
    io_uring_prep_accept(sqe, sockfd, addr, addrlen, flags);
​
    return 0;
}
​
// 设置recv事件
int set_event_recv(struct io_uring *ring, int sockfd, void *buf, size_t len, int flags){
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
​
    struct req_info recv_info = {
        .fd = sockfd,
        .event = EVENT_READ
    };
​
    memcpy(&sqe->user_data, &recv_info, sizeof(struct req_info));
    io_uring_prep_recv(sqe, sockfd, buf, len, flags);
​
    return 0;
}
​
// 设置send事件
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 req_info send_info = {
        .fd = sockfd,
        .event = EVENT_WRITE
    };
​
    memcpy(&sqe->user_data, &send_info, sizeof(struct req_info));
    io_uring_prep_send(sqe, sockfd, buf, len, flags);
​
    return 0;
}
​
int main(int argc, void *argv[]){
    unsigned int port = 9999;
    int sockfd = init_server(port);
​
    struct io_uring_params params;
    memset(&params, 0, sizeof(struct io_uring_params));
    
    // 初始化io_uring 其中包含了sq和cq
    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);
​
    while(1){
        char buffer[BUFFER_LENGTH] = {0};
​
        // 事件提交
        io_uring_submit(&ring);
​
        // 等待事件完成,其他开发场景下非必要情况无需等待
        struct io_uring_cqe *cqe;
        io_uring_wait_cqe(&ring, &cqe);
​
        // 获取完成事件
        struct io_uring_cqe *cqe_list[128];
        int nready = io_uring_peek_batch_cqe(&ring, cqe_list, 128);
        
        // echo逻辑处理
        for(int i = 0; i < nready; ++i){
            struct io_uring_cqe *entries = cqe_list[i];
            struct req_info result;
            memcpy(&result, &entries->user_data, sizeof(struct req_info));
            if(result.event == EVENT_ACCEPT){
                printf("accept client\n");
                set_event_accept(&ring, sockfd, (struct sockaddr*)&clientaddr, &len, 0);
​
                int connfd = entries->res;
​
                set_event_recv(&ring, connfd, buffer, BUFFER_LENGTH, 0);
            }else if(result.event == EVENT_READ){
                int connfd = entries->res;
                if(connfd == 0){
                    printf("close fd:%d\n", result.fd);
                    close(result.fd);
                }else if(connfd > 0){
                    printf("recv: len:%d, data:%s\n", connfd, buffer);
                    set_event_send(&ring, result.fd, buffer, connfd, 0);
                }else{
                    printf("error recv!\n");
                    close(result.fd);
                }
            }else if(result.event == EVENT_WRITE){
                int ret = entries->res;
                printf("set_event_send ret: %d, %s\n", ret, buffer);
                set_event_recv(&ring, result.fd, buffer, BUFFER_LENGTH, 0);
            }
        }
​
        // 清除完成队列中完成表项,以免事件重复处理
        io_uring_cq_advance(&ring, nready);
    }
​
    return 0;
}

更多内容可参考:0voice · GitHub 

<think>我们被要求解释 io_uring_prep_recv 函数,并参考了提供的两个引用。 引用[1]提供了函数的基本使用和功能:用于准备一个接收数据的I/O操作,并添加到SQE中。 引用[2]提供了io_uring框架的背景:是Linux内核5.1引入的高性能异步I/O框架,采用提交队列和完成队列的机制。 根据这些信息,我们可以组织回答如下: 1. 首先介绍io_uring_prep_recv函数的作用:准备一个接收数据的异步I/O操作。 2. 解释参数含义(根据引用[1]和一般知识): - sqe: 指向提交队列条目的指针 - sockfd: 套接字文件描述符 - buffer: 接收数据的缓冲区 - len: 要接收的数据长度 - flags: 标志位(例如MSG_DONTWAIT等) 3. 结合引用[2]说明该函数在io_uring框架中的位置:它将一个接收操作提交到提交队列,然后内核会异步处理这个操作,完成后将结果放入完成队列。 注意:由于用户明确要求解释该函数,因此我们应详细说明其参数和功能。 另外,根据系统指令,我们需要在回答中自然添加引用标识。同时,生成相关问题。 回答结构: 1. 函数功能概述 2. 参数解释 3. 在io_uring框架中的工作机制 4. 示例代码片段(可选,但用户没有要求,且引用中没有提供,可省略) 5. 相关问题 由于引用[1]中已经有一个调用示例,我们可以展示该调用,但不必展示完整代码,因为用户只要求解释函数。 注意:根据指令,行内数学表达式使用$...$,但本题没有数学表达式,所以可以忽略。独立公式也没有,所以不使用。 我们将生成2-3个相关问题。 最后,注意使用中文。 </think> ### io_uring_prep_recv 函数解释 `io_uring_prep_recv` 是 Linux `io_uring` 异步 I/O 框架中的核心函数之一,用于准备一个**异步接收数据操作**。以下是详细说明: #### 1. **功能作用** - 该函数将一个**接收数据(recv)的 I/O 请求**封装到 `io_uring` 的提交队列条目(SQE)中。 - 它不直接执行操作,而是将操作描述符提交到内核管理的环形队列,实现真正的异步 I/O 模型[^2]。 #### 2. **函数原型** ```c void io_uring_prep_recv(struct io_uring_sqe *sqe, int sockfd, void *buf, size_t len, int flags); ``` #### 3. **参数详解** | 参数 | 类型 | 说明 | |---------|-------------------|----------------------------------------------------------------------| | `sqe` | `io_uring_sqe*` | 指向提交队列条目的指针,由 `io_uring_get_sqe()` 获取[^1]。 | | `sockfd`| `int` | 目标套接字文件描述符(如 TCP/UDP socket)。 | | `buf` | `void*` | 接收数据的用户态缓冲区地址。 | | `len` | `size_t` | 请求接收的数据长度(字节数)。 | | `flags` | `int` | 标志位(如 `MSG_DONTWAIT` 非阻塞接收、`MSG_PEEK` 仅查看数据等)[^1]。| #### 4. **工作流程** 1. **提交阶段**: 应用程序调用 `io_uring_prep_recv` 填充 SQE,再通过 `io_uring_submit()` 将请求提交到内核队列。 2. **内核处理**: 内核在后台异步执行接收操作,无需阻塞应用线程[^2]。 3. **完成通知**: 操作完成后,内核将结果(成功字节数或错误码)放入完成队列(CQ),应用程序通过 `io_uring_wait_cqe()` 获取结果。 #### 5. **优势** - **零拷贝**:数据直接从内核空间复制到用户态缓冲区。 - **高性能**:避免系统调用和上下文切换开销,适合高并发网络场景[^2]。 - **非阻塞**:应用可继续执行其他任务,无需等待 I/O 完成。 #### 示例代码片段 ```c struct io_uring ring; io_uring_queue_init(32, &ring, 0); // 初始化io_uring struct io_uring_sqe *sqe = io_uring_get_sqe(&ring); // 获取SQE io_uring_prep_recv(sqe, sockfd, buffer, 1024, 0); // 准备接收操作 io_uring_submit(&ring); // 提交请求 // ... 异步处理其他任务 ... struct io_uring_cqe *cqe; io_uring_wait_cqe(&ring, &cqe); // 等待完成事件 printf("Received %d bytes\n", cqe->res); io_uring_cqe_seen(&ring, cqe); // 标记事件已处理 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值