如果一個應用程序去處理多個設備,例如應用程序讀取網路數據,按鍵,串口,一般能想到的有三種方法:
方法1:
串行+阻塞的方式讀取:
while(1) {
read(標准輸入);
read(網絡);
}
缺點:每當阻塞讀取標准輸入時,如果用戶不進行標准輸入的操作,而此時客戶端給服務器發送數據,導致服務器無法讀取客戶端發送來的數據!
方法2:
采用多線程或者多進程機制來實現讀取:
開辟多個線程,每一個線程處理一個設備,不會導致的數據的無法讀取,但是系統的開銷相比方法1要大!
方案3:采用linux系統提供的高級IO的處理機制
select/poll:兩者一樣,主進程能夠利用select或者poll能夠對多個設備進行監聽!
其原理好像:方法1相當於有一個保安,看十戶房子,如果小偷進來從第十戶開始偷,保安卻從第一戶挨個檢查,沒有小偷確還在第一家等着。
方法2相當於雇了十個保安,開銷大
方法3相當於買了10套監控設備,一個保安看監控錄像,有情況報警
************************************************************************************
select函數原型:
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout);
函數功能:
主進程利用此函數能夠對多個設備進行監聽,一旦發現監聽的設備都不可用(不可讀、也不可寫、也沒有異常),那么主進程進入休眠狀態,一旦監聽的設備中,只要有一個設備可用(可讀或者可寫或者有異常)都會喚醒休眠的主進程,select也就會返回。
注意這個函數僅僅起到一個監聽的功能,數據的后續處理,例如讀寫都是通過read,write,ioctl來進行!
參數說明:
nfds:
對設備的訪問永遠先open獲取fd;
監聽的設備中,最大的文件描述符fd+1;
數據類型fd_set:文件描述符集合,用來保存描述監聽的設備,里面存放是被監聽設備的文件描述符;如果select要監聽某一個設備,必須把這個設備的fd添加到對應的文件描述符集合中!
readfds:讀文件描述符集合指針,如果select要監聽設備是否可讀,需將設備的fd添加到這個集合中!
writefds:寫文件描述符集合指針,如果select要監聽設備是否可寫,需將設備的fd添加到這個集合中!
exceptfds:異常文件描述符集合指針,如果select要監聽設備是否有異常,需將設備的fd添加到這個集合中!
注意:一個設備的fd可以同時添加到三個集合中!
timeout:指定監聽的超時時間,如果此參數指定了一個時間,例如5秒鍾,select發現設備不可用,主進程進入休眠狀態,如果5秒之內設備還不可用,5秒到期,主進程主動喚醒;如果此參數指定為NULL,休眠為永久休眠!
返回值:有三種
如果等於0:表明是超時;
如果小於0:表明系統出錯;
如果大於0:表明設備可用(至少是一個設備,或者全部);
文件描述符集合操作的方法:
fd_set rfds; //定義讀文件描述符集合
//從集合中解除對fd設備的監聽
void FD_CLR(int fd, fd_set *set);
//判斷是否是設備fd引起的主進程的喚醒,如果是返回true,否則返回false
int FD_ISSET(int fd, fd_set *set);
//添加一個新的被監聽的設備
void FD_SET(int fd, fd_set *set);
//清空文件描述符集合
void FD_ZERO(fd_set *set);
注意:如果要重復監聽,需要再次清空集合和添加監聽設備!
***********************************************************************************
以上是應用程序層面上的函數調用
其在內核層面上:
在sys_select中做休眠,poll不引起休眠
select系統調用過程:
1.應用程序調用select,首先調用C庫的select函數實現;
2.C庫的select保存select系統調用號到R7寄存器中,調用SVC(新的
)或者SWI(老的)觸發軟中斷,至此由用戶空間陷入內核空間,ARM
的工作模式由用戶模式轉變為SVC管理模式;
3.跳轉到內核准備好的異常向量表的入口地址,根據R7保存的系統調
用號,以它為索引在系統調用表中找到對應的實現函數sys_select
4.sys_select要完成:
1.把被監聽的設備對應驅動程序的poll函數挨個調用一遍,
被監聽的設備都不可用時,它們的驅動的poll函數都返回0;
2.判斷是否是驅動主動喚醒,還是超時喚醒,還是接收到信號喚醒;
3.如果即沒有驅動主動喚醒,也沒有超時喚醒,沒有接收到信號,
sys_select調用poll_schedule_timeout主動讓進程進入休眠;
4.假設被監聽的設備中,有一個設備可用(可讀或者可寫或者異常
,硬件通過中斷來判斷),都會喚醒休眠的主進程;
5.sys_select的poll_schedule_timeout函數返回,不再休眠
6.再次把被監聽的設備驅動的poll函數挨個調用一遍,此時可用
的設備對應的驅動poll函數會返回非0;
7.if (ret || time_out || ...) //ret = 1,立即返回到用戶空
間,返回值為ret值
總結:
1.明確本來應該底層驅動的poll函數利用等待隊列機制讓進程休眠,
但是等待隊列休眠9步驟並不都是驅動的poll來編寫,有一部分是有內
核sys_select來實現;
2.驅動poll函數完成如下內容即可:
1.調用poll_wait,將當前進程添加到驅動定義的等待隊列頭中
2.根據設備是否可用,決定返回0還是非0
3.明確:監聽機制,底層poll函數不是必須的,如果要監聽設備還可
以使用多線程機制也能夠完成監聽;但是如果要使用select/poll監
聽設備,驅動必須有poll實現!
下圖是sys_select的簡單實現:
通過對內核代碼的分析,真正的休眠實現是在內核中實現的
poll_schedule_timeout函數中的schedule_hrtimeout_range中的schedule_hrtimeout_range_clock函數實現的
並不是在poll函數中實現的
poll(輪詢)操作在應用程序中用於同時阻塞在多個文件上,當其中任何一個文件有應用程序所等待的事件(可讀、可寫、出錯等)時,poll返回相應的掩碼通知應用程序,使得應用程序知道應該對哪個文件做何種操作。按照我的理解,poll的本質可以這樣解釋:休眠等待多個指定文件中的任何一個發生特定的事件,並將被該文件喚醒;醒來后輪詢所有相關文件(通過再次調用所有文件對應驅動的poll方法),獲取所有被監控文件的事件信息返回給應用程序。
從這里就可以看出:
(1)其中等待隊列的使用是必不可少的。實際上調用poll的進程將會休眠在多個等待隊列(一般所有被監控文件的都有至少一個的等待隊列)上,從其中任何一個隊列上喚醒該進程,都可能使poll函數返回。
(2)驅動中的poll方法不實現休眠,而是:
- i、把當前進程添加到相應的等待隊列中(僅在休眠時執行,喚醒時不會執行此功能)。
- ii、返回文件當前的狀態掩碼(告知是否有事件發生,休眠和喚醒都會執行)。
通過對內核源碼、《深入Linux設備驅動程序內核機制》的學習,我對Poll系統調用和內核驅動的poll方法的關系和結構有了整體且深入的了解,基本搞清了poll系統調用的執行脈絡。對於poll系統調用的內核原理,請大家先看《深入Linux設備驅動程序內核機制》那本書寫的比較詳細了,我不廢話了。以后我會把我自己覺得需要注意的地方寫出來。這里我把這個關系和數據結構圖繪制了出來,請大家指正:
對於等待隊列的情況,我用下面一個例子和圖來示意一下:
例如有3個進程:
task-1:使用poll檢測文件1~3
task-2:使用poll檢測文件2~3
task-3:使用poll檢測文件3

