select 函式原始碼簡析
阻塞式I/O: “有美人兮,見之不忘,一日不見兮,思之如狂。”
select: “所用皆鷹騰,破敵過箭疾”
0 1
select()
允許一個程式監聽多個檔案描述符,等待一個或者多個檔案描述符的I/O操作變成“就緒”狀態(比如:可讀)。
引數
02
核心實現
閱讀的Linux核心版本: linux-2.6.32.68
select原始碼位於fs/select.c檔案
執行流程
select函式執行從此開始,關鍵呼叫流程如下: select -> core_sys_select() -> do_select() 。
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()
在 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
此處為跳出迴圈的程式碼,也就是在超時之後,還要再迴圈一次才能跳出。 可以看出來,select 的開銷大在於每次都要遍歷掃描每一個檔案描述符就緒狀態,並且是從最小的描述符 0 開始比較,做了很多無用功,所以效率很低。隨著檔案描述符的增加,效率會越來越低。