在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制。Linux内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
自旋锁与信号量"类似而不类",类似说的是它们功能上的相似性,"不类"指代它们在本质和实现机理上完全不一样,不属于一类。
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。这就是它们的"不类"。
但是,无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。这就是它们的"类似"。
鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如 果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
区别总结如下:
1、由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。
2、相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠引起的耗时可能比锁被占用的全部时间还要长。
3、由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中(使用自旋锁)是不能进行调度的。
4、你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其它进程试图获得同一信号量时不会因此而死锁,(因为该进程也只是去睡眠而已,而你最终会继续执行的)。
5、在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
6、信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
7、信号量不同于自旋锁,它不会禁止内核抢占(自旋锁被持有时,内核不能被抢占),所以持有信号量的代码可以被抢占,这意味着信号量不会对调度的等待时间带来负面影响。
除了以上介绍的同步机制方法以外,还有BKL(大内核锁),Seq锁等。
BKL是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。
Seq锁用于读写共享数据,实现这样锁只要依靠一个序列计数器。
sem就是一个睡眠锁.如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。信号量一般在用进程上下文中.它是为了防止多进程同时访问一个共享资源(临界区).
spin_lock叫自旋锁.就是当试图请求一个已经被持有的自旋锁.这个任务就会一直进行 忙循环——旋转——等待,直到锁重新可用(它会一直这样,不释放CPU,它只能用在短时间加锁).它是为了防止多个CPU同时访问一个共享资源(临界区).它一般用在中断上下文中,因为中断上下文不能被中断,也不能被调度.
自旋锁对信号量
需求 建议的加锁方法
低开销加锁 优先使用自旋锁
短期锁定 优先使用自旋锁
长期加锁 优先使用信号量
中断上下文中加锁 使用自旋锁
持有锁是需要睡眠、调度 使用信号量
进程间的sem.线程间的sem与内核中的sem的功能就很类似.
进程间的sem,线程间的sem功能是一样的.只是线程的sem,它在同一个进程空间,他的初始化,使用更方便.
进程间的sem,就是进程间通信的一部分,使用semget,semop等系统调用来完成.
内核中的sem 被锁定,就等于被调用的进程占有了这个sem.其它进程就只能进行睡眠队列.这与进程间的sem基本一致.
区别 |
Spin_lock |
semaphore |
保护的对象 |
一段代码 |
一个设备(必要性不强), 一个变量, 一段代码 |
保护区可被抢占 |
不可以(会被中断打断) |
可以。 |
可允许在保护对象(代码)中休眠 |
不可以 |
可以。但最好不这样。 |
保护区能否被中断打断 |
可以,这样容易引发死锁。 最好是关了中断再使用此锁。 因为有可能中断处理例程也需要得到同一个锁。 |
可以。 |
其它功能 |
可完成同步,有传达信息的能力。 |
|
试图占用锁不成功后,进程的表现 |
不放开CPU,自己自旋。 |
进入一个等待队列。 |
释放锁后,还有其它进程等待时,内核如何处理 |
哪个进程得到运行的权力,它就得到了锁。 |
从等待队列中选一个出来占用此sem. |
内核对使用者的要求 |
被保护的代码执行时间要短,是原子的, 不能主动的休眠。 不能调用有可以休眠的内核函数。 |
|
风险 |
发生死锁 |
|
不允许锁的持有者二次请求同一个锁。 |
不允许锁的持有者二次请求同一个锁。 |
信号量在生产者与消费者模式中可以进行同步。
当sem的down和UP分别出现在对立函数中(读,写函数),其实这就是在传达一种信息。表示当前是否有数据可读的信息。
read_somthing()
{
down(设备) 占用了此设备 此时没有其它人都使用此设备上的所有操作(函数)
if(有数据)
{
读完它。
()
}
else
{
up(设备)
down(有数据的sem)sem=1表示有数据,为0表示无数据。
}
}
write_somthing()
{
down(设备) 占用了此设备 此时没有其它人都使用此设备上的所有操作(函数)
if(有数据)
{
不写。
up(设备)
return
}
else
{
写入数据
up(有数据的sem)sem=1表示有数据,为0表示无数据。
up(设备)
return;
}
}
总结:
信号量适用于长时间片段,可能会睡眠(挂起调度) 所以只能用在进程上下文,不能用在中断上下文。
自旋锁使用于短时间片段 不会睡眠(挂起调度)抱着cpu不放, 用在中断上下文 但是必须先关闭本地中断,否则很可能因为
获取不到自旋锁又抱着cpu不让别人持有而释放自旋锁从而陷入死锁。
信号量可以用的前提下尽量用信号量,万不得已(中断中)使用自旋锁,短时间片段比较适合自旋锁,调度(进程之间的切换)本身
占用时间,自旋等待的时间很短时就没必要调度了,这时选用自旋锁死抱cpu不放比较好。
时间函数
忙等待(一直消耗cpu)
ndelay、udelay、mdelay
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay));
睡着等待(不会一直消耗cpu)
msleep()、msleep_interruptible()、ssleep()、interruptible_sleep_on_timeout();