IO複用——select內核源代碼剖析

*在高併發編程中,多次使用IO複用select函數,本篇就來深入剖析一下其內核源碼。。。→_→*

瞭解poll機制底層原理請戳傳送門——IO複用——poll機制內核源代碼分析

瞭解select應用實例請戳傳送門——IO複用——select函數應用實例

< – 2017-08-11 23:40 – >
還有一部分代碼沒貼。。。明天把select這個硬骨頭啃下來。。。

< – 2017-08-16 13:32 – >
宿舍沒網。。我的流量啊。。因爲源代碼大部分都在Select.c中。。。所以會有點多。。。看官請直接進sys_select。。。

//linux-2.4.0\fs\Select.c

//提供了6個宏函數,返回要求位圖或結果位圖中對應的下標元素的值
#define __IN(fds, n)        (fds->in + n)
#define __OUT(fds, n)       (fds->out + n)
#define __EX(fds, n)        (fds->ex + n)
#define __RES_IN(fds, n)    (fds->res_in + n)
#define __RES_OUT(fds, n)   (fds->res_out + n)
#define __RES_EX(fds, n)    (fds->res_ex + n)

//這個元素下標可以同時對應三種位圖,所以在一個位圖中監聽存在就行
#define BITS(fds, n)        (*__IN(fds, n)|*__OUT(fds, n)|*__EX(fds, n))

static int max_select_fd(unsigned long n, fd_set_bits *fds)
{
    unsigned long *open_fds;
    unsigned long set;
    int max;

    /* handle last in-complete long-word first */
    set = ~(~0UL << (n & (__NFDBITS-1)));
    n /= __NFDBITS; //將所監聽的文件描述符的個數轉化爲位圖的元素下標
    open_fds = current->files->open_fds->fds_bits+n;
    max = 0; //記錄最大的序號
    if (set) {
        set &= BITS(fds, n);
        if (set) {
            if (!(set & ~*open_fds))
                goto get_max;
            return -EBADF;
        }
    }
    while (n) {
        open_fds--;
        n--;
        set = BITS(fds, n); //判斷在位圖下標位n的元素中是否有要監聽的文件描述符
        if (!set) //如果位圖中下標爲n的元素中沒有要監聽的文件描述符,就繼續尋找
            continue;
        if (set & ~*open_fds)
            return -EBADF;
        if (max) //這裏的最大序號可以理解爲一個標誌位
            continue;
get_max:
        do {
            max++;
            set >>= 1;
        } while (set);
        max += n * __NFDBITS; //再將位圖中的元素序號轉化爲文件描述符對應的個數
    }

    return max;
}

#define BIT(i)      (1UL << ((i)&(__NFDBITS-1)))
#define MEM(i,m)    ((m)+(unsigned)(i)/__NFDBITS)
#define ISSET(i,m)  (((i)&*(m)) != 0)
#define SET(i,m)    (*(m) |= (i))

#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

