linux的select源碼分析

先說(shuō)說(shuō)內(nèi)核的職責(zé)

我們已經(jīng)知道了所有的io操作都是交給內(nèi)核去處理了,在linux中,已經(jīng)抽象出了一個(gè)文件系統(tǒng),對(duì)任何io設(shè)備的讀寫(xiě)都可以當(dāng)做對(duì)文件系統(tǒng)的某一個(gè)文件進(jìn)行讀寫(xiě)。文件是一個(gè)抽象出來(lái)的概念(它包含了實(shí)際對(duì)應(yīng)的驅(qū)動(dòng),當(dāng)前文件指針,文件大小,數(shù)據(jù)讀寫(xiě)緩沖區(qū)指針等信息),當(dāng)用戶程序需要讀寫(xiě)一個(gè)文件時(shí),需要先調(diào)用sys_open,這樣內(nèi)核會(huì)從文件系統(tǒng)讀取該文件的節(jié)點(diǎn)信息,每個(gè)進(jìn)程都有一個(gè)fd數(shù)組,內(nèi)核會(huì)給fd數(shù)組增加一條記錄,然后返回給用戶程序這個(gè)新增的數(shù)組下標(biāo)。所以我們平時(shí)用戶程序拿到的fd其實(shí)僅僅是內(nèi)核中為我們打開(kāi)的fd數(shù)組中的下標(biāo),我們是對(duì)文件的細(xì)節(jié)一無(wú)所知的。剩下我們讀寫(xiě)的時(shí)候,會(huì)傳入fd,這樣內(nèi)核就能從數(shù)組中讀取對(duì)應(yīng)的節(jié)點(diǎn)對(duì)象,從中取出緩沖區(qū)等信息,當(dāng)然了內(nèi)核也是分為很多模塊的,除了驅(qū)動(dòng)層會(huì)直接跟真正的外設(shè)打交道,其他模塊都是對(duì)緩沖區(qū)進(jìn)行操作的。

內(nèi)核本就是異步的

內(nèi)核層對(duì)文件的處理本就是異步進(jìn)行的,讀的時(shí)候會(huì)告訴驅(qū)動(dòng)程序讀取的扇面號(hào)(比如是讀寫(xiě)磁盤(pán))和需要讀取的緩沖區(qū)地址,驅(qū)動(dòng)程序向文件控制模塊發(fā)送指令后就干別的了,當(dāng)硬件完成了工作后會(huì)向cpu發(fā)送中斷信號(hào)從而被內(nèi)核捕獲,內(nèi)核會(huì)從中斷點(diǎn)開(kāi)始繼續(xù)執(zhí)行。

用戶程序?yàn)槭裁葱枰惒?/h3>

看早期的linux代碼,會(huì)發(fā)現(xiàn)還沒(méi)加入非堵塞io,也就是說(shuō)用戶進(jìn)程讀寫(xiě)一個(gè)fd的時(shí)候,雖然內(nèi)核是異步的,但內(nèi)核卻會(huì)把進(jìn)程給睡眠掉,而當(dāng)中斷返回后才喚醒進(jìn)程(其實(shí)只有讀,寫(xiě)的話非嚴(yán)格模式時(shí)內(nèi)核只是把數(shù)據(jù)從用戶空間搬到了文件緩沖區(qū)中并把緩沖區(qū)設(shè)置為臟,內(nèi)核會(huì)在未來(lái)某個(gè)時(shí)候才去調(diào)用驅(qū)動(dòng)真正寫(xiě)入磁盤(pán))。

但這樣就對(duì)用戶程序有很多限制了,有很多程序并不是計(jì)算型的,而是io型的,比如一個(gè)記事本程序,它最主要的工作就是堅(jiān)挺鼠標(biāo)和鍵盤(pán)的輸入,如果只有堵塞模型,會(huì)有很大的困難。

select的出現(xiàn)

