这周主要学习了设备驱动开发详解中的第八章阻塞与非阻塞 I/O , 不过对于阻塞和非阻塞部分总体上理解的还很肤浅,对于其中所涉及到的好几个头文件以及源代码之间的关联还缺乏很大程度的了解。
对这部分的学习涉及到的文件大致有: wait.h 、 wait.c 、 sched.c 、 spinlock.h 和 list.h 等。我觉得在学习过程中首先应该把所要用到的几个类型搞清楚,包括等待队列头:
struct __wait_queue_head {
spinlock_t lock;
struct list_head task_list;
};
typedef struct __wait_queue_head wait_queue_head_t;
和等待队列节点:
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;
};
队列头中只包含了一个自旋锁和一个 list_head 成员,而队列节点则有等待标志,以表示任务是采用互斥访问资源还非互斥访问资源方式,另外还有一个普通指针,我们可以随意的给这个指针赋任何类型,这也大大的提高了队列的灵活性,另外还用 wait_queue_func_t 函数指针定义了一个指针变量 fuc ,最后是一个双向循环链表的成员。 接下来就是通过对等待队列头和等待队列节点的初始化来完成等待队列的构建,从上来的结构体也能看出头和节点结构之间的差异,所以要分别进行初始化。 wait.h 头文件中定义了两个内部宏来分别初始化它们,并且又定义了两个外部宏留作外部调用,在队列初始化宏里将 tsk 指针赋给了 private ,将 default_wake_function 赋给了 fuc ,这个函数在 sched.h 中调用了 try_to_wake_up 。同时也可以采用函数来初始化等待队列头,这其中声明了一个包含在 wait.c 源文件中的外部函数 __init_waitqueue_head() ,头文件中使用了 init_waitqueue_head() 来调用这个外部函数。
接下来就是添加和移除等待队列了,内核定义了一系列内联函数以供内部调用如: __add_wait_queue() 、 __add_wait_queue_exclusive 等, 这些函数调用了 list_add() 或 list_add_tail() 等将新结点插入到队列头和第一个结点之间或队尾,外部则调用 add_wait_queue() 、 add_wait_queue_exclusive() 等函数留作接口,移除则只定义了一个内部调用函数 __remove_wait_queue() ,外部接口函数则是 remove_wait_queue() ,这些接口函数都在 wait.c 源文件中,内联函数和带参数的宏定义都有异曲同工之妙,避免了函数调用的频繁分配、回收栈空间以及进出栈的时空开销,编译器在遇到 inline 关键字时会生将内联函数生成一段代码,遇到函数调用就用这段代码来替换,其参数个数不受限,这一点比带参数的宏定义有优势。
睡眠状态有两种, 分为深度睡眠和浅度睡眠,睡眠函数为此也要分为两类,一类函数名前面有 interruptible ,另一类则没有,共有四个睡眠函数: interruptible_sleep_on() 、 interruptible_sleep_on_timeout() 、 sleep_on() 、 sleep_on_timeout() ,这些都在 sched.h 中有定义, wait.h 中则只是声明成外部函数而已,这四个函数最终都是调用 sleep_on_common(), 这个睡眠公共函数有三个参数,分别对应等待队列头、睡眠状态、睡眠时间,不加 _timeout 后缀的函数的第三个参数采用 MAX_SCHEDULE_TIMEOUT 来指定最大调度的超时时间, sleep_on_common() 函数代码如下:
static long __sched
sleep_on_common(wait_queue_head_t *q, int state, long timeout)
{
unsigned long flags;
wait_queue_t wait;
init_waitqueue_entry(&wait, current);
__set_current_state(state);
spin_lock_irqsave(&q->lock, flags);
__add_wait_queue(q, &wait);
spin_unlock(&q->lock);
timeout = schedule_timeout(timeout);
spin_lock_irq(&q->lock);
__remove_wait_queue(q, &wait);
spin_unlock_irqrestore(&q->lock, flags);
return timeout;
}
这个函数并非是连续执行的,代码第三行调用的函数用来初始化当前等待队列项,包括设标志、赋 tsk 结构、以及唤醒方式,接下来改变进程状态,关中断并保存状态字,将当前进程 ( 线程 ) 加入等待队列最前面,开中断,设置超时时间,此时处理器可以调度其它进程并执行,当进程 ( 线程 ) 到期被唤醒后再关中断,将当前进程 ( 线程 ) 移出等待队列,最后是开中断并恢复状态,并返回超时时间。
唤醒队列采用调用 wait.h 中的诸如 wake_up() 的一系列函数,而这一系列的函数都采用宏定义,而最终调用的是 __wait_up(), 只是传递的参数不同, __wait_up(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, void *key) 函数有四个参数,第一个是等待队列头,第二个是睡眠方式,深度的还是浅度的睡眠,第三个参数是唤醒进程的个数,第四个参数用以传递任意的指针, __wait_up() 函数又调用了 __wait_up_common() 函数 ( 它才是最终的落实者 ) ,其函数原型及代码如下:
static void __wake_up_common(wait_queue_head_t *q, unsigned int mode,
int nr_exclusive, int wake_flags, void *key)
{
wait_queue_t *curr, *next;
list_for_each_entry_safe(curr, next, &q->task_list, task_list) {
unsigned flags = curr->flags;
if (curr->func(curr, mode, wake_flags, key) &&
(flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
break;
}
等待队列会将所有的非互斥访问进程排在前面,互斥访问进程排在后面,代码执行过程是随着等待队列的遍历和唤醒,在遍历的过程中取出进程的访问方式,将其和 1 按位与,当 nr_exclusive 为 0 时函数则会将整个等待队列遍历一遍,而当其值为 1 时则会唤醒等待队列中所有的非互斥进程和一个互斥进程。
Linux 内核中最简单的休眠方式是称为 wait_event 的宏 ( 及其变种 ) ,它实现了休眠和进程等待的条件的检查内核中提供了四个基本的函数, wait_event(wq,condition) 、 wait_event_interruptible(wq,condition) 、 wait_event_timeout(wq,condition,timeout) 、 wait_event_interruptible_timeout(wq,condition,timeout) 第一个是参数是等待队列头,第二个是条件,如果条件满足的话则返回,否则阻塞,当加上 _timeout 后的宏意味着阻塞等待的超时时间,以 jiffy 为单位,当超时到达时,不论 condition 是否满足,均返回。 wait_event() 要与 wake_up() 成对使用,其他的等待事件函数也要与相应的唤醒函数成对使用。