select需要驅動程序的支持,驅動程序實現fops內的poll函數。select通過每個設備文件對應的poll函數提供的信息判斷當前是否有資源可用(如可讀或寫),如果有的話則返回可用資源的文件描述符個數,沒有的話則睡眠,等待有資源變為可用時再被喚醒繼續執行。
下面我們分兩個過程來分析select:
1. select的睡眠過程
支持阻塞操作的設備驅動通常會實現一組自身的等待隊列如讀/寫等待隊列用於支持上層(用戶層)所需的BLOCK或NONBLOCK操作。當應用程序通過設備驅動訪問該設備時(默認為BLOCK操作),若該設備當前沒有數據可讀或寫,則將該用戶進程插入到該設備驅動對應的讀/寫等待隊列讓其睡眠一段時間,等到有數據可讀/寫時再將該進程喚醒。
select就是巧妙的利用等待隊列機制讓用戶進程適當在沒有資源可讀/寫時睡眠,有資源可讀/寫時喚醒。下面我們看看select睡眠的詳細過程。
select會循環遍歷它所監測的fd_set內的所有文件描述符對應的驅動程序的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 < 0)
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 < n; ++rinp, ++routp, ++rexp) { //遍歷每個描述符
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 < __NFDBITS; ++j, ++i, bit <<= 1) { //遍歷每個長字裡的每個位
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 < 0) {
/* 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;