所以這時(shí)候出現(xiàn)select也就是一件很自然的事情了,如果我們自己假設(shè)是內(nèi)核開(kāi)發(fā)人員,是不是也能大概猜到一些細(xì)節(jié)了。是的,內(nèi)核本來(lái)就是異步的,以前進(jìn)程只能監(jiān)聽(tīng)一個(gè)fd,那我們就增加一個(gè)委托人幫我們監(jiān)聽(tīng)多個(gè)fd,進(jìn)程把需要監(jiān)聽(tīng)的fd交給我們的委托人,委托人去監(jiān)聽(tīng)這些fd,當(dāng)任何一個(gè)fd可以讀寫(xiě)的時(shí)候委托人就跑過(guò)來(lái)喚醒我們的進(jìn)程。沒(méi)錯(cuò),這個(gè)委托人就是今天的主角select。

源碼

源碼是1.0版本的,雖然很老,但思想都是差不多的,最新版本的模塊化啥的看起來(lái)如果對(duì)內(nèi)核不熟悉很難看(其實(shí)就是因?yàn)槲也斯?/p>

首先看入口方法sys_select

asmlinkage int sys_select( unsigned long *buffer )
{
// 還記得c方法中select的參數(shù)嗎
// 第一個(gè)是最大fd值,第二個(gè)是監(jiān)聽(tīng)的讀fd_set,第三個(gè)是監(jiān)聽(tīng)的寫(xiě)fd_set,第四個(gè)是錯(cuò)誤fd_set,最后一個(gè)是超時(shí) 
// 這兒參數(shù)只有一個(gè),其實(shí)是一種變參,C中局部變量是放在棧中的,按從右到左的順序入棧
// 所以拿到第一個(gè)參數(shù),就能依次獲取剩下的參數(shù)了
// get_fs_long方法,linux中進(jìn)入內(nèi)核態(tài)使用的段是內(nèi)核段,但內(nèi)核會(huì)把fs寄存器存入用戶段選擇符,所以
// get_fs_long方法就是從用戶內(nèi)存空間讀數(shù)據(jù)到內(nèi)核空間
    int i;
    fd_set res_in, in, *inp;
    fd_set res_out, out, *outp;
    fd_set res_ex, ex, *exp;
    int n;
    struct timeval *tvp;
    unsigned long timeout;

// 內(nèi)存檢測(cè)
    i = verify_area(VERIFY_READ, buffer, 20);
    if (i)
        return i;
// 這就是取第一個(gè)參數(shù)最大fd值n,并且把buffer指針加一,讓它指向第二個(gè)參數(shù)
    n = get_fs_long(buffer++);
// 對(duì)n進(jìn)行校驗(yàn),可以看到這兒判斷了n的最大值不能超過(guò)NR_OPEN,這也就是網(wǎng)上很多說(shuō)select有數(shù)量限制的原因,這個(gè)后面細(xì)說(shuō)
    if (n < 0)
        return -EINVAL;
    if (n > NR_OPEN)
        n = NR_OPEN;
// 依次讀出用戶程序的參數(shù)
    inp = (fd_set *) get_fs_long(buffer++);
    outp = (fd_set *) get_fs_long(buffer++);
    exp = (fd_set *) get_fs_long(buffer++);
    tvp = (struct timeval *) get_fs_long(buffer);

// 上面的inp,outp,exp,tvp看出變量名都有個(gè)p,它們都是指針,我們只是把4個(gè)指針讀到內(nèi)核空間了
// get_fd_set作用就是從用戶空間把指針對(duì)應(yīng)的值讀入內(nèi)核空間的in,out,ex變量,超時(shí)是一個(gè)時(shí)間對(duì)象,這個(gè)具體可以自己查詢一下
    if ((i = get_fd_set(n, inp, &in)) ||
        (i = get_fd_set(n, outp, &out)) ||
        (i = get_fd_set(n, exp, &ex))) return i;
    timeout = ~0UL;
    if (tvp) {
        i = verify_area(VERIFY_WRITE, tvp, sizeof(*tvp));
        if (i)
            return i;
        timeout = ROUND_UP(get_fs_long((unsigned long *)&tvp->tv_usec),(1000000/HZ));
        timeout += get_fs_long((unsigned long *)&tvp->tv_sec) * HZ;
        if (timeout)
            timeout += jiffies + 1;
    }
// 設(shè)置進(jìn)程的超時(shí)屬性,current代表當(dāng)前進(jìn)程的描述符對(duì)象
    current->timeout = timeout;
// 好了上面其實(shí)都是從用戶空間搬運(yùn)數(shù)據(jù)到內(nèi)核,接下來(lái)才是真正的主角do_select
    i = do_select(n, &in, &out, &ex, &res_in, &res_out, &res_ex);
    /* 記錄實(shí)際等待時(shí)間 */
    if (current->timeout > jiffies)
        timeout = current->timeout - jiffies;
    else
        timeout = 0;
    current->timeout = 0;
    if (tvp) {
        put_fs_long(timeout/HZ, (unsigned long *) &tvp->tv_sec);
        timeout %= HZ;
        timeout *= (1000000/HZ);
        put_fs_long(timeout, (unsigned long *) &tvp->tv_usec);
    }
    if (i < 0)
        return i;
    if (!i && (current->signal & ~current->blocked))
        return -ERESTARTNOHAND;
    /* 將監(jiān)測(cè)到的結(jié)果回寫(xiě)到參數(shù)對(duì)應(yīng)的內(nèi)存當(dāng)中 */
    set_fd_set(n, inp, &res_in);
    set_fd_set(n, outp, &res_out);
    set_fd_set(n, exp, &res_ex);
    return i;
}

