歡迎光臨
每天分享高質量文章

select函式原始碼簡析

阻塞式I/O: “有美人兮,見之不忘,一日不見兮,思之如狂。”

select: “所用皆鷹騰,破敵過箭疾”

01

簡介

select()允許一個程式監聽多個檔案描述符,等待一個或者多個檔案描述符的I/O操作變成“就緒”狀態(比如:可讀)。

引數

int nfds引數表示待監聽的集合裡的最大檔案描述符的值 + 1。

fd_set *readfdsfd_set *writefdsfd_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_fdslong長整型集合。首先預分配了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 + 8n是使用者態程式指定的最大描述符+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

此處為跳出迴圈的程式碼,也就是在超時之後,還要再迴圈一次才能跳出。

最後跳出迴圈後,呼叫poll_freewait(&table;);移出等待佇列。

可以看出來,select 的開銷大在於每次都要遍歷掃描每一個檔案描述符就緒狀態,並且是從最小的描述符 0 開始比較,做了很多無用功,所以效率很低。隨著檔案描述符的增加,效率會越來越低。

贊(0)

分享創造快樂