Linux 设备驱动中的阻塞与非阻塞 I/O
概念1:阻塞与非阻塞
阻塞是指在执行设备操作时,若不能获得资源则挂起进程,同时将CPU 礼让给其他进程使用,被挂起的进程进入休眠态,被从调度器的运行队列移走,直到条件被满足,它又将被调度器调度进来,再次判断能否获得资源。
而非阻塞在获取不到资源时并不挂起,它会不停的查询,直到它的时间片用完(放弃,等待下一次调度)为止,这样反而占用CPU 。
概念2:进程的休眠
休眠(被阻塞)进程被标志为一个特殊的不可执行状态,并从调度器的运行队列中移走。
进程休眠有各种原因,但肯定都是为了等待一些事件。事件可能是一段时间、从文件I/O读更多数据,或者是某个硬件事件。
休眠有两种相关的进程状态:TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE。它们的惟一区别是处于TASK_UNINTERRUPTIBLE 状态的进程不会被信号打断,而处于TASK_INTERRUPTIBLE 状态的进程可以被信号打断,从而唤醒并处理信号(然后再次进入等待睡眠状态)。
概念3:等待队列
休眠进程可以使用等待队列(wait queue)来唤醒。
等待队列是由等待某些事件发生的进程组成的简单链表。内核用 wake_queue_head_t 来代表等待队列。
等待队列可以通过DECLARE_WAITQUEUE_HEAD() 静态创建,也可以用 init_waitqueue_head() 动态创建。
概念4:在等待队列上休眠
在Linux 设备驱动中,可以通过sleep_on() 和 interruptible_sleep_on() 来使进程在等待队列上休眠。
代码清单:sleep_on () 函数
1 void __sched sleep_on(wait_queue_head_t *q)
2 {
3 sleep_on_common(q,TASK_UNINTERRUPTIBLE,MAX_SCHEDULE_TIMEOUT);
4 }
5
6 static long __sched sleep_on_common(wait_queue_head_t *q,int state,long timeout)
7 {
8 unsigned long flags;
9 wait_queue_t wait;
10
11 init_waitqueue_entry(&wait,current);
12
13 __set_current_state(state);
14
15 spin_lock_irqsave(&q->lock,flags);
16 __add_wait_queue(q,&wait); /* 加入等待队列 */
17 spin_unlock(&q->lock);
18 timeout = schedule_timeout(timeout); /* 进程切换 */
19 spin_lock_irq(&q->lock);
20 __remove_wait_queue(q,&wait); /* 移出等待队列 */
21 spin_unlock_irqrestore(&q->lock,flags);
22
23 return timeout;
24 }
代码清单2:interruptible_sleep_on() 函数
1 void __sched interruptible_sleep_on(wait_queue_head_t *q)
2 {
3 sleep_on_common(q,TASK_INTERRUPTIBLE,MAX_SCHEDULE_TIMEOUT);
4 }
由两个代码清单可以看出,不论是sleep_on() 函数还是interruptible_sleep_on() 函数,都会调用sleep_on_common() 函数,其流程如下:
(1)定义并初始化一个等待队列,将进程状态切换为TASK_UNINTERRUPTIBLE 和 TASK_INTERRUPTIBLE ,并将等待队列添加至等待队列头;
(2)通过schedule_timeout() 放弃CPU ,调度其他进程执行;
(3)进程被其他地方唤醒,将等待队列移出等待队列头。
概念5:进程状态切换
从上面的代码清单中,我们已经知道进程状态切换的函数是:set_current_state() 。
值得注意的是:在内核中通常使用set_current_state() 或 __add_current_state() 函数来实现目前进程状态的改变,还有一种方法就是采用:current->state = TASK_UNINTERRUPTIBLE 类似的赋值语句。
一般说来,set_current_state() 在任何环境下都可以使用,不会存在并发问题,但是效率要低于__add_current_state() 。
进程切换发生在schedule()函数中。
代码清单3: 在驱动程序中改变进程状态并调用schedule()
1 static ssize_t xxx_write(strcut file *file,const char *buffer,size_t count,loff_t *ppos)
2 {
3 ...
4 DECLARE_WAITQUEUE(wait,current); /* 定义等待队列 */
5 add_wait_queue(&xxx_wait,&wait); /* 添加等待队列 */
6
7 ret = count; /* 等待设备缓冲区可写 */
8 do {
9 avail = device_writable(...);
10 if (avail < 0)
11 __set_current_state(TASK_INTERRUPTIBLE); /*改变进程状态 */
12 if (avail < 0){
13 if(file->f_flags &O_NONBLOCK) {/* 非阻塞 */
14 if (!ret)
15 ret = - EAGAIN;
16 goto out;
17 }
18 schedule(); /* 调度其他进程执行 */
19 if (signal_pending(current)) {/* 如果是因为信号唤醒 */
20 if (!ret)
21 ret = - ERESTARTSYS;
22 goto out;
23 }
24 }
25 }while (avail < 0);
26
27 /* 写设备缓冲区 */
28 device_write(...);
29 out:
30 remove_wait_queue(&xxx_wait, &wait); /* 将等待队列移出等待队列头 */
31 set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
32 return ret;
33 }