linux设备驱动--阻塞IO(续)

来源:互联网 发布:数据处理软件 编辑:程序博客网 时间:2024/04/28 22:34

在《linux设备驱动--阻塞IO》中对阻塞IO的实现做了一个模板式的概述,也给出了内核中等待队列的系列函数接口的使用。但是阻塞IO到底是如何实现的,总觉得还有层窗户纸没戳破,这个让我有点不爽。


        于是再多花点时间戳个小窟窿看看,但是仅仅是个小窟窿,因为进程调度那部分的具体分析还不是现在能够搞定的。但是戳这个小窟窿还是很有意义的,因为kernel并不是窗户纸。。一下子戳不破的,我只有今天戳个窟窿,明天再戳个,这样直到有一天这些窟窿连在了一起,形成了一个大窟窿,那么看kernel就看的更清晰了。


        下面继续练习葵花点穴手了:

         首先,我们在scull_dev结构体中添加了两个成员,一是用来表示读等待的等待队列头—r_wait,另一个是用来表示写等待的等待队列头—w_wait

      init_waitqueue_head(&scull_devices[i].r_wait);      init_waitqueue_head(&scull_devices[i].w_wait);

我们已经知道等待队列是基于内核中广泛使用的双向链表这个很基本的数据结构实现的。所以呢,上面的初始化就是将

struct __wait_queue_head {spinlock_t lock;struct list_head task_list;};typedef struct __wait_queue_head wait_queue_head_t;

中的task_list中的前驱指针和后驱指针指向自己。

既然是表结构,那就有表头和项。上面说了等待队列头的初始化,下面就是等待队列项的初始化了。


scull_read函数中,利用

  1. DECLARE_WAITQUEUE(wait,current);宏定义来初始化等待队列项。

比如说我们用cat/dev/scull来进行读操作,那这个current就是这个进程。


等待队列项的结构体如下:

struct __wait_queue {unsigned int flags;#define WQ_FLAG_EXCLUSIVE0x01void *private;wait_queue_func_t func;struct list_head task_list;};

   在

#define __WAITQUEUE_INITIALIZER(name, tsk) {\.private= tsk,\.func= default_wake_function,\.task_list= { NULL, NULL } }#define DECLARE_WAITQUEUE(name, tsk)\wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)

中,将名为wait的等待队列项的的private指针指向当前调用进程的task_struct任务结构体。

wait_queue_func_t函数指针指向default_wake_functiondefault_wake_function函数很重要,因为因当前资源不可得到而阻塞,引起当前进程的休眠,那么当资源可得时,这个函数就是用来唤醒进程的。同样的初始化内嵌的list_head结构体。



2add_wait_queue(&dev->r_wait,&wait);

将这个等待队列项加入到r_wait这个等待队列头中,实际上是双向链表的操作。


经过这一步后,这个等待队列头上就挂了一个等待队列项,这个等待队列项包含当前进程,用于唤醒的函数指针。



3、对当前资源进行判断,如果dev->current_len等于0,那么表明资源不可用,调用scull_read的进程应该进入阻塞状态:

__set_current_state(TASK_INTERRUPTIBLE);设置进程为休眠状态。

然后利用schedule();函数对系统中的进程进行调度。



在我们的scullfifo的设计中,read是要靠write唤醒的。只有write有内容,read才有的读。


那么我们用echo“godlike” > /dev/scull来进行写操作,他将怎么唤醒cat/dev/scull这个读进程呢?



scull_write中,同样地初始化一个等待队列项,该队列项的private指针指向写进程的task_struct任务结构体。

然后将这个等待队列项加入到w_wait写等待队列头的链表中。


因为scull_dev设备的初始当前长度肯定是0,所以这个时候设备是不会写阻塞的。


if(copy_from_user(dev->mem + dev->current_len, buf, count)){ret = -EFAULT;goto out;}else{dev->current_len += count;ret = count;printk(KERN_WARNING "write %d bytes current_len: %d\n", count , dev->current_len);wake_up_interruptible(&dev->r_wait);}

在写成功后,会wake_up_interruptible(&dev->r_wait);


这里其实就是沿着r_wait这个写等待队列头后面挂的等待队列项走一遍,对每个等待队列项都调用在前面看到

#define __WAITQUEUE_INITIALIZER(name, tsk) {\.private= tsk,\.func= default_wake_function,\.task_list= { NULL, NULL } }

default_wake_function函数来唤醒每个等待队列项所代表的进程。



这样,再进行dev->current_len== 0的判断时就为假,不会阻塞导致休眠了,cat/dev/scull进程就唤醒了,就可以看到写的内容了。



这里为什么有个while(dev->current_len== 0)来判断资源是否可用?


我的理解是这样的:如果有多个读进程阻塞在这里,比如说两个读进程(一个用cat读,一个自己写的应用程序:app_read读)阻塞在这里。那就有两个等待队列项挂在r_wait这个读等待队列头中。那么刚才的写进程就会唤醒这两个读进程,那在调度这两个读进程时,肯定一个先读(哪个先读应该由系统的进程调度算法决定吧),那么先读的就输出了”godlike”,并将dev->current_len-=count;中的current_len又变为0了,这样后来的的进程发现current_len又是0,那么只能再次阻塞了,等待下一次了。



做个测试看看:

运行两个读进程:


再运行写进程如下:


第一次写”godlike”,是调度的./app_read,因为输出是read8 typesgodlike;输出后./app_read进程结束;这次调度没调到cat,所以cat还是阻塞。

第二次写”godlike”,调度的是cat/dev/scull。这次肯定是cat被调用,因为app_read已经结束了,并且在读等待队列头中已经删除了等待队列项。


经过多次测试,第一次写时,重新调度的可能是cat,也可能是./app_read


在进行有关进程调度的分析时,我的kgdb不好用了(kgdb分析过文件系统的module,那时候用着还可以),不能做明确的验证测试工作,只能凭着自己对代码的阅读作出分析。如果分析有什么问题,欢迎大家指出讨论。或者如果有什么其他debugkernel的方法,希望告知我,^_^





原创粉丝点击