阻塞式I/O: “有美人兮,見之不忘,一日不見兮,思之如狂。”
select: “所用皆鷹騰,破敵過箭疾”
01
select()
允許一個程式監聽多個檔案描述符,等待一個或者多個檔案描述符的I/O操作變成“就緒”狀態(比如:可讀)。
引數
int nfds
引數表示待監聽的集合裡的最大檔案描述符的值 + 1。
fd_set *readfds
、fd_set *writefds
、fd_set *exceptfds
三個集合分別存放需要監聽讀、寫、異常三個操作的檔案描述符。
struct timeval *timeout
表示超時時間。設為0則立刻掃描並傳回,設為NULL則永遠等待,直到有檔案描述符就緒。
02
閱讀的Linux核心版本:linux-2.6.32.68
select原始碼位於fs/select.c檔案
執行流程
select函式執行從此開始,關鍵呼叫流程如下: select -> core_sys_select() -> do_select() 。
selcet的主要操作在do_select()
函式中完成。
在上述函式中,主要把超時時間tvp
的值從使用者空間複製到核心空間,並且呼叫poll_select_set_timeout()
函式把超時時間的長度加到當前時間上,獲得最終的結束時間點to
。由於poll_select_set_timeout()
的時間精度是納秒,所以需要轉換。
之後呼叫core_sys_select()
函式執行主要邏輯。
在主要程式執行完之後,還會呼叫poll_select_copy_remaining()
把等待時間中的剩餘時間傳回給使用者態的tvp
。
03
core_sys_select()
core_sys_select()
函式主要為真正的select操作分配儲存空間。這裡分配了一個名為stack_fds
的long
長整型集合。首先預分配了long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];
,根據
可以看到stack_fds
的大小為 256bit 。
之前存放檔案描述符集合的型別是fd_set
,根據
可以看到 fd_set
型別在核心裡是實際上__kernel_fd_set
結構體,裡面只包含了一個unsigned long
型別的陣列fds_bits
。這個陣列的大小是 1024/(8 * sizeof(unsigned long)) ,也就是這個陣列佔用空間為 1024bit 。在select中檔案描述符在集合裡是以點陣圖的形式存在的,把檔案描述符存放在三個集合中,最大直到 1023 ,也就是隻能監聽最多 1024 個檔案描述符,並且只能是0 ~ 1023。
所以,存放檔案描述符的資料結構限制了 select() 最多隻能監聽 1024 個檔案描述符。
回到剛剛core_sys_select()
裡的stack_fds
陣列,這個變數的佔用空間大小是 256*sizeof(long) bit 。
根據We need 6 bitmaps (in/out/ex for both incoming and outgoing)
以及程式碼可以看到,stack_fds
要存放的是6個點陣圖,分別對應使用者態傳入的存放監聽讀、寫、異常三個操作的檔案描述符集合,以及這三個操作在select執行過後需要傳回的三個集合。
這是 select 的機制,每次執行 select() 之後,函式把“就緒”的檔案描述符留下,傳回。下一次,再次執行 select() 時,需要重新把需要監聽的檔案描述符傳入。
我認為,如果要節約空間,完全可以在傳入的三個集合中進行刪減,不必浪費三個集合的空間。(我的想法,可能有其他問題。)
如果剛從棧中分配的stack_fds
不夠存放6個集合的資料,那麼再從 kmalloc 分配(用於分配大空間)。
6個集合分別用指標指向stack_fds
中的不同部分空間,依次利用。size
為間隔大小。
根據
size=FDS_BYTES(n);
它的大小是((((n)+(8*sizeof(long))-1)/(8*sizeof(long)))*sizeof(long))
,以32位系統為例,long
為8位元組,則大小為((((n)+8*8-1)/(8*8))*8)
化簡為(n-1)/8 + 8
。n
是使用者態程式指定的最大描述符+1,如果我要監聽的最大檔案描述符為7, n 為8,由於這是整型運算,則結果為 8 。也就是確保能存下所有描述符,而且大小為 8 的倍數 。所以kmalloc
分配的空間6個集合是可以存放下去的。
之後從使用者態空間把集合資料複製過來,並且初始化用於輸出的3個點陣圖空間為0。
進入do_select()
函式。
04
在do_select()
裡面,主要是一遍一遍迴圈遍歷每一個檔案描述符,查詢哪一個為就緒狀態。
在外層的迴圈for (;;)
,每一次是整個集合遍歷一遍。這是死迴圈,直到達到觸發條件 1.有就緒的檔案描述符 2.超時 3.中斷。第一遍之後,當前行程會進入睡眠狀態,以節約資源,直到下一次被喚醒(由檔案描述符變為就緒狀態觸發喚醒)。
第二層的for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
迴圈,每一次是遍歷__NFDBITS
個描述符,這是由第三層迴圈決定的。從i < n
可知,因為函式只會迴圈到 n-1 ,所以才需要輸入的最大檔案描述符值nfds
+ 1 。
第三層for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
迴圈每次遍歷一個 bit 即一個檔案描述符,遍歷__NFDBITS
次。
根據:
可以看到,遍歷的數量就是8個unsigned long長度。因為對於點陣圖,可以一次比較多位,都沒有需要監聽的檔案描述符就跳過,以加快速度。
迴圈裡先根據檔案描述符獲得檔案結構體,然後呼叫結構體裡f_op
中掛載的poll
函式,以獲取就緒資訊。可以看到select的功能依賴檔案的驅動實現。mask = (*f_op->poll)(file, wait);
是 select 的關鍵,這裡不僅檢測了檔案是否就緒,而且還把當前行程加入等待佇列,如果該檔案描述符就緒,則會觸發回呼,以及喚醒該行程。這需要該檔案掛載的驅動配合的。
retval
變數用於累計“就緒”的檔案描述符數量,包括3個集合所有的。
一整次掃描完成的最後,呼叫poll_schedule_timeout
函式,如果還未超時,則進入睡眠,等待就緒的檔案描述符喚醒。超時則,timed_out = 1;
。所以可以看到
THE END
此處為跳出迴圈的程式碼,也就是在超時之後,還要再迴圈一次才能跳出。
最後跳出迴圈後,呼叫poll_freewait(&table;);
移出等待佇列。
可以看出來,select 的開銷大在於每次都要遍歷掃描每一個檔案描述符就緒狀態,並且是從最小的描述符 0 開始比較,做了很多無用功,所以效率很低。隨著檔案描述符的增加,效率會越來越低。