操作系统——并发相关问题

目录

1. 什么是操作系统中的并发(Concurrency)?

1.1 并发的概念

1.2 并发引起的问题

1.3 编程中的并发

1.3.1 并发计算与并行计算

1.3.2 为什么要使用并发计算?

2. 基于多处理器和多核处理器的操作系统设计考量

2.1  基于对称多处理器的操作系统设计考量

2.2  基于多核处理器的操作系统设计考量

3. 并发:互斥和同步

3.1  与并发相关的关键术语

3.2  并发的硬件支持

3.2.1 禁止中断

3.2.2 专用机器指令

3.3  常用的并发机制(互斥和有序)

4. 几种主要操作系统的并发机制(Windows,UNIX,Linux)

4.1  Windows系统的并发机制

4.1.1  等待函数(事件)

4.1.2  派发对象

4.1.3  临界区(Critical Sections)

4.1.4  轻量读写锁和条件变量(Critical Sections)

4.1.5  无锁机制(Lock-Free)

4.2  UNIX系统的并发机制

4.2.1  管道

4.2.2  消息

4.2.3  共享内存

4.2.4  信号量

4.2.5  信号

4.2.6  其它Unix版本中的个性化并发机制(以FreeBSD为例)

4.3  Linux内核的并发机制

4.3.1 原子操作(Atomic Operations)(关键部分是lock前缀指令)

4.3.2  自旋锁(Spinlocks)

4.3.3  信号量

4.3.4  屏罩(Barriers)

4.3.5  读-复制-更新原语操作(Read-Copy-Update,简记为RCU)


1. 什么是操作系统中的并发(Concurrency)?

1.1 并发的概念

    并发(concurrency)是一个系统(程序、网络、计算机、等等)的属性,在系统中,多个计算同时执行,并且存在潜在的相互交互。计算从开始,执行到结束都处于重叠时间周期上;它们可以以精准的相同时间片运行(例如,并行),但不是必须

    在单处理器多道程序设计系统中,进程在时间上交错运行从而产生表面上的同时执行(每个进程轮流使用处理器,看起来好像是同时运行)。尽管并不能实现真正的并行处理,而且在进程之间来回切换还存在一定的资源开销,但交错执行在处理效率和程序结构方面提供了主要优势。而在多处理器系统中,可能不仅仅是交错运行多进程,而是重叠运行多进程。交错运行和重叠运行都是并发问题。

1.2 并发引起的问题

并发会给操作系统设计带来如下问题(在以下描述中,线程可以看成一个轻量级的进程):

(1) 使全局资源共享充满危险。例如,两个进程使用同一个全局变量并且都在此变量上执行读写操作。则在其中各种读和写的次序就变得很关键。

(2) 对于操作系统而言,最佳地管理资源分配变得困难。例如,进程A可以请求使用一个专用I/O通道,并获得了控制权,然后在使用通道之前被挂起。它可能不希望操作仅仅是锁住通道并且阻止其它进程对它的访问,事实上,这会导致产生死锁条件。

(3) 使得开发人员定位程序错误变得非常困难,因为错误常常不确定且不可重现。

1.3 编程中的并发

并发是在编程逻辑中实现的,方法是通过显式地为系统内的计算单元或进程提供单独的执行点或控制线程。并发允许这些计算避免等待所有其他计算完成——就像顺序编程中的情况一样。

1.3.1 并发计算与并行计算

并发计算包含并行计算,但是两者有一定的区别。并行计算以准确的相同时间片运行,典型目标在于优化模块计算的性能。这使得并行计算利用多个内核,因为每个控制线程同时运行,并在运行时间片内占用整个内核的时钟周期。因此,并行计算不可能在单核处理器上完成(单核处理器被独占后没法调度作他用,而并行计算则可适用于单核)。这与并发计算不同并发计算侧重于计算重叠的生命周期(lifetime),而不一定是它们均等的执行时间片(time slices)。 例如,一个进程的执行步骤可以分解为时间片,如果整个进程在其时间片内没有完成,那么它可以在另一个进程开始时暂停。

1.3.2 为什么要使用并发计算?

并发计算的终极目标在于使执行运算的计算机的资源利用率最大化。这通常会导致执行时间的速度提升,因为程序不再受制于正常的顺序行为。从2000年代初开始,个人计算机的一个普遍趋势是使用多核处理单元而不是单个全能CPU内核。这有助于通过将负载分散到所有内核来优化具有多个线程的进程的总执行时间。

在现代编程语言中使用的并发的概念通常是通过进程内的多线程来体现的。多线程的机制允许同一个进程运行多个线程。因此,获得了并行化的优点(更快地计算,更有效地利用计算机资源,等等)但也带来了并行化的问题,这就是为什么的编程语言为使用称为全局解释锁(Global Interpreter Lock,简记为GIL)机制的原因。GIL机制最常见于Python和Ruby的标准实现(分别对应于CPython和Ruby MRI),它会阻止超过一个线程执行同时执行——甚至基于多内核也是这样。这看起来似乎是一个巨大的设计缺陷,但是GIL的存在会阻止任何线程不安全的行为,这意味着在一个线程上执行的所有代码都不会以危及其他线程安全执行的方式操作任何共享数据结构。通常使用 GIL 的语言实现会提高单线程程序的速度,并使与C库的集成更容易(因为它们通常不是线程安全的),但代价是失去多线程功能。

但是,如果通过具有GIL的语言实现的并发性是一个非常重要的问题,那么通常有一些方法可以绕过这个障碍。虽然多线程不是一种选择,但通过GIL解释的应用程序仍然可以设计为完全在不同的进程上运行——每个进程都有自己的GIL。

2. 基于多处理器和多核处理器的操作系统设计考量

2.1  基于对称多处理器的操作系统设计考量

在对称多处理器系统中,内核可以在任何处理器上运行,典型的情况是,每个处理器从可用进程或线程池中进行自我调度。内核可以构建为多进程或多线程,允许内核的各个部分并行地执行。对称多处理方案使得操作系统复杂化。为了使系统可以从同时执行的操作系统的多个部分共享资源(例如,数据结构)并使进程协调行动(例如,访问设备),操作系统设计者必须处理这种复杂性。必须使用某些技术来解决和同步对共享资源的使用请求。

对称多处理操作系统管理处理器和其它计算机资源,以便用户可以将其视为多道程序单处理器系统一样。用户可以构建基于多进程或进程内多线程的应用程序,而不用考虑是否可以获得单个处理器或多个处理器。因此,多处理器操作系统必须提供多道程序系统的所有功能,以及适应多处理器的附加功能。关键设计问题包括以下:

(1) 并行进程或线程:内核例程必须可重入以允许多个处理器同时执行同一段内核代码。由于多个处理器执行内核代码的相同或不同部分,内核表和管理结构必须恰当地避免数据中断或无效操作。

(2) 并行进程或线程:任何处理器都可以执行调度,这使执行调度策略和确保避免调度器数据结构中断的任务复杂化。假如使用内核层级的多线程,那么就有机会在多个处理器上同时调度来自同一进程的多个线程

