1. 程式人生 > >(3.2)一個按鍵所能涉及的:poll機制

(3.2)一個按鍵所能涉及的:poll機制

概述

        啥子事poll機制呢?直白來說,當你在應用程式中使用poll,程式就會在給定時間內進入沉睡狀態等待某項資源,

只回在兩種情況下返回結束,1.設定的等待時間到了;2.所等待的資源到了。

應用價值:還是說我們的按鍵程式,上一小節,我們雖然使用了中斷的方式,極大的節省了資源,但我們的測試程式一旦啟動,就會在while中迴圈等待中斷,永遠永遠,程式一直在執行,這顯然不好。所以這節就是引入poll機制,讓測試程式等待指定時間,如果一直沒有中斷產生,就會自動結束程式。

實驗

驅動程式的實現並不複雜,Linux作為一個成熟的系統,已經為我們做好了體系,我們往往只要實現具體的功能就可以了。

poll作為應用中常用的功能,被定義在了file_operation結構體中(這個結構體是驅動的基礎啊)

1.在file_operation結構體中新增poll函式

static const struct file_operations jz2440_buttons_fops = {
	.owner 	 = 	THIS_MODULE,
	.read	 =	buttons_drv_read,  
	.open  	 = 	buttons_drv_open,
	.release =  buttons_drv_close,
	.poll	 =  buttons_drv_poll,     /* poll的具體函式 */
};

2. 既然我們採用poll的方式休眠,自然不用再read的時候休眠了

static ssize_t buttons_drv_read(struct file *file, char __user *buf, size_t size, loff_t *ppos)
{
	if (1 != size)
		return -EINVAL;

	/*如果沒有按鍵發生, 休眠
	* 如果有,返回
	*/
	//wait_event_interruptible(button_waitq, ev_press);   /* 註釋掉這行 */
	ev_press = 0;
	
	copy_to_user(buf, &key_val, 1);
	
	return 1;
}

3. 實現poll函式

unsigned int buttons_drv_poll (struct file *file, struct poll_table_struct *wait)
{
	unsigned int mask = 0;

	poll_wait(file, &button_waitq, wait);  //不會立即休眠,只是將程式掛在佇列上

	if(ev_press)
		mask |= POLLIN | POLLRDNORM;

	return  mask;
}

看來關鍵的就是poll_wait(file, &button_waitq, wait); 函式,傳進三個引數

    file:是監測的檔案

    button_waitq:是定義的睡眠佇列

    wait:追加到裝置驅動上的 poll_table結構體指標引數

4. 喚醒方式不變仍然在中斷處理函式中喚醒程式

ev_press = 1;
wake_up_interruptible(&button_waitq); /*喚醒*/

5. 測試程式

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <poll.h>

/*
*  poll 機制
*/
int main(int argc, char **argv)
{
	int fd;
	char key_val;
	struct pollfd fds[1];
	int ret;
	
	fd = open("/dev/buttons", O_RDWR);
	if (fd < 0){
		printf("can't open dev nod !\n");
		return -1;
	}
		
	fds[0].fd 	  = fd;
	fds[0].events = POLLIN;
	
	while(1){
		ret = poll(fds, 1, 5000);  // 等待5s,關鍵!!
		if(ret == 1){
			read(fd, &key_val, 1);
			printf("key_val = 0x%x\n", key_val);
		}
		else{
			printf("time out \n");
		}
	}
	return 0;
}

原理淺析

  驅動中poll原型:unsigned poll(struct file *file, poll_table *wait)

  應用中poll原型:int poll(struct pollfd *fds, nfds_t nfds, int timeout);

其中

struct pollfd {
    int fd;
    short events;
    short revents;
};

在linux中執行man poll可以看到events為期待發生的事件,revent為真實發生的事件

nfds為*fds中pollfd的個數

timeout為睡眠時間,毫秒單位。

1. poll作為file_operation的一部分,在呼叫時與open等大致相同

對於系統呼叫poll或select,經過系統呼叫層unistd.h

#define __NR_poll 1068
__SYSCALL(__NR_poll, sys_poll)

linux核心通過巨集定義呼叫, 函式有三個引數對應為fs/select.c

SYSCALL_DEFINE3(poll, struct pollfd __user *, ufds, unsigned int, nfds, int, timeout_msecs)
{
    ... 
    ret = do_sys_poll(ufds, nfds, to);
    ...    
}

