Linux设备驱动三 (1)自旋锁,信号量

来源:互联网 发布:淘宝客返利分销源码 编辑:程序博客网 时间:2024/06/05 22:42
2.12 自旋锁
内核中是有很多的锁,自旋锁是其中的一种。它的作用在于,只要代码在进入临界区前加上锁,在进程还没出临界区之前,别的进程(包括自身处理器和别的处理器上的进程)都不能进入临界区。
自旋锁的可以这样理解,每个进程进入上锁的临界区前,必须先获得锁,否则在获得锁这条代码上查询(注意,不是休眠,是忙等待,循环执行指令),知道临界区里面的进程走出临界区,别的进程获得锁后进入临界区。有且只有一个获得锁的进程进入临界区。
也来个生活上的例子,公司有一个上锁的厕所,A在上厕所时,拿到钥匙,把门锁上后欢快地上厕所。这时B也想上厕所,但他看到门锁上了,没办法,只好在门口等待,直到A开门出来,把钥匙交给B,B才能去上厕所。

接下来说一下如何让使用,需要包含头文件

1)使用自旋锁需要先定义并初始化自旋锁:
同样的,你可以使用静态定义并初始化:
spinlock_t lock = SPIN_LOCK_UNLOCKED;
也可以使用动态定义并初始化:
spinlock_t lock;
spin_lock_init(&lock);

2)在进入临界区前,必须先获得锁,使用函数:
spin_lock(&lock);

3)在退出临界区后,需要释放锁,使用函数:
spin_unlock(&lock);

所以,一个完整的上锁代码应该这样使用:
#include
spinlock_t lock; //1.定义一个自旋锁
spin_lock_init(&my_dev.lock); //2.初始化锁

spin_lock(&lock); //3.获得锁
临界区。。。。。
spin_unlock(&lock); //4.释放锁

接着说函数spin_lock()实现了什么操作:
第一步:关抢占。
第二步:获得锁,防止别的处理器访问。
相对的,spin_unlock()实现了相反的操作:
第一步:开抢占。
第二步:释放锁。
所以,如果在单处理器支持内核抢占的内核下,spin_lock()函数会退化成关抢占。在单处理器不支持内核抢占的内核下,这将会是一条空语句。


上面的代码防了两种情况,但还没防中断,防中断有两种方法:
方法一:在需要访问临界区的中断代码也加锁:
do_irq() //中断处理函数
{
spin_lock();
/*临界区。。*/
spin_unlock();
}
方法二:直接在加锁的同时把中断也禁掉:
#include
spinlock_t lock;
spin_lock_init(&my_dev.lock);
unsigned long flag = 0;

loacl_irq_save(flag);
spin_lock(&lock);
临界区。。。。。
local_irq_restroe(flag);
spin_unlock(&lock);

当然,贴心的内核工作者将两个函数合成一个函数,只用调用一个函数就能既上锁有关中断了:
spin_lock_irq(spinlock_t *lock) = spin_lock(spinlock_t *lock) + local_irq_disable()
spin_unlock_irq(spinlock_t *lock) = spin_unlock(spinlock_t *lock) + local_irq_enable()
spin_lock_irqsave(spinlick_t *lock, unsigned long falg) = spin_lock(spinlock_t *lock) + local_irq_save(unsigned long flag)
spin_unlock_irqrestore(spinlick_t *lock, unsigned long falg) = spin_unlock(spinlock_t *lock) + local_irq_restorr(unsigned long flag)

自旋锁的一个重要特征是,只要没获得锁,进程会占用CPU查询,直到获得锁,有些人不想查询,可以使用以下函数:
int spin_try_trylock(spinlock_t *lock);
一看函数名字就知道,他是尝试获得锁,成功返回非零,失败返回零。

这个强大的功能必定有他的弊端:
弊端一:持有锁的时间必须尽量的短:
进程在没获得锁前不进入睡眠,而是会占用CPU查询,这样的做法是为了节省进程从TASK_RUNNING切换至TASK_INTERRUPTIBLE后又切换回来消耗的时间。同时也是出于这样的原因,被上锁的临界区代码必须尽量的短。

弊端二:持有锁的期间不能睡眠:
也就是说,在临界区的代码里不能有引起睡眠的操作。譬如,一个进程上锁后睡眠,此时切换执行中断处理函数,可中断处理函数也要获得锁,这样就会使中断自旋,并且没人能打断。
最简单的生活例子,上厕所的时候你锁上门睡觉了,还让别人在门口瞎等!这种事情多不合理!

弊端三:要注意上锁的顺序:
如果进程进入临界区前需要那A、B两把锁,一个进程拿了A,另一个进程拿了B,它们死活也不让步,都不能获得另外一把锁,那只好在临界区代码前死等了。

弊端四:不能嵌套上锁:
简单的说,就是获得锁后后的进程不能再上一次同样的锁。


2.13 信号量
信号量是一种睡眠锁,当进程试图获取已经被占同的信号量,他就会被放到等待队列中,直到信后信号里释放后被唤醒。
继续刚才上锁的厕所,话说A把门锁上后上厕所,B要来上厕所是看到厕所被占用了,于是,他在门口上贴张纸条“我是B,你出来后叫我上厕所”,然后就离开了。A出来后,看到门口有纸条,就按照纸条所说的去通知B。

所以,信号量就是允许长时间上锁的睡眠锁。

接下来看一下怎么使用信号量,信号量有两种:互斥信号量和计数信号量。
互斥信号量,就是说同一时间只能有一个进程获得锁并进入临界区。
而计数信号量,那就是锁的数量可以多于一个,允许多个获得锁的进程进入临界区,同时这也是和自旋锁不同的地方。

