前面的文章说到 io_uring
是 Linux 中最新的原生异步 I/O 实现,实际上 io_uring
也支持 polling,是良好的 epoll 替代品。
API
使用 io_uring
来 poll 一个 fd 很简单。首先初始化 io_uring 对象(io_uring_queue_init),拿到 sqe(io_uring_get_sqe)是所有 io_uring
操作都必要的,前文已经介绍这里不做过多说明。拿到 sqe 之后,使用 io_uring_prep_poll_add 初始化 sqe 指针。
static inline void io_uring_prep_poll_add(struct io_uring_sqe *sqe, int fd,
short poll_mask);
第一个参数就是前面获得的 sqe 指针;第二个参数是你要 poll 的文件描述符;第三个是标志位,这里 io_uring 没有引入新的标志(宏),而是沿用了 poll(2)
定义的标志,如 POLLIN、POLLOUT 等。
如其他 I/O 请求一样,每个 sqe 都可以设置一个用户自己的值在里面,使用 io_uring_sqe_set_data
可以看到一次只能添加一个 poll 请求。如果有多个 fd,那么重复调用 io_uring_get_sqe
获取多个 sqe 指针分别 io_uring_prep_poll_add
即可。io_uring_get_sqe
不是系统调用不会进入内核,io_uring_prep_poll_add
则是简单的结构体参数赋值,所以没有速度问题。
添加完需要的请求后使用 io_uring_submit
统一提交、使用 io_uring_peek_cqe
获取完成情况等操作与标准异步 I/O 请求一致。
使用 io_uring
做 polling 与 epoll、poll 的默认模式有一个很大的区别就是 io_uring
的 polling 始终工作在 one-shot 模式下(等同于 epoll 的 EPOLLONESHOT
),即一旦某个 poll 操作完成,用户必须重新提交 poll 请求否则不会触发新的事件,这样保证每个 poll 请求有且只有一个响应。然后既然是 one-shot 模式,也就没有类似 epoll 中的 LT、ET 模式之分
清除进行中的 polling 请求使用 io_uring_prep_poll_remove
static inline void io_uring_prep_poll_remove(struct io_uring_sqe *sqe,
void *user_data);
也是需要 sqe 然后 submit。可以看到这个函数很特别的直接需要 user_data 参数。内核是在用之前提交的 user_data 和你现在指定的 user_data 做对比,删除值相等的请求。
示例
在网络编程中最开始的需求就是异步监听客户端接入(O_NONBLOCK accept),这也是好多 epoll 的代码示例。用 io_uring
如下:
int sockfd = socket(...);
bind(...);
listen(...);
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_poll_add(sqe, sockfd, POLLIN);
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
int clientfd = accept(sockfd, ...);
完
个人感觉如果拿 io_uring
纯做 polling 的话没有什么优势。拿 io_uring
做 polling 最有用的一点是把 polling 和 aio 的完成事件做统一监听和处理。想象拿到 clientfd 之后就可以立即使用 io_uring_prep_readv
读取请求体,同时又可以再使用 io_uring_prep_poll_add
接受其他客户端接入,这样才是真正的异步编程。