multiplex I/O多路複用(一)select 解析

1. I/O多路複用介紹

       在介紹I/O多路複用之前,我們先看看一個最基本的socket client/server端是如何接收處理數據的:

       

       client: socket -> bind -> connect -> read/write -> close

       server: socket -> bind -> listen -> accept -> read/write -> close

       對於server端,這種方式邏輯簡單實現容易,但是不足之處也比較明顯,那就是隻支持一個client的連接和數據收發。那如何支持多個client呢?很容易想到使用多線程,即每當accept一個新連接的時候,就創建一個新的線程,在新的線程裏處理該連接對應socket的數據收發。但是多線程也有自己的問題,比如當client非常多時,系統需要創建和維護大量的線程,這對系統資源消耗比較大,同時對共享資源的訪問需要各種加鎖解鎖等操作,影響系統效率。

       那有沒有一種方法,只用一個線程就能支持多個client的數據收發交互呢?答案是肯定的,那就是I/O多路複用。I/O多路複用通過一種機制,使單線程可以監視多個文件描述符,一旦有一個或多個fd有事件發生就通知應用程序,然後進行相應fd的讀寫等操作。與多線程相比,I/O 多路複用的最大優勢是系統開銷小,不需要創建和管理大量的線程,提高系統效率。當前常見的多路複用機制有select, poll, epoll等,各個機制都有自己特點。本篇介紹select的實現機制,通過對源代碼的解析,瞭解其實現機制,進而理解其優缺點,如爲何select默認最大監聽fd數量爲1024個,爲何每次調用select前需要重新註冊fd監聽事件和設置超時參數?帶着這些疑問,我們開始下面的講解。

 

2. 源碼

a. 下載地址

        https://www.kernel.org/

        該網頁上可以下載最新版本的linux kernel代碼,本文以5.2.13爲例講解

b. 函數調用關係

        壓縮包解壓後,找到fs/select.c文件,該文件就是select函數和poll函數的代碼實現,本篇只介紹select函數相關的實現

        select函數入口:

SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
		fd_set __user *, exp, struct timeval __user *, tvp)
{
	return kern_select(n, inp, outp, exp, tvp);
}

        其主要函數調用關係:select -> kern_select -> core_sys_select -> do_select

 

3. fd_set

a. 類型解析

      在開始select解析之前,首先介紹一個非常重要的數據結構fd_set,其定義在文件 include/linux/types.h:

typedef __kernel_fd_set		fd_set;

      我們轉到源結構__kernel_fd_set,其定義在文件 include/uapi/linux/posix_types.h:

#undef __FD_SETSIZE
#define __FD_SETSIZE	1024