上面注釋已經(jīng)寫(xiě)得蠻清晰了,總結(jié)一下sys_select干了什么,其實(shí)就是先從用戶空間把需要監(jiān)聽(tīng)的fd_set和超時(shí)時(shí)間讀到內(nèi)核然后調(diào)用do_select方法。那接下來(lái)就看看do_select

int do_select(int n, fd_set *in, fd_set *out, fd_set *ex,
    fd_set *res_in, fd_set *res_out, fd_set *res_ex)
{
    int count;
    select_table wait_table, *wait;
    struct select_table_entry *entry;
    unsigned long set;
    int i,j;
    int max = -1;

    /* 循環(huán)檢查每個(gè)文件集合的位 */
    for (j = 0 ; j < __FDSET_LONGS ; j++) {
        /* 一個(gè)unsigned long能夠表示32個(gè)文件描述符,32=2的5次方*/
        i = j << 5;
        /* 超過(guò)最大文件描述符,則停止 */
        if (i >= n)
            break;
        set = in->fds_bits[j] | out->fds_bits[j] | ex->fds_bits[j];
        /* set移動(dòng)8次就等于0了 */
        for ( ; set ; i++,set >>= 1) {
            if (i >= n)
                goto end_check;
            /* 測(cè)試集合中的最后一位 */
            if (!(set & 1))
                continue;
            // 如果進(jìn)程并未打開(kāi)該fd返回錯(cuò)誤
            if (!current->filp[i])
                return -EBADF;
            // 如果進(jìn)程打開(kāi)的fd沒(méi)有節(jié)點(diǎn)屬性返回錯(cuò)誤
            if (!current->filp[i]->f_inode)
                return -EBADF;
            /* 記錄最大的文件描述符 */
            max = i;
        }
    }
end_check:
    /* 記錄實(shí)際監(jiān)視的文件描述符的最大值+1 */
    n = max + 1;

    /* 獲取占用一頁(yè)大小的select_table_entry內(nèi)存 */
    if(!(entry = (struct select_table_entry*) __get_free_page(GFP_KERNEL)))
        return -ENOMEM;
    FD_ZERO(res_in);
    FD_ZERO(res_out);
    FD_ZERO(res_ex);
    count = 0;
    /* 初始化等待列表 */
    wait_table.nr = 0;
    wait_table.entry = entry;
    wait = &wait_table;
repeat:
    current->state = TASK_INTERRUPTIBLE;
    /* 循環(huán)掃描所有監(jiān)視的文件描述符,注意這里的wait第一次檢測(cè)到有文件變動(dòng)之前傳入check函數(shù)不為NULL,
     * 之后就都為NULL,因?yàn)槊恳粋€(gè)網(wǎng)絡(luò)文件描述符都對(duì)應(yīng)一個(gè)唯一的struct sock,當(dāng)前進(jìn)程只需要在struct sock
     * 的sleep中等待一次就夠了。
     */
    for (i = 0 ; i < n ; i++) {
        if (FD_ISSET(i,in) && check(SEL_IN,wait,current->filp[i])) {
            FD_SET(i, res_in);
            count++;
            wait = NULL; /* 此處設(shè)置為NULL,就代表已經(jīng)檢測(cè)到了一個(gè)文件變動(dòng),所以不需要在等待在其他文件上
                              * 在if(!count-----)判斷時(shí)就可以快速返回 
                              */
        }
        if (FD_ISSET(i,out) && check(SEL_OUT,wait,current->filp[i])) {
            FD_SET(i, res_out);
            count++;
            wait = NULL;
        }
        if (FD_ISSET(i,ex) && check(SEL_EX,wait,current->filp[i])) {
            FD_SET(i, res_ex);
            count++;
            wait = NULL;
        }
    }
    /* 所有的文件被掃描一次過(guò)后,就已經(jīng)添加到struct sock中的sleep當(dāng)中,
     * 如果wait不為NULL,則會(huì)繼續(xù)添加一次 
     */
    wait = NULL;
    /* 注意這里的阻塞道理,經(jīng)過(guò)一輪掃描過(guò)后,一旦監(jiān)視到文件有變動(dòng),就立即返回
     * 在第三個(gè)判斷條件當(dāng)中,如果當(dāng)前進(jìn)程接收到了信號(hào),并且沒(méi)有
     * 被阻塞,則select函數(shù)不會(huì)繼續(xù)阻塞了,因?yàn)樵摵瘮?shù)是在內(nèi)核態(tài)當(dāng)中,
     * 因?yàn)樾盘?hào)的處理函數(shù)是在內(nèi)核態(tài)返回到用戶態(tài)的時(shí)候執(zhí)行的,所以為了
     * 盡快響應(yīng)信號(hào),就讓進(jìn)程從該函數(shù)當(dāng)中快速退出 
     */
    if (!count && current->timeout && !(current->signal & ~current->blocked)) {
        /* 此時(shí)當(dāng)前進(jìn)程已經(jīng)添加到所有strut sock中的sleep鏈表當(dāng)中,主動(dòng)放棄cpu */
        schedule();
        goto repeat;
    }
    /* 將當(dāng)前進(jìn)程從所有已經(jīng)添加到struct sock的sleep鏈表中刪除 */
    free_wait(&wait_table);
    free_page((unsigned long) entry);
    current->state = TASK_RUNNING;
    return count;
}