int do_select(int n, fd_set_bits *fds, long *timeout)
{
    //poll_table類型結構在下面有解釋說明
    poll_table table, *wait;
    int retval, i, off;
    long __timeout = *timeout;

    read_lock(&current->files->file_lock);
    //計算所監聽的文件描述符在位圖中的最大的序號是多少,高於這個序號的文件描述符都與本次操作無關
    //max_select_fd定義在上面
    retval = max_select_fd(n, fds);
    read_unlock(&current->files->file_lock);

    if (retval < 0)
        return retval;
    n = retval;

    //當一個進程要進入睡眠,而想要某個設備的驅動程序在設備的狀態發生變化時將其喚醒,就要準備一個wait_queue_t數據結構,並將這個數據結構掛入目標設備的某個等待隊列中。而wait_queue_t就封裝在poll_table類型結構中
    //初始化poll_table類型結構變量,將table成員置爲NULL,error成員置爲0
    poll_initwait(&table);
    wait = &table;
    if (!__timeout)
        wait = NULL;
    retval = 0;
    //進入for循環,直到所監聽的事件就緒,或指定的睡眠等待時間到期,或者當前進程收到了信號時纔會結束
    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE); //將當前進程的狀態置爲可中斷阻塞,即當前進程將會進入淺睡眠狀態
        //內層for循環中,對所要監視的文件描述符對應的位圖進行一次掃描
        for (i = 0 ; i < n; i++) {
            unsigned long bit = BIT(i);
            unsigned long mask;
            struct file *file;

            off = i / __NFDBITS; //將文件描述符的個數i轉化爲位圖中所對應的元素下標off
            if (!(bit & BITS(fds, off))) //如果三種位圖的某一位爲1,就對相應的文件描述符做一次詢問
                continue;
            file = fget(i);
            mask = POLLNVAL;
            //文件的具體詢問方式和其類型有關,即是通過file_operations數據結構中的函數指針poll進行的。關於poll操作將會在另一篇文章中總結。。
            if (file) {
                mask = DEFAULT_POLLMASK;
                if (file->f_op && file->f_op->poll)
                    mask = file->f_op->poll(file, wait);
                fput(file);
            }
            //retval記錄一共有幾個事件就緒
            //將詢問的輸入結果彙集到fds所指的fd_set_bits變量中
            if ((mask & POLLIN_SET) && ISSET(bit, __IN(fds,off))) {
                SET(bit, __RES_IN(fds,off));
                retval++; //記錄一共有多少個事件就緒
                wait = NULL;
            }
            //將詢問的輸出結果彙集到fds所指的fd_set_bits變量中
            if ((mask & POLLOUT_SET) && ISSET(bit, __OUT(fds,off))) {
                SET(bit, __RES_OUT(fds,off));
                retval++; //記錄一共有多少個事件就緒
                wait = NULL;
            }
            //將詢問的異常結果彙集到fds所指的fd_set_bits變量中
            if ((mask & POLLEX_SET) && ISSET(bit, __EX(fds,off))) {
                SET(bit, __RES_EX(fds,off));
                retval++; //記錄一共有多少個事件就緒
                wait = NULL;
            }
        }
        wait = NULL;
        //對所有的文件描述符進行詢問後,檢查是否有事件就緒、睡眠等待時間超時、接收到了信號,如果有條件滿足,就不會再進入睡眠狀態,直接結束大循環
        //統計好就緒事件後,此時retval不爲0,從break跳出,結束大循環
        if (retval || !__timeout || signal_pending(current))
            break;
        //檢查是否出錯,如果出錯,也不會進入睡眠狀態,直接結束大循環
        if(table.error) {
            retval = table.error;
            break;
        }
        //進入睡眠狀態,被喚醒後再進行一次掃描詢問
        //除第一次以外,以後都是在進程被喚醒時才執行一遍循環,從本質上講是一種do-while循環
        __timeout = schedule_timeout(__timeout);
    }
    current->state = TASK_RUNNING; //設置當前進程的狀態爲運行態

    //將所有進程對應的wait_queue_t結構從各個等待隊列中刪除
    //poll_freewait定義在下面
    poll_freewait(&table);

    /*
     * Up-to-date the caller timeout.
     */
    *timeout = __timeout;
    return retval; //返回就緒事件的總數
}

static void *select_bits_alloc(int size)
{
    return kmalloc(6 * size, GFP_KERNEL); //通過kmalloc分配6個位圖
}

static void select_bits_free(void *bits, int size)
{
    kfree(bits);
}

/*
 * We can actually return ERESTARTSYS instead of EINTR, but I'd
 * like to be certain this leads to no problems. So I return
 * EINTR just for safety.
 *
 * Update: ERESTARTSYS breaks at least the xview clock binary, so
 * I'm trying ERESTARTNOHAND which restart only when you want to.
 */
