用 io_uring 替代 epoll 實現高速 polling

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章