typedef struct {
	unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;

...

#endif /* _LINUX_POSIX_TYPES_H */

       可以看到,fd_set本質是一個unsigned long型的數組,其數組長度與__FD_SETSIZE有關。這個宏定義有什麼作用呢,這個值1024與select默認最大監聽fd數量1024個有沒有關係呢?

       我們知道,linux系統中一切皆是文件,如鍵盤,屏幕,串口,socket等等,每打開一個設備系統都會分配一個fd來指示該文件,其值從0開始。如進程啓動的時候系統默認打開三個文件(0:stdin, 1:stdout, 2:stderr),因此我們可以直接從鍵盤讀取輸入數據或直接輸出數據到屏幕,而不需要手動調用open()函數。當我們繼續打開其他文件時,系統會爲該文件分配新的fd,新的fd是該進程的文件描述符表中值最小的可用文件描述符,即每次分配的fd值自增++。Linux 中一個進程默認最多能打開1024(NR_OPEN_DEFAULT)個文件,因此默認fd取值範圍爲0-1023。

       select與fd又有什麼關係呢?我們可以想到select函數定會有一個輸入參數(後面可以看到有三個相關參數),用來保存需要監聽的fd,那麼這個參數是什麼類型呢,如何能存儲需要監聽的多個fd呢?答案就是用1024個bit位來存儲。由於fd範圍是0-1023,我們可以以fd爲索引,當需要監聽該fd時,將對應索引的bit位置1,當不需要監聽該fd時,將對應索引的bit位置0,這樣就可以保存所有設置的監聽描述符了。如果1024個bit位全部置1,表示最多可同時監聽1024個fd,這就是爲什麼select默認最多隻能監聽1024個fd的原因了。那1024個bit以什麼方式存在呢?爲了後續處理效率,系統選用unsigned long型數組爲載體來存儲,即unsigned long fds[1024 / (8 * sizeof(long))]。如果我們將1024這個值用宏定義__FD_SETSIZE來指示,即爲unsigned long fds[__FD_SETSIZE / (8 * sizeof(long))],是不是有些眼熟,這不就是最開始__kernel_fd_set/fd_set的定義嘛!由此我們知道,select就是使用unsigned long數組來存儲監聽的fd,數組中的每個unsigned long以及unsigned long 中的每個bit,都表示一個fd是否被監聽。假設一個long 4個字節,則fd_set圖示如下:

       上述過程我們發現有兩個宏定義,__FD_SETSIZE 和 NR_OPEN_DEFAULT,那麼這兩個值與select有什麼關係呢?NR_OPEN_DEFAULT 表示一個進程最多可以打開的文件個數,即約束了fd的取值爲 0 - NR_OPEN_DEFAULT-1,而 __FD_SETSIZE 表示 select 用於存儲fd所用的bit位個數。如果NR_OPEN_DEFAULT > __FD_SETSIZE, 則select可能無法監聽所有打開的文件,即fd範圍在 __FD_SETSIZE - NR_OPEN_DEFAULT之間的文件無法監聽。如果NR_OPEN_DEFAULT < __FD_SETSIZE, 則select可以監聽所有的文件,只是會有多餘無用的bit位。兩個宏定義默認值都爲1024,如果修改需要重新編譯系統,並且最好以sizeof(long)爲單位進行修改,後面可以看到如果取值不合適,會影響select的執行效率。

b. FD_ZERO、FD_SET、FD_CLR、FD_ISSET

      瞭解了fd_set的結構和實現之後,再來看看跟它相關的幾個常見的函數,主要就是對特定的bit位進行置1,置0操作,以及查詢某個bit位當前是置1還是置0等:

void FD_ZERO(fd_set *fdset);          // fd_set中的所有bit位置0
void FD_SET(int fd, fd_set *fdset);   // fd_set中以文件描述符fd爲索引的bit位置1
void FD_CLR(int fd, fd_set *fdset);   // fd_set中以文件描述符fd爲索引的bit位置0
int FD_ISSET(int fd, fd_set *fdset);  // 查詢fd_set中以fd值爲索引的bit位是否置位,用於判斷fd是否發生了事件

 

4. select 函數原型

       瞭解了fd_set的實現之後,我們就可以來看看select函數的原型聲明瞭。其函數聲明如下:

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

a. nfds

       後面我們會分析到,select內部是通過循環判斷fd_set中所有置1的bit位所對應的fd是否有事件發生,當最高置1的bit位判斷完成後,循環就可以提前結束了,因爲再往高位所有的bit位都置0了,也就是那些fd並沒有被監聽,因此可以提前退出循環提高效率。如下圖所示:

       nfds參數即表示查詢完所有置1的bit位所需要的循環次數,由於索引值就是fd的值,因此該參數取值通常爲所有的監聽描述符中最大值+1。當然直接取1024也沒有問題,只是select內部可能會多做些無用的循環判斷,稍稍影響些效率。

b. readfd/writefds/exceptfds

      三個監聽文件描述符集合,可分別設置讀/寫/異常事件的監聽描述符集合,如果所有fd均不需要監聽某個時間則該集合可以設爲NULL

c. timeout

      超時時間,通過s和us兩個字段設置,它表示如果所有的fd都沒有任何事件發生的時候,函數返回之前需要等待的時間。參數有三種取值對應不同的處理機制:

      1. NULL, 表示永遠等待下去,直到有一個或多個fd發生了事件函數纔會返回,返回值爲所有fd發生的所有事件的個數之和

      2. 具體等待時間,即s和us至少一個不爲0,表示當所有的fd都沒有事件發生的時候,函數返回前需要等待的時間。在等待期間如果任何一個fd發生任何一個事件,則函數返回,返回值爲所有fd發生的所有事件的個數之和;如果時間到了仍沒有fd發生任何事件,則函數返回0

      3. 不等待,s和us均爲0,即非阻塞方式,表示檢查完所有監聽的fd,不管有沒有事件發生函數均立即返回。如果有事件發生,則返回值爲所有fd發生的所有事件的個數之和;如果沒有任何fd發生任何事件,則函數返回0

d. 返回值

       上面已經提到過了,返回值表示所有fd發生的所有事件個數之和。由於返回值僅僅是一個int值,因此select只能告訴我們總共有多少個事件發生了,但不能告訴我們是哪個fd發生了事件(可能是一個,多個,甚至全部),如果同一個fd發生了多個事件,也不能告訴我們具體發生的是哪些事件。所以當select返回後,用戶只能通過FD_ISSET函數循環判斷每個fd是否有事件發生,如果有事件發生再判斷該fd發生的是可讀、可寫還是異常事件,然後再進行相應的讀寫操作。所以select具有O(n)的輪詢複雜度,監聽的fd越多循環時間就越長。後面當分析epoll的實現的時候,我們會發現epoll只返回發生事件的fd信息,用戶可以直接進行讀寫操作,大大提高了效率

 

5. core_sys_select 解析

int core_sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timespec64 *end_time)
{
    ...
    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)]; // SELECT_STACK_ALLOC = 256
    size = FDS_BYTES(n); // n個fd,n個bit,使用long型數組存儲,需要數組長度 size = (n+8*sizeof(long)-1)/8*(sizeof(long))
    bits = stack_fds;    // 內核預分配的long型數組空間,共256個字節,數組長度 256/4 = 64

    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size; // 6個long型數組指針,分別用於保存輸入和輸出的in/out/ex描述符集合

    get_fd_set(n, inp, fds.in);
    get_fd_set(n, outp, fds.out);
    get_fd_set(n, exp, fds.ex);  // 用戶監聽的三組fd分別拷貝到內核態,每組拷貝size個long型長度

    zero_fd_set(n, fds.res_in);  // 清空用於保存結果的三組long型數組,每組清空size個long型長度
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    ret = do_select(n, &fds, end_time); // 循環查詢所有fd的驅動poll函數,檢查是否有可讀可寫事件,有則返回,無則進入睡眠等待超時或等待事件發生

    if (ret <= 0)
        return ret; // 返回0表示沒有任何fd有事件發生,<0表示發生錯誤

    set_fd_set(n, inp, fds.res_in);
    set_fd_set(n, outp, fds.res_out);
    set_fd_set(n, exp, fds.res_ex); // 拷貝保存結果的三組long型數組數據到用戶態

    return ret; // 返回所有fd發生的所有事件個數之和
}

