Linux : select()詳解 和 實現原理【轉】

weixin_34293059發表於2017-07-19

轉自:http://blog.csdn.net/huntinux/article/details/39289317

原文:http://blog.csdn.net/boboiask/article/details/4055655


Linux—select詳解

 

select系統呼叫時用來讓我們的程式監視多個檔案控制程式碼的狀態變化的。程式會停在select這裡等待,直到被監視的檔案控制程式碼有一個或多個發生了狀態改變。

關於檔案控制程式碼,其實就是一個整數,通過socket函式的宣告就明白了:

int socket(int domain, int type, int protocol);

我們最熟悉的控制程式碼是0、1、2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。0、1、2是整數表示的,對應的FILE *結構的表示就是stdin、stdout、stderr。

繼續上面的select,就是用來監視某個或某些控制程式碼的狀態變化的。select函式原型如下:

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

函式的最後一個引數timeout是一個超時時間值。其型別是struct timeval *,即一個struct timeval結構的變數的指標,所以我們在程式裡要宣告一個struct timeval tv;然後把變數tv的地址&tv傳遞給select函式。struct timeval結構如下:

struct timeval

{

       long tv_sec;  //seconds

       long tv_usec; //microseconds

};

第2、3、4三個引數是一樣的型別;fd_set *,即我們在程式裡要申請幾個fd_set型別的變數,比如rdfds,wtfds,exfds,然後把這個變數的地址&rdfds,&wtfds,&exfds傳遞給select函式。這三個引數都是一個控制程式碼的集合,第一個rdfds是用來儲存這樣的控制程式碼的:當控制程式碼的狀態變成可讀時系統就告訴select函式返回,同理第二個函式是指向有控制程式碼狀態變成可寫時系統就會告訴select函式返回,同理第三個引數exfds是特殊情況,即控制程式碼上有特殊情況發生時系統會告訴select函式返回。特殊情況比如對方通過一個socket控制程式碼發來了緊急資料。如果我們程式裡只想檢測某個socket是否有資料可讀,我們可以這樣:

fd_set  rdfds;

struct timeval tv;

int ret;

FD_ZERO(&rdfds);

FD_SET(socket, &rdfds);

tv.tv_sec = 1;

tv.tv_uses = 500;

ret = select (socket + 1, %rdfds, NULL, NULL, &tv);

if(ret < 0) perror (“select”);

else if (ret = = 0) printf(“time out”);

else {

       printf(“ret = %d/n”,ret);

       if(FD_ISSET(socket, &rdfds)){

    /* 讀取socket控制程式碼裡的資料 */

recv( );

}

}

注意select函式的第一個引數,是所有加入集合的控制程式碼值的最大那個那個值還要加1.比如我們建立了3個控制程式碼;

int sa, sb, sc;

sa = socket(……);

connect (sa,….);

 

sb = socket(….);

connect (sb,…);

 

sc = socket(….);

connect(sc,…);

 

FD_SET(sa, &rdfds);

FD_SET(sb, &rdfds);

FD_SET(sc, &rdfds);

 

在使用select函式之前,一定要找到3個控制程式碼中的最大值是哪個,我們一般定義一個變數來儲存最大值,取得最大socket值如下:

int maxfd = 0;

if(sa > maxfd) maxfd = sa;

if(sb > maxfd) maxfd = sb;

if(sc > maxfd) maxfd = sc;

 

然後呼叫select函式:

ret = select (maxfd+1, &rdfds, NULL, NULL,&tv);

 

同樣的道理,如果我們是檢測使用者是否按了鍵盤進行輸入,我們就應該把標準輸入0這個控制程式碼放到select裡來檢測,如下:

FD_ZERO(&rdfds);

FD_SET(0, &rdfds);

tv.tv_sec = 1;

tv.tv_usec = 0;

ret = select (1, &rdfds,NULL,NULL,&tv);

if(ret < 0) perror(“select”);

else if (ret = = 0) printf (“time out/n”);

else{

       scanf(“%s”,buf);

}


===================select 實現原理=======================

參考:http://linux.chinaunix.NET/techdoc/net/2009/05/03/1109887.shtml

Select函式實現原理分析(轉載)

select需要驅動程式的支援,驅動程式實現fops內的poll函式。select通過每個裝置檔案對應的poll函式提供的資訊判斷當前是否有資源可用(如可讀或寫),如果有的話則返回可用資源的檔案描述符個數,沒有的話則睡眠,等待有資源變為可用時再被喚醒繼續執行。

下面我們分兩個過程來分析select:

1. select的睡眠過程

支援阻塞操作的裝置驅動通常會實現一組自身的等待佇列如讀/寫等待佇列用於支援上層(使用者層)所需的BLOCK或NONBLOCK操作。當應用程式通過裝置驅動訪問該裝置時(預設為BLOCK操作),若該裝置當前沒有資料可讀或寫,則將該使用者程式插入到該裝置驅動對應的讀/寫等待佇列讓其睡眠一段時間,等到有資料可讀/寫時再將該程式喚醒。

