標籤:驅動開發 核心 linux核心 linux kernel
/** * Author:hasen * 參考 :《linux裝置驅動開發詳解》 * 簡介:android小菜鳥的linux * 裝置驅動開發學習之旅 * 主題:非同步I/O * Date:2014-11-11 */
linux中最常用的輸入/輸出(I/O)模型是同步I/O。在這個模型中,請求發出後,應用就會阻塞,知道請求滿足
為止。但是在某些情況下,I/O請求可能需要與其他的進程進行交疊。可移植作業系統介面(POSIX)非同步I/O(AIO)
應用程式介面(API)就提供了這種功能。
AIO基本思想是允許進程發起很多的I/O操作,而不用阻塞或者等待任何操作完成。稍後或在接受到I/O操
作完成的通知時,進程再檢索I/O操作的結果。
在非同步非阻塞I/O中,每個傳輸操作都有唯一的上下文,通過aiocb(AIO I/O Control Block)結構體進行區
分,包含了有關傳輸的所有資訊,以及為資料準備的使用者緩衝區。在產生I/O通知(稱為完成)時,該結構唯一標
識所完成的I/O操作。
AIO系列API被GNU C庫函數所包含,它被POSIX.1b所要求,主要有以下的函數:
(1)aio_read int aio_read(struct aiocb *aiocbp) ;/*對一個有效檔案描述符進行非同步作業*/檔案描述符可以表示檔案,通訊端,管道,aio_read()在請求進行排隊後立即返回,執行成功返回0,出現錯誤返回-1,並設定errno的值。(2)aio_write int aio_write(struct aiocb *aiocbp) ;/*用來請求一個非同步寫操作*/ aio_write()函數會立即返回,說明請求已經進行排隊,成功返回0,失敗返回-1,並相應地設定errno。(3)aio_erroraio_error(struct aiocb *aiocbp) ;/*用來確定請求的狀態*/這個函數可以返回以下內容。 EINPROGRESS:說明請求尚未完成。 ECANCELLED:說明請求被應用程式取消了。 -1:說明發生了錯誤,具體錯誤原因由errno記錄。(4)aio_return/*非同步I/O和標準I/O方式之間的另一個區別是不能立即訪問這個函數的返回狀態,因為非同步I/O並沒有阻塞在read()調用上。在標準的read()調用中,返回狀態是在該函數返回時提供的。但是在非同步I/O中,使用aio_return()函數*/ssize_t aio_return(struct aiocb *aiocbp) ;只有在aio_error()調用確定請求已經完成(可能成功,或者發生錯誤)之後,才會調用這個函數,aio_return()函數的傳回值就等價於同步情況中read()和write()的傳回值,成功返回位元組數,錯誤返回負值。
下面的程式碼範例給出了使用者空間進行非同步讀操作,它首先開啟文檔,然後準備aiocb結構體,之後調用aio_read(&my_aiocb)進行提出非同步讀請求,當aio_error(&my_aiocb)==EINPROGRESS,即操作還在進行中,一直等待,結束後通過aio_return(&my_aiocb)獲得返回值。
#include <aio.h>...int fd ,ret ;struct aiocb my_aiocb ;fd = open("file.txt",O_RDONLY) ;if(fd<0)perror("open") ;/*清零aiocb結構體*/bzero((char *)&my_aiocb,sizeof(struct aiocb)) ;/*為aiocb結構體請求分配資料緩衝區*/my_aiocb.aio_buf = malloc(BUFSIZE + 1) ;if(!my_aiocb.aio_buf)perror("malloc") ;/*初始化aiocb的成員*/my_aiocb.aio_fildes = fd ;my_aiocb.aio_nbytes = BUFSIZE ;my_aiocb.aio_offset = 0 ;ret = aio_read(&my_aiocb) ;if(ret < 0)perror("aio_read") ;while(aio_error(&my_aiocb) == EINPROGRESS)continue ;if((ret = aio_return(&my_aiocb)) > 0){/*獲得非同步讀的傳回值*/}else{/*讀失敗,分析errno*/}
(5)aio_suspend使用者可以使用aio_suspend()函數來掛起(或阻塞)調用進程,直到非同步請求完成為止,此時會產生一個訊號,或者發生其他逾時操作。調用者提供了一個aiocb引用列表,其中任何一個完成都會導致aio_suspend()返回。int aio_suspend(const struct aiocb cblist[] ,int n,const struct timespec *timeout) ;注意,aio_suspend 的第二個參數是 cblist 中元素的個數,而不是 aiocb 引用的個數。cblist中任何NULL元素都會被 aio_suspend 忽略。如果為 aio_suspend 提供了逾時,而逾時情況的確發生了,那麼它就會返回 -1,errno 中會包含 EAGAIN。
樣本:使用者空間非同步I/O aio_suspend()函數使用
struct aioct *cblist[MAX_LIST]/*清零aioct結構體鏈表*/bzero((char *)cblist,sizeof(cblist)) ;/*將一個或更多的aiocb放入aioct結構體鏈表*/cblist[0] = &my_aiocb ;ret = aio_read(&my_aiocb) ;ret = aio_suspend(cblist,MAX_LIST,NULL) ;
(6)aio_cancelaio_cancel()函數允許使用者取消對某個檔案描述符執行的一個或者所有的I/O請求,原型如下:int aio_cancel(int fd ,struct aiocb *aiocbp) ;要取消一個請求,我們需要提供檔案描述符和 aiocb 引用。如果這個請求被成功取消了,那麼這個函數就會返回AIO_CANCELED。如果請求完成了,這個函數就會返回 AIO_NOTCANCELED。要取消對某個給定檔案描述符的所有請求,我們需要提供這個檔案的描述符,以及一個對 aiocbp的 NULL引用。如果所有的請求都取消了,這個函數就會返回 AIO_CANCELED;如果至少有一個請求沒有被取消,那麼這個函數就會返回 AIO_NOT_CANCELED;如果沒有一個請求可以被取消,那麼這個函數就會返回 AIO_ALLDONE。我們然後可以使用 aio_error 來驗證每個 AIO 請求。如果這個請求已經被取消了,那麼 aio_error 就會返回-1,並且errno 會被設定為 ECANCELED。(7)lio_listio最後,AIO 提供了一種方法使用 lio_listio API 函數同時發起多個傳輸。這個函數非常重要,因為這意味著我們可以在一個系統調用(一次核心環境切換)中啟動大量的 I/O 操作。從效能的角度來看,這非常重要,因此值得我們花點時間探索一下。lio_listio API 函數的原型如下:int lio_listio( int mode, struct aiocb *list[], int nent,struct sigevent *sig );mode 參數可以是 LIO_WAIT 或 LIO_NOWAIT。LIO_WAIT 會阻塞這個調用,直到所有的 I/O 都完成為止。在操作進行排隊之後,LIO_NOWAIT 就會返回。list 是一個 aiocb 引用的列表,最大元素的個數是由nent定義的。注意 list 的元素可以為 NULL,lio_listio 會將其忽略。sigevent 引用定義了在所有 I/O操作都完成時產生訊號的方法。
樣本:使用者空間非同步I/O lio_listio()的使用
struct aiocb aiocb1,aiocb2 ;struct aiocb *list[MAX_LIST] ;.../*準備第一個aiocb*/aiocb1.aio_fildes = fd ;aiocb1.aio_buf = malloc(BUFSIZE + 1) ;aiocb1.aio_nbytes = BUFSIZE ;aiocb1.aio_offset = next_offset ;/*LIO_READ讀操作,LIO_WRITE寫操作,LIO_NOP空操作*/aiocb1.aio_lio_opcode = LIO_READ;/*非同步讀操作*/... /*準備多個aiocb*/bzero((char *)list,sizeof(list)) ;/*將aiocb填入鏈表*/list[0] = aiocb1 ;list[1] = aiocb2 ;...ret = lio_listio(LIO_WAIT,list,MAX_LIST,NULL) ;/*發起大量I/O操作*/
使用回呼函數作為AIO的通知
除了訊號之外,應用程式還可以提供一個回調(Callback)函數給核心,以便AIO的請求完成後核心調用這個函數。
樣本:使用回呼函數作為AIO非同步I/O通知機制
/*設定非同步I/O請求*/void setup_io(...){int fd ;struct aiocb my_aiocb ;.../*設定AIO請求*/bzero((char *)&my_aiocb,sizeof(struct aiocb)) ;aiocb.aio_fildes = fd ;aiocb.aio_buf = malloc(BUFSIZE + 1) ;aiocb.aio_nbytes = BUFSIZE ;aiocb.aio_offset = next_offset ;/*串連AIO請求和線程回呼函數*/my_aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD ;my_aiocb.aio_sigevent.notify_function = aio_completion_handler ;/*設定回呼函數*/my_aiocb.aio_sigevent.notify_attributes = NULL ;my_aiocb.aio_sigevent.sigev_value_sival_ptr = &my_aiocb ;...ret = aio_read(&my_aiocb) ;}/*非同步I/O完成回呼函數*/void aio_completion_handler(sigval_t sigval){struct aiocb *req ;req = (struct aiocb *)sigval.sival_ptr ;/*AIO請求完成*/if(aio_error(req) == 0)/*請求完成,獲得傳回值*/ret = aio_return (req) ;}
上述程式在建立aiocb請求之後,使用了SIGEV_THREAD請求了一個線程回呼函數作為通知方法。在回調
函數中,通過(struct aiocb*)sigval.sival_ptr可以獲得對應的aiocb指標,使用AIO函數可驗證請求是否已經完成。
proc檔案系統包含了兩個虛擬檔案,他們可以用來對非同步I/O的效能進行最佳化。
(1)/proc/sys/fs/aio-nr 檔案提供了系統範圍非同步I/O請求現在的數目。
(2)/proc/sys/fs/aio-max-nr 檔案是允許的並發請求的最大個數,最大個數通常是64KB,這對於大部分應用
程式來說都已經足夠了。
AIO與裝置驅動
在核心中,每個I/O請求都對應於一個kiocb結構體,其ki_filp成員指向對應的file指標,通過is_sync_kiocb()
可以判斷某kiocb是否為同步I/O請求,如果返回非真,表示為非同步I/O請求。
塊裝置和網路裝置本身是非同步,只有字元裝置必須明確表明應支援AIO,AIO對於大多數字元裝置而言都
不是必須的,只有極少數裝置需要,比如,對於磁帶機,由於I/O操作很慢,這時候使用非同步I/O可以改善效能。
字元裝置驅動程式中,file_operations包含3個與AIO相關的成員函數:
ssize_t (*aio_read)(struct kiocb *iocb,char *buffer,size_t count,loff_t offset);ssize_t (*aio_write)(struct kiocb *iocb,char *buffer,size_t count,loff_t offset);ssize_t (*aio_fsync)(struct kiocb *iocb,int datasync) ;
aio_read()和aio_write()與filp_operations中的read()和write()中的offset參數不一樣,它直接傳值,
而後者是傳遞的指標,這是因為AIO從來不需要改變檔案的位置。
aio_read()和aio_write()函數本身不一定完成了讀和想寫操作,它只是發起,初始化讀和寫操作。
樣本:裝置驅動中的I/O函數
struct async_work(){struct kiocb *iocb ;/*kiocb結構體指標*/int result ;/*執行結果*/struct work_struct work ;/*工作結構體*/};.../*非同步讀*/static ssize_t xxx_aio_read(struct kiocb *iocb,char *buf,size_t count,loff_t pos){return xxx_defer_op(0,iocb,count,pos) ;}/*非同步寫*/static ssize_t xxx_aio_write(struct kiocb *iocb,const char *buf,size_t count,loff_t pos){return xxx_defer_op(1,iocb,count,pos) ;}/*初始化非同步I/O*/static int xxx_defer_op(int write,struct kiocb *iocb,char *buf,size_t count,loff_t pos){struct async_work *async_wk ;int result ;/*當我們能訪問buffer時進行copy*/if(write)result = xxx_write(iocb->ki_filp,buf,count,&pos) ;elseresult = xxx_read(iocb->filp,buf,count,&pos) ;/*如果是同步IOCB,立即返回狀態*/if(is_sync_kiocb(iocb))return result ;/*否則,推後幾秒執行*/async_wk = kmalloc(sizeof(*async_wk),GFP_KERNEL) ;if(async_wk == NULL)return result ;/*調度延遲的工作*/async_wk->iocb = iocb ;async_wk->result = result ;INIT_WORK(&async_wk->work,xxx_do_deferred_op,async_wk) ;schedule_delay_work(&async_wk->work ,Hz/100) ;return -EIOCBQUEUED ;/*控制權返回使用者空間*/}/*延遲後執行*/static void xxx_do_deferred_op(void *p){struct async_work *async_wk = (struct async_work *)p ;... /*執行I/O操作*/aio_complete(async_wk->iocb,async_wk->result,0) ;kfree(async_wk) ;}
上述代碼最核心的是使用work_struct機制通過schedule_delay_work()函數將I/O操作順延強制,而在具體的
I/O操作執行完成後,調用aio_complete()通知核心驅動程式已經完成了I/O操作。
通常而言,具體的字元裝置驅動一般不許呀AIO支援,而核心中僅有fs/direct-io.c,driver/usb/gadget/inode.c,
fs/nfs/direct.c等少量地方使用了AIO。
Hasen的linux裝置驅動開發學習之旅--非同步I/O