访问共享资源的代码区域称为临界区( critical sections),临界区需要被以某种互斥机制加以保护,方法有中断屏蔽、原子操作、自旋锁和信号量等。
在驱动程序中,当多个线程同时访问相同的资源时(驱动程序中的全局变量是一种典型的共享资源),可能会引发"竞态",因此我们必须对共享资源进行并发控制。Linux内核中解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。
1、中断屏蔽
在单 CPU 范围内避免竞态的一种简单而省事的方法是在进入临界区之前屏蔽系统的中断。
local_irq_disable() /* 屏蔽中断 */
. . .
critical section /* 临界区*/
. . .
local_irq_enable() /* 开中断*/
如果只是想禁止中断的底半部,应使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部应该调用local_bh_enable()。
由于 Linux 的异步 I/O、进程调度等很多重要操作都依赖于中断,中断对于内核的运行非常重要,在屏蔽中断期间所有的中断都无法得到处理,因此长时间屏蔽中断是很危险的,有可能造成数据丢失乃至系统崩溃等后果。这就要求在屏蔽了中断之后,当前的内核执行路径应当尽快地执行完临界区的代码。
local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 内的中断,因此,并不能解决 SMP 多 CPU 引发的竞态。因此,单独使用中断屏蔽通常不是一种值得推荐的避免竞态的方法, 它适宜与下文将要介绍的自旋锁联合使用。
2、自旋锁
自旋锁( spin lock)是一种典型的对临界资源进行互斥访问的手段,其名称来源于它的工作方式。为了获得一个自旋锁, 在某 CPU 上运行的代码需先执行一个原子操作,该操作测试并设置( test-and-set) 某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行; 如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“ 测试并设置” 操作,即进行所谓的“ 自旋”,通俗地说就是“在原地打转”。 当自旋锁的持有者通过重置该变量释放这个自旋锁后,某个等待的“测试并设置” 操作向其调用者报告锁已释放。
自旋锁一般这样被使用:
/* 定义一个自旋锁*/
spinlock_t lock; //定义自旋锁
spin_lock_init(&lock); //初始化自旋锁
spin_lock (&lock) ; /* 获取自旋锁,保护临界区 */
. . ./* 临界区*/
spin_unlock (&lock) ; /* 解锁*/
自旋锁主要针对 SMP 或单 CPU 但内核可抢占的情况,对于单 CPU 和内核不支持抢占的系统, 自旋锁退化为空操作。
尽管用了自旋锁可以保证临界区不受别的 CPU 和本 CPU 内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部( BH,稍后的章节会介绍)的影响。为了防止这种影响,就需要用到自旋锁的衍生。 spin_lock()/spin_unlock()是自旋锁机制的基础,它们和关中断 local_irq_disable()/开中断 local_irq_enable()、关底半部 local_bh_disable()/开底半部local_bh_enable()、关中断并保存状态字 local_irq_save()/开中断并恢复状态 local_irq_restore()结合
就形成了整套自旋锁机制,关系如下:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
驱动工程师应谨慎使用自旋锁:
( 1)自旋锁实际上是忙等锁,因此,只有在占用锁的
时间极短的情况下,使用自旋锁才是合理的。 当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁会降低系统的性能。
( 2)自旋锁可能导致系统死锁。引发这个问题最常见的情况是递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的 CPU 想第二次获得这个自旋锁,则该 CPU 将死锁。
( 3)自旋锁锁定期间不能调用可能引起进程调度的函数。如果进程获得自旋锁之后再阻塞, 如调用 copy_from_user()、 copy_to_user()、 kmalloc()和 msleep()等函数,则可能导致内核的崩溃。
3、信号量
信号量( semaphore)是用于保护临界区的一种常用方法,它的使用方式和自旋锁类似。与自旋锁相同,只有得到信号量的进程才能执行临界区代码。但是,与自旋锁不同的是,当获取不到信号量时,进程不会原地打转而是进入休眠等待状态。
1.定义信号量
下列代码定义名称为 sem 的信号量:
struct semaphore sem;
2.初始化信号量
void sema_init(struct semaphore *sem, int val);
该函数初始化信号量,并设置信号量 sem 的值为 val。尽管信号量可以被初始化为大于 1 的值从而成为一个计数信号量,但是它通常不被这样使用。
#define init_MUTEX(sem) sema_init(sem, 1)
该宏用于初始化一个用于互斥的信号量,它把信号量 sem 的值设置为 1;
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
该宏也用于初始化一个信号量,但它把信号量 sem 的值设置为 0;
此外,下面两个宏是定义并初始化信号量的“快捷方式”:
DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)
前者定义一个名为 name 的信号量并初始化为 1;后者定义一个名为 name 的信号量并初始化为 0。
3.获得信号量
void down(struct semaphore * sem);
该函数用于获得信号量 sem,它会导致睡眠,因此不能在中断上下文使用;
int down_interruptible(struct semaphore * sem);
该函数功能与 down 类似,不同之处为,因为 down()而进入睡眠状态的进程不能被信号打断,但因为 down_interruptible()而进入睡眠状态的进程能被信号打断,信号也会导致该函数返回,这时候函数的返回值非 0;
int down_trylock(struct semaphore * sem);
该函数尝试获得信号量 sem,如果能够立刻获得,它就获得该信号量并返回 0,否则,返回非 0 值。它不会导致调用者睡眠,可以在中断上下文使用。
在使用 down_interruptible()获取信号量时,对返回值一般会进行检查,如果非 0,通常立即返回- ERESTARTSYS,如:
if (down_interruptible(&sem))
return - ERESTARTSYS;
4.释放信号量
void up(struct semaphore * sem);
该函数释放信号量 sem,唤醒等待者。
信号量一般这样被使用:
/* 定义信号量
DECLARE_MUTEX(mount_sem);
down(&mount_sem);/* 获取信号量,保护临界区
. . .
critical section /* 临界区
. . .
up(&mount_sem);/* 释放信号量
1 /*增加并发控制后的 globalmem 读函数*/
2 static ssize_t globalmem_read(struct file *filp, char _ _user *buf, size_t size,
3 loff_t *ppos)
4 {
5 unsigned long p = *ppos;
6 unsigned int count = size;
7 int ret = 0;
8 struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
9
10 /*分析和获取有效的写长度*/
11 if (p >= GLOBALMEM_SIZE)
12 return 0;
13 if (count > GLOBALMEM_SIZE - p)
14 count = GLOBALMEM_SIZE - p;
15
16 if (down_interruptible(&dev->sem)) /* 获得信号量*/
17 return - ERESTARTSYS;
18
19 /*内核空间→用户空间*/
20 if (copy_to_user(buf, (void*)(dev->mem + p), count)) {
21 ret = - EFAULT;
22 } else {
23 *ppos += count;
24 ret = count;
25
26 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
27 }
28 up(&dev->sem); /* 释放信号量*/
29
30 return ret;
31 }
32
33 /*增加并发控制后的 globalmem 写函数*/
34 static ssize_t globalmem_write(struct file *filp, const char _ _user *buf,
35 size_t size, loff_t *ppos)
36 {
37 unsigned long p = *ppos;
38 unsigned int count = size;
39 int ret = 0;
40 struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/
41
42 /*分析和获取有效的写长度*/
43 if (p >= GLOBALMEM_SIZE)
44 return 0;
45 if (count > GLOBALMEM_SIZE - p)
46 count = GLOBALMEM_SIZE - p;
47
48 if (down_interruptible(&dev->sem)) /* 获得信号量 */
49 return - ERESTARTSYS;
50
51 /*用户空间→内核空间*/
52 if (copy_from_user(dev->mem + p, buf, count))
53 ret = - EFAULT;
54 else {
55 *ppos += count;
56 ret = count;
57
58 printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
59 }
60 up(&dev->sem); /* 释放信号量*/
61 return ret;
62 }
如果信号量被初始化为 0,则它可以用于同步,同步意味着一个执行单元的继续执行需等待另一执行单元完成某事,保证执行的先后顺序。如图 所示,执行单元 A 执行代码区域 b 之前,必须等待执行单元 B 执行完代码单元 c,信号量可辅助这一同步过程。
4、completion(完成量)
completion是比用上述信号量做同步更好的方法,用于一个执行单元等待另一个执行单元执行完某事。
1.定义完成量
下列代码定义名为 my_completion 的完成量:
struct completion my_completion;
2.初始化 completion
下列代码初始化 my_completion 这个完成量:
init_completion(&my_completion);
对 my_completion 的定义和初始化可以通过如下快捷方式实现:
DECLARE_COMPLETION(my_completion);
3.等待完成量
下列函数用于等待一个 completion 被唤醒:
void wait_for_completion(struct completion *c);
4.唤醒完成量
下面两个函数用于唤醒完成量:
void complete(struct completion *c);
void complete_all(struct completion *c);
前者只唤醒一个等待的执行单元,后者释放所有等待同一完成量的执行单元。
自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环查看是否该自旋锁的保持者已经释放了锁,"自旋"就是"在原地打转"。而信号量则引起调用者睡眠,它把进程从运行队列上拖出去,除非获得锁。这就是它们的"不类"。
sem就是一个睡眠锁.如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。信号量一般在用进程上下文中.它是为了防止多进程同时访问一个共享资源(临界区).
spin_lock叫自旋锁.就是当试图请求一个已经被持有的自旋锁.这个任务就会一直进行 忙循环——旋转——等待,直到锁重新可用(它会一直这样,不释放CPU,它只能用在短时间加锁).它是为了防止多个CPU同时访问一个共享资源(临界区).它一般用在中断上下文中,因为中断上下文不能被中断,也不能被调度。
但是,无论是信号量,还是自旋锁,在任何时刻,最多只能有一个保持者,即在任何时刻最多只能有一个执行单元获得锁。这就是它们的"类似"。
鉴于自旋锁与信号量的上述特点,一般而言,自旋锁适合于保持时间非常短的情况,它可以在任何上下文使用;信号量适合于保持时间较长的情况,会只能在进程上下文使用。如果被保护的共享资源只在进程上下文访问,则可以以信号量来保护该共享资源,如果对共享资源的访问时间非常短,自旋锁也是好的选择。但是,如 果被保护的共享资源需要在中断上下文访问(包括底半部即中断处理句柄和顶半部即软中断),就必须使用自旋锁。
区别总结如下:
1、由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况。
2、相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠引起的耗时可能比锁被占用的全部时间还要长。
3、由于执行线程在锁被争用时会睡眠,所以只能在进程上下文中才能获取信号量锁,因为在中断上下文中(使用自旋锁)是不能进行调度的。
4、你可以在持有信号量时去睡眠(当然你也可能并不需要睡眠),因为当其它进程试图获得同一信号量时不会因此而死锁,(因为该进程也只是去睡眠而已,而你最终会继续执行的)。
5、在你占用信号量的同时不能占用自旋锁,因为在你等待信号量时可能会睡眠,而在持有自旋锁时是不允许睡眠的。
6、信号量锁保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区,因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一进程企图获取本自旋锁,死锁就会发生。
7、信号量不同于自旋锁,它不会禁止内核抢占(自旋锁被持有时,内核不能被抢占),所以持有信号量的代码可以被抢占,这意味着信号量不会对调度的等待时间带来负面影响。
除了以上介绍的同步机制方法以外,还有BKL(大内核锁),Seq锁等。
BKL是一个全局自旋锁,使用它主要是为了方便实现从Linux最初的SMP过度到细粒度加锁机制。
Seq锁用于读写共享数据,实现这样锁只要依靠一个序列计数器。
自旋锁和信号量选用的 3 项原则:
( 1)当锁不能被获取到时,使用信号量的开销是进程上下文切换时间 Tsw,使用自旋锁的开销是等待获取自旋锁(由临界区执行时间决定) Tcs,若 Tcs 比较小,宜使用自旋锁,若 Tcs 很大,应使用信号量。
( 2)信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁则绝对要避免用来保护包含这样代码的临界区。因为阻塞意味着要进行进程的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
( 3)信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁。当然,如果一定要使用信号量,则只能通过down_trylock()方式进行,不能获取就立即返回以避免阻塞。
自旋锁对信号量:
需求 | 建议的加锁方法 |
低开销加锁 | 优先使用自旋锁 |
短期锁定 | 优先使用自旋锁 |
长期加锁 | 优先使用信号量 |
中断上下文中加锁 | 使用自旋锁 |
持有锁是需要睡眠、调度 | 使用信号量 |
进程间的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不放比较好。
5、互斥体
尽管信号量已经可以实现互斥的功能,但是“正宗”的 mutex 在 Linux 内核中还是真实地存在着。
1、定义名为 my_mutex 的互斥体
struct mutex my_mutex;
2、初始化它
mutex_init(&my_mutex);
3、获取互斥体的两个函数
void inline _ _sched mutex_lock(struct mutex *lock);
int _ _sched mutex_lock_interruptible(struct mutex *lock);
int _ _sched mutex_trylock(struct mutex *lock);
mutex_lock()与 mutex_lock_interruptible()的区别和 down()与 down_trylock()的区别完全一致,前者引起的睡眠不能被信号打断,而后者可以。 mutex_trylock()用于尝试获得 mutex,获取不到mutex 时不会引起进程睡眠。
4、释放互斥体
void __sched mutex_unlock(struct mutex *lock);
mutex 的使用方法和信号量用于互斥的场合完全一样:
struct mutex my_mutex; /* 定义 mutex */
mutex_init(&my_mutex); /* 初始化 mutex */
mutex_lock(&my_mutex); /* 获取 mutex */
.../* 临界资源*/
mutex_unlock(&my_mutex); /* 释放 mutex */
6、时间函数
忙等待(一直消耗cpu)
ndelay、udelay、mdelay
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay));
睡着等待(不会一直消耗cpu)
msleep()、msleep_interruptible()、ssleep()、interruptible_sleep_on_timeout();