這一段的注釋是摘自github,感謝

主要的判斷fd是否準(zhǔn)話好了是check方法,看看它干了啥

static int check(int flag, select_table * wait, struct file * file)
{
    struct inode * inode;
    struct file_operations *fops;
    int (*select) (struct inode *, struct file *, int, select_table *);

// 取fd對(duì)應(yīng)文件的節(jié)點(diǎn)對(duì)象
    inode = file->f_inode;
// f_op是一個(gè)操作對(duì)象,因?yàn)槲募泻芏喾N,有塊文件字符文件,網(wǎng)絡(luò)文件,對(duì)應(yīng)不同的f_op對(duì)象
// 取實(shí)際對(duì)象的select方法,然后執(zhí)行
    if ((fops = file->f_op) && (select = fops->select))
        return select(inode, file, flag, wait)
            || (wait && select(inode, file, flag, NULL));
    /* 普通文件肯定是可以被讀寫(xiě)的 */
    if (S_ISREG(inode->i_mode))
        return 1;
    return 0;
}

struct select_table_entry {
    struct wait_queue wait;         /* 實(shí)際添加到等待隊(duì)列中的變量 */
    struct wait_queue ** wait_address;  /* 指向?qū)嶋H等待鏈表的首部,方便從wait_address中刪除或添加wait */
};

typedef struct select_table_struct {
    int nr;
    struct select_table_entry * entry;
} select_table;

