Linux设备驱动六 (1)等待队列
2016-12-30 12:09
288 查看
1 概述
等待队列在Linux内核中有着举足轻重的作用,很多Linux驱动都或多或少涉及到了等待队列。因此,对于linux内核及驱动开发者来说,掌握等待队列是必须课之一。Linux内核的等待队列是以双循环链表为基础数据结构,与进程调度机制紧密结合,能够用于实现核心的异步事件通知机制。
它有两种数据结构:等待队列头(wait_queue_head_t)和等待队列项(wait_queue_t)。
等待队列头和等待队列项中都包含一个list_head类型的域作为”连接件”。它通过一个双链表和把等待task的头,和等待的进程列表链接起来。
1.1 作用
在内核里面,等待队列是有很多用处的,尤其是在中断处理、进程同步、定时等场合。可以使用等待队列在实现阻塞进程的唤醒。它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制,同步对系统资源的访问等。1.2 定义
等待队列数据结构在内核中的定义/include/linux/wait.h
1.2.1 wait_queue_head_t
等待队列头struct __wait_queue_head {
spinlock_tlock;
structlist_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
1.2.1.1 spinlock_t lock
在对task_list与操作的过程中,使用该锁实现对等待队列的互斥访问1.2.1.2 struct list_head task_list
struct list_head {structlist_head *next, *prev;
};
双向循环链表,存放等待的进程。
1.2.2 wait_queue_t
等待队列项struct __wait_queue {
unsignedint flags;
#defineWQ_FLAG_EXCLUSIVE 0x01
void*private; //私有的
wait_queue_func_tfunc;
structlist_head task_list;
};
typedef struct __wait_queue wait_queue_t;
struct list_head {
structlist_head *next, *prev;
};
1.3 操作
1.3.1 等待队列头与等待队列项区别
等待队列(wait_queue_t)和等待对列头(wait_queue_head_t)的区别:等待队列是等待队列头的成员。也就是说等待队列头的task_list域链接的成员就是等待队列类型的(wait_queue_t)。
1.3.2 定义并初始化等待队列头
1.3.2.1 动态定义并初始化
wait_queue_head_t my_queue;init_waitqueue_head(&my_queue);
init_waitqueue_head()函数会将自旋锁初始化为未锁,等待队列初始化为空的双向循环链表。
1.3.2.2 静态定义并初始化
DECLARE_WAIT_QUEUE_HEAD(my_queue);作用和动态定义相同
1.3.3 定义并初始化等待队列项
DECLARE_WAITQUEUE(name,tsk);注意此处是定义一个wait_queue_t类型的变量name,
并将其private与设置为tsk。
其中flags域指明该等待的进程是互斥进程还是非互斥进程。
其中0是非互斥进程,
WQ_FLAG_EXCLUSIVE(0x01) 是互斥进程。
1.3.3.1 添加等待队列项
补充一点自旋锁和开关中断的知识自旋锁:
作用:
只要代码在进入临界区前加上锁,在进程还没出临界区之前,别的进程(包括自身处理器和别的处理器上的进程)都不能进入临界区
1) 动态定义并初始化
spinlock_t lock;
spin_lock_init(&lock);
2) 上锁
spin_lock(&lock); //关抢占,获得锁,防止别的处理器访问
/*执行临界区代码*/
3) 释放锁
spin_unlock(&lock); //开抢占,释放锁
开关中断:
unsigned long flag;
local_irq_save(flag); //在关中断前,先报存原来的中断状态
/*执行临界区代码*/
local_irq_restore(flag); //开启中断,然后还原原来的中断状态
自旋锁不能防止中断的发生,所以在进入临界区代码时上锁同时关中断,就有了以下函数
//上锁同时关中断(保存原来的中断状态)
spin_lock_irqsave(spinlick_t *lock,unsigned long falg) =
spin_lock(spinlock_t*lock) + local_irq_save(unsigned long flag)
//解锁同时关中断(还原原来的中断状态)
spin_unlock_irqrestore(spinlick_t *lock,unsigned long falg) =
spin_unlock(spinlock_t*lock) + local_irq_restorr(unsigned long flag)
voidadd_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
{
unsignedlong flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock,flags);
__add_wait_queue(q,wait);
spin_unlock_irqrestore(&q->lock,flags);
}
EXPORT_SYMBOL(add_wait_queue);
功能:设置等待的进程为非互斥进程,并将其添加进等待队列头(q)的队头中
voidadd_wait_queue_exclusive(wait_queue_head_t *q, wait_queue_t *wait)
功能:设置等待的进程为互斥进程,并将其添加进等待队列头(q)的队头中
1.3.3.2 移出等待队列项
voidremove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait){
unsignedlong flags;
spin_lock_irqsave(&q->lock,flags);
__remove_wait_queue(q,wait);
spin_unlock_irqrestore(&q->lock,flags);
}
EXPORT_SYMBOL(remove_wait_queue);
在等待的资源或事件满足时,进程被唤醒,使用该函数被从等待头中删除。
1.3.4 等待事件
补充一点信号知识TASK_RUNNING为可执行状态(执行状态、执行等待状态)
TASK_INTERRUPTIBLE:
信号可以唤醒进程(信号和事件都可以唤醒该进程)。这就是所谓的伪唤醒(唤醒不是因为事件的发生,而是由信号唤醒的),因此检查并处理信号。
注: 信号和等待事件都可以唤醒处于TASK_INTERRUPTIBLE
状态的进程,信号唤醒该进程为伪唤醒;该进程被唤醒后,如果 (!condition)
结果为真,则说明该进程不是由等待事件唤醒的,而是由信号唤醒的。所以该进程处理信号后将再次让出CPU控制权;
TASK_UNINTERRUPTIBLE:
进程会忽略信号
TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE
的区别:
TASK_UNINTERRUPTIBLE状态的进程会忽略信号,而处于TASK_INTERRUPTIBLE状态的进程如果收到信号会被唤醒并处理信号(然后再次进入等待睡眠状态)。两种状态的进程位于同一个等待队列上,等待某些事件,不能够运行。
所以:
TASK_INTERRUPTIBLE是可以被信号和wake_up()唤醒的,当信号到来时,进程会被设置为可运行。
TASK_UNINTERRUPTIBLE只能被wake_up()唤醒。
信号本质:信号是在软件层次上对中断机制的一种模拟,软中断
信号来源:信号事件的发生有两个来源:
硬件来源:(比如我们按下了键盘或者其它硬件故障);
软件来源:最常用发送信号的系统函数是kill,raise, alarm和setitimer以及sigqueue函数,软件来源还包括一些非法运算等操作。
区分是什么原因唤醒进程,用signal_pending( current );
检查当前进程是否有信号处理,返回不为0表示有信号需要处理。-ERESTARTSYS表示信号函数处理完毕后重新执行信号函数前的某个系统调用。
也就是说,如果信号函数前有发生系统调用,在调度用户信号函数之前,内核会检查系统调用的返回值,看看是不是因为这个信号而中断了系统调用.如果返回值-ERESTARTSYS,并且当前调度的信号具备-ERESTARTSYS属性,系统就会在用户信号函数返回之后再执行该系统调用。
所以经常我们在睡眠的代码中会看到这样的例子:
if (signal_pending(current))
{
ret =-ERESTARTSYS;
return ret;
}
分析:
当一个系统调用处于等待状态时,比如等待输入缓冲区不为空,此时产生了信号,这个信号仅仅是在该进程的thread_info结构中标识一下,就是所谓的“发信号”,然后唤醒进程的系统调用,系统调用醒来后,此时仅仅用signal_pending()检查一下是否有信号,这里,不处理信号的,当此时有信号,系统调用返回ERESTARTSYS,在从系统调用的返回用户空间时,会根据thread_info中信号标识位调用相应的信号处理函数,这里就是所谓的“接收信号”,对于Linux,上层库函数会根据系统调用的ERESTARTSYS返回值重启该系统调用。
在Linux中,重启的系统调用会再次检查缓冲区,为空,说明刚才的信号不是缓冲区有数据了的信号,继续等待,重复刚才的过程,不为空,就可以直接处理数据,系统调用正常结束
注:“发信号”仅仅是标识thread_info,系统调用醒来检查信号,仅仅是signal_pending()判断一下thread_info中是否有任何一个信号标识,真正的“接受信号”是从系统调用返回时,或者异常处理程序返回时,比如每次的时钟中断处理函数返回时,检查thread_info中具体哪个信号,调用相应处理程序
1.3.4.1 wait_event
#define wait_event(wq, condition) \do { \
if(condition) \
break; \
__wait_event(wq,condition); \
} while (0)
在等待队列中睡眠直到condition为真。在等待的期间,进程会被置为TASK_UNINTERRUPTIBLE进入睡眠,直到condition变量变为真。每次进程被唤醒的时候都会检查condition的值.TASK_UNINTERRUPTIBLE:睡眠不能被中断打断;
1.3.4.2 wait_event_interruptible()
和wait_event()的区别是调用该宏在等待的过程中当前进程会被设置为TASK_INTERRUPTIBLE状态.在每次被唤醒的时候,首先检查condition是否为真,如果为真则返回,否则检查如果进程是被信号唤醒,会返回-ERESTARTSYS错误码.如果是condition为真,则返回0.
被信号唤醒:
1.3.4.3 wait_event_timeout()
也与wait_event()类似.不过如果所给的睡眠时间为负数则立即返回.如果在睡眠期间被唤醒,且condition为真则返回剩余的睡眠时间,否则继续睡眠直到到达或超过给定的睡眠时间,然后返回0.1.3.4.4 wait_event_interruptible_timeout()
与wait_event_timeout()类似,不过如果在睡眠期间被信号打断则返回ERESTARTSYS错误码.1.3.4.5 wait_event_interruptible_exclusive()
同样和wait_event_interruptible()一样,不过该睡眠的进程是一个互斥进程.1.3.5 唤醒队列
1.3.5.1 wake_up()
void __wake_up(wait_queue_head_t *q,unsigned int mode,int nr_exclusive, void *key){
unsignedlong flags;
spin_lock_irqsave(&q->lock,flags);
__wake_up_common(q,mode, nr_exclusive, 0, key);
spin_unlock_irqrestore(&q->lock,flags);
}
EXPORT_SYMBOL(__wake_up);
唤醒等待队列.可唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERUPTIBLE状态的进程,和wait_event/wait_event_timeout成对使用.
1.3.5.2 wake_up_interruptible()
和wake_up()唯一的区别是它只能唤醒TASK_INTERRUPTIBLE状态的进程,与 wait_event_interruptible
wait_event_interruptible_timeout
wait_event_interruptible_exclusive
成对使用.
1.3.6 在等待队列上睡眠
1.3.6.1 sleep_on()
void __schedsleep_on(wait_queue_head_t *q){
sleep_on_common(q,TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);
}
EXPORT_SYMBOL(sleep_on);
该函数的作用是定义一个等待队列(wait),并将当前进程添加到等待队列中(wait),然后将当前进程的状态置为TASK_UNINTERRUPTIBLE,并将等待队列(wait)添加到等待队列头(q)中。之后就被挂起直到资源可以获取,才被从等待队列头(q)中唤醒,从等待队列头中移出。在被挂起等待资源期间,该进程不能被信号唤醒。
1.3.6.2 sleep_on_timeout()
与sleep_on()函数的区别在于调用该函数时,如果在指定的时间内(timeout)没有获得等待的资源就会返回。实际上是调用schedule_timeout()函数实现的。值得注意的是如果所给的睡眠时间(timeout)小于0,则不会睡眠。该函数返回的是真正的睡眠时间。1.3.6.3 interruptible_sleep_on()
该函数和sleep_on()函数唯一的区别是将当前进程的状态置为TASK_INTERRUPTINLE,这意味在睡眠如果该进程收到信号则会被唤醒。1.3.6.4 interruptible_sleep_on_timeout()
类似于sleep_on_timeout()函数。进程在睡眠中可能在等待的时间没有到达就被信号打断而被唤醒,也可能是等待的时间到达而被唤醒。相关文章推荐
- Linux 设备驱动--- 内核等待队列
- Linux驱动开发-11、设备阻塞访问-等待队列
- Linux_设备驱动阻塞/非阻塞IO_等待队列
- linux驱动开发--字符设备:内核等待队列
- Linux 设备驱动--- 内核等待队列 --- wait_queue_head --- wait_event_interruptible --- 按键驱动程序优化
- Linux 设备驱动--- 内核等待队列 --- wait_queue_head --- wait_event_interruptible --- 按键驱动程序优化
- linux设备驱动--内核等待队列知识点---结合中断使用
- linux高级字符设备驱动之 二 内核等待队列
- linux驱动---等待队列、工作队列、Tasklets
- linux 等待队列 与linux 等待队列头关系-不懂linux驱动阻塞操作可以看看
- linux驱动的等待队列
- [arm驱动]linux等待队列阻塞中断IO的应用
- Linux驱动阻塞与非阻塞IO之等待队列
- 【Linux驱动】内核等待队列
- 字符设备驱动之Buttons-等待队列
- 按键驱动(等待队列+设备树+属性文件)
- linux驱动之等待队列
- Linux驱动之等待队列和poll使用
- linux驱动编程--等待队列浅析
- 字符设备驱动之循环缓冲队列+读写等待