并发及其管理
1、并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race condition)。
2、在设计自己的驱动程序时,第一个要记住的规则是,只要可能,就应该避免资源的共享。如果没有并发的访问,也就不会有竞态的产生。因此,仔细编写的内核代码应具有最少的共享。这种思想的最明显应用就是避免使用全局变量。
但是资源的共享是不可避免的,如硬件资源的本质就是共享、指针传递等。
3、资源共享的硬规则:
(1)在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源的访问。
(2)当内核代码创建了一个可能和其他内核部分共享的对象时,该对象必须在还有其他组件引用自己时保持存在(并正确工作)。
信号量和互斥体
接下来研究如何为scull添加锁定。我们的目的是使对scull数据结构的访问是原子的,这意味着在涉及到其他执行线程之前,整个操作就已经结束了。
访问共享资源的代码区域称为临界区(critical section)。
一个信号量本质上是一个整数值,它和一对函数联合使用,这一对函数通常称为P和V。
希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小一,而进程可以继续。相反,如果信号量的值为零(或者更小),进程必须等待直到其他人释放该信号量。对信号量的解锁通过调用V完成:该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同时在一个临界区中运行),信号量的值应初始化为1。这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量也称为一个“互斥体”(mutex),它是互斥(mutual exclusion)的简称,在Linux内核中几乎所有的信号量均用于互斥。
Linux信号量的实现
要使用信号量,内核代码必须包含<asm/semaphore.h>。相关的类型是struct semaphore。
Linux内核中与信号量相关的操作:
1、定义信号量
1 struct semaphore sem;
2、初始化信号量
直接创建信号量:
1 void sema_init(struct semaphore *sem, int val);
以计数值val初始化信号量sem。可将信号量初始化为大于1的值从而成为一个计数信号量。
运行时动态分配互斥信号量:
1 init_MUTEX(struct semaphore *sem);
以计数值1初始化动态创建的互斥信号量(二值信号量)。
1 init_MUTEX_LOCKED(struct semaphore *sem);
以计数值0初始化动态创建的互斥信号量。初始为加锁状态。
声明和初始化一个互斥信号量(声明+初始化宏):
1 DECLARE_MUTEX(name);
定义并以计数值1初始化信号量。DECLARE_MUTEX(name)实际上是定义一个semaphore,所以它的使用应该对应信号量的P,V函数。
3、获得信号量
P函数被称为down—或者这个名字的其他变种。这里,“down”指的是该函数减小了信号量的值,它也许会将信号量置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。down有三个版本:
1 void down(struct semaphore *sem);
减小信号量的值,并在必要时一直等待,也会导致睡眠,不能用在中断上下文中。
1 int down_interruptible(struct semaphore *sem);
完成相同的工作,但操作是可中断的,即在等待信号量的过程中可被信号中断。要小心,如果操作被中断,该函数会返回非零值,而调用者不会拥有该信号量。对其的正确使用需要始终检查返回值,并作出相应的响应。
1 int down_trylock(struct semaphore *sem);
永远不会睡眠。如果信号量在调用时不可获得,其会立即返回一个非零值。
当线程成功调用上述down的某个版本后,就称为该线程“拥有”了信号量,这样,该线程就被赋予访问由该信号量保护的临界区的权利。
4、 释放信号量
当互斥操作完成后,必须返回该信号量。Linux等价于V的函数是up:
1 void up(struct semaphore *sem);
调用up之后,调用者不再拥有该信号量。
任何拿到信号量的线程都必须通过一次(只有一次)对up的调用而释放该信号量。
在出现错误的情况下,经常需要特别小心;如果在拥有一个信号量时发生错误,必须在将错误状态返回给调用者之前释放该信号量。
互斥体
“互斥体(mutex)”这个称谓所指的是任何可以睡眠的强制互斥锁,比如使用计数是1的信号量。但在最新的Linux内核中,“互斥体(mutex)”这个称谓现在也用于一种实现互斥的特定睡眠锁。也就是说,互斥体是一种互斥信号。
mutex在内核中对应的数据结构是mutex,其行为和使用计数为1的信号量类似,但操作接口更简单,实现也更高效,而且使用限制更强。使用互斥体需包含<linux/mutex.h>。
1、定义及初始化互斥体
静态定义互斥体(声明+初始化宏)
1 DEFINE_MUTEX(mutexname);
运行时动态初始化互斥体
1 mutex_init(&mutex);
2、获取互斥体
1 void mutex_lock(struct mutex *lock); 2 int mutex_lock_interruptible(struct mutex *lock); 3 int mutex_trylock(struct mutex *lock);
上述函数的操作行为和信号量的down有类似之处,_try函数永不睡眠。
3、释放互斥体
1 void mutex_unlock(struct mutex *lock);
4、mutex的使用
1 struct mutex my_mutex; // 定义mutex 2 mutex_init(&my_mutex); // 初始化mutex 3 4 mutex_lock(&my_mutex); // 获取mutex 5 /* 临界区 */ 6 mutex_unlock(&my_mutex); // 释放mutex
mutex的使用方法和信号量用于互斥的场合完全一样。
在scull中使用信号量
正确使用锁定机制的关键是,明确指定需要保护的资源,并确保每一个对这些资源的访问都正确使用了锁。
在我们的实例程序中,所有的信息都包含在scull_dev结构体中,因此,该结构体就是我们锁定机构的逻辑范围:
1 /* 定义scull_dev结构体用来描述scull设备 */ 2 struct scull_dev { 3 struct scull_qset *data; /* 指向第一个scull_qset结构体 */ 4 int quantum; /* 量子大小,量子也是指针,指向的内存区域大小即为quantum */ 5 int qset; /* 量子集大小(指针数组元素个数),量子集即指针数组,其元素即量子 */ 6 unsigned long size; /* 数据总量,动态量,使用时由写入数据总量决定 */ 7 unsigned int access_key; /* used by sculluid and scullpriv */ 8 struct semaphore sem; /* 互斥信号量 */ 9 struct cdev cdev; /* 字符设备结构 */ 10 };
scull例程中,为每个设备都使用单独的信号量,允许不同设备上的操作可以并行处理。从而可以提高性能。
信号量使用前的初始化:
1 /* Initialize each device. */ 2 /* 初始化每个设备的访问区块--struct scull_dev结构体 */ 3 for (i = 0; i < scull_nr_devs; i++) { 4 scull_devices[i].quantum = scull_quantum;/* 设定量子大小 */ 5 scull_devices[i].qset = scull_qset;/* 设定量子集大小 */ 6 init_MUTEX(&scull_devices[i].sem);/* 初始化互斥信号量 */ 7 scull_setup_cdev(&scull_devices[i], i);/* 向内核注册字符设备 */ 8 }
信号量必须在scull设备对其他设备可用之前被初始化。
读取者/写入者信号量
信号量对所有的调用者互斥,而不管每个线程到底想做什么。
允许多个并发的读取者是可能的,Linux内核为这种情形提供了一种特殊的信号量类型,称为“rwsem”(或者reader/write semaphore,读取者/写入者信号量)。使用rwsem的代码必须包含<linux/rwsem.h>,rwsem相关的数据类型是struct rw_semaphore。
1、初始化rwsem
1 init_rwsem(struct rw_semaphore *sem);
2、只读访问,可用接口
1 void down_read(struct rw_semaphore *sem); 2 int down_read_trylock(struct rw_semaphore *sem); 3 void up_read(struct rw_semaphore *sem);
3、针对写入者的接口
1 void down_write(struct rw_semaphore *sem); 2 int down_write_trylock(struct rw_semaphore *sem); 3 void up_write(struct rw_semaphore *sem); 4 /* downgrade write lock to read lock */ 5 void downgrade_write(struct rw_semaphore *sem);
在结束修改之后,可以调用downgrade_write,来允许其他读取者的访问。
一个rwsem可允许一个写入者或无限多个读取者拥有该信号量。写入者具有更高优先级,其有可能导致读取者“饿死”。最好在很少需要写访问且写入者只会短期拥有信号量的时候使用rwsem。
completion
信号量用于同步
如果信号量被初始化为0,则它可以用于同步,同步意味着一个执行的继续执行需等待另一执行单元完成某事,保证执行的先后顺序。
完成量用于同步
Linux内核提供了一种更好的同步机制,即完成量(completion),完成量允许一个线程告诉另一个线程某个工作已经完成,其声明在<linux/completion.h>中。
1、创建和初始化completion
1 DECLARE_COMPLETION(my_completion); // 定义+初始化
动态创建和初始化完成量
1 struct completion my_completion; 2 void init_completion(&my_completion);
2、等待完成量
1 void wait_for_completion(struct completion *);
执行一个非中断的等待,如果调用了wait_for_completion且没有人会完成该任务,则会产生一个不可杀的进程。
3、唤醒完成量
1 void complete(struct completion *); //唤醒一个等待进程 2 void complete_all(struct completion *); // 唤醒所有等待进程
一个completion通常是个单次(one-shot)设备,它只会被使用一次,然后被丢弃。如果没有使用completion_all,则我们可以重复使用一个completion结构,但是,如果使用了completion_all,则必须在重复使用该结构体前重新初始化它。下面这个宏用来快速执行重新初始化:
1 INIT_COMPLETION(struct completion c);
4、完成量用于同步
自旋锁
自旋锁(spinlock)可在不能睡眠的代码中使用,比如中断例程。
一个自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的单个位。希望获得某特定锁的代码测试相关的位。如果锁可用,则“锁定”位被设置,而代码继续进入临界区。相反,如果锁被其他人获得,则代码进入忙循环并重复检查这个锁,直到该锁可用为止,这个循环就是自旋锁的“自旋”部分。
“测试并设置”的操作必须以原子的方式完成。
适用于自旋锁的核心规则是:
1、任何拥有自旋锁的代码都必须是原子的。它不能休眠,事实上,它不能因为任何原因放弃处理器,除了服务中断以外(某些情况下也不能放弃CPU,如果在中断服务例程中,也需要该自旋锁,则会发生“死锁”,因此,在拥有自旋锁时会禁止本地CPU的中断)。任何时候,只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。当我们编写在自旋锁下执行的代码时,必须注意每一个所调用的函数,他们不能休眠。
2、自旋锁必须在可能的最短时间内拥有。拥有自旋锁的时间越长,其他处理器不得不自旋的时间就越长,而它不得不自旋的可能性就越大。
自旋锁API
要使用自旋锁原语,需要包含头文件<linux/spinlock.h>。
1 spinlock_t my_lock = SPIN_LOCK_UNLOCKED;/* 编译时初始化spinlock*/ 2 void spin_lock_init(spinlock_t *lock);/* 运行时初始化spinlock*/ 3 4 /* 所有spinlock等待本质上是不可中断的,一旦调用spin_lock,在获得锁之前一直处于自旋状态*/ 5 void spin_lock(spinlock_t *lock);/* 获得spinlock*/ 6 void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);/* 获得spinlock,禁止本地cpu中断,保存中断标志于flags*/ 7 void spin_lock_irq(spinlock_t *lock);/* 获得spinlock,禁止本地cpu中断*/ 8 void spin_lock_bh(spinlock_t *lock)/* 获得spinlock,禁止软件中断,保持硬件中断打开*/ 9 10 /* 以下是对应的锁释放函数*/ 11 void spin_unlock(spinlock_t *lock); 12 void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags); 13 void spin_unlock_irq(spinlock_t *lock); 14 void spin_unlock_bh(spinlock_t *lock); 15 16 /* 以下非阻塞自旋锁函数,成功获得,返回非零值;否则返回零*/ 17 int spin_trylock(spinlock_t *lock); 18 int spin_trylock_bh(spinlock_t *lock); 19 20 21 /*新内核的<linux/spinlock.h>包含了更多函数*/
读取者/写入者自旋锁
允许任意数量的读取者进入临界区,但写入者必须互斥访问。读取者/写入者具有rwlock_t类型,在<linux/spinkock.h>中定义。
读取者/写入者自旋锁API
1 rwlock_t my_rwlock = RW_LOCK_UNLOCKED;/* 编译时初始化*/ 2 3 4 rwlock_t my_rwlock; 5 rwlock_init(&my_rwlock); /* 运行时初始化*/ 6 7 8 void read_lock(rwlock_t *lock); 9 void read_lock_irqsave(rwlock_t *lock, unsigned long flags); 10 void read_lock_irq(rwlock_t *lock); 11 void read_lock_bh(rwlock_t *lock); 12 13 14 void read_unlock(rwlock_t *lock); 15 void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 16 void read_unlock_irq(rwlock_t *lock); 17 void read_unlock_bh(rwlock_t *lock); 18 19 /* 新内核已经有了read_trylock */ 20 21 void write_lock(rwlock_t *lock); 22 void write_lock_irqsave(rwlock_t *lock, unsigned long flags); 23 void write_lock_irq(rwlock_t *lock); 24 void write_lock_bh(rwlock_t *lock); 25 int write_trylock(rwlock_t *lock); 26 27 28 void write_unlock(rwlock_t *lock); 29 void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 30 void write_unlock_irq(rwlock_t *lock); 31 void write_unlock_bh(rwlock_t *lock); 32 33 /*新内核的<linux/spinlock.h>包含了更多函数*/
在Linux使用读-写自旋锁时,这种锁机制照顾照顾读比写要多一点。当读锁被持有时,写操作为了互斥访问只能等待,但是读者可以继续成功占有锁。而自旋等待的写者在所有读者释放锁之前是无法获得锁的。所以,大量读者必然使挂起的写者处于饥饿状态。
如果加锁时间不长且代码不会睡眠(比如中断处理程序),利用自旋锁是最佳选择。如果加锁时间可能很长或者代码在持有锁时有可能睡眠,那么最好使用信号量来完成加锁功能。
陷阱锁
锁定模式的设置必须在一开始就要设置好,否则其后的改进会非常困难。
不明确的规则
信号量和自旋锁是不可递归的。在scull中,我们的设计规则是:由系统调用直接调用的那些函数均要获得信号量,以便保护要访问的设备结构。而其他的内部函数,只会由其他的scull函数调用,则假定信号量已被正确获取。
锁的顺序规则
必须获取多个锁时,应该始终以相同的顺序获得。
有帮助的两个规则是:
1、 如果必须要获得一个局部锁(比如一个设备锁),以及一个属于内核更中心位置的锁,则应该首先获取自己的局部锁。
2、 如果我们拥有自旋锁和信号量的组合,则必须首先获得信号量。
细粒度和粗粒度的对比
设备驱动程序中的锁通常相对直接,可以用单个锁来处理所有的事情,或者可以为每个设备建立一个锁。作为通常规则,我们应该在最初使用粗粒度的锁。
除了锁之外的办法
在某些情况下,原子的访问不需要完整的锁。
免锁算法
经常用于免锁的生产者/消费者任务的数据结构之一是循环缓冲区(circular buffer)。循环缓冲区的使用在设备驱动程序中相当普遍。特别是网络适配器,经常使用循环冲区和处理器交换数据。在Linux内核中,有一个通用的循环缓冲区实现,有关其使用可参阅<linux/kfifo.h>。
原子变量
有时,共享的资源可能恰好是一个简单的整数,完整的锁机制对一个简单的整数来讲显得有些浪费。针对这种情况,内核提供了一种原子的整数类型,称为atomic_t,定义在<ams/atomic.h>中。
一个atomic_t变量在所有内核支持的架构上保存了一个int值。但是,由于某些处理器上这种数据类型的工作方式有些限制,因此不能使用完整的整数范围,也就是说,在atomic_t变量中不能记录大于24位的整数。原子操作速度非常快,因为只要可能,它们就会被编译成单个机器指令。
原子变量操作函数:
1 void atomic_set(atomic_t *v, int i); /*设置原子变量 v 为整数值 i.*/ 2 atomic_t v = ATOMIC_INIT(0); /*编译时使用宏定义 ATOMIC_INIT 初始化原子值.*/ 3 4 int atomic_read(atomic_t *v); /*返回 v 的当前值.*/ 5 6 void atomic_add(int i, atomic_t *v);/*由 v 指向的原子变量加 i. 返回值是 void*/ 7 void atomic_sub(int i, atomic_t *v); /*从 *v 减去 i.*/ 8 9 void atomic_inc(atomic_t *v); 10 void atomic_dec(atomic_t *v); /*递增或递减一个原子变量.*/ 11 12 int atomic_inc_and_test(atomic_t *v); 13 int atomic_dec_and_test(atomic_t *v); 14 int atomic_sub_and_test(int i, atomic_t *v); 15 /*进行一个特定的操作并且测试结果; 如果, 在操作后, 原子值是 0, 那么返回值是真; 否则, 它是假. 注意没有 atomic_add_and_test.*/ 16 17 int atomic_add_negative(int i, atomic_t *v); 18 /*加整数变量 i 到 v. 如果结果是负值返回值是真, 否则为假.*/ 19 20 int atomic_add_return(int i, atomic_t *v); 21 int atomic_sub_return(int i, atomic_t *v); 22 int atomic_inc_return(atomic_t *v); 23 int atomic_dec_return(atomic_t *v); 24 /*像 atomic_add 和其类似函数, 除了它们返回原子变量的新值给调用者.*/
atomic_t类型数据必须只能通过上面的函数来访问。如果将原子变量传递给了需要整型参数的函数,则会遇到编译错误。只有原子变量的数目是原子的,atomic_t变量才能正常工作,需要多个atomic_t变量的操作,仍然需要某种类型的锁。
原子位操作
为了实现位操作,内核提供了一组可原子地修改和测试单个位的函数。
原子位操作非常快,只要底层硬件允许,这种操作就可以使用单个机器指令来执行,并且不需要禁止中断。这些函数依赖于具体的架构,因此在<asm/bitops.h>中声明。即使是在SMP计算机上,这些函数也可确保为原子的,因此,能提供跨处理器的一致性。
这些函数使用的数据类型也是依赖于具体架构的。nr参数(用来描述要操作的位)通常被定义为int,但在少数架构上被定义为unsigned long。要修改的地址通常是指向unsigned long指针,但在某些架构上却使用void *来代替。
可用的位操作如下:
1 void set_bit(nr, void *addr); /*设置第 nr 位在 addr 指向的数据项中。*/ 2 3 void clear_bit(nr, void *addr); /*清除指定位在 addr 处的无符号长型数据.*/ 4 5 void change_bit(nr, void *addr);/*翻转nr位.*/ 6 7 test_bit(nr, void *addr); /*这个函数是唯一一个不需要是原子的位操作; 它简单地返回这个位的当前值.*/ 8 9 /*以下原子操作如同前面列出的, 除了它们还返回这个位以前的值.*/ 10 11 int test_and_set_bit(nr, void *addr); 12 int test_and_clear_bit(nr, void *addr); 13 int test_and_change_bit(nr, void *addr);
seqlock
2.6内核包含了一对新机制打算来提供快速地,无锁地存取一个共享资源。seqlock要保护的资源小,简单,并且常常被存取,并且很少写存取但是必须要快。seqlock 通常不能用在保护包含指针的数据结构。seqlock 定义在<linux/seqlock.h> 。
1 /*两种初始化方法*/ 2 seqlock_t lock1 = SEQLOCK_UNLOCKED; 3 4 seqlock_t lock2; 5 seqlock_init(&lock2);
这个类型的锁常常用在保护某种简单计算,读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作。在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试。读者代码形式:
1 unsigned int seq; 2 do { 3 seq = read_seqbegin(&the_lock); 4 /* Do what you need to do */ 5 } while read_seqretry(&the_lock, seq);
如果你的 seqlock可能从一个中断处理里存取,你应当使用IRQ安全的版本来代替:
1 unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags); 2 int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);
写者必须获取一个排他锁来进入由一个seqlock保护的临界区,写锁由一个自旋锁实现,调用:
1 void write_seqlock(seqlock_t *lock); 2 void write_sequnlock(seqlock_t *lock);
因为自旋锁用来控制写存取, 所有通常的变体都可用:
1 void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags); 2 void write_seqlock_irq(seqlock_t *lock); 3 void write_seqlock_bh(seqlock_t *lock); 4 5 void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags); 6 void write_sequnlock_irq(seqlock_t *lock); 7 void write_sequnlock_bh(seqlock_t *lock);
还有一个write_tryseqlock在它能够获得锁时返回非零。
读取-复制-更新
读取-拷贝-更新(RCU) 是一个高级的互斥方法, 在合适的情况下能够有高效率。它在驱动中的使用很少。使用RCU的代码须包含<linux/rcupdate.h>。
开发板实验
代码参考了Tekkaman
的,然后加入了自己理解的一些注释。
实验板是256M的mini2440。
模块程序链接:http://files.cnblogs.com/ycz9999/complete.zip
模块测试程序链接:http://files.cnblogs.com/ycz9999/complete_test.zip
注意
:
在实验时,如果先执行读进程,其会阻塞,使得实验无法继续进行。因此在执行读取进程时,在其末尾加上&,使其在后台运行!
[root@FriendlyARM complete]# ls complete.ko completion_testr completion_testw [root@FriendlyARM complete]# insmod complete.ko [root@FriendlyARM complete]# echo 8 > /proc/sys/kernel/printk [root@FriendlyARM complete]# cat /proc/devices Character devices: 1 mem 4 /dev/vc/0 4 tty 5 /dev/tty 5 /dev/console 5 /dev/ptmx 7 vcs 10 misc 13 input 14 sound 21 sg 29 fb 81 video4linux 89 i2c 90 mtd 116 alsa 128 ptm 136 pts 180 usb 188 ttyUSB 189 usb_device 204 s3c2410_serial 253 complete 254 rtc Block devices: 259 blkext 7 loop 8 sd 31 mtdblock 65 sd 66 sd 67 sd 68 sd 69 sd 70 sd 71 sd 128 sd 129 sd 130 sd 131 sd 132 sd 133 sd 134 sd 135 sd 179 mmc [root@FriendlyARM complete]# mknod -m 666 /dev/complete c 253 0 [root@FriendlyARM complete]# ls complete.ko completion_testr completion_testw [root@FriendlyARM complete]# ./completion_testr& [root@FriendlyARM complete]# process 745 (completion_test) going to sleep [root@FriendlyARM complete]# ./completion_testr& [root@FriendlyARM complete]# process 746 (completion_test) going to sleep [root@FriendlyARM complete]# ps PID USER VSZ STAT COMMAND 1 root 3068 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [events/0] 5 root 0 SW [khelper] 11 root 0 SW [async/mgr] 209 root 0 SW [sync_supers] 211 root 0 SW [bdi-default] 213 root 0 SW [kblockd/0] 222 root 0 SW [khubd] 228 root 0 SW [kmmcd] 244 root 0 SW [rpciod/0] 251 root 0 SW [kswapd0] 298 root 0 SW [aio/0] 302 root 0 SW [nfsiod] 306 root 0 SW [crypto/0] 416 root 0 SW [mtdblockd] 580 root 0 SW [scsi_eh_0] 581 root 0 SW [usb-storage] 635 root 0 SW [usbhid_resumer] 701 root 3068 S syslogd 704 root 3328 S /usr/sbin/inetd 708 root 1940 S /usr/sbin/boa 711 root 1416 S /usr/bin/led-player 720 root 9320 S /opt/Qtopia/bin/qpe 721 root 3392 S -/bin/sh 722 root 3068 S init 723 root 3068 S init 725 root 3068 S init 733 root 2036 S ts_calibrate 736 root 0 SW [flush-31:0] 745 root 1408 D ./completion_testr 746 root 1408 D ./completion_testr 747 root 3392 R ps [root@FriendlyARM complete]# ./completion_testw process 748 (completion_test) awakening the readers... awoken 745 (completion_test) read ok! code=0 write ok! code=0 [1] - Done ./completion_testr [root@FriendlyARM complete]# ps PID USER VSZ STAT COMMAND 1 root 3068 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [events/0] 5 root 0 SW [khelper] 11 root 0 SW [async/mgr] 209 root 0 SW [sync_supers] 211 root 0 SW [bdi-default] 213 root 0 SW [kblockd/0] 222 root 0 SW [khubd] 228 root 0 SW [kmmcd] 244 root 0 SW [rpciod/0] 251 root 0 SW [kswapd0] 298 root 0 SW [aio/0] 302 root 0 SW [nfsiod] 306 root 0 SW [crypto/0] 416 root 0 SW [mtdblockd] 580 root 0 SW [scsi_eh_0] 581 root 0 SW [usb-storage] 635 root 0 SW [usbhid_resumer] 701 root 3068 S syslogd 704 root 3328 S /usr/sbin/inetd 708 root 1940 S /usr/sbin/boa 711 root 1416 S /usr/bin/led-player 720 root 9320 S /opt/Qtopia/bin/qpe 721 root 3392 S -/bin/sh 722 root 3068 S init 723 root 3068 S init 725 root 3068 S init 733 root 2036 S ts_calibrate 736 root 0 SW [flush-31:0] 746 root 1408 D ./completion_testr 749 root 3392 R ps [root@FriendlyARM complete]# ./completion_testw process 750 (completion_test) awakening the readers... awoken 746 (completion_test) read ok! code=0 write ok! code=0 [2] + Done ./completion_testr [root@FriendlyARM complete]# ps PID USER VSZ STAT COMMAND 1 root 3068 S init 2 root 0 SW [kthreadd] 3 root 0 SW [ksoftirqd/0] 4 root 0 SW [events/0] 5 root 0 SW [khelper] 11 root 0 SW [async/mgr] 209 root 0 SW [sync_supers] 211 root 0 SW [bdi-default] 213 root 0 SW [kblockd/0] 222 root 0 SW [khubd] 228 root 0 SW [kmmcd] 244 root 0 SW [rpciod/0] 251 root 0 SW [kswapd0] 298 root 0 SW [aio/0] 302 root 0 SW [nfsiod] 306 root 0 SW [crypto/0] 416 root 0 SW [mtdblockd] 580 root 0 SW [scsi_eh_0] 581 root 0 SW [usb-storage] 635 root 0 SW [usbhid_resumer] 701 root 3068 S syslogd 704 root 3328 S /usr/sbin/inetd 708 root 1940 S /usr/sbin/boa 711 root 1416 S /usr/bin/led-player 720 root 9320 S /opt/Qtopia/bin/qpe 721 root 3392 S -/bin/sh 722 root 3068 S init 723 root 3068 S init 725 root 3068 S init 733 root 2036 S ts_calibrate 736 root 0 SW [flush-31:0] 751 root 3392 R ps [root@FriendlyARM complete]# ./completion_testw process 752 (completion_test) awakening the readers... write ok! code=0 [root@FriendlyARM complete]# ./completion_testr process 753 (completion_test) going to sleep awoken 753 (completion_test) read ok! code=0 [root@FriendlyARM complete]#
实验表明:如果先读数据,读的程序会被阻塞(因为驱动在wait_for_completion,等待写的完成)。如果先写,读程序会比较顺利的执行下去(虽然也会休眠,但马上会被唤醒!)。