推论:
- 参与死锁的所有进程都在等待资源
- 参与死锁的进程是当前系统中所有进程的子集
原因:资源数量有限、锁和信号量错误使用,导致了死锁的出现。
资源的使用方式: “申请—分配—使用—释放”模式。
资源的分类:
可重用资源:可被多个进程多次使用
可消耗资源:只可使用一次、可创建和销毁的资源
活锁
先加锁
再轮询
既无进展也没有阻塞
饥饿
互斥使用(资源独占)
占有且等待(请求和保持,部分分配)
不可抢占(不可剥夺)
循环等待
为了解决死锁的问题,用有向图描述系统资源和进程的状态。
二元组G=(V,E)
V:结点的集合,分为P(进程),R(资源)两部分
E:有向边的集合,其元素为有序二元组(Pi, Rj) 或 (Rj, Pi)
系统由若干类资源构成,一类资源称为一个资源类; 每个资源类中包含若干个同种资源,称为资源实例。
分配边:资源实例——>进程
申请边:进程 ——>资源类
例子:
如果资源分配图中没有环路,则系统中没有死锁, 如果图中存在环路则系统中可能存在死锁
如果每个资源类中只包含一个资源实例,则环路是死锁存在的充分必要条件
化简步骤:
找一个非孤立、且只有分配边的进程结点 去掉分配边,将其变为孤立结点
再把相应的资源分配给一个等待该资源的进程 即将该进程的申请边变为分配边
这张图最后能够完全被化简掉,所以没有死锁。
四种方法来解决死锁问题:
不考虑此问题(鸵鸟算法)
不让死锁发生
死锁预防
死锁避免
让死锁发生
破坏产生死锁的四 个必要条件之一
资源转换技术:把独占资源变为共享资源
SPOOLing技术的引入
实现方案1:要求每个进程在运行前必须一次性申请它所要求的所有资源,且仅当该进程所要资源均可满足时才给予一次性分配
实现方案2:在允许进程动态申请资源前提下规定,一个进程在申请新的资源不能立即得到满足 而变为等待状态之前,必须释放已占有的全部资源,若需要再重新申请
实现方案:
局限性:
实施方案:资源有序分配法
实现时要考虑什么问题呢?
在系统运行过程中,对进程发出的每一个系统 能够满足的资源申请进行动态检查,并根据检 查结果决定是否分配资源,若分配后系统发生 死锁或可能发生死锁,则不予分配,否则予以分配。
安全状态:如果系统中存在一个由所有进程构成的安全序列P1 ,…, Pn,则称系统处于安全状态。
一个进程序列{P1,…,Pn}是安全的,如果对于每一个进程$Pi(1≤i≤n):$ 它以后还需要的资源量不超过系统当前剩余资 源量与所有进程Pj (j < i )当前占有资源量之和,则称系统处于安全状态。
安全状态一定没有死锁发生.
不安全状态:系统中不存在一个安全序列
不安全状态一定导致死锁
应用条件:
在固定数量的进程中共享数量固定的资源
每个进程预先指定完成工作所需的最大资源数量
进程不能申请比系统中可用资源总数还多的资源
进程等待资源的时间是有限的
如果系统满足了进程对资源的最大需求,那么, 进程应该在有限的时间内使用资源,然后归还给系统
n:系统中进程数量
m:资源类数量
Available: ARRAY[1..m] of integer;//可以分配的资源
Max: ARRAY[1..n,1..m] of integer; //最大需求
Allocation: ARRAY[1..n,1..m] of integer; //当前拥有的资源
Need: ARRAY[1..n,1..m] of integer; //还需要的资源
Request: ARRAY[1..n,1..m] of integer;//本次对资源的申请
当进程Pi提出资源申请时,系统执行下列步骤:
(1)若Request[i] ≤ Need[i],转(2);否则,报错返回;
(2)若Request[i] ≤ Available,转(3); 否则,进程等待;
(3)假设系统分配了资源,则有
Available = Available - Request[i];
Allocation[i] = Allocation[i] + Request[i];
Need[i] = Need[i] - Request[i];
若系统新状态是安全的,则分配完成
若系统新状态是不安全的,则恢复原来状态,进程等待
如何判断是否安全:
为进行安全性检查,定义数据结构:
Work: ARRAY[1..m] of integer;
Finish: ARRAY[1..n] of Boolean;
安全性检查的步骤:
(1) Work = Available; Finish = false;
(2) 寻找满足条件的i:
a. Finish[i]==false;
b. Need[i]≤Work; 如果不存在,则转(4)
(3) Work = Work + Allocation[i]; Finish[i] = true;转(2)
(4) 若对所有i,Finish[i]==true, 则系统处于安全状态,否则系统处于不安全状态
死锁检测:
允许死锁发生,但是操作系统会不断监视系统进 展情况,判断死锁是否真的发生
一旦死锁发生则采取专门的措施,解除死锁并以最小的代价恢复操作系统运行
检测时机:
当进程由于资源请求不满足而等待时检测死锁
定时检测
每个进程、每个资源指定唯一编号
设置一张资源分配表记录各进程与其占用资源之间的关系
重要的是以最小的代价恢复系统的运行。
方法如下
问题模型:应用程序中并发线程 执行时,协调处理共享资源。
把筷子当做信号量来进行处理
1 | semaphore fork [5] = {1}; int i; |
增加了room变量
1 | semaphore fork[5] = {1}; semaphore room = {4}; int i; |
使用管程解决哲学家就餐问题
1 | void philosopher[k=0 to 4] |
1 | monitor dining_controller; |
为了避免死锁,把哲学家分为三种状态,思考,饥饿,进食,并且一次拿到两只筷 子,否则不拿。
1 |
|
块设备
字符设备
另一个角度:
- 存储设备(磁盘、磁带)
传输设备(网卡、Modem)
人机交互设备(显示器、 键盘、鼠标)
共享设备
虚设备
在一类设备上模拟另一类设备,常用共享设备模拟独占设备,用高速设备模拟低速设备,被模拟的设备称为虚设备
目的:将慢速的独占设备改造成多个用户可共享的设备,提高设备的利用率
实例:SPOOLing技术,利用虚设备技术——用硬盘模拟输入输出设备
按照用户的请求,控制设备的各种操作,完成I/O设 备与内存之间的数据交换,最终完成用户的I/O请求
建立方便、统一的独立于设备的接口
方便性:向用户提供使用外部设备的方便接口,使用户编程时不考虑设备的复杂物理特性。
统一性:对不同的设备采取统一的操作方式,即在用户程序中使用的是逻辑设备
逻辑设备与物理设备
屏蔽硬件细节(设备的物理特性、错误处理、不同I/O过程的差异性)
充分利用各种技术(通道,中断,缓冲,异步I/O等)提高CPU设备、设备与设备之间的并行工作能力,充分利用资源,提高资源利用率
性能 CPU与I/O的速度差别大
→减少由于速度差异造成 的整体性能开销
→尽量使两者交叠运
保护
I/O设备一般由机械和电子两部分组成
机械部分是设备本身(物理装置)
电子部分又称设备控制器(或适配器)
(端口)地址译码
按照主机与设备之间约定的格式和过程接受计算 机发来的数据和控制信号或向主机发送数据和状态信号
将计算机的数字信号转换成机械部分能识别的模拟信号,或反之
实现设备内部硬件缓冲、数据加工等提高性能或增强功能
操作系统将命令写入控制器的接口寄存器(或接口缓冲区)中,以实现输入/输出,并从接口寄存器读取状态 信息或结果信息
当控制器接受一条命令后,可独立于CPU完成指定操作,CPU可以另外执行其他计算;命令完成时,控制器产生一个中断,CPU响应中断,控制转给操作系统;通过读控制器寄存器中的信息,获得操作结果和设备状态
I/O端口地址:接口电路中每个寄存器具有的、唯一的地址,是个整数
所有I/O端口地址形成I/O端口空间(受到保护)
I/O指令形式与I/O地址是相互关联的, 主要有两种形式:
分配给系统中所有端口的地址空间 完全独立,与内存地址空间无关
使用专门的I/O指令对端口进行操作
优点
缺点:I/O端口操作的指令类型少,操作不灵活
例子:8086/8088,分配给I/O端口 的地址空间64K,0000H~0FFFFH, 只能用in和out指令进行读写操作
操作系统必须要做的事情:避免把包含控制寄存器的那部分地址空间放入任何用户的虚拟地址空间之中
例如,如果指令TEST可以测试一个内存字是否为0,那么它 也可以用来测试一个控制寄存器是否为0
中断驱动I/O
DMA
1.CPU直接控制外围设备
2.增加了控制器或I/O部件,CPU使用非中断的可编程I/O
CPU开始从外部设备接口的具体细节中分离出来
3.与2相同,但采用了中断方式
CPU无需花费等待执行一次I/O操作所需的时间,效率提高
4.I/O部件通过DMA直接控制存储器
可以在没有CPU参与的情况下,从内存中移出或者往内存中 移入一块数据,仅仅在传送开始和结束时需要CPU干预
5.I/O部件增强为一个单独的处理器,有专门为I/O设计的指令 集;CPU指导I/O处理器执行内存中的一个I/O程序。I/O处理器在 没有CPU干涉的情况下取指令并执行这些指令
6.I/O部件有自己的局部存储器(其本身就是一台计算机)
使用这种体系结构可以控制许多I/O设备,并且使需要CPU参 与程度降到最小(通常用于控制与交互终端的通信,I/O处理器 负责大多数控制终端的任务)
分层的设计思想:
(1)用户进程层执行输入输出系统调用,对I/O数据进行格 式化,为假脱机输入/输出作准备
(2)独立于设备的软件实现设备的命名、设备的保护、成块处理、缓冲技术和设备分配
(3)设备驱动程序设置设备寄存器、检查设备的执行状态
(4)中断处理程序负责I/O完成时,唤醒设备驱动程序进程,进行中断处理
(5)硬件层实现物理I/O的操作
好处:设备分配时的灵活性,易于实现I/O重定向
用户编写的程序可以访问任意I/O设备,无需事先指定设备
从用户角度:用户在编制程序时,使用逻辑设备名, 由系统实现从逻辑设备到物理设备(实际设备)的 转换,并实施I/O操作。
从系统角度:设计并实现I/O软件时,除了直接与设备打交道的低层软件之外,其他部分的软件不依赖于硬件。
操作系统中最早引入的技术
解决CPU与I/O设备之间速度的不匹配问题凡是数据到达和离去速度不匹配的地方均可采用缓冲技术
提高CPU与I/O设备之间的并行性
减少了I/O设备对CPU的中断请求次数,放宽 CPU对中断响应时间的要求
缓冲区分类
缓冲区管理
例子
终端输入软件中的键盘驱动程序 其任务之一:收集字符
两种常见的字符缓冲方法:
公共缓冲池(驱动程序中)
终端数据结构缓冲
描述设备、控制器等部件的表格:系统中常常为每一个部件、每一台设备分别设置一张表格,常称为设备表或部件控制块。这类表格具体描述设备的类型、标识符、状态,以及当前使用者的进程标识符等
建立同类资源的队列:系统为了方便对I/O设备的分配管理,通常在设备表的基础上通过指针将相同物理属性的设备连成队列(称设备队列)
面向进程I/O请求的动态数据结构:每当进程发出I/O请求时,系统建立一张表格(称I/O请求包),将此次 I/O请求的参数填入表中,同时也将该I/O有关的系统缓冲区地址等信息填入表中。I/O请求包随着I/O的完成而被删除
建立I/O队列:如请求包队列
在申请设备时,如果设备空闲,就将其独占,不再允许其他进程申请使用,一直等到该设备被释放,才允许被其他进程申请使用考虑效率问题,并避免由于不合理的分配策略造成死锁。
与设备密切相关的代码放在设备驱动程序中,每个设备驱动程序处理一种设备类型
一般,设备驱动程序的任务是接收来自与设备无关的上层软件的抽象请求,并执行这个请求
与操作系统的接口
与系统引导的接口(初始化,包括分配数据结构, 建立设备的请求队列)
与设备的接口
使CPU利用率尽可能不被I/O降低 使CPU尽可能摆脱I/O
减少或缓解速度差距 → 缓冲技术
Windows提供两种模式的I/O操作: 异步和同步
异步模式:用于优化应用程序的性能
同步I/O:应用程序被阻塞直到I/O操作完成
系统实现
用户实现
文件是对磁盘的抽象。 所谓文件是指一组带标识(标识即为文件名) 的、在逻辑上有完整意义的信息项的序列。
信息项:构成文件内容的基本单位(单个字节, 或多个字节),各信息项之间具有顺序关系
文件内容的意义:由文件建立者和使用者解释
要去读文件去写文件,需要定位读写指针,因此对于这样一个信息项的序列会有一个读写指针指到某一个具体的信息项。
怎样组织、管理文件?
文件系统如何呈现在用户面前:
可以实施的操作?
等等
操作系统中统一管理信息资源的一种软件,管理 文件的存储、检索、更新,提供安全可靠的共享 和保护手段,并且方便用户使用。
按文件性质和用途分类(UNIX):普通文件;目录文件;特殊文件(设备文件);管道文件;套接字
普通文件(regular)
包含了用户的信息,一般为ASCII或二进制文件
目录文件(directory)
管理文件系统的系统文件
特殊文件(special file)
- 字符设备文件:和输入输出有关,用于模仿串行I/O设备,例如终端,打印机,网卡等
- 块设备文件:磁盘
从用户角度看文件,由用户的访问方式确定
a以一个字节为单位,把文件看成是一个字节的序列。这是非常典型一个流式结构文件
b以记录为单位,一个记录由若干个字节组成,因此文件信息项的单位就是记录
还可以组织成堆、顺序、索引、索引顺序、散列等结构
文件是有逻辑意义、无结构的一串字符的集合
每条记录有其内部结构
典型的存储介质:磁盘(包括固态盘SSD)、磁带、光盘、U盘、……
文件的信息保存在这些存储介质上,通常是以一个独立单位来进行信息的存储传输和分配。 而这个独立单位就是物理块。
物理块(块block、簇cluster)
一块盘若干个盘片组成,每个盘片上有两个盘面
每一个盘面的信息读取都需要一个读写磁头,若干个读写磁头固定在了一个磁臂上。由磁臂带动的这些磁头沿着半径的方向进行移动。
在盘面上有若干个同心圆,称之为磁道。 在磁道上存放信息。
把磁道划分成很多段,每一段称之为扇区。 信息实际上是存放在每一个扇区里
任何时刻只有一个磁头处于活动状态:输入输出数据流以位串形式出现
物理地址形式: 磁头号(盘面号)、磁道号(柱面号)、扇区号
扇区:标题(10字节)、数据(512字节)、ECC纠错信息(12-16字节)
通常所说的一个扇区是 512 字节,其实是说存放的信息或者存放的数据是 512 字节,其实还有其他信息
一次访盘请求所需要给出的参数:
完成一次访盘请求的过程由三个动作组成:
旋转延迟(时间):等待指定扇区从磁头下旋转经过
数据传输(时间):数据在磁盘与内存之间的实 际传输
用一串二进制位反映磁盘空间中分配使用情况,每个物理块对应一位,分配物理块为0,否则为1
申请物理块时,可以在位示图中查找为1的位,返 回对应物理块号
位图计算公式:
已知字号i、位号j :$块 号 = i × 字 长 + j$
已知块号:$字 号 = [ 块 号 / 字 长 ] 位 号 = 块 号\quad mod\quad 字 长$
在专用块里有若干个字段,第一个字段存放了第一组的空闲块的块数,所以空闲块块数现在是二十。接着的各个字段就把二十个空闲块的块号记录在了每一个字段里头。一个文件想获取一个新的空闲块就从专用块来挑选空闲块,挑选时是从下往上挑选。 如果先分配的话,一定是801这一块,然后接着分配802这一块,最后才分配820这一块。
查L单元(空闲块数):
当空闲块数>1 ,i=L+空闲块数;
当空闲块数=1 取出L+1单元内容(一组的第一块块号或0);
把专用块内容读到内存L开始的区域
归还一块:
查L单元的空闲块数;
当空闲块数<100 空闲块数加1;
当空闲块数=100,则把内存中登记的信息写入归还块中;
操作系统为了管理文件会把文件的各种属性都记录下来。因此这些属性是操作系统管理文件所需要的信息,按照之前对进程的设计, 把文件属性所存放的这些信息称为文件控制块。
为管理文件而设置的数据结构,保存管理文件所 需的所有有关信息。(有时称为文件属性或元数据)
文件名,文件号,文件大小,文件地址,创建时 间,最后修改时间,最后访问时间,保护,口令,创建者,当前拥有者,文件类型,共享计数,各种标志(只读、隐藏、系统、归档、ASCII/二进制、 顺序/随机访问、临时文件、锁)
文件目录是操作系统中统一管理每个文件的元数据,以支持文件名到文件物理地址的转换。
将所有文件的管理信息组织在一起,即构成文件目录
构成文件目录的基本单元
目录项可以是FCB,目录是文件控制块的有序集合
路径名(文件名)
当前目录/工作目录
目录操作
这是一个根目录文件,目录文件是由若干目录项组成的,可以看到这里有ABC 三个目录项,通过这个目录项,找到目录项所对应的文件。
是指文件在存储介质上的存放方式。
主要解决两个问题:
假设一个文件被划分成N块,这N块在磁盘上是怎么存放的?
存放之后,其地址(块号或簇号)在FCB中是怎样记录的?
是指文件的信息存放在若干连续的物理块中。
在FCB中如何记录文件地址?
给出这两个信息就能知道文件中的任意一块的地址。
优点
缺点
一个文件的信息存放在若干不连续的物理块中, 各块之间通过指针连接,前一个物理块指向下一个物理块。
在FCB中如何记录文件地址?
优点
缺点
链接结构的一个变形: 文件分配表 FAT
把所有指针存放在一个文件中。
表项的值有三种:0,下一块块号,-1
某文件的起始块号从何处得到?记录在FCB中。
在FCB中如何记录文件地址?
索引表存放在何处?
索引表会放在某一个物理块中,这个物理块就是索引块。索引块中就存放着所分配给文件的物理块块号。
优点
保持了链接结构的优点,又解决了其缺点
缺点
问题:索引表很大,需要多个物理块存放时怎么 办?
解决方式:
链接方式
多级索引方式
综合模式
UNIX文件系统采用的是多级索引结构(综合模式)
每个文件的索引表有15个索引项,每项2个字节
前12项直接存放文件的物理块号(直接寻址)
如果文件大于12块,则利用第13项指向一个物理块, 在该块中存放文件物理块的块号(一级索引表)
对于更大的文件还可利用第14和第15项作为二级和三级索引表
试问:采用这种结构,一个文件最大可达到 ?个物理块
实现文件系统需要考虑磁盘上与内存中的内容布局。
磁盘分区(partition):把一个物理磁盘的存储空间划分为几个相互独立的部分,称为分区
文件卷(volume):磁盘上的逻辑分区,由一个或多个物 理块(簇)组成
格式化(format):在一个文件卷上建立文件系统,即建立并初始化用于文件分配和磁盘空闲空间管理的管理数据——元数据
存放的内容:
引导区
卷(分区)信息
目录文件(根目录文件及其他目录文件)
用户文件
访问一个文件时,用户通常给出一个文件名,通过使用文件名把文件的内容读入内存通常具有两个步骤:
目录检索
文件寻址
例子:
假设 一个FCB 占 48 个字节,物理块大小 512 字节
符号目录项:占 8 字节(文件名6字节,文件号2字节)
基本目录项:占48–6=42字节
一个目录文件有128个目录项
分解前:占13块
分解后:符号文件占 2 块,基本文件占11块
查找一个文件的平均访盘次数:
分解前:7次
分解后:2.5次
目录文件改进后减少了访盘次数,提高了文件检索速度
FCB划分成两部分:目录项+i节点
目录项:文件名+i节点号
目录文件由目录项构成
i节点:描述文件的相关信息
UNIX每个文件由一个目录项、一个i节点和若干磁盘块构成。
FAT表项:2字节
目录项:32字节
主引导记录通常放在0号扇区。
可以把文件分配表看成是一个整数数组,每个整数代表磁盘分区的一个簇号
状态
FAT32——长文件名的目录项格式
Windows为其建立了五个目录项、四个保存长文件名、一个保存压缩文件名THEQUI~1。
建立系统与文件的联系,实质是建立文件的FCB。
在目录中为新文件建立一个目录项,根据提供的参数及需要填写相关内容
分配必要的存储空间
根据文件名在文件目录中检索,并将该文件的目录项读入内存,建立相应的数据结构,为后续的文件操作做好准备。
文件描述符/文件句柄
create(文件名,访问权限)。
检查参数的合法性
例如:文件名是否符合命名规则;有无重名文件;
合法→2,否则→报错、返回
申请空闲目录项,并填写相关内容;
打开文件是为文件读写做准备。
给出文件路径名,获得文件句柄(file handle)或文件描述 符(file descriptor),需将该文件的目录项读到内存
fd=open(文件路径名,打开方式)
根据文件路径名查目录,找到目录项 (或I节点号) ;
根据文件号查系统打开文件表,看文件是否已被打开;
是 → 共享计数加1
否则 → 将目录项 (或I节点)等信息填入系统打开文件 表空表项,共享计数置为1;
根据打开方式、共享说明和用户身份检查访问合法性;
在用户打开文件表中获取一空表项,填写打开方式等,并指向系统打开文件表对应表项
返回信息:fd:文件描述符,是一个非负整数,用于以 后读写文件
seek(fd, 新指针的位置)
系统为每个进程打开的每个文件维护一个读写指针,即相对于文件开头的偏移地址(读写指针指向每次文件读写的开始位置,在每次读写完成后, 读写指针按照读写的数据量自动后移相应数值)
由fd查用户打开文件表,找到对应的表项;
将用户打开文件表中文件读写指针位置设为新指针的位置,供后继读写命令存取该指针处文件内容。
read(文件描述符,读指针,要读的长度,内存目的地址)
根据打开文件时得到的文件描述符,找到相应的文件控制块(目录项)
确定读操作的合法性
读操作合法→2,
否则→出错处理
问题:文件尚未打开?
将文件的逻辑块号转换为物理块号
申请缓冲区
可靠性: 抵御和预防各种物理性破坏和人为性破坏的能力
全量转储:
增量转储:
物理转储:
设计一个实用程序,当系统再次启动时,运行该 程序,检查磁盘块和目录系统。
UNIX一致性检查工作过程:
两张表,每块对应一个表中的计数器,初值为0
表一:记录了每块在文件中出现的次数
表二:记录了每块在空闲块表中出现的次数
通写(write-through)
内存中的修改立即写到磁盘
缺点:速度性能差
例: FAT文件系统
延迟写(lazy-write)
可恢复写(transaction log)
实现:
每个用户一个
记录文件名及访问权限
采用文件的二级存取控制审查用户的身份、审查操作的合法性。
第一级:对访问者的识别,对用户分类:
第二级:对操作权限的识别,对操作分类:
磁盘服务是速度成为系统性能的主要瓶颈之一,设计文件系统应尽可能减少磁盘访问次数。
提高文件系统性能的方法有:目录项(FCB)分解、当前目录、磁盘碎片整理;块高速缓存、磁盘调度、提前读取、合理分配磁 盘空间、信息的优化分布、RAID技术… …
块高速缓存又称为文件缓存、磁盘高速缓存、缓冲区高速缓存。
基本思想是:在内存中为磁盘块设置的一个缓冲区,保存了磁盘中某些块的副本。
利用方式:
引入原因:由于访问的局部性原理,当一数据块被读入块高速缓存以满足一个I/O请求时,很可能将来还会再次访问到这一数据块
不使用文件缓存
预读取。
异步模式
用户对磁盘的访问通过访问文件缓存来实现。
由Windows的CacheManager实现对缓存的控制
读取数据的时候预取
在Cache满时,根据LRU原则清除缓存的内容
Write-back机制
例子
分配磁盘块时,把有可能顺序存取的块放在一起。尽量分配在同一柱面上,从而减少磁盘臂的移动次数和距离。
当有多个访盘请求等待时,采用一定的策略,对这 些请求的服务顺序调整安排,从而降低平均磁盘服务时间,达到公平、高效。
- 公平:一个I/O请求在有限时间内满足
- 高效:减少设备机械运动带来的时间开销
一次访盘时间 = 寻道时间+旋转延迟时间+传输时间
• 减少寻道时间
• 减少延迟时间
例子:
例子:假设磁盘访问序列: 98,183,37,122,14,124,65,67
读写头起始位置:53
要求计算:
思想:按访问请求到达的先后次序服务
优点:简单,公平
缺点:效率不高,相临两次请求可能会造成最内到最 外的柱面寻道,使磁头反复移动,增加了服务时间, 对机械也不利
思想:优先选择距当前磁头最近的访问请求进行服务 主要考虑寻道优先
优点:改善了磁盘平均服务时间
缺点:造成某些访问请求长期等待得不到服务
思想:当设备无访问请求时,磁头不动;当有访问请求时,磁头 按一个方向移动,在移动过程中对遇到的访问请求进行服 务,然后判断该方向上是否还有访问请求,如果有则继续 扫描;否则改变移动方向,并为经过的访问请求服务,如此反复。
按柱面(磁道)位置选择访问者
移动臂到达最后一个柱面后,立即带动读写磁头快速 返回到0号柱面
效果:减少了新请求的最大延迟
克服“磁头臂的粘性”
旋转调度:根据延迟时间来决定执行次序的调度
三种情况:
若干等待访问者请求访问同一磁头上的不同扇区
若干等待访问者请求访问不同磁头上的不同编号的扇区
若干等待访问者请求访问不同磁头上具有相同的扇区
解决方案:
对于前两种情况:总是让首先到达读写磁头位置下的扇区先进行传送操作
对于第三种情况:这些扇区同时到达读写磁头位置下,可任意选择一个读写磁头进行传送操作
例子
记录在磁道上的排列方式也会影响输入输出操作的时间。
例子:处理程序要求顺序处理8个记录;磁盘旋转一周为20毫秒/周;花5毫秒对记录进行处理。
记录的成组:把若干个逻辑记录合成一组存放一块的工作
进行成组操作时必须使用内存缓冲区,缓冲区的 长度等于逻辑记录长度乘以成组的块因子。
成组目的:提高了存储空间的利用率;减少了启 动外设的次数,提高系统的工作效率。
记录的分解从一组逻辑记录中把一个逻辑记录分离出来。
RAID(独立磁盘冗余阵列) (Redundant Arrays of Independent Disks) :多块磁盘按照一定要求构成一个独立的存储设备。
目标:提高可靠性和性能
考虑:磁盘存储系统 的 速度、容量、容错、数据 灾难发生后的数据恢复
数据的组织方式:
通过把多个磁盘组织在一起,作为一个逻辑卷提供磁盘跨越功能
通过把数据分成多个数据块,并行写入/读出多个磁盘,以提高数据传输率(数据分条stripe)
通过镜像或校验操作,提供容错能力(冗余)
最简单的RAID组织方式:镜像
最复杂的RAID组织方式:块交错校验
数据分布在阵列的所有磁盘上
有数据请求时,同时多个磁盘并行操作
充分利用总线带宽,数据吞吐率提高,驱动器负载均衡
要解决的问题是:如何把一个进程的地址空间的内容装载到内存,然后合理地来分配使用内存,使得每一个进程能够正确地执行。
综上,引出了地址重定位的支持。
用户程序经过编译、汇编后形成目标代码,目标代码通常采用相对地址的形式,其首地址为0,其余地址都相对于首地址而编址。不能用逻辑地址在内存中读取信息
为了保证CPU执行指令时可正确访问内存单元,需要将用户程序中的逻辑地址转换为运行时可由机器直接寻址的物理地址,这一过程称为地址重定位。
当用户程序加载到内存时,一次性实现逻辑地址到物理地址的转换。一般可以由软件完成。
- 优点:在程序的执行过程中,地址就直接可以拿来去到内存中取指令或者取数据。
- 缺点:程序在内存的位置不能改变,一旦改变就要重新计算这个转换过程 。
在进程执行过程中进行地址变换。 →→ 即逐条指令执行时完成地址转换。为了加快速度,需要硬件部件支持。
假设起始的地址是在重定位寄存器(基址寄存器)中,会把逻辑送到了这个寄存器进行计算, CPU在执行时会取到一个逻辑地址,然后就把这个地址送到了这个重定位寄存器来完成地址转换的工作,得到真正的物理地址。
所以逻辑地址经过这样一个部件的转换就会得到物理地址,然后用这个物理地址到内存中去存取相关的指令或者数据。
管理方式主要分为:等长划分和不等长划分。
主要使用的数据结构是 位图。
每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)
主要使用的数据结构是 空闲区表,已分配区表和空闲块链表。
空闲区表,已分配区表
表中每一项记录了空闲区 (或已分配区)的起始地址、长度、标志
空闲块链表
每一个表项用链串联起来
以空闲区表和已分配区表为例介绍内存分配的算法。
首次适配 first fit
下次适配 next fit
最佳适配 best fit
最差适配 worst fit
当找到了满足要求的空闲区后,将该空闲区分为两部分,一部分供进程使用,另一部分形成新的空闲区。
示例:
内存回收算法主要考虑的就是合并。
当某一块归还后,前后空闲空间合并,修改内存空闲区表
四种情况
伙伴系统是Linux低层内存管理采用的一种经典的内存分配方案。它是一种特殊的 “分离适配”算法。
主要思想:将内存按2的幂进行划分,组成若干空闲块链表;查找该链表找到能满足进程需求的最佳匹配块。
算法:
否则,将块划分为两个大小相等的伙伴,大小为$2^{U-1}$
一直划分下去直到产生大于或等于 s 的最小块
示例:
单一连续区、固定分区和可变分区,它们的特点进程作为一个整体进入一片连续的区域。
页式,段式和段页式这三种方案不是整个进程进入内存的,而是进入内存的若干个区域,而且这些区域也不连续。
基本思想:一段时间内只有一个进程在内存。
特点:简单,内存利用率低
有三种不同的布局:
示例:
根据进程的需要,把内存空闲空间分割出一个分区,分配给该进程
剩余部分成为新的空闲区
缺点:
碎片:很小的、不易利用的空闲区,会导致内存利用率下降。
解决方案:
紧缩技术(memory compaction):在内存移动程序,将所有小的空闲区合并为较大的空闲区。
不是所有进程都可以随便的搬家移动的,需要考虑两个问题:
开销。如果有很多进程都需要移动,这会导致时间,空间上的开销。
移动的时机。 比如一个进程正在做磁盘的IO操作 ,那这时此进程就不能够移动去别处,因为这样会影响IO的结果。
以页为单位进行分配,并按进程需要的页数来分配;逻辑上相邻的页,物理上不一定相邻。
典型页面尺寸:4K 或 4M
逻辑地址实际上由两部分组成,页号和页内地址。
这种划分是系统硬件自动完成的。 对用户来讲实际上是透明的,这是页式存储管理方案的一个特点。
页表会把逻辑上的某一页和物理上的某一页框的对应关系记录下来, 通过页表记录的这样一个映射关系。页表里的一行叫做页表项。
会产生内碎片问题。
假设一个进程需要五页,然后加一条指令,尽管只有还剩一条指令,但还是得要给它分六页,所以实际上还是得给它分成六页,假设最后一条指令在这个第六页上,只占了很小很小的空间,整个大部分的页面都是空的,而这些空的内存空间就是内碎片。
逻辑地址和页式不一样的地方是:段号和段内地址不是自动划分的,必须显示给出。
示例:
要把逻辑地址和物理地址对应关系记录下来就需要一个数据结构,这就是段表。
段表:
物理内存管理:
地址转换(硬件支持)
数据结构及有关操作
每个进程由一张段表和多个页表组成的。
地址转换
首先拿到逻辑地址,逻辑地址是由两部分组成:段号和段内地址。用段号去查段表,得到了所对应的页表的起始地址和长度,然后再把段内地址自动划分成两部分:页号页内地址,用页号去查对应的页表,得到页框号,然后再和页内地址拼接成最后的物理地址。
整个这个过程比较复杂,成本也比较高。
为了解决在较小的内存空间运行较大的进程。采用了内存”扩充”技术。
解决的问题:程序大小超过物理内存总和
程序执行过程中,程序的不同部分在内存中相互替代
按照其自身的逻辑结构,将那些不会同时执 行的程序段共享同一块内存区域
要求程序各模块之间有明确的调用结构
程序员声明覆盖结构,操作系统完成自动覆盖
主要用于早期的操作系统
设计思想
讨论:实现时遇到的问题
虚拟存储技术是指:当进程运行时,先将其一部分装入内存,另一部分暂留在磁盘,当要执行的指令或访问的数据不在内存时,由操作系统自动完成将它们从磁盘调入内存的工作。
虚拟地址空间:引入了虚拟存储技术之后每个进程的地址空间
虚拟地址:是在虚拟内存中指令或数据的位置,该位置可以被访问,仿佛它是内存的一部分
产生疑问:虚拟内存的地址究竟在哪里?
虚存的大小受到了计算机系统的寻址机制还有磁盘空间中可用空间的这两方面的限制。
防止地址越界,防止访问越权。
示例
把虚拟存储技术应用到页式存储管理方案就得到了虚拟页式存储管理系统
基本思想
1、请求调页(demand paging)
当需要这个页面,这个页面还没在内存,这时操作系统把它调入内存。
2、预先调页(prepaging)
预测猜测哪些页面即将会被用到,提前把它调入内存
虚拟存储技术其实以CPU时间和磁盘空间换取昂贵内存空间, 这是操作系统中的资源转换技术。
当进程运行的过程中如果要访问一些页面,那就要通过一个页表,页表记录哪些页面已经加载到内存。
页表由页表项组成,包含页框号、有效位、访问位、修改位、保护位。
有效位(驻留位、中断位):表示该页是在内存还是在磁盘
访问位:引用位
通常,页表项是硬件设计的。
32位虚拟地址空间的页表规模
64位虚拟地址空间的页表规模
页面大小为4K;页表项大小为8字节
页表规模: 32,000 TB
页表页在内存中若不连续存放,则需要引入页表页的 地址索引表 → 页目录
虚拟地址空间$2^{48}$
从物理地址空间出发,整个系统建立一张页表
页表项记录进程i的某虚拟地址(虚页号)与页框号的映射关系
PowerPC、UltraSPARC和IA-64 等体系结构采用
将虚拟地址的页号部分映射到一个散列值
散列值指向一个反转页表
整个过程是由MMU内存管理单元完成的。
内存管理单元的作用将CPU取到的虚拟地址转换成物理地址。
MMU的位置如下:
页表 → 两次或两次以上的内存访问
CPU的指令处理速度与内存指令的访问速度差异大,CPU的速度得不到充分利用
如何加快地址映射速度,以改善系统性能?
利用程序访问的局部性原理→引入快表(TLB)→加快地址转换的速度
快表(Translation Look-aside Buffers):实际上是一个缓冲区。
特点:
一种随机存取型存储器,除连线寻址机制外,还有接线逻辑,能按特定的匹配标志在一个存储周期内对所有的字同时进行比较
快表通常称为相联存储器,特点是按内容并行查找。
快表容量有限,保存正在运行进程的页表的子集(部分页表项)
快表的大小:像高速缓存一样分成几级,每一级的大小不一样。
快表的位置:在CPU片上
页错误又称页面错误、页故障、页面失效。指的是地址转换过程中硬件产生的异常。
具体原因:
所访问的虚拟页面没有调入物理内存 → 缺页异常
页面访问违反权限(读/写、用户/内核)
错误的访问地址
代码,数据呀,以及引用的一些共享库是有内容的,但是在访问过程当中的虚拟地址指向了没有 内容的位置,这相当于错误的访问地址。
是一种PageFault
在地址映射过程中,硬件检查页表时发现所要访 问的页面不在内存,则产生该异常——缺页异常
由操作系统执行缺页异常处理程序,主要工作:获得磁盘地址, 启动磁盘,将该页调入内存
可以增加预取的功能。 也就是在把页面调入内存的同时,顺带的把相关的一些页面也都调进内存。 比如Windows就会这么做,当要读入一段页面,这个页面是代码内容的话,就会接着多读入几个页面。 数据的话,它也会多读入几个页面,这就可以在一定程度上防止缺页的再发生。
驻留集大小:给每个进程分配多少页框。
分配策略:
固定分配策略
可变分配策略
当一个内存已经用完,没有空闲页框时,就需要挑一些页框把它内容换出去,这就是置换。
置换范围
置换策略
局部置换策略
全局置换策略
所有策略的目标
→置换出的页框是最近最不可能访问的页
根据局部性原理,最近的访问历史和最近将要 访问的模式间存在相关性,因此,大多数策略都基于过去的行为来预测将来的行为
注意:置换策略设计得越精致、越复杂,实现 的软硬件开销就越大
约束:不能置换被锁定的页框
为什么要锁定页面?
例如:操作系统核心代码、关键数据结构、I/O缓冲
清除:从进程的驻留集中收回页框
虚拟页式系统工作的最佳状态:发生缺页异常时,系统中有大量的空闲页框
结论:在系统中保存一定数目的空闲页框供给比使用所有内存并在需要时搜索一个页框有更好的性能
设计一个分页守护进程(paging daemon),多数时间睡眠着,可定期唤醒以检查内存的状态
如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存
如果页面装入内存后被修改过,则将它们写回磁盘。分页守护进程可保证所有的空闲页框是“干净”的
当进程需要使用一个已置换出的页框时,如果该页框还没有被新的内容覆盖,系统将它从空闲页框集 合中移出即可恢复该页面。
页缓冲技术:
不丢弃置换出的页,将它们放入两个表之一:如果未被修改,则放到空闲页链表中,如果修改了,则放到修改页链表中
被修改的页定期写回磁盘(不是一次只写一个,大大减 少I/O操作的数量,从而减少了磁盘访问时间)
被置换的页仍然保留在内存中,一旦进程又要访问该页,可以迅速将它加入该进程的驻留集合(代价很小)
设计思想:
这个算法的实现是要建立在已经知道页面的走向序列的基础之上才能够实施这个算法。所以通常作为一种标准来衡量其他算法的性能。
设计思想
实现:页面链表法
在先进先出基础上做的改进
设计思想
第二次机会算法问题:摘链、 挂链都需要花一些开销。
时钟算法是把所有的页框组织成一个环形,然后用一个指针,通过移动指针来选择下一个要淘汰的页框。
设计思想:
实现:
如果硬件没有这 些位,则可用软 件模拟(做标记)
启动一个进程时,R、M位置0。R位被定期清零(复位)
发生缺页中断时,操作系统检查R,M:
算法思想: 随机从编号最小的非空类中选择一页置换
从指针的当前位置开始,扫描页框缓冲区,选择遇到的第一个页框 (r=0;m=0) 用于置换( 本扫描过程中,对使用位不做任何修改)
如果第1步失败,则重新扫描,选择第一个(r=0; m=1)的页框(本次扫描过程中,对每个跳过的页框,将其使用位设置成0)
如果第2步失败,指针将回到它的最初位置,并且集合中所有页框的使用位均为0。重复第1步,并且,如果有必要,重复第2步。这样将可以找到供置换的页框
设计思想:
优点:
实现:
设计思想:
实现:
改进(模拟LRU):计数器在加R前先右移一位,R位加到计数器的最左端
系统给某进程分配3个页框(固定分配策略),初始为空
进程执行时,页面访问顺序为: 2 3 2 1 5 2 4 5 3 2 5 2
要求:
例子:系统给某进程分配 m个页框,初始为空
页面访问顺序为:1 2 3 4 1 2 5 1 2 3 4 5
采用FIFO算法,计算当 m=3 和 m=4 时的缺页中断次数:
颠簸(Thrashing,抖动) :虚存中,页面在内存与磁盘之间频繁调度,使得调度页面所需的时间比进程实际运行的时间还多, 这样导致系统效率急剧下降,这种现象称为颠簸或抖动。
Intel80x86/Pentium:4096或4M。
引用多种页面尺寸,为有效使用TLB带来灵活性,但给操作系统带来复杂性。
页面尺寸考虑的因素:
最优页面大小:$P = \sqrt{2se}$
例子:分配了一个页框;页面大小为128个整数; 矩阵$A_{128 \times 128}$按行存放
左边的编制方法导致的缺页次数比右边多很多。
根据程序的局部性原理,一般情况下,进程在一段时间内总是集中访问一些页面,这些页面称为活跃页面,如果分配给一个进程的物理页面数太少了,使该进程所需的活跃页面不能全部装入内存,则进程在运行过程中将频繁发生中断。如果能为进程提供与活跃页面数相等的物理页面数,则可减少缺页中断次数。
工作集是需要随时调整的,所以要计算当前的一个工作集是多少。
工作集内容取决于三个因素:
访页序列特性
时刻 t
工作集窗口长度 $(Δ)$
窗口越大,工作集就越大
找出一个不在工作集中的页面并置换它
思路:
每个页表项中有一个字段:记录该页面最后一次 被访问的时间
设置一个时间值T
扫描所有页表项,执行操作
如果一个页面的R位是1,则将该页面的最后一 次访问时间设为当前时间,将R位清零
如果一个页面的R位是0,则检查该页面的访问时间是否在 “当前时间-T” 之前
(1). 如果是,则该页面为被置换的页面;
(2). 如果不是,记录当前所有被扫描过页面的最后访问时间里面的最小值。扫描下一个页面并重复1、2
进程通过一个系统调用(mmap)将一个文件(或部分)映射到其虚拟地址空间的一部分,访问这个文件就象访问内存中的一个大数组,而不是对文件进行读写
在多数实现中,在映射共享的页面时不会实际 读入页面的内容,而是在访问页面时,页面才 会被每次一页的读入,磁盘文件则被当作后备存储
是利用了虚拟存储机制中的缺页异常,来把相应文件内容读入内存的
当进程退出或显式地解除文件映射时,所有被修改页面会写回文件
例如:两个进程共享三个页,每页都标志成写时复制
当某一个进程试图要改变其中一个页面的内容时,它要做一个写操作,那么和只读就产生了冲突,就会产生Page Fault。进入了操作系统后,操作系统检查出它是一个写时复制操作,就会在内存里另开辟一个页面把相应的内容写到这个页面里。而新复制的这个页面对于执行写操作的这个进程来讲是一个私有的,对于其他的进程是共享写时复制页面的进程,看不到这样一个结果。这是写时复制技术的一个基本的实现。
]]>问题的提出:这一讲的问题都是由于并发所引起的.并发是所有问题产生的基础,并发也是操作系统设计的一个基础。
每个进程在它的生命周期期间一会儿上CPU执行,一会儿由于某种原因暂停执行,所以每个进程的执行是间断性的
进程执行的间断性使得进程的相对执行速度是不可预测的。由于有进程调度,有其他事件的发生,每个进程上CPU执行可能执行一段时间停止,然后再接着执行,所以整个执行的时间是不可预测的。
在一个并发环境下多个进程或者线程之间会共享某些资源,在这些资源的使用过程中会产生进程之间的一种制约性。比如当一个进程享用打印机这个资源,另外一个进程在第一个进程没有释放这个资源的前提之下就得不到这个资源,那就得等待。因此在一个并发环境下多个进程的执行会带来一种制约。
进程执行的结果和它的相对执行速度是有关系的,因此在不同的执行顺序的情况下,进程的执行结果也是不确定的。
场景是 get、copy和put 三个进程并发执行:
假设 g,c,p 分别为 get,copy 和 put 的一次循环过程。因此从当前状态出发能得到正确的结果。
所以如果不满足进程的制约关系,调度的顺序不正确,那就会带来这些错误的结果。
三个进程的制约关系如下:
当get执行完第一个循环之后,只能够copy执行它的第一个循环。
当copy执行完第一个循环之后可以 是put执行第一个循环,也可以是get执行第二个循环。
以上两个都执行完了,才能够去执行copy的第二个循环。
因此这三个进程之间的制约关系应该满足这样一个前趋图才能保证不出错误。
竞争条件定义:两个或多个进程读写某些共享数据, 而最后的结果取决于进程运行的精确时序。
和时间是相关的 这就是带来了竞争条件。
竞争条件是由于有一个共享的资源,而多个进程都对这个数据进行相应的操作,所以会带来竞争条件这个概念,比如打印机。
进程互斥的概念:由于各进程要求使用共享资源(变量、文件等), 而这些资源需要排他性使用,各进程之间竞争使用这些资源,这一关系称为进程互斥。
临界资源 (critical resource):系统中某些资源一次只允许一个进程使用,称这样的资源为临界资源或互斥资源或共享变量。
进程互斥所使用的共享资源是一个核心,这个共享资源就是临界资源。
临界区(互斥区) [critical section(region)]: 各个进程中对某个临界资源(共享变量)实施操作的程序片段。
当多个进程都要使用同一个共享资源时,它的代码里就会有相应的操作。而这些代码就是临界区。
这些程序片段,分散在不同的进程里,它们的共同的特点是对同一个共享变量进行一些操作。这一段代码,和另外一个进程的这一段代码互为临界区,互为互斥区。
如下图所示,A进程在临界区里还没有出临界区,如果B进程上CPU之后也想进临界区,应该不能够让它进去,如果B进程也进临界区,就会出现关键活动的交叉,带来前面所介绍的各种各样的错误。因此当B进程想要进临界区时,由于A进程还在临界区里,所以 B 进程只能够被阻塞。
回顾上一章优先级反转问题。 有一个低优先级的进程进入了临界区,因为它优先级比较低,有更高优先级进程就绪时就会抢占它的CPU ,可低优先级进程已经在临界区里,所以更高优先级的进程也上不了CPU运行,因为上CPU运行也进不了临界区。所以就被阻塞。
而在高优先级和低优先级之间又会有中级优先级且非常耗时的一些进程在执行,使得低优先级的进程上不了CPU ,也就不能够让高优先级的进程尽快上 CPU。
给出临界区的使用原则:
假设 P先上的CPU,free的初值是 false ,所以循环结束。
如果这时进程P被切换下CPU,而上CPU的正好又是进程 Q,那么进程Q也要判断free是不是false,由于进程P还没有来得及改变它的值,因此Q检测的结果free也等于false ,继续往下执行把 free 变成 true ,然后进入了临界区。 如果Q进入临界区之后又一次被切换下去了。假设正好又是P上CPU了,把 free 变成 true ,然后进入临界区。结果在临界区里两个进程,不满足原则。这就是一个错误的解法。
解决方案:
把两条语句写成一个lock函数,如果把lock函数设计成一个原语,在执行过程中不容许被中断,那么这个操作就是正确的了。
如果P进程想进临界区,而turn等于false ,所以它一直在等待进入临界区。可是如果Q进程始终没有进过临界区,也不想进临界区,那么 P进程就进不了临界区,尽管临界区里没有进程。也就是说,在临界区外的进程Q阻止了P进程进临界区。这也是不允许的。
如果当pturn设置为true之后,紧跟着qturn也是true,这就会导致两个进程都进不了临界区,都在”谦让”。而这,就是After you
问题。
在解法三的基础上引入turn,让turn值来决定是哪一个进程进入临界区,但是这个算法会出现忙等待 busy waiting
问题,浪费了时间。
当任何一个进程想进临界区时,它只需要调用enter_region
函数,查看能发安全地进入临界区,如果能安全地进入临界区,那么就是相当于这个函数执行结束,可以进临界区。如果不能够安全地进入临界区,就会在这个函数当中去等待进入临界区。
调用这个函数是用进程号,当进程使用完临界区的相关资源之后,出临界区时就调用leave_region
函数,就可以让其他的进程进入临界区。
利用开关中断指令,是一条允许中断或禁止中断的指令。
操作就是在进临界区之前先把中断关闭,然后进入临界区做相应的操作,出临界区时再把中断打开,允许中断。这实际上就是原语操作。
特点:
用测试并加锁这条指令去操作, 这一条指令做了两个事情,一是先读内存单元内容,读到寄存器,然后再去写,把内存单元的内容写上某个值。
TSL指令:TEST AND SET LOCK
交换指令的作用是把两个位置,可能是寄存器或内存单元,在一条指令结束的时把两个位置的内容进行一个交换。
XCHG指令:EXCHANGE
进程互斥指进程之间具有一种竞争关系,而进程同步指多个进程之间的协作关系 。
进程同步 (synchronization):指系统中多个进程中发生的事件存在某种时序关系,需要相互合作,共同 完成一项任务。具体地说,一个进程运行到某一点时, 要求另一伙伴进程为它提供消息,在未获得消息之前,该进程进入阻塞态, 获得消息后被唤醒进入就绪态。
来看一个场景 :
消费者判断count是否等于 0,如果在它判断count = 0还没有去调用sleep之前消费者被切换下CPU。那么这个时候count是0,假设生产者又生产了一个数据上CPU,可以看到就不断地去生产放到缓冲区,count也不等于 N ,刚才是0,现在生产者生产了一个数据加完 1 之后count就等于1。这时生产者知道count 等于 1,所以会做一个wakeup。但是由于刚才的消费者还没有sleep,所以做的这个wakeup实际上做了一个空操作,因为没有进程在睡眠,所以就继续接着执行,生产者继续生产。如果生产者被切换下CPU,消费者一上来肯定首先要做sleep,但是这个sleep的进程刚才wakeup已经做完了,所以就不会再被唤醒了。所以没有做到完全解决生产者/消费者问题。
之所以叫同步机制是因为通常把进程的互斥看成是一种特殊的同步。它既解决同步的问题,也能解决互斥的问题。这种典型的进程同步机制就称为信号量及PV 操作。
信号量:是一个特殊变量,用于进程间传递信息的一个整数值。
定义如下:
1 | struc semaphore { |
由一个值和一个队列组成,int值
传递信息的整数值,队列允许进程挂到上面。
声明一个信号量:semaphore s;
对信号量可以实施的操作: 初始化、P和V (P、V分别 是荷兰语的test(proberen)和increment(verhogen))
P、V操作为原语操作 (primitive or atomic action)
在信号量上定义了三个操作: 初始化(非负数)、P操作、V操作
最初提出的是二元信号量(解决互斥)
之后,推广到一般信号量(多值)或计数信号量(解决同步)
假定有三个进程 P1、 P2、 P3,它们都对同一个临界资源进行相应的操作,设定了一个信号量 mutex ,初值是 1。在临界区的前和后把 P、V操作加上。
让出 CPU 之后,假设 P3进程又上CPU。它也要进临界区。mutex -1 = 2。因此P3 进程也等在这个信号量上,等在P2后面,让出CPU。
假设P1又上CPU, 然后它在临界区里完成了工作后出临界区。,接着会执行一个 V(mutex) ,mutex + 1 = -1,这个时候信号量的值还是小于等于 0,因此V操作就会到队列里找到进程P2并把它送到就绪队列,然后P1接着做别的事情。
如果P2上CPU了,它就下一个就进入临界区。P操作执行完后,它接着就进临界区,当它出临界区又做一次 V 操作,mutex +1 = 0,还是小于等于 0 ,所以V操作就会把队列里等的P3进入就绪,就是这样一个过程。
如果把消费者的P(&full)和P(&mutex)互换,当出现缓冲区个数为0的时候,执行P(&mutex)和P(&full)之后,mutex=-1,此时如果生产者执行P(&emputy)和P(&mutex)会出现死锁。
两个V 操作的顺序可以颠倒,因为V操作只是把信号量的值加一。然后查看有无进程等在队列里,如果有就把它释放。 因此V操作不会使得调用V操作的这个进程进入等待状态,所以这两个的顺序是可以颠倒的。 颠倒的结果可能会带来其他的一些问题,比如说,临界区里头会多一点点指令,其他的进程想进临界区可能会稍微晚一点。但是代码中的是最理想的。
问题描述:
多个进程共享一个数据区,这些进程分为两组:
要求满足条件:
如果读者执行:
如果写者执行:
写者是和其他写者是互斥的,和读者也是互斥的。 所以读者写者问题本质上是一个互斥的问题。
第一类读者写者问题实际上是要解决的是多个读者可以同时读,因此不需要每个读者都去做P(w)或做V(w) 操作。
经过分析发现第一个读者 到来时如果没有其他的读者和写者,他可以去读。它在读之前首先要把临界区保护起来,所以第一个读者做P(w)。只要前面有读者在读,那么后续来的读者都可以去读。 当所有的读者都读完就会把临界区还回来,所以最后一个读者去做V(w)操作。
在代码中的读者里引入一个计数器rc,它用来记录现在有几个读者。进来一个读者之后通过 rc + 1
判断是不是第一个读者,如果rc = 1
,表示是第一个读者,那就去做P(w)的工作。每个读者离开都要去 rc- 1
,当最后一个读者做完 rc - 1
, rc就等于0了,最后一个读者去做V(w)操作。
因为多个读者都对rc进行相应的操作,所以rc就成为了一个新的临界资源。rc = rc + 1
判断 rc,或者 rc = rc - 1
判断 rc 都是一个临界区。 因此还要针对这样一个临界区再增加一个互斥的信号量,对rc这段代码进行保护。
应用场景:
如果每个执行实体对临界区的访问或者是读或者是写共享变量,但是它们都不会既读又写时,读写锁是最好的选择。
实例:
Linux的IPX路由代码中使用了读-写锁,用 ipx_routes_lock的读-写锁保护IPX路由表的并发访问
要通过查找路由表实现包转发的程序需要请求读锁;需要添加和删除路由表中入口的程序必须获取写锁(由于通过读路由表的情况比更新路由表的情况多得 多,使用读-写锁提高了性能)
信号量机制具有一些缺点,比如用信号量及PV操作解决问题的时程序编写需要很高的技巧。 如果没有合理地安排PV操作的位置,就会导致一些出错的结果比如说出现死锁等问题。 所以有人提出一种新的同步机制就是管程。 它实际上是在程序设计语言中引入的一个成分,称之为高级同步机制。
管程是一种特殊的模块,每个管程都有一个名字。管程主要是管理共享资源所对应的数据结构,所以管程在管理共享资源的同时也提供了在这个共享资源之上需要的各种各样的操作,也就是由一组操作的过程来组成。
示例:
进程与管程的关系:
作为进程,它只能通过调用管程给提供的各种过程来间接地来使用管程当中的数据结构。
作为一种同步机制,管程要解决两个问题:
管程中设置条件变量及等待/唤醒操作以解决同步问题
作用可以让一个进程或线程在条件变量上等待(此时,应先释放管程的使用权),也可以通过发送信号将等待在条件变量上的进程或线程唤醒
场景:
当一个进入管程的进程 执行等待操作时,它应当释放管程的互斥权。当后面进入管程的进程执行唤醒操作时(例如后进的P唤醒前面的Q),管程中便存在两个同时处于活动状态的进程。
解决方法:
因为管程的互斥是由编译器保证的,是语言机制。 所以这里头只考虑怎么样解决同步问题,同步问题是通过条件变量和条件变量上实施的wait和signal这两个操作来完成的。
条件变量——在管程内部说明和使用的一种特殊类型的变量。对于条件变量,可以执行wait和signal操作
wait(c):
如果紧急等待队列非空,则唤醒第一个等待者;否则释放管程的互斥权,执行此操作的进程进入 c(条件变量) 链末尾。
signal(c):
如果 c 链为空,则相当于空操作,执行此操作的进程继续执行;否则唤醒第一个等待者,执行此操作的进程进入紧急等待队列的末尾。
管程实现的两个主要途径:
直接构造——效率高
因为它是语言机制,所以可以在某个语言当中加入这样一个管程成分,然后去编写相应的编译器
间接构造——用某种已经实现的同步机制去构造
比如可以用信号量及 PV 操作来构造一个管程
引入MESA的原因:
Hoare管程的一个缺点:两次额外的进程切换
解决方式:解决:
signal
改成 notify
notify的结果:位于条件队列头的进程在将来合适的时候且当处理器可用时恢复执行
由于不能保证在它之前没有其他进程进入管程, 因而这个进程必须重新检查条件
用while循环取代if语句
导致对条件变量至少多一次额外的检测(但不再有额外的进程切换),并且对等待进程在notify 之后何时运行没有任何限制
对notify的一个很有用的改进
超时可以防止如下情况的发生:
broadcast:使所有在该条件上等待的进程都被释放并进入就绪队列
应用场景:
例子:生产者/消费者问题中,假设insert和remove 函数都适用于可变长度的字符块,此时,如果一个 生产者往缓冲区中添加了一批字符,它不需要知道 每个正在等待的消费者准备消耗多少字符,而仅仅 执行一个broadcast,所有正在等待的进程都得到通 知并再次尝试运行
Mesa管程优于Hoare管程之处在于Mesa管程错误比较少
在Mesa管程中,由于每个过程在收到信号后都重新检查管程变量,并且由于使用了while结构,一个进程不正确的broadcast广播或发信号notify,不会导致收到信号的程序出错
收到信号的程序将检查相关的变量,如果期望的条件没有满足,它会重新继续等待
管程:抽象数据类型 有一个明确定义的操作集合,通过它且只有通过它才能操纵该数据类型的实例
实现管程结构必须保证下面几点:
条件变量:为提供进程与其他进程通信或同步而引入
wait/signal 或 wait/notify 或 wait/broadcast
Pthread解决互斥的问题:在Pthread 中使用一个互斥量,通过对互斥量提供相应的操作来保护临界区。
Pthread解决同步问题:在Pthread 中使用条件变量,以及在条件变量上的各种操作
pthread_cond_wait的执行分解为三个主要动作:
1、解锁
2、等待
当收到一个解除等待的信号 (pthread_cond_signal或者 pthread_cond_broad_cast)之后, pthread_cond_wait马上需要做的动作是:
3、上锁
信号量和管程只能传递很简单的信息,不能传递大量的信息
比如要把一个大的数组传送给另外一 进程,那么信号量和管程在这一方面是做不到的。
管程不适合于用于多处理器的情况
因此在传递大量信息的时候需要引入新的通信机制,这个通信机制就是进程间通信机制。
非常典型的形式就是消息传递 ,实际上就是由send和receive提供这样的原语操作。当一个进程要把消息发送给另外一个进程时就去调用send; 当另外一个进程想接收消息时就去调用receive操作。
使用情况:分布式系统、基于共享内存的多处理机系统、单处理机系统。可以解决进程间的同步 问题、通信问题
发送消息:发送进程只是把消息准备好,调用send 操作,然后操作系统做相应的复制消息的内容,挂接的内容。
接收消息:接收进程接收消息时把请求提交给操作系统,操作系统完成把消息复制到接收进程空间的工作。
所以操作系统要提供这样一个通信机制,来完成进程之间的信息传送。
该通信模式需要解决两个问题:
第一个问题:需要在物理内存里建一个大家能够共享的一块内存空间。通过相应的映射能把这个物理内存空间映射到了两个进程相应的地址空间里。
第二个问题:其实就是读者写者问题,因为共享内存不能同时去写,可以同时去读。 所以可以利用控制读写者问题的这个方法来解决互斥问题。
利用一个缓冲传输介质——内存或文件连接两个相互通信的进程。
Linux提供了很多种工具,根据需要挑选合适的一种。
原子操作是不可分割,在执行完之前不会被其他任务或事件中断。它常用于实现资源的引用计数。
例子:
1 | atomic_t v = atomic_init(0); |
屏障主要是用于对一组线程进行协调的。
应用场景:
一组线程协同完成一项任务,需要所有线程都到达一个汇合点后再一起向前推进。
]]>CPU调度的任务是控制、 协调多个进程对CPU的竞争。即按照一定的调度算法,从就绪队列中选择一个进程,然后把CPU的控制权交给被选中的进程。如果就绪队列没有其它的进程,那么系统会安排一个空闲进程(idle 进程)上 CPU 运行。
CPU调度所面临的场景:
WHAT:按什么原则选择下一个要执行的进程
— 调度算法
WHEN:何时选择
— 调度时机
HOW:如何让被选中的进程上CPU运行
— 调度过程(主要内容是:进程的上下文切换)
系统运行时,会发生很多的事件。比如说,一些进程的操作:I/O 中断,时间中断。 这些事件发生以后系统要做相应的处理。 这是第二讲的主要内容。
事件发生 → 当前运行的进程暂停运行 → 硬件机制响应 后 → 进入操作系统,处理相应的事件 → 结束处理后:
事件处理完后某些进程的状态发生了变化,也可能创建出了一些新的进程。这就导致了就绪队列的改变,而这就需要进程调度。按照事先预定的算法从就绪队列中重新选择一个进程 ,这就是调度的时机。 即就绪队列的改变引发了重新调度。
进程调度的时机有4个:
总而言之什么时候重新调度?
往往是内核对中断,陷入,系统调用等处理之后,返回到用户态,这个时候要重新调度。这就是CPU 调度的时机。
当有一个新的进程被选中之后 ,这个进程可以是刚刚被暂停执行的进程,也可以是一个新的进程。 如果是一个新的进程,那就要发生一个进程切换。
进程切换就指的是一个进程让出CPU,另外一个进程占用CPU的一个过程。
进程切换主要包括两部分工作:
先切换全局页目录,以此来加载一个新的地址空间
因为新的进程上CPU ,那么它要用自己的地址空间
是切换内核栈和硬件上下文,其中硬件上下文包括了内核执行新进程需要的全部信息,如 CPU相关寄存器
因此,进程切换实际上包括了对原有进程的各种状态的保存,以及对新的进程的状态的恢复,这样一个过程。
例子:如果进程A下CPU,进程B上CPU ,那么进程切换主要做的工作呢?
保存进程A的上下文环境 (程序计数器、程序状态字、其他寄存器……)
用新状态和其他相关信息更新进程A的PCB
把进程A移至合适的队列 (就绪、阻塞……)
将进程B的状态设置为运行态
从进程B的PCB中恢复上下文(程序计数器、程 序状态字、其他寄存器……)
做完了这些事情,进程B就上CPU运行, 而进程A的所有信息保存好之后,以后它还可以继续上CPU接着执行。
上下文切换的开销包括两部分:
直接开销:内核完成上下文切换所花费CPU的时间
这些时间用于:
- 保存和恢复寄存器
- 用于切换地址空间 (切换地址空间的指令比较昂贵)
间接开销
高速缓存(Cache)、缓冲区缓存(BufferCache) 和 TLB快表(Translation Look-aside Buffer)失效
高速缓存中存放了刚才执行的这些进程中的一些指令和数据, TLB快表中存放了进程的一些页表表项。当新的进程上CPU之后,原来的这些内容都都失效了。还要把新的进程所需要的指令数据,送入高速缓存或者是把新的进程的页表表项送入TLB快表里。这又需要花一些时间,这些都是间接开销。
从操作系统发展的角度来看调度算法的一些演变:
批处理系统 → 多道程序设计系统 → 批处理与分时的混合系统 → 个人计算机 → 网络服务器
- 早期的批处理对调度算法的要求不高,也比较简单,那么因为它是一个程序执行完了以后接着执行下一个程序
- 有了多道程序设计系统之后,多个程序都要想进内存,去上CPU,此时调度算法就要去选择让哪些进程早一点上CPU了
- 到了批处理与分时系统混合系统中,调度算法既要照顾到前台的进程也要照顾到那些后台的进程,需要合理地安排调度的顺序,让不同的进程都能有机会上CPU执行
- 个人计算机刚开始时调度算法非常简单,后来慢慢慢慢演化到现在桌面操作系统里头调度算法变得非常复杂
- 网络服务器由于有多个客户端同时向它提出服务请求,所以要在调度算法上下一些功夫
从用户角度和系统角度对调度算法的要求不一样,下图描述了性能方面的不同要求:
从上图可以看到,用户的角度和系统的角度对于调度算法有不同的要求。 而这些要求有时候是相互矛盾的。 因此在设计调度算法的时候是要在各种因素中折中权衡。
吞吐量 Throughput
每单位时间完成的进程数目
周转时间TT (Turnaround Time)
每个进程从提出请求到运行完成的时间
响应时间RT (Response Time)
从提出请求到第一次回应的时间
其他
CPU 利用率 (CPU Utilization)
CPU做有效工作的时间比例
等待时间 (Waitingtime)
每个进程在就绪队列 (ready queue) 中等待的时间
设计调度算法时要考虑以下几个问题:
进程控制块PCB中
需要记录哪些与CPU调度有关的信息
进程优先级及就绪队列的组织
优先级和优先数是两个不同的概念。
优先级是表现进程的重要性和紧迫性。 优先数是一个数值,它反映了某一个优先级。
有些系统像 UNIX 优先数小的优先级高,但是不能完全根据数的大小来决定优先级的高低。
根据优先级是否能改变分为静态优先级和动态优先级。
静态优先指在进程创建时,指定优先级之后在进程运行的过程中,这个优先级不再发生改变。
动态优先级是在进程运行过程中,优先级的级别还会不断地调整。
有的时候需要动态优先级,比如当一个进程在就绪队列当中等待的时间很长的时候,要提升它的优先级,让它尽快有机会得到CPU去运行。
按照优先级来排队 :
可以按照优先级来组织就绪队列。当创建一个新的进程的时候,根据它的优先级排不同的就绪队列,在这里设定n个就绪队列优先级,不同的优先级进入不同的就绪队列。 就绪队列 1 的优先级是最高的,当调度程序选择进程的时候,首先应该从高优先级的队列来选择进程 。如果高优先级队列没有进程了,再从次高就绪队列来选择进程。
另一种排队方式:
所有的进程第一次创建之后都进到第一级就绪队列,随着进程的运行可能会降低某些进程的优先级。比如当一个进程分配给它的时间片用完了,那么它就会降一级,降到第二级就绪队列。 如果它经常地用完时间片,慢慢的这个进程就会降低到最后一级就绪队列。进程调度首先在第一级队列里选,说明第一级就绪队列的优先级比较高。如果这个队列是空的,那么调度就会从其他的就绪队列里头选择进程。如果进程已经进到了最后一级队列,那么它的优先级就越来越低,它被调度上 CPU 的机会就会变得很小。
指占用CPU的方式:
当有比正在运行的进程优先级更高的进程就绪时,系统可强行剥夺正在运行进程的CPU,提供给具有更高优先级的进程使用。
某一进程被调度运行后,除非由于它自身的原因不能运行,否则一直运行下去
按进程执行过程中的行为划分:
I/O密集型或I/O型(I/O-bound)
频繁的进行I/O,通常会花费很多时间等待I/O操作的完成
CPU密集型或CPU型或计算密集型(CPU-bound)
需要大量的CPU时间进行计算
在设计调度算法时,通常会对I/O型的进程会有一些友好,希望更多的I/O型进程早一点上CPU运行。
因为这些进程上CPU 之后只用了很短的一下,CPU时间就让出CPU,因为它要去做其它的输入输出操作。因此对于一般的调度程序,都会对I/O型进程更偏好一些。
时间片指的是一个时间段,指分配给调度上CPU的进程,允许这个进程在CPU上执行多长时间,它是一个时间的长度。
那么如何选择时间片呢? 应该考虑很多的因素:
进程切换的开销有多大
进程对响应时间的要求
系统当中有多少进程处于就绪
CPU的能力有多大
进程的行为(不同进程可能区别对待)
因此在设计时间片大小的时候要考虑到这些因素。 具体分配时间片的方式会在时间片轮转算法时进行介绍。
介绍批处理系统中的常用的调度算法:
在批处理操作系统中对于算法的要求往往是看它的带来的 吞吐量大小,周转时间,CPU 利用率,公平和平衡 这几个因素。
先来先服务调度算法也称为先进先出。指的是按照进程就绪的先后顺序来使用 CPU。先进先出调用算法是一个非抢占式的调度算法。
优点:
选择进程时从队首选择, 新就绪的进程排在队尾。
缺点:
如果有一个短的进程排在了一个长时间运行进程的后面, 它需要等待的时间比较长, 这样使得用户体验不够友好。
假设有三个进程,按照P1、P2、P3就绪。 P1进程需要运行24秒,P2和P3分别需要3秒计算。
采用先来先服务的调度算法调度:
同样还是这个例子, 改变调度的顺序:
可以看到平均周转时间降低到了13秒。
所以可以看到改变调度的顺序,也就是调度算法的改变会改善平均周转时间, 进而可能改善了响应时间这些指标。
短作业优先指的是具有最短完成时间的进程优先执行。
如果短作业优先调度算法再改进一下: 加抢占模式,变成了抢占式的版本。这就是最短剩余时间优先调度算法。
所谓最短剩余时间就是当 一个新就绪的进程 比 正在当前运行的进程它的 剩余的运行时间短的时候, 系统就会去抢占当前的进程,然后把CPU交给这个运行时间更短的进程。
因为在一个短作业运行的过程中,可能有一个新的进程创建出来,或者一个进程从等待变成就绪,那么它剩余的时间比当前正在运行的这个进程剩余的这个运行时间还短, 就要抢占了。
有四个进程到达时刻不是同时到达的,有不同的时刻:
优点:
缺点:
既有先来先服务的优点,又有短作业优点,,这是操作系统当中的一个常用的手段——折衷权衡。 这种调度算法其中之一就是最高响应比优先。
基本思想是这样的:
调度时,首先计算每个进程的响应比R;之后,总是选择R最高的进程执行。
其中一种方案:
很明显地看到,如果处理时间短,也就是短作业,那么分母小,整个的值就大,因此响应比就大。
如果很早就绪了,在就绪队列中等的时间非常长,那等待时间就变得越来越大, 最后响应比变得非常大,被调度上 CPU。
介绍交互式系统中所采用的一些调度算法:
追求的指标主要是响应时间,公平,资源的平衡使用。最短进程优先调度算法呢短作业优先调度算法差不多,就不再介绍。
左图看到当前正在运行的是B进程,排在它后面的是F进程。当B进程用完它的时间片之后就回到队列的末位,调度就会选择下一个进程,所以右图当前运行进程F上CPU。这就是一个时间片轮转,然后F运行完它的时间片后就继续去排队。队列里面的每一个进程都有机会轮流上CPU。
目标:
具体做法:
周期性地切换
每个进程分配一个时间片
以此来达到改善短作业的平均响应时间的目标。
上图可以看到这个进程开始运行后,给它一个时间片,在没有用完时间片的前提下就已经完成了一个响应。如果系统中绝大部分进程都不到一个时间片就可以完成响应的话,那么时间片轮转算法实际上已经退化成了一个先来先服务的这种算法。另外如果时间片过长会延长某些进程的响应时间。
如果时间片太短,响应时间也会变长,频繁地去切换,那切换这个行为本身会带来开销。前面已经讲过上下文切换是有开销的,所以切换也就浪费了CPU的时间。
公平 (进程轮流上CPU)
有利于交互式计算,响应时间快
对于不同大小的进程是有利的
由于进程切换,时间片轮转算法要花费较高的开销 (得让切换所带来的开销对于时间片来说能做到忽略不计)
对于大小相同的进程是不利的
时间片轮转算法往往不区分I/O型进程与CPU型进程。这样会给I/O型进程带来一定的不公平。
因为当一个CPU型进程被调度上CPU之后,用完它的时间片就会去重新排队,下一次再调度上CPU又用会完一个完整时间片。而I/O型进程被调度上CPU之后,运行完它的时间片,可能运行的很短就完成了它的任务,然后就去等待I/O,进入等待队列。一旦等待的结果到来,又变成一个就绪再次上 CPU,它又没有用完分配给它的时间片就结束了任务。所以CPU型的进程总是用完给它的时间片。 所以它占用了更多的 CPU 时间而I/O型进程总是用不完它的时间片,所以这个调度算法对它有些不公平。
上图是设计的虚拟轮转法:
当一个I/O型进程让出 CPU进到等待队列,从等待队列又重新回到就绪状态的时候。不去进入原来的就绪队列,会单独为它设置一个队列,叫做辅助队列。也就是所有I/O型的进程从等待变成就绪的时会进到这个队列。调度算法在选择进程的时候首先从这个辅助队列里去选择I/O 型进程。I/O 型进程上CPU之后,又很快地放弃了CPU进入等待直到辅助队列为空。实践表明这个方案改善了这个对I/O型进程的一种不公平性。
最高优先级调度算法思想比较简单,总是选择优先级最高去执行。
通常情况下:
优先级可以是静态不变的,也可以动态调整。优先数可以决定优先级。就绪队列可以按照优先级组织
特点:
如果是基于优先级的抢占式调度算法,就会出现一个优先级反转问题。
优先级反转又称:优先级反置、翻转、倒挂
现象:
一个低优先级进程持有一个高优先级进程所需要的资源,使得高优先级进程等待低优先级进程运行。
例子:
影响:
解决方案
设置优先级上限
凡是进入临界区的进程优先级都是最高的。 不在临界区的进程优先级都会比这个进入临界区的这进程优先级要低。这样的话就可以执行完成,然后把临界区还回去。
优先级继承
如果一个低优先级的进程阻碍了一个高优先级进程执行,它可以临时地继承这个高优先级的这个进程的优先级,一下子把自己优先级继承到这个高优先级的这个程度。那么就可以去运行,然后把临界区还回去。
使用中断禁止
凡是进入临界区的进程,那么就不再响应中断的。 直到出临界区才响应中断,这样就保护了这个进程,让它继续去执行。
多级反馈队列调度算法是UNIX 的一个分支,是BSD5.3版所采用的调度算法。它是在前面各种调度算法的基础之上提出的一个综合的调度算法,是在考虑了各种因素之后进行折中权衡的一个结果。
多级反馈队列调度算法的基本思想:
设置多个就绪队列,第一级队列优先级最高
给不同就绪队列中的进程分配长度不同的时间片,第一级队列优先级最高但时间片最小;随着队列优先级别的降低,时间片增大
在进行调度时,先从优先级高的进行。当第一级队列为空时,在第二级队列调度,以此类推
各级队列按照时间片轮转方式进行调度
当一个新创建进程就绪后,进入第一级队列
进程用完时间片而放弃CPU,进入下一级就绪队列
由于阻塞而放弃CPU的进程进入相应的等待队列,一旦等待的事件发生,该进程回到原来一级就绪队列
可以根据不同的情况来设计不同的方案以体现系统对这一类进程的偏好程度。
比如说进程是回到原来一级就绪队列的队首还是队尾?如果回到队首,说明系统对这类进程更加友好
另外当进程再度被调度上 CPU 之后是让它运行完剩余的时间片还是重新给它分配一个完整的时间片让它去运行? 也体现了系统对这类进程的偏好程度。
以上为非抢占式的。
若允许抢占:
当有一个更高优先级的进程就绪的时候可以抢占正在运行进程的CPU,那么被抢占的进程会回到原来一级就绪队列的末尾。
当然也可以有不同的设计方案比如说回到原来一级就绪队列的队首。
当这个进程再度被调度上CPU时可以运行完它刚才剩余的时间片,也可以重新给它一个完整的新的时间片让它运行。因此又派生出不同的设计方案 。
上图反映了一个进程在队列里头的一些迁移活动。
当创建一个新的进程时,所有的进程都进入第一级队列。
如果是 I/O 型的进程,那么它可能被调度上 CPU 之后很短时间就去等待I/O,当它从等待队列又回到就绪队列的时候,让它回到原来一级就绪队列,所以优先级没有降低,被调度上 CPU 的机会很多。
但是对于CPU 型的进程,它被调度上CPU用完了一个时间片之后就会回到下一级队列。如果每次都用完了它的时间片,它就会降级。可能一个CPU 型的进程就慢慢降到了优先级最低的这个队列里。
因此可以看到这个调度算法可以慢慢地区分出来哪些进程是CPU型进程,哪些进程是I/O型进程,很显然多级反馈队列调度算法对I/O型进程更偏好一点,对CPU型进程呢不太有利。但是它也做了一些弥补,比如说优先级高的队列时间片短,而优先级低的队列时间片会很大,所以当低优先级的CPU型进程被调度上CPU之后,它可以运行更长的时间。这里也是一种平衡的结果。
不仅要决定选择哪一个进程执行
还需要决定在哪一个CPU上执行
要考虑进程在多个CPU之间迁移时的开销
高速缓存失效、TLB失效
尽可能使进程总是在同一个CPU上执行
如果每个进程可以调度到所有CPU上,假如进程上次在CPU1上执行,本次被调度到CPU2,则会增加高速缓存失效、TLB失效;如果每个进程尽量调度到指定的CPU上,各种失效就会减少
考虑负载均衡问题
由于Windows操作系统支持内核级线程,所以CPU的调度单位是线程。
Windows的线程调度采用的是基于动态优先级的,抢占式调度,同时结合了时间配额的调整。
基本思想:
引发线程调度的条件:
然后再此基础上又添加了两个条件:
线程的亲和处理机集合:有这样一个处理机的集合,允许线程在这个处理机集合上执行,除了处理机集合之外的其他处理机空闲,这个线程也不能执行。这个集合就是这个线程的亲和处理机集合。
如果这个集合改变了,比如增加了一个新的处理机在这个集合当中,那么就可以引发新的线程调度。
Windows把线程分成了32 个优先级分成了三类:
实时优先级的线程:一旦确定了优先级,就不会再改变了。
可变优先级的线程:它的优先级可以在一定的范围内提升或者是降低。
对于可变优先级的线程,可以区分为 基本优先级 和 当前优先级。
通常系统会安排一个零页线程,即给物理内存清零的线程,把它的级别安排在零集,那么当没有其他进程运行的时候,有一个线程可以做这样的工作。
时间配额不是一个时间长度值而是一个称为配额单位(quantum unit)的一个整数。一个线程用完了自己的时间配额时,如果没有其他相 同优先级的线程,Windows将重新给该线程分配一个新的时间配额,让它继续运行。
时间配额的特殊作用:
假设用户首先启动了一个运行时间很长的电子表格计算程序,然后切换到一个游戏程序(需要复杂图形计算并显示,CPU型)
如果前台的游戏进程提高它的优先级,则后台的电子表格计算进程就几乎得不到CPU时间了
所以增加游戏进程的时间配额,则不会停止执行电子表格计算, 也能给游戏进程的CPU时间多一些。
有一个正在运行的线程,优先级是20。在运行过程中,由于需要等待输入输出的结果,那么这个线程就转到了阻塞态让出了 CPU。 调度程序会去选择一个新的线程上CPU运行。
如果刚才被阻塞的线程被唤醒了,由于它的优先级高,它会去抢占CPU去运行,那么被抢占的这个线程就回到了就绪队列。
当线程被抢占时,它被放回相应优先级的就绪队列的队首
处于实时优先级的线程在被抢占时,时间配额被重置为一个完整的时间配额
处于可变优先级的线程在被抢占时,时间配额不变,重新得到CPU后将运行剩余的时间配额
假设线程A的时间配额用完
为什么一个线程用完了它的时间配额后,优先级会被降低?
那是因为这个线程在此之前,优先级被提升过。
Windows的调度策略
解决方案
下列5种情况,Windows会提升线程的当前优先级:
线程优先级的提升是针对可变优先级范围内(1至15)的线程优先级。
例子:
Wdm.h
或Ntddk.h
中IoCompleteRequest
来指定优先级提升的幅度系统线程平衡集管理器(balance set manager)
每秒钟扫描一次就绪队列,发现是否存在等待时间超过300个时钟中断间隔的线程
如果存在,平衡集管理器将这些线程的优先级提升到15 (最高),并分配给它一个长度为正常值4倍的时间配额
因此当被提升的线程用完它的时间配额后,立即衰 减到它原来的基本优先级
以多道程序设计技术为切点。多道程序设计技术是操作系统最早引入的软件技术。它的基本思想是允许多个程序同时进入内存并运行。主要是为了提高 CPU的利用率,进而提高整个系统的效率。
来看一个例子:
在A图中, 内存里有四个程序,因为只有一个物理的程序计数器,所以这四个程序呢是串形执行的。
有了多道程序设计技术之后,每个程序变换成了一个独立的控制流,占用一个逻辑的程序计数器。这也是操作系统虚拟性的一个体现,把一个物理的程序计数器,给它变换成多个逻辑的程序计数器,实际上每个程序都有自己的程序计数器,由于物理上只有一个程序计数器,所以每个程序真正的上 CPU 就把逻辑程序计数器的内容,推送到物理程序计数器里头。通过这种变换,达到了在内存中同时有多个程序,达到并发执行的效果。
C图表示出在一个时间间隔内,每一个程序 A B C D 都执行过了。由于只有一个物理 CPU ,所以这些程序是轮流在 CPU 上执行。但是从宏观上讲它们都在并发执行。
同时又产生了一个问题:在这样一个计算环境下,多个程序并发执行,如何管理在并发环境下同时执行的这些程序?首先来看下并发环境与并发程序。
并发环境:一段时间间隔内,单处理器上有两个或两个以上的程序同时处于开始运行但尚未结束的状态, 并且次序不是事先确定的。
并发程序:在并发环境中执行的程序。
在一个并发环境下执行的并发程序,怎么样来刻画这样的程序呢?于是进程的定义就应运而生了。
定义:进程是具有独立功能的程序,关于某个数据集合上的一次运行活动,是资源分配的单位,也是CPU调度的单位。又称任务。
特点:
进程是程序的一次执行过程。(一个程序执行了两次,三次, 那就是不同的进程)
进程是运行程序的一个抽象。(它代表了所运行的那个环境,代表了一个 CPU,因此有时候说进程是对 CPU 的一个抽象)
将一个 CPU 把它变换成多个虚拟的 CPU 。(虚拟化技术)
操作系统的资源是以进程为单位来分配的。比如说内存,文件等等。 最重要的一个资源就是地址空间。(操作系统为每一个进程分配了一个独立的地址空间)
操作系统把CPU的控制权,交给了某一个进程,让这个进程上去运行,这称之为一个调度。
在操作系统执行过程中,会有很多的程序向操作系统提出申请来运行,那么操作系统怎么知道这些进程是存在,还是不存在呢? 这里就介绍操作系统为了管理进程所设计的一个非常重要的数据结构,进程控制块 PCB 。
PCB:Process Control Block.又称进程描述符、进程属性。
操作系统为了管理进程而,设计的一个非常重要的数据结构,这就是进程控制块PCB。这个数据结构是专门用于控制和管理进程的,它保存控制和管理进程所需要的所有的信息。主要是记录了进程的各种属性,并且描述出进程的运动变化过程,进程的发展程度。进程控制块PCB这个数据结构是操作系统感知进程存在的一个标志。它们是一一对应的。
进程表:由于操作系统管理了很多的进程,为了便于管理,就把所有进程的每个进程的PCB集中在一起,放在了内存的固定区域,这就形成了进程表。
进程表是所有进程的 PCB 的一个集合。而且就是进程表的大小往往是固定的,确定了在一个操作系统中最多支持多少个进程 。
CPU的现场信息是指当进程不运行的时候,操作系统要把一些重要的信息,硬件执行的状态信息,保存在PCB 里。
寄存器值(通用寄存器、程序计数 器PC、程序状态字PSW、栈指针)
指向该进程页表的指针
SOLARIS是基于 Unix 操作系统,它的进程控制块的名字一般叫Proc结构。每一个Proc结构代表一个PCB。把所有的Proc结构组织成一个链,那么这就是一个进程表。
Proc 结构保存的信息:(重点介绍三个)
第一个是可执行文件 p_exec,通过这样一个记录信息,可以找到这个进程所对应的可执行文件在磁盘上的位置。
第二个是进程的地址空间 p_as,进程地址空间放了很多内容,每一项内容都放在一段里头,通过段来把进程地址空间描述清楚,把这些段按照地址大小的顺序,把它建立成一个 AVL 树,便于以后的查找。
第三个是文件表,通过这张表可以把所有打开的文件都能找到。
运行态(Running)
进程占有CPU,并在CPU上运行。
就绪态(Ready)
进程已经具备运行条件,但由于没有空闲CPU,而暂时不能运行。
等待态(Waiting/Blocked)
也称阻塞态、封锁态、睡眠态
进程因等待某一事件而暂时不能运行。
已完成创建一进程所必要的工作,比如分配了PID、填写了PCB。但由于某些原因,操作系统尚未同意执行该进程。
终止执行后,进程进入该状态。
可完成一些数据统计工作
资源回收
在操作系统当中,如果想进行一些负载调节时,可能会把进程送入这个状态。
比如如果现在系统中进程太多,CPU也忙不过来了,此时操作系统会把一部分进程,让它暂时不能运行,但是它又不是等待某个事件发生,所以就把它弄成一个特殊的状态,叫挂起态。
一旦进程进入了挂起态,操作系统会把它的内存空间呢收回来。把这些进程的相关的内容送到磁盘上保存起来,一旦继续让它运行,我们通常称之为激活。进程的内容再从磁盘上读入内存就可以了。
操作系统在设计进程模型的时候要确定有什么样的状态,确定状态之间的转换,在什么条件下转换 ,通过什么样的操作来促成这种转换。 而且操作系统当中有很多的进程,它们都处于不同的状态。所以需要按不同的状态把它们管理起来,因此,操作系统设计了一个若干个进程队列。
操作系统设计了一个若干个进程队列,为每一个类进程建立一个或者多个队列也是可以的。 每个队列的元素,实际上就是PCB 状态的改变,其实就是某个进程的PCB从一个队列出队,然后在另一个队列里头入队的过程。也就是伴随着状态的改变,进程的PCB从一个队列进入到另外一个队列。
五状态进程模型的队列模型:
进程控制操作主要是完成进程之间的各状态之间的转换,进程控制操作实际上就是具有特定功能的程序。 这个程序执行的时候,由于不允许被中断,所以把它称之为原语。
原语: 所谓原语(有时候又称之为原子操作) 是完成某种特定功能的一段程序,比如说完成创建,或者是完成阻塞,它是一段程序,完成了某种特定功能,但是这个程序在执行过程中,是具有不可分割性,或者是不可中断的,它必须持续地执行,不允许被打断,这就是原语。
实现原语需要操作系统通过屏蔽中断的一些措施来达到这样一个结果。进程控制操作最重要的一个就是进程的创建。
主要完成以下几个工作:
然后要给这个进程分配它所需要的地址空间。 (如果这个地址空间在虚拟存储机制之下,就假设给了它,只是给了一个虚拟地址空间)。
再初始化这个进程控制块,填写相应的内容。
进程的撤销实际上就是结束进程。
结束进程其实主要做两件事情:
把进程所占有的资源回收
资源回收之后最重要的,是要把分配给它的 PCB 收回。
处于运行状态的进程,在其运行过程中期待某一事件发生,如等待键盘输入、等待 磁盘数据传输完成、等待其它进程发送消息, 当被等待的事件未发生时,由进程自己执行阻塞原语,使自己由运行态变为阻塞态。
fork( ):通过复制调用进程来建立新的进程,是 最基本的进程建立过程
exec( ):包括一系列系统调用,它们都是通过用 一段新的程序代码覆盖原来的地址空间,实现进程执行代码的转换
他们都是以系统调用的形式,作为一个接头呈现给用户,由用户程序来调用 。
首先会为子进程分配一个空闲的进程描述符,也就是 PCB。
PCB在 UNIX 一般叫 proc 结构
给子进程分配了一个唯一的标识pid
给子进程分配地址空间
在 UNIX 里 fork 以一次一页的方式把父进程的地址空间内容完全地拷贝给子进程
从父进程那里继承各种共享资源
比如打开的文件,当前工作目录等等
子进程的状态 设置为就绪态,并且把它插入到了就绪队列
做完这项工作之后,fork 就为子进程返回一个值 0
那么也就是说,fork 执行完后,原来一个进程,父进程就一分为二,变成了两个进程,一个父进程,一个子进程。
在父进程得到的返回值是 pid,在子进程里得到的返回值是 0。
问题:以一次一页的方式来复制父进程的地址空间有什么弊端?
父进程把它所有的内容都拷贝给子进程 ,但是子进程不一定需要,而且通常情况下,父进程创建子进程是让子进程做与父进程所不同的工作。所以把发父进程所有内容拷贝给子进程,实际上,子进程也不需要。因此,子进程会接着执行 exec这样一个函数来把父进程拷贝过来的这些地址空间给覆盖掉。因此之前的这种复制工作,实际上就是无用功了。
Linux 使用了写时复制技术 Copy-On-Write,Linux中父进程把地址空间的指针传递给子进程,再把地址空间设置为只读。那么当子进程要往地址空间里写东西的时候,操作系统会为子进程单独再开辟一块空间,把相应的内容放进去 那么这样的话呢,节省了之前复制父进程地址空间的时间,加快了 fork 的实现速度。
代码例子:
1 |
|
流程:
系统进程是操作系统为了管理一些资源而设计的进程,它的主要特点是优先级比较高。相对于用户进程而言,系统进程会优先被调度上CPU 去执行,因为它完成了一些关键的工作。
前台进程实际上就是和用户直接交互的这样一些进程:用户敲键盘、 动鼠标。
后台进程往往是操作系统在启动了以后创建的一些进程,这些进程为用户来进行服务,比如说打印进程。
有一些应用进程也在系统启动的时候被创建了, 比如说防火墙,还有一些电子邮件的接收。这样的一些进程它们在后台工作,然后发生了一些事件,它们来接收这些事件。对于用户来讲,他所打交道的是前台进程。
有一些进程需要用到很多的 CPU 时间。比如说,画面渲染需要大量的计算,因此把它称之为 CPU 密集型进程
有些经常需要输入、 输出、 读盘这样一些操作,这些进程被称之为 I/O 密集型
那么这两类进程的区分也是为了以后调度程序的选择做一些准备 。
UNIX 进程都是在一个家族里,这个家族树有一个根,这个根是一个 init 进程,是个 1 号进程,是所有进程的一个祖宗。在某些情况下,某一个进程它结束了,那么它的子孙进程,其实也必须全部的结束。
那在 Windows 中也是一个进程创建另一个进程,但是创建完之后,这两个进程的关系比较疏远,也就是它们的地位是相同的。
操作系统会给每一个进程都分配了一个地址空间。怎么理解这句话?怎么理解这样一个场景?
以代码为例:
1 | int myval; |
这个程序实际上是从命令行接收了参数,把这个参数赋给一个变量 myval 然后就是循环,来打印这个变量的值,同时把这个变量的位置 打印出来。
现在同时执行两个 myval 程序。 也就是这两个进程在执行,因为用了一个循环,所以这个进程会一直在那运行下去。如果运行这两个进程,分别用参数 7,8 来调用这个程序,输出的结果如下:
当执行 myval 7 的时候,myval 的这个变量值是 7 ,地址是 60104C 。同时 myval 8 也在执行。 发现 myval 的值是 8 ,而myval 的地址呢 也是 60104C。 为何变量 myval 的值是不一样的,地址却是相同的呢?
实际上每个进程有自己相对独立地址空间。 两个进程实际上是两个地址空间。而且它们的地址空间是隔离的。不同的地址空间,它的地址不是实际的物理内存地址。 实际上是一个相对地址,如果支持虚存的系统当中,那么这个地址就是虚拟地址,也可以说是一个相对地址,或者是逻辑地址。
上图是一个进程地址空间的表示图。在这个空间里,操作系统会占一部分内容:
上半部分是操作系统内核的地址空间
下面是用户地址空间
用户地址空间包括了用户执行的过程中所需要的一些代码数据,一些临时变量。 还有在运行过程中如果进行了过程调用函数调用,需要用栈来传递参数,那么主体有这样一些内容。在进程运行过程中还可能调一些共享库, 因此还有一些共享库放在这个位置;如果打开了文件以文件内存映射文件的方式来使用这个文件的话,那么也用到这些空间。这就是进程用户地址空间的内容。
myvalue 7 和 myvalue 8 这两个进程每个都有这么一个地址空间 , 因此myvalue这个变量实际上是在不同的地址空间里的相同的位置。因此,我们看到了虚拟地址虽然是相同的 那么这个相同指的是对于这个地址空间的位置,而不是指的在物理内存的位置。
进程映像指的是进程执行过程中它的全过程的一个静态描述,可以把它看成是在某一瞬间的进程的快照。
包括的内容:地址空间的内容,硬件寄存器的内容,以及与该进程相关的一些内核数据结构和内核栈。
用户相关:进程地址空间(包括代码段、数据段、 堆和栈、共享库……)
寄存器相关:程序计数器、指令寄存器、程序状态 寄存器、栈指针、通用寄存器等的值
内核相关:
将CPU硬件状态从一个进程换到另一个进程的过程称为上下文切换。
进程运行时,其硬件状态保存在CPU上的寄存器中。
寄存器:程序计数器、程序状态寄存器、栈指针、通用寄存器、其他控制寄存器的值。
进程不运行时,这些寄存器的值保存在进程控制块 PCB中;当操作系统要运行一个新的进程时,将 PCB中的相关值送到对应的寄存器中。这个就完成了上下文切换的一个过程。
为什么在进程中再派生线程?具体有三个理由:
以Web服务器为例,其工作方式是:
每次到磁盘上搜索相关的网页,进程就会停在那里,这样性能就比较慢。 怎么样去提高服务器的工作的效率?
通常情况下,是在服务器的内存里头,开辟一个网页缓存 ( Web page Cache ),保存了常用的网页。当Web服务器从客户端接收了网页请求之后会先到网页缓存当中去查找,如果找到就直接把结果返回给客户端,就不用到磁盘上去找。 但是如果没找到就先到磁盘上去搜索相关的网页,得到了之后写入网页缓存,然后再把结果返回给用户。
此时有两种解决方案:
设定一个服务进程,这个服务进程只能是顺序编程 。也就是说,如果它到磁盘上去搜寻网页,那就不能再去接收客户端的请求。因此会造成服务器性能下降。
为什么不能设定多个服务进程?
每个进程有自己独立的地址空间,所以它不能共享信息,所以只能有一个服务进程。
有限状态机的方法实际上是用一个复杂的编程模型来自己模拟一些并发的工作,即进程自己来模拟并发的工作。
比如说接收了一个用户请求之后,如果要到磁盘上搜寻这个网页,那么原本这个进程会被暂停,这个时候就要改造这个搜寻网页的操作,把它改造成一个非阻塞的I/O。
到磁盘上去搜寻网页的同时这个进程还可以继续做与这个网页内容无关的一些工作。 所以叫非阻塞 I/O。
在查询网页的同时,磁盘在工作,而且这个进程就可以回来做别的事情,它可以继续去接收用户的客户端请求。 当新的请求被接收到之后,它继续可能在 Web Cache 里找网页,然后返回去,如果没找到网页呢,继续再去调用磁盘。但是这就出现了一个问题:磁盘的这个结果返回了,究竟是哪一个客户端的请求呢?
所以这个时候,进程要自己把这些信息记录下来,然后磁盘返回了请求之后,就要判断是哪一个客户端的请求,然后返回给对应的客户 端。因此,它的编程模型呢是比较复杂的。
工作方式:
把线程分成两类:
分派线程
分派线程只需要一个,分派线程的主要工作就是监听客户端,客户端只要有请求就把请求读进来,但是它不完成客户端的请求,它把这个请求分派给其它的线程来完成,而完成工作的线程就是工作线程。
工作线程
Web 服务器上有一堆工作线程,它们都是用来完成的是服务客户请求的。分派线程获得了客户端请求之后就把它分给某一个工作线程,工作线程呢还跟前面一样先到Web Cache里去查找网页是否存在,如果存在就返回给客户端,如果不存在就要启动磁盘,到磁盘上去搜寻网页。它到磁盘上搜寻网页,这是一个阻塞的I/O,这时它就会被阻塞,然后等待,但是没有关系,因为还有其它的工作线程。
如果一个进程里头又有多个线程,而这些线程,有的计算,有的去 I/O,当有多个处理器的时候,就可以充分发挥这个优势了。所以当多处理器的情况下,一个进程就可以有很多的任务同时在执行。性能就提高了很多。
进程有两个基本的属性:
进程是资源的拥有者。
进程是CPU的一个调度单位。
但是有了线程之后,线程就继承了进程中的一个属性,也就是线程成为了 CPU 的调度单位。 而进程依然还是管理资源,然后是资源的一个拥有者。
线程实际上是进程中的一个运行实体。
从运行的角度,它是一个运行的实体,它是一个 CPU 的调度单位。 有的时候把线程称之为轻量级进程。 也就是说,在进程当中又增加了多个执行序列, 让这些执行序列可以并发执行,以提高软件的运行效率。
所以强调的是:在进程中增加了多个执行序列,叫线程。
线程是一个运行实体,它有属于线程自己的一些属性:
有标识符ID
同一个进程的不同线程要区分
有状态及状态转换
因为线程是上 CPU 的,所以它有状态。它也有状态的转换,也需要提供一些针对线程的操作。
不运行的时需要保存上下文环境
上下文环境:程序计数器等寄存器(保存在线程的相对的数据结构里)
有自己的栈和栈指针
不同的线程,这些信息也是不一样的。
共享所在进程的地址空间和其他资源
同一个进程的不同线程它们是共享所在进程地址空间,内容和这个进程所拥有的资源的,所以这是非常重要的,也就是线程之间的通信,或者其他的一些操作带来了便利的地方,它们是共享同一个进程的地址空间和有关的资源。
可以创建、撤消另一个线程
当创建进程以后,实际上是只有一个线程,我们称之为一个主线程。 然后由它再创建其他的线程,所以程序开始的时候,我们可以看成是一个单线程的进程在运行。
在操作系统中, 如何来支持线程机制的实现? 通常有三种方式:用户级线程、 核心级线程和混合方式。
由于在线程的概念提出之前,操作系统已经运行了很多年,进程的概念已经用了很长时间。 因此当有一个新的机制提出来的时候, 不同的操作系统对这个机制的支持是不一样的。
用户级线程:在用户空间建了一个线程库, 这个线程库里提供了一系列的针对线程的操作。 这些线程的管理是通过一个 Run-time System 运行时系统来管理的。 它完成的就是这些线程的创建和线程数据结构的一些管理工作。 如下图所示:
有一个Run-time System,它是管理这些线程里的数据结构、 线程表。这是用户级线程的一个实现。
对于内核而言, 线程的实现是在用户空间,所以操作系统内核并不知道线程的存在,也就是说,它的管理还是以进程为单位来管理,它没有感知线程的存在。
从图中可以看到线程的数据结构是由 Run-time System 来管理的。 内核只看到了进程的数据结构, 因此线程的切换,从一个线程换到另外一个线程不需要操作系统内核的干预,也不需要进入内核来做这件事情, 所以速度比较快。
UNIX 内的操作系统通常采用这种方式来支持线程。
UNIX内的操作系统通常采用这种方式来支持线程。 它支持线程的时候是遵循 POSIX 规范。也就是 POSIX 规范当中确定了多线程的这种编程的接口。
那以什么样的方式呈现给用户呢?它对线程库进行了相应的规范。 这个规范就是 PTHREAD 线程库。 这个线程库按照规范要提供若干个函数来支持线程、 创建线程、撤销线程,等待某个线程的结束。
在这堆操作当中,重点介绍一下 yield 函数,这个函数表示这个线程自愿让出 CPU。
我们知道一个进程的若干线程实际上是相互配合来完成一项任务的。 所以这线程之间是可以协商由谁上 CPU,所以一个线程如果占 CPU 时间太长,那么别的线程得不到机会,就需要这个线程
高尚
一点,让出 CPU,它就调用 yield 让出 CPU。 如果它不让出 CPU,其实其它线程是没法上 CPU 的,因为对于线程而言,它感知不到时钟中断,因为整个时钟段是对进程而言的。
优点:
用户级线程可运行在任何操作系统上(只需要实现线程库)
缺点:
内核只将处理器分配给进程,同一进程中的两 个线程不能同时运行于两个处理器上
大多数系统调用是阻塞的,因此,由于内核阻塞进程,故进程中所有线程也被阻塞
改变:
- 把系统调用,阻塞系统调用改成一个非阻塞的
- 用 Jacketing/ wrapper 的这种技术在系统调用之外封装一层。 在调用系统调用之前,先判断一下调用这个系统调用会不会导致线程阻塞,如果导致线程阻塞,那么就赶紧地换其它线程,这样的话,就不会因为某个线程调用了一个阻塞的系统调用使得整个进程被阻塞。
第二类实现线程机制的方法是核心级线程。这个方案就是彻底地改造了操作系统。
内核管理所有的线程。 通过 API 的接口向用户提供一些 API 的函数,由用户可以创建线程。
所以内核既维护了进程的数据结构,也维护了进程里头的各个线程的数据结构。
从下图中可以看到内核里头既管了线程表, 也管了进程表。
实现核心级线程机制的典型的操作系统就是 Windows。
混合模型就是线程的创建是在用户空间用线程库来完成的。 但是内核也要管理线程,也就是说调度是由内核来完成的。
这个采用这种混合模型实现线程机制的是 Solaris 操作系统。
用户空间的线程和内核的这个关系是什么?
用户线程通过了一个多路复用来复用多个内核级线程,也就是核外的用户空间的线程通过一个机制和核内的一个内核线程对应起来。调度内核这个线程上 CPU 其实就是调度这个核外的这个线程上 CPU。这是 Solaris 的一种实现。
并发性:任何进程都可以与其他进程一起向前推进
动态性:进程是正在执行程序的实例
独立性:进程是资源分配的一个独立单位
例如:各进程的地址空间相互独立
交互性:指进程在执行过程中可能与其他进程产生直 接或间接的关系
异步性:每个进程都以其相对独立的、不可预知的速 度向前推进
进程映像:程序 + 数据 + 栈(用户栈、内核栈) + PCB
多线程应用场景
知道什么情况下去应用多线程
线程基本概念、属性
程作为一个进程中运行的一个实体,有哪些是属于它自己的信息?有哪些是共享同一个进程的其他的一些资源?
线程实现机制
操作系统中如何来实现线程?如何来支持线程?
看一个非常重要的概念:可再入程序(可重入程序)。
所谓可再入程序指的是可以被多个进程同时调用的程序,因此对这个程序有限制。 也就是它必须具有的性质是它是纯代码的, 在执行过程中这个代码不会改变。 如果有改变,就需要调用它的进程提供不同的数据区。 这些改变可以放在数据区,因为代码部分是不再改变的。 实际上是大部分进程和线程都必须是可再入程序才能去运行。
]]>也就成功进行了Debugl1app
子项目,论文没有进展,数据结构也没有真的开始,要戒骄戒躁啊。重新规划一下吧。
明天开始晚上停止打球了,不然累了什么都不想干。坚持跳绳21天,每天5k个,看看究竟能瘦多少。
程序的执行
启动程序,执行程序以及程序的结束工作
完成体系结构相关的工作(操作系统与其他软件所不同的地方,与硬件打交道)
完成应用程序所需的共性任务(读盘,申请内存,打印文件)
提供各种基本服务
性能,安全,健壮等问题
用下图来明确操作系统的的地位:
从图中我们可以看出操作系统是在硬件基础上的第一层扩展。
它底下是硬件, 这个硬件都做了什么工作呢? 怎么样去了解底下的硬件呢?这就是本讲的一个内容之一:操作系统运行环境。 和硬件相关的很多的工作实际上是和操作系统的各个功能相结合的,这里介绍最基本的 CPU 状态: 中断与异常机制。 其他的像虚拟页式存储管理的机制, 设备等等的,会在后面跟操作系统功能结合起来讲。
操作系统完成应用程序当中的一些共性的工作, 向应用程序提供一些基本服务,这是本讲另外一方面的内容:操作系统运行机制,这里重点介绍系统调用机制。
处理器由运算器、控制器、一系列的寄存器以及高速缓存构成。
寄存器主要分为两类:
对于用户可见寄存器,高级语言编译器通过优化算法分配这些寄存器,并且使用这些寄存器的主要的目的是为了减少访问内存的次数, 来提高程序的运行效率。
操作系统设计者更加关注的是控制和状态寄存器。 而控制处理器的操作,那么通常只能由操作系统代码来使用。
控制和状态寄存器用于控制处理器的操作,只能在某种特权级别下可以访问、修改。
常见的控制和状态寄存器:
下面来探讨一下操作系统对硬件的需求,其中一个非常重要的需求就是保护。
因为操作系统运行在一个多进程的这样一个环境下,为了支持这些进程的运行。 因此得从操作系统的特征来考虑:并发、 共享。
操作系统为多个程序的执行提供了这样一个并发的环境,而多个进程之间又共享操作系统所管理的各种资源。 那么这样一个并发、 共享的计算环境就要求保护 。
其中保护是的:
这就从操作系统的角度给硬件提出了实现保护、 实现保护控制这个需求。
通常,希望硬件提供一个基本运行机制:CPU 具有一个特权级别,在不同的特权级下可以运行不同的指令集合。 这样把指令分成不同的集合,供操作系统和用户程序分别使用。 通过保护又使得操作系统与用户能够相隔离。 比如说当要访问操作系统空间的时候,那么用户程序是不能够访问的,但是操作系统可以访问用户程序空间, 因此通过一个保护机制达到操作系统与用户的隔离。
有了这样一个需求之后,现代处理器通常把CPU 的状态设计为两种、 三种、 或者是四种。 但是这样在CPU 上时而运行操作系统、时而运行用户程序,CPU 如何知道是运行哪一种状态呢? 这样就有赖于一些寄存器的某些位的设置:通常是在程序状态字寄存器 PSW中专门设置一位或是两位,根据运行程序对资源和指令的权限不同,设置不同的 CPU 状态。
这样不同的程序对资源和指令的使用要求是不同的。
以X86 处理器当中的典型的一个标志寄存器 EFLAGS,EFLAGS 寄存器为例:
其中就有一位IOPL
是 IO 的权限级别。 这个级别用两位来表示四个状态。 除了这个寄存器以外,还有一些 描述符设置了权限级别。 这是硬件提供的各种不同的CPU状态 。
操作系统需要两种 CPU状态:
因为操作系统只需要两个状态,而这两个状态可以指向不同的指令集合。因此把指令的集合也划分成两类:
其实整个指令系统操作系统是都可以使用的,它既可以使用特权指令,也可以使用非特权指令。而用户只能使用这个指令系统当中的一个子集,那么这个子集就是非特权指令。
X86支持4个处理器特权级别。特权环:R0、R1、R2和R3。
有了不同的特权级别,那么就需要让用户程序和操作系统之间能有转换。
用户态——> 内核态:中断/异常/陷入机制
内核态 ——> 用户态:设置程序状态字PSW
实际上用户程序在执行的过程中如果需要操作系统的服务,它就要从用户态能够陷入,进入内核态。而从用户态进入内核态的一个唯一的途径就是中断/异常/陷入机制 。这也是下一个非常重要的主题。
而从内核态返回到用户态则比较简单啊,只是通过设置程序状态字寄存器就可以了。
上面所提到的特殊的指令:陷入指令(访管指令)。它的作用是提供给用户程序的一个接口,用这个接口使用户程序可以向操作系统提出各种服务请求。
之所以叫访管指令, 是因为有的时候内核态也被称为一个supervisor mode管理态。 所以在这种情况下,从用户态进入了管理态,相当于访问管理态,所以叫访管指令。
而这条特殊的指令在不同的计算机系统当中实际上是用不同的指令的。 比如 int 指令,trap 指令,syscall,sysenter/sysexit。 这些指令都是不同的计算机体系结构提供的这种特殊的指令,用于陷入(用于访管)。所以这是关于CPU状态之间的转换需要的不同的条件。
操作系统当中的中断和异常机制就好比是汽车中的发动机,或者是飞机引擎。靠它来驱动操作系统的运行。 有的时候是可以这么说,操作系统是由中断驱动/事件驱动的。有了这个机制,操作系统就可以做很多的事情。
主要作用:
所谓中断与异常,实际上是 CPU 对系统发生的某个事件的一种反应。当这个事件发生的时候,通过对这个事件的处理实际上是改变了一个控制流。我们通常说事件的发生改变了 CPU 的一个控制流。
过程:
特点:
原因:中断的引入实际上是为了支持CPU与外部设备的一个并行操作
早期的计算机系统,如果没有中断机制的话 那么 CPU 要负责对设备的所有工作的管理。但是有了中断机制之后就可以这样来工作了。
比如:CPU 会去启动 输入输出设备的工作。 启动做完了之后设备本身就可以独立自己进行工作了 。而 CPU 这个时候呢可以转去处理一些和这次输入输出没有关系的事情。当设备完成了它的任务之后,它通过通过了中断向 CPU 报告这次输入输出的结果。让 CPU 来决策下面该做什么事情。
原因:表示CPU执行指令时本身出现的问题
如算术溢出、除零、取数时的奇偶错,访存地址时越界或执行了“陷入指令” 等,这时硬件改变了CPU当前的执行流程,转到相应的错误处理程序或异常处理程序或执行系统调用。
中断是外部事件在CPU 之外产生的事件打断了 CPU 。这些事件是正在运行的程序所不期望的。
异常是由正在执行的指令而引发的 。
中断/异常机制实际上是现代计算机系统中的核心机制之一。它的主要工作是硬件和软件相互配合来完成的通过软硬件的配合,来使计算机的能力得到充分地发挥。中断/异常机制的工作原理主要是从硬件与软件两个方面分析。
硬件:捕获中断源发出的各种中断/异常的请求 ,以某一种方式来响应,通过把控制权转交给特定的处理程序来完成这个过程,而这个过程就是中断 。异常的响应,响应中断,响应异常这是硬件完成的工作。
软件:识别中断, 异常类型完成对应的处理,实际上就是处理程序,把它称之为中断/异常处理程序。
中断响应是发现中断、接收中断的过程,这部分由中断硬件部件完成。处理器控制部件中设有中断寄存器,其中保存了各种中断信号。
CPU响应中断过程示意:
中断向量表是个非常重要的软硬件结合的这么一个数据结构。 每一行呢实际上是一个中断向量 。中断向量表是由若干中断向量组成。
每一个中断向量其实就是一个内存单元。它是存放了中断处理程序的这个入口地址,以及这个程序,在运行的时候所需要的一个处理机的状态字。
中断响应示意:
设计操作系统时,为每一类中断/异常事件编好相 应的处理程序,并设置好中断向量表。系统运行时若响应中断,中断硬件部件将CPU控 制权转给中断处理程序。
中断处理程序主要做的工作:
以设备输入输出中断为例:
系统调用是用户在编程时可以调用的操作系统功能。系统调用全称应该是操作系统功能调用,简称系统调用 。
系统调用是操作系统给编程人员提供的唯一接口。 通过系统调用使得 CPU 的状态从用户态陷入了内核态。
- 应用程序可以直接调系统调用。但是通常情况下,应用程序都是通过了 C 函数库或者是 API 的接口来间接地调用系统调用。
- 在操作系统内核当中,提供了很多的内核函数。这些内核函数经过了封装把它提供到了 C 函数库,或者是 API 接口 。所以系统调用对于内核而言,内核函数就是这个系统调用的处理程序,而这些处理程序通过封装在 C 函数库或者 API 接口呢提供给用户来使用。
- 但是 C 函数库里头或者是 API 接口里头还有一些函数不是系统调用,它们就是一些普通的函数在完成一些功能。
- 有一些函数通过系统调用对应到了多个内核函数,也可能是某一个函数通过系统调用对应内核的一个函数。都是不太一样的。
- 内核函数当中 也有一些函数呢是不开放给用户使用的
应用程序大部分情况下是通过调用函数。 函数执行过程中再去变成系统调用来进入内核来完成。
首先利用硬件给我们提供的支持(就是中断异常机制)。通过这个机制实现系统调用服务。
然后选择一条特殊的指令即陷入指令,也称之为访管指令 通过这条指令的执行引发一个异常完成从用户态到内核态的切换工作。看到只有一条指令。也就是说所有的系统调用,都是通过这条指令来进入内核的。
在中断向量表或中断描述符表当中有一行专门用于系统调用。
然后操作系统呢要为每一个系统调用事先确定一个编号(系统调用号),因为要确定是哪一个系统调用。所以要通过编号来区分。而且每个系统调用其实还有不同的参数 ,所以呢我们还要设计相应的参数。这些工作除了操作系统的设计之外,还需要编译器来帮忙,编译器会把这个封装的系统调用把它展开 ,在过程中生成这条特殊的陷入指令,以及这些参数的推送寄存器的这些指令。
每一个系统调用其实都有一段内核函数,或者是一段代码来对应。 找到对应的内核函数就需要设计一张系统调用表,这张表就把系统调用的各项服务的入口地址填在这张表里头,那这张表也是在这个初始化的时候设置好了。
实现用户程序的参数传递给内核常用以下3种实现方法:
当CPU执行到特殊的陷入指令时:
Question:
通过下面程序的执行过程来分析操作系统对此程序的执行有哪些支持。
1 |
|
helloworld
程序.helloworld
程序的相关信息,检查其类型是否是可执行文件; 并通过程序首部信息, 确定代码和数据在可执行文件中的位置并计算出对 应的磁盘块地址.操作系统: 创建一个新的进程,并将 helloworld
可 执行文件映射到该进程结构,表示由该进程执行 helloworld
程序.
操作系统:为 helloworld
程序设置CPU上下文环境, 并跳到程序开始处,准备执行程序 (假设调度程序选中hello程序)
helloworld
程序.helloworld
程序执行 puts
函数[系统调用(操作系统完成这个功能)],在显示器上写一字符串.从用户程序在执行过程中,不断请求操作系统服务的角度来看程序执行的过程:
在CPU上执行一个用户程序,这个用户程序会不时地去请求操作系统的服务,因此在CPU上,时而运行的是用户程序。时而运行操作系统程序。
以helloworld程序为例:
在执行的时候,需要操作系统来创建进程,因此转到操作系统创建进程
进程创建完了以后会从操作系统返回 ,接着执行这个helloworld程序,在出现了缺页异常以后,控制权又转回了操作系统
操作系统分配物理内存给这个用户程序 ,然后又转向用户程序接着执行,然后helloworld程序又要去执行puts函数
此时又转向了操作系统去进行系统调用。
换一个角度看用户程序的执行:
实际上,是操作系统在执行过程中呢,如果选中了一个程序,那么就去通过调度选中这个程序,去执行这个程序
程序执行过程中,会不断地去陷入操作系统, 由操作系统完成一些服务.然后再通过调度再选中程序,接着执行.
从上面的介绍过程,看到在一个程序的执行过程中,操作系统负责了这个程序的启动过程, 负责了这个程序执行的过程, 同时在执行的 过程中,不断地去为用户程序的执行 提供各种各样的支持.
操作系统是计算机系统中的一个系统软件,是一 些程序模块的集合,这些模块相互协作,相互配合,来完成以下这些功能:
它们能以尽量有效、合理的方式组织和管理计算机的软硬件资源。
合理地组织计算机的工作流程,控制程序的执行并向用户提供各种服务功能。
使得用户能够灵活、方便地使用计算机,使整个计算机系统高效率运行。
有效:让系统效率,资源利用率得以提高。
合理:各种软硬件资源的管理是否公平合理。
方便使用:两种角度:用户界面与编程接口。
资源的管理者:有效
向用户提供的各种服务:方便使用
对硬件机器的扩展:扩展能力
自底向上看,OS是资源的管理者。
硬件资源:CPU,内存,设备(I/O设备、磁盘、时钟、网 络卡等)。
软件资源:磁盘上的文件、各类管理信息等。
跟踪记录资源的使用状况(通过数据结构与算法)
如:哪些资源空闲,分配给谁使用,允许使用多长时间等
确定资源分配策略——算法
进程/线程管理(CPU管理)
进程线程状态、控制、同步互斥、通信、调度、……
存储管理
分配/回收、地址转换、存储保护、内存扩充、……
文件管理
文件目录、文件操作、磁盘空间、文件存取控制、……
设备管理
设备驱动、分配回收、缓冲技术、……
用户接口
系统命令、编程接口
在操作系统之上,从用户角度来看:
操作系统为用户提供了一组功能强大、方便易用的命令或系统调用
典型的服务:
进程的创建、执行;文件和目录的操作;I/O设备 的使用;各类统计信息; ……
操作系统在应用程序与硬件之间建立了一个等价的扩展机器(虚拟机),对硬件抽象,提高可移植性;比底层硬件更容易 编程。
并发(concurrency): 指处理多个同时性活动的能力。
由于并发将会引发很多的问题: 活动切换、保护、相互依赖的活动间的同步.
在计算机系统中同时存在多个程序运行,单CPU上
宏观上:这些程序同时在执行。
微观上:任何时刻只有一个程序真正在执行,即这些程序在CPU上是轮流执行的。
并行(parallel):与并发相似,但多指不同程序同时在多 个硬件部件上执行
有趣的例子记忆:
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
共享:操作系统与多个用户的程序共同使用计算机系统中的资源(共享有限的系统资源)。
操作系统要对系统资源进行合理分配和使用,使得这些资源在一个时间段内交替被多个进程所用。
共享分为:
互斥共享(如打印机)
同时共享(如可重入代码、磁盘文件)
正是这些功能,所以又有了复杂的问题: 资源分配难以达到最优化,如何保护资源?
虚拟(Virtual) : 一个物理实体映射为若干个对应的逻辑实体——分时或分空间。虚拟是操作系统管理系统资源的重要手段,可提高资源利用率。
典型的例子:
- CPU——每个进程的”虚处理机”
- 存储器——每个进程都有独立的虚拟地址空间(代码+数据+堆栈)
- 显示设备——多窗口或虚拟终端
随机:操作系统必须随时对以不可预测的次序发生的事件进行响应并处理。
因为随机性带来的例子:
进程的运行速度不可预知:多个进程并发执行,“走走停停”,无法预知每个进程的运行推进的快慢。
带来的问题:难以重现系统在某个时刻的状态(包括重现运行中的错误)。
]]>线性表(List):零个或有限个数据类型相同的数据元素的有限序列。
性质:
- 线性表中的数据元素之间是有顺序的。
- 线性表中的数据元素个数是有限的。
- 线性表中的数据元素的类型必须相同。
</br>
线性表的抽象数据类型定义如下:
1 | ADT 线性表(List) |
</br>
线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。线性表(a1,a2,……,an)的顺序存储示意图如下:
.
在C中用一维数组来实现顺序存储结构:
1 | /* |
</br>
操作步骤:
1 | //1.创建 |
</br>
操作步骤:
1 | //1.判断线性表是否合法 |
</br>
操作步骤:
1 | //1.判断线性表是否合法 |
</br>
操作步骤:
1 | char ret = -1; |
</br>
</br>
链式存储定义:为了表示每个数据元素与其直接后继的元素之间的逻辑关系,每个元素除了存储本身的信息之外,还需要存储指示其直接后继的消息。
链式存储逻辑结构:n个结点链接成一个链式线性表的结构叫做链表,当每个结点中只包含一个指针域时,叫做单链表。
链表的基本概念:
C语言用结构体来定义链表中的结点:
1 | typedef _tag_LinkListNode LinkListNode; |
</br>
操作步骤:
1 | //1.判断线性表是否合法 |
</br>
操作步骤:
1 | //1.判断线性表是否合法 |
</br>
操作步骤:
判断线性表是否合法
判断插入位置是否合法
1 | LinkListNode* ret = NULL; |
</br>
</br>
静态链表:让数组的元素都是由两个数据域组成,data和next。也就是说,数组的每个下标都对应一个data和一个next。数据域data,用来存放数据元素,也就是通常我们要处理的数据;而next相当于单链表中的next指针,存放该元素的后继在数组中的下标,我们把next叫做游标。
**静态链表是在顺序表的基础 静态链表是在顺序表的基础上利用数组实现的单链表。**表头结点中,
data = 3
表示链表长度为3,next = 1
表示下一个结点的位置是1。
在C中用一维数组来实现顺序存储结构:
1 | //节点结构体定义 |
</br>
操作步骤:
1 | //1.判断线性表是否合法 |
</br>
操作步骤:
1 |
</br>
操作步骤:
1 |
</br>
</br>
循环链表的定义:将单链表中最后一个数据元素的next指针指向第一个元素.
</br>
循环链表拥有单链表的所有操作。
</br>
1.数据:是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识别,并输入给计算机处理的符号集合。数据不仅仅包括整型、实型等数值类型,还包括字符及声音、图像、视频等非数值类型。
这里说的数据,其实就是符号,而且这些符号必须具备两个前提:
- 可以输入到计算机中。
- 能被计算机程序处理。
2.数据元素:是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。也被称为记录。
3.数据项:一个数据元素可以由若干个数据项组成。
数据项是数据不可分割的最小单位。在数据结构中,把数据项定义为最小单位,是有助于我们更好地解决问题。
4.数据对象:是性质相同的数据元素的集合,是数据的子集,简称数据。
性质相同,是指数据元素具有相同数量和类型的数据项。
5.数据结构:是相互之间存在一种或多种特定关系的数据元素的集合。
在计算机中,数据元素并不是孤立、杂乱无序的,而是具有内在联系的数据集合。数据元素之间存在的一种或多种特定关系,也就是数据的组织形式。
逻辑结构:是指数据对象中数据元素之间的相互关系。其实这也是我们今后最需要关注的问题。逻辑结构分为以下四种。
物理结构:是指数据的逻辑结构在计算机中的存储形式。
数据是数据元素的集合,那么根据物理结构的定义,实际上就是如何把数据元素存储到计算机的存储器中。存储器主要是针对内存而言的,像硬盘、软盘、光盘等外部存储器的数据组织通常用文件结构来描述。
数据元素的存储结构形式有两种:顺序存储和链式存储。
顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。如下图所示。
链式存储结构:是把数据元素存放在任意的存储单元里,这组存储单元可以是连续的,也可以是不连续的。数据元素的存储关系并不能反映其逻辑关系,因此需要用一个指针存放数据元素的地址,这样通过地址就可以找到相关联数据元素的位置。如下图所示。
数据类型:是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。
数据类型是按照值的不同进行划分的。在高级语言中,每个变量、常量和表达式都有各自的取值范围。类型就用来说明变量或表达式的取值范围和所能进行的操作。
抽象是指抽取出事物具有的普遍性的本质。它是抽出问题的特征而忽略非本质的细节,是对具体事物的一个概括。抽象是一种思考问题的方式,它隐藏了繁杂的细节,只保留实现目标所必需的信息。
抽象数据类型(Abstract Data Type,ADT):是指一个数学模型及定义在该模型上的一组操作。抽象数据类型的定义仅取决于它的一组逻辑特性,而与其在计算机内部如何表示和实现无关。
</br>