struct file {
    mode_t f_mode;          /* 文件不存在時(shí),創(chuàng)建文件的權(quán)限 */
    dev_t f_rdev;           /* needed for /dev/tty */
    off_t f_pos;            /* 文件讀寫(xiě)偏移量 */
    unsigned short f_flags; /* 以什么樣的方式打開(kāi)文件,如只讀,只寫(xiě)等等 */
    unsigned short f_count;  /*文件的引用計(jì)數(shù)*/
    unsigned short f_reada;
    struct file *f_next, *f_prev;
    struct inode * f_inode;     /* 文件對(duì)應(yīng)的inode */
    struct file_operations * f_op;    // 對(duì)應(yīng)inode的文件操作
};

/* 對(duì)inode對(duì)應(yīng)文件的操作
 */
struct file_operations {
    int (*lseek) (struct inode *, struct file *, off_t, int);
    int (*read) (struct inode *, struct file *, char *, int);
    int (*write) (struct inode *, struct file *, char *, int);
    int (*readdir) (struct inode *, struct file *, struct dirent *, int);
    int (*select) (struct inode *, struct file *, int, select_table *);
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
    int (*mmap) (struct inode *, struct file *, unsigned long, size_t, int, unsigned long);
    int (*open) (struct inode *, struct file *);
    void (*release) (struct inode *, struct file *);
    int (*fsync) (struct inode *, struct file *);
};

現(xiàn)在知道了,內(nèi)核對(duì)每個(gè)fd_set遍歷,然后對(duì)每個(gè)node調(diào)用check方法,check方法是根據(jù)每個(gè)文件對(duì)象綁定的,這類似面向?qū)ο蟮慕涌冢恳环N文件類型都要實(shí)現(xiàn)它的file_operations屬性,屏蔽底層實(shí)現(xiàn)的不同而對(duì)上層暴露統(tǒng)一的接口。
比如搜了下內(nèi)核就會(huì)發(fā)現(xiàn)有

  • pipe_select 通道
  • mouse_select 鼠標(biāo)
  • tty_select 字符設(shè)備
  • sock_select 網(wǎng)絡(luò)文件
  • tcp_select tcp

還有很多就不一一列舉了,那我們找個(gè)簡(jiǎn)單的看一下吧- -! pipe_select

static int pipe_select(struct inode * inode, struct file * filp, int sel_type, select_table * wait)
{
    switch (sel_type) {
        case SEL_IN:
            if (!PIPE_EMPTY(*inode) || !PIPE_WRITERS(*inode))
                return 1;
            select_wait(&PIPE_WAIT(*inode), wait);
            return 0;
        case SEL_OUT:
            if (!PIPE_FULL(*inode) || !PIPE_READERS(*inode))
                return 1;
            select_wait(&PIPE_WAIT(*inode), wait);
            return 0;
        case SEL_EX:
            if (!PIPE_READERS(*inode) || !PIPE_WRITERS(*inode))
                return 1;
            select_wait(&inode->i_wait,wait);
            return 0;
    }
    return 0;
}

extern inline void select_wait(struct wait_queue ** wait_address, select_table * p)
{
    struct select_table_entry * entry;

    /* 如果有任意一個(gè)指針為NULL,則返回,不做處理 */
    if (!p || !wait_address)
        return;
    /* 數(shù)量不能超過(guò) */
    if (p->nr >= __MAX_SELECT_TABLE_ENTRIES)
        return;
    /* 獲取當(dāng)前操作的entry的地址 */
    entry = p->entry + p->nr;
    /* 注意這個(gè)地方很重要,也就是要記錄wait變量是等待在哪個(gè)鏈表當(dāng)中 */
    entry->wait_address = wait_address;
    entry->wait.task = current;
    entry->wait.next = NULL;
    add_wait_queue(wait_address,&entry->wait);
    /* 增加nr的數(shù)量 */
    p->nr++;
}

嗯,快接近真相了,內(nèi)核最終調(diào)用add_wait_queue把資源加入等待隊(duì)列中