#define MAX_SELECT_SECONDS \
    ((unsigned long) (MAX_SCHEDULE_TIMEOUT / HZ)-1)

//n表示調用時的參數表中一共有多少個位圖,即需要監聽的文件描述符最大值,一般爲最大值加1
//fd_set類型結構在下面有解釋說明
//fd_set表示已打開文件的位圖,位圖的每一位都代表着當前進程的一個已打開文件,根據其結構定義得知,select最多可以監聽1024個文件描述符
//timeval類型結構在下面有解釋說明
//tvp表示睡眠等待的最長時間,如果爲0則表示立即返回,如果爲NULL則表示阻塞等待,直到所監聽的事件就緒
asmlinkage long
sys_select(int n, fd_set *inp, fd_set *outp, fd_set *exp, struct timeval *tvp)
{
    //fd_set_bits類型結構在下面有解釋說明
    //其中分別保存了3種位圖的要求和結果
    fd_set_bits fds;
    char *bits;
    long timeout;
    int ret, size;

    timeout = MAX_SCHEDULE_TIMEOUT;
    if (tvp) {
        time_t sec, usec;

        //將所需數據數據從用戶空間拷貝到內核空間
        if ((ret = verify_area(VERIFY_READ, tvp, sizeof(*tvp)))
            || (ret = __get_user(sec, &tvp->tv_sec))
            || (ret = __get_user(usec, &tvp->tv_usec)))
            goto out_nofds;

        ret = -EINVAL;
        if (sec < 0 || usec < 0)
            goto out_nofds;

        if ((unsigned long) sec < MAX_SELECT_SECONDS) {
            timeout = ROUND_UP(usec, 1000000/HZ);
            timeout += sec * (unsigned long) HZ;
        }
    }

    ret = -EINVAL;
    if (n < 0)
        goto out_nofds;

    //判斷文件描述符數量有沒有超過最大值
    if (n > current->files->max_fdset)
        n = current->files->max_fdset;

    /*
     * We need 6 bitmaps (in/out/ex for both incoming and outgoing),
     * since we used fdset we need to allocate memory in units of
     * long-words. 
     */
    ret = -ENOMEM;
    size = FDS_BYTES(n);

    //select_bits_alloc定義在上面
    //一共分配6個位圖
    bits = select_bits_alloc(size);
    //爲要求和結果,一共6個位圖初始化
    if (!bits)
        goto out_nofds;
    fds.in      = (unsigned long *)  bits;
    fds.out     = (unsigned long *) (bits +   size);
    fds.ex      = (unsigned long *) (bits + 2*size);
    fds.res_in  = (unsigned long *) (bits + 3*size);
    fds.res_out = (unsigned long *) (bits + 4*size);
    fds.res_ex  = (unsigned long *) (bits + 5*size);

    //將3個要求位圖從用戶空間複製到內核空間中的fds的要求位圖
    if ((ret = get_fd_set(n, inp, fds.in)) ||
        (ret = get_fd_set(n, outp, fds.out)) ||
        (ret = get_fd_set(n, exp, fds.ex)))
        goto out;
    //將內核空間的fds的結果位圖初始化爲0
    zero_fd_set(n, fds.res_in);
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    //好戲開始。。。do_select定義在上面。。。→_→
    // ret記錄就緒事件的總數
    ret = do_select(n, &fds, &timeout);

    if (tvp && !(current->personality & STICKY_TIMEOUTS)) {
        time_t sec = 0, usec = 0;
        if (timeout) {
            sec = timeout / HZ;
            usec = timeout % HZ;
            usec *= (1000000/HZ);
        }
        put_user(sec, &tvp->tv_sec);
        put_user(usec, &tvp->tv_usec);
    }

    if (ret < 0)
        goto out;
    if (!ret) {
        ret = -ERESTARTNOHAND;
        if (signal_pending(current))
            goto out;
        ret = 0;
    }

    //將3個結果位圖的內容複製到用戶空間中
    set_fd_set(n, inp, fds.res_in); 
    set_fd_set(n, outp, fds.res_out);
    set_fd_set(n, exp, fds.res_ex);

out:
    select_bits_free(bits, size); //釋放要求和結果位圖的6個位圖的空間
out_nofds:
    return ret; //返回就緒事件的總數
}
// linux-2.4.0\include\linux\\Poll.h
//記錄要求的3個位圖和結果的3個位圖
typedef struct {
    unsigned long *in, *out, *ex; //要求
    unsigned long *res_in, *res_out, *res_ex; //結果
} fd_set_bits;
//linux-2.4.0\include\linux\Time.h
struct timeval {
    time_t      tv_sec;     /* 秒數 seconds */
    suseconds_t tv_usec;    /* 微秒數 microseconds */
};
//fd_set類型的定義
//linux-2.4.0\include\linux\Posix_types.h
#undef __NFDBITS
//__NFDBITS的值爲32
#define __NFDBITS   (8 * sizeof(unsigned long))

