现在的位置: 首页 > 综合 > 正文

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

2013年10月22日 ⁄ 综合 ⁄ 共 4004字 ⁄ 字号 评论关闭

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

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

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx


***************************************************************************************

不可抢占内核的特点

  在不支持内核抢占的内核中,内核代码可以一直执行,到它完成为止。也就是说,调度程序没有办法在一个内核级的任务正在执行的时候重新调度—内核中的各任务是协作方式调度的,不具备抢占性。当然,运行于内核态 的进程可以主动放弃CPU,比如,在系统调用服务例程中,由于内核代码由于等待资源而放弃CPU,这种情况叫做计划性进程切换(planned process switch)。内核代码一直要执行到完成(返回用户空间)或明显的阻塞为止,

  在单CPU情况下,这样的设定大大简化了内核的同步和保护机制。可以分两步对此加以分析:

  首先,不考虑进程在内核中自愿放弃CPU的情况(也即在内核中不发生进程的切换)。一个进程一旦进入内核就将一直运行下去,直到完成或退出内核。在其没有完成或退出内核之前,不会有另外一个进程进入内核,即进程在内核中的执行是串行的,不可能有多个进程同时在内核中运行,这样内核代码设计时就不用考虑多个进程同时执行所带来的并发问题。Linux的内核开发人员就不用考虑复杂的进程并发执行互斥访问临界资源的问题。当进程在访问、修改内核的数据结构时就不需要加锁来防止多个进程同时进入临界区。这时只需再考虑一下中断的情况,若有中断处理例程也有可能访问进程正在访问的数据结构,那么进程只要在进入临界区前先进行关中断操作,退出临界区时进行开中断操作就可以了。

  再考虑一下进程自愿放弃CPU的情况。因为对CPU的放弃是自愿的、主动的,也就意味着进程在内核中的切换是预先知道的,不会出现在不知道的情况下发生进程的切换。这样就只需在发生进程切换的地方考虑一下多个进程同时执行所可能带来的并发问题,而不必在整个内核范围内都要考虑进程并发执行问题。

***********************************************************************************************


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


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

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

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

最简单的生活例子,上厕所的时候你锁

抱歉!评论已关闭.