(3) 同步:由于多个活动进程具有潜在的访问共享地址空间或才共享I/O资源的可能性,因此,必须注意提供有效的同步机制。同步机制是一种设施,用于强制互斥和事件按序(注:同步是两个点,一是互斥,即控制并发访问共享资源;二是有序,控制事件按设计的先后次序执行)。在多处理器系统中,常用同步机制是共享资源加锁策略。

(4) 内存管理:基于多处理器的内存管理必须处理在单处理器计算机内存管理上遇到的所有问题。此外,系统需要探索可获得的硬件并行化以实现性能最大化。当多个处理器共享一个分页或一个分段的时候,基于不同处理器的分页机制必须协作一致强制使用并发机制,以确定页的置换。物理分页的重用是系统设计最关注的问题;即,系统必须保证,在页用作新的使用时必须保证系统不再访问任何旧的内容。

(5) 可靠性和容错:在处理器执行失败的情况下,操作系统必须提供优雅的降级机制。必须让调度器和内核的其它部分能即时感知到系统的失败并相应地重建管理表。

由于多处理器操作系统设计问题往往包括多道程序设计的单处理器器问题的扩展方案,因此,我们不单独对待多处理器操作系统。另外,具体的多处理器问题取决于合适的上下文环境。

2.2  基于多核处理器的操作系统设计考量

多核处理器操作系统设计的所有问题,包括了所有对称多处理器操作系统设计所面对的所有问题。而且,还有其它更多的问题需要考量。问题之一便是潜在并行化的尺度。当前的多核供应商在一个单芯片上集成了数十个内核。随着每一代后续的处理器技术的产生,内核数量和共享及专用缓存的数量增加,因此,我们进入了“多核系统”时代。

一个多核的多核操作系统的设计挑战在于高效地利用并控制多核的处理能力并智能化地管理大量片上资源一个核心要点是如何将多核系统的内在并行性与使应用程序的性能要求相匹配。事实上,在当代多核系统中,潜在的并行性存在于三个层级上。首先,是存在于每个内核处理器上的硬件并行化,称为指令级并行化,这可能会也可能不会被应用程序程序员和编译器发掘。第二,是存在于每个处理器上的多道程序和多线程程序执行的潜在并行化(姑且称为单核处理器程序级并行化)。第三,是并单应用程序在跨多核的并发进程或线程内运行时的潜在并行化(姑且称为跨多核处理器程序级并行化)。对于后两种并行化,没有强大有效的操作系统的支持,不能有效地使用硬件资源。

3. 并发:互斥和同步

操作系统设计的核心问题是对线程和进程的管理。主要是:

(1) 多道程序设计(multiprogramming):单处理器内的多进程的管理。

(2) 多处理程序设计(multiprocessing):多处理器内的多进程的管理。

(3) 分布式处理程序设计(distributed processing): 管理在多个分布式计算机系统上执行的多个进程。最近集群的扩散就是这种系统的一个典型例子。

所有这些管理的核心,也是操作系统设计的核心,就是并发,并发包含许多设计问题,包括进程之间的通信、资源的共享和竞争(例如内存、文件和 I/O 访问)、多个进程的活动同步以及进程的处理器时间分配。我们将看到,这些问题不仅出现在多处理和分布式处理环境中,而且出现在单处理器多道程序系统中

并发会出现在三种不同的运行上下文化环境中:

(1) 多道程序:发明多道程序是为了允许在多个活动应用程序之间动态共享处理时间。

(2) 结构化程序:作为模块化设计和结构化编程原理的扩展,一些应用程序可以有效地编写为一组并发进程。

(3) 操作系统结构:同样,结构化优势也适用于系统程序,我们已经看到,操作系统本身通常是通过一组进程或一组线程来实现的。

3.1  与并发相关的关键术语

  1. 原子操作(Atomic Operation)

以一系列似乎不可分割的一条或多条指令的形式实现的一种功能或操作;即,任何其他进程都不能看到其中间状态或中断其操作。指令序列保证作为一个组执行,或者根本不执行,对系统状态没有明显的影响。原子性保证了与并发进程的隔离。

临界区(Critical Section)

进程中一段需要访问共享资源的一段代码,并且当另一个进程在相应的代码段中时当前进程不得执行该代码段。

死锁(Deadlock)

两个或多个进程无法继续执行而无限期地等待下去的情况,因为每个进程互相都在等待其他进程中的某一个执行某项操作。

活锁(Livelock)

两个或多个进程在不做任何有用工作的情况下响应其他进程的变化,从而不断改变其状态的情况。

互斥(Mutual Exclusion)

当一个进程位于访问共享资源的临界区时,其他进程不得位于访问任何这些共享资源的临界区。

竞争条件(Race Condition)

多个线程或进程读取和写入共享数据项的情况,最终结果取决于它们执行的相对时间。

饿死(Starvation)

调度程序无限期地忽略一个可运行进程的情况;尽管它能够继续,但它从未被选中。

3.2  并发的硬件支持

3.2.1 禁止中断

在单处理器系统中,并发的进程不会重叠执行;它们只会交错执行。另外,进程调用了操作系统服务或者直到它被中断,进程才会停下来,否则它会一直执行。因此,要保证互斥,只要阻止中断即可。此功能可以以操作系统内核定义的原语(primitives)形式提供,用于禁用和启用中断。因为临界区不能中断,所以保证了互斥。然而,这种方法的代价很高。由于处理器交错进程的能力有限,因此执行效率可能会显着降低。另一个问题是这种方法不适用于多处理器架构。当计算机包含多个处理器时,可能(并且通常)一次执行多个进程。在这种情况下,禁用的中断不能保证互斥

3.2.2 专用机器指令

在多处理器配置中,多个处理器共享对公共主存储器的访问。在这种情况下,不存在主从关系;相反,处理器在对等关系中独立运行。处理器之间没有可以基于互斥的中断机制。

如前所述,在硬件层级,对内存位置的访问是独占式的。以此为基础,处理器设计人员提出了几个机器指令,它们以原子方式(即指令被视为单步操作,不可中断)执行两个动作,例如读取和写入或读取和测试,使用一个指令获取周期对单个内存位置进行读取和写入或读取和测试。在指令执行期间,对于引用该内存位置的任何其他指令,硬件都会阻止其对该内存位置的访问

这些指令主要是可以加前缀LOCK的指令,这些指令操作简单,运行速度快,可能是采取锁地址总结的方式实现。LOCK 前缀只能与以下写入内存操作数的指令形式一起使用:ADC、ADD、AND、BTC、BTR、BTS、CMPXCHG、CMPXCHG8B、CMPXCHG16B、DEC、INC、NEG、NOT、OR、SBB、SUB 、XADD、XCHG 和 XOR。如果 LOCK 前缀与任何其他指令一起使用,则会发生无效操作码异常(参见Intel和AMD官方文档)。

3.3  常用的并发机制(互斥和有序)

常用的并发机制主要有以下几种:

常用的并发机制主要有以下几种:

信号量(Semaphore)

