Linux内核中的同步机制:进程的同步同步机制,线程的同步机制 原文地址:https://blog.csdn.net/weixin_/article/details/ 1. 什么是同步,为什么要同步,什么是互斥 1.1 同步和互斥 同步:用于实现控制多个进程按照一定的规则或顺序访问某些系统资源的机制。 互斥:实现控制某些系统资源在的机制。 1.2 为什么要同步 在操作系统引入了进程概念,进程成为调度实体后,系统就具备了并发执行多个进程的能力,但也导致了系统中各个进程之间的资源竞争和共享。另外,由于中断、异常机制的引入,以及内核态抢占都导致了这些内核执行路径(进程)以交错的方式运行。对于这些交错路径执行的内核路径,如不采取必要的同步措施,将会对一些关键数据结构进行交错访问和修改,从而导致这些数据结构状态的不一致,进而导致系统崩溃。因此,为了确保系统高效稳定有序地运行,linux必须要采用同步机制。 内核中造成竞争条件的原因:竞争原因说明中断中断随时会发生,也就会随时打断当前执行的代码。如果中断和被打断的代码在相同的临界区,就产生了竞争条件软中断和tasklet软中断和tasklet也会随时被内核唤醒执行,也会像中断一样打断正在执行的代码内核抢占内核具有抢占性, 发生抢占时,如果抢占的线程和被抢占的线程在相同的临界区,就产生了竞争条件睡眠及用户空间的同步用户进程睡眠后,调度程序会唤醒-个新的用户进程,新的用户进程和睡眠的进程可能在同一个临界区中对称多处理2个或多个处理器可以同时执行相同的代码 举一个多进程访问同一个资源的例子小明房间里有一双滑板鞋,早上起床上学时看到还在,于是在学校里和同学约好下课一块玩滑板,结果小明房间的门没有锁,在小明离开房间期间,他的弟弟进入房间将滑板拿走了,等小明回去一看,滑板鞋已经不在了。造成的后果就是小明将失信于同学,解决办法就是将房间锁住。 上面的滑板鞋就是临界区,所谓 小明和他弟弟构成了竞争条件() 同步技术就是为了解决资源访问竞争产生问题的一种方法。上面的锁门就说同步技术的其中一种方法 2. 死锁 2.1 概念死锁就是所有线程都持有对方所需要的资源,他们在相互等待对方释放,导致谁也无法继续执行下去。死锁就是所有线程都持有对方所需要的资源,他们在相互等待对方释放,导致谁也无法继续执行下去。 2.2 例子ABBA死锁 线程1 线程2获得锁A 获得锁B试图获得锁B 试图获得锁A等待锁B 等待锁A 2.3 死锁的起因1.竞争不可抢占性资源引起死锁2.竞争可消耗资源引起死锁3.进程推进顺序不当引起死锁(对资源申请和访问的不当) 2.4.产生死锁的必要条件(*)条件(在一段时间,一个资源只能被一个进程占用)条件(进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源已经被其他进程占有,此时的进程被阻塞,但对自己的资源又保持不放)资源(进程已经获得的资源未被用完不能被抢占,只能在进程使用完时由自己释放)条件(在发生死锁时,必然存在一个进程——资源的循环链) 2.5.处理死锁的方法预防死锁(设置某些限制条件去破坏产生死锁四个必要条件之一)避免死锁(在资源的动态分配过程中,用某种方法防止系统进入不安全状态,从而可以避免发生死 锁,如,它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。)检测死锁(在进程运行过程红发生死锁后,通过检测机构检测死锁的发生,然后采取适当的措施,把进程从死锁中解救出来)解除死锁(撤销一些进程,或回收资源,将他们分配给已处于阻塞状态的进程)对死锁的防范程度: 预防死锁>避免死锁>检测死锁>解除死锁对资源的利用率的提高,以及进程因资源因素而阻塞的频度:解除死锁>检测死锁>避免死锁>预防死锁(四个方法讲其中的考点方法) 下面一些简单的规则可以帮助我们避免死锁:如果有多个锁的话,尽量确保每个线程都是。(即加锁a->b->c,解锁c->b->a)。即设置一个超时时间,防止一直等待下去。。设计应。加锁的方案越复杂就越容易出现死锁。 3. 同步的几种手段 原子操作——指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束自旋锁——当一个线程了锁之后,其他试图这个锁的线程一直在循环等待这个锁,直至锁重新可用。读写自旋锁——对读和写特殊要求的自旋锁信号量——和循环检测的自旋锁不同,线程不到信号量的时候,会进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。读写信号量——对读和写特殊要求的信号量互斥体——互斥体也是一种可以睡眠的锁,相当于二值信号量,也就是最多只允许一个线程访问临界区完成变量大内核锁顺序锁禁止抢占顺序和屏障读-复制-更新(RCU)每CPU变量 本节讨论了大约11种内核同步方法,除了大内核锁已经不再推荐使用之外,其他各种锁都有其适用的场景。 了解了各种同步方法的适用场景,才能正确的使用它们,使我们的代码在安全的保障下达到最优的性能。 同步的目的就是为了保障数据的安全,其实就是保障各个线程之间共享资源的安全,
3.1 原子操作 定义: 所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。将整个操作视作一个整体是原子性的核心特征。i++是不是原子操作?——不是I++做了三次指令操作,两次内存访问,第一次,从内存中读取i变量的值到CPU的寄存器,第二次在寄存器中的i自增1,第三次将寄存器中的值写入内存。这三次指令操作中任意两次如果同时执行的话,都会造成结果的差异性。而对于++i,在多核机器上,CPU在读取内存时也可能同时读到同一个值,这样就会同一个值自增两次,而实际上只自增了一次,所以++i也不是原子操作 常用原子操作函数举例:
测试:(互斥阻塞)
3.2 自旋锁(循环,spin) 原子操作只能用于临界区只有一个变量的情况,实际应用中,临界区的情况要复杂的多。 对于复杂的临界区,linux内核中也提供了多种同步方法,自旋锁就是其中一种。 由于线程实在一直循环的这个锁,所以会造成CPU处理时间的浪费,因此最好将 自旋锁的实现与体系结构有关,所以相应的头文件 <asm/spinlock.h> 位于相关体系结构的代码中。自旋锁使用时有2点需要注意:自旋锁是的,递归的请求同一个自旋锁会自己锁死自己。线程自旋锁之前,要。(防止锁的线程和中断形成竞争条件)比如:当前线程自旋锁后,在临界区中被中断处理程序打断,中断处理程序正好也要这个锁,于是中断处理程序会等待当前线程释放锁,而当前线程也在等待中断执行完后再执行临界区和释放锁的代码。 自旋锁方法列表如下: 实例1:基本流程 实例2:禁止本地中断 自旋锁和下半部:1、下半部处理和进程上下文共享数据时,由于下半部的处理可以抢占进程上下文的代码,所以进程上下文在对共享数据加锁前要禁止下半部的执行,解锁时再允许下半部的执行。2、中断处理程序(上半部)和下半部处理共享数据时,由于中断处理(上半部)可以抢占下半部的执行,所以下半部在对共享数据加锁前要禁止中断处理(上半部),解锁时再允许中断的执行。3、同一种tasklet不能同时运行,所以同类tasklet中的共享数据不需要保护。4、对于软中断,无论同种类型,如果数据被软中断共享,那它必须得得到锁的保护,但是同一处理器上一个软中断不会抢占另一个软中断,因此不需要禁止下半部。 3.3 读写自旋锁(循环) 读写自旋锁除了和普通自旋锁一样有自旋特性以外,还有以下特点:读锁之间是共享的即一个线程持有了读锁之后,其他线程也可以以读的方式持有这个锁写锁之间是互斥的即一个线程持有了写锁之后,其他线程不能以读或者写的方式持有这个锁读写锁之间是互斥的即一个线程持有了读锁之后,其他线程不能以写的方式持有这个锁 注:读写锁要分别使用,不能混合使用,否则会造成死锁。 正常的使用方法: 读写锁相关文件参照 各个体系结构中的 <asm/rwlock.h> 读写锁的相关函数如下: 3.4 信号量(sem) :https://blog.csdn.net/weixin_/article/details/ 信号量:用于的临界区有,一个房间可以设置>1/=1人进入常用,即互斥信号量 信号量也是一种锁,和自旋锁不同的是,线程不到信号量的时候,不会像自旋锁一样循环的去试图锁,而是进入睡眠,直至有信号量释放出来时,才会唤醒睡眠的线程,进入临界区执行。 由于使用信号量时,线程会睡眠,所以等待的过程不会占用CPU时间。所以信号量适用于等待时间较长的临界区。 信号量消耗的CPU时间的地方在于使线程睡眠和唤醒线程,如果 (使线程睡眠 + 唤醒线程)的CPU时间 > 线程自旋等待的CPU时间,那么可以考虑使用自旋锁。 3.5 读写信号量 读写信号量和信号量之间的关系 与 读写自旋锁和普通自旋锁之间的关系 差不多。 读写信号量都是,即计数值最大为1,增加读者时,计数器不变,增加写者,计数器才减一。 也就是说读写信号量保护的临界区,最多只有一个写者,但可以有多个读者。 读写信号量的相关内容参见:<asm/rwsem.h> 具体实现与硬件体系结构有关。 3.6 互斥体(mutex_lock) 互斥体也是一种可以睡眠的锁,相当于二值信号量,只是提供的API更加简单,使用的场景也更严格一些,如下所示:mutex的计数值只能为1,也就是最多只允许一个线程访问临界区在同一个上下文中上锁和解锁不能递归的上锁和解锁持有mutex时,进程不能退出mutex不能在中断或者下半部中使用,也就是mutex只能在进程上下文中使用mutex只能通过官方API来管理,不能自己写代码操作它 在面对互斥体和自旋锁的选择时,参见下表: 建议的加锁方法
互斥体头文件:<linux/mutex.h> 常用的互斥体方法如下: 3.7 完成变量(completion) 完成变量的机制类似于信号量, 完成变量的头文件:<linux/completion.h> 完成变量的API也很简单: 一般在2个任务需要简单同步的情况下,可以考虑使用完成变量。 3.8 大内核锁 大内核锁已经不再使用,只存在与一些遗留的代码中。大内核锁(BKL)的设计是在kernel hacker们对多处理器的同步还没有十足把握时,引入的大粒度锁。他的设计思想是,一旦某个内核路径了这把锁,那么其他所有的内核路径都不能再到这把锁。 所谓的大内核锁,顾名思义,就是给整个内核上的一把锁,那么为什么需要这么一把锁呢?这就要追溯到 Linux 早期了,当时 Linux 对 SMP 的支持非常不足,于是为了保证内核能在 SMP 环境下正常运行,开发者们就想出了一个权宜之计,即用一把锁把整个内核用自旋锁“锁”起来,这把锁就是所谓的大内核锁。每个进程在进入内核态运行内核代码时都要先得到大内核锁,这样就能保证每次只有 1 个进程在内核态运行(第一个获得锁的进入内核态,后来者就只能在内核态门口等待),这样由于多个处理器不会同时在内核态运行,自然也不会发生同步上的问题。这种做法实现起来非常简单,但缺点却也非常的明显,即:只有 1 个处理器的程序能进入内核态,而其他处理器上的程序要进入内核态时则会被阻止,这会极大影响多处理器的威力。因此开发者们不停的缩小大内核锁的管制范围,他们首先把管住内核态入口的大内核锁去掉,但这样还不够,因为这样虽然能让多个处理器上的程序进入内核态,但有些资源上的使用还是非常麻烦,比如说 A 和 B 是两个完全不相关的资源,而且他们都被大内核锁锁住,那么只要有个进程因使用 A 资源而得到大内核锁,其他处理器上的进程就无法同时得到 B 资源(因为使用 B 资源也需要获得大内核锁,而大内核锁已经被使用 A 资源的进程率先获得了),所以他们还把这些资源一一的挑出来单独保护,这样大内核锁就只需要保护少量关键资源即可。现在随着内核开发者对大内核锁的逐步改进,在最新版的 Linux 内核中,大内核锁已经不复存在了。 3.9 顺序锁(和读写锁有关,seqlock) 顺序锁为读写共享数据提供了一种简单的实现机制。 之前提到的读写自旋锁和读写信号量,在读锁被之后,写锁是不能再被的,也就是说,必须等所有的读锁释放后,才能对临界区进行写入操作。 顺序锁则与之不同,读锁被的情况下,写锁仍然可以被。 使用顺序锁的读操作在读之前和读之后都会检查顺序锁的序列值,如果前后值不符,则说明在读的过程中有写的操作发生, 那么读操作会重新执行一次,直至读前后的序列值是一样的。 顺序锁优先保证写锁的可用,所以适用于那些读者很多,写者很少,且写优于读的场景。 顺序锁的使用例子可以参考:kernel/timer.c和kernel/time/tick-common.c文件 3.10 禁止抢占 其实使用自旋锁已经可以防止内核抢占了,但是有时候仅仅需要禁止内核抢占,不需要像自旋锁那样连中断都屏蔽掉。这时候就需要使用禁止内核抢占的方法了: 禁止抢占的头文件参见:<linux/preempt.h> 3.11 顺序和屏障(禁止编译器优化,mb) 就是禁止编译器优化 对于一段代码,编译器或者处理器在编译和执行时可能会对执行顺序进行一些优化,从而使得代码的执行顺序和我们写的代码有些区别。 一般情况下,这没有什么问题,但是在并发条件下,可能会出现取得的值与预期不一致的情况比如下面的代码: 由于编译器或者处理器的优化,线程A中的赋值顺序可能是b先赋值后,a才被赋值。 所以如果线程A中 b=4; 执行完,a=5; 还没有执行的时候,线程B开始执行,那么线程B打印的是a的初始值1。 这就与我们预期的不一致了,我们预期的是a在b之前赋值,所以线程B要么不打印内容,如果打印的话,a的值应该是5。 在某些并发情况下,为了保证代码的执行顺序,引入了一系列屏障方法来阻止编译器和处理器的优化。方法描述rmb()阻止跨越屏障的载入动作发生重排序read_ barrier_ depends()阻止跨越屏障的具有数据依赖关系的载入动作重排序wmb()阻止跨越屏障的存储动作发生重排序mb()阻止跨越屏障的载入和存储动作重新排序smp_ rmb()在SMP上提供rmb()功能,在UP 上提供barrier()功能smp_ read_ brier_ depends()在SMP上提供read_ barrier_ depends()功能,在UP上提供barrier()功能smp_ wmb()在SMP上提供wmb()功能,在UP上提供barrier()功能smp_ mb()在SMP上提供mb()功能,在UP上提供arrier()功能barrier()阻止编译器跨越屏障对载入或存储操作进行优化 为了使得上面的小例子能正确执行,用上表中的函数修改线程A的函数即可: 3.12 读-复制-更新(RCU) RCU(Read-Copy-Update,读-复制-更新)机制可以看做是读写自旋锁的扩展。在rwlock机制中读自旋锁和写自旋锁时互斥的,但是在RCU机制中读和写操作是可以并发执行的。 RCU可以看做是读写自旋锁(rwlock)的高性能版本,比起读写自旋锁,RCU的优点是既允许多个对共享数据进行的读操作同时执行,又允许多个对共享数据进行读操作和写操作的线程同时执行。但是,RCU并不能替代读写自旋锁,因为如果写操作比较多的情况下,对读操作的性能提高是不能弥补写操作导致的性能消耗的。因为使用RCU时,多个写操作之间的同步开销会很大,它需要延迟数据结构的释放,赋值被修改的数据结构,它也必须使用某种锁机制来同步并发其他对共享数据进行修改的写操作。 3.13 每CPU变量 每CPU变量(per-cpu-variable)是内核中一种重要的同步机制。顾名思义,每CPU变量就是为每个CPU构造一个变量的副本,这样多个CPU相互操作各自的副本,互不干涉。比如我们标识当前进程的变量current_task就被声明为每CPU变量。 每CPU变量的特点: 1、用于多个CPU之间的同步,如果是单核结构,每CPU变量没有任何用处。 2、每CPU变量不能用于多个CPU相互协作的场景。(每个CPU的副本都是独立的) 3、每CPU变量不能解决由中断或延迟函数导致的同步问题 4、访问每CPU变量的时候,一定要确保关闭进程抢占,否则一个进程被抢占后可能会更换CPU运行,这会导致每CPU变量的引用错误。 每CPU变量分为静态和动态两种,静态的每CPU变量使用DEFINE_PER_CPU声明,在编译的时候分配空间;而动态的使用alloc_percpu和free_percpu来分配回收存储空间。下面我们来看看Linux中的具体实现: /arch/i386/kernel/vmlinux.lds.S根据指明的section属性,我们知道在内核启动过程中该节所占的存储空间被释放了 具体的代码参加start_kernel中调用的setup_per_cpu_areas函数。代码如下: /arch/alpha/kernel/irq.c里同样也使用到了per_cpu 所以每CPU在内核的使用还是很重要的 4. 进程的同步机制 回顾进程的通讯方式有:管道(2种),信号,信号量,消息队列,共享内存,套接字。而进程的异步通信方式 。 5. 线程的同步机制 线程因为是进程里的,资源共享,所以容易产生临界区,需要用到锁,或者信号量来同步互斥量: 采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。信号量: 它允许同一时刻多个线程来访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。事件(信号):通过通知操作的方式来保持多线程同步,还可以方便实现多线程优先级的比较作。临界区:临界区对象和互斥对象非常相似,只是互斥量允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。 临界区: 当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止。
Linux 内核/服务器开发/架构师 面试题、学习资料、教学视频和学习路线图,免费分享有需要的可以自行添加学习交流群 Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈 学习视频教程:https://ke.sigusoft.com/course/?flowToken=
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/87578.html