io_submit:Linux內核新加入的epoll替代方案

2020-12-11 蟲蟲搜奇

在Linux內核4.18版本中新添加了一種新的內核輪詢接口Linux AIO 方法IOCB_CMD_POLL。該補丁由Christoph Hellwig,還提議將Linux AIO接口配合網絡套接字等一起使用。

Linux的AIO是最初是設計用於磁碟異步IO的接口。文件與網絡套接字是大相逕庭的東西,可以用Linux AIO 接口,將其統一起來呢?

在本文中,我們介紹如何使用Linux AIO API的優勢來編寫更好,更快的網絡伺服器。

Linux AIO簡介

我們先來介紹一下Linux AIO。

Linux AIO用於將異步磁碟IO暴露給用戶空間。在Linux上所有磁碟操作都是阻塞的。無論是open(),read(),write()還是fsync(),如果所需要的數據和元數據還沒有在磁碟緩存準備好,則線程就會掛住。通常這不是問題。如果做少量的IO操作或者內存足夠大,則磁碟syscall會逐漸填充高速緩存,這樣整體來說速度還是會很快。

但是對於IO繁重的工作負載(例如資料庫或緩存Web代理),IO操作性能會下降很多。在這類應用中,很可能由於一些read()系統調用等待磁碟導致卡頓那是致命的。

要解決此類問題,通常變通使用如下方法:

使用線程池並將卸載的系統調用到工作線程。這就是glibc POSIX AIO(不要與Linux AIO混淆)包裝器的作用。

posix_fadvise使用預熱磁碟緩存,並希望達到最佳效果。

將Linux AIO與XFS文件系統一起使用,使用O_DIRECT打開文件,並避免出現未說明的陷阱。

這些變通的方法都不是完美的。

即使不小心使用Linux AIO,也可能會阻塞io_submit()調用。

Linux異步I/O(AIO)自從產生以來爭議就很多,大多數人希望的至少是異步的,實際上也沒有實現。但是由於種種原因,AIO操作可能會在內核中阻塞,從而使AIO在調用線程確實無法承受的情況下難以使用。

最簡單的AIO示例

要使用Linux AIO,首先需要定所需的系統調用。glibc不提供包裝函數。要使用Linux AIO,需要:

首先調用io_setup()以設置aio_context數據結構。內核提供了一個不透明的指針。

然後調用io_submit()提交一個"I/O控制塊"向量結構體 iocb進行處理。

最後,調用io_getevents() 塊並等待一個向量結構體 io_event-iocb的完成通知。

一個iocb中可以提交8個命令。4.18內核中引入了兩個讀取,兩個寫入,兩個fsync變體和一個POLL命令:

IOCB_CMD_PREAD = 0,

IOCB_CMD_PWRITE = 1,

IOCB_CMD_FSYNC = 2,

IOCB_CMD_FDSYNC = 3,

IOCB_CMD_POLL = 5, /* 4.18 */

IOCB_CMD_NOOP = 6,

IOCB_CMD_PREADV = 7,

IOCB_CMD_PWRITEV = 8,

在iocb結構體傳遞到io_submit,調整為磁碟IO。這是一個簡化的版本:

struct iocb {

__u64 data; /* 用戶數據 */

...

__u16 aio_lio_opcode; /* IOCB_CMD_ */

...

__u32 aio_fildes; /* 文件描述符 */

__u64 aio_buf; /* 緩衝指針 */

__u64 aio_nbytes; /* 緩衝大小*/

...

}

從io_getevents以下位置檢索完成通知:

struct io_event {

__u64 data; /* 用戶數據 */

__u64 obj; /* iocb請求指針 */

__s64 res; /* 事件結果碼*/

__s64 res2; /* 第二結果*/

};

讓我們舉一個簡單的例子,使用Linux AIO API讀取/etc/passwd文件:

fd = open("/etc/passwd", O_RDONLY);

aio_context_t ctx = 0;

r = io_setup(128, &ctx);

char buf[4096];

struct iocb cb = {.aio_fildes = fd,

.aio_lio_opcode = IOCB_CMD_PREAD,

.aio_buf = (uint64_t)buf,

.aio_nbytes = sizeof(buf)};

struct iocb *list_of_iocb[1] = {&cb};

r = io_submit(ctx, 1, list_of_iocb);

struct io_event events[1] = {{0}};

r = io_getevents(ctx, 1, 1, events, NULL);

bytes_read = events[0].res;

printf("read %lld bytes from /etc/passwd\n", bytes_read);

我們用strace追蹤程序的執行

這一切都OK!但磁碟讀取並非異步的,io_submit系統調用被阻止並完成了所有工作!io_getevents通話瞬間完成。

