linux设备驱动归纳总结:5.SMP下的竞态和并发

Posted 飞雪天龙

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了linux设备驱动归纳总结:5.SMP下的竞态和并发相关的知识,希望对你有一定的参考价值。

linux设备驱动归纳总结(四):5.多处理器下的竞态和并发


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

这节将在上一节的基础上介绍支持多处理器和内核抢占的内核如何避免并发。除了内核抢占和中断外,由于多处理起的缘故,它可以做到多个程序同时执行。所以,进程除了要防自己的处理器外,还要防别的处理器,这个就是这节要介绍的内容。

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


一、多处理器抢占式内核的内核同步需要防什么


1)防内核抢占。

2)防中断打断。

3)防其他处理器也来插一脚。


所以,在上一节讲的防抢占和防中断,接下来的内容实在这两个的基础上说一下如何防其他处理器。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


二、自旋锁


内核中是有很多的锁,自旋锁是其中的一种。它的作用在于,只要代码在进入临界区前加上锁,在进程还没出临界区之前,别的进程(包括自身处理器和别的处理器上的进程)都不能进入临界区。

自旋锁的可以这样理解,每个进程进入上锁的临界区前,必须先获得锁,否则在获得锁这条代码上查询(注意,不是休眠,是忙等待,循环执行指令),知道临界区里面的进程走出临界区,别的进程获得锁后进入临界区。有且只有一个获得锁的进程进入临界区

也来个生活上的例子,公司有一个上锁的厕所,A在上厕所时,拿到钥匙,把门锁上后欢快地上厕所。这时B也想上厕所,但他看到门锁上了,没办法,只好在门口等待,直到A开门出来,把钥匙交给BB才能去上厕所。


接下来说一下如何让使用,需要包含头文件<linux/spinlock.h>

1)使用自旋锁需要先定义并初始化自旋锁:

同样的,你可以使用静态定义并初始化:

spinlock_t lock = SPIN_LOCK_UNLOCKED;

也可以使用动态定义并初始化:

spinlock_t lock;

spin_lock_init(&lock);

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

spin_lock(&lock);

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

spin_unlock(&lock);


所以,一个完整的上锁代码应该这样使用:

#include <linux/spinlock.h>

spinlock_t lock; //1.定义一个自旋锁

spin_lock_init(&my_dev.lock); //2.初始化锁


spin_lock(&lock); //3.获得锁

临界区。。。。。

spin_unlock(&lock); //4.释放锁

我将这段代码加上了驱动程序4th_mutex_5/1st/test.c,注意,这段函数并不是很规范,我只是想举例示范一下这几个函数应该加在代码中的什么位置。其中,代码中的临界区我只是打印了一句话,并不是什么共享数据。

验证一下效果:

[root: 1st]# insmod test.ko

alloc major[253], minor[0]

hello kernel

[root: 1st]# mknod /dev/test c 253 0

[root: 1st]# insmod irq/irq.ko

hello irq

[root: 1st]# cd app/

[root: app]# ./app&

[root: app]# <app> runing

<app> runing

[root: app]# ./app_read

<kernel>[test_open]

<app_read> pid[400]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

key down

key down

key down

key down

key down

<kernel>[test_read]task pid[400], context [app_read]

会发现,因为我在一个死循环上了自旋锁(当然这种做法是不恰当的),程序运行起来就和关了抢占效果一样!内核线程陷入循环,只有中断能够打断。


接着说函数spin_lock()实现了什么操作:

第一步:关抢占。

第二步:获得锁,防止别的处理器访问。

相对的,spin_unlock()实现了相反的操作:

第一步:开抢占。

第二步:释放锁。

所以,如果在单处理器支持内核抢占的内核下,spin_lock()函数会退化成关抢占。在单处理器不支持内核抢占的内核下,这将会是一条空语句。


上面的代码防了两种情况,但还没防中断,防中断有两种方法:

方法一:在需要访问临界区的中断代码也加锁:

do_irq() //中断处理函数

{

spin_lock();

/*临界区。。*/

spin_unlock();

}

方法二:直接在加锁的同时把中断也禁掉:

#include <linux/spinlock.h>

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后又切换回来消耗的时间。同时也是出于这样的原因,被上锁的临界区代码必须尽量的短