用于进制之间表示信号的整数值。在信号量上,只可能有三个操作,并且全部操作都是原子操作:初始化,递增,递减。递减操作可能导致进程阻塞,而递增操作可能会导致解除阻塞。也称为计数信号量通用信号量

二值信号量(Binary Semaphore)

仅取值0和1的信号量。

互斥(Mutex)

类似于二值信号量,二者之间的主要区别在于,锁住信号量(置为0)的进程必须是解除锁信号量(置为1)的同一进程。

条件变量(Condition Variable)

一种数据类型,用于阻止进程或者线程,直到某个特定的条件变为真值(true)。

监视器(Monitor)

一种编程语言的结构,它将变量,访问例程,和初始化代码封装在一个抽象数据结构类型中。监视器的变量仅通过其例程才能访问,并且在任何时刻,仅允许一个激活进程访问监视器。可能有一个监视器队列排队等候访问监视器。

事件标识(Event Flags)

用作同步机制的一个内存字(word)。应用程序代码可以与事件标识中的每一个位相关联。一个线程可以通过检查对应标识中的一个位或多个位,从而等待一个事件或组合事件的产生。当在满足要求的位被设置之前(组合事件,多个事件同时满足用“与”,或者至少一个事件满足用“或”),线程都处于阻塞状态(或者直到触发超时)。

邮件/消息(Mailboxes/Messages)

两个进程之间交换消息(同一系统或者不同的系统),可以用于信息同步。

自旋锁(Spinlocks)

一种互斥机制,其中进程无限循环地执行,等待锁定变量的值指示可用为止。

4. 几种主要操作系统的并发机制(Windows,UNIX,Linux)

4.1  Windows系统的并发机制

    Windows提供线程之间的同步作为对象体系结构的一部分。同步的最重要的方法是执行程序派发对象(Dispatcher Objects),用户模式临界区(Critical Sections),轻量读写锁(Slim Reader/Writer Locks),条件谈到量以及锁释放操作(Lock-Free Operations)。派发对象使用等待函数。

4.1.1  等待函数(事件)

等待函数允许线程阻止自己继续执行。等待函数只有在满足条件后才会返回。这种等待函数限制了使用条件。当等待函数被调用的时候,它会检测是否满足返回条件。如果不满足返回条件,则调用线程处于等待状态,此状态不会占用处理器时间。等待单个事件的函数是WaitForSingleObject/WaitForSingleObjectEx,等待多个事件的函数是WaitForMultipleObjects/WaitForMultipleObjectsEx,只体使用参数和注意事项参见Windows官方文档。

4.1.2  派发对象

Windows执行程序用来实现同步工具的机制是派发对象族。如下列表所示:

对象类型

定义

何时改变状态

对等待线程的影响

通知事件

发生系统事件的一个通知信号

线程设置事件时

所有线程释放

同步事件

发生系统事件的一个通知信号

线程设置事件时

一个线程释放

互斥量

提供互斥能力的一种机制;

相当于二值信号量

拥有线程或其它线程;

释放锁时

一个线程释放

信量号

调节可以使用资源的线程数的计数器

信号记数下降到0

所有线程释放

等候定时器

记录时间流逝的计数器

触及定时时间或者定时超时

所有线程释放

文件句柄

打开的文件或 I/O 设备的实例

I/O操作完成

所有线程释放

进程

程序调用,包括运行程序所需的地址空间和资源

最后一个线程中止

所有线程释放

线程

进程内的一个执行实体

线程中上

所有线程释放

上表中的前5个对象设计用途是专用于同步。其它对象设计用于其它用途,但也可以用于同步。

每一个派发对象要么是受信状态,要么是未受信状态。一个未受信对象可以阻塞线程;当派发对象受信后线程获得释放以继续执行(否则被阻止)。机制很简单:线程使用派发对象的句柄向Windows发起一个Wait请求。当一个派发对象进入受信状态时,Windows释放一个或多个在派发对象上等待的线程对象

事件对象用于线程之间发送通知时特别有用。互斥对象用于互斥式地独占访问共享资源,每次仅允许一个线程访问共享资源。因此,它充当了二值信号量的角色。当互斥体进入受信状态时,在互斥体上等待的某一个线程某得释放得以继续执行。互斥体可以用于不同进程之间的线程同步。与互斥体一样,信号量也可以被多进程共享访问。Windows信号量是记数信号量,本质上,是可等待计时器对象在特定时间和/或定期发出信号。

4.1.3  临界区(Critical Sections)

临界区提供的同步机制与互斥体对象类似,区别在于临界区仅可用于单个进程内的线程。事件、互斥体、信号量也可用于单进程应用,但对于互斥而言临界区更快更高效。进程负责为临界区分配内存。一般应用是,声明一个CRITICAL_SECTION变量,调用InitializeCriticalSection(或InitializeCriticalSectionAndSpinCount,推荐使用本函数)完成初始化,线程在使用之前调用EnterCriticalSection或TryEnterCriticalSection请求拥有临界区所有权,调用LeaveCriticalSection函数释放临界区拥有权。EnterCriticalSection会无了期等待获得拥有权,TryEnterCriticalSection尝试获得临界区,而无需调用函数无限等待。

当线程试图获得互斥权限的时候,临界区采用了一种较为复杂的算法。如果系统是多处理器系统,代码将尝试获得一个自旋锁,当昨界区占用时间较短的情况下,这种算法效果良好。自旋锁针对当前拥有临界区的线程正在另一个处理器上执行的情况进行了有效的优化。如果在合理的迭代次数内无法获得自旋锁,则使用调度程序对象来阻塞线程,以便内核可以将另一个线程调度到处理器上。

事实上,我们可以看一下EnterCriticalSection的反汇编代码:

sub         rsp,28h 

mov         rax,qword ptr gs:[30h] 

lock btr    dword ptr [rcx+8],0 

mov         rax,qword ptr [rax+48h] 

jae         00007FFF3E87FACC 

mov         qword ptr [rcx+10h],rax 

xor         eax,eax 

mov         dword ptr [rcx+0Ch],1 

add         rsp,28h 

ret 

其中btr(Bit Test and Reset)为位测试和重置指定,这个指令是硬件支持的,也就是说临界区的关键部分利用了硬件的互斥方法协助。

调度程序对象仅作为最后手段进行分配。大多数临界区都需要校正,但在实践中很少有争议。通过延迟分配派发对象,系统节省了大量内核虚拟内存。

4.1.4  轻量读写锁和条件变量(Critical Sections)

在Windows Vista及以上的版本中,Windows加入了一个用户模式的读写锁。与临界区类似,系统只有在尝试使用了自旋锁以后才进入内核阻塞模式。轻量(slim)”的意义在于它通常只需要分配一块指针大小的内存

要使用读写锁,需要声明一个SRWLOCK变量并调用InitializeSRWLock函数完成初始化,线程调用AcquireSRWLockExclusive或AcquireSRWLockShared获得锁,调用ReleaseSRWLockExclusive或ReleaseSRWLockShared释放锁。

同样,我们看一下的反汇编代码:

push        rbp 

push        rsi 

sub         rsp,58h 

xor         ebp,ebp 

mov         rsi,rcx 