extern inline void add_wait_queue(struct wait_queue ** p, struct wait_queue * wait)
{
    unsigned long flags;

// 調(diào)試相關(guān)
#ifdef DEBUG
    if (wait->next) {
        unsigned long pc;
        __asm__ __volatile__("call 1f\n"
            "1:\tpopl %0":"=r" (pc));
        printk("add_wait_queue (%08x): wait->next = %08x\n",pc,(unsigned long) wait->next);
    }
#endif
    save_flags(flags);
// 關(guān)中斷
    cli();
    /* 如果隊(duì)列為空,則讓wait指向隊(duì)首,next指向自己
     * 當(dāng)繼續(xù)向隊(duì)列中添加節(jié)點(diǎn)時(shí),整個(gè)隊(duì)列就是一個(gè)閉合的圓環(huán),
     * 否則將wait添加到以*p為隊(duì)首的下一個(gè)地方
     */
    if (!*p) {
        wait->next = wait;
        *p = wait;
    } else {
        wait->next = (*p)->next;
        (*p)->next = wait;
    }
    restore_flags(flags);
}

// 恢復(fù)flags
#define restore_flags(x) \
__asm__ __volatile__("pushl %0 ; popfl": /* no output */ :"r" (x):"memory")

好了,就是當(dāng)前進(jìn)程和等待位置加入到對(duì)應(yīng)的節(jié)點(diǎn)的等待隊(duì)列中,這樣當(dāng)內(nèi)核收到中斷返回文件可用時(shí)候就會(huì)挨個(gè)通知等待隊(duì)列上的任務(wù)了。

總結(jié)一下

select其實(shí)只是擴(kuò)展了內(nèi)核異步的結(jié)構(gòu),內(nèi)核的喚醒機(jī)制沒(méi)太大變化,只是給進(jìn)程委托了一個(gè)select的代理對(duì)象,但是性能來(lái)看,首先每次select都要從用戶空間把數(shù)據(jù)搬運(yùn)到內(nèi)核,而且還有一個(gè)fd_set這個(gè)數(shù)量限制,這里重點(diǎn)說(shuō)一下這個(gè)吧,fd_set是一個(gè)long性數(shù)組,linux用它的二進(jìn)制做位圖,1.0版本數(shù)組大小是8,也就是可以存放最大848=256個(gè)描述符,現(xiàn)在linux默認(rèn)是1024,我們可以修改該值來(lái)完成對(duì)select數(shù)量的限制。poll的原理其實(shí)和select差不多,只是不在使用這種參數(shù)傳遞的方式,而是傳入一個(gè)數(shù)組對(duì)象,這樣可以避開(kāi)fd_set的大小的限制

#define __FDSET_LONGS 8
typedef struct fd_set {
    unsigned long fds_bits [__FDSET_LONGS];
} fd_set;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • 1. 硬鏈接和軟連接區(qū)別 硬連接-------指通過(guò)索引節(jié)點(diǎn)來(lái)進(jìn)行連接。在Linux的文件系統(tǒng)中,保存在磁盤(pán)分區(qū)...
    杰倫哎呦哎呦閱讀 2,330評(píng)論 0 2
  • IO概念 Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)備的操作都可以看做對(duì)文件進(jìn)行操...
    消失er閱讀 1,914評(píng)論 0 5
  • 本文討論的背景是Linux環(huán)境下的network IO。 一、 概念說(shuō)明 在進(jìn)行解釋之前,首先要說(shuō)明幾個(gè)概念: 用...
    faunjoe閱讀 4,404評(píng)論 1 15
  • 必備的理論基礎(chǔ) 1.操作系統(tǒng)作用: 隱藏丑陋復(fù)雜的硬件接口,提供良好的抽象接口。 管理調(diào)度進(jìn)程,并將多個(gè)進(jìn)程對(duì)硬件...
    drfung閱讀 3,569評(píng)論 0 5
  • 本文摘抄自linux基礎(chǔ)編程 IO概念 Linux的內(nèi)核將所有外部設(shè)備都可以看做一個(gè)文件來(lái)操作。那么我們對(duì)與外部設(shè)...
    VD2012閱讀 1,025評(píng)論 0 2