epoll 原理(一)實現基礎

本序列涉及的 Linux 源碼都是基於 linux-4.14.143 。

1. 文件抽象 與 poll 操作

1.1 文件抽象

在 Linux 內核裡,文件是一個抽象,設備是個文件,網絡套接字也是個文件。

文件抽象必須支持的能力定義在 file_operations 結構體裡。

在 Linux 裡,一個打開的文件對應一個文件描述符 file descriptor/FD,FD 其實是一個整數,內核把進程打開的文件維護在一個數組裡,FD 對應的是數組的下標。

文件抽象的能力定義:

// 源碼位置:include/linux/fs.h
struct file_operations {
struct module *owner;
loff_t (*llseek) (struct file *, loff_t, int);
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
int (*iterate) (struct file *, struct dir_context *);
int (*iterate_shared) (struct file *, struct dir_context *);
// 對於 select/poll/epoll 最重要的實現基礎
// 非阻塞的輪詢文件狀態的函數
unsigned int (*poll) (struct file *, struct poll_table_struct *);
// 省略其他函數指針
} __randomize_layout;
// 源碼位置:include/linux/poll.h
typedef struct poll_table_struct {
// 文件的 file_operations.poll 實現一定會調用的隊列處理函數

poll_queue_proc _qproc;
// poll 操作敢興趣的事件
unsigned long _key;
} poll_table;
// poll 隊列處理函數
typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

1.2 文件 poll 操作

poll 函數的原型:

unsigned int (*poll) (struct file *, poll_table *);
/**
* 如果 poll_table 有回調函數,則回調它。
*
* @filp 要監聽的目標文件
* @wait_address 要監聽事件的等待隊列頭
* @p select/poll/epoll 調用裡傳入裡的等待節點
*/
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
if (p && p->_qproc && wait_address)
p->_qproc(filp, wait_address, p);
}

文件抽象 poll 函數的具體實現必須完成兩件事(這兩點算是規範了):

1. 在 poll 函數敢興趣的等待隊列上調用 poll_wait 函數,以接收到喚醒;具體的實現必須把 poll_table 類型的參數作為透明對象來使用,不需要知道它的具體結構。

2. 返回比特掩碼,表示當前可立即執行而不會阻塞的操作。

下面是某個驅動的 poll 實現示例,來自:https://www.oreilly.com/library/view/linux-device-drivers/0596000081/ch05s03.html:

unsigned int scull_p_poll(struct file *filp, poll_table *wait)
{
Scull_Pipe *dev = filp->private_data;
unsigned int mask = 0;
/*
* The buffer is circular; it is considered full
* if "wp" is right behind "rp". "left" is 0 if the
* buffer is empty, and it is "1" if it is completely full.
*/
int left = (dev->rp + dev->buffersize - dev->wp) % dev->buffersize;
// 在不同的等待隊列上調用 poll_wait 函數
poll_wait(filp, &dev->inq, wait);
poll_wait(filp, &dev->outq, wait);
/* readable */
if (dev->rp != dev->wp) mask |= POLLIN | POLLRDNORM;
/* writable */
if (left != 1) mask |= POLLOUT | POLLWRNORM;
return mask;
}

2. poll 的等待與喚醒

poll 函數接收的 poll_table 只有一個隊列處理函數 _qproc 和感興趣的事件屬性 _key。

文件抽象的具體實現在構建時會初始化一個或多個 wait_queue_head_t 類型的事件等待隊列 。

poll 等待的過程:

  • poll 函數被調用時,其實現肯定會調用 poll_wait,進而調用到 _qproc 函數。
  • _qproc 負責構建包含 wait_queue_entry 結構體的等待節點(比如 select 操作是 poll_table_entry 結構體),並把 wait_queue_entry 添加到要監聽文件的等待隊列 wait_address 上(wait_queue_entry 結構體指定了事件發生時的喚醒函數,比如 select 操作裡指定的是 pollwake 函數)。
  • poll 函數返回文件當前可立即執行而不阻塞的操作表示碼。

事件發生時的喚醒過程:

  • 當事件發生時,文件的具體實現遍歷等待隊列,調用其喚醒函數,由喚醒函數進行具體的喚醒操作,喚醒函數的類型為 typedef int (*wait_queue_func_t)(struct wait_queue_entry *wq_entry, unsigned mode, int flags, void *key)。
  • 具體的喚醒函數實現根據 wait_queue_entry 找到 _qproc 函數里構建的等待節點,利用其數據判斷是否需要喚醒,是則喚醒等待進程。

一個小困惑:

喚醒函數是如何根據 wait_queue_entry 找到真實的等待節點呢??

這是藉助內核的一個宏 container_of 實現的,container_of 是指針的一個靈活應用,作用是通過結構體變量中某個成員的首地址進而獲得整個結構體變量的首地址。


分享到:


相關文章: