一、回顧
在linux poll機制使用(一)寫了個實現poll機制的簡單例子。在驅動模塊中需要實現struct file_operations
的.poll
成員。在驅動模塊中xxx_poll函數
的的作用是將當前進程添加到等待隊列中;然后判斷事件是否發生,發生則返回POLLIN | POLLRDNORM
,否則返回0(可以看看上一章的例子);接下來分析一下 linux 內核中 poll 機制的實現。
二、poll機制分析
1、系統調用
當應用層調用poll函數時,linux發生系統調用(
系統調用入口CALL(sys_poll)
),程序從應用空間切換到內核空間,然后執行sys_poll
函數(sys_poll函數在fs\select.c
文件中)。sys_poll
函數的作用是 根據時間計算滴答頻率數,然后調用do_sys_poll
函數。代碼塊如下:
/* ufds:應用層傳遞過來的struct pollfd結構體數組
* nfds:應用層傳遞過來的struct pollfd結構體個數
* timeout_msecs:超時時間*/
asmlinkage long sys_poll(struct pollfd __user *ufds, unsigned int nfds,long timeout_msecs)
{
s64 timeout_jiffies;
if (timeout_msecs > 0) {
/* 大於0,根據時間計算滴答頻率數 */
timeout_jiffies = msecs_to_jiffies(timeout_msecs);
} else {
/* Infinite (< 0) or no (0) timeout */
timeout_jiffies = timeout_msecs;
}
/* 然后調用do_sys_poll函數*/
return do_sys_poll(ufds, nfds, &timeout_jiffies);
}
2、do_sys_poll函數
do_sys_poll
函數在fs\select.c
文件中;do_sys_poll
函數的主要作用初始化table(table->pt->qproc = __pollwait)
,然后初始化poll列表
(poll列表的作用是存放用戶空間的struct pollfd
)接着調用do_poll
函數。下面的代碼塊省略了部分源代碼。代碼塊如下:
int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds, s64 *timeout)
{
struct poll_wqueues table;
struct poll_list *head;
struct poll_list *walk;
.....//省略了部分源代碼
if (nfds > current->signal->rlim[RLIMIT_NOFILE].rlim_cur)
return -EINVAL;
/* 初始化 table */
/* table->pt->qproc = __pollwait
* __pollwait 主要將當前進程掛到等待隊列中,這里只是做初始化而已。
* 在驅動程序里調用的 poll_wait(file, &button_waitq, wait); 函數里面最終
* 調用的就是table->pt->qproc()函數,也就是調用__pollwait()函數。
*/
poll_initwait(&table);
.....//省略部分源代碼(省略的源代碼其實就是分配空間和初始化head列表)
/* do_poll函數的參數說明:
*
* nfds: 應用層傳遞過來的struct pollfd結構體個數
* head: poll列表頭,列表里面包含應用層傳進來的struct pollfd數組的信息
* table: table->pt->qproc指向__pollwait函數(主要將當前進程掛到等待隊列中)
* timeout: 等待超時
*/
/* 調用do_poll函數 */
fdcount = do_poll(nfds, head, &table, timeout);
.....//省略部分源代碼(這部分的源代碼是將revents(返回的事件) copy 到應用層)
return err;
}
3、do_poll函數
do_poll
函數在fs\select.c
文件中。do_poll
函數的作用主要是設置當前進程的任務狀態,然后遍歷poll列表、調用do_pollfd
函數,然后根據實際情況是否將當前進程休眠或者喚醒當前進程,代碼塊如下:
//參數說明
/*nfds: 應用層傳遞過來的struct pollfd結構體個數
*list:poll列表頭,列表里面包含應用層傳進來的struct pollfd數組的信息
*wait:wait->pt->qproc指向__pollwait函數(主要將當前進程掛到等待隊列中)
*timeout:等待超時時間
*/
static int do_poll(unsigned int nfds, struct poll_list *list,
struct poll_wqueues *wait, s64 *timeout)
{
int count = 0;
poll_table* pt = &wait->pt;
/* Optimise the no-wait case */
if (!(*timeout))
pt = NULL;
/* 執行poll的時候有一個大循環 */
for (;;) {
struct poll_list *walk;
long __timeout;
/* 設置當前的任務狀態為可中斷休眠 */
set_current_state(TASK_INTERRUPTIBLE);
/* 遍歷poll列表 */
for (walk = list; walk != NULL; walk = walk->next) {
struct pollfd * pfd, * pfd_end;
/* 獲取頭部的地址 */
pfd = walk->entries;
/* 獲取尾部指針 */
pfd_end = pfd + walk->len;
/* 遍歷struct pollfd結構體數組 */
for (; pfd != pfd_end; pfd++) {
/*------------------------------------------------------------------*/
/* do_pollfd其實就是調用開發者所寫的xxx_poll函數了(里面調用的的是struct file_operations .poll函數)
* do_pollfd函數下面的代碼塊分析
*/
/* 執行完do_pollfd后,這個進程就被掛到等待隊列里面了。
* 這里僅僅是掛到等待隊列而已,進程的休眠是在下面
* 執行 schedule_timeout 函數的時候休眠的
*/
if (do_pollfd(pfd, pt)) {
/* 如果執行驅動程序的poll返回的是非0值 則count++ */
/* 表名設備有數據要返回給應用程序 */
count++;
pt = NULL;
}
/*------------------------------------------------------------------*/
}
}
pt = NULL;
/* 結束大循環的條件有三個:
* count: 表示設備有數據返回給應用程序
* timeout: 等待超時時間到
* signal_pending(current): 當前進程接收到信號
*/
if (count || !*timeout || signal_pending(current))
break;
/* 發生錯誤退出 */
count = wait->error;
if (count)
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;
}
/* 執行到這里之后 timeout 的值已經為零,下一次循環就會超時返回 */
/* 這里讓當前進程休眠一會
* 喚醒進程的處理休眠的時間到之外,還可以由驅動程序喚醒
*/
__timeout = schedule_timeout(__timeout);
if (*timeout >= 0)
*timeout += __timeout;
}
/* 將當前進程設置為就緒狀態,等待CPU調度 */
__set_current_state(TASK_RUNNING);
return count;
}
4、do_pollfd
do_pollfd
函數在fs\select.c
文件中,函數的作用就是,根據文件描述符找到struct file
結構體,然后調用驅動程序的poll函數
//參數說明
/* pollfd: struct pollfd結構體
* pwait:pwait->qproc指向__pollwait函數(主要將當前進程掛到等待隊列中)
*/
static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
unsigned int mask;
int fd;
mask = 0;
fd = pollfd->fd;
if (fd >= 0) {
int fput_needed;
struct file * file;
/* 根據文件描述符,獲取file結構
* file結構是每打開一個文件就會有一個file
*/
file = fget_light(fd, &fput_needed);
mask = POLLNVAL;
if (file != NULL) {
mask = DEFAULT_POLLMASK;
if (file->f_op && file->f_op->poll)
/* 調用驅動里面的poll函數,這個函數是驅動開發者添加的poll函數 */
/* 如果驅動有數據讓應用程序讀的話,就返回非0, 否側返回0 */
mask = file->f_op->poll(file, pwait);
/* Mask out unneeded events. */
/* 根據應用層傳遞的events,來屏蔽不需要的事件 */
mask &= pollfd->events | POLLERR | POLLHUP;
fput_light(file, fput_needed);
}
}
pollfd->revents = mask;
/* 設備有數據可讀則返回非0 */
/* 否則返回0 */
return mask;
}
三、總結
- poll > sys_poll > do_sys_poll > poll_initwait,poll_initwait函數注冊一下回調函數__pollwait,它就是我們的驅動程序執行poll_wait時,真正被調用的函數。
- 接下來執行file->f_op->poll,即我們驅動程序里自己實現的poll函數
它會調用poll_wait把自己掛入某個隊列,這個隊列也是我們的驅動自己定義的;
它還判斷一下設備是否就緒- 如果設備未就緒,do_sys_poll里會讓進程休眠一定時間
- 進程被喚醒的條件有兩個 :1、“一定時間”到了 ;2、二是被驅動程序喚醒。驅動程序發現條件就緒時,就把“某個隊列”上掛着的進程喚醒,這個隊列,就是前面通過poll_wait把本進程掛過去的隊列。
- 如果驅動程序沒有去喚醒進程,那么chedule_timeout(__timeou)超時后,會重復2、3動作,直到應用程序的poll調用傳入的時間到達。