io_uring学习笔记

概述

  • io_uring最大的特点就是读写io改为了异步方式
  • 使用io_uring可以利用应用层提供的 liburing
  • 修改异步的常用方式:
    • init/create — io_uring_setup();
    • commit() ----io_uring_register();
    • callback()----io_uring_enter(); 收集消息
    • destroy()

不同ioengine下的磁盘的iops

iops:Input/Output Per Second

psync :posix sync

ioengine = psyc时,iops = 5122

ioengine = io_uring时,iops = 11.6k

ioengine = libaio时,iops = 10.7k

ioengine = spdk_bdev时,iops = 157k ,可能直接就是内存操作了

上面数据说明了采用异步方案后读写磁盘速度有了明显提升

io_uring 的用户态 API

io_uring 的实现仅仅使用了三个 syscall

io_uring_setup,io_uring_enter io_uring_register。它们分别用于设置 io_uring 上下文,提交并获取完成任务,以及注册内核用户共享的缓冲区。使用前两个 syscall 已经足够使用 io_uring 接口了。

用户和内核通过提交队列和完成队列进行任务的提交和收割,以下是一些缩略语:

  • SQ Submission Queue 提交队列 一整块连续的内存空间存储的环形队列。用于存放将执行操作的数据。
  • CQ Completion Queue 完成队列 一整块连续的内存空间存储的环形队列。用于存放完成操作返回的结果。
  • SQE Submission Queue Entry 提交队列项 提交队列中的一项。
  • CQE Completion Queue Entry 完成队列项 完成队列中的一项。
  • Ring Ring 环 比如 SQ Ring,就是“提交队列信息”的意思。包含队列数据、队列大小、丢失项等等信息。

io_uring和liburing之间关系

在这里插入图片描述

liburing就是对三个syscall进行封装提供给我们进行使用的库。

io_uring基本原理

在这里插入图片描述

利用liburing实现echoserver

io_uring_setup

long io_uring_setup(u32 entries, struct io_uring_params __user *params)

用户通过调用 io_uring_setup [1]初始化一个新的 io_uring 上下文。

该函数返回一个 file descriptor。

并将 io_uring 支持的功能、以及各个数据结构在 fd 中的偏移量存入 params。用户根据偏移量将 fd 映射到内存 (mmap) 后即可获得一块内核用户共享的内存区域。这块内存区域中,有 io_uring 的上下文信息:提交队列信息 (SQ_RING) 和完成队列信息 (CQ_RING);还有一块专门用来存放提交队列元素的区域 (SQEs)。SQ_RING 中只存储 SQE 在 SQEs 区域中的序号,CQ_RING 存储完整的任务完成数据。

entries 指示队列的长度。

在这里插入图片描述

在这里插入图片描述

在 Linux 5.12 中,SQE 大小为 64B,CQE 大小为 16B。因此,相同数量的 SQE 和 CQE 所需要的空间不一样。初始化 io_uring 时,用户如果不在 params 中设置 CQ 长度,内核会分配 entries 个 SQE,以及 entries * 2 个 CQE。

内存映射函数 mmap

void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);
int munmap(void* start,size_t length);

