韋東山:Linux驅動基石之POLL機制

1.適用場景

在前面引入中斷時,我們曾經舉過一個例子:
在這裏插入圖片描述

媽媽怎麼知道臥室裏小孩醒了?
① 時不時進房間看一下:查詢方式
簡單,但是累
② 進去房間陪小孩一起睡覺,小孩醒了會吵醒她:休眠-喚醒
不累,但是媽媽幹不了活了
③ 媽媽要幹很多活,但是可以陪小孩睡一會,定個鬧鐘:poll方式
要浪費點時間,但是可以繼續幹活。
媽媽要麼是被小孩吵醒,要麼是被鬧鐘吵醒。
④ 媽媽在客廳幹活,小孩醒了他會自己走出房門告訴媽媽:異步通知
媽媽、小孩互不耽誤

使用休眠-喚醒的方式等待某個事件發生時,有一個缺點:等待的時間可能很久。我們可以加上一個超時時間,這時就可以使用poll機制。
① APP不知道驅動程序中是否有數據,可以先調用poll函數查詢一下,poll函數可以傳入超時時間;
② APP進入內核態,調用到驅動程序的poll函數,如果有數據的話立刻返回;
③ 如果發現沒有數據時就休眠一段時間
④ 當有數據時,比如當按下按鍵時,驅動程序的中斷服務程序被調用,它會記錄數據、喚醒APP;
⑤ 當超時時間到了之後,內核也會喚醒APP;
⑥ APP根據poll函數的返回值就可以知道是否有數據,如果有數據就調用read得到數據

2.使用流程

媽媽進入房間時,會先看小孩醒沒醒,鬧鐘響之後走出房間之前又會再看小孩醒沒醒。
注意:看了2次小孩!
POLL機制也是類似的,流程如下:
在這裏插入圖片描述
函數執行流程如上圖①~⑧所示,重點從③開始看。假設一開始無按鍵數據:
③ APP調用poll之後,進入內核態;
④ 導致驅動程序的drv_poll被調用:
注意,drv_poll要把自己這個線程掛入等待隊列wq中;假設不放入隊列裏,那以後發生中斷時,中斷服務程序去哪裏找到你嘛?
drv_poll還會判斷一下:有沒有數據啊?返回這個狀態。
⑤ 假設當前沒有數據,則休眠一會;
⑥ 在休眠過程中,按下了按鍵,發生了中斷:
在中斷服務程序裏記錄了按鍵值,並且從wq中把線程喚醒了。
⑦ 線程從休眠中被喚醒,繼續執行for循環,再次調用drv_poll:
drv_poll返回數據狀態
⑧ 哦,你有數據,那從內核態返回到應用態吧
⑨ APP調用read函數讀數據
如果一直沒有數據,調用流程也是類似的,重點從③開始看,如下:
③ APP調用poll之後,進入內核態;
④ 導致驅動程序的drv_poll被調用:
注意,drv_poll要把自己這個線程掛入等待隊列wq中;假設不放入隊列裏,那以後發生中斷時,中斷服務程序去哪裏找到你嘛?
drv_poll還會判斷一下:有沒有數據啊?返回這個狀態。
⑤ 假設當前沒有數據,則休眠一會;
⑥ 在休眠過程中,一直沒有按下了按鍵,超時時間到:內核把這個線程喚醒;
⑦ 線程從休眠中被喚醒,繼續執行for循環,再次調用drv_poll:
drv_poll返回數據狀態
⑧ 哦,你還是沒有數據,但是超時時間到了,那從內核態返回到應用態吧
⑨ APP不能調用read函數讀數據

注意幾點:
① drv_poll要把線程掛入隊列wq,但是並不是在drv_poll中進入休眠,而是在調用drv_poll之後休眠
② drv_poll要返回數據狀態
③ APP調用一次poll,有可能會導致drv_poll被調用2次
④ 線程被喚醒的原因有2:中斷髮生了去隊列wq中把它喚醒,超時時間到了內核把它喚醒
⑤ APP要判斷poll返回的原因:有數據,還是超時。有數據時再去調用read函數。

3. 驅動編程