select就是巧妙的利用等待佇列機制讓使用者程式適當在沒有資源可讀/寫時睡眠,有資源可讀/寫時喚醒。下面我們看看select睡眠的詳細過程。

select會迴圈遍歷它所監測的fd_set(一組檔案描述符(fd)的集合)內的所有檔案描述符對應的驅動程式的poll函式。驅動程式提供的poll函式首先會將呼叫select的使用者程式插入到該裝置驅動對應資源的等待佇列(如讀/寫等待佇列),然後返回一個bitmask告訴select當前資源哪些可用。當select迴圈遍歷完所有fd_set內指定的檔案描述符對應的poll函式後,如果沒有一個資源可用(即沒有一個檔案可供操作),則select讓該程式睡眠,一直等到有資源可用為止,程式被喚醒(或者timeout)繼續往下執行。

下面分析一下程式碼是如何實現的。
select的呼叫path如下:sys_select -> core_sys_select -> do_select
其中最重要的函式是do_select, 最主要的工作是在這裡, 前面兩個函式主要做一些準備工作。do_select定義如下:
int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
       struct poll_wqueues table;
       poll_table *wait;
       int retval, i;

       rcu_read_lock();
       retval = max_select_fd(n, fds);
       rcu_read_unlock();

       if (retval
              return retval;
       n = retval;

       poll_initwait(&table);
       wait = &table.pt;
       if (!*timeout)
              wait = NULL;
       retval = 0;      //retval用於儲存已經準備好的描述符數,初始為0
       for (;;) {
              unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
              long __timeout;

              set_current_state(TASK_INTERRUPTIBLE);      //將當前程式狀態改為TASK_INTERRUPTIBLE

              inp = fds->in; outp = fds->out; exp = fds->ex;
              rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

              for (i = 0; i 遍歷每個描述符
                     unsigned long in, out, ex, all_bits, bit = 1, mask, j;
                     unsigned long res_in = 0, res_out = 0, res_ex = 0;
                     const struct file_operations *f_op = NULL;
                     struct file *file = NULL;

                     in = *inp++; out = *outp++; ex = *exp++;
                     all_bits = in | out | ex;
                     if (all_bits == 0) {
                            i += __NFDBITS;  // //如果這個字沒有待查詢的描述符, 跳過這個長字(32位)
                            continue;
                     }

                     for (j = 0; j 遍歷每個長字裡的每個位
                            int fput_needed;
                            if (i >= n)
                                   break;
                            if (!(bit & all_bits))
                                   continue;
                            file = fget_light(i, &fput_needed);
                            if (file) {
                                   f_op = file->f_op;
                                   MARK(fs_select, "%d %lld",
                                                 i, (long long)*timeout);
                                   mask = DEFAULT_POLLMASK;
                                   if (f_op && f_op->poll)
/* 在這裡迴圈呼叫所監測的fd_set內的所有檔案描述符對應的驅動程式的poll函式 */
                                          mask = (*f_op->poll)(file, retval ? NULL : wait);
                                   fput_light(file, fput_needed);
                                   if ((mask & POLLIN_SET) && (in & bit)) {
                                          res_in |= bit; //如果是這個描述符可讀, 將這個位置位
                                          retval++; //返回描述符個數加1
                                   }
                                   if ((mask & POLLOUT_SET) && (out & bit)) {
                                          res_out |= bit;
                                          retval++;
                                   }
                                   if ((mask & POLLEX_SET) && (ex & bit)) {
                                          res_ex |= bit;
                                          retval++;
                                   }
                            }
                            cond_resched();
                     }
//返回結果
                     if (res_in)
                            *rinp = res_in;
                     if (res_out)
                            *routp = res_out;
                     if (res_ex)
                            *rexp = res_ex;
              }
              wait = NULL;
/* 到這裡遍歷結束。retval儲存了檢測到的可操作的檔案描述符的個數。如果有檔案可操作,則跳出for(;;)迴圈,直接返回。若沒有檔案可操作且timeout時間未到同時沒有收到signal,則執行schedule_timeout睡眠。睡眠時間長短由__timeout決定,一直等到該程式被喚醒。
那該程式是如何被喚醒的?被誰喚醒的呢?
我們看下面的select喚醒過程*/
              if (retval || !*timeout || signal_pending(current))
                     break;
              if(table.error) {
                     retval = table.error;
                     break;
              }

              if (*timeout
                     /* Wait indefinitely */
                     __timeout = MAX_SCHEDULE_TIMEOUT;
              } else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
                     /* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
                     __timeout = MAX_SCHEDULE_TIMEOUT - 1;
                     *timeout -= __timeout;
              } else {
                     __timeout = *timeout;
                     *timeout = 0;
              }
              __timeout = schedule_timeout(__timeout);
              if (*timeout >= 0)
                     *timeout += __timeout;
       }
       __set_current_state(TASK_RUNNING);

       poll_freewait(&table);

       return retval;
}

2.  select的喚醒過程
前面介紹了select會迴圈遍歷它所監測的fd_set內的所有檔案描述符對應的驅動程式的poll函式。驅動程式提供的poll函式首先會將呼叫select的使用者程式插入到該裝置驅動對應資源的等待佇列(如讀/寫等待佇列),然後返回一個bitmask告訴select當前資源哪些可用。
一個典型的驅動程式poll函式實現如下:
(摘自《Linux Device Drivers – ThirdEdition》Page 165)
static unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
    struct scull_pipe *dev = filp->private_data;
    unsigned int mask = 0;
    /*
     * The buffer is circular; it is considered full
     * if "wp" is right behind "rp" and empty if the
     * two are equal.
     */
    down(&dev->sem);
    poll_wait(filp, &dev->inq,  wait);
    poll_wait(filp, &dev->outq, wait);
    if (dev->rp != dev->wp)
        mask |= POLLIN | POLLRDNORM;    /* readable */
    if (spacefree(dev))
        mask |= POLLOUT | POLLWRNORM;   /* writable */
    up(&dev->sem);
    return mask;
}
將使用者程式插入驅動的等待佇列是通過poll_wait做的。
Poll_wait定義如下:
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
       if (p && wait_address)
              p->qproc(filp, wait_address, p);
}
這裡的p->qproc在do_select內poll_initwait(&table)被初始化為__pollwait,如下:
void poll_initwait(struct poll_wqueues *pwq)
{
       init_poll_funcptr(&pwq->pt, __pollwait);
       pwq->error = 0;
       pwq->table = NULL;
       pwq->inline_index = 0;
}
__pollwait定義如下:
/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address,
                            poll_table *p)
{
       struct poll_table_entry *entry = poll_get_entry(p);
       if (!entry)
              return;
       get_file(filp);
       entry->filp = filp;
       entry->wait_address = wait_address;
       init_waitqueue_entry(&entry->wait, current);
       add_wait_queue(wait_address,&entry->wait);
}
通過init_waitqueue_entry初始化一個等待佇列項,這個等待佇列項關聯的程式即當前呼叫select的程式。然後將這個等待佇列項插入等待佇列wait_address。Wait_address即在驅動poll函式內呼叫poll_wait(filp, &dev->inq,  wait);時傳入的該驅動的&dev->inq或者&dev->outq等待佇列。

