前面的文章說到 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
接受其他客戶端接入,這樣纔是真正的異步編程。