Linux设备驱动开发详解-Note(16)---Linux 设备驱动中的阻塞与非阻塞 I/O(1)

时间:2021-02-26 17:56:48

Linux 设备驱动中的阻塞与非阻塞 I/O(1)

成于坚持,败于止步

阻塞与非阻塞 I/O

阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。被挂起的进程进入休眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时并不挂起,它或者放
弃,或者不停地查询,直至可以进行操作为止。 

驱动程序通常需要提供这样的能力:当应用程序进行 read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的 xxx_read()、xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的 read()、write()等调用才返回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的 xxx_read()、xxx_write()等操作应立即返回,read()、write()等系统调用也随即被返回。 

阻塞从字面上听起来似乎意味着低效率,实则不然,如果设备驱动不阻塞,则用户想获取设备资源只能不停地查询,这反而会无谓地耗费 CPU 资源。而阻塞访问时,不能获取资源的进程将进入休眠,它将 CPU 资源让给其他进程。 因为阻塞的进程会进入休眠状态,因此,必须确保有一个地方能够唤醒休眠的进程。唤醒进程的地方最大可能发生在中断里面,因为硬件资源获得的同时往往伴随着一个中断。 

下面代码清单分别演示了以阻塞和非阻塞方式读取串口一个字符的代码。实际的串口编程中,若使用非阻塞模式,还可借助信号(sigaction)以异步方式访问串口以提高 CPU 利用率,而这里仅仅是为了说明阻塞与非阻塞的区别。

char buf; 
fd = open("/dev/ttyS1", O_RDWR);  
... 
res = read(fd,&buf,1); //当串口上有输入时才返回 
if(res==1) 
	printf("%c\n", buf); 
阻塞地读取串口一个字符
char buf; 
fd = open("/dev/ttyS1", O_RDWR| O_NONBLOCK);  
... 
while(read(fd,&buf,1)!=1); //串口上无输入也返回,所以要循环尝试读取串口 
printf("%c\n", buf); 
非阻塞地读取串口一个字符

等待队列 

在 Linux 驱动程序中,可以使用等待队列(wait queue)来实现阻塞进程的唤醒。

wait queue 很早就作为一个基本的功能单位出现在 Linux 内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,能够用于实现内核中的异步事件通知机制。等待队列可以用来同步对系统资源的访问,信号量在内核中也依赖等待队列来实现。Linux 2.6 提供如下关于等待队列的操作。 

1.定义“等待队列头”。 

wait_queue_head_t my_queue; 

2.初始化“等待队列头”。 

init_waitqueue_head(&my_queue); 而下面的 DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头的“快捷方式”。 

DECLARE_WAIT_QUEUE_HEAD (name) 

3.定义等待队列。 

DECLARE_WAITQUEUE(name, tsk) 

该宏用于定义并初始化一个名为 name 的等待队列。 

4.添加/移除等待队列。 

void fastcall add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 

void fastcall remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait); 

add_wait_queue()用于将等待队列 wait 添加到等待队列头 q 指向的等待队列链表中,而 remove_wait_queue()用于将等待队列 wait 从附属的等待队列头 q 指向的等待队列链表中移除。 

5.等待事件。 

wait_event(queue, condition) 

wait_event_interruptible(queue, condition) 

wait_event_timeout(queue, condition, timeout) 

wait_event_interruptible_timeout(queue, condition, timeout) 

等待第一个参数 queue 作为等待队列头的等待队列被唤醒,而且第二个参数condition 必须满足,否则阻塞。wait_event()和 wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上_timeout 后的宏意味着阻塞等待的超时时间,以 jiffy 为单位,在第三个参数的 timeout 到达时,不论 condition 是否满足,均返回。 

wait()的定义如代码清单所示,从其源代码可以看出,当 condition 满足时,wait_event()会立即返回,否则,阻塞等待 condition 满足。

1  #define wait_event(wq, condition)             \ 
2  do {                         \ 
3   if (condition)   /*条件满足立即返回*/          \ 
4     break;                      \ 
5   __wait_event(wq, condition);/*添加等待队列并阻塞*/ 
6  } while (0)       
7   
8  #define __wait_event(wq, condition)            \ 
9  do {                         \ 
10  DEFINE_WAIT(__wait);                  \ 
11                            \ 
12  for (;;) {                      \ 
13    prepare_to_wait(&wq, &__wait, TASK_UNINTERRUPTIBLE);  \ 
14    if (condition)                  \ 
15      break;                    \ 
16    schedule();/*放弃 CPU*/                \ 
17  }                          \ 
18  finish_wait(&wq, &__wait);               \ 
19 } while (0) 
20  
21 void fastcall 
22 prepare_to_wait(wait_queue_head_t *q, wait_queue_t *wait, int state) 
23 { 
24  unsigned long flags; 
25  
26  wait->flags &= ~WQ_FLAG_EXCLUSIVE; 
27  spin_lock_irqsave(&q->lock, flags); 
28  if (list_empty(&wait->task_list)) 
29    __add_wait_queue(q, wait);//添加等待队列 
30  if (is_sync_wait(wait)) 
31    set_current_state(state);//改变当前进程的状态为休眠 
32  spin_unlock_irqrestore(&q->lock, flags); 
33 } 
34  
35 void fastcall finish_wait(wait_queue_head_t *q, wait_queue_t *wait) 
36 { 
37  unsigned long flags;
38  
39  __set_current_state(TASK_RUNNING);// 恢 复 当 前 进 程 的 状 态 为TASK_RUNNING 
40  if (!list_empty_careful(&wait->task_list)) { 
41    spin_lock_irqsave(&q->lock, flags); 
42    list_del_init(&wait->task_list); 
43    spin_unlock_irqrestore(&q->lock, flags); 
44  } 
45 } 