do_sys_poll函式也位於位於fs/select.c檔案中,我們忽略其他不重要的程式碼:

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

2. 先來看poll_initwait(&table)

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

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

即table->pt->qproc = __pollwait,__pollwait將在驅動的poll函式裡用到。在poll_initwait()中註冊一下回調函式__pollwait,它就是我們的驅動程式執行poll_wait()時,真正被呼叫的函式。

void poll_initwait(struct poll_wqueues *pwq)
{
	init_poll_funcptr(&pwq->pt, __pollwait);
	pwq->polling_task = current;
	pwq->triggered = 0;
	pwq->error = 0;
	pwq->table = NULL;
	pwq->inline_index = 0;
}
static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
	pt->_qproc = qproc;
	pt->_key   = ~0UL; /* all events enabled */
}

3. 再來do_poll函式位於fs/select.c檔案中,程式碼如下:

static int do_poll(unsigned int nfds,  struct poll_list *list,
		   struct poll_wqueues *wait, struct timespec *end_time)
{
01  ...
02  for (;;) 
    {
03	struct poll_list *walk;
04	bool can_busy_loop = false;

05	for (walk = list; walk != NULL; walk = walk->next) 
        {
06	    struct pollfd * pfd, * pfd_end;
07	    pfd = walk->entries;
08	    pfd_end = pfd + walk->len;
            //查詢多個驅動程式
09	    for (; pfd != pfd_end; pfd++) 
            {
	        /*
		 * Fish for events. If we found one, record it
	         * and kill poll_table->_qproc, so we don't
		 * needlessly register any other waiters after
		 * this. They'll get immediately deregistered
		 * when we break out and return.
		 */
                
                //do_pollfd函式相當於呼叫驅動裡面的forth_drv_poll函式,下面另外再進行分析,返回值mask非零,count++,記錄等待事件發生的程序數
10		if (do_pollfd(pfd, pt, &can_busy_loop, busy_flag)) 
                {
11		    count++;
12		    pt->_qproc = NULL;
		    /* found something, stop busy polling */
13		    busy_flag = 0;
14		    can_busy_loop = false;
		}
	    }
	}
		
        /*
	 * All waiters have already been registered, so don't provide
	 * a poll_table->_qproc to them on the next loop iteration.
	 */
15	pt->_qproc = NULL;
16	if (!count) 
        {
17	    count = wait->error;
18	    if (signal_pending(current))
19	        count = -EINTR;
	}

20	if (count || timed_out)   /* 若count不為0(有等待的事件發生了)或者timed_out不為0(有訊號發生或超時),則退出休眠 */
	    break;

	/* only if found POLL_BUSY_LOOP sockets && not out of time */
21	if (can_busy_loop && !need_resched()) 
        {
22	    if (!busy_end) 
            {
23		busy_end = busy_loop_end_time();
24		continue;
	    }
25	    if (!busy_loop_timeout(busy_end))
26		continue;
	}
27	busy_flag = 0;

	/*
	 * If this is the first loop and we have a timeout
	 * given, then we convert to ktime_t and set the to
	 * pointer to the expiry value.
	 */
28	if (end_time && !to) 
        {
29	    expire = timespec_to_ktime(*end_time);
30	    to = &expire;
	}

        //上述條件不滿足下面開始進入休眠,若有等待的事件發生了,超時或收到訊號則喚醒
31	if (!poll_schedule_timeout(wait, TASK_INTERRUPTIBLE, to, slack))
32	    timed_out = 1;
	}
33	return count;
}

分析其中的程式碼,可以發現,它的作用如下:

①  從<02行>可以知道,這是個迴圈,它退出的條件為:<20行>的2個條件之一( count非0,超時 )

      count非0表示<10行>的do_pollfd至少有一個成功 or <17行>有錯誤產生 or <18行>有訊號等待處理.

②  重點在do_pollfd函式,後面再分析

③  第<31行>,讓本程序休眠一段時間,注意:應用程式執行poll呼叫後,如果①②的條件不滿足,程序就會進入休眠。那麼,誰喚醒呢?除了休眠到指定時間被系統喚醒外,還可以被驅動程式喚醒──記住這點,這就是為什麼驅動的poll裡要呼叫poll_wait的原因,後面分析。