mov         dword ptr [rsp+70h],ebp 

lock bts    qword ptr [rcx],0 

jb          00007FFF3E8790C0 

add         rsp,58h 

pop         rsi 

pop         rbp 

ret

关键代码处也是借助了硬件机制。

Windows也提供了条件变量,声明一个CONDITION_VARIABLE变量,使用InitializeConditionVariable函数完成初始化,条件变量可以与临界区和或轻量读写锁一起使用,因此有对于条件变量,有两个操作方法,SleepConditionVariableCS和SleepConditionVariableSRW,以原子的方式在指定的条件上休眠并释放指定的锁。有两个唤醒的方法,WakeConditionVariable和WakeAllConditionVariable,分别为唤醒一个和所有睡眠的线程。条件变量按以下方式使用:

(1) 获得独占锁

(2) while(predicate()==false) SleepConditionVariable()

(3) 执行保护操作

(4) 释放锁

4.1.5  无锁机制(Lock-Free)

Windows也严重依赖于互锁操作实现同步。互锁操作以硬件设施来确保对内存的--操作始终以一个原子操作的形式执行。例如,InterlockedIncrement和InterlockedCompareExchange,这些函数底层是处理器指令集中可以加前缀Lock的指令,由硬件实现锁的机制

下面分别看一下这两个函数的反编码代码:

InterlockedIncrement的反汇编代码:

lea         rax,[ldata] 

lock inc    dword ptr [rax]  

关键代码调用的是inc汇编指令,lock前缀表示硬件支持该指令锁的机制。

InterlockedCompareExchange的反汇编代码:

调用:

    LONG ldata = 10;

    InterlockedCompareExchange(&ldata, 100, 10);

反汇编代码:

mov         eax,64h 

mov         dword ptr [rsp+24h],eax 

lea         rcx,[ldata] 

mov         eax,0Ah 

mov         edx,dword ptr [rsp+24h] 

lock cmpxchg dword ptr [rcx],edx

关键代码调用的是cmpxchg汇编指令,lock前缀表示硬件支持该指令锁的机制。

很多同步原语(primitives)在它们的实现中使用互锁操作,然而,程序员也可以在不想使用软件锁的场景下进行这些操作。这些所谓的无锁(lock-free)同步原语的优点是,线程在保持锁的情况下永远不能从处理器切换出去(比如在时间片结束时)。因此,它们无法阻止另一个线程运行。

可以通过互锁操作构建更复杂的无锁原语,尤其是Windows SLists,它提供了无锁后进先出队列。SList使用以下功能进行管理:InterlockedPushEntrySList和InterlockedPopEntrySList。

4.2  UNIX系统的并发机制

Unix提供了处理器之间通信和同步的各种机制。下面仅列举几种最重要的机制:

管道(Pipes)

消息(Messages)

共享同存(Shared Memory)

信号量(Semaphores)

信号(Signals)

4.2.1  管道

UNIX对操作系统开发的最重要的贡献之一便是设计出了管道。受协同概念的启发,

道是一个循环缓冲区,允许两个进程基于生产者-消费者模型通信。因此,它是一个先进先出的队列,由一个进程写入而由另一个进程读取。

当管道被创建的时候,设定一个初始字节大小。当进程尝试向管理写入数据的时候,如果管道有足够的存储空间,则写入请求立即执行写入操作;否则,系统阻塞写入时程。与此类似,如果读取进程尝试读取超出当前已有管理数据的数据量,则系统阻塞读取进程;否则读取进程立即执行读取操作。操作系统会强制互斥:即,每次只允许一个进程访问管道

Unix有两种管道,命名管道和无名管道。只有关联进程才可以共享无名管道,而关联进程和非关联进程均可以共享命名管道。

4.2.2  消息

一个消息是具有一种相伴类型的字节块。UNIX为进程提供了两个系统调用msgsnd和msgrcv用于加入消息的传递。每个进程与一个消息队列关联,其功能类似邮箱。

消息发送者为每个发送的消息指定具体的类型,这可以作为接收方的选择标准。接收方可以按先进先出的方式或按字节流的方式取出数据。尝试向一个已满的队列发送消息将被阻塞。一个进程试图读取空队列也将被阻塞。如果某个进程尝试读取某个类型的消息,但由于不存在该类型的消息而失败,则该进程不会被阻止。

4.2.3  共享内存

UNIX中提供的最快的进程间通信形式是共享内存。这是多个进程共享的虚拟内存的公共块。使用与读写虚拟内存空间的其他部分相同的机器指令来处理共享内存的读写。权限是进程的只读或读写权限,根据每个进程确定。互斥约束不是共享内存设施的一部分,但必须由使用共享内存的进程提供。

4.2.4  信号量

信号量是操作系统定义的一个原语,几个操作可以同时执行,并且递增和递减操作的值可以大于 1,但信号量的值永远不可以小于0。操作系统内核自动处理所有的请求操作,在信号量上的所有操作完成之后其它进程不能访问信号量。一个信号量由以下元素组成:

(1) 信号量的当前值

(2) 最后在进程上进行操作的进程ID

(3) 等待信号量值大于其当前值的进程数

(4) 等待信号量值为零的进程数

与信号量相关的是在该信号量上阻塞的进程队列。

信号量实际上是以集合的形式创建的,一个信号量集合由一个或多个信号量组成。有一个 semctl 系统调用允许同时设置集合中的所有信号量值。此外,还有一个 sem_op 系统调用,它将信号量操作列表作为参数,每个信号量操作都定义在一组信号量中的一个上。 进行此调用时,内核一次执行一个指示的操作。 对于每个操作,实际功能由值 sem_op 指定。 以下是可能性:

(1) 如果 sem_op 是正数,内核增加信号量的值并唤醒所有等待信号量值增加的进程。

(2) 如果 sem_op 为0,内核检查信号量值。如果信号量值等于0,内核继续执行列表中的其他操作。否则,内核增加等待此信号量值变为0的进程数量,并挂起进等待信号量值变为0的事件的进程。

(3) 如果 sem_op 为负且其绝对值小于或等于信号量值,内核将 sem_op(负数)添加到信号量值。如果结果为0,内核唤醒所有等待信号量值等于0的进程。

(4) 如果 sem_op 为负且其绝对值大于信号量值,则内核在信号量值增加时挂起进程。

以下看一下符合POSIX规范的UNIX信号量实现:

POSIX信号量允许进程和线程同步它们的动作。一个信号量就是一个整数,其值不允许小于0。在信号量上允许两个操作:按1的大小递增(使用函数sem_post)信号量;按1的大小递减(使用函数sem_wait)信号量。如果当前信号量值递减到0,将一个sem_wait操作将阻塞,直到其值大于0才解除阻塞。POSIX信号量以两种形式出现:命名信号量有无名信号量。

(1) 命名信号量:命名信号量通过反斜线+名字的形式(/somename)标识,即,以空结尾的最大长度为NAME_MAX-4(即251)的字符串由斜线开头并后接一个或多个字符组成,字符部分不能含有斜线。两个进程可以通过传递给函数sem_open相同的名称人而操作同一个信号量。sem_open创建一个新的命名信号量或者打开一个已存在的命名信号量,在信号量被打开之后,就可以使用函数sem_post和sem_wait对其进行操作。但一个进程使用完毕信号量之后,使用函数sem_close关闭信号量,当所有信号量都使用完毕此信号量之后,可以使用sem_unlink函数将其从系统中移除。