注: 關於等待佇列的工作原理可以參考下面這篇文件:
http://blog.chinaunix.net/u2/60011/showart_1334657.html

到這裡我們明白了select如何將當前程式插入所有所監測的fd_set關聯的驅動內的等待佇列,那程式究竟是何時讓出CPU進入睡眠狀態的呢?
進入睡眠狀態是在do_select內呼叫schedule_timeout(__timeout)實現的。當select遍歷完fd_set內的所有裝置檔案,發現沒有檔案可操作時(即retval=0),則呼叫schedule_timeout(__timeout)進入睡眠狀態。

喚醒該程式的過程通常是在所監測檔案的裝置驅動內實現的,驅動程式維護了針對自身資源讀寫的等待佇列。當裝置驅動發現自身資源變為可讀寫並且有程式睡眠在該資源的等待佇列上時,就會喚醒這個資源等待佇列上的程式。
舉個例子,比如核心的8250 uart driver:
Uart是使用的Tty層維護的兩個等待佇列, 分別對應於讀和寫: (uart是tty裝置的一種)
struct tty_struct {
       ……
       wait_queue_head_t write_wait;
       wait_queue_head_t read_wait;
       ……
}
當uart裝置接收到資料,會呼叫tty_flip_buffer_push(tty);將收到的資料push到tty層的buffer。
然後檢視是否有程式睡眠的讀等待佇列上,如果有則喚醒該等待會列。
過程如下:
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))       //若有程式阻塞在read_wait上則喚醒
wake_up_interruptible(&tty->read_wait);

到這裡明白了select程式被喚醒的過程。由於該程式是阻塞在所有監測的檔案對應的裝置等待佇列上的,因此在timeout時間內,只要任意個裝置變為可操作,都會立即喚醒該程式,從而繼續往下執行。這就實現了select的當有一個檔案描述符可操作時就立即喚醒執行的基本原理。

Referece:
1.       Linux Device Drivers – ThirdEdition
2.       核心等待佇列機制原理分析
http://blog.chinaunix.Net/u2/60011/showart_1334657.html
3.       Kernel code : Linux 2.6.18_pro500 - Montavista



本文來自ChinaUnix部落格,如果檢視原文請點:http://blog.chinaunix.net/u3/94284/showart_1917293.html


==========另外參考LDD chp6 poll和select==============

使用者空間程式在驅動程式關聯的檔案描述符上執行poll、select、epoll系統呼叫時,驅動程式中 fops裡面的 poll方法被呼叫。

相關文章