mmap()[1] 必须以PAGE_SIZE为单位进行映射,而内存也只能以页为单位进行映射,若要映射非PAGE_SIZE整数倍的地址范围,要先进行内存对齐,强行以PAGE_SIZE的倍数大小进行映射。参数start:指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。

  • 参数length:代表将文件中多大的部分映射到内存。

  • 参数prot:映射区域的保护方式。可以为以下几种方式的组合:
    PROT_EXEC 映射区域可被执行
    PROT_READ 映射区域可被读取
    PROT_WRITE 映射区域可被写入
    PROT_NONE 映射区域不能存取

  • 参数flags:影响映射区域的各种特性。在调用mmap()时必须要指定MAP_SHARED 或MAP_PRIVATE。
    MAP_FIXED 如果参数start所指的地址无法成功建立映射时,则放弃映射,不对地址做修正。通常不鼓励用此旗标。
    MAP_SHARED对映射区域的写入数据会复制回文件内,而且允许其他映射该文件的进程共享。
    MAP_PRIVATE 对映射区域的写入操作会产生一个映射文件的复制,即私人的“写入时复制”(copy on write)对此区域作的任何修改都不会写回原来的文件内容。
    MAP_ANONYMOUS建立匿名映射。此时会忽略参数fd,不涉及文件,而且映射区域无法和其他进程共享。
    MAP_DENYWRITE只允许对映射区域的写入操作,其他对文件直接写入的操作将会被拒绝。
    MAP_LOCKED 将映射区域锁定住,这表示该区域不会被置换(swap)。

  • 参数fd:要映射到内存中的文件描述符。如果使用匿名内存映射时,即flags中设置了MAP_ANONYMOUS,fd设为-1。有些系统不支持匿名内存映射,则可以使用fopen打开/dev/zero文件,然后对该文件进行映射,可以同样达到匿名内存映射的效果。

  • 参数offset:文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。

  • 返回值:

若映射成功则返回映射区的内存起始地址,否则返回MAP_FAILED(-1),错误原因存于errno 中。

下面说一下内存映射的步骤:

  1. open系统调用打开文件, 并返回描述符fd.
  2. mmap建立内存映射, 并返回映射首地址指针start.
  3. 对映射(文件)进行各种操作, 显示(printf), 修改(sprintf).
  4. munmap(void *start, size_t lenght)关闭内存映射.
  5. close系统调用关闭文件fd.

UNIX网络编程第二卷进程间通信对mmap函数进行了说明。该函数主要用途有三个:
1、将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存读写取代I/O读写,以获得较高的性能
2、将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
3、为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

io_uring 数据结构

完成队列项(CQE)数据结构

struct io_uring_cqe {
	__u64 user_data; //!!!(后面会用到)包含应用对IO请求的标识信息,一种常用的做法是采用指针的方式指向原始的IO请求,内核不会对该字段进行修改
	__s32 res; //IO请求结果
	__u32 flags; //保存与本次IO操作相关的元数据(metda data),目前,该字段没有使用
};

请求队列项(SQE)数据结构

struct io_uring_sqe {
	__u8 opcode; //保存IO请求的操作类型,比如IORING_OP_READV 表示向量化的读取操作
	__u8 flags;  //保存修改标志
	__u16 ioprio; //IO优先级
	__s32 fd;
	__u64 off;	//本次IO操作开始的位置偏移量
	__u64 addr;
	__u32 len;
	union {
		__kernel_rwf_t rw_flags;
		__u32 fsync_flags;
		__u16 poll_events;
		__u32 sync_range_flags;
		__u32 msg_flags;
	};
	__u64 user_data; 
	union {
		__u16 buf_index;
		__u64 __pad2[3];
	};
};

io_uring_prep_accept

void io_uring_prep_accept(struct io_uring_sqe *sqe,
                          int sockfd,
                          struct sockaddr *addr,
                          socklen_t *addrlen,
                          int flags);

这些函数准备一个异步accept()请求。
io_uring_prep_accept()函数准备接受请求。提交队列条目sqe被设置为使用文件描述符sockfd开始接受由addr处的套接字地址和结构长度addrlen描述的连接请求,并在标志中使用修饰符标志。

io_uring_prep_recv

void io_uring_prep_recv(struct io_uring_sqe *sqe,
                        int sockfd,
                        void *buf,
                        size_t len,
                        int flags);

提交队列条目sqe被设置为使用文件描述符sockfd来开始将数据接收到大小为len且具有修改标志flags的缓冲区目的地buf中。
此函数用于准备异步recv()请求。

io_uring_prep_send

void io_uring_prep_send(struct io_uring_sqe *sqe,
                               int sockfd,
                               const void *buf,
                               size_t len,
                               int flags);

io_uring_prep_send()函数准备发送请求。提交队列条目sqe被设置为使用文件描述符sockfd开始发送buf的数据,其中长度为len,并带有修改标志flags。

