并发和竞态
1. 并发及其管理
竞态通常作为对资源的共享访问结果而产生。
当两个执行线程需要访问相同的数据结构(或硬件资源)时,混合的可能性就永远存在。因此在设计自己的驱动程序时,第一个要记住的原则是,只要可能,就应该避免资源的共享。如果没有并发的访问,也就不会有竞态的产生。因此,仔细编写的内核代码应该具有最少的共享。这种思想的最明显应用就是避免使用全局变量。
2. 信号量和互斥体
为scull添加锁定。目的是使对scull数据结构的操作是原子的,这意味着在涉及到其他执行线程之前,整个操作就已经结束了。对我们的内存泄漏示例来说,需要确保当一个线程发现特定内存块需要分配时,它应该拥有执行分配的机会,并需要在其他线程执行同一测试之前完成这个工作。为此,我们必须建立临界区:在任意给定的时刻,代码只能被一个线程执行。
信号量:一个信号量本质上是一个整数值,它和一对函数(P和V)联合使用.希望进入临界区的进程将在相关信号量上调用P;如果信号量的值大于零,则该值会减小1,而进程可以继续。相反,如果信号量为0(或更小),进程必须等待直到其他人释放该信号量。对信号量的解锁通过调用V完成;该函数增加信号量的值,并在必要时唤醒等待的进程。
当信号量用于互斥时(即避免多个进程同时在一个临界区中运行),信号量的值应初始化1.这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一个信号量有时也称为一个“互斥体(mutex)”,它是互斥(mutual exclusion)的简称。Linux内核中几乎所有的信号量均用于互斥。
3. Linux信号量的实现
定义信号量:struct semaphore sem;
用于声明和初始化用在互斥模式中的信号量的两个宏:
DECLARE_MUTEX(sem);//sem被初始化为1
DECLARE_MUTEX_LOCKED(sem);//sem被初始化为0
在运行时初始化信号量:
Voidinit_MUTEX(struct semaphore *sem);
Voidinit_MUTEX_LOCKED(struct semaphore *sem );
在Linux世界中,P函数被称为down——down指的是该函数减少了信号量的值,它也许将调用者置于休眠状态,然后等待信号量变得可用,之后授予调用者对被保护资源的访问。
Voiddown(struct semaphore *sem);//不可中断,,不建议使用
Int down_interruptible(struct semaphore *sem);//可被信号中断,常用
Int down_trylock(struct semaphore *sem);//不会休眠,在信号量不可用时立即返回
当一个线程成功调用上述down的某个版本后,就称为该线程拥有了该信号量。这样,该线程就赋予访问由该信号量保护的临界区的权利。当互斥操作完成后,必须返回该信号量。Linux等价于V的函数是up:
Voidup(struct semaphore *sem);调用up之后,调用者不再拥有该信号量。
4. 自旋锁
信号量对互斥来讲是非常有用的工具,但他不是内核提供的唯一的这类工具,相反,大多数锁定通过称为“自旋锁(spinlock)”的机制实现。和信号量不同,自旋锁可在不能休眠的代码中使用,比如中断处理历程。在正确使用的情况下,自旋锁通常可以提供比信号量更高的性能。
自旋锁是一个互斥设备,它只能有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某特定锁的代码测试相关的位。如果锁可用,则“锁定”位被设置,而代码继续进入临界区;相反,如果被其他人获得,则代码进入忙循环并重复检查这个锁,直到该锁可用为止。这个循环就是自旋锁的“自旋”部分。当存在自旋锁时,等待执行忙循环的处理器做不了任何有用的工作。
自旋锁和信号量(睡眠锁)的区别:获得不到信号量时会将CPU让出来,但是自旋锁会一直占用CPU.
l 信号量可能允许有多个持有者,而自旋锁在任何时候只允许一个持有者
l 信号量适合于保持时间长的情况,而自旋锁适合于保持时间非常短的情况。
l 自旋锁会导致死循环,锁定期间不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。
5. 自旋锁API介绍
1) 自旋锁初始化
l spinlock_t my_lock=SPIN_LOCK_UNLOCKED;//在编译时期
l void spin_lock_init(spinlock_t *lock);//或者在运行时调用
2) 在进入临界区之前,必须调用下面函数获得需要的锁
Void spin_lock(spinlock_t *lock);//所有的自旋锁等待在本质上都是不可中断的,一旦调用了spin_lock,在获得锁之前一直处于自旋状态。
3) 释放已经获得的锁
Void spin_unlock(spinlock_t *lock);
6. 自旋锁和原子上下文
适用于自旋锁的核心规则是:任何拥有自旋锁的代码都必须是原子的。它不能休眠,事实上,它不能因为任何原因放弃处理器,除了中断服务以外(某些情况下此时也不能放弃处理器)。任何时候,只要内核代码拥有自旋锁,在相关处理器上的抢占就会被禁止。
在拥有自旋锁时禁止中断(仅在本地CPU上)。
自旋锁使用上的最后一个重要原则是,自旋锁必须在可能的最短时间内拥有。拥有自旋锁的时间越长,其他处理器不得不自旋以等待释放该自旋锁的时间就越长,而它不得不永远自旋的可能性就越大。长的锁拥有时间将阻止对当前处理器的调度,这意味着更高优先级饿进程(真正应该获得CPU的进程)不得不等待。
7. 规则
如果某个获得锁的函数要调用其他同样试图获得这个锁的函数,我们的代码就会死锁。不论是信号量还是自旋锁,都不允许拥有者第二次获得这个锁;如果试图这么做,系统将挂起。
为了让锁定正确工作,则不得不编写一些函数,这些函数假定调用者已经获得了相关的锁。通常,内部的静态函数可通过这种方式编写,而提供给外部调用的函数则必须显式地处理锁定。