Wait_Queue------linux内核等待队列机制

案例:当串口设备不可读的时候(没有数据可读),那么应用程序应该怎么办? 

案例:当按键设备没有操作时(按键数据不可读),那么应用程序应该怎么办? 

答:应用程序对设备的这种状态(数据不可用的状态),应用程序要不就轮询读取设备的数据直到读到有效的数据,当然这种方法相当的糟糕,这种操作方式其实也是一种忙等待。 

当然还可以通过睡眠的等待,就是当设备数据不可用时,由底层驱动来检测,检查,识别设备数据可用不可用,如果不可用,底层设备驱动就让应用程序进入休眠状态(结果让当前进程的CPU资源撤下来给别的任务去使用),并且底层驱动能够检查设备可用不可用,如果一旦检查到设备数据可用,再次唤醒休眠的进程(休眠的进程一旦被唤醒,就会获取CPU的资源),然后去读取数据即可。 


问:如何实现一个应用程序在设备驱动程序中进行休眠和唤醒呢? 

答:要实现这种机制,根本上需要驱动程序具备能够检查,检测到设备可用不可用的功能! 


linux内核等待队列实现进程休眠和唤醒的方法和步骤: 

1.分配等待队列头 

wait_queue_head_t wq; 


2.初始化等待队列头 

init_waitqueue_head(&wq); 


//宏名用于定义并初始化,相当于"快捷方式"

DECLARE_WAIT_QUEUE_HEAD (my_queue);


/*定义并初始化一个名为name的等待队列 ,注意此处是定义一个wait_queue_t类型的变量name,并将其private设置为tsk*/

DECLARE_WAITQUEUE(name,tsk);


3.分配等待队列 

wait_queue_t wait; 


4.初始化等待队列,将当前进程添加到这个容器中 

init_waitqueue_entry(&wait, current); 

说明:current是内核的一个全局变量,用来记录当前进程,内核对于每一个进程,在内核空间都有一个对应的结构体struct task_struct,而current指针就指向当前运行的那个进程的task_struct结构体,你可以通过current指针来获取当前进程的pid和进程的名字(current->pid, current->comm) 


5.将当前进程添加到等待队列头中(并没有真正的休眠) 

add_wait_queue(&wq, &wait); 


6.设置当前进程为可中断的休眠状态(还没有真正的休眠) 

set_current_state(TASK_INTERRUPTIBLE);//能够接收处理信号 

说明:设置状态之前,进程是TASK_RUNNING状态! 


7.调用schedule()完成真正的休眠工作

当调用此函数时,会将当前进程占用的CPU资源让出来给别的任务,并且让当前进程进入真正的休眠状态,一旦进程被唤醒,schedule()函数就返回,代码继续往下执行。 


8.一旦被唤醒以后,要判断是什么原因使当前进程唤醒

唤醒进程的原因:1.数据可用的唤醒,2.接收到了信号 


9.调用signal_pending(current)来判断是否是因为接收到信号引起的唤醒

如果此函数返回非0,表明是接收到了信号,如果返回0,表明没有接收到信号,那说明这个唤醒是由于数据可用引起的唤醒操作。如果是接收到信号的唤醒,一般就不要在操作硬件设备了 


10.如果是设备数据可用引起的唤醒,一旦唤醒,调用: 

current->state = TASK_RUNNING; //设置当前进程为运行状态 

remove_wait_queue(&state->wait_queue, &wait);//将唤醒的进程从等待队列头所在的数据连中移除。 


11.进程读取或者操作设备即可。 


参考代码:

假设串口没有数据到来,应用程序调用read读->驱动uart_read:

wait_queue_head_t rwq; //分配一个读的等待队列头, 全局变量

init_waitqueue_head(&wq); //在驱动入口函数初始化

uart_read()

{

        wait_queue_t wait; //分配等待队列

        init_waitqueue_entry(&wait, current); //将当前进程添加到容器中

        add_wait_queue(&rwq, &wait); //将当前进程添加到队列头中

        set_current_state(TASK_INTERRUPTIBLE);//设置当前进程的状态

        schedule(); //进入真正的休眠状态(CPU资源让给别的任务)

        set_current_state(TASK_RUNNING);

        remove_wait_queue(&rwq, &wait);

        //一旦被唤醒,要判断是哪个原因引起的唤醒

        if(signal_pending(current))

        {

                printk("RECV SIN!\n");

               return -ERESTARTSYS; //返回用户空间的read

       }  else {  

                //由于数据可用引起的唤醒读取串口数据

                copy_to_user(...); //上报数据

        }

}


编程实现方法2: 

案例:如果串口没有数据到来,应用程序调用read->uart_read 

1.分配等待队列头 

wait_queue_heat_t rwq; 


2.初始化等待队列头 

init_waitqueue_head(&rwq); 


3.在uart_read函数中,直接调用 

wait_event/wait_evnet_timeout/wait_event_interruptible_timeout 

wait_event_interruptible(&rwq, condition); //如果数据可用,condition为真,如果数据不可用,condition为假,当前进程就会进入休眠。 

4.一旦被唤醒,当前进程直接去操作设备即可


进程通过执行下面几个步骤将自己加入到一个等待队列中

-------------------------------------------------------------------------------

调用宏 DEFINE_WAIT() 创建一个等待队列的项。

调用 add_wait_queue() 把自己加入到队列中(链表操作)。该队列会在进程等待的条件满足时唤醒它。当然我们必须在其他地方撰写相关代码,在事件发生时,对等待队列执行 wake_up() 操作

调用 prepare_to_wait() 方法将进程的状态变更为 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE 。而且该函数会在必要的情况下将进程加回到等待队列,这是在接下来的循环遍历中所需要的。

如果状态被设置为 TASK_INTERRUPTIBLE ,则信号唤醒进程。这就是所谓的伪唤醒(唤醒不是因为事件的发生),因此检查并处理信号。

当进程被唤醒的时候,它会再次检查条件是否为真。如果是,它就退出循环;如果不是,它再次调用 schedule() 并一直重复这步操作。

当条件满足后,进程将自己设置为 TASK_RUNNING 并调用 finish_wait() 方法把自己移出等待队列。


/* 'q' 是我们希望休眠的等待队列 */  

DEFINE_WAIT(wait);  

add_wait_queue(q, &wait);  

while (!condition)   /* 'condition' 是我们在等待的事件 */  

{   

       {

              prepare_to_wait(&q, &wait, TASK_INTERRUPTIBLE);  

        }

if (signal_pending(current))  

       {

              /* 处理信号 */  

              schedule();  

       }  

}

finish_wait(&q, &wait); 


唤醒

唤醒操作通过函数 wake_up() 进行,它会唤醒指定的等待队列上的所有进程。它调用函数 try_to_wake_up() ,该函数负责将进程设置为 TASK_RUNNING 状态,调用 enqueue_task() 将此进程放入红黑树中,如果被唤醒的进程优先级比当前执行的进程优先级高,还要设置 need_resched 标志。通常哪段代码促使等待条件达成,它就要负责随后调用 wake_up() 函数 。举例来说,当磁盘数据到来时,VFS 就要负责对等待队列调用 wake_up() ,以便唤醒队列中等待这些数据的进程。

关于休眠有一点需要注意,存在虚假的唤醒(信号)。有时候进程被唤醒并不是因为它所等待的条件达成了,所以需要用一个循环处理来保证它等待的条件真正达成。下图描述了每个调度程序状态之间的关系


推荐阅读更多精彩内容