#undef __FD_SETSIZE
#define __FD_SETSIZE    1024

#undef __FDSET_LONGS
//__FDSET_LONGS的值爲32
#define __FDSET_LONGS   (__FD_SETSIZE/__NFDBITS)

#undef __FDELT
#define __FDELT(d)  ((d) / __NFDBITS)

#undef __FDMASK
#define __FDMASK(d) (1UL << ((d) % __NFDBITS))

//因此fd_set實際上是一個具有32個元素的unsigned long類型的數組
typedef struct {
    unsigned long fds_bits [__FDSET_LONGS];
} __kernel_fd_set;

typedef __kernel_fd_set     fd_set;
//linux-2.4.0\include\linux\Poll.h
//poll_table_page類型結構在下面有解釋說明
typedef struct poll_table_struct {
    int error;
    struct poll_table_page * table;
} poll_table;
//linux-2.4.0\fs\Select.c
struct poll_table_entry {
    struct file * filp;
    wait_queue_t wait; //被封裝的wait_queue_t
    wait_queue_head_t * wait_address; //等待隊列的隊頭
};

struct poll_table_page {
    //一個頁面用完了就再分配一個,通過next鏈成一條單鏈
    struct poll_table_page * next;
    //poll_table_entry類型結構上面有解釋說明
    //entry總是指向entries中第一個空閒的poll_table_entry結構,根據需要動態的分配entries中的表項
    struct poll_table_entry * entry;
    //表示該數組可以動態地確定大小,實際使用中分配一個頁面,頁面中能容納幾個poll_table_entry,這個數組就有多大
    struct poll_table_entry entries[0]; 
};
//linux-2.4.0\fs\Select.c
void poll_freewait(poll_table* pt)
{
    struct poll_table_page * p = pt->table; //p指向第一個poll_table_page結構,poll_table_page結構由next成員鏈成單鏈
    //當p不爲NULL時,繼續循環
    while (p) {
        struct poll_table_entry * entry;
        struct poll_table_page *old;

        entry = p->entry; //entry指向entries中第一個空閒的poll_table_entry結構,entries是一個數組
        do {
            entry--; //entry前移
            remove_wait_queue(entry->wait_address,&entry->wait); //將entry表示的wait_queue_t結構從等待隊列中刪除
            fput(entry->filp);
        } while (entry > p->entries); //判斷此entries數組中是否還有元素未刪除
        old = p; //此時poll_table_page結構中的entries數組中已沒有元素,此時old記錄當前poll_table_page 結構
        p = p->next; //p指向下一個poll_table_page 結構
        free_page((unsigned long) old); //釋放old所指向的poll_table_page結構頁面
    }
}

*哇。。。終於分析完了!!!一會兒直接上poll操作的源碼分析和select的應用實例。。。→_→*

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