我們可以嘗試使磁碟異步讀取,但這需要O_DIRECT標誌來跳過緩存。

讓舉另一個更好能說明io_submit普通文件的阻止性質。從文件中讀取大的1GiB塊時顯示strace /dev/zero:

io_submit(0x7fe1e800a000, 1, [{aio_lio_opcode=IOCB_CMD_PREAD, aio_fildes=3, aio_buf=0x7fe1a79f4000, aio_nbytes=1073741824, aio_offset=0}]) = 1 <0.738380>

io_getevents(0x7fe1e800a000, 1, 1, [{data=0, obj=0x7fffb9588910, res=1073741824, res2=0}], NULL) = 1 <0.000015>

內核中io_submit花費738ms,只有15us io_getevents消耗的。內核的行為與網絡套接字相同,所有工作都在io_submit中完成。

用Linux AIO處理Socket

io_submit的處理相當保守。除非傳遞的描述符是O_DIRECT文件,否則它將阻塞並執行請求的操作。對於網絡套接字:

對於阻塞套接字,IOCB_CMD_PREAD將掛起,直到數據包到達為止。

對於非阻塞套接字,IOCB_CMD_PREAD返回-11(EAGAIN)。

這些語法與read()系統調用完全相同。對於網絡套接字而言,io_submit並不比舊式讀/寫調用更好用。

重要的是要注意iocb傳遞給內核的請求是按順序進行的。

儘管Linux AIO不能幫助異步操作,但它絕對可以用於系統調用批處理。如果有一個Web伺服器需要從數百個網絡套接字發送和接收數據,那麼使用io_submit是個好主意。這可以避免不必的send和recv數百次調用,將提高性能。從用戶空間和內核之間來回跳轉是需要耗時的。

為了說明的io_submit批處理,我們創建一個小程序,將數據從一個TCP套接字轉發到另一個。最簡單的形式,如果沒有Linux AIO,該程序將像下面這樣簡單:

while True:

d = sd1.read(4096)

sd2.write(d)

我們可以使用Linux AIO表達相同的邏輯。該代碼將如下:

struct iocb cb[2] = {{.aio_fildes = sd2,

.aio_lio_opcode = IOCB_CMD_PWRITE,

.aio_buf = (uint64_t)&buf[0],

.aio_nbytes = 0},

{.aio_fildes = sd1,

.aio_lio_opcode = IOCB_CMD_PREAD,

.aio_buf = (uint64_t)&buf[0],

.aio_nbytes = BUF_SZ}};

struct iocb *list_of_iocb[2] = {&cb[0], &cb[1]};

while(1) {

r = io_submit(ctx, 2, list_of_iocb);

struct io_event events[2] = {};

r = io_getevents(ctx, 2, 2, events, NULL);

cb[0].aio_nbytes = events[1].res;

}

以上代碼向io_submit提交兩個作業。首先,請求將數據寫入sd2,然後從sd1中讀取數據。讀取完成後,代碼將確定寫緩衝區的大小並再次循環。該代碼用了一個很酷的技巧:第一次寫入大小為0。之所以這樣做,是因為我們可以在一個io_submit中融合寫入+讀取(但不能讀取+寫)。讀取完成後,我們必須修複寫入緩衝區的大小。

這代碼比簡單的讀/寫版本快嗎?還不是。

兩個版本都有兩個系統調用:read + write和io_submit + io_getevents。

但是我們改善它。

擺脫io_getevents

當運行io_setup()時,內核為該進程分配幾頁內存。這是此內存塊在/proc/<pid> /maps中的樣子:

cat /proc/`pidof -s aio_passwd`/maps

...

7f7db8f60000-7f7db8f63000 rw-s 00000000 00:12 2314562 /[aio] (deleted)

...

內存區域由io_setup分配。內存範圍用於存儲完成事件的環形緩衝區。在大多數情況下,沒有任何理由對io_geteventssyscall真正的調用。可以從環形緩衝區輕鬆檢索完成數據,而無需請求內核。下面是一個無需內核調用的修訂版本:

int io_getevents(aio_context_t ctx, long min_nr, long max_nr,

struct io_event *events, struct timespec *timeout)

{

int i = 0;

struct aio_ring *ring = (struct aio_ring*)ctx;

if (ring == NULL || ring->magic != AIO_RING_MAGIC) {

goto do_syscall;

}

while (i < max_nr) {

unsigned head = ring->head;

if (head == ring->tail) {

break;

} else {

/* There is another completion to reap */

events[i] = ring->events[head];

read_barrier();

ring->head = (head + 1) % ring->nr;

i++;

}

}

if (i == 0 && timeout != NULL && timeout->tv_sec == 0 && timeout->tv_nsec == 0) {

return 0;

}

if (i && i >= min_nr) {

return i;

}

do_syscall:

return syscall(__NR_io_getevents, ctx, min_nr-i, max_nr-i, &events[i], timeout);

}