io_uring_submit

int io_uring_submit(struct io_uring *ring);

将下一个事件提交到属于ring的提交队列SQ。
调用者使用io_uring_get_sqe()检索提交队列条目(SQE)并使用提供的帮助程序之一准备SQE后,可以使用io_ uring_ submit()提交。

返回值:
成功时返回提交的提交队列条目数。
失败时返回-errno。

io_uring_wait_cqe

int io_uring_wait_cqe(struct io_uring *ring,
                             struct io_uring_cqe **cqe_ptr);

函数等待属于ring的队列中的 IO 完成,如有必要,等待它。如果调用时某个事件在ring队列中已可用,则不会发生等待。cqe_ptr参数在成功时填充。
cqe是一个数组,元素类型全为struct io_uring_cqe。

当 io_uring_submit 提交请求后,应用程序可以使用io_uring_wait_cqe检索完成队列项(cqe)

io_uring_peek_batch_cqe

int io_uring_peek_batch_cqe(struct io_uring *ring,
                      struct io_uring_cqe **cqe_ptr,
                      int count);

io_uring_peek_batch_cqe是对io_uring_peek_cqe的封装,表示一次最多从ring参数获取count个IO完成队列事件。
返回值:
成功时io_uring_peek_cqe()返回0,并填写cqe_ptr参数。失败时返回-EAGAIN。
成功时io_uring_peek_cqe()返回IO完成数量,并填写cqe_ptr参数。失败时返回-EAGAIN。

io_uring_cq_advance

void io_uring_cq_advance(struct io_uring *ring,
                          unsigned nr);

io_uring_cq_advance()函数将属于ring参数的nr个io完成事件(cqe)标记为consumed。

echoserver代码及调试

#include <stdio.h>
#include <liburing.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>

#include <unistd.h>

#define ENTRIES_LENGTH 4096

enum {
    READ,
    WRITE,
    ACCEPT,
};

struct conninfo {
    int connfd;
    int type;
};

void set_write_event(struct io_uring *ring, int fd, const void *buf, size_t len, int flags) {
    // 非系统调用,获取一个可用的 submit_queue_entry,用来提交IO
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

    //准备一个异步的发送请求
    io_uring_prep_send(sqe, fd, buf, len, flags);

    struct conninfo ci = {
        .connfd = fd,
        .type = WRITE
    };

    memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}

void set_read_event(struct io_uring *ring, int fd, void *buf, size_t len, int flags) {
    // 非系统调用,获取一个可用的 submit_queue_entry,用来提交IO
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

    io_uring_prep_recv(sqe, fd, buf, len, flags);

    struct conninfo ci = {
        .connfd = fd,
        .type = READ
    };

    memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}

void set_accept_event(struct io_uring *ring, int fd, struct sockaddr *clienaddr, socklen_t *clilen, unsigned flags) {
    // 非系统调用,获取一个可用的 submit_queue_entry,用来提交IO
    struct io_uring_sqe *sqe = io_uring_get_sqe(ring);

    //将listenfd添加到sqe中,内部会执行accept 进行接受连接,内部调accept4
    io_uring_prep_accept(sqe, fd, clienaddr, clilen, flags);

    struct conninfo ci = {
        .connfd = fd,
        .type = ACCEPT
    };

    memcpy(&sqe->user_data, &ci, sizeof(struct conninfo));
}

