libeio源碼分析 – 主流程
簡介
This library provides fully asynchronous versions of most POSIX functions dealing with I/O. Unlike most asynchronous libraries, this not only includes read and write, but also open, stat, unlink and similar functions, as well as less rarely ones such as mknod, futime or readlink. It also offers wrappers around sendfile (Solaris, Linux, HP-UX and FreeBSD, with emulation on other platforms) and readahead (Linux, with emulation elsewhere>). The goal is to enable you to write fully non-blocking programs.
緣起
相信上面這段話已經將 libeio 的 feature 講的足夠清楚:提供全套異步文件操作的接口,讓使用者能寫出完全非阻塞的程序。阻塞意味著低效,但非阻塞一定要有很好的通知機制才能做到高效。
其實 linux 下的 AIO(異步 IO)并不是沒有解決方案:在用戶態,多線程同步來模擬的異步 IO,如 Glibc 的 AIO;以及在內核態實現異步通知,如 linux 內核2.6.22之后實現的 Kernel Native AIO。但兩者都存在讓使用者望而祛步的問題。
Glibc 的 AIO bug 太多,而且 IO 發起者并不是最后的 IO 終結者(callback 是在單獨的線程執行的);而 kernel Native AIO 只支持O_DIRECT 方式,無法利用 Page cache。
正是由于上述原因,Marc Alexander Lehmann 大佬決定自己開發一個 AIO 庫,及 libeio。libeio 也是在用戶態用多線程同步來模擬異步 IO,但實現更高效,代碼也更可靠,目前雖然是 beta 版,但已經可以上生產了(node.js 底層就是用 libev 和 libeio 來驅動的)。還要強調一點:libeio 里 IO 的終結者正是當初 IO 的發起者(這一點非常重要,因為 IO 都是由用戶的 request 而發起,而 IO 完成后返回給用戶的 response 也能在處理 request 的線程中完成)。
實現
一次異步 IO 操作可以分為三個階段:
初始化 –> 提交 IO –> 通知 worker 線程 (主線程);
取 request –> 執行 IO –> 通知主線程 (worker 線程);
取 response –> callback –> 結束 IO (主線程)。
下面我們通過一張流程圖來剖析一下 libeio 的源碼實現。
1. 主線程調用 eio_init 函數,主要是初始化 req_queue,res_queue 以及對應的 mutex 和 cond;
2-3. 所有的 IO 操作其實都是對 eio_sumbit 的調用,而 eio_sumbit 的職能是將 IO 操作封裝為 request 并插入到 req_queue;并調用 cond_signal 向 worker 線程發出 reqwait 已經 OK 的信號;
libeio 處理流程圖
4. worker 線程被創建后執行的函數為 etp_proc,etp_proc 啟動后會一直等待 reqwait 條件的出現;
5-6. 當 reqwait 條件變量滿足時,etp_proc 從 req_queue 中取得一個待處理的 request;并調用 eio_execute 來同步執行該 IO 操作;
7-8. eio_execute 完成后,將 response 插入到 res_queue 隊列中;同時調用 want_poll 來通知主線程 request 已經處理完畢;
9. 這里 worker 線程通知主線程的機制是通過向 pipe[1]寫一個 byte 數據;
10. 當主線程發現 pipe[0]可讀時,就調用 eio_poll;
11. eio_poll 從 res_queue 里取 response,并調用該 IO 操作在 init 時設置的 callback 函數完成后續處理;
12. 在 res_queue 中沒有待處理 response 時,調用 done_poll;
13-14. done_poll 從 pipe[0]讀出一個 byte 數據,該 IO 操作完成。
后記
libeio 的實現就是這么簡潔,這里需要說明兩點:
1. 在 worker 線程完成 IO 請求,通知主線程的機制是需要使用者自定義的,wait_poll 和 done_poll 就是 libeio 提供給使用者的接口(pipe 是一種常用的線程通知機制)。
2. worker 線程并不是為每個請求都創建一個,而是維護了一個 worker 線程池,關于這部分將會在下面的文章中單獨講到。
參考
《libeio 接口文檔》
http://pod.tst.eu/http://cvs.schmorp.de/libeio/eio.pod
《linux 異步 IO 淺析》
http://hi.baidu.com/_kouu/blog/item/e225f67b337841f42f73b341.html
《linux AIO (異步 IO) 那點事兒》
http://club.cnodejs.org/topic/4f16442ccae1f4aa270010a7