(2) 无名信号量(基于内存的信号量):一个无名信号量没有名称。而是位于一块由多线程(线程共享信号量)或多进程(进程共享信号量)共享的内存区域(例如,全局变量)。线程共享信号量必须置于线程共享区域(例如,使用函数shmget创建的System V共享内存段,或者使用函数sem_open创建的POSIX共享内存)。无名信号量在使用前必须使用函数sem_init初始化,然后才可以使用sem_post和sem_wait对其进行操作。当不再使用时,在释放这块内存之前,应使用函数sem_destroy将其销毁。

需要注意,sem_postsem_wait在其内部真正需要互斥的地方,仍然使用了硬件层的lock前缀的指令

4.2.5  信号

信号是一种软件机制,用于通知在异步事件发生时通知进程。信号类似于硬件中为,但是不使用优先级。即,平等地对待所有信号;同时发生的信号一次只处理一个,没有特别的顺序要求。进程之间可以互相发送信号,或者内核也可以内部发送信号。因为每个信号以一个位维持,因此,不能对给定类型的信号排队。信号只有在进程被唤醒运行后才被处理,或者在进程从系统调用返回后的任何时候开始处理。进程可以通过执行某些缺省的动作 (比如,终止)、运行信号处理函数、或忽略信号来响应信号。下表中列出了UNIX SVR4中定义的信号:

名称

描述

01

SIGHUP

挂起信号;当内核认为那个进程的用户没有做任何有用工作的时候发送给进程。

02

SIGINT

中断信号

03

SIGQUIT

中止信号;由用户发送用以引导进程停止并产生内核转储文件

04

SIGILL

无效指令信号

05

SIGTRAP

跟踪陷阱信号;触发运行处理跟踪的代码

06

SIGIOT

即输入/输出陷阱指令信号(input/output trap)

07

SIGEMT

模拟器陷阱指令信号(emulator trap)

08

SIGFPE

浮点异常信号

09

SIGKILL

中止进程信号

10

SIGBUS

总线错误信号

11

SIGSEGV

段越界信号;用于处理尝试访问虚拟地址空间以外的内存空间

12

SIGSYS

系统调用参数错误通知信号

13

SIGPIPE

向无读取操作附加的管道写入信息通知信号

14

SIGALRM

告警时钟信号;当一个进程在一个时间周期以后希望接收信号时发起

15

SIGTERM

软件中止信号

16

SIGUSR1

用户定义信号1

17

SIGUSR2

用户定义信号2

18

SIGCHLD

子进程中止信号

19

SIGPWR

上电失败信号

4.2.6  其它Unix版本中的个性化并发机制(以FreeBSD为例)

FreeBSD内核中使用的锁,允许在内核中进行高效的多进程程序处理。锁可以通过多种方式实现。 数据结构可以由互斥锁或 lockmgr(9) 锁保护。一些变量只需使用原子操作来访问它们就可以得到保护。

4.2.6.1 Mutexes

互斥锁只是用来保证互斥的锁。具体来说,互斥锁一次只能由一个实体拥有。如果另一个实体希望获得已经拥有的互斥锁,它必须等到互斥锁被释放。在FreeBSD内核中,互斥锁归进程所有。

互斥锁可以递归获取,但它们的目的是保持极短的时间。具体来说,持有互斥锁时可能无法休眠。如果您需要在休眠期间持有锁,请使用lockmgr(9)锁。

每个互斥锁都有几个属性:

变量名:内核源代码中 struct mtx变量的名称。

逻辑名:mtx_init分配给它的互斥锁的名称。此名称显示在KTR跟踪消息和见证错误和警告中,用于区分见证代码中的互斥锁。

类型:就MTX_* 标志而言的互斥锁类型。每个标志的含义与其在mutex(9)中记录的含义相关:

MTX_DEF:休眠互斥

MTX_SPIN:自旋互斥

MTX_RECURSE:允许递归的互斥。

保护对象:此条目保护的数据结构或数据结构成员的列表。对于数据结构成员,名称将采用[结构名称.成员名称]的形式。

依赖函数:只有在持有此互斥体时才能调用的函数。

互斥列表:

变量名

逻辑名

类型

保护对象

依赖函数

sched_lock

"sched lock"

MTX_SPIN | MTX_RECURSE

_gmonparam, cnt.v_swtch, cp_time, curpriority, mtx.mtx_blocked, mtx.mtx_contested, proc.p_procq, proc.p_slpq, proc.p_sflag, proc.p_stat, proc.p_estcpu, proc.p_cpticks proc.p_pctcpu, proc.p_wchan, proc.p_wmesg, proc.p_swtime, proc.p_slptime, proc.p_runtime, proc.p_uu, proc.p_su, proc.p_iu, proc.p_uticks, proc.p_sticks, proc.p_iticks, proc.p_oncpu, proc.p_lastcpu, proc.p_rqindex, proc.p_heldmtx, proc.p_blocked, proc.p_mtxname, proc.p_contested, proc.p_priority, proc.p_usrpri, proc.p_nativepri, proc.p_nice, proc.p_rtprio, pscnt, slpque, itqueuebits, itqueues, rtqueuebits, rtqueues, queuebits, queues, idqueuebits, idqueues, switchtime, switchticks

setrunqueue, remrunqueue, mi_switch, chooseproc, schedclock, resetpriority, updatepri, maybe_resched, cpu_switch, cpu_throw, need_resched, resched_wanted, clear_resched, aston, astoff, astpending, calcru, proc_compare

vm86pcb_lock

"vm86pcb lock"

MTX_DEF

vm86pcb

vm86_bioscall

Giant

“Giant”

MTX_DEF | MTX_RECURSE

几乎所有

许多

callout_lock

“callout_lock”

MTX_SPIN | MTX_RECURSE

callfree, callwheel, nextsoftcheck, proc.p_itcallout, proc.p_slpcallout, softticks, ticks

4.2.6.2 共享互斥锁

这些锁提供了基本的读写类型功能,并且可以被休眠进程持有。目前由lockmgr(9)支持。

共享锁列表:

变量名

保护对象

allproc_lock

allproc zombproc pidhashtbl proc.p_list proc.p_hash nextpid

proctree_lock

proc.p_children proc.p_sibling

4.2.6.3  原子化保护变量

原子保护变量是不受显式锁保护的特殊变量。相反,对变量的所有数据访问都使用 atomic(9)中描述的特殊原子操作。很少有变量会以这种方式处理,尽管其他同步原语(例如互斥锁)是使用原子保护变量实现的。

mtx.mtx_lock

4.3  Linux内核的并发机制

Linux包括所有其它UNIX操作系统(例如,SVR4)具有的并发机制,包括管道、消息、共享内存、和信号。Linux还支持一种特别类型的信号,称为实时信号(real-time,简记为RT)。这些是POSIX.1b实时扩展功能的一部分。RT信号主要在三个方面区别于标准UNIX(或POSIX.1)信号:

(1) 支持以优先级次序投递信号。

(2) 多个信号入队列。

(3) 对于标准信号,可以向目标进程发送无值信号或消息;这只是一个通知。对于RT信号,可以附带信号发送一个值(整数或者指针)。

Linux也包括一组丰富的并发机制,特别是针对于线程在内核模式运行的时候采用的并发机制。即,这些是内核中使用的机制,用于在内核代码的执行中提供并发性。下面考察一些并发机制。

4.3.1 原子操作(Atomic Operations)(关键部分是lock前缀指令)

Linux提供了一组操作,确保在变量上执行的操作是原子操作。这些操作可以用以避免简单的竞争条件。一个原子操作无中断无干扰地执行。在单处理器系统中,一个原子操作一旦启动,直到它完成,整个操作过程不能被中断在多处理器系统中,变量在锁定状态下被线程操作,直到操作完成才释放锁

在Linux中定义了两种原子操作:在整数上的操作和在位图上的操作(在一个位图的某个位上操作)。这些操作必须在实现Linux的任何架构(指的是硬件架构,例如,ARM架构、X64架构、等等)上实现。对于某些架构,对应于原子操作有相应的汇编语言指令。在某它的一些架构上,采用锁定内存地址总线的方式来确保操作的原子性。

(1) Linux中整数上的原子操作函数(以IA64架构为例说明)

函数原型

说明

ATOMIC_INIT (int i)

声明时:将atomic_t初始化为i

int atomic_read(atomic_t *v)

读取v指向的值(原子读)

实现代码:

#define atomic64_read(v)  (*(volatile long *)&(v)->counter)

void atomic_set(atomic_t *v, int i)

将v指向的值设置为i

#define atomic64_set(v,i)  (((v)->counter) = (i))

作宏定义看,这个函数是还没进入并发环境时用于设置变量值时调用的。

void atomic_add(int i,atomic_t *v)

将i的值加到v指向的变量

实现代码:

static __inline__ long

ia64_atomic64_add (__s64 i, atomic64_t *v)

{

         __s64 old, new;

         CMPXCHG_BUGCHECK_DECL

         do {

                   CMPXCHG_BUGCHECK(v);

                   old = atomic64_read(v);

                   new = old + i;

         } while (ia64_cmpxchg(acq, v, old, new, sizeof(atomic64_t)) != old);

         return new;

}

ia64_cmpxchg其实是一个宏定义:

#define cmpxchg64(ptr, o, n)                                           \

({                                                                       \

         __typeof__(*(ptr)) __ret;                             \

         __typeof__(*(ptr)) __old = (o);                                    \

         __typeof__(*(ptr)) __new = (n);                                  \

         alternative_io(LOCK_PREFIX_HERE                                     \

                            "call cmpxchg8b_emu",                          \

                            "lock; cmpxchg8b (%%esi)" ,                   \

                          X86_FEATURE_CX8,                              \

                          "=A" (__ret),                              \

                          "S" ((ptr)), "0" (__old),          \

                          "b" ((unsigned int)__new),             \

                          "c" ((unsigned int)(__new>>32))                  \

                          : "memory");                               \

         __ret; })

最终底层调用的是lock前缀的指令,互斥由硬件实现,下同

void atomic_sub(int i,atomic_t *v)

从v指向的变量上减去i

void atomic_inc(atomic_t *v)

v指向的变量叠加1

void atomic_dec(atomic_t *v)

v指向的变量递减1

int atomic_sub_and_test(int i, atomic_t *v)

从v指向的变量减1,如果结果为0返回1,否则返回0

int atomic_add_negative(int i, atomic_t *v)

将i加到v指向的变量,如果结果为负则返回1,否则返回0

int atomic_dec_and_test(atomic_t *v)

从v指向的变量减1,如果结果为0返回1,否则返回0

int atomic_inc_and_test(atomic_t *v)

v指向的变量叠加1,如果结果为0返回1,否则返回0

(2) Linux中位图上的原子操作函数(以IA64架构为例说明)

函数原型

说明

void set_bit(int nr, void *addr)

将nr设置到指针addr所指向的位图

void clear_bit(int nr, void *addr)

清理指针addr所指向的位图

void change_bit(int nr,void *addr)

反转 addr 指向的位图中的位 nr

int test_and_set_bit(int nr, void *addr)

在addr指向的位图中设置位nr; 返回旧位值

int test_and_clear_bit(int nr, void *addr)

清除addr指向的位图中的位nr; 返回旧位值

int test_and_change_bit(int nr, void *addr)

反转addr指向的位图中的位nr; 返回旧位值

int test_bit(int nr, void *addr)

返回 addr 指向的位图中位 nr 的值

对于整数的原子操作,使用专用的类型atomic_t,实际定义如下:

typedef struct {

         int counter;

} atomic_t;

如果是64位,定义为long

typedef struct {

         long counter;

} atomic64_t;

原子整数操作可以仅用于这种数据类型,其它的操作不允许使用这种数据类型。这种限制有以下的优点:

(1) 原子操作永远不会用于在某些情况下可能不受竞争条件保护的变量。

(2) 这种数据类型的变量受到保护,不会被非原子操作不当使用。

(3) 编译器不能错误地优化对值的访问(例如,通过使用别名而不是正确的内存地址)。

(4) 此数据类型用于屏蔽其实现中具体体系结构的差异。

原子数据类型的一个典型用法就是用于实现计数器

   

    原子位图操作对由指针变量指示的任意内存位置的位序列中的一个进行操作。因此,没有等价于原子整数操作所需的atomic_t数据类型。

4.3.2  自旋锁(Spinlocks)

在Linux中保护临界区的最通用的技术就是自旋锁。每次只能有一个线程访问自旋锁。任何其它尝试访问同一个自旋锁的线程将保持不断尝试(自旋,即无限循环),直到它可以访问锁为止。本质上,自旋锁是创建于内存位置上的整数,它可以被每个试图进入临界区之前的线程检测。假如其值为0,则线程将其值设置为1,然后进入线程的临界区(多线程共享的代码及其变量)。假如其值非0,则线程会继续循环检测其值,直到其值为0为止。自旋锁很容易实现,但是它有一个缺点,被锁阻止在外的线程在忙等待的模式下继续执行(空转,耗费处理器时间)因此,自旋锁在预期等待访问锁的时间极短的场景下最为有效,比如说,少于两个上下文切换的指令周期。使用自旋锁的基本形式如下:

spin_lock(&lock)

/* critical section */

spin_unlock(&lock)

4.3.2.1 基本自旋转锁

   这里说的基本自旋锁是相对于随便的读写自旋锁而言的,有4种风格:

函数原型

说明

void spin_lock(spinlock_t *lock)

获取具体的锁,如果需要会一直自旋直到可得

void spin_lock_irq(spinlock_t *lock)

类似于spin_lock,但是在本地处理器上也会禁用中断

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)

类似于spin_lock_irq,但也在flags中保存当前中断状态

