Linux内核学习笔记 -32 内核同步概述

如果我们把内核看作不断对各种请求进行响应的服务器,那么正在CPU上执行的进程,发出中断请求的外部设备等就相当于客户,正如服务器要随时响应客户的请求一样,内核也会随时响应进程/中断/系统调用等的请求,我们之所以这样比喻,是为了强调内核中的各个任务并不是严格按照顺序依次执行的,而是相互交错执行的。对所有的内核任务而言,内核中的很多数据都是共享资源,就像高速公路供很多车辆行驶一样,对这些共享资源的访问必须遵循一定的访问规则,否则就可能造成对共享资源的破坏。就如同不遵守交通规则会造成撞车一样。

并发执行是由什么引起的呢?我们在这里给出四种原因。

第一种是中断,中断几乎可以在任何时刻异步的发生,也可能随时打断正在执行的代码

第二种:内核抢占,若内核具有抢占性,内核种的任务就可能会被另一任务抢占

第三种:睡眠,在内核执行的进程可能会睡眠,这将唤醒调度程序,导致调度一个新的进程执行

第四种:对称多处理器,两个或多个处理器可以同时执行代码,那么此时就可能会造成并发,问题是,系统种还有没有其它的并发源,在并发执行的过程中会产生竞争

当以下两个条件同时发生的时候,竞争就产生,

1- 至少有两个可执行上下文”并行执行“。在这里有两种情况,一种是真正的并行,比如两个系统调用在不同的处理器上执行;另一种是其中一个上下文能随意抢占另一个,比如一个中断抢占系统调用。 第二种情况是可执行上下文对共享变量执行”读写“的访问,这里也随之给出一个问题

 

竞争条件导致错误,在这里有一个例子,比如一个释放资源的例子。

在大多数情况下,释放资源的函数只释放一次资源,上图中,如果counter的初值为2,当线程A将改变量的值减为1,B线程就抢占了A,counter的值又被减了一次,为0,此时线程B调用释放函数释放资源,然后线程A恢复执行,由于此时counter的值为0,线程A也释放资源,因此就出现了一个资源被释放两次这样的错误发生。

如何解决?

这里引出同步中重要的概念,临界区。所谓临界区就是访问和操作共享数据的代码段,多个内核任务并发访问同一个资源通常是不安全的,为了避免对临界区进行并非访问,编程者必须保证临界区代码被原子地执行,即代码在执行期间不可被打断,就如同整个临界区是一个不可分割的指令一样,如上图,A进程进入临界区后,B试图进入的时候就会被阻塞,只要当A离开临界区后,B才能进入

如何包含临界区?

1- 使临界区的操作原子地进行,例如使用原子指令,

2- 进入临界区后禁止抢占,例如通过禁止中断,禁止下半部处理程序或者线程抢占等等

3- 串行的访问临界区,比如使用自旋锁,互斥锁,只允许一个内核任务访问临界区

并发执行中,共享变量v的加1操作:

多个CPU和内存是通过总线互联的,在任意一个时刻,只能有一个总线主设备,比如CPU DMA控制器访问该从设备,在这个场景中,从设备就是RAM芯片,因此来自两个CPU上的读内存操作就被串行化执行,分别获得了统一的旧值,比如说0。完成修改后,两个CPU都想进行写操作,把修改的值协会到内存,但是硬件中的限制使得CPU的写回必须是串行化的,因此CPU1首先获得了访问权,进行写回操作,随后CPU2完成写回操作,在这种情况下,CPU1的对内存的修改被CPU2的操作覆盖了,因此执行结果是错误的。本来v加两次1一行为2,结果v就成了1.

在多处理器的SMP系统中,为了提供原子操作,不同CPU体系结构提供了不同的技术,例如在x86下,当执行加有lock前缀的指令的时候,lock前缀用于锁定系统总线,使得前面的错误不会发生。

对那些由多个内核任务进行共享的变量,对变量的装载/修改/存储必须原子地进行即不可分割,

于是linux内核提供了一个特殊的类型,叫atomic_t,具体定义如上。

从定义可以看出,atomic_t实际上就是一个int类型的counter变量

在linux内核中,提供了一组原子操作的API,如上表

前面的例子里面,我们说,释放一个资源的时候,首先对一个变量先减1操作,然后检测它是不是为零,这两个操作实际上是不可分割的,所有在原子操作的函数里面有一个函数叫atomic_dec_and_test,它实际上就是实现原子计数减1并检查的功能,这两个操作就可以原子地进行,有了这样的原子操作的函数之后,我们就不会出现前面的一个资源被释放两次这样的错误。

共享队列和加锁

当共享资源是一个复杂的数据结构的时候,竞争状态往往会使该数据结构遭到破坏,对于这种情况怎么进行处理呢? 锁机制是可以避免竞争状态的

正如锁和门一样,门后面的房间可以想象称一个临界区,在一个给定的时间里,房间里只能有一个内核任务存在,当一个任务进入房间以后,它会锁住身后的房门,当它结束对共享数据的操作以后,就会走出房间打开门锁,如图:A/B两个任务同时进入房间,当一个任务进去后就必须加锁,出来以后打开锁

 

任何要访问队列的代码首先都要占住相应的锁,这样该锁就能阻止来自其它内核任务的并发访问,比如任务1试图锁定队列,它成功获得锁之后,才能访问这个队列,

此时任务2试图锁定队列,结构可能失败,那么它就需要等待,一直等到任务1释放锁,它成功获得锁以后才能访问队列。

如何确定锁保护的对象?实际上是非常难的。找出哪些数据需要保护是关键所在,即找出谁是临界区。

内核任务的局部数据仅仅被本身访问,显然不需要保护;如果数据只被特定的进程访问,也不需要锁。

那么哪些需要加锁呢?实际上大多数内核数据结构都是临界区,都是需要加锁的,所有在内核代码里面,我们看到大量的锁机制

 

既然有锁,实际上就会出现死锁现象。什么是死锁?就是所有任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何任务都无法继续执行,这种情况就是死锁

典型的死锁有两种,一种叫四路交通堵塞,就像交通中发生的堵车现象一样;另外一种叫自死锁,一个任务试图去获得一个自己已经持有的锁的时候,就会发生自死锁的现象

如何避免死锁的发生?

如何避免死锁的发生,实际上

1- 加锁的顺序非常关键。使用嵌套的锁时必须保证以相同的顺序获取锁,这样可以阻止致命拥抱类型的死锁的发生

2- 防止发生饥饿的现象。不用重复请求同一个锁,更重要的是,要注意越复杂的加锁方案,越有可能造成死锁

3- 实际应当力求简单。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值