a. FDS_BYTES(n)

      我們先來看宏定義FDS_BYTES(n),經過多層宏定義調用,最終爲:

define FDS_BYTES(n) = (n+8*sizeof(long)-1)/8*(sizeof(long))

      即n個bit位由long型數組來存儲所需要的數組長度。我們以long爲4個字節來說明,那麼1-32個bit位只需要長度爲1的long型數組,如果有33個bit位,則就需要長度爲2的long型數組了。因此如前所述,當我們在給select傳入第一個參數nfds的時候,該參數取值最好爲所有監聽fd中最大值+1,讓系統分配能存下nfds個bit位的最小long型數組長度,如果直接傳入1024,這裏就會多分配無用的數組長度,後續也會多做無用的循環判斷

b. stack_fds

       stack_fds就是系統預先分配的long型數組空間,用於存儲所有需要監聽的fd,以及用於存儲查詢結果,返回讀,寫,異常事件的查詢結果。即long型數組空間所需長度爲6*FDS_BYTES(n),如果nfds取值最大1024,即進程所有打開的1024個fd全部進行可讀可寫和異常監聽,那麼此時FDS_BYTES(n) = (1024+8*sizeof(long)-1)/8*(sizeof(long)) = 32,即存儲監聽可讀事件的fd需要長度爲32的long型數組,加上監聽可寫和異常事件的fd,以及返回結果的存儲,總共需要長度爲 32*6 = 192的long型數組,而long型數組stack_fds默認長度爲 256/4 = 64,因此當這個默認長度存儲不下時,就需要對數組長度進行擴展,擴展爲6*FDS_BYTES(n)。源代碼如下:

...
size = FDS_BYTES(n);
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) 
{
    /* Not enough space in on-stack array; must use kmalloc */
    ...
    alloc_size = 6 * size;
    bits = kvmalloc(alloc_size, GFP_KERNEL);
}

c. 函數流程圖

       當long型數組stack_fds空間足夠了之後,通過6個間距爲FDS_BYTES(n)的long型指針,標記6段長度爲FDS_BYTES(n)的long型數組空間,用來存儲所有輸入和輸出的fd。函數處理流程如下圖:

  1.  通過get_fd_set函數將需要監聽可讀可寫和異常事件的fd拷貝到三個輸入long型數組空間fds->in/out/ex,通過zero_fd_set先將三個輸出long型數組空間fds->res_in/out/ex清空,用於保存查詢結果
  2. 通過do_select函數循環查詢所有fd發生的事件,如果返回值<0表示內部發生異常,如果=0表示沒有事件發生,如果>0表示共發生了多少個事件

  3. 將查詢結果賦值到三個輸出long型數組空間fds->res_in/out/ex

  4. 通過set_fd_set將查詢結果從輸出long型數組空間fds->res_in/out/ex拷貝回用戶輸入的fd_set,然後返回發生的事件個數,並作爲select函數的最終返回值

       由第4步可知,select返回監聽結果是複用輸入的fd_set的,因此當調用完一次select函數後,需要重新置位需要監聽的fd。否則假設我們要監聽5個fd,結果第一次select返回發現其中只有2個fd有事件發生,這時另外3個fd標誌位是被清零了,如果下次調用select之前不重新置位,那麼這3個fd就不會再進入查詢循環中,即便有事件發生,select函數再也不會返回這些fd的結果了。因此在調用完select之後,一定要將需要監聽的fd重新置位,具體可參考後面的示例程序

 

6. do_select 解析

       通過core_sys_select函數的解析,我們發現真正完成查詢事件發生結果的是do_select函數。該函數首先獲取三個輸入long型數組,獲取需要查詢的fd,查詢完成後,再將結果寫入到三個輸出long型數組中。下面我們通過5種場景,分別分析各個場景的處理機制:

// 1. 有fd收到數據

// 2. 沒有fd收到數據,超時時間設爲0,即不等待

// 3. 沒有fd收到數據,超時時間設爲NULL,即永遠等待直到有fd收到數據

// 4. 沒有fd收到數據,超時時間設爲具體值,定時結束之前有fd收到數據

// 5. 沒有fd收到數據,超時時間設爲具體值,定時結束之前沒有fd收到數據