void spin_lock_bh(spinlock_t *lock)

类似于spin_lock,但也禁用所有底半部的执行

void spin_unlock(spinlock_t *lock)

释放指定的锁

void spin_unlock_irq(spinlock_t *lock)

释放指定锁,并启用局部中断

void

spin_unlock_irqrestore(spinlock_t

*lock, unsigned long flags)

释放指定锁,并恢复中断到指定的之前状态

void spin_unlock_bh(spinlock_t

*lock)

释放指定锁,并启用所有下半部

void spin_lock_init(spinlock_t

*lock)

初始化指定的自旋锁

int spin_trylock(spinlock_t

*lock)

尝试获取自旋锁;如果当前锁被占用返回非0,可用则返回0

int spin_is_locked(spinlock_t

*lock)

如果当前锁被占用返回非0,可用则返回0

普通自旋锁(Plain):如果临界区的代码没有被中断处理程序执行,或者在临界区代码的执行期间禁用中断,则可以使用普通自旋锁(即,中断不打断临界区代码的执行)。

中断式自旋锁(_irq): 如果始终启用中断,则应该使用中断式自旋锁。

中断保存式自旋锁(_irqsave):假如不知道在临界区代码的执行期间,是否启用或禁用中断,则应该使用这种类型的自旋锁。当获得锁时,本地处理器上的当前中断状态被保存,在此锁释放后用于恢复为之前的中断状态。

下半部中断自旋锁(_bh): 当中断发生后,对应的中断处理程序会执行最小化的必要工作。一段称为“下半部(bottom half)”的代码会完成剩下的中断处理相关工作,允许尽快启用当前中断。下半部中断自旋锁允许禁用然后又启用下半部从而避免与受保护的临界区冲突。

当程序员明确地知道受保护数据不会部中断处理程序或者下半部访问,则可以使用普通自旋锁。否则,应选用对应合适的自旋锁。

在单处理器架构和多处理器架构上,自旋锁的实现有所不同。对于单处理器架构,应考量的是:假如内核抢占调度被关闭,因此执行内核代码的线程不会被中断,则在编译时,锁会被删除,因为不需要锁;如果启用内核抢占调度从而允许中断,那么自旋锁会再次编译掉(即,不会对自旋锁内存位置进行检测),而是简单地实现为启用/禁用中断的代码。对于多处理器架构,应考量的是:自旋锁被编译成实际上检测自旋锁位置的代码。在程序中使用自旋锁机制具有独立性,不用管它是在单处理器架构还是在多处理器架构上执行。

4.3.2.2 读写自旋转锁

读写自旋锁机制相比于基本自旋锁机制,允许在内核执行更大数量的并发操作。自旋锁允许多线程在读取数据时同时访问相同的数据结构,但是仅允许一个线程独占式更新同一个数据结构。每一个读写自旋锁由一个24位的读取计数器和一个释放锁标识组成,解释如下:

计数器

标识

解释

0

1

自旋锁已被释放,变量可用

0

0

自旋锁已被某线程获得,用于写

n(n>0)

0

自旋锁已被n个线程获得,用于读

n(n>0)

1

无效

对于基本自旋锁,有普通自旋锁,中断式自旋锁,中断保存式自旋锁这三种版本。

注意,读写自旋锁相对于写而言,更倾向于读(即,非常适用于并发读居多,并发写较少的这种场景)。假如读取线程获得了自旋锁,则假如只要有一个线程在读,则自旋锁就不会被写抢占。此外,即使写线程还在等待,也可以加入新的读线程使用自旋锁

4.3.3  信号量

在用户层面,Linux提供了一个对应于在UNIX SVR4中的信号量接口。在内核里,Linux提供了一个供其自己使用的信号量实现。即,属于内核部分的代码可以调用内核信号量。用户层的应用程序不能凭借系统调用直接访问内核信号量。它们是内核内部实现的功能,因此比用户可见信号量更加有效。

在内核层,Linux提供了三种信号量设施:二值信号量,记数信号量,和读写信号量。

4.3.3.1 二值信号量和计数信号量

二值信号量和计数信号量在Linux 2.6中开始定义。与前面讲到的信号量的功能相同。函数down和up分别用于等待和受信。

(1) 传统信号量

函数原型

说明

void sema_init(struct semaphore *sem, int count)

以指定的记数初始化动态创建的信号量

void init_MUTEX(struct

semaphore *sem)

以记数1初始化动态创建的信号量(初始化为未锁状态)

void init_MUTEX_LOCKED(struct

semaphore *sem)

以记数0初始化动态创建的信号量(初始化为锁定状态)

void down(struct semaphore *sem)

尝试获取信号量,如果信号量不可得,则进入不可中断的休眠状态

int down_interruptible(struct

semaphore *sem)

尝试获取信号量,如果信号量不可得,则进入可中断的休眠状态;如果收到一个信号而不是up操作,则返回一个EINTR值

int down_trylock(struct

semaphore *sem)

尝试获取信号量,如果信号量不可得,则返回一个非零值

void up(struct semaphore *sem)

释放指定的信号量

(2) 读写信号量

函数原型

说明

void init_rwsem(struct

rw_semaphore, *rwsem)

以记数1初始化动态创建的信号量

void down_read(struct

rw_semaphore, *rwsem)

读的down操作

void up_read(struct

rw_semaphore, *rwsem)

读的up操作

void down_write(struct

rw_semaphore, *rwsem)

写的down操作

void up_write(struct

rw_semaphore, *rwsem)

写的up操作

计数信号量使用sema_init函数初始化,此函数给信号量指定一个名字并指定一个初始值,在Linux中,二值信号量称为MUTEXes,使用init_MUTEX和init_MUTEX_LOCKED函数初始化,分别初始化信号量为1或0。Linux提供了三种版本的down操作。

(1) down函数对应传统的wait操作,即,线程检测信号量,如果信号量不可得,则阻塞线程。如果在信号量上发生了一个相应的up操作,则被塞的线程被将唤醒。注意,这个函数名用于计数信号量或二值信号量。

(2) 当线程被阻塞在down操作的时候,down_interruptible函数允许接收和响应内核信号。假如线程被信号唤醒,down_interruptible函数会叠加信号量的计数值,并返回一个Linux已知的错误码-EINTR。这提醒线程调用的信号量函数已中止。实际上,是线程被迫“放弃”信号量。这个特征对于设备驱动和其它方便重写信号量的服务非常有用。

(3) down_trylock函数使得可以在非阻塞的模式下请求信号量。如果信号量可得则正常返回。否则,返回一个非零值而无需阻塞线程。

4.3.3.2 读写信号量

读写信号量将用户分成读和写;允许多线程并发读(只有读)但仅允许单个线程独占写(无并发读)。实际上,对于读而言,读写信号量充当的是信号量,而对于写,读写信号量充当的是二值信号量(互斥)。读写信号量使用非中断休眠模式,因此,每个down操作仅有一种版本。

4.3.4  屏罩(Barriers)