通過此代碼修復了該io_getevents功能, Linux AIO版本的TCP代理每個循環僅只需要一個系統調用,並且讀寫版本代碼快一點。

替代Epoll

通過在內核4.18中添加IOCB_CMD_POLL,io_submit還可以用於替代select/poll/epoll。例如,以下代碼在Socket監聽等待數據:

struct iocb cb = {.aio_fildes = sd,

.aio_lio_opcode = IOCB_CMD_POLL,

.aio_buf = POLLIN};

struct iocb *list_of_iocb[1] = {&cb};

r = io_submit(ctx, 1, list_of_iocb);

r = io_getevents(ctx, 1, 1, events, NULL);

strace追蹤顯示:

io_submit(0x7fe44bddd000, 1, [{aio_lio_opcode=IOCB_CMD_POLL, aio_fildes=3}]) = 1 <0.000015>

io_getevents(0x7fe44bddd000, 1, 1, [{data=0, obj=0x7ffef65c11a8, res=1, res2=0}], NULL) = 1 <1.000377>

如上,該程序"異步"部分運行良好,io_submit立即完成,io_getevents等待數據成功阻塞了1秒鐘。這是非常強大的功能,可以代替epoll_wait()系統調用使用。

另外,通常情況下,epoll雜項處理需要epoll_ctl系統調用。應用程式開發人員竭盡全力避免過多地調用調用。使用io_submit輪詢可以解決整個的複雜工作,並且過程中不需要任何虛假的系統調用。只需將套接字推給iocb請求向量,只調用io_submit一次並等待完成即可。

總結

本文中,我們回顧了Linux AIO接口。儘管最初被認為是僅用於磁碟的接口API,但它似乎與網絡套接字上的常規讀/寫系統調用的工作方式相同。但是與讀/寫不同的是,它允許系統調用批處理io_submit,從而可以提高性能。從4.18版內核開始io_submit,io_getevents可用於等待網絡套接字上的事件如POLLIN和POLLOUT,用於替代epoll()事件循環。