static int do_select(int n, fd_set_bits *fds, struct timespec64 *end_time)
{
    ...
    int timed_out = 0; 
    u64 slack = 0;

    // 註冊_qproc函數,用於查詢事件發生結果時將當前進程加入讀寫等待隊列
    poll_initwait(&table);
    
    // 我們有如下5種場景,下面分析一下分別會有什麼樣的處理
    // 1. 有fd收到數據
    // 2. 沒有fd收到數據,超時時間設爲0,即不等待
    // 3. 沒有fd收到數據,超時時間設爲NULL,即永遠等待直到有fd收到數據
    // 4. 沒有fd收到數據,超時時間設爲具體值,定時結束之前有fd收到數據
    // 5. 沒有fd收到數據,超時時間設爲具體值,定時結束之前沒有fd收到數據

    // 對於場景2,超時時間設爲0,設置timed_out = 1,該標誌位用於場景2退出for (;;)循環
    if (end_time && !end_time->tv_sec && !end_time->tv_nsec) 
        timed_out = 1;

    // 對於場景4和5,超時時間設爲具體值,則將超時時間結構體轉爲U64整數,便於後續定時器處理
    if (end_time && !timed_out) 
        slack = select_estimate_accuracy(end_time);

    retval = 0; // 統計所有fd發生的所有事件個數之和,也是select函數的返回值
    for (;;)    // 不同場景有不同的機制來退出該死循環
    {
        // 6個long型指針inp/outp/exp/rinp/routp/rexp,分別指向stack_fds中用於輸入和輸出的in/out/ex文件描述符集合
        inp   = fds->in;
        outp  = fds->out;
        exp   = fds->ex;
        rinp  = fds->res_in;
        routp = fds->res_out;
        rexp  = fds->res_ex;

        // 循環遍歷所有的n個bit位
        for (i = 0; i < n; ++rinp, ++routp, ++rexp)
        {
            unsigned long in, out, ex, all_bits, bit = 1;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;

            // n個bit位以long型爲單位進行多組(即32個爲一組)循環遍歷
            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;
            if (all_bits == 0) { // 該組的32(BITS_PER_LONG)個fd都不需要監聽in/out/ex事件,則調過該組循環
                i += BITS_PER_LONG;
                continue;
            }

            // 循環遍歷一個組的32個bit位
            for (j = 0; j < BITS_PER_LONG; ++j, ++i, bit <<= 1)
            {
                // 當遍歷的是最後一組時,該組fd個數可能不到32個了,則遍歷完最後一個fd就可以提前退出該組循環
                if (i >= n)
                    break;

                // 該bit位對應的fd沒有註冊監聽in/out/ex事件,則跳過該bit位循環
                if (!(bit & all_bits))
                    continue;

                f = fdget(i); // 通過fd值獲取file結構

                // vfs_poll調用驅動程序 file->f_op->poll(file, pt),查詢該fd發生的事件,驅動函數poll功能:
                // 1. 返回該fd發生的in/out/ex事件,用mask標識發生的事件
                // 2. 將當前進程加入到該fd的讀寫等待隊列,當進程喚醒後再從等待隊列中移除該進程相關的節點
                mask = vfs_poll(f.file, wait);

                // 當fd發生了in/out/ex事件,且用戶設置監聽該事件,則保存查詢結果:
                // 1. 保存發生事件的fd值
                // 2. retval++,即所有fd發生的所有事件個數之和,即如果同一個fd發生2個事件,統計結果是+2而不是+1,這就是select返回值的意義
                if ((mask & POLLIN_SET) && (in & bit)) {
                    res_in |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
                if ((mask & POLLOUT_SET) && (out & bit)) {
                    res_out |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
                if ((mask & POLLEX_SET) && (ex & bit)) {
                    res_ex |= bit;
                    retval++;
                    wait->_qproc = NULL;
                }
            }

            // 循環遍歷完該組的32個fd後,將發生事件的fd分別寫到用於保存in/out/ex結果的三組long型數組中
            if (res_in)
                *rinp = res_in;
            if (res_out)
                *routp = res_out;
            if (res_ex)
                *rexp = res_ex;
        }

        // 場景1,有事件發生,retval不爲0,退出for (;;)循環,函數返回事件發生個數之和
        // 場景2,沒有事件發生,retval爲0,但因爲timed_out已被設爲1,因此退出for (;;)循環,函數返回事件發生個數之和,即爲0
        if (retval || timed_out || signal_pending(current)) 
            break;

        // 場景3,4,5,沒有事件發生,則需等待
        if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE, to, slack))
        {
                // 設置當前進程爲睡眠狀態,然後循環查詢進程是否被喚醒
                set_current_state(TASK_INTERRUPTIBLE);

                // 場景3,沒有事件發生,永遠等待直到有任何fd收到數據
                if (!expires)
                    schedule(); // while循環查詢當前進程是否被喚醒,當有fd收到數據時其驅動程序將喚醒進程
                    return -EINTR;

                // 場景4和5,沒有事件發生,開啓定時器等待特定時間
                // 設置定時時間
                hrtimer_set_expires_range_ns(&t.timer, *expires, delta);

                // 初始化定時器
                hrtimer_init_sleeper(&t, current);
                    t->timer.function = hrtimer_wakeup; // 設置超時回調函數hrtimer_wakeup
                    t->task = current; // 場景4,設置t->task指向當前進程,用於poll_schedule_timeout判斷進程喚醒是因爲有fd收到數據
                        // 如果直到定時器超時都沒有收到數據,則觸發超時回調函數
                        static enum hrtimer_restart hrtimer_wakeup(struct hrtimer *timer)
                            t->task = NULL; // 場景5,設置t->task爲空,用於poll_schedule_timeout判斷進程喚醒是因爲超時
                            wake_up_process(task); // 喚醒當前進程

                // 啓動定時器
                hrtimer_start_expires(&t.timer, mode);

                schedule(); // while循環查詢當前進程是否被喚醒,當有fd收到數據或者定時器超時時進程被喚醒

                set_current_state(TASK_RUNNING); // 進程被驅動程序或超時回調函數喚醒,退出睡眠狀態

                // 根據t->task值判斷進程喚醒是因爲超時還是因爲有fd收到數據
                // 場景4,超時前有fd收到數據,不觸發回調函數,t->task沒有被置空,poll_schedule_timeout返回-EINTR
                // 場景5,一直沒有fd收到數據,觸發回調函數,t->task被置空,poll_schedule_timeout返回0
                return !t.task ? 0 : -EINTR; 

            // 根據poll_schedule_timeout返回值判斷進程喚醒是否因爲超時,如果是則設置timed_out = 1
            timed_out = 1;
        }

        /*
        至此,我們完成了第一次for (;;)循環
        1. 對於場景1,第一次for (;;)循環就能退出死循環,返回事件發生個數之和
        2. 對於場景2,第一次for (;;)循環就能退出死循環,返回事件發生個數之和,此時和爲0
        3. 對於場景3,4,5,在第一次for (;;)循環結束後並不能退出死循環,而是接着進入第二次循環,我們來分析在第二次循環的處理:
            3.1. 場景3,fd收到了數據後進入第二次for (;;)循環,回到場景1,退出死循環返回事件發生個數之和
            3.2. 場景4,fd收到了數據後進入第二次for (;;)循環,回到場景1,退出死循環返回事件發生個數之和
            3.3. 場景5,超時後timed_out被置1,進入第二次for (;;)循環後,回到場景2,退出死循環返回事件發生個數之和,此時和爲0
        因此,對於所有場景,經過一次或者兩次for (;;)循環後,均能退出死循環,返回事件發生個數之和
        */
    }

    poll_freewait(&table); // 進程喚醒後,移除所有fd的讀寫等待隊列中該進程相關的表項

    return retval; // 返回所有fd發生的所有事件的個數之和
}

       主要的處理邏輯在註釋中已做說明,do_select主要處理邏輯就是:循環遍歷所有的n個bit位對應的fd,通過調用其驅動函數poll獲取該fd是否有事件發生:

       對於場景1,第一次for (;;)循環就能退出死循環,返回事件發生個數之和

       對於場景2,第一次for (;;)循環就能退出死循環,返回事件發生個數之和,此時和爲0

       對於場景3,fd收到了數據後進入第二次for (;;)循環,回到場景1,退出死循環返回事件發生個數之和

       對於場景4,fd收到了數據後進入第二次for (;;)循環,回到場景1,退出死循環返回事件發生個數之和

       對於場景5,超時後timed_out被置1,進入第二次for (;;)循環後,回到場景2,退出死循環返回事件發生個數之和,此時和爲0

 

