select 和 poll 是 Linux 提供的經典 I/O 多路復用機制,核心邏輯在於「驅動註冊等待隊列 → 核心輪詢檢查就緒狀態 → 無就緒則睡眠 → 就緒/超時/信號喚醒」。本文深度剖析 do_select 核心流程、驅動 poll 接口契約與等待隊列交互機制。
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
fd_set 位圖操作宏:FD_ZERO/SET/CLR/ISSETnfds:最大文件描述符值 + 1timeout:NULL 永久等待,0 非阻塞,>0 微秒級超時
sys_select
→ core_sys_select // 複製用戶 fd_set,調用 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); // 計算實際最大 fd
rcu_read_unlock();
if (retval < 0) return retval;
n = retval;
poll_initwait(&table); // 初始化等待隊列表
wait = &table.pt;
if (!*timeout) wait = NULL; // 非阻塞模式不掛載等待隊列
retval = 0; // 就緒描述符計數
for (;;) {
// 1. 設置當前進程可中斷睡眠狀態
set_current_state(TASK_INTERRUPTIBLE);
// 2. 遍歷所有 fd_set 位圖
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; continue; } // 跳過空字
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
if (i >= n) break;
if (!(bit & all_bits)) continue;
int fput_needed;
file = fget_light(i, &fput_needed); // 獲取 file 結構
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
// 關鍵:調用驅動 poll 函數
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
// 根據返回掩碼設置結果位圖
if ((mask & POLLIN_SET) && (in & bit)) { res_in |= bit; retval++; }
if ((mask & POLLOUT_SET) && (out & bit)) { res_out |= bit; retval++; }
if ((mask & POLLEX_SET) && (ex & bit)) { res_ex |= bit; retval++; }
}
cond_resched(); // 避免長時間佔用 CPU
}
// 寫回結果位圖
if (res_in) *rinp = res_in;
if (res_out) *routp = res_out;
if (res_ex) *rexp = res_ex;
}
wait = NULL; // 後續循環不再掛載等待隊列
// 3. 判斷退出條件
if (retval || !*timeout || signal_pending(current))
break; // 有就緒、非阻塞、收到信號 → 退出
if (table.error) { retval = table.error; break; }
// 4. 計算睡眠時間並調度
if (*timeout < 0) __timeout = MAX_SCHEDULE_TIMEOUT;
else if (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
__timeout = MAX_SCHEDULE_TIMEOUT - 1;
*timeout -= __timeout;
} else {
__timeout = *timeout;
*timeout = 0;
}
schedule_timeout(__timeout); // 睡眠等待喚醒或超時
}
__set_current_state(TASK_RUNNING);
return retval;
}
for(;;) 處理睡眠/喚醒週期,內層遍歷所有 fdretval ? NULL : wait 確保只有首次輪詢時將進程加入驅動等待隊列retval 累計就緒描述符數,結果寫回 res_in/out/exschedule_timeout 以 jiffies 為單位,大超時分片處理
// include/linux/poll.h
typedef struct poll_table_struct {
wait_queue_head_t *wait; // 當前掛載的等待隊列頭
wait_queue_t wait; // 等待隊列項
wait_queue_func_t func; // 喚醒回調
} poll_table;
unsigned int (*poll) (struct file *filp, poll_table *wait);
unsigned int mydrv_poll(struct file *filp, poll_table *wait)
{
struct mydev *dev = filp->private_data;
unsigned int mask = 0;
// 1. 將當前進程加入驅動等待隊列
poll_wait(filp, &dev->read_queue, wait); // 可讀隊列
poll_wait(filp, &dev->write_queue, wait); // 可寫隊列
// 2. 檢查硬件就緒狀態
if (data_available(dev))
mask |= POLLIN | POLLRDNORM; // 可讀
if (space_available(dev))
mask |= POLLOUT | POLLWRNORM; // 可寫
return mask;
}
void poll_wait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
if (!p || !wait_address) return;
// 初始化等待項,綁定當前進程,回調為 pollwake
init_waitqueue_entry(&p->wait, current);
p->wait.func = pollwake;
p->wait_address = wait_address;
// 加入驅動等待隊列
add_wait_queue(wait_address, &p->wait);
}
static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
struct poll_table_struct *p = container_of(wait, struct poll_table_struct, wait);
// 關鍵:觸發 select/poll 進程重新調度
if (key && !(key & (POLLIN | POLLOUT | POLLEX))) return 0;
return __pollwake(wait, mode, sync, key);
}
當驅動調用 wake_up_interruptible(&dev->read_queue) 時,pollwake 會被調用,最終導致 schedule_timeout 返回,do_select 重新進入輪詢循環。
| 掩碼 | 值 | 含義 |
|---|---|---|
POLLIN | 0x001 | 有數據可讀 |
POLLRDNORM | 0x040 | 正常數據可讀 |
POLLRDBAND | 0x080 | 優先數據可讀 |
POLLPRI | 0x002 | 高優先級數據可讀 |
POLLOUT | 0x004 | 可寫入數據 |
POLLWRNORM | 0x100 | 正常寫入 |
POLLWRBAND | 0x200 | 優先寫入 |
POLLERR | 0x008 | 錯誤狀態 |
POLLHUP | 0x010 | 掛起/斷開 |
POLLNVAL | 0x020 | 無效請求 |
| 特性 | select | poll | epoll |
|---|---|---|---|
| 最大 fd 限制 | FD_SETSIZE (1024) | 無限制 | 無限制 |
| 數據結構 | 位圖 fd_set | 陣列 pollfd | 紅黑樹 + 就緒鏈表 |
| 掃描方式 | 全掃描 O(n) | 全掃描 O(n) | 回調通知 O(1) |
| 用戶/核心拷貝 | 每次全拷貝 | 每次全拷貝 | 共享內存/事件通知 |
| 水平/邊沿觸發 | 水平 | 水平 | 支援兩者 |
select/poll 核心邏輯在 do_select:遍歷 fd → 調用驅動 poll → 驅動通過 poll_wait 掛載等待隊列 → 無就緒則 schedule_timeout 睡眠 → 驅動事件觸發 wake_up → pollwake 喚醒進程 → 重新輪詢。理解這一「掛載-睡眠-喚醒-重試」循環,是掌握 I/O 多路復用本質與驅動 poll 實現的關鍵。