从linux内核看io_uring的实现( 三 )


io_uring_setup就分析完了,但是还不能使用 。io_uring在设计中,为了减少系统调用和用户、内核数据通信的成本,实现了用户、内核共享数据结构的方式,这样用户和内核就可以操作同一份数据结构达到通信目的,而不用通过系统调用,更不需要设计来回复制 。为了达到这个目的,用户拿到io_uring实例后,还需要调用mmap获取对应的内存映射 。我们通过liburing库的逻辑来分析 。
4 从liburing库看io_uring的使用int io_uring_queue_init_params(unsigned entries, struct io_uring *ring,struct io_uring_params *p){int fd, ret;// 调用io_uring_setup,拿到fdfd = __sys_io_uring_setup(entries, p);if (fd < 0)return -errno;// 内存映射ret = io_uring_queue_mmap(fd, p, ring);// 保存系统支持的属性ring->features = p->features;return 0;}我们重点看一下io_uring_queue_mmap 。
int io_uring_queue_mmap(int fd, struct io_uring_params *p, struct io_uring *ring){int ret;memset(ring, 0, sizeof(*ring));ret = io_uring_mmap(fd, p, &ring->sq, &ring->cq);// 记录flags和fdif (!ret) {ring->flags = p->flags;ring->ring_fd = fd;}return ret;}继续看io_uring_mmap 。
static int io_uring_mmap(int fd, struct io_uring_params *p,struct io_uring_sq *sq, struct io_uring_cq *cq){size_t size;int ret;// 请求队列需要映射的内存大小,即整个结构体struct io_rings结构体的大小sq->ring_sz = p->sq_off.array + p->sq_entries * sizeof(unsigned);// 请求队列和完成队列映射的内存大小一样,等于请求队列的cq->ring_sz = sq->ring_sz;// 映射并拿到虚拟地址,大小是sq->ring_szsq->ring_ptr = mmap(0, sq->ring_sz, PROT_READ | PROT_WRITE,MAP_SHARED | MAP_POPULATE, fd, IORING_OFF_SQ_RING);cq->ring_ptr = sq->ring_ptr;// 通过首地址和偏移拿到对应字段的地址sq->khead = sq->ring_ptr + p->sq_off.head;sq->ktail = sq->ring_ptr + p->sq_off.tail;sq->kring_mask = sq->ring_ptr + p->sq_off.ring_mask;sq->kring_entries = sq->ring_ptr + p->sq_off.ring_entries;sq->kflags = sq->ring_ptr + p->sq_off.flags;sq->kdropped = sq->ring_ptr + p->sq_off.dropped;sq->array = sq->ring_ptr + p->sq_off.array;// 映射保存请求队列节点的内存size = p->sq_entries * sizeof(struct io_uring_sqe);sq->sqes = mmap(0, size, PROT_READ | PROT_WRITE,MAP_SHARED | MAP_POPULATE, fd,IORING_OFF_SQES);// 同上cq->khead = cq->ring_ptr + p->cq_off.head;cq->ktail = cq->ring_ptr + p->cq_off.tail;cq->kring_mask = cq->ring_ptr + p->cq_off.ring_mask;cq->kring_entries = cq->ring_ptr + p->cq_off.ring_entries;cq->koverflow = cq->ring_ptr + p->cq_off.overflow;cq->cqes = cq->ring_ptr + p->cq_off.cqes;if (p->cq_off.flags)cq->kflags = cq->ring_ptr + p->cq_off.flags;return 0;}io_uring_mmap除了保存一些常用的字段信息外,最重要的是做了内存映射 。我们看看mmap的最后一个参数分别是IORING_OFF_SQ_RING和IORING_OFF_SQES,接下来我们看看io_uring的mmap钩子的实现 。
static int io_uring_mmap(struct file *file, struct vm_area_struct *vma){size_t sz = vma->vm_end - vma->vm_start;unsigned long pfn;void *ptr;ptr = io_uring_validate_mmap_request(file, vma->vm_pgoff, sz);pfn = virt_to_phys(ptr) >> PAGE_SHIFT;return remap_pfn_range(vma, vma->vm_start, pfn, sz, vma->vm_page_prot);}static void *io_uring_validate_mmap_request(struct file *file,loff_t pgoff, size_t sz){struct io_ring_ctx *ctx = file->private_data;loff_t offset = pgoff << PAGE_SHIFT;struct page *page;void *ptr;switch (offset) {case IORING_OFF_SQ_RING:case IORING_OFF_CQ_RING:ptr = ctx->rings;break;case IORING_OFF_SQES:ptr = ctx->sq_sqes;break;default:return ERR_PTR(-EINVAL);}page = virt_to_head_page(ptr);if (sz > page_size(page))return ERR_PTR(-EINVAL);return ptr;}这里设计的内容涉及到了复杂的内存管理,从代码中我们大概知道,返回的地址分别是ctx->rings和ctx->sq_sqes 。即我们操作mmap返回的虚拟地址时,映射到内核的数据结构是ctx的字段 。这样就完成了数据共享 。最后形成的架构图如下 。

从linux内核看io_uring的实现

文章插图
 
至此,分析就告一段落,io_uring的实现实在是复杂,需要反复阅读和思考,才能慢慢理解和了解它的原理 。
后记:io_uring作为新一代IO框架,未来应该会在各大软件中使用,尤其是对性能有极高要求的服务器,所以是非常值得关注和学习的 。




推荐阅读