linux AIO (异步IO) 那点事儿
生活随笔
收集整理的這篇文章主要介紹了
linux AIO (异步IO) 那点事儿
小編覺得挺不錯的,現在分享給大家,幫大家做個參考.
在高性能的服務器編程中,IO 模型理所當然的是重中之重,需要謹慎選型。對于網絡套接字,我們可以采用epoll 的方式來輪詢,盡管epoll也有一些缺陷,但總體來說還是很高效的,尤其來大量套接字的場景下;但對于Regular File 來說,是不能夠用采用 poll/epoll 的,即O_NOBLOCK 方式對于傳統文件句柄是無效的,也就是說我們的 open ,read, mkdir 之類的Regular File操作必定會導致阻塞。在多線程、多進程模型中,可以選擇以同步阻塞的方式來進行IO操作,任務調度由操作系統來保證公平性,但在單進程/線程模型中,以nodejs 為例 ,假如 我們需要在一個用戶請求中處理10個文件:?
這時候進程至少會阻塞10次,而這可能會導致其他的上千個用戶請求得不到處理,這當然是不能接受的.?
Linux AIO 早就被提上議程,目前比較知名的有 Glibc 的 AIO ??與 Kernel Native AIO?
Glibc AIO: http://www.ibm.com/developerworks/linux/library/l-async/ ?
Kernel Native AIO:? http://lse.sourceforge.net/io/aio.html ?
我們用Glibc 的AIO 做個小實驗,寫一個簡單的程序:異步方式讀取一個文件,并注冊異步回調函數:
int ?main(){struct aiocb my_aiocb;fd = open("file.txt", O_RDONLY);...my_aiocb.aio_sigevent.sigev_notify_function = aio_completion_handler;…ret = aio_read(&my_aiocb);…write(1, "caller thread\n", 14);sleep(5);}void aio_completion_handler(sigval_t sigval){write(1, "callback\n", 9);struct aiocb *req;...req = (struct aiocb *)sigval.sival_ptr;printf("data: %s\n" ,req->aio_buf);return;}
我們用 strace 來跟蹤調用,得到以下結果 (只保留主要語句):?
23908 open("file.txt", O_RDONLY) ???????= 3?
23908 clone(...) = 23909?
23908 write(1, "caller thread\n", 14) ??= 14?
23908 nanosleep({5, 0}, ??
...?
23909 pread(3, "hello, world\n", 1024, 0) = 13?
23909 clone(..)= 23910?
23909 futex(0x3d3a4082a4, FUTEX_WAIT_PRIVATE, 1, {0, 999942000}?
...?
23910 write(1, "callback\n", 9) ????????= 9?
23910 write(1, "data: hello, world\n", 19) = 19?
23910 write(1, "\n", 1) ????????????????= 1?
23910 _exit(0) ?????????????????????????= ??
23909 <... futex resumed> ) ????????????= -1 ETIMEDOUT (Connection timed out)?
23909 futex(0x3d3a408200, FUTEX_WAKE_PRIVATE, 1) = 0?
23909 _exit(0) ?????????????????????????= ??
23908 <... nanosleep resumed> {5, 0}) ??= 0?
23908 exit_group(0) ????????????????????= ??
在Glibc AIO 的實現中, 用多線程同步來模擬 異步IO ,以上述代碼為例,它牽涉了3個線程,?
主線程(23908)新建 一個線程(23909)來調用 阻塞的pread函數,當pread返回時,又創建了一個線程(23910)來執行我們預設的異步回調函數, 23909 等待23910結束返回,然后23909也結束執行..?
實際上,為了避免線程的頻繁創建、銷毀,當有多個請求時,Glibc AIO 會使用線程池,但以上原理是不會變的,尤其要注意的是:我們的回調函數是在一個單獨線程中執行的.?
Glibc AIO 廣受非議,存在一些難以忍受的缺陷和bug,飽受詬病,是極不推薦使用的.?
詳見: http://davmac.org/davpage/linux/async-io.html?
在Linux 2.6.22+ 系統上,還有一種 Kernel AIO 的實現,與Glibc 的多線程模擬不同 ,它是真正的做到內核的異步通知,比如在較新版本的Nginx 服務器上,已經添加了AIO方式 的支持.?
http://wiki.nginx.org/HttpCoreModule?
aio?
syntax: aio [on|off|sendfile]?
default: off?
context: http, server, location?
This directive is usable as of Linux kernel 2.6.22. For Linux it is required to use directio,? this automatically disables sendfile support.?
location /video {?
aio on;?
directio 512;?
output_buffers 1 128k;?
}?
聽起來Kernel Native AIO 幾乎提供了近乎完美的異步方式,但如果你對它抱有太高期望的話,你會再一次感到失望.?
目前的Kernel AIO 僅支持 O_DIRECT 方式來對磁盤讀寫,這意味著,你無法利用系統的緩存,同時它要求讀寫的的大小和偏移要以區塊的方式對齊,參考nginx 的作者 Igor Sysoev 的評論:? http://forum.nginx.org/read.php?2,113524,113587#msg-113587?
nginx supports file AIO only in 0.8.11+, but the file AIO is functional?
on FreeBSD only. On Linux AIO is supported by nginx only on kerenl?
2.6.22+ (although, CentOS 5.5 has backported the required AIO features).?
Anyway, on Linux AIO works only if file offset and size are aligned?
to a disk block size (usually 512 bytes) and this data can not be cached?
in OS VM cache (Linux AIO requires DIRECTIO that bypass OS VM cache).?
I believe a cause of so strange AIO implementaion is that AIO in Linux?
was developed mainly for databases by Oracle and IBM.?
同時注意上面的橙色字部分,啟用AIO 就會關閉sendfile -這是顯而易見的,當你用Nginx作為靜態服務器,你要么選擇以AIO 讀取文件到用戶緩沖區,然后發送到套接口,要么直接調用sendfile發送到套接口,sendfile 雖然會導致短暫的阻塞,但開啟AIO 卻無法充分的利用緩存,也喪失了零拷貝的特征 ;當你用Nginx作為動態服務器,比如 fastcgi + php 時,這時php腳本中文件的讀寫是由php 的 文件接口來操作的,這時候是多進程+同步阻塞模型,和文件異步模式扯不上關系的.?
所以現在Linux 上,沒有比較完美的異步文件IO 方案,這時候苦逼程序員的價值就充分體現出來了,libev 的作者 Marc Alexander Lehmann 老大就重新實現了一個AIO library :?
http://software.schmorp.de/pkg/libeio.html?
其實它還是采用線程池+同步模擬出來的,和Glibc 的 AIO 比較像,用作者的話說,這個庫相比與Glibc 的實現,開銷更小,bug更少(不然重新造個輪子還有毛意義呢?反正我是信了) ,不過這個輪子的代碼可讀性實在不敢恭維,Marc 老大自己也說了:Currently in BETA! Its code, documentation, integration and portability quality is currently below that of libev, but should soon be ready for use in production environments.?
(其實libev代碼和文檔可讀性也不咋地,貌似驅動內核搞多了都這樣?)好吧,腹誹完了,我們還是閱讀下它的源碼 ,來稍微分析一下它的原理:?
(這個文章的流程圖還是蠻靠譜的: http://cnodejs.org/blog/?p=244??,此處更詳細的補充一下下)?
int eio_init (void (*want_poll)(void), void (*done_poll)(void))?
初始化時設定兩個回調函數,它有兩個全局的數據結構 : req 存放請求隊列,res 存放已經完成的隊列 當我,當你提交一個異步請求時(eio_submit),其實是放入req隊列中,然后向線程池中處于信號等待的線程發送信號量(如果線程池中沒有線程就創建一個),獲得信號的線程會執行如下代碼:?
ETP_EXECUTE (self, req);X_LOCK (reslock);++npending;if (!reqq_push (&res_queue, req) && want_poll_cb)want_poll_cb ();X_UNLOCK (reslock);
ETP_EXECUTE 就是實際的阻塞調用,比如read,open,,sendfile之類的,當函數返回時,表明操作完成,此時加鎖方式向完成隊列添加一項 ,然后調用 want_pool ,這個函數是我們eio_init時候設置的,然后釋放鎖。?
注意:每次完成任務時,都要調用want_poll ,所以這個函數應該是線程安全且盡量短促,實際上我們為了避免陷入多線程的泥淖,我們往往配合eio使用事件輪詢機制,比如:我們創建一對管道,我們把“讀”端的管道加入 epoll 監控結構中,want_poll 函向“寫”端管道寫數入一個字節或字長 ,所以當下次epoll_wait 返回時,我們會執行 “讀” 端管道的 回調函數,類似如下:?
void r_pipe_cb(){...eio_poll();}
在eio_poll 中 有類似以下代碼:?
for(;;){X_LOCK (reslock);req = reqq_shift (&res_queue);if (req){if (!res_queue.size && done_poll_cb)done_poll_cb ();}X_UNLOCK (reslock);res = ETP_FINISH (req);...if(empty) break;}
eio_poll 函數就是從完成隊列res 依次shift ,依次執行我們的回調函數(ETP_FINISH 就是執行用戶回調),在取出完成隊列的最后一項但還沒有執行用戶回調之前,調用我們設定的done_poll ,對res隊列的操作當然也是加鎖的,注意此時我們自定義的異步回調函數是在我們的主線程中執行的!這才是我們的最終目的!?
在eio 線程池中,默認最多4個線程,在高性能的程序中,過多的進程/線程往往也是一個瓶頸,?
寄存器的進出棧還是其次,進程虛存地址切換、各級cache 的miss ,這才是最昂貴的,所以,最理想的情形就是:有幾個cpu ,就有同樣數目的active ?線程/進程,但因為io線程往往會陷入sleep模式,所以,還是需要額外的待切換的線程的,作為經驗法則,線程池的數量最好是cpu 的數目 X? 2(參見windows 核心編程 IOCP卷).?
libeio 雖不完美,但目前還是將就著用用吧 ...?
http://www.360doc.com/content/13/0128/15/7982302_262867240.shtml
總結
以上是生活随笔為你收集整理的linux AIO (异步IO) 那点事儿的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下aio异步读写详解与实例
- 下一篇: Linux AIO