目录
📚文件与外存空间
🐇文件的概念
🐇文件系统的概念
🐇文件的组织——路径与目录
🐇文件的存储⭐️
🥕连续分配法
🥕链接分配法
🥕扩展分配法
🥕链表备份法
🥕索引分配法
🐇空闲块的组织
🐇文件的查找:索引节点
🐇文件的引用
🐇文件权限管理——访问控制矩阵(ACM)
📚设备与交互
🐇输入输出设备
🐇设备的分类
🐇字符设备:实时时钟
🐇块设备:机械硬盘
🥕缓冲区 (VS 缓存)
🥕机械磁盘——排序策略⭐️
🐇驱动程序
🐇接口的分类
🐇设备的共享
📚关系与协调
🐇竞争关系:互斥
🥕并发执行条件——Bernstein条件
🥕竞合的实现——Peterson算法
🐇互斥锁:自旋锁⭐️
🥕关中断(或锁调度器)实现自旋锁:CLI/STL
🥕原子指令实现自旋锁(SWAP/XCHG)
🥕原子指令实现自旋锁(TAS/BTS)
🥕原子指令实现自旋锁:CMPXCHG/CAS
🐇阻塞锁
🐇管程(Monitor)
🐇条件变量
🥕惊群效应
🥕条件变量的语义
🐇信号量
🐇活锁与死锁⭐️⭐️
📚进程间通讯
🐇管道
🐇信号、信号量、消息队列
🐇共享内存
📚文件与外存空间
🐇文件的概念
- 文件(File):一个具备一些属性的数据记录。我们可以根据其属性对相对应的数据记录进行增删改查(合称CRUD)。它一般需要被永久保存,因而存储在非易失性外存上;可以看作是对外存的一种组织和抽象。
- 属性(Attributes):文件的属性一般至少包括文件名,有时还包括文件大小、创建日期、修改日期、文件权限、所有者等内容。除文件名外,它们一般都被储存在文件控制块(FCB)。中。
逻辑结构:程序员看到的逻辑上的文件组织形式。其中,字节流结构是最基本的结构,其余的结构都可以在这个基础上进行搭建。文件内部的结构是由使用这个文件的应用程序决定的,操作系统本身并不干涉。
- 字节流结构:提供一个连续的存储空间,可存放任意的数据。
- 记录结构:提供一系列固定大小的存储空间,可以存放多个大小固定的数据块。
- 索引结构:提供一系列的键值对,可以根据键来查询值。
物理结构:文件在外存上的物理保存形式,由多个中间映射层负责决定。
🐇文件系统的概念
- 文件与外存:多个文件需要存放在多个外存设备上。文件的存在是为了方便用户对数据进行增删改查,而外存则具有诸多特性和限制。
- 文件系统(Filesystem):将文件中的文件块按照一定的方法最终映射到物理设备上的物理块,并在物理存储器特性的限制下提供尽可能高的信息管理效率。提供这种功能的软件栈称为文件系统。一般地,它提供按名称和路径访问文件的功能。(核心:按名存取)广义的文件系统包括这个映射中的所有层次。狭义的文件系统仅仅指从文件映射到逻辑设备这一个层次。
文件系统与(关系型)数据库
- 文件系统更底层、更原始,提供的功能更少,更偏重于信息的存储而非查询,可以看做是最基本的键值存储(Key-Value Store)型非关系型数据库。
- 关系型数据库则在文件系统的基础上提供更多数据抽象、数据查询、数据去重、数据统计、并发控制、故障恢复、权限控制等功能,可以看做是文件系统的升级版。
🐇文件的组织——路径与目录
- 文件系统一般以树状的方式组织文件,因为这和日常生活中我们组织、保管和使用文档的习惯是相符的。
- 路径(Path):文件在文件系统中的位置用路径表示。路径字符串由一系列由分隔符隔开的子字符串组成,它唯一确定一个文件。路径又可以分为绝对路径和相对路径。分隔符一般是“/”或者“\”。
- 相对路径:从某个目录D起始,经由逐步查找能找到某文件F的路径,称为F相对于D的路径。
- 绝对路径:某文件F相对于系统根目录的路径。也即从系统根目录出发,一步步跟随查找,能找到该文件F。
- 目录(Directory/Folder ):文件路径中由分隔符隔开的子字符串,又称文件夹。每一个目录都对应于一个文件组织层次。目录可以互相嵌套,一个目录下的其它目录称为它的子目录。
- 工作目录:某应用程序进行文件路径解析的起始目录。该程序中的一切路径都看做是对该目录的相对路径。
🐇文件的存储⭐️
🥕连续分配法
- 概念:将文件一个接一个地放置在外存上,每个文件占据连续的物理介质块。FCB中只要记录文件的起始块号和长度,就能唯一确定一个文件。
- 优点:顺序和随机读写速度都快,不会出现内部碎片。管理数据结构短小精悍,存储介质利用率高。存储介质损坏只影响部分数据,数据存储可靠性好。
- 缺点:文件长度难以随意追加,有可能存在外部碎片。
- 应用场合:数据备份和归档等都倾向于单次写多次读。
🥕链接分配法
- 概念:按照某个固定的块大小(下图一个分配块对应两个设备块)分配文件,每个块内部放置一个指针,指向下一个块。如果没有后续块,使用空指针。FCB中只要记录文件的起始块号就能顺藤摸瓜找到整个文件;也可以说每个文件对应了一条块链。
- 优点:文件长度可以随意增减,不会产生外部碎片。
- 缺点:顺序读写慢和随机读写更慢,管理数据结构包括指针,指针本身也要消耗存储介质。存储介质损坏可能导致指针丢失,数据存储可靠性差。
🥕扩展分配法
- 概念:连续分配和链式分配的折中。使用链表来避免外部碎片,使用可变长度的块来避免内部碎片。这种可变长度的连续块称为扩展(Extent),其内部包含指向下一个扩展的指针,以及本扩展的长度。FCB的设计与链表分配法相同。
- 优点:文件长度可以随意增减,不会产生外部碎片;扩展大小灵活,也不会产生内部碎片。
- 缺点:顺序读写有一定的提升,随机读写还是慢。管理数据结构包含指针和扩展长度,在存储碎片化严重时进一步消耗存储介质。存储介质损坏可能会导致指针或扩展长度丢失,因此,数据存储可靠性仍然差。
🥕链表备份法
- 概念:不将指针、扩展长度等管理结构和文件数据存储在一起,而是给它们单独的存储空间。
- 这样,管理结构就和数据本身完成了解耦,我们只要单独备份管理结构就可以了。管理结构中含有所有文件的所有块的组织信息,而且本身短小精悍,可以在盘上保存多个备份。
- 只要还有一份管理信息在,整个文件系统就没有受结构性破坏,抗损性强。数据区受损只会引起数据损坏,不会引起文件系统崩溃。
- 随机访问提速:小巧的管理结构可以被单独加载进内存。这样,随机寻址虽然还是要做指针追逐,但是在内存中追逐指针速度要快得多。
🥕索引分配法
随机寻址仍然需要在内存中进行指针追逐,这还是比较慢。另外,管理结构集中保存仍然存在一损俱损的可能。几个备份同时损坏并不鲜见。
- 概念:给每个文件创建一个线性索引表,每个表项记载对应于该逻辑块的物理块。
- 优点:连续和随机访问都快,抗损性能也强。
- 缺点:索引预留多了,浪费;索引预留少了,文件大小受限。
改进:多级索引
- 像组织页表那样,将多个索引表以层次的形式组织起来,每个层次负责翻译逻辑块号的一部分,最终得到物理块号。不使用的索引块不创建、不填充就可以了。虽然随机访问仍然引起指针追逐,但是追逐的次数是有限的,也即索引的层数。
稀疏文件:有一些文件仅占用整个逻辑空间中的几个很分散的逻辑块,而其他块都是0。索引分配法可以轻松应对这些情况:不为那些全0的逻辑块填充索引,也不为他们分配空间即可。
问题:磁盘上80%-90%都是小文件,系统磁盘也是如此。在操作系统启动时,需要集中加载大量小文件,而每个小文件都采取多级索引的话会引起大量外存指针追逐。也就是说,小文件其实不需要那么多级别的索引。
改进:混合索引
- 文件的索引采取多级方法进行,但索引的级数随着文件块号的增加而增加。文件越靠前的部分,索引的级别越少。这样,小文件的存储效率就提高了。
- 一级索引多,文件上限小,但查存效率高;三级索引多,查存效率低,但文件上限大。
🐇空闲块的组织
空闲块组织和文件组织的区别:空闲块都一样。
- 文件系统中的暂未被占用的物理块称为空间块,可将空闲块做特殊处理。
- 连续存储:将所有的空闲块的地址和长度记录在表格里,好像连续分配法那样。
- 链表存储:将所有的空闲块放在一个链表里面,好像链接分配法那样。
- 索引存储:将所有的空闲块放在一个多级索引里面,好像索引分配法那样。
分组索引法
空闲块可以先按照预定的数量分组,每个索引表都包含指向下一个索引表的一个指针;除了这个指针之外,每组空闲块的地址直接登记在该索引表内。先索引,再链接。
- 单个添加:查找索引头指针指向的索引表。若它未满,将空闲块添加进列表内;若它已满,将索引头指向它,然后将它初始化成索引表,将它的后继指针指向索引头原本指向的索引表。
- 单个分配:查找空闲块头指针指向的索引表。若它未空,从它里面取出一个空闲块;若它已空,将它本身取出,然后将索引头指向它原本指向的索引表。
位图法
用一系列二进制位对应每个块。如果该块被分配出去,那么位图的对应位置就置1,否则置0。
🐇文件的查找:索引节点
- 将FCB拆成两部分,文件名直接储存在目录文件中,而其它属性放在索引节点中。
- 索引节点单独使用一个线性表存放,目录文件仅仅储存索引节点到索引节点编号的映射。这样,移动文件时完全不需要触碰索引节点,仅需要触碰目录文件中的一行。
- 索引节点可以保存几个备份,目录文件就不需要保存多个备份了。这样,一旦目录损坏,丢失的无非是文件名,文件的内容是不会丢失的,进行文件恢复只要扫描索引节点表。
🐇文件的引用
- 硬链接:
- 同一个文件在文件系统层面拥有两个文件名,它们都指向相同的索引节点,因而可以通过两个文件名访问同样的内容。
- 这种通过某个名称可以访问另一个名称所属内容的形式叫做“链接”,在文件系统数据结构层面实现的链接称为硬链接。
- 硬链接即为同一个文件的多个名字(本身与艺名)
- 软链接:
- 一个独立于其目标的的链接文件,该文件的内容是它指向的原文件的路径。
- 当应用程序操作软链接文件时,会通过某种方式将操作转接到它链接的原文件上。
- 软链接不是文件。
- 元文件系统:
- 文件系统的文件系统,又叫虚拟文件系统(Virtual File System,VFS)。
- 系统中所有的文件系统的根目录作为一个子目录出现在这个文件系统中,这样就可以用一种统一的路径描述来访问各个文件系统,减轻了应用程序的负担。
- VFS不是真正的文件系统,它是操作系统提供的对实际文件系统的统筹和抽象。
- 一切皆文件:硬件设备以及系统本身的运行状态等也可以使用文件抽象,使用和文件相同的权限管理模型和系统调用。这大大扩展了文件系统的内涵,让它变得无所不包。
🐇文件权限管理——访问控制矩阵(ACM)
文件系统上有很多文件,其中一些可能保存着敏感信息,如密码、隐私等。
访问控制矩阵:记录不同保护域(进程)对文件操作权限的矩阵。它一般情况下是一个稀疏矩阵。
📚设备与交互
🐇输入输出设备
- 外部设备:输入输出设备也叫外部设备,简称设备。可以实现人机交互和机间通讯等功能。如键盘、鼠标、显示器、网卡等。
- 外部接口:外部设备和计算机通信的接口,其实现包括三个寄存器:
- 数据寄存器:存放外设和主机间传送的数据
- 状态寄存器:保存外设或接口的状态信息
- 命令寄存器:保存CPU发给外设或接口的控制命令
- 信息交换:外设和主机交互包括四种主要方式:
- 直接交换:外设总是准备好接受或发送数据,主机直接读写
- 查询(轮询):在读写前查询外设是否准备好,主机再读写
- 中断 :外设通过中断主动通知主机准备好,主机再读写
- 成组传送:由DMA控制器读写设备,CPU不干涉,性能最高
- 四种方式使用都很普遍,其中中断相对而言是主流方式。
- 编址方式:
- 独立编址:x86有专用的I/O地址,它和存储器地址相互独立。
- 统一编址:ARM将设备映射到存储器地址当作存储器访问。
🐇设备的分类
设备按用途分类
设备按速度分类
设备按数据传输单位分类
块设备一般速度较高,字符设备速度一般较低但灵活性高。
设备按可共享性分类
设备按数据传输方式分类
设备按真实存在性分类
🐇字符设备:实时时钟
- 定时器(Timer):带可编程计数上限的计数器。一组D触发器负责计数,另一组触发器则储存一个上限值。一个比较器负责比较这两组值,一旦计数值达到上限值(溢出),就将计数值重置为0。
- 定时器中断:一旦定时器的输入时钟频率已知,那么其溢出的时间间隔(时间片)也就决定了。我们可以设置定时器在每次溢出时产生一个中断报告给操作系统,操作系统就能够强制定期启动调度器。
- 软定时器:用系统定时器作为时基来模拟一系列的软件定时器。当程序需要定时时间t时,操作系统内核将记录经过的系统定时器嘀嗒数,待经过t个嘀嗒时就完成软定时器的定时。
- 实时时钟(Real-Time Clock):
- 一个电子万年历,可以记录时间和日期,并具备闹钟功能。其往往还有一个单独的后备电池供电,用来在计算机主电源不通电时保持时间走时。
- 其内部是一系列互相串联的定时器。秒定时器的溢出连接到分定时器的计数时钟输入端,如此继续下去,直到年定时器为止。
🐇块设备:机械硬盘
- 读写单位:虽然磁盘的设备块是扇区,但操作系统并不会按照扇区来读写硬盘,因为这个单位太小了,效率不高。操作系统实际使用的读写单位是簇(Cluster),它是一系列连续扇区的集合。
磁盘是一个共享设备,这意味着很多进程都会同时提交大量的写盘请求。磁盘的工作效率显著地比CPU低,那么如何处理这些请求使I/O效率最高?
- 磁道与磁道之间动磁头;簇与簇之间以及簇内部不动磁头。
- 磁道:越靠近的磁道之间的访问延迟更低(磁头摆动幅度小;ms量 级),同时从内圈往外圈读访问延迟比从外圈往内圈读低(盘片斜进量,μs级)。
- 簇:同一个磁道,从前往后读快,从后往前读慢(扇区的排列顺序;μs级)。一个簇内部的写则最好一次写完。
- 顺着磁盘的旋转方向读。
- 在调度磁盘I/O时,主要考虑磁头在磁道上的摆动耗时,这个时间是以ms计算的,远远超过其它耗时。
🥕缓冲区 (VS 缓存)
缓冲区(Buffer )
- 缓冲区是一个临时的存储区,用以缓和通信双方I/O速度、数据传输单位和并发性上的差异,其基本特点是(可含有一定次序重排的)先进先出队列。
- 磁盘返回的读数据都被提交到读数据缓冲区,等待操作系统拿走这些数据;提交到磁盘的读写请求都要在操作缓冲区中进行排队,等到合适的时候再操作磁盘。
- 在操作缓冲区中,同一个簇的读写请求会被合并,同一磁道的不同簇之间的读写请求会被按照簇的次序来排序。不同磁道之间的请求则按照某种策略排序。
- 缓冲区的类型:
- 单缓冲:含有一个请求的缓冲区。
- 双缓冲:含有两个请求的缓冲区。并发能力更好。
- 环形多缓冲:队列中的多个请求组成一个环形,并有头尾两个指针。头指针负责写,尾指针负责读,如果头赶上尾则说明队列已满,如果尾赶上头则说明队列已空。
- 缓冲池:多个缓冲区的集合,由内核各模块共享。程序需要时申请,用完后释放回来,提供统一的接口并集中管理,提高内存利用率。不同的设备可能需要不同粒度和槽位的缓冲区,这些都可以从缓冲池中申请创建。
缓存(Cache)
- 缓冲区的改进,增加了数据存留的功能,不再具备先进先出的特点。
- 现代操作系统的磁盘缓冲实际上都是缓存,它们就像CPU的缓存那样工作,含有一系列簇的副本;如果操作请求命中它们,就可以免去真正的磁盘读写。
- 这种用来替代缓冲区的缓存习惯上叫做缓冲缓存(Buffer-Cache)。
🥕机械磁盘——排序策略⭐️
线性扫描:从磁盘头扫描到磁盘尾,在一次扫描中响应所有的请求。
先来先服务(FCFS):直接按请求的发起顺序来发起磁盘访问。
最短寻道时间优先(SSTF):在访问时先计算下次磁头移动的距离,选择最近的磁道进行访问。有可能造成饥饿现象(那些“偏远地区”总是访问不到)。
电梯扫描算法(C-SCAN/SCAN/C-LOOK/LOOK):以C-LOOK为例,将队列中的请求排序,从头扫描到尾,然后再从头开始。在扫描到最后一个请求之前,不做回溯,直到扫描完成后才一次性拉回来。如响应请求时在同一个地址又产生请求,等到下次扫描再响应它。
🐇驱动程序
- 概念:
- 直接控制设备的接口程序,简称驱动。
- 它通过直接读写控制器中的寄存器来操作设备;一般运行在内核态,是内核的一部分。
- 和应用程序不同,驱动程序是设备依赖的。这是由于它们直接控制设备,与设备特性紧密相关。一旦更换设备,就需要更换驱动程序。驱动程序一般由设备厂商提供,原则上与内核厂商无关。
- 驱动程序的目标是将设备的特性抽象掉,保留设备的共性,以保持内核的其他部分以及应用程序的设备独立的。
- 驱动程序的对应:
- 驱动程序根据设备的属性来查找设备。
- 一般而言这是指设备的设备号:主设备号(厂商号)用来标记厂商,次设备号(设备 号)用来标记具体产品。当然也可以有更复杂的编号方案。
- 驱动程序的功能:
- 查找设备:扫描并检测硬件改动,识别应当识别的设备。
- 初始化设备:初始化并对设备进行必要设置,注册中断ISR。
- 响应设备请求:响应设备的中断等,在设备运行中与设备交互。
- 响应软件请求:执行操作系统和应用程序下发的设备操作指示。
🐇接口的分类
接口按设备分类
接口按阻塞性分类
接口按同步性分类
🐇设备的共享
在多个程序争用同一不可共享的设备时,怎么处理?
设备同时联机操作(Spool)
- 如果该设备同时只能执行一个程序的操作,但程序不关心也不等待操作的执行完成才能继续进展,则各程序可以将请求提交到队列,设备则从队列中依次拿出请求执行。
- 相当于将异步I/O操作转化成了一个同步I/O操作。这种设备就是之前介绍的假脱机设备,这种操作也叫做假脱机操作。最常见的是打印机。
- 一般用于那些速度很慢的批处理性质的共享设备,而且程序往往不关心具体何时设备操作真正结束。
守护进程
- 在实际实现中,对于每个假脱机设备,我们都会启动专用进程用来管理队列和操作I/O,而其他进程则把这个进程当成虚拟设备并操作它。
- 这个专用进程随着设备的启动而启动,随着设备的关闭而关闭,因此叫做守护进程。
📚关系与协调
🐇竞争关系:互斥
- 临界资源:不能同时被两个指令流使用的资源。它只能分别在两个指令流中独占共享。常见的临界资源包括文件、设备等。
- 临界区:访问临界资源的程序段,临界区不能并发执行。又叫做关键区域。临界区越短越好,最好只包括访问临界资源的必要步骤。
- 互斥:临界资源不能并发使用的现象称为互斥。它在指令流的执行上表现为临界区在任何时候只能最多被一个指令流执行。
🥕并发执行条件——Bernstein条件
对任意指令流S,我们使用R(S)表示其进行读操作的资源的集合,W(S)表示其进行写操作的资源的集合。则假设有两个指令流S1、S2,若[R(S1)∩W(S2)] ∪ [R(S2)∩W(S1)] ∪ [W(S1)∩W(S2)] = Φ,则操作可以并发执行。
🥕竞合的实现——Peterson算法
- 上述实现解决了临界区的互斥问题:进入临界区时要么(1)对方want = 0,或者(2)当前 是对方主动让给自己的turn,对方肯定在等待。
- (1)不会出现死锁,因为turn保证了有一方的等待条件总是遭到破坏。
- (2)不会出现活锁,因为双方都在等待时均不放弃自己的进入意图。
- (3)不会出现饥饿,因为turn只代表发生竞争时的优先进入权。
🐇互斥锁:自旋锁⭐️
- 互斥锁:一种用以控制临界区访问的互斥访问原语,分为加锁和解锁两个原子操作。进入临界区先加锁,退出临界区后解锁。
- 加锁操作:检查条件并在满足时获取锁的拥有权。如果在这个过程中指令流做忙等待(Busy Wait),也即循环检测锁的条件,好像一个高速旋转的陀螺,此时称之为自旋锁(Spin Lock)。
- 解锁操作:释放锁的拥有权。在此之后,指令流不再拥有临界区的访问权限。
- 好自旋锁的三个标准:
🥕关中断(或锁调度器)实现自旋锁:CLI/STL
该算法要赋予所有指令流直接开关中断的权限(所以不安全)。一旦有恶意指令流,它关中断后不再开启,就可以得到100% CPU,可以轻易破坏线程间的时间隔离。出于信息安全原因,我们不能粗暴地这样做。当然,如果操作系统本身就不是信息安全的(也即不支持进程),那就没有这个顾虑了。
封禁调度器的一个最简单办法就是关中断。一旦关中断,时钟中断就无法发生(此时若时钟中断到来,需要等待开中断后才发生),自然无法进行指令流或线程切换。
🥕原子指令实现自旋锁(SWAP/XCHG)
🥕原子指令实现自旋锁(TAS/BTS)
🥕原子指令实现自旋锁:CMPXCHG/CAS
🐇阻塞锁
自旋的缺点:对于自旋锁,在不能获得锁时会频繁检查条件。
- 无谓地浪费了CPU
- 造成内存总线压力(尤其是多核系统)
- 在指令流对线程多对一模型中是无法使用的
- 由硬件原子指令实现的锁还无法确保公平性(原子指令自旋锁不能公平进入)
阻塞锁:为了克服自旋锁的弱点,我们需要引入阻塞锁。阻塞锁与自旋锁的区别只有一个,就是当指令流无法获得锁时就停止执行,并等待锁的释放。阻塞锁中还可以实现等待队列,指令流按照先来后到的顺序阻塞在等待队列中。一旦锁的占用者释放,我们就从队列的头部唤醒一个指令流,从而实现公平性。
阻塞锁的实现 :阻塞锁可以在线程级别(内核空间)实现,也可以在指令流级别(用户空间)实现。如果是前者,线程上的任何指令流阻塞线程都陷入内核并阻塞;如果是后者,则整个线程上的所有指令流都阻塞,线程才阻塞。
好阻塞锁的标准:在好自旋锁的标准上增加一条“让权等待”,也即无法获得锁时应立即释放处理机。
🐇管程(Monitor)
- 一种语言级或库级别的构造,该构造通过将互斥(或同步)机制与临界资源操作封装在一起并作为对资源的唯一接口导出,保证了任何程序都通过该接口访问临界资源,进而保证了资源的互斥。
- 管程看起来就像一个对象,该对象的各个成员函数均封装了加锁、访问资源、解锁三个过程。
- 管程的特点:
- 互斥性:管程内的临界区被互斥原语(如锁等)保护,只能由一个(或某个有限数量的)指令流进入并执行。
- 模块化:管程是可单独编译的程序的基本单元,一般写在一个单独的源文件内。
- 封装性:管程封装了对原始临界资源的一切操作,不向外界暴露任何接口。管程内的任何数据只能通过管程的接口访问,同时管程代码也只访问管程内部的数据。
- 抽象性:管程抽象掉了原始临界资源的细节属性,仅保留了其高层次的功能接口。
🐇条件变量
一种同步原语,可以使指令流阻塞并等待,直到某个条件发生。它总是与一个锁配合使用:
- 如果条件不满足,则指令流自动释放锁并加入等待队列;
- 当条件满足时,等待队列头部的指令流将被唤醒并自动恢复对锁的持有。
🥕惊群效应
- 当多个指令流(尤指并发情况下的线程)的阻塞被同时解除,导致其同时被唤醒、系统负载瞬时极大升高的情况,可能导致系统短暂失去响应。俗称炸窝。
- 这种效应在服务质量(Quality-of-Service)上一般表现为尾延迟升高,客户计算体验变差。
- 如果醒来的指令流竞争同一个资源,且仅有其中一个得到资源、其它指令流需重返阻塞状态,就可能导致反复惊群,进一步恶化系统性能。因此,在使用条件变量时应当慎用全唤醒操作。
🥕条件变量的语义
- Hoare语义:
- 条件变量的唤醒操作将保证被唤醒的线程立即得到调度并持有锁。它比较高效、比较简单,适用于应用程序能完全控制调度器的场合,比如用户模式的协程库。
- 在这个语义下,一旦某个指令流被唤醒了,那就说明条件百分之百成立、指令流无需重复检查条件是否仍成立。
- Mesa语义(常用):
- 条件变量的唤醒操作仅保证被唤醒的线程进入就绪状态,且当它们被调度时有机会参与锁的竞争,并不保证该线程立即得到调度。
- 它比较低效、复杂,但适应于应用程序不能控制调度器的场合,如操作系统和中间件等提供的条件变量接口。
- 在这个语义下,被唤醒的线程仅仅是得到了一个提醒:你的条件曾经可能满足过。具体满足不满足,要在醒来后再次检查。
🐇信号量
- 概念:将一个条件变量与一个计数器封装起来,就可以得到(计数)信号量。它是一种比锁和条件变量都更强大的互斥/同步通用工具,同时具备资源计数和等待两种功能,非常适用于系统级编程中常见的生产者-消费者关系,或者复数资源的计数。
- 初始化操作:设置信号量的初始值,代表系统中有此数量的初始资源。
- 获取操作(“P”操作):减少计数器的计数,代表从系统中拿走资源。如果系统中当前没有资源,则可以阻塞或自旋并等待。
- 释放操作(“V”操作):增加计数器的计数,代表向系统中投入资源。
🐇活锁与死锁⭐️⭐️
死锁:系统中有指令流发生无法自行解除的循环等待。只要这种循环等待发生,则系统发生死锁。具体地,有如下三个条件:
- 互斥条件:系统中存在有一定互斥性的资源。
- 持有条件:指令流已获得的资源无法释放。持有条件又可以细分为如下两条:
- 保持请求:指令流不主动放弃已经持有的资源。
- 无法剥夺:不允许指令流互相剥夺资源;非抢占。
- 循环等待:指令流互相循环等待资源释放,不能进展。
活锁:指令流并未死锁,但其在多次反复尝试获取资源均失败,无法进展或进展缓慢。
资源分配图
分析方法:
- 若某指令流的所有请求都可以得到满足,则删除其所有请求边和分配边,并将分配边上的资源归还给各个资源池。
- 重复上述步骤,直到无可执行。此时若所有指令流都执行结束则无死锁。若还有指令流存在(无法被化简掉),则它们一定在循环等待,此时发生死锁。
- 只有最后剩下的指令流是死锁的
- 上图图一AD满足后,释放资源,所以BC也满足,不构成死锁。
- 图二构成死锁,BC都不满足。
- 图三R1有剩余资源能用,所以也不构成死锁。
预防与避免
不安全=死锁?
假设所有指令流都是自私的,在得到自己可能请求的全部资源之前绝不会释放已经占有的资源(只进不出),而是要等到结束后才一并释放,那么一旦系统执行进入不安全区域,就等于死锁。如果指令流在执行中可能释放已占有的资源,那么即便进入不安全区域也不一定死锁。
(1)
银行家算法
- 在已知每个指令流对资源的最大消耗时,它可以通过检查分配请求并仅批准那些不会让系统进入不安全状态的请求,完全避免死锁风险。
- 在现实生活中,银行业的核心竞争力就是风险控制。这里我们把资源总量看作银行的资金池,将指令流看作是诚实的贷款客户(贷后必还)。
- 假设系统中有S个指令流和R种资源,则可用如下矩阵表示资源等待图:
死锁的对策
鸵鸟政策:操作系统仅提供同步工具,不负责探测和解决死锁,而是由人或其它系统服务在死锁时介入。这么做是有道理的:会死锁的程序有正确性问题,而保证正确性归根结底是程序员的责任。
- 破坏互斥条件:只要想办法让资源本身不互斥、不竞争,就自然不存在等待问题,就不可能死锁。
- 破坏保持请求:要求指令流在无法进展时主动放弃之前获得的资源。
- 破坏无法剥夺:一旦探测到有可能死锁,就强制剥夺死锁参与者的全部资源。
- 破坏循环等待:阻止系统中生成可能的等待环路。
📚进程间通讯
约定俗成的叫法,只要是通信都可以简写为IPC,哪怕它不是进程间
指令流间和线程间通讯可以在用户空间实现,进程间通讯和应用程序间通讯只能在内核空间实现?
进程和应用程序间通讯必然要跨越空间边界,而跨越空间边界只有操作系统才做得到。跨越时间边界和角色边界则可以在用户模式完成, 因为空间是共享的。
在实践中,为什么线程间通讯总是在内核空间实现?
线程是内核管理的,其阻塞式通信就必须在内核空间实现。指令流则是用户空间管理的,其阻塞式通信可以在用户空间实现。
🐇管道
管道是一种简单的点对点通信工具,只能在两个进程之间进行先进先出的信息传递。在Linux中,管道分为无名和有名管道。
- 无名管道(Pipe):用于父子进程之间点对点通讯的工具。父进程创建无名管道,然后调用fork(),父进程端使用一个端口,子进程端使用一个端口,且只能父发子收或父收子发。如果需要全双工,则只能创建两条无名管道。
- 有名管道:用于任意进程之间通讯的工具。任意进程均可创建有名管道,该管道将作为一个特殊文件存在。此后,参与通信的进程可以通过打开并读写该管道文件进行通讯。虽然有名管道允许多个读者和写者,但多个写者之间存在写交叉的风险。
🐇信号、信号量、消息队列
- 信号:Linux系统中的一种回调函数机制,可以用来在不同进程之间或同一进程的不同线程之间发送轻量级通知。当通知被送达时,线程暂停执行原程序,转去执行信号处理例程(Signal Handler),然后再返回原程序继续执行。
- 信号量:Linux系统提供的标准信号量接口,同时具备资源计数和等待两种功能,非常适用于系统级编程中常见的生产者-消费者关系,或者复数资源的计数。
- 消息队列:Linux提供的标准消息队列机制,可以在不同进程之间传送信息。和管道不同,消息队列允许多个发送者和多个接收者,且收发均以消息为单位,无需考虑数据交叠的问题。为了跨越地址空间,Linux会先将消息从发送者拷贝到内核,然后再从内核拷贝到接收者。
🐇共享内存
Linux提供的地址空间共享机制,可以使两个地址空间之间共享一段物理内存(虚拟地址未必要一致)。此种方法等于是将裸内存直接暴露给了通信的参与者,需要参与者自行在共享内存中组织合适的数据结构进行通信,内核不再介入数据传递。