在想要学习linux技术的学员面前,最难学习的时候就是要学习linux知识里面的那些运行机制吧。不管是linux运行的阻塞机制还是运行的非阻塞机制都是学员比较难懂的。那么linux运行的阻塞机制是怎样的呢?下面西安鸥鹏的讲师就给大家讲解下:
一、阻塞和非阻塞
阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放弃,或者不停地查询,直至可以进行操作为止。
二、等待队列
在Linux驱动程序中,可以使用等待队列(waitqueue)来实现阻塞进程的唤醒。waitqueue很早就作为一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。
希望等待特定事件的进程把自己放进合适的等待队列,并放弃控制权。因此,等待队列是一组睡眠的进程,当某一条件变为真时,由内核唤醒他们。
1,等待队列头
每个等待队列都有一个等待队列头,驱动注意操作等待队列头来实现阻塞的功能,二等待队列项的内容不需要关心,因为等待队列是由中断处理程序和主要内核函数修改的,其双向链表必须进行保护,防止多进程同时进行访问修改,造成不可预知的后果,所以定义了lock来锁住链表操作的区域
等待队列头结构体的定义:内核使用等待队列头来挂起一个进程,也使用等待队列头来唤醒进程
struct__wait_queue_head{
spinlock_tlock;//自旋锁变量,用于在对等待队列头
structlist_headtask_list;//指向等待队列的list_head
};
typedefstruct__wait_queue_headwait_queue_head_t;
操作函数
#include<linux/sched.h>
#include<linux/wait.h>
1).定义“等待队列头”
wait_queue_head_tmy_queue;
2).初始化“等待队列头”。
voidinit_waitqueue_head(wait_queue_head_t*);
而下面的DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。
DECLARE_WAIT_QUEUE_HEAD(name);
3).条件等待/休眠函数一边休眠等待条件
//当cond条件是false(0)则休眠(不可中断版,不推荐使用)
voidwait_event(wait_queue_head_twq,intcond);
上面程序的执行过程:
1.用当前的进程描述块(PCB)初始化一个wait_queue描述的等待任务。
2.在等待队列锁资源的保护下,将等待任务加入等待队列。
3.判断等待条件是否满足,如果满足,那么将等待任务从队列中移出,退出函数。
4.如果条件不满足,那么任务调度,将CPU资源交与其它任务。
5.当睡眠任务被唤醒之后,需要重复(2)、(3)步骤,如果确认条件满足,退出等待事件函数。
使用举例:flag可以是一个条件表达式
staticwait_queue_head_twq;
init_waitqueue_head(&wq);//初始化等待队列头
//if(!flag){
while(!flag){//条件不满足
if(wait_event_interruptible(wq,flag))//如果是被其他信号唤醒则返回错误
return-ERESTARTSYS;
}
使用while而不使用if的原因是:wait_event_interruptible
可以被中断及信号打断,使用while(1),可以避免被打断的情况。
注:其实可以不用加while,查看内核源码的用法,如果被中断或者信号打断,直接返回错误。
flag=1;//先设置条件,再唤醒
wake_up(&wq);//条件满足时唤醒等待队列头上所有的进程
//当cond条件是false(0)则休眠(超时版,timeout是超时值,单位是计数值)
//超时返回值为0,被唤醒大于0需判断返回值
intwait_event_timeout(wait_queue_head_twq,intcondition,unsigendlongtimeout);
//当cond条件是false则休眠(可中断版)
//返回值:打断:负数,绝对值是错误码,成功0返回值需要做判断
intwait_event_interruptible(wait_queue_head_twq,intcondition);
//当cond条件是false则休眠(可超时中断版)
//打断:负数,绝对值是错误码;超时:0;条件满足:>0
intwait_event_interruptible_timeout(wait_queue_head_twq,intcondition,unsigendlongtimeout);
4).唤醒函数另一边条件成熟时唤醒
voidwake_up(wait_queue_head_t*)//能唤醒所以状态的进程
voidwake_up_interruptible(wait_queue_head_t*)//只适用于interruptible,配对使用
注意:唤醒函数当条件满足时,一定要先设置条件condition,再唤醒调用唤醒函数。因为等待睡眠函数返回后会首先检查condition是否满足,若不满足会继续睡
如:counter=count;
wake_up_interruptible(&wq);
2,等待队列项
定义等待对列:
struct__wait_queue{
unsignedintflags;//prepare_to_wait()里有对flags的操作,查看以得出其含义
#defineWQ_FLAG_EXCLUSIVE0x01//一个常数,在prepare_to_wait()用于修改flags的值
void*private//通常指向当前任务控制块
wait_queue_func_tfunc;//唤醒阻塞任务的函数,决定了唤醒的方式
structlist_headtask_list;//阻塞任务链表
};
typedefstruct__wait_queuewait_queue_t;
1)定义一个等待队列
wait_queue_twait;
2)初始化等待队列
内核中定义的接口如下:
staticinlinevoidinit_waitqueue_entry(wait_queue_t*q,structtask_struct*p)
{
q->flags=0;
q->private=p;//私有数据指针,一般指向当前任务控制块
q->func=default_wake_function;//使用默认的唤醒函数
}
使用范例:
init_waitqueue_entry(&wait,current);
3)添加/等待队列。
voidfastcalladd_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);
add_wait_queue()用于将等待队列wait添加到等待队列头q指向的等待队列链表
4)移除等待队列。
voidfastcallremove_wait_queue(wait_queue_head_t*q,wait_queue_t*wait);
remove_wait_queue()用于将等待队列wait从附属的等待队列头q指向的等待队列链表中移除。
5)判断等待队列是否为空。
staticinlineintwaitqueue_active(wait_queue_head_t*q)
{
return!list_empty(&q->task_list);
}
判断等待对列头是否为空,当一个进程访问设备而得不到资源时就会被放入等待队列头指向的等待队列中。
三、函数sleep_on的实现
Sleep()函数相信大家早已耳熟能详了,可是内部究竟是怎么实现的呢?让我们一起来揭开它的面纱
voidsleep_on(wait_queue_head_t*wq)
{
wait_queue_twait;//定义等待队列
init_waitqueue_entry(&wait,current);
//初始化等待队列
current->state=TASK_UNINTERRUPTIBALE;//设置进程状态
add_wait_queue(wq,&wait);
//加入等待队列
schedule();
//调度,当前进程进入睡眠
remove_wait_queue(wq,&wait);
//醒后从等待队列中移除
}
可以发现,程序之所以能睡眠,是因为他改变了自己的状态,并执行调度,放弃了占用CPU。但是我们要唤醒进程,必须要找到它,怎么找到它呢,关键就在于进程在睡眠前我们把它加入了等待对应,只要找到等待队列我们就能找到挂起的进程并唤醒它。
等待队列是一个具有头节点的双向循环链表,把所有睡眠的进程连接起来,每个节点元素都有进程相关的信息