?
在Linux驅動開發中,應用程序通過循環讀取或者中斷的方式都會使得CPU的占用率很高。本文介紹五種IO模型,可以用來優化文件讀寫方式,降低CPU的使用率
?
1. 阻塞式I/O模型
阻塞式I/O模型是最常用、最簡單的模型。當應用程序對設備驅動進行操作時,若不能獲取到設備資源,阻塞式IO就會將應用程序對應的線程掛起,直到設備資源可以獲取為止 阻塞就是進程被休息, CPU處理其它進程去了。如下圖,應用程序進行recefrom系統調用,操作系統收到recefrom系統調用請求,經過等待數據準備好和內核將數據從內核緩沖區復制到用戶緩沖區這兩個階段后,調用返回,應用程序解除阻塞阻塞訪問代碼示例:
int?fd;
int?data?=?0;
fd?=?open("/dev/xxx_dev",?O_RDWR);??? /*?阻塞方式打開?*/
ret?=?read(fd,?&data,?sizeof(data));?? /*?讀取數據?*/
?
2. 非阻塞式I/O模型
對于非阻塞IO,當設備不可用或數據未準備好時會立即向內核返回一個錯誤碼,表示數據讀取失敗。應用程序會再次重新讀取數據,這樣一直往復循環,直到數據讀取成功 非阻塞就是輪詢的方式,在該模型中,I/O操作不會立即完成。例如下圖示,應用程序進行recefrom系統調用,操作系統收到recefrom系統調用請求,若無數據會立刻返回一個錯誤狀態;應用程序則需要不斷輪詢,直到內核緩沖區數據準備好非阻塞訪問代碼示例:
int?fd;
int?data?=?0;
fd?=?open("/dev/xxx_dev",?O_RDWR?|?O_NONBLOCK);??/*?非阻塞方式打開?*/
ret?=?read(fd,?&data,?sizeof(data));????? /*?讀取數據?*
?
3. I/O復用模型
由于非阻塞I/O方式需要不斷輪詢,會消耗大量CPU時間,而后臺又可能有多個任務在同時輪詢,為此人們想到了一種方式:循環查詢多個任務的完成狀態,只要有任何一個任務完成,就去處理它
IO多路復用有三個特別的系統調用select、poll和epoll
select函數:能夠監視的文件描述符數量最大為1024
/********************select函數原型***************************************/
int?select(?int?nfds,
???fd_set?*readfds,
???fd_set?*writefds,
???fd_set?*exceptfds,
???struct?timeval?*timeout?)
// nfds:所要監視的三類文件描述集合中,最大文件描述符加1
// readfds:用于監視這些文件是否可以讀取
// writefds:用于監視些文件是否可以寫操作
// exceptfds:用于監視這些文件的異常
// timeout:超時時間
//?返回值:0,超時發生;-1,發生錯誤;其他值,表示可進行操作的文件描述符個數
/************************************************************************/
/******?fd_set類型變量的每一個位都代表一個文件描述符?*****/
//?例如:從一個設備文件中讀取數據,需要定義一個fd_set變量,并傳遞給參數readfds
void?FD_ZERO(fd_set?*set)???//將fd_set變量的所有位都清零
void?FD_SET(int?fd,?fd_set?*set)?//將fd_set某個位置1,即向變量中添加文件描述符fd
void?FD_CLR(int?fd,?fd_set?*set)?//將fd_set某個位清零,即從變量中刪除文件描述符fd
int?FD_ISSET(int?fd,?fd_set?*set)?//測試一個文件fd是否屬于某個集合
/******?timeval結構體定義?*****/
struct?timeval?{
?long?tv_sec;??//秒?
?long?tv_usec;??//微妙
};
poll函數:能夠監視的文件描述符數量沒有限制
/********************poll函數原型***************************************/
int?poll(struct?pollfd?*fds,
???nfds_t?nfds,
???int?timeout)
// fds:要監視的文件描述符集合以及要監視的事件,pollfd結構體類型
// nfds:要監視的文件描述符數量
// timeout:超時時間(ms)
//?返回值:0,超時發生;-1,發生錯誤;其他值,發生事件或錯誤的文件描述符數量
/******?pollfd?結構體定義?*****/
struct?pollfd?{
?int?fd;???//文件描述符:若fd無效,則events監視事件也無效,revents返回0
?short?events;??//請求的事件:可監視的事件類型如下
?short?revents;??//返回的事件
};
/***?events可監視的事件類型?***/
POLLIN???//有數據可以讀取。
POLLPRI??//有緊急的數據需要讀取。
POLLOUT??//可以寫數據。
POLLERR??//指定的文件描述符發生錯誤。
POLLHUP??//指定的文件描述符掛起。
POLLNVAL??//無效的請求。
POLLRDNORM??//等同于?POLLIN
epoll函數:selcet 和 poll 函數會隨著所監聽的 fd 數量的增加,出現效率低下的問題,epoll 就是為處理大并發而準備的
/************************************************************************/
/******?使用時應用程序需要先使用?epoll_create?函數創建一個?epoll?句柄?******/
int?epoll_create(int?size)???
//?size?為大于0的數值
//?返回值:epoll句柄;返回-1,表示創建失敗
/************************************************************************/
/**?句柄創建后使用?epoll_ctl?函數向其中添加要監視的文件描述符以及監視的事件?**/
int?epoll_ctl(int?epfd,
?????int?op,
?????int?fd,
?????struct?epoll_event?*event)
// epfd:要操作的epoll句柄
// op:對epfd進行的操作(包括EPOLL_CTL_ADD/EPOLL_CTL_MOD/EPOLL_CTL_DEL)
// fd:要監視的文件描述符
// event:要監視的事件類型,為 epoll_event 結構體類型指針
//?返回值:?0,成功;?-1,失敗,并且設置 errno 的值為相應的錯誤碼
/******?epoll_event?結構體定義?*****/
struct?epoll_event?{
?uint32_t?events;??/*?epoll?事件?*/
?epoll_data_t?data;??/*?用戶數據?*/
};
/******?events可選的事件如下?******/
EPOLLIN??//有數據可以讀取
EPOLLOUT??//可以寫數據
EPOLLPRI??//有緊急的數據需要讀取
EPOLLERR??//指定的文件描述符發生錯誤
EPOLLHUP??//指定的文件描述符掛起
EPOLLET??//設置 epoll 為邊沿觸發,默認觸發模式為水平觸發
EPOLLONESHOT?//一次性的監視,若完成后還要再次監視,就需要將fd重新添加到epoll里
/************************************************************************/
/****?上述步驟設置好后應用程序就可以通過?epoll_wait?函數來等待事件的發生?*****/
int?epoll_wait?(int?epfd,
????struct?epoll_event?*events,
????int?maxevents,
????int?timeout)
// epfd:要等待的epoll
// events:指向epoll_event結構體的數組
// maxevents:events數組大小,必須大于?0
// timeout:超時時間,單位為ms
//?返回值:?0,超時;?-1,錯誤;其他值,準備就緒的文件描述符數量
?
4. 信號驅動I/O模型
信號類似于硬件上使用的"中斷",只不過信號是軟件層面上的。可理解為軟件層次上對中斷的一種模擬,驅動通過主動向應用程序發送可訪問的信號,應用程序獲取到信號后即可從驅動設備中讀取或寫入數據了 例如下圖示,應該程序進行read系統調用,進程繼續運行不會阻塞,立即返回,等待內核緩沖區數據準備好后,通過SIGIO信號通知應用程序,應用程序再進行read系統調用,內核將內核緩沖區中的數據拷貝到用戶緩沖區,調用完成。整個過程中應用程序沒有去查詢驅動設備是否可以訪問,是由驅動設備通過SIGIO信號告訴給應用程序的信號驅動模型的核心就是信號,內核arch/xtensa/include/uapi/asm/signal.h文件中定義了Linux所支持的所有信號:
#define?SIGHUP???1???/*?終端掛起或控制進程終止?*/
#define?SIGINT???2???/*?終端中斷(Ctrl+C?組合鍵)?*/
#define?SIGQUIT??3???/*?終端退出(Ctrl+組合鍵)?*/
#define?SIGILL???4???/*?非法指令?*/
#define?SIGTRAP??5???/*?debug?使用,有斷點指令產生?*/
#define?SIGABRT??6???/*?由?abort(3)發出的退出指令?*/
#define?SIGIOT???6???/*?IOT?指令?*/
#define?SIGBUS???7???/*?總線錯誤?*/
#define?SIGFPE???8???/*?浮點運算錯誤?*/
#define?SIGKILL??9???/*?殺死、終止進程?*/
#define?SIGUSR1??10???/*?用戶自定義信號?1?*/
#define?SIGSEGV??11???/*?段違例(無效的內存段)?*/
#define?SIGUSR2??12???/*?用戶自定義信號?2?*/
#define?SIGPIPE??13???/*?向非讀管道寫入數據?*/
#define?SIGALRM??14???/*?鬧鐘?*/
#define?SIGTERM??15???/*?軟件終止?*/
#define?SIGSTKFLT??16???/*?棧異常?*/
#define?SIGCHLD??17???/*?子進程結束?*/
#define?SIGCONT??18???/*?進程繼續?*/
#define?SIGSTOP??19???/*?停止進程的執行,只是暫停?*/
#define?SIGTSTP??20???/*?停止進程的運行(Ctrl+Z?組合鍵)?*/
#define?SIGTTIN??21???/*?后臺進程需要從終端讀取數據?*/
#define?SIGTTOU??22???/*?后臺進程需要向終端寫數據?*/
#define?SIGURG???23???/*?有"緊急"數據?*/
#define?SIGXCPU??24???/*?超過?CPU?資源限制?*/
#define?SIGXFSZ??25???/*?文件大小超額?*/
#define?SIGVTALRM??26???/*?虛擬時鐘信號?*/
#define?SIGPROF??27???/*?時鐘信號描述?*/
#define?SIGWINCH??28???/*?窗口大小改變?*/
#define?SIGIO???29???/*?可以進行輸入/輸出操作?*/
#define?SIGPOLL??SIGIO?/*?#define?SIGLOS?29?*/
#define?SIGPWR???30???/*?斷點重啟?*/
#define?SIGSYS???31???/*?非法的系統調用?*/
#define?SIGUNUSED??31???/*?未使用信號?*/
這些信號就相當于中斷號,不同的中斷號代表了不同的中斷,不同的中斷所做的處理不同,因此,驅動程序可以通過向應用程序發送不同的信號來實現不同的功能使用中斷時需要設置中斷處理函數,同樣的,在應用程序中使用信號,就必須設置信號所使用的信號處理函數,在應用程序中使用signal函數來設置指定信號的處理函數,其函數原型如下所示:
/***********************signal函數原型*****************************/
sighandler_t?signal(int?signum,?sighandler_t?handler)
// signum:要設置處理函數的信號
// handler:信號的處理函數
//?返回值:成功,返回信號的前一個處理函數;失敗,返回 SIG_ERR
/***********************信號處理函數原型***************************/
typedef?void?(*sighandler_t)(int)
?
5. 異步I/O模型
相對于同步IO,異步IO不是順序執行。例如下圖示,用戶進程進行aio_read系統調用之后,無論內核數據是否準備好,都會直接返回給用戶進程,然后用戶態進程可以去做別的事情。等到socket數據準備好了,內核直接復制數據給進程,然后從內核向進程發送通知。IO兩個階段,進程都是非阻塞的
?
6. 五種I/O模型的比較
對于Liunx的五種I/O模型,主要在等待數據和數據復制這兩個時間段不同,它們的區別詳見下表
審核編輯:湯梓紅
評論
查看更多