6.唤醒队列。 

void wake_up(wait_queue_head_t *queue); 

void wake_up_interruptible(wait_queue_head_t *queue); 

上述操作会唤醒以 queue 作为等待队列头的所有等待队列中所有属于该等待队列头的等待队列对应的进程。 

wake_up() 应 与 wait_event() 或 wait_event_timeout()成对使用 ,而wake_up_interruptible() 则应与wait_event_interruptible() 或wait_event_interruptible_timeout() 成对使用 。 wake_up()可唤醒处于TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE的进程 , 而wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE 的进程。 

7.在等待队列上睡眠。 

sleep_on(wait_queue_head_t *q ); 

interruptible_sleep_on(wait_queue_head_t *q ); 

sleep_on()函数的作用就是将目前进程的状态置成 TASK_UNINTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头 q,直到资源可获得,q 引导的等待队列被唤醒。 

interruptible_sleep_on()与 sleep_on()函数类似,其作用是将目前进程的状态置成TASK_ INTERRUPTIBLE,并定义一个等待队列,之后把它附属到等待队列头 q,直到资源可获得,q 引导的等待队列被唤醒或者进程收到信号。 

sleep_on() 函 数 应 该 与 wake_up() 成 对 使 用 , interruptible_sleep_on() 应 该 与wake_up_interruptible()成对使用。 

代码清单分别列出了 sleep_on()和 interruptible_sleep_on()函数的源代码。

1  void fastcall _ _sched interruptible_sleep_on(wait_queue_head_t *q) 
2  { 
3   SLEEP_ON_VAR 
4    /* #define SLEEP_ON_VAR          \ 
5      unsigned long flags;           \ 
6      wait_queue_t wait;            \ 
7      init_waitqueue_entry(&wait, current); */ 
8   current->state = TASK_UNINTERRUPTIBLE;//改变当前进程状态 
9   
10  SLEEP_ON_HEAD 
11   /* #define SLEEP_ON_HEAD          \ 
12    spin_lock_irqsave(&q->lock,flags);    \ 
13    _ _add_wait_queue(q, &wait);        \ 
14    spin_unlock(&q->lock);*/ 
15 
16  schedule();//放弃 CPU 
17  SLEEP_ON_TAIL 
18   /* #define SLEEP_ON_TAIL          \ 
19     spin_lock_irq(&q->lock);         \ 
20     _ _remove_wait_queue(q, &wait);      \ 
21     spin_unlock_irqrestore(&q->lock, flags); */ 
22 } 
1  void fastcall _ _sched interruptible_sleep_on(wait_queue_head_t *q) 
2  { 
3   SLEEP_ON_VAR 
4   
5   current->state = TASK_INTERRUPTIBLE;//改变当前进程状态 
6   
7   SLEEP_ON_HEAD 
8   schedule();//放弃 CPU 
9   SLEEP_ON_TAIL 
10 } 

从代码可以看出,不论是 sleep_on() 还 是interruptible_sleep_on(),其流程都如下所示。 

(1)定义并初始化一个等待队列,将进程状态改变为 TASK_UNINTERRUPTIBLE(不能被信号打断)或 TASK_INTERRUPTIBLE(可以被信号打断),并将等待队列添加到等待队列头。 

(2)通过 schedule()放弃 CPU,调度其他进程执行。 

(3)进程被其他地方唤醒,将等待队列移出等待队列头。 在内核中使用set_current_state()函数或_ _add_wait_queue()函数来实现目前进程状态的改变,直接采用 current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句也是可行的。通常而言,set_current_state()函数在任何环境下都可以使用,不会存在并发问题,但是效率要低于_ _add_ wait_queue()。 因此,在许多设备驱动中,并不调用 sleep_on()或 interruptible_sleep_on(),而是亲自进行进程的状态改变和切换,如代码所示。

1  static ssize_t xxx_write(struct file *file, const char *buffer, size_t count, 
2    loff_t *ppos) 
3  { 
4    ... 
5    DECLARE_WAITQUEUE(wait, current); //定义等待队列 
6    _ _add_wait_queue(&xxx_wait, &wait);  //添加等待队列 
7   
8    ret = count; 
9    /* 等待设备缓冲区可写 */ 
10   do 
11   { 
12     avail = device_writable(...); 
13     if (avail < 0) 
14       _ _set_current_state(TASK_INTERRUPTIBLE);//改变进程状态 
15  
16     if (avail < 0) 
17     { 
18       if (file->f_flags &O_NONBLOCK)  //非阻塞 
19       { 
20         if (!ret) 
21           ret =  - EAGAIN; 
22         goto out; 
23       } 
24       schedule();   //调度其他进程执行 
25       if (signal_pending(current))//如果是因为信号唤醒 
26       { 
27         if (!ret) 
28           ret =  - ERESTARTSYS; 
29         goto out; 
30       } 
31     } 
32   }while (avail < 0); 
33  
34   /* 写设备缓冲区 */ 
35   device_write(...) 
36   out: 
37   remove_wait_queue(&xxx_wait, &wait);//将等待队列移出等待队列头 
38   set_current_state(TASK_RUNNING);//设置进程状态为 TASK_RUNNING
39   return ret; 
40 } 

今天就到这里了,O(∩_∩)O~

我的专栏地址:http://blog.csdn.net/column/details/linux-driver-note.html

待续。。。。