7. 設備驅動函數poll示例

       在上一節中,我們知道do_select函數通過調用驅動函數poll來獲取fd發生的事件,那麼驅動函數poll是如何知道該fd上有沒有事件發生呢,以及如何將進程添加到自己的讀寫等待隊列的呢?我們來分析一個例子,該設備用一個環形隊列作爲輸入輸出緩存,當環形隊列還有空間時,則設置可寫事件,當隊列有數據時,則設置可讀事件:

static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    unsigned int mask = 0;
    ...

    poll_wait(filp, &dev->inq,  wait); // 將當前進程加入fd讀寫等待隊列
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;   // 發生可讀事件
    if (spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;  // 發生可寫事件

    return mask; // 返回fd發生的事件
}

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && p->_qproc && wait_address)
        p->_qproc(filp, wait_address, p); // 調用註冊的_qproc函數
}

void poll_initwait(struct poll_wqueues *pwq)
{
    init_poll_funcptr(&pwq->pt, __pollwait); // 註冊_qproc函數
    pwq->polling_task = current; // 賦值當前進程,用於將當前進程加入fd讀寫等待隊列
    ...
}

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
    ...
    add_wait_queue(wait_address, &entry->wait); // 將包含當前進程的節點添加到fd讀寫等待隊列
}

        驅動函數調用poll_wait將當前進程加入fd讀寫等待隊列,poll_wait調用poll_initwait函數中註冊的__pollwait函數,而在__pollwait函數中,通過調用add_wait_queue函數才真正的將當前進程添加到fd讀寫等待隊列

 

