如果驱动程序的请求无法立即满足,该如何处理?
- 驱动程序应该阻塞该进程,并将该进程置为休眠状态直到请求可以继续。
1. 阻塞与非阻塞IO
- 阻塞操作是指在执行设备操作时,若不能获得资源则挂起进程直到满足可操作的条件后再进行操作。
- 非阻塞操作的进程在不能进行设备操作时,并不挂起,它或者放弃,或者不停的查询,直到可以进行操作为止。
1.1 区别
- 当进程采用阻塞方式进行设备访问时,若设备的资源不能获得,则应用程序的read(),write()等系统调用不会返回,并且此进程(如cat、echo进程)会进入休眠状态,同时它会将CPU资源“礼让“给其他的进程使用,直到能够获取到设备资源为止。在这整个过程中仍然进行了正确的设备访问,且用户感知不到这个阻塞。当他获取不到资源时,会进入休眠状态,等下次调度器调用到它时,它会再次判断资源是否可以获得,如果还是不能获得,它又将进入休眠态。反正有一次它会被调度到,并且那时资源是可获得的状态。到那个时候,它才会返回系统调用。由于系统的调度时间起始是很短的,所以,有时候用户是感觉不到的。
- 若采用非阻塞的方式,则当用户获取不到资源时,它会不停的查询(直到它的时间片用完,如果下次调度到它时,它还是不能获取资源,则又会不停的查询,直到它的时间片用完),这反而会无谓地消耗CPU资源。
由于阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。而唤醒进程的最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。
2. 进程休眠与唤醒
当一个进程被置入休眠时,它会被标志为一种特殊状态并从调度器的运行队列中移走。直到某些情况下修改了这个状态,进程才会在任意CPU上调用,也即运行进程。休眠中的进程会被搁置在一边,等待将来的某个事件(一般为wake_up()函数)发生。
2.1 怎么休眠?
等待事件的休眠:Linux内核中最简单的休眠方式是称为wait_event的宏(以及它的几个变种),它使queue作为等待队列头的等待队列中所有属于该等待队列对应的进程休眠;在实现休眠的同时,它也检查进程等待的条件,当条件condition满足且queue作为等待队列头的等待队列被唤醒时,函数返回。Wait_event的形式如下所示:
- Wait_event(queue, condition )
- Wait_event_interruptible(queue, condition)
- Wait_event_timeout(queue, condition, timeout)
- Wait_event_interruptible_timeout(queue, condition, timeout)
Wait_event()和wait_event_interruptible()区别在于后者可以被信号打断,而前者不会。 Wait_event_interruptible_timeout()可以设置一个timeout。
还可以通过显示方式使进程休眠,如:
__set_current_state(TASK_INTERRUPTIBLE);
schedule(); //CPU调度其他进程执行
如果有唤醒该进程的条件发生,如修改进程状态,进程会继续schedule()之后的语句。
2.2 怎么唤醒?
其他的某个执行线程(可能是另一个进程或者中断处理例程,注意:中断处理例程不属于进程上下文)必须为我们执行唤醒,因为我们的进程正在休眠中。而用来唤醒休眠进程的基本函数是wake_up(),它也有多种形式:
注意:
- Void wake_up(wait_queue_head_t *queue)
- Void wake_up_interruptible(wai_queue_head_t *queue)
注意:
wake_up()应该与wait_event()或者wait_event_timeout()成对使用,wake_up_interruptible()则应该与wait_event_interruptibl() 或wait_event_interruptible_timeout() 成对使用。Wake_up()可以唤醒处于TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于TASK_UNINTERRUPTIBLE的进程。
3. 等待队列
在linux驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。Wait queue很早就作为一个基本的功能单位出现在linux内核里面了,它以队列为基础的数据结构,
与进程调度机制,如schedule()和set_current_state()紧密结合,能够用于实现内核中的异步事件通知机制。
异步通知的概念:一旦设备就绪,则主动通知应用程序,这样应用程序根本就不需要查询设备状态,这一点非常类似于硬件上的“中断”的概念,它比较准确的称谓是“信号驱动的异步IO”,注意,信号是在软件层次上对中断机制的一种模拟。
等待队列通俗点讲:
就是一个进程链表,其中包含了等待某个特定事件的所有进程。
在linux中,一个等待队列通过一个“等待队列头(wait queue head)”来管理,等待队列头是一个类型为wait_queue_head_t的结构体,定义在linux/wait.h中,这个结构中包含了休眠进程的信息及期望被唤醒的相关信息。
在linux中,一个等待队列通过一个“等待队列头(wait queue head)”来管理,等待队列头是一个类型为wait_queue_head_t的结构体,定义在linux/wait.h中,这个结构中包含了休眠进程的信息及期望被唤醒的相关信息。
struct __wait_queue_head {
spinlock_t lock; /*自旋锁*/
struct list_head task_list; /*任务链表,此链表中保存的是一个等待队列入口,该入口声明为wait_queue_t类型。*/
};
typedef struct __wait_queue_head wait_queue_head_t;
/*静态的方法定义一个等待队列头*/
DECLARE_WAIT_QUEUE_HEAD(name)
/*动态的方法定义一个等待队列头*/
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
#define wait_event(wq, condition) \
do { \
if (condition) \ /*条件满足立即返回*/
break; \
__wait_event(wq, condition); \ /*添加等待队列并阻塞*/
} while (0)
#define __wait_event(wq, condition) \
do { \
DEFINE_WAIT(__wait); \ /*定义一个等待队列*/
for (;;) { \
prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE); \
if (condition) \
break; \
schedule(); \ /*放弃CPU,通过schedule调度其他进程执*/
} \
finish_wait(&wq, &__wait); \
} while (0)
void fastcall prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state)
{
unsigned long flags;
wait->flags &= ~WQ_FLAG_EXCLUSIVE;
spin_lock_irqsave(&q->lock, flags);
if (list_empty(&wait->task_list))
__add_wait_queue(q, wait); /*加入等待队列*/
/*
* don't alter the task state if this is just going to
* queue an async wait queue callback
*/
if (is_sync_wait(wait))
set_current_state(state); /*设置进程状态为等待状态(TASK_UNINTERRUPUTIBLE)*/
spin_unlock_irqrestore(&q->lock, flags);
}
void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait)
{
unsigned long flags;
__set_current_state(TASK_RUNNING); /*恢复进程状态为TASK_RUNNING状态*/
if (!list_empty_careful(&wait->task_list)) {
spin_lock_irqsave(&q->lock, flags);
list_del_init(&wait->task_list);
spin_unlock_irqrestore(&q->lock, flags);
}
}
4. wait_queue_t(等待队列)与wait_queue_head_t(等待队列头)的关系
4.1 定义
等待队列的定义如下:
typedef struct __wait_queue wait_queue_t;
struct __wait_queue
{
unsigned int flags; //标志
#define WQ_FLAG_EXCLUSIVE 0x01
void *private; //私有数据
wait_queue_func_t func; //等待队列功能函数
struct list_head task_list; //任务链表
};
wait_queue_func_t如下定义:
typedef int (*wait_queue_func_t)(wait_queue_t *wait, unsigned mode, int sync, void *key);
默认的wake function如下定义:int default_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key); 成员func是个函数指针;
默认的wake function如下定义:int default_wake_function(wait_queue_t *wait, unsigned mode, int sync, void *key); 成员func是个函数指针;
list_head结构体变量task_list用于构成双向链表,定义如下:
struct list_head
{
struct list_head *next, *prev;
};
private用于保存私有数据;
等待队列头的定义如下:
typedef struct __wait_queue_head wait_queue_head_t;
struct __wait_queue_head
{
spinlock_t lock; //锁
struct list_head task_list; //任务链表
};
通常的做法是task被标识为一个wait_queue_t(wait_queue_t中的private可指向当前的task)挂接到wait_queue_head_t链表上去,然后等待事件的发生即可wait_event。
4.2 初始化
下面的宏用于wait_queue_t变量的声明及初始化
#define __WAITQUEUE_INITIALIZER(name, tsk) { \
.private = tsk, \
.func = default_wake_function, \
.task_list = { NULL, NULL } }
default_wake_fuction函数的定义位于sched.c文件中:
int default_wake_function(wait_queue_t *curr, unsigned mode, int sync, void *key)
{
return try_to_wake_up(curr->private, mode, sync);
}
其中try_to_wake_up函数用于唤醒一个thread, curr->private就是等待被唤醒的thread,mode就是task states的掩码,sync表示是否进行同步唤醒。
在驱动程序中我们通常的用法为:
DECLARE_WAITQUEUE(wait, current);该宏用于定义一个等待队列wait,并将private初始化为current(当前进程)状态。
DECLARE_WAITQUEUE宏的定义如下:
#define DECLARE_WAITQUEUE(name, tsk) \
wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
等待队列头wait_queue_head_t的初始化可以采用两种方式:静态方法和动态方法。
静态方法:
DECLARE_WAIT_QUEUE_HEAD(name);
#define DECLARE_WAIT_QUEUE_HEAD(name) \
wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
该宏用于声明并初始化wait_queue_head_t变量name。
__WAIT_QUEUE_HEAD_INITIALIZER(name)用于初始化一个wait_queue_head_t变量name,将lock成员变量初始化为unlock状态,并将链表头task_list初始化,宏定义:
#define __WAIT_QUEUE_HEAD_INITIALIZER(name) { \
.lock = __SPIN_LOCK_UNLOCKED(name.lock), \
.task_list = { &(name).task_list, &(name).task_list } }
动态方法:
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
init_waitqueue_head函数的定义位于wait.c文件中:
void init_waitqueue_head(wait_queue_head_t *q)
{
pin_lock_init(&q->lock);
INIT_LIST_HEAD(&q->task_list);
}
5. 具体实例
static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, loff_t *ppos)
{
... ...
DECLARE_WAITQUEUE(wait, current); /* 定义等待队列,初始化一个名为name的等待队列 */
add_wait_queue(&xxx_wait, &wait); /* 添加等待队列,将等待队列wait添加到等待队列头xxx_wait指向的等待队列链表中 */
ret = count;
/* 等待设备缓冲区可写 */
do {
avail = device_writable(...);
if (avail < 0)
__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态,也可以直接采用current->state = TASK_INTERRUPTIBLE,类似有__add_current_state() */
if (avail < 0) {
if (file->f_flags & O_NONBLOCK) { /* 非阻塞 */
if(!ret)
ret = -EAGAIN;
goto out;
}
schedule(); /* 调度其他进程执行 */
if (signal_pending(current)) { /* 如果是因为信号唤醒 */
if(!ret)
ret = -ERESTARTSYS;
goto out;
}
}
}while (avail < 0);
/* 写设备缓冲区 */
device_write(...);
out:
remove_wait_queue(&xxx_wait, &wait); /* 将等待队列移出等待队列头 */
set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
return ret;
}
对于这个例子有三个要点:
- 如果设备是非阻塞访问(O_NONBLOCK被设置),设备忙时直接返回“ -EAGAIN”;
- 对于阻塞访问,进程会发生状态切换并显示地通过“schedule()”调度其他进程执行;此时进程进入休眠,执行语句暂停;
- 当进程被唤醒的时候,由于调度出去的时候进程状态是“TASK_INTERRUPTIBLE",因此有可能是信号唤醒;所以这里先用”signal_pending()“来判断是否是信号唤醒,如果是,返回-ERESTARTSYS,如果不是,继续循环判断设备是否可写。
6. 总结
当驱动程序无法满足请求时,应该让进程阻塞,并让进程进入休眠状态。而进入休眠状态的方法则是通过调用wait_event()函数或者wait_event_interruptible()函数
。在驱动程序中等待队列的使用一般为:
第一:
分配并初始化一个wait_queue_t结构体(也就是分配并初始化一个等待队列),然后将其加入到等待队列头。目的:不管谁来负责唤醒该进程,都能找到正确的进程。
第二:
设置进程状态,将其标记为休眠。在<linux/sched.h>定义了多个任务状态,其中有两个状态表明了进程处于休眠状态:TASK_INTERRUPTIBALE和TASK_UNINTERRUPTIBLE;前者表示不能被信号打断,后者表示可以被信号打断。但要注意:如果我们调用的是wait_event()则进程状态会被设置为TASK_UNINTERRUPTIBLE状态,如果调用的是wait_event_interruptible()则进程状态会被设置为TASK_UNINTERRUPTIBLE状态。
第三:
通过调用schedule()函数来放弃CPU,以调度其他进程执行。无论什么时候调用这个函数,都将告诉内核重新选择其他进程运行。
第四:
。在驱动程序中等待队列的使用一般为:
第一:
分配并初始化一个wait_queue_t结构体(也就是分配并初始化一个等待队列),然后将其加入到等待队列头。目的:不管谁来负责唤醒该进程,都能找到正确的进程。
第二:
设置进程状态,将其标记为休眠。在<linux/sched.h>定义了多个任务状态,其中有两个状态表明了进程处于休眠状态:TASK_INTERRUPTIBALE和TASK_UNINTERRUPTIBLE;前者表示不能被信号打断,后者表示可以被信号打断。但要注意:如果我们调用的是wait_event()则进程状态会被设置为TASK_UNINTERRUPTIBLE状态,如果调用的是wait_event_interruptible()则进程状态会被设置为TASK_UNINTERRUPTIBLE状态。
第三:
通过调用schedule()函数来放弃CPU,以调度其他进程执行。无论什么时候调用这个函数,都将告诉内核重新选择其他进程运行。
第四:
当中断产生时,在中断处理程序中通过调用wake_up()或者wake_up_interruptible()来唤醒被休眠的进程。