int main() {

    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    if(listenfd == -1) return -1;

    struct sockaddr_in servaddr, clientaddr;
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(9999);

    if(-1 == bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) {
        return -2;
    }


    listen(listenfd, 10);

    struct io_uring_params params;
    memset(&params, 0x00, sizeof(params));

    struct io_uring ring;
    
    //setup io_uring submission and completion queues,内部将队列映射到内存空间中(mmap)
    io_uring_queue_init_params(ENTRIES_LENGTH, &ring, &params);

    socklen_t clilen = sizeof(clientaddr);
    set_accept_event(&ring, listenfd, (struct sockaddr*)&clientaddr, &clilen, 0);

    char buffer[1024] = {0};
    while(1) {
        struct io_uring_cqe *cqe; //completion queue entry

        //将下一个事件提交给属于该ring的submission queues。
        io_uring_submit(&ring); //1
    
        //int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe_ptr);
        //注意这里第二参数是二级指针,表明完成队列CQ中存的是cqe指针
        //wait for one io_uring completion event
        int ret = io_uring_wait_cqe(&ring, &cqe); //2

        struct io_uring_cqe *cqes[10];
        //将io完成事件(cqe) 写入到cqes数组中
        int cqecount = io_uring_peek_batch_cqe(&ring, cqes, 10); //3

        int i = 0;
        unsigned count = 0;
        for(i = 0; i < cqecount; i++ ) {
            cqe = cqes[i];
            count++;

            struct conninfo ci;
            memcpy(&ci, &cqe->user_data, sizeof(ci));

            if(ci.type == ACCEPT) {
                int connfd = cqe->res;
                
                set_read_event(&ring, connfd, buffer, 1024, 0); //设置可读事件
            } else if (ci.type == READ) {
                
                int bytes_read = cqe->res;   
                if(bytes_read == 0) { //读到的数据为0,表示已经断开连接
                    close(ci.connfd);
                } else if(bytes_read < 0) {

                } else{
                    printf("buffer : %s\n", buffer); 
                    //echo
                    set_write_event(&ring, ci.connfd, buffer, bytes_read, 0);
                }
                
            } else if(ci.type == WRITE) {
                set_read_event(&ring, ci.connfd, buffer, 1024, 0);
            }
        }

        //标记count个CQE已经被消费
        io_uring_cq_advance(&ring, count);
    }
    
    return 0;
}

编译

jyhlinux@ubuntu:~/share/2.5.1_io_uring$ gcc -o vio_iouring vio_iouring.c -luring
jyhlinux@ubuntu:~/share/2.5.1_io_uring$ ./vio_iouring

使用NetAssist测试数据可以正常收发

函数调用流程

io_uring_queue_init_params();
io_uring_prep_accept();
io_uring_prep_recv();
io_uring_prep_send(uring,);
//-------------------------------
io_uring_submit(&ring); //递交io给内核中的SQ,内部处理了之后放到CQ中
io_uring_wait_cqe(&ring, &cqe); //wait for one io_uring completion event
io_uring_peek_batch_cqe(&ring, cqes, 10); //取出完成事件(CQE)放到指定的数组中
io_uring_cq_advance(&ring, count); //标记count个CQE已经被消费

杂项

如果希望自己用io_uring封装接口,则核心要实现以下3个功能:

  1. app_uring_setup
  2. submit_to_sq
  3. read_from_cq

基于系统调用封装liburing自己再去实现意义不大

  • epoll设置listenfd之后,不更改就会一直触发

  • 多客户端连接时只能第一个客户端收发数据成功,原因是:

    listen但没有accept:

    在这里插入图片描述

  • io_uring不止用于网络io,也用于磁盘io

  • io_uring相比epoll好在哪里?

    共享内存,将磁盘存储空间映射到内存,修改变量都是通过共享内存出来。

  • 系统调用的形式

    在这里插入图片描述

系统调用中,io_uring_setup是函数名,u32是函数对应的参数1的类型,entries是函数对应的参数1的名称,参数4是函数对应的参数2的类型,参数5是函数对应的参数2的名

在这里插入图片描述

先触发一个中断 0x80

将参数1,2移动到寄存器eax和ebx

在这里插入图片描述

这种系统调用就是相当于调用io_uring_enter,后面是这个函数的参数。

  • io_uring未来可能是和epoll共存的情况,取代epoll可能比较困难

liburing安装

安装git,并下载liburing

sudo apt-get install git
git clone git://git.kernel.dk/liburing

进入liburing 根目录

cd liburing/

安装liburing

sudo make install

参考资料

  1. 一篇文章带你读懂 io_uring 的接口与实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值