相關焦點

  • 宋寶華: 資料庫為什麼有可能喜歡Linux AIO(異步I/O)?
    我們都知道Linux的IO模型有阻塞、非阻塞、SIGIO、多路復用(select,epoll)、AIO(異步I/O)等。資料庫可能比較傾向於使用AIO。從時序上面來講,AIO是用戶應用發起IO請求io_submit()後,它就不需要去等待,讓後臺給它搞定讀寫。
  • 從linux源碼看epoll
    從linux源碼看epoll前言在linux的高性能網絡編程中,繞不開的就是epoll。
  • 「正點原子Linux連載」第五十二章Linux阻塞和非阻塞IO實驗
    Linux內核提供了等待隊列(wait queue)來實現阻塞進程的喚醒工作,如果我們要在驅動中使用等待隊列,必須創建並初始化一個等待隊列頭,等待隊列頭使用結構體wait_queue_head_t表示,wait_queue_head_t結構體定義在文件include/linux/wait.h中,結構體內容如下所示:示例代碼52.1.2.1 wait_queue_head_t
  • linux開發各種I/O操作簡析,以及select、poll、epoll機制的對比
    在linux中,默認情況下所有的socket都是阻塞的,一個典型的讀操作流程大概是這樣:當用戶進程調用了read()/recvfrom() 等系統調用函數,它會進入內核空間中,當這個網絡I/O沒有數據的時候,內核就要等待數據的到來,而在用戶進程這邊,整個進程會被阻塞,直到內核空間返回數據。
  • epoll和select的區別
    4.一種較好的方式為I/O多路轉接(I/O multiplexing)(貌似也翻譯多路復用),先構造一張有關描述符的列表(epoll中為隊列),然後調用一個函數,直到這些描述符中的一個準備好時才返回,返回時告訴進程哪些I/O就緒。select和epoll這兩個機制都是多路I/O機制的解決方案,select為POSIX標準中的,而epoll為Linux所特有的。
  • 框架篇:見識一下linux高性能網絡IO+Reactor模型
    這就是「I/O多路復用」,多路是指多個socket套接字,復用是指復用同一個進程linux提供了select、poll、epoll等多路復用I/O的實現方式,是現階段主流框架常用的高性能I/O模型與阻塞IO不同,select不會等到socket數據全部到達再處理,而是有了一部分socket數據準備好就會恢復用戶進程來處理。怎麼知道有一部分數據在內核准備好了呢?
  • 35-python高級篇-C10K問題和io多路復用
    信號處理程序接收信號是在內核數據拷貝之後,其省去了數據報準備好時的狀態獲取過程,同時也在aio.read後做到了立即返回。不過異步IO相較其他模式,實現上會複雜很多,目前做到大量使用的仍是框架較成熟的IO復用技術。IO復用中的select,poll和epoll    select,poll和epoll都是IO多路復用的機制。
  • 【面試】徹底理解 IO多路復用?
    等系統調用獲取fd列表,遍歷有事件的fd進行accept/recv/send,使其能支持更多的並發連接請求fds = [listen_fd]// 偽代碼描述while(1) {  // 通過內核獲取有讀寫事件發生的fd,只要有一個則返回,無則阻塞  // 整個過程只在調用select、poll、epoll這些調用的時候才會阻塞
  • 【面試】徹底理解 IO多路復用
    等系統調用獲取fd列表,遍歷有事件的fd進行accept/recv/send,使其能支持更多的並發連接請求fds = [listen_fd]// 偽代碼描述while(1) {  // 通過內核獲取有讀寫事件發生的fd,只要有一個則返回,無則阻塞  // 整個過程只在調用select、poll、epoll這些調用的時候才會阻塞
  • Golang是怎麼利用epoll的
    常見的IO多路復用函數有select,poll,epoll。select與poll的最大缺點是每次調用時都需要傳入所有要監聽的fd集合,內核再遍歷這個傳入的fd集合,當並發量大時候,用戶態與內核態之間的數據拷貝以及內核輪詢fd又要浪費一波系統資源(關於select與poll這裡不展開)。
  • Linux 內核學習:環境搭建和內核編譯
    如果沒有加入,或者想加入別的CD安裝源也非常簡單,只需執行以下操作:將ISO加入虛擬機掛載光碟:sudo mount /dev/cdrom /media/cdrom將光碟加入安裝源:sudo apt-cdrom add打開/etc/apt/sources.list查看是否添加成功更新軟體件表:sudo
  • MySQL 引擎特性:InnoDB IO 子系統
    如果上述三者不是文件系統邏輯塊大小的整數倍,則在調用讀寫函數時候會報錯EINVAL,但是如果文件不使用O_DIRECT打開,則程序依然可以運行,只是退化成同步IO,阻塞在io_submit函數調用上。後臺有若干異步io處理線程(innobase_read_io_threads和innobase_write_io_threads這兩個參數控制)不斷從這個隊列中取出請求,然後使用同步IO的方式完成讀寫請求以及讀寫完成後的工作。另外一種就是Native aio。目前在linux上使用io_submit,io_getevents等函數完成(不使用glibc aio,這個也是模擬的)。
  • Linux2.6內核驅動移植參考
    作者:晏渭川 隨著Linux2.6的發布,由於2.6內核做了教的改動,各個設備的驅動程序在不同程度上要 進行改寫。為了方便各位Linux愛好者我把自己整理的這分文檔share出來。該文當列舉 了2.6內核同以前版本的絕大多數變化,可惜的是由於時間和精力有限沒有詳細列出各個 函數的用法。
  • (二十五)深入淺出TCPIP之 epoll和select,poll的區別
    但select,poll,epoll本質上都是同步I/O,因為他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。          epoll跟select都能提供多路I/O復用的解決方案。
  • Linux 系統內核的調試
    但是,Linux 系統的開發者出於保證內核代碼正確性的考慮,不願意在 Linux 內核原始碼樹中加入一個調試器。他們認為內核中的調試器會誤導開發者,從而引入不良的修正[1]。所以對 Linux 內核進行調試一直是個令內核程式設計師感到棘手的問題,調試工作的艱苦性是內核級的開發區別於用戶級開發的一個顯著特點。
  • epoll原理簡介
    epoll_create使用epoll時需要使用epoll_create()創建一個epoll的文件句柄,epoll_create()函數的原型如下:intepoll_create(int size);此接口用於創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。
  • 淺談分析Arm linux 內核移植及系統初始化的過程二
    的內核線程,繼續初始化。MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch * to SMDK2410 *//* Maintainer: Jonas Dietsche */.phys_io = S3C2410_PA_UART,.io_pg_offst
  • Linux內核編譯初體驗
    下載內核在ftp://ftp.kernel.org/pub/linux/kernel/下載原版內核本文引用地址:http://www.eepw.com.cn/article/201611/319326.htm此處使用linux-2.6.22.6.tar.bz22.
  • Ubuntu中升級Linux內核
    新內核4.2有哪些改進:  ●重寫英特爾的x86彙編代碼  ●支持新的ARM板和SoC  ●對F2FS的per-file加密  ●AMD GPU內核DRM驅動程序  ●對Radeon DRM驅動的VCE1視頻編碼支持  ●初步支持英特爾Broxton Atom SoC