linux内核等待队列机制:
案例:分析应用程序串口工具操作串口硬件设备的过程。
1.外设的处理速度要远远慢于CPU!
2.应用程序在用户空间没有权利访问硬件设备,只有通过系统调用跑到内核空间才有权限访问硬件设备!
3.一个应用程序读取串口硬件设备采用两种方法:
轮询方式:相当的耗费CPU的资源,让CPU做大量的无用功!
中断方式:CPU一旦发现串口设备不可读(没数据),CPU干别的事情,一旦串口接收到数据,给CPU产生一个接收中断信号,让CPU来获取串口数据。问:这个应用程序在做什么呢?
当串口设备没有接收到数据,应用程序一旦发现,利用内核提供的睡眠机制,应用在内核空间进入休眠状态;一旦串口设备给CPU产生中断信号,中断信号的到来也就代表这数据的到来,这时只需唤醒休眠的应用程序,让应用程序读取串口数据。
问:如何设备数据没有准备就绪,如果让进程在内核空间进行休眠
答:linux内核等待队列机制
本质目的:就是让进程在内核空间进行休眠
注意:区别于工作队列
工作队列:是中断底半部的机制,是实现延后执行的一个种手段
等待队列:是让进程在内核空间进行休眠的
但是它们针对处理的对象都是进程!
进程的状态:
运行:TASK_RUNNING
准备就绪:TASK_READY
可中断休眠:TASK_INTERRUPTIBEL
不可中断休眠:TASK_UNINTERRUPTIBLE
进程要“休眠":要休眠进程会将CPU资源全部从当前进程中撤下来,将CPU资源给别的任务去使用,比如另外一个进程;
进程之间的切换:又内核调度器来实现,这个调度器就是用来管理进程的!
linux内核等待队列机制实现过程:
老鹰-》调度器:内核已经实现
鸡妈妈-》等待队列头
小鸡-》休眠的进程
内核描述等待队列头涉及的数据类型:
wait_queue_head_t
内核描述休眠的进程,装载休眠进程的容器的数据类型:
wait_queue_t
等待队列让进程休眠的方法,而不是唤醒:
方法1:
步骤:
1.分配等待队列头
wait_queue_head_t wq;
2.初始化等待队列头
init_waitqueue_head(&wq);
3.如果一个进程要访问设备,发现设备不可用,进入休眠,此时分配这个进程的休眠的容器
DECLARE_WAITQUEUE(wait, current);
wait:表示保存当前进程的容器
current:它是内核的全局变量,在linux内核中,内核用struct task_struct结构体来描述每一个进程,那么当进程获取CPU资源是,current就指向当前进程(哪个进程获取CPU资源,current就指向这个进程对应的task_struct结构体对象)
例如打印出当前进程的pid和name
printk("current process name is %s pid is %d\n",
current->comm, current->pid);
或者:
wait_queue_t wait;
init_waitqueue_entry(&wait, current);
注意:如果有多个休眠的进程,必须为每一个进程分配一个容器,并且current也会分别执行不同的进程!
4.然后将当前进程添加到休眠的队列中去
add_wait_queue(&wq, &wait);
注意:仅仅是将当前进程添加到这个队列中去,但进程还处于运行状态!
5.设置当前进程的休眠状态(可中断或者不可中断)
可中断的休眠状态:
current->state = TASK_INTERRUPTIBLE;
不可中断的休眠状态:
current->state = TASK_UNINTERRUPTIBLE;
6.让进程进入真正的休眠状态
schedule(); //启动调度器,并且让CPU资源从当前进程撤下来,给别的进程去使用,至此当前进程运行到这个地方就停止不动!一旦被唤醒,这个函数返回,当前进程接着执行
7.如果进程被设置为可中断的休眠状态,进程被唤醒的方法有两种:
硬件设备可用,产生中断,由中断来唤醒;
进程接收到了信号引起的唤醒,所以要判断唤醒的原因:
判断是否接收到信号引起的唤醒:
if (signal_pending(current)) {
printk("当前进程是由于接收到了信号引起的唤醒");
printk("给用户返回-ERESTARTSYS");
} else {
printk("中断引起的唤醒,说明数据可用");
printk("后续继续获取数据");
}
8.不管是硬件中断引起的唤醒还是信号引起的唤醒,重新设置当前进程的状态为运行状态
current->state = TASK_RUNNING;
9.将当前进程从休眠队列中移除
remove_wait_queue(&wq, &wait);
参考代码:有一个进程读取按键数据:
wait_queue_head_t wq; //全局变量
驱动入口函数或者open函数:
init_waitqueue_head(&wq);
驱动read函数:
static ssize_t btn_read(...)
{
wait_queue_t wait; //分配一个当前进程的容器
init_waitqueue_entry(&wait, current); //把当前进程添加到这个容器中
add_wait_queue(&wq, &wait);//将当前进程添加到休眠队列中
current->state = TASK_INTERRUPTIBLE;//设置当前进程的休眠状态
schedule(); //进入真正的休眠,一旦被唤醒,进程接着执行
//判断唤醒的原因
if (signal_pending(current)) {
printk("接收到了信号引起的唤醒");
ret = -ERESTARTSYS;
} else {
printk("按键有操作,产生中断引起的唤醒");
}
current->state = TASK_RUNNING;//设置当前进程的状态为运行
remove_wait_queue(&wq, &wait);//从休眠队列中移出
上报按键数据
return ret;
}
唤醒的方法有两种:
1.接收到信号引起的唤醒
2.驱动主动唤醒休眠的进程,方法如下:
wake_up(wait_queue_head_t *queue);
唤醒由queue指向的等待队列数据链中的所有睡眠类型的等待进程
wake_up_interruptible(wait_queue_head_t *queue);
唤醒由queue指向的等待队列数据链中的所有睡眠类型为TASK_INTERRUPTIBLE的等待进程
案例1:实现读进程唤醒写进程,写进程唤醒读进程
实验步骤:
insmod btn_drv.ko
./led_test r & //读进程
./led_test w & //写进程
./led_test r & //读进程
kill 读进程或者写进程
案例2:根据以上案例,在底层驱动的read函数能够给用户上报一个按键的信息(键值和按键的状态),提示可以把底层驱动的write函数作为中断来使用。
案例3:利用等待队列,实现按键驱动,要求按键上报的信息为键值和按键的状态!例如:
KEY_UP: 0x50
KEY_DOWN:0x51
按键状态:按下为1,松开为0
分析:
read->fops->cdev->中断->休眠->等待队列
指定超时时间的休眠:
把schedule()换成schedule_timeout(5*HZ);
前者的休眠是永久休眠(没有被驱动主动唤醒或者接收到信号)
后者的休眠是指定了睡眠的时间,例如5秒,如果没有接收到信号,也没有接收到驱动主动唤醒,一旦5秒到期,此函数也会返回,返回0,否则返回非0(驱动主动唤醒或者接收到了信号)!
案例:实现按键驱动,指定超时,而是永久休眠!
方法2:
1.分配等待队列头
wait_queue_head_t wq;
2.初始化等待队列头
init_waitqueue_head(&wq);
3.如果进程进入休眠状态:
wait_event(wq, condition);
condition为真,立即返回,不休眠
condition为假,进程进入不可中断的休眠状态,进程被添加 到wq所对应的等待队列头中
或者
wait_event_interruptible(wq, condition);
condition为真,立即返回,不休眠
condition为假,进程进入可中断的休眠状态,进程被添加 到wq所对应的等待队列头中
或者
wait_event_timeout(wq, condition, 5*HZ);
condition为真,立即返回,不休眠
condition为假,进程进入不可中断的休眠状态,进程被添加 到wq所对应的等待队列头中,并且超时时间到期,进程也被唤醒;
或者
wait_event_interruptible_timeout(wq, condition,5*HZ);
condition为真,立即返回,不休眠
condition为假,进程进入可中断的休眠状态,进程被添加 到wq所对应的等待队列头中,并且超时时间到期,进程也被唤醒;
总结:以上宏涉及的condition其实就是代表是否是驱动主动唤醒,如果驱动主动唤醒,应该让condition设置为真,否则还是为假!
案例:利用等待队列编程方法2来实现按键驱动