Linux设备驱动六 (1)等待队列


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_INTERRUPTIBLETASK_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()函数。进程在睡眠中可能在等待的时间没有到达就被信号打断而被唤醒,也可能是等待的时间到达而被唤醒。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值