弊端二:持有锁的期间不能睡眠:

也就是说,在临界区的代码里不能有引起睡眠的操作。譬如,一个进程上锁后睡眠,此时切换执行中断处理函数,可中断处理函数也要获得锁,这样就会使中断自旋,并且没人能打断

最简单的生活例子,上厕所的时候你锁上门睡觉了,还让别人在门口瞎等!这种事情多不合理!

弊端三:要注意上锁的顺序:

如果进程进入临界区前需要那AB两把锁,一个进程拿了A,另一个进程拿了B,它们死活也不让步,都不能获得另外一把锁,那只好在临界区代码前死等了。

弊端四:不能嵌套上锁:

简单的说,就是获得锁后后的进程不能再上一次同样的锁。


xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


三、信号量


上面说了自旋锁的缺点,如不能睡眠,要求临界区执行时间尽可能的短。出于这样的情况,就有了另一种内核同步的机制——信号量。

信号量是一种睡眠锁,当进程试图获取已经被占同的信号量,他就会被放到等待队列中,直到信后信号里释放后被唤醒。

继续刚才上锁的厕所,话说A把门锁上后上厕所,B要来上厕所是看到厕所被占用了,于是,他在门口上贴张纸条“我是B,你出来后叫我上厕所”,然后就离开了。A出来后,看到门口有纸条,就按照纸条所说的去通知B


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


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


以下的函数需要包含头文件<asm?semaphore.h>,信号量使用数据类型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 <asm/semaphore.h>


struct semaphore sem;

sema_init(&sem, 1);


if (down_interruptible(&sem)){

return – ERESTARTSYS;

}

临界区代码。。。。。

up(&sem);


4th_mutex_5/2nd/test.c我写了加上信号量的代码,还是那一句,代码不规范(在死循环加信号量无疑是自杀),我只是想告诉大家这几条函数一般使用在什么地方。在抢占式内核的情况下,使用信号量和使用自旋锁保护代码会不一样

[root: /]# cd review_driver/4th_mutex/4th_mutex_5/2nd/

[root: 2nd]# insmod test.ko //加载模块

alloc major[253], minor[0]

hello kernel

[root: 2nd]# mknod /dev/test c 253 0

[root: 2nd]# insmod irq/irq.ko //加载中断

hello irq

[root: 2nd]# cd app/

[root: app]# ./app_read& //先后台运行app_read

<kernel>[test_open]

<app_read> pid[400] //注意进程号400

<kernel>[test_read]task pid[400], context [app_read]

[root: app]# <kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

[root: app]# ./app_read& //再后台运行一个app_read

<kernel>[test_open]

<app_read> pid[401] //注意进程号401,后面的打印没有一个是401!!!

[root: app]# <kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

[root: app]# ./app //后台运行app

<app> runing

<kernel>[test_read

key down

key down

key down

<kernel>[test_read]task pid[400], context [app_read]

<app> runing

<kernel>[test_read]task pid[400], context [app_read]

<app> runing

]task pid[400], context [app_read]

<app> runing //app能打印!!!!!

<kernel>[test_read]task pid[400], context [app_read]

<kernel>[test_read]task pid[400], context [app_read]

<app> runing

key down //中断也能执行!!!

key down

key down

key down

key down

<kernel>[test_read]task pid[400], context [app_read]

<app> runing

<kernel>[test_read]task pid[400], context [app_read]

<app> runing

不知道各位注意到上面的现象与自旋锁的有什么区别。

第一:信号量没有关抢占,如果别的进程没有访问上锁的临界区(app),这个进程照样可以运行。

第二:访问了上锁临界区的进程,就不能执行了(如第二次运行的app_read)。

以上是关于linux设备驱动归纳总结:5.SMP下的竞态和并发的主要内容,如果未能解决你的问题,请参考以下文章

15 同步于互斥 并发竞态和编译乱序执行乱序

[实践中的并发 7.2.5] 中提到的竞态条件的年表是啥

Android C++系列:Linux信号

来自共享内存中的数据结构的竞态检查错误

JUC并发编程 -- 避免临界区的竞态条件之synchronized 解决方案(同步代码块)

linux并行与竞态