Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學
您现在的位置: Windows教程網 >> Windows教程 >  >> Linux 系統教學 >> Linux 教學

Linux select/poll 系統調用實現原理:驅動 poll 接口與核心輪詢邏輯深度解析

編輯:Linux 教學

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/ISSET
  • nfds:最大文件描述符值 + 1
  • timeout:NULL 永久等待,0 非阻塞,>0 微秒級超時

系統調用調用鏈


sys_select
  → core_sys_select      // 複製用戶 fd_set,調用 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);  // 計算實際最大 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;
}

關鍵設計點

  1. 雙層循環:外層 for(;;) 處理睡眠/喚醒週期,內層遍歷所有 fd
  2. 首輪掛載等待隊列:retval ? NULL : wait 確保只有首次輪詢時將進程加入驅動等待隊列
  3. 結果累積:retval 累計就緒描述符數,結果寫回 res_in/out/ex
  4. 超時處理:schedule_timeout 以 jiffies 為單位,大超時分片處理

驅動 poll 接口契約


// 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;
}

poll_wait 核心邏輯


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);
}

喚醒回調:pollwake


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 重新進入輪詢循環。

關鍵掩碼常量

掩碼值含義
POLLIN0x001有數據可讀
POLLRDNORM0x040正常數據可讀
POLLRDBAND0x080優先數據可讀
POLLPRI0x002高優先級數據可讀
POLLOUT0x004可寫入數據
POLLWRNORM0x100正常寫入
POLLWRBAND0x200優先寫入
POLLERR0x008錯誤狀態
POLLHUP0x010掛起/斷開
POLLNVAL0x020無效請求

select vs poll vs epoll

特性selectpollepoll
最大 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 實現的關鍵。

Copyright © Windows教程網 All Rights Reserved