異步IO:新時代的IO處理利器
無論是非阻塞IO,IO復用,還是信號驅動式IO,都不是真正意義上的IO,真正的異步IO是數據從內核空間拷貝到用戶空間也是異步處理的,拷貝完成,再通知應用進程,應用進程直接讀取用戶空間的數據進行操作。
到目前為止,我們介紹了阻塞IO,非阻塞IO,信號驅動式IO,IO復用,我們打個形象的比方,來對這幾種IO做下區分。
我們去網上買東西,下完單之后,你可以有如下幾種處理方式:
- 下完單之后,在門口一直等待快遞小哥把快遞送上門,這就是同步阻塞IO;
- 下完單之后就不管了,直到快遞小哥打電話給你通知你去取快遞,這就是同步非阻塞IO里面的信號驅動式IO;
- 下完單之后,你定時的去物流app上面查看你所有快遞的狀態,只要有快遞送到了寄存點,你就去取,這就是同步非阻塞IO里面的IO復用;
- 下完單之后,你就不管了,直到快遞小哥給你送上門,你直接拿到了快遞,你不用出門就可以拿到快遞了,這就是異步IO。
異步IO最關鍵的一點就是在讀取數據的時候,將IO的buffer提交給內核,讓內核往這個buffer寫數據。
這節我們就來介紹下異步IO模型和相關API,并且順便介紹下當下最新的更高性能的IO模型。
閱讀完本文,你將了解到:
- 異步IO的原理;
- POSIX下定義的異步IO接口以及使用方式;
- 異步IO的發展方向。
1、異步I/O模型介紹
下面是異步IO(asynchronous I/O)的執行流程流程:
通過異步處理函數如aio_read告知內核啟動某個動作,并且讓內核在整個操作完成之后再通知應用進程,內核會在把數據復制到用戶空間緩沖區之后再進行通知。整個IO過程應用進程都不會被阻塞。
異步IO最大的優化點在于:系統調用是昂貴的,異步IO將輪訓等待數據的系統調用(如select,poll,epoll)和讀取數據操作合并起來。
下面我們就通過具體了例子來演示下異步IO程序的處理流程。
2、異步IO相關函數使用案例
本節我們介紹下POSIX定義的異步操作接口。
2.1、異步IO相關API
每個異步函數都需要傳入一個aiocb結構(異步IO控制塊),這個結構格式如下:
- struct aiocb {
- /* The order of these fields is implementation-dependent */
- int aio_fildes; /* File descriptor */
- off_t aio_offset; /* File offset */
- volatile void *aio_buf; /* Location of buffer */
- size_t aio_nbytes; /* Length of transfer */
- int aio_reqprio; /* Request priority */
- struct sigevent aio_sigevent; /* Notification method */
- int aio_lio_opcode; /* Operation to be performed;
- lio_listio() only */
- /* Various implementation-internal fields not shown */
- };
該結構體指定了要異步操作的套接字描述符,操作過程中用到的緩沖,其中aio_sigevent告訴AIO在IO操作完成時,應該指向什么操作。
常見的異步IO相關函數如下:
INT AIO_READ(STRUCT AIOCB *AIOCBP)
請求異步讀操作,該函數將aiocbp指向的緩沖區描述的I/O請求排隊。
注意:aio_read的aiocbp中一定要設置偏移量
在傳統的非異步read操作中,偏移量是在文件描述符上下文進行維護的,對于每個操作,偏移量都需要更新,以便后續的操作可以對下一塊數據進行尋址。
而對于異步read操作來說,可以同時執行很多異步IO read操作,所以這里需要的指明處理的文件的偏移量aiocbp->aio_offset和異步讀取的內容的長度aiocbp->aio_nbytes。
aio_read調用后,文件偏移量變為未設置。
INT AIO_WRITE(STRUCT AIOCB *AIOCBP)
請求異步寫操作,該函數將aiocbp指向的緩沖區描述的I/O請求排隊。
aio_write不一定要設置偏移量
如果打開的文件,設置了O_APPEND選項,那么偏移量就會被忽略,數據會被附加到文件的末尾;如果未設置O_APPEND,那么從aiocbp->aio_offset開始寫入數據,而不考慮文件的偏移量。
SSIZE_T AIO_RETURN(STRUCT AIOCB *AIOCBP)
獲取完成的異步請求的返回狀態。
由于IO異步化了,需要有專門的函數來獲取異步處理的狀態。
aio_return的返回值即相當于read或write等系統調用的返回值。如果出錯,則返回-1,并正確設置errno。
可能的響應值:
- 成功后,將返回處理的字節數;
- -1:發生錯誤,并且設置errno以指示錯誤原因;
只有在aio_error調用返回EINPROGRESS之外的值之后,才可以調用這個函數,并且只允許調用一次。
INT AIO_ERROR(CONST STRUCT AIOCB *AIOCBP)
檢查異步請求的狀態,可能的響應值:
- EINPROGRESS:如果請求還沒有完成;
- ECANCELED:如果請求已經被取消;
- 0:如果請求已完成;
- 如果異步IO操作失敗,則為一個正數的error number,與同步的read(2), write(2), fsync(2),或者 or fdatasync(2)系統的errorno一致。
AIO_SUSPEND
- int aio_suspend(const struct aiocb * const aiocb_list[],
- int nitems, const struct timespec *timeout);
掛起調用進程,直到一個或者多個異步請求完成或失敗。
aiocb_list中存放需要等待的異步請求,如:
- struct aioct *cblist[MAX_LIST];
- ...
- cblist[0] = &aiocb1;
- ret = aio_read( my_aiocb1 );
- ret = aio_suspend( cblist, MAX_LIST, NULL );
INT AIO_CANCEL(INT FD, STRUCT AIOCB *AIOCBP)
取消異步IO請求。
LIO_LISTIO
- int lio_listio(int mode, struct aiocb *const aiocb_list[],
- int nitems, struct sigevent *sevp);
發起一系列的IO操作,啟動數組aiocb_list描述的I/O操作列表。
下面通過具體例子展示aio的用法。
2.2、aio_read例子
如下是一個使用aio_read的例子:
我把重要的處理步驟都標注起來了,并在代碼中做了說明,這里不重復描述,需要注意幾點:
- aio_read的aiocbp中一定要設置偏移量;
- 一定要在調用aio_error,并且返回值不是EINPROGRESS之后,才調用aio_return后去異步IO處理狀態。
以上就是目前異步IO API的設計和基本使用方法。
3、操作系統對異步IO的支持情況
3.1、Linux下的異步IO
上一節介紹了POSIX下定義的異步操作接口,但是可惜Linux的aio操作不是真正的操作系統級別的支持,而是在用空間中借由GNU庫函數由pthread方式實現的,沒有對套接字IO進行支持。
基于以上原因,Linux下面,大部分還是通過使用epoll多路復用技術,以及非阻塞IO,通過事件分發模型來構建高性能網絡程序。
3.2、Windows下的異步IO
Windows實現了一套稱為IOCP(I/O Completion Ports,IO完成端口)[1]的完整的異步編程接口。IOCP提供了一種有效的線程模型,用于在多處理器系統上處理多個異步I / O請求。
當進程創建IOCP時,系統會為請求創建關聯的隊列對象,其唯一目的是為這些請求提供服務。
一個進程通過將IOCP與預分配的線程池結合使用,來處理許多并發異步IO請求,相比于通過在接收IO請求時創建線程,會更快,更高效。
基于IOCP,產生了Proactor模式,一種與Reactor模式類似,但是更加高效的模式。
這里是不是看的有點不太懂,沒關系,在后續高性能網絡編程范式章節中,我們會詳細介紹這兩種模式。
4、更高效的IO
4.1、背景
由于Linux下并沒有廣泛被采用的AIO技術,aio系列的函數是有POSIX定義的異步操作接口,并不是真正操作系統內核支持的異步IO。
目前最流行的還是基于epoll的多路復用技術,以及依托多路復用技術產生的Reactor模式。
為了推動AIO在Linux系統的發展,實現更加高效的IO,于是后來變有了io_uring。
4.2、io_uring
io_uring是在Linux Kernel 5.1中添加的,用于替代AIO和io_submit,構造通用的異步系統調用接口。
關于異步IO就介紹到這里,在下一篇文章中,我們會詳細探討使用各種IO模型的高性能網絡編程范式。
博客鏈接:https://www.itzhai.com