Linux设备驱动中的阻塞与非阻塞IO

      如果驱动程序的请求无法立即满足,该如何处理?

  • 驱动程序应该阻塞该进程,并将该进程置为休眠状态直到请求可以继续。

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)  
上述操作会唤醒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中,这个结构中包含了休眠进程的信息及期望被唤醒的相关信息。 
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); 

Wait_event()函数的定义如下所示,从其源代码可知,当condition满足时,wait_event()会立即返回,否则,阻塞等待condition满足。
#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是个函数指针; 

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,以调度其他进程执行。无论什么时候调用这个函数,都将告诉内核重新选择其他进程运行。 
第四:
当中断产生时,在中断处理程序中通过调用wake_up()或者wake_up_interruptible()来唤醒被休眠的进程。




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值