在一些架构中,编译器和/或处理器硬件可以重新排列源代码中的内存访问次序,以达到优化性能的目的。做这些次序重排的目的是为了优化处理器中的指令流水的用法。次序重排算法包括检测以确保不违背数据依懒性。例如,下面的代码:

a=1;

b=1;

可以调整其顺序,使得变量b在变量a之前先更新。而对于以下的代码:

a=1;

b=a;

则不会被重排次序。即使是这样,有时,由于使用了另一个线程或硬件设备生成的信息,因此必须按照指定的顺序执行读取或写入操作

    为了强制指令按指定的次序执行,Linux提供了内存屏罩(barriers)设施。下面的表格列出了这个设施中最重要的函数定义。rmb()操作确保在代码中在整个通过rmb()的位置定义的屏罩上不会有读取发生。类似地,wmb()操作确保在代码中在整个通过wmb()的位置定义的屏罩上不会有写入发生。mb()操作提供了载入和存储屏罩。

Linux内存屏罩操作:

函数原型

说明

rmb()

阻止跨域屏罩载入重排序

wmb()

阻止跨域屏罩写入重排序

mb()

阻止编译器跨域屏罩载入或存储重排序

smp_rmb()

基于对称多处理,提供一个rmb(),以及基于单处理器,提供一个barrier()

smp_wmb()

基于对称多处理,提供一个wmb(),以及基于单处理器,提供一个barrier()

smp_mb()

基于对称多处理,提供一个mb(),以及基于单处理器,提供一个barrier()

关于屏罩操作,有两个要点:

(1) 屏罩与机器指令相关,即载入和存储。因此,更高级的语言指令a=b既包括从b的内存位置载入(读),又包括对内存位置b存储(写)。

(2) rmb()wmb()mb()操作规定了编译器和处理器的行为。对于编译器,屏罩操作要求编译器在编译过程中不能对指令重排序。对于处理器,屏障操作要求,在屏障之前管道中挂起的任何指令必须在屏障之后的遇到任何指令之前提交执行(即,屏罩之前的挂起的指令,在能在屏罩内执行,屏罩内的指令执行完成后,先执行屏罩前挂起的指令,再执行其它指令)

barrier()操作是一个更高权重版本的mb()操作,因为它仅控制着编译器的行为。假如它明确知道处理器不会执行非期望的重排序,这个操作就很有用。例如,Intel X86不会重排写的次序。

smp_rmb(),smp_wmb()和smp_mb()操作提供了一种代码优化策略,使代码既可以在单处理器上编译,也可以在对称多处理机上编译。对于对称多处理器,这些指令定义为普通的内存屏罩,但是对于单处理器,它们全部视为编译器屏罩smp_操作在相关数据依赖关系仅出现在对称多处理上下文中的情况下非常有用

4.3.5  读-复制-更新原语操作(Read-Copy-Update,简记为RCU)

RCU(Read-Copy-Update)原语机制是一种先进的轻量级同步机制,在2002年被集成到Linux内核中。RCU原语机制在Linux内核中广泛应用,例如,在网络子系统中、在内存子系统中、在虚拟文件系统中、等等。RCU原语机制也用于其它操作系统;DragonFly BSD操作系统使用了一个类似于Linux可休眠(Sleepable)RCU的机制(SRCU)。还有一个名为liburcu的用户空间RCU库。

与常规Linux同步机制相反的是,RCU读操作无锁。由RCU保护的共享资源必须通过指针访问(后面标红字体部分会说明理由)。RCU的内核API很小,仅由以下函数组成:

rcu_read_lock()

rcu_read_unlock()

call_rcu()

synchronize_rcu()

rcu_assign_pointer()

rcu_dereference()

除了以上函数,还有20个RCU应用程序编程接口次要方法。

    RCU机制提供了对共享资源的多读多写读写机制;当一个写操作希望更新共享资源时,它首先创建一个它的副本,然后更新副本,再将指向共享资源的指针改为指向副本(如此则避免了锁定共享资源再修改,而不影响其它线程对共享资源的读写,从而提高了并发的效率)。之后,当它不需要的时候,旧版本的共享资源就释放掉了。更新一个指针是一个原子操作,因此,在更新操作完成前后,读操作均可以访问共享资源,但是在更新操作本身期间,则不能读。就性能而言,RCU操作最适合频繁读而极少写的场景

访问共享资源的代码必须封装在函数rcu_read_lock() / rcu_read_unlock()的代码块之间;此外。在这个封装的代码块中访问共享资源的指针必须通过rcu_dereference(ptr)引用,不通过一个指针直接访问,并且,也不应该在这样的代码块之外调用rcu_dereference函数。

在一个写操作已经创建了一个副本并完成更新操作之后(即完成了更新操作),在确保所有读操作不再需要读旧版的共享资源之前,写操作不能释放旧版本的共享数据。可以通过调用函数synchronize_rcu()或者非阻塞函数call_rcu()来完成。call_rcu()是指向一个回调,当RCU确认可以释放旧版本的共享资源后,会触发这个回调。

参考资料:

1. <<Operating Systems: Internals and Design Principles >> 9th William Stallings

2. Intel和AMD官方文档

3. 微软官方开发文档

4. FreeBSD官方文档

5. 其它网络资源

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
需求文档:劳动保护用品后台管理系统 1. 简介: 劳动保护用品后台管理系统是一个用于管理和监控劳动保护用品的系统。它可以帮助企业管理劳动保护用品的库存、采购、分发和报废等流程,提高劳动保护用品的管理效率和准确性。 2. 功能需求: 2.1 用户管理: - 管理员账号注册和登录 - 用户权限管理,包括管理员、普通用户等 - 添加、编辑和删除用户信息 2.2 用品管理: - 劳动保护用品分类管理,包括添加、编辑和删除分类信息 - 劳动保护用品信息管理,包括添加、编辑和删除用品信息 - 用品库存管理,包括库存数量、库存预警等 - 用品采购管理,包括采购订单、供应商信息等 - 用品分发管理,包括分发记录、领用人信息等 - 用品报废管理,包括报废记录、报废原因等 2.3 统计分析: - 用品库存统计,包括库存总量、分类库存占比等 - 采购统计,包括采购金额、采购数量等 - 分发统计,包括分发数量、领用人统计等 - 报废统计,包括报废数量、报废原因统计等 2.4 系统设置: - 部门管理,包括添加、编辑和删除部门信息 - 系统参数设置,包括库存预警值、审批流程等 3. 非功能需求: 3.1 安全性: - 用户认证和授权机制,保证只有授权用户才能访问和操作系统 - 数据加密,保护敏感信息的安全性 - 系统日志记录,追踪用户操作,便于审计和追责 3.2 可靠性: - 数据备份和恢复机制,防止数据丢失 - 系统稳定性,保证系统正常运行,不出现崩溃和故障 3.3 性能: - 快速响应,保证用户操作的实时性和高效性 - 并发处理能力,支持多个用户同时操作系统 3.4 用户友好性: - 界面简洁清晰,操作简单直观 - 错误提示和帮助信息,提升用户体验 以上是劳动保护用品后台管理系统的基本功能和要求。根据具体的业务需求,可能还需要进一步的定制开发和功能拓展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值