使用poll機制時,驅動程序的核心就是提供對應的drv_poll函數。
在drv_poll函數中要做2件事:
① 把當前線程掛入隊列wq:poll_wait
APP調用一次poll,可能導致drv_poll被調用2次,但是我們並不需要把當前線程掛入隊列2次。
可以使用內核的函數poll_wait把線程掛入隊列,如果線程已經在隊列裏了,它就不會再次掛入。
② 返回設備狀態:
APP調用poll函數時,有可能是查詢“有沒有數據可以讀”:POLLIN,也有可能是查詢“你有沒有空間給我寫數據”:POLLOUT。
所以drv_poll要返回自己的當前狀態:(POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
POLLRDNORM等同於POLLIN,爲了兼容某些APP把它們一起返回。
POLLWRNORM等同於POLLOUT ,爲了兼容某些APP把它們一起返回。

APP調用poll後,很有可能會休眠。對應的,在按鍵驅動的中斷服務程序中,也要有喚醒操作。
驅動程序中poll的代碼如下:

static unsigned int gpio_key_drv_poll(struct file *fp, poll_table * wait)
{
	printk("%s %s line %d\n", __FILE__, __FUNCTION__, __LINE__);
	poll_wait(fp, &gpio_key_wait, wait);
	return is_key_buf_empty() ? 0 : POLLIN | POLLRDNORM;
}

4 應用編程

注意:APP可以調用poll或select函數,這2個函數的作用是一樣的。
poll/select函數可以監測多個文件,可以監測多種事件:
事件類型 說明
POLLIN 有數據可讀
POLLRDNORM 等同於POLLIN
POLLRDBAND Priority band data can be read,有優先級較較高的“band data”可讀
Linux系統中很少使用這個事件
POLLPRI 高優先級數據可讀
POLLOUT 可以寫數據
POLLWRNORM 等同於POLLOUT
POLLWRBAND Priority data may be written
POLLERR 發生了錯誤
POLLHUP 掛起
POLLNVAL 無效的請求,一般是fd未open

事件類型 說明
POLLIN 有數據可讀
POLLRDNORM 等同於POLLIN
POLLRDBAND Priority band data can be read,有優先級較較高的“band data”可讀
Linux系統中很少使用這個事件
POLLPRI 高優先級數據可讀
POLLOUT 可以寫數據
POLLWRNORM 等同於POLLOUT
POLLWRBAND Priority data may be written
POLLERR 發生了錯誤
POLLHUP 掛起
POLLNVAL 無效的請求,一般是fd未open

在調用poll函數時,要指明:
① 你要監測哪一個文件:哪一個fd
② 你想監測這個文件的哪種事件:是POLLIN、還是POLLOUT
最後,在poll函數返回時,要判斷狀態。

應用程序代碼如下:

struct pollfd fds[1];
int timeout_ms = 5000;
int ret;

fds[0].fd = fd;
fds[0].events = POLLIN;

ret = poll(fds, 1, timeout_ms);
if ((ret == 1) && (fds[0].revents & POLLIN))
{
	read(fd, &val, 4);
	printf("get button : 0x%x\n", val);
}

5. POLL機制的內核代碼詳解

Linux APP系統調用,基本都可以在它的名字前加上“sys_”前綴,這就是它在內核中對應的函數。比如系統調用open、read、write、poll,與之對應的內核函數爲:sys_open、sys_read、sys_write、sys_poll。
對於系統調用poll或select,它們對應的內核函數都是sys_poll。分析sys_poll,即可理解poll機制。

5.1 sys_poll函數

sys_poll位於fs/select.c文件中,代碼如下:

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds,
		int, timeout_msecs)
{
	struct timespec64 end_time, *to = NULL;
	int ret;

	if (timeout_msecs >= 0) {
		to = &end_time;
		poll_select_set_timeout(to, timeout_msecs / MSEC_PER_SEC,
			NSEC_PER_MSEC * (timeout_msecs % MSEC_PER_SEC));
	}

	ret = do_sys_poll(ufds, nfds, to);
……

SYSCALL_DEFINE3是一個宏,它定義於include/linux/syscalls.h,展開後就有sys_poll函數。
sys_poll對超時參數稍作處理後,直接調用do_sys_poll。

5.2 do_sys_poll函數

do_sys_poll位於fs/select.c文件中,我們忽略其他代碼,只看關鍵部分:

int do_sys_poll(struct pollfd __user *ufds, unsigned int nfds,
		struct timespec64 *end_time)
{
……
	poll_initwait(&table);
	fdcount = do_poll(head, &table, end_time);
	poll_freewait(&table);
……
}

poll_initwait函數非常簡單,它初始化一個poll_wqueues變量table:

poll_initwait
   init_poll_funcptr(&pwq->pt, __pollwait);
       pt->qproc = qproc;

即table->pt->qproc = __pollwait,__pollwait將在驅動的poll函數裏用到。

do_poll函數纔是核心,繼續看代碼。

5.3 do_poll函數

do_poll函數位於fs/select.c文件中,這是POLL機制中最核心的代碼,貼圖如下:
在這裏插入圖片描述
① 從這裏開始,將會導致驅動程序的poll函數被第一次調用。
沿着②③④⑤,你可以看到:驅動程序裏的poll_wait會調用__pollwait函數把線程放入某個隊列。
當執行完①之後,在⑥或⑦處,pt->_qproc被設置爲NULL,所以第二次調用驅動程序的poll時,不會再次把線程放入某個隊列裏。
⑧ 如果驅動程序的poll返回有效值,則count非0,跳出循環;
⑨ 否則休眠一段時間;當休眠時間到,或是被中斷喚醒時,會再次循環、再次調用驅動程序的poll。

回顧APP的代碼,APP可以指定“想等待某些事件”,poll函數返回後,可以知道“發生了哪些事件”:
在這裏插入圖片描述
驅動程序裏怎麼體現呢?在上上一個圖中,看②位置處,細說如下:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章