以下的函数需要包含头文件,信号量使用数据类型struct semaphore表示。
一、创建和初始化信号量:
同样有两种方法。

第一种是静态定义并初始化

static DECLARE_SEMAPHORE_GENERIC(name, count)
定义并初始化一个叫name的计数信号量,允许conut个进程同时持有锁。

static DECLARE_MUTEX(name)
定义并初始化一个叫name的互斥信号量。

第二种是动态定义并初始化
首先你要定义一个信号量结构体:
struct semaphore sem;
然后初始化:初始化是指定信号量的个数
sema_init(&sem, count);

当然也有一些方便定义互斥信号量的函数:
/*初始化一个互斥信号量*/
#define init_MUTEX(sem) sema_init(sem, 1)
/*初始化一个互斥信号量并加锁*/
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)

二、使用信号量:

一般的获得信号量有三个函数:
1/*获取信号量sem,如果不能获取,切换状态至TASK_UNINTERRUPTIBLE*/
voud down(struct semaphore *sem)
上面的函数不太常用,因为它的睡眠不能被中断打断,一般使用下面的函数

2/*获取信号量sem,如果不能获取,切换状态至TASK_INTERRUPTIBLE,如果睡眠期间被中断打断,函数返回非0值*/
int down_interruputible(struct semaphore *sem)

3/*尝试获得信号量,如果获得信号量就返回零,不能获得也不睡眠,返回非零值*/
int down_try_lock(struct semaphore *sem)

因为上面的函数在睡眠时会被中断打断,一般会如下使用:
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
即如果在睡眠期间被中断打断,返回-ERESTARTSYS给用户,告知用户重新执行。如果是被唤醒,则会往下执行。

释放信号量函数:
void up(struct semaphore *sem);

所以,信号量一般这样使用:
#include
static DECLARE_SEMAPHORE_GENERIC(sem, 1)
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
临界区代码。。。。。
up(&sem);
第一:信号量没有关抢占,如果别的进程没有访问上锁的临界区(如app),这个进程照样可以运行。
第二:访问了上锁临界区的进程,就不能执行了
第三:临界区还是可以被中断打断的,因为信号量根本没关中断,如果临界区的资源不能被中断访问,那就像之前说的处理,要不在中断处理函数在进入临界区前获得锁,要不就把中断也关了。
所以,简单的说,信号量就是一个数,你获得这个数了,你就可以进去临界区。
2.13.1 信号量与自旋锁的区别:
区别一:实现方式
自旋锁是自旋等待,进程状态始终处于TASK_RUNNING。
信号量是睡眠等待,进程在等待是处于TASK_INTERRUPTIBLE。

区别二:睡眠死锁陷阱:
在自旋锁的临界区中,进程是不能陷入睡眠的。
而信号量可以睡眠。
同时,基于上面的原因,中断上下文中只能使用自旋锁(中断里不能休眠),在有睡眠代码的临界区只能使用信号量

区别三:CPU的使用情况:
明显的,信号量对系统的负载小,因为它睡眠了。

区别四:执行的效率方面:
自旋锁的效率比较高,因为它少了进程状态切换的消耗。
相对的信号量的效率比较低,因为进程的等待需要切换进程状态。

区别五:上锁的时间长短:
因为自旋锁是忙等待,所以临界区的代码不能太长。
而信号量可以使用在运行时间较长的临界区代码。

区别六:是否关抢占:
自旋锁是关抢占的,所以在单处理器非抢占的内核下,自旋锁是没用的。是空操作。
信号量并没有关抢占,所以,只有需要获得锁的进程才会睡眠,其他进程还可以继续运行,如上面的例子。

居于上面的区别,有这样的一个表:
需求 建议的加锁方法
低开销的加锁 优先考虑自旋锁
短时间的加锁 优先考虑自旋锁
长时间的加锁 优先是使用信号量
中断上下文中加锁 必须使用自旋锁
上锁后会有睡眠 必须使用信号量

2.14 互斥量
其实上面介绍了两种锁使用的情况,其实,可以睡眠的临界区,都可以使用信号量,这就是信号量强大的地方。然而,越强大的功能,内核实现起来就越是困难。所以。内核开发者实现了轻量级的睡眠锁——互斥量。
使用互斥量使用结构体struct mutex来表示:
一、定义并初始化,两种方法:
静态定义:
DEFINE_MUTEX(name)
动态定义并初始化:
struct mutex mutex;
mutex_init(&mutex);

二、互斥量的操作:
获得互斥里
void inline __sched mutex_lock(struct mutex *lock) //不能获得锁是进入不可中断睡眠
int __sched mutex_lock_interruptible(struct mutex *lock) //进入可中断睡眠
int __sched mutex_trylock(struct mutex *lock) //尝试获得锁
这三个函数的用法的信号量的三个完全一样,返回值也是,所以我就不细讲了。
释放信号量:
void __sched mutex_unlock(struct mutex *lock)

当然,互斥量是升级版的轻量级信号量,它必然会有限制:
1)同一时间只能有一个进程获得锁,这是互斥的概念。
2)只能在同一进程上锁和解锁,而信号量不一样,可以在这个进程上锁,另外的进程解锁。
3)同一个进程获得锁后这段期间在获得这个锁,也就是说不能递归使用,原因很简单,因为是互斥,上锁的只有一次,只能解锁有在上锁。
4)进程持有锁是不能退出。
5)中断上下文不能使用锁,即使是mutex_trylock()。
6)互斥锁只能通过内核提供的API接口来操作。

内核推荐,在能使用互斥锁的情况下优先考虑,而不是使用信号量。



0 0
原创粉丝点击