8. 進程喚醒

       我們思考一個問題,對於場景3和4,當進程處於睡眠狀態的時候,如果這個時候任何一個fd上發生了事件,進程都將會喚醒,進而函數返回。這是如何實現的呢,爲什麼任何fd發生了事件都可以喚醒進程呢?

      上面分析驅動函數poll的功能的時候我們知道,在調用每個fd的poll函數的時候,會將當前進程加入到該fd的讀寫等待隊列,當完成一次for (;;)循環之後,該進程就被加入到所有fd的讀寫等待隊列了。當任意一個fd收到收據,都會喚醒自己讀寫等待隊列裏的所有進程,這就實現了當任何一個fd可讀可寫時就能立即喚醒進程的原理

       我們以一個串口設備爲例,當硬件設備收到數據時,觸發interrupt中斷處理,經過一系列的處理,最終調用到receive_buf函數來喚醒進程:

serial8250_interrupt -> serial8250_handle_port -> receive_chars -> tty_flip_buffer_push -> flush_to_ldisc -> disc->receive_buf

disc->receive_buf()
    if (waitqueue_active(&tty->read_wait))
        wake_up_interruptible(&tty->read_wait);

        在receive_buf函數中,首先判斷是否有進程阻塞在自己的讀寫等待隊列中,如果有則調用wake_up_interruptible函數喚醒這些進程。當進程喚醒後,select函數返回之前,通過調用poll_freewait()函數移除所有fd的讀寫等待隊列中該進程相關的表項

 

9. select 示例程序

#include <sys/select.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char *argv[])
{
    fd_set rfds;
    struct timeval tv;
    int fd;

    mkfifo("test_fifo", 0666);
    fd = open("test_fifo", O_RDWR);
    while(1)
    {
        FD_SET(0, &rfds);   // 每次select調用之前都需要重新設置監聽描述符和超時參數
        FD_SET(fd, &rfds);  
        tv.tv_sec = 3;
        tv.tv_usec = 0;

        int ret = select(FD_SETSIZE, &rfds, NULL, NULL, &tv);
        if (-1 == ret) // 異常
        {
            perror("select()");
        }
        else if (0 == ret) // 超時
        {
            printf("timeout\n");
        }
        else if (ret > 0) // 有fd收到數據
        {
            char buf[128] = {0};
            if (FD_ISSET(0, &rfds))  // 通過FD_ISSET判斷是哪個fd發生了事件
            {
                read(0, buf, sizeof(buf));
                printf("stdin = %s\n", buf);
            }
            else if (FD_ISSET(fd, &rfds))
            { 
                read(fd, buf, sizeof(buf));
                printf("fifo = %s\n", buf);
            }
        }
    }
    return 0;
}

 

10. select 特點

優點

      a. select目前幾乎在所有的平臺上支持,有良好跨平臺支持特性,在某些Unix系統上不支持poll()和epoll()

      b. select對於超時提供了更好的精度,微秒級,而poll是毫秒

缺點

       a. 每次調用 select,都需要把 fd 從用戶態拷貝到內核態,然後在內核輪詢遍歷所有 fd,再把結果拷貝回用戶態。返回後用戶還需要輪詢fd_set來知道哪些fd是有事件發生的,fd多的時候開銷很大

       b. 單個進程能夠監視的文件描述符的數量存在最大限制,在 Linux 上一般爲 1024,可以通過修改宏定義重新編譯內核的方式提升這一限制,但是這樣也會造成效率的降低

       c. select返回結果時複用了用戶註冊監聽事件的fd_set結構,因此每次調用select前需要重新註冊監聽事件。超時參數在返回時也是未定義的,每次調用select之前也需要重新設置超時參數

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章