大部分竞态可通过使用内核的并发控制原语,并应用几个基本的原理来避免。第一个规则是,只要可能,就应该避免资源的共享,这种思想的明显应用就是避免使用全局变量。但硬件资源本质上就是共享的,软件资源经常需要对其他执行线程可用。全局变量并不是共享数据的唯一途径,只要我们的代码将一个指针传递给了内核的其他部分,一个新的共享就可能建立。在单个执行线程之外共享硬件或软件资源的任何时候,因为另外一个线程可能产生对该资源的不一致观察,因此必须显示地管理对该资源的访问。访问管理的常见技术是“锁定”或“互斥”----确保一次只有一个执行线程可操作共享资源。
临界区:在任意给定的时刻,代码只能被一个线程执行。并不是所有的林汲取都是一样的,因此内核为不同的需求提供了不同的原语。
进程休眠:当一个Linux进程到达某个时间点,此时它不能进行任何处理时,它将进入休眠状态,这将把处理器让给其他执行线程直到将来它能够继续完成自己的处理为止。我们可以使用一种锁定机制,当进程在等待临界区的访问时,此机制可让进程进入休眠状态。重要的是,我们将执行另外一个操作,该操作也可能会休眠,因此休眠可能在任何时刻发生。为了让我们的临界区正确工作,我们选择使用的锁定原语必须在其他拥有这个锁并休眠的情况下工作。信号量是一个众所周知的概念,一个信号量本质上是一个整数,它和一对函数联合使用,这一对函数通常称为P和V。希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于0,则该值会减小1,而进程继续。相反,如果信号量的值为0(或更小),进程必须等待直到其他人释放该信号量。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。当信号量用于互斥时,信号量的值该初始化为1。
要使用信号量,内核代码必须包含<asm/semaphore.h>。相关的类型是struct semaphore。
a.信号量的声明和初始化:直接创建信号量void sema_init(struct semaphore *sem, int val),其中val是信号量的初始值 DECLART_MUTEX(name);DECLARE_MUTEX_LOCKED(name);第一个宏初始化信号量name的初始值为1,第二个宏初始值为0.如果互斥体在运行时被初始化,使用下面的函数之一:void init_MUTEX(struct semaphore *sem);或void init_MUTEX_LOCKED(struct semaphore *sem);
b.在Linux世界中,P函数被称为down,或者该名字的其他变种。
void down(struct semaphore *sem);----减小信号量的值,并在必要时一直等待。
int down_interruptible(struct semaphore *sem);----完成与down相同的工作,但操作是可以中断的。
int down_trylock(struct semaphore *sem);----永远不会休眠,如果信号量在调用时不可获得,会立即返回非零值。
当一个线程成功调用上述down的某个版本后,就称该线程“拥有”了该信号量,这样,该线程就被赋予访问由该信号量包含的临界区的权利。当互斥操作完成后,必须返回该信号量。Linux等价于V的函数是up:void up(struct semaphore *sem);任何拿到信号量的线程都必须通过一次对up调用而释放该信号量。
许多任务可以划分为两种不同的工作类型:一些任务只需要读取受保护的数据结构,而其他的则必须做出修改。允许多个并发的读取者是可能的,只要它们之中没有哪个要做修改。这样可以提高性能,因为只读任务可并行完成它们的工作,而不需要等待其他读取者退出临界区。
Linux内核为这种情形提供了一种特殊的信号量类型,称为"rwsem"。在驱动程序中使用rwsem的机会相对较少,但偶尔也比较有用。使用rwsem的代码必须包括<linux/rwsem.h>。读取者/写入者信号量相关的数据类型是struct rw_semaphore;
一个rwsem对象必须在运行时通过下面的函数显示的初始化:void init_rwsem(struct rw_semaphore *sem);
初始化的rwsem可用于其后出现的任务。对只读访问,可用的接口如下:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semphore *sem);
void up_read(struct rw_semaphore *sem);
对down_read的调用提供了对受保护资源的只读访问,可和其他读取者并发地访问,down_read可能会将调用进程置于不可中断的休眠。
down_read_trylock不会在读取访问不可获取时等待;它在授予访问时返回非零,其他情况下返回零。由down_read获得的rwsem对象最终必须通过up_read被释放。
针对写入者的接口类似于读取者接口:
void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
down_write/down_write_trylock/up_wrtie与读取者的对应函数行为相同,当然,他们提供的是写入访问。当某个快速改变获得写入者锁,而其后是更长时间的只读访问的话,我们可以在结束修改之后调用downgrade_write,来允许其他读取者的访问。
一个rwsem可允许一个写入者或无限多个读取者拥有该信号量。写入者具有更高的优先级;当某个给定写入者试图进入临界区时,在所有写入者完成其工作之前,不会允许读取者获得访问。如果有大量的写入者竞争该信号量,则这种实现会导致读取者“饿死”即可能会长时间拒绝读取者的访问。为此,最好在很少需要写入访问且写入者只会短期拥有信号量的时候使用rwsem。
3.completion
completion是一中轻量级的机制,它允许一个线程告诉另一个线程某个工作已经完成。为了使用completion代码必须包含<linux/completion.h>可利用下面的接口创建completion:DECLART_COMPLETION(my_completion);或者动态地创建和初始化completion,则使用下面的方法:struct completion my_completion;
/*…………………………………………………………………………*/
init_completion(&my_completion);
要等待completion,可进行如下调用:void wait_for_completion(struct completion *c);
注意,该函数执行一个非中断的等待,如果代码调用了wait_for_completion且没有人会完成该任务,则将产生一个不可杀的进程。
另一方面,实际的completion事件可通过调用下面函数之一来触发:void complete(struct completion *c);
void complete_all(struct completion *c); complete只会唤醒一个等待线程,而complete_all允许唤醒所有等待线程。
completion机制的典型使用是模块退出时的内核线程终止。在这种原型中,某些驱动程序的内部工作由一个内核线程在while(1)循环中完成。当内核准备清除该函数模块时,exit函数会告诉该线程退出并等待completion。
<三>.自旋锁