4. do_pollfd函式位於fs/select.c檔案中,程式碼如下:

static inline unsigned int do_pollfd(struct pollfd *pollfd, poll_table *pwait)
{
    ...
    if (file->f_op && file->f_op->poll) {   /*  當fops存在且poll被實際定義時 */
        ...
        mask = file->f_op->poll(file, pwait);
        ...
    }
    /* Mask out unneeded events. */
    mask &= pollfd->events | POLLERR | POLLHUP;    //期望events和實際事件相與;相同有值,不同為零
    pollfd->revents = mask;   /* 實際事件 */
    return mask;
}

可見,它就是呼叫我們的驅動程式裡註冊的poll函式。再把驅動中的poll拷貝過來看一下。

unsigned int buttons_drv_poll (struct file *file, struct poll_table_struct *wait)
{
	unsigned int mask = 0;
	poll_wait(file, &button_waitq, wait);  //不會立即休眠,只是將程式掛在佇列上
	if(ev_press)
		mask |= POLLIN | POLLRDNORM;
	return  mask;
}

可以預見

1. 當按鍵沒有被按下,也就是ev_press==0時,返回的mask=0;do_pollfd返回值為0,count為0,不滿足條件,迴圈繼續執行。

2. 按鍵按下,在中斷處理函式中,ev_press=1,則在poll函式中返回mask |= POLLIN | POLLRDNORM;,當和預期events(測試程式中)fds[0].events = POLLIN;相與後do_pollfd返回真,count++,退出迴圈,結束poll函式的等待,應用程式繼續執行。

5. 驅動程式裡與poll相關的地方有兩處:一是構造file_operation結構時,要定義自己的poll函式。二是通過poll_wait來呼叫上面說到的__pollwait函式,pollwait的程式碼如下:

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
    if (p && wait_address)
        p->qproc(filp, wait_address, p);
}

p->qproc就是__pollwait函式,從它的程式碼可知,它只是把當前程序掛入我們驅動程式裡定義的一個佇列裡而已。它的程式碼如下:

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
     struct poll_table_entry *entry = poll_get_entry(p);

     if (!entry)
         return;

     get_file(filp);
     entry->filp = filp;
     entry->wait_address = wait_address;

     init_waitqueue_entry(&entry->wait, current);

     add_wait_queue(wait_address, &entry->wait);
}

        可以看出,執行到驅動程式的poll_wait函式時,程序並沒有休眠,只是把程序掛在了一個佇列裡。讓程序進入休眠,是前面分析的do_poll函式的<31行>poll_schedule_timeout()【思考一:這個函式具體是如何實現睡眠延時的?】函式。

        poll_wait只是把本程序掛入某個佇列,應用程式呼叫poll > sys_poll > do_sys_poll > poll_initwait,do_poll > do_pollfd > fops.poll,再呼叫poll_schedule_timeout進入休眠。如果我們的驅動程式發現情況就緒,可以把這個佇列上掛著的程序喚醒。可見,poll_wait的作用,只是為了讓驅動程式能找到要喚醒的程序。即使不用poll_wait,當超時,time_out=1,下次迴圈式程式也會退出。

現在來總結一下poll機制:

1. poll > sys_poll > do_poll > poll_initwait,poll_initwait函式註冊一下回調函式__pollwait,它就是我們的驅動程式執行poll_wait時,真正被呼叫的函式。

2. 接下來執行file->f_op->poll,即我們驅動程式裡自己實現的poll函式。 它會呼叫poll_wait把自己掛入某個佇列,這個佇列也是我們的驅動自己定義的,這個佇列的意義是為了能讓其他程式知道去哪裡喚醒它。

3. 程序被喚醒的條件有2:一是上面說的“一定時間”到了,二是被驅動程式喚醒。驅動程式發現條件就緒時,就把“某個佇列”上掛著的程序喚醒,這個佇列,就是前面通過poll_wait把本程序掛過去的佇列。

4. 如果驅動程式沒有去喚醒程序,那麼poll_schedule_timeout超時後,time_out=1,下次迴圈式程式也會退出。

        應用程式呼叫poll,經過一系列呼叫,進入do_poll,首先判斷一下是否有資源,有就直接結束,沒有等待喚醒或者超時,再次判斷是否有資源,有就退出。