进程系列
什么是进程?OS中为什么要引入进程?
- 进程是对正在运行的程序过程的抽象,是一种数据结构,目的在于清晰地刻画动态系统的内在规律,有效管理和调度进入计算机系统主存储器运行的程序。
- 进程是程序的一次执行。
- 进程是一个程序及其数据结构在处理机上顺序执行时所发生的活动。
- 进程是具有独立功能的程序在一个数据集合上运行的过程,它是系统进行资源分配和调度的一个独立单位。
- 程序是指令运行的集合体,CPU是按照指令顺序不断的运行。CPU的运行是高效的,而程序进行IO是低效的,这就会导致程序在进行IO的时候出现CPU空闲在等程序的情况,降低了实际效率。这时候不如让这个程序挂起,切换到下一个需要CPU执行的程序。为了方便对程序切换的管理,引入了进程的概念,一个进程对应了一个运行的程序,它能申请到资源并且独立的给程序提供资源,提高了系统资源利用率和系统的处理能力。
进程的组成
为了使参与并发执行的每个程序(含数据)都能独立的运行,在操作系统中必须为之配置一个专门的数据结构,称为进程控制块(PCB)。系统利用PCB来描述进程的基本情况和活动过程,进而控制和管理进程。这样,**由程序段、相关数据段和PCB三部分就构成了进程实体。**创建进程实质上就是创建进程实体中的PCB,撤销进程实质上就是撤销进程中的PCB。
进程控制块PCB
PCB:为了便于系统的描述和管理进程的运行。PCB作为进程实体的一部分,记录了操作系统所需的,用于描述进程的当前情况以及管理进程运行的全部信息,是操作系统中最重要的记录型数据结构。
PCB的作用是使一个在多道程序环境下不能独立运行的程序(含数据)成为一个能独立运行的基本单位,一个能与其他进程并发执行的过程。
- 作为独立运行基本单位的标志;
- 能实现间断性运行方式;
- 提供进程管理、调度所需要的信息;
- 实现与其他进程的同步与通信
PCB的组成:
- 进程标识符:外部标识符(方便用户对进程的访问);内部标识符(方便系统对进程的调用)
- 处理机状态:通用寄存器、指令计数器、程序状态字PSW、用户栈指针
- 进程调度信息:进程状态、进程优先级、进程调度所需的其他信息、事件
- 进程控制信息:程序和数据的地址、进程同步和通信机制、资源清单
PCB的组织方式:
- 线性方式:所有的PCB都组织在一张线性表中,将该表的首地址存放在内存的一个专用区域中。该方式实现简单、开销小,但每次查找都需要扫描整张表,适合进程数目不多的场景。
- 链接方式:把具有相同状态进程的PCB分别通过PCB中的链接字链接成一个队列。这样可以形成就绪队列、若干个阻塞队列和空白队列等。
- 就绪队列:按照进程的优先级将PCB从高到低进行排列。
- 阻塞队列:根据其阻塞原因进行排列。
- 索引方式:系统根据所有进程状态的不用,建立几张索引表,例如就绪索引表、阻塞索引表等,并把各索引表在内存的首地址记录在内存的一些专用单元中。在每个索引表的表目中,记录具有相应状态的某个PCB在PCB表中的位置。
进程的基本特征
- 基本特征:并发性和动态性
- 并发性:并发性是进程的一个重要特征,同时也是OS的重要特征。引入进程的目的正是为了使其程序能和其它建立了进程的程序并发执行,而程序本身是不能并发执行的。
- 动态性:表现为由创建而产生,由调度而执行,因得不到资源而暂停执行,以及由撤销而消亡,因而进程有一定的生命期;而程序只是一组有序指令的集合,是静态实体。
- 其他特征:独立性、异步性、结构性
- 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的基本单位。
- 异步性:由于进程间的相互制约,使进程具有执行的间断性。即进程按各自独立的、不可预知的速度向前推进。
- 结构性:进程是由程序、数据和进程控制块三部分组成。
进程与程序之间的区别与联系?
- 进程可以看做是程序的一部分。一个程序可以对应多个进程,但一个进程只能对应一个程序。类似与演出和剧本,一个剧本可以由不同的演员去演出,但是这些演员都是照着这个剧本去演的。
- 进程具有一定的生命周期,是动态的;而程序则是指令的集合,是静态的。
- 进程具有并发性,而程序没有;进程是竞争计算机资源的基本单位,而程序不是。
进程的状态及转换
进程状态的出现,使得OS对于进程的管理更加便捷。状态之间的不断转换说明了进程是动态的。
- 就绪:指进程已经处于准备好运行的状态,即已经分配到所需要的系统资源,只要获得CPU时间片就可以运行。如果系统中有许多处于就绪状态的进程,通常将他们按照一定的策略(如优先级策略)排成一个队列,称该队列为就绪队列。
- 运行:指进程获取了CPU时间片正在执行,在单处理机系统中,最多只有一个进程处于该状态。
- 阻塞:指正在执行的进程,在执行过程中发生了某事件(IO请求,申请缓存失败,等待接收数据等)而导致暂时无法执行。此时需要把该进程的处理机释放掉,并选取其他就绪的进程执行。处于阻塞状态的进程都在阻塞队列中。
- 创建:
- 由进程去申请一个空白的进程控制块,并向PCB中填写用于控制和管理进程的信息。
- 为该进程分配运行时必要的资源
- 把该进程转为就绪状态并插入到就绪队列中
- 终止:当一个进程到达了自然结束点,或者是出现了无法克服的错误,或者被操作系统终结,就进入了终止状态。首先等待OS进行善后处理(停止执行,终止子进程、归还资源等),然后将其PCB清零并将该空间归还给系统。
进程的挂起与激活
**当挂起操作作用于某个进程时,意味着此时该进程处于停止状态。如果进程正在执行,它将暂停执行;若原本处于就绪状态,则该进程暂时不接受调度。**与挂机操作对应的就是激活操作。
为什么要挂起?
- **终端用户的需要:**当终端用户在自己的程序运行期间发现有问题,希望暂停自己的程序的运行,让其停下来,以便用户研究其执行的情况或者对程序进行修改。
- **父进程的请求:**有时父进程希望挂起自己的某个子进程,以便考察和修改子进程,或者协调各个子进程之间的活动。
- **负荷调节的需要:**当实时系统中的工作负荷较重,已经可能影响到对实时任务的控制时,可由系统把一些不重要的进程挂起,以保证系统能正常运行。
- **操作系统的需要:**操作系统有时希望挂起来某些进程,以便检查运行中的资源使用情况或者进行记账。
引入挂起和激活操作后,可能会出现以下状态转换:
- 活动就绪-》静止就绪:当进程处于未被挂起的就绪状态时,称此为活动就绪状态,此时进程可以接受调度。当挂起该进程时,该进程处于静止就绪状态,不再接受调度。
- 活动阻塞-》静止阻塞:当进程处于未被挂起的阻塞状态时,称此为活动阻塞状态。当挂起该进程时该进程处于静止阻塞状态。处于该状态的进程在出现其所期待的事件后,从静止阻塞变为静止就绪。
- 静止就绪-》活动就绪:给某个静止就绪的进程激活操作。
- 静止阻塞-》活动阻塞:给某个静止阻塞的进程激活操作。
进程的创建与终止过程
在系统中每当出现了创建新进程的请求后,OS便调用进程创建原语Create按照下述步骤创建一个新进程:
- 申请空白PCB:为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白PCB;
- 为新进程分配其运行所需的资源,包括各种物理和逻辑资源,如内存、文件、IO设备和CPU时间等;
- 初始化进程控制块PCB;
- 如果进程就绪队列能够接纳新进程,则将新进程插入就绪队列。
如果系统中发生了要求终止进程的某事件,OS便调用进程终止原语,按照下述过程去终止指定进程:
- 读取PCB状态:根据被终止进程的标识符,从PCB集合中检索出该进程的PCB,从中读出该进程的状态;
- 终止执行:若被终止进程正在处于执行状态,应该立刻终止该进程的执行,并设置调度标志为true,用于指示该进程终止后应该重新调度;
- 终止子孙进程:若该进程还有子孙进程,还应该将其子孙进程都终止,防止其子孙进程成为孤儿进程;
- 归还资源:将被终止进程所拥有的全部资源归还给父进程或者系统,防止其子孙进程称为僵尸进程;
- 移除PCB:将被终止进程PCB所在的队列中移除,等待其他程序来收集信息。
进程的切换
进程的切换主要分为两种,主动放弃处理器和被动放弃处理器。并且进程的切换一定发生在中断、异常或者系统调用处理过程中。
- 主动:
- 线程正常结束,主动放弃。
- 线程在执行时发生了异常。
- 被动:
- 进程在执行时有个更紧急的任务(IO操作)。
- 遇到有更高优先级的进程。
- 进程还没结束但时间片用完。
进程切换的步骤:
- 保存处理机上下文,包括程序计数器和其他寄存器。
- 更新PCB信息并把进程的PCB移入相应的队列,如就绪、阻塞等队列。
- 选择另一个进程执行,并更新其PCB。
- 更新内存管理的数据结构。
- 恢复处理机上下文。
并不是所有的中断/异常都会引起进程的切换。有一些中断/异常不会引起进程状态的切换,只是在处理完成后把控制权交给被中断的进程
- (中断/异常等触发)正向模式切换并压入PSW/PC
- 保存被中断进程的线程信息
- 处理具体中断、异常
- 恢复被中断进程的现场信息
- (中断返回指令触发)逆向模式转换并弹出PSW/PC
进程同步的几种方式
临界区、硬件同步机制、信号量、管程机制
硬件同步机制:一些特殊的硬件指令,来解决临界区的访问问题。本质就是加锁:锁开进入,锁关等待。而不是像临界区一样每次都检查是否正在被访问。
管程:代表共享资源的数据结构,以及由对该共享数据结构实施操作的一组过程所组成的资源管理程序。这两个共同构成了一个操作系统的资源管理模块,就是管程。
线程同步的四种方式
临界区
通过对多线程的串行化来访问公共资源或者一段代码,速度快,适合控制数据访问。
优点:保证在某一时刻只有一个线程能访问数据的简便方法。
缺点:只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。
互斥量
为了协调共同对一个共享资源的单独访问而设计的。和临界区很相似,但是比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。
优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享(跨进程同步)。
缺点:创建互斥量所需要的资源较多,如果不是为了实现跨线程同步完全没有必要。
信号量
为了控制一个具有有限数量用户资源而设计的。它允许多个线程在同一时刻访问同一资源,但是需要限制在用一时刻访问此资源的最大线程数目。
缺点:信号量机制必须有公共内存,不能用于分布式操作系统;对信号量的操作很分散,而且难以控制,读写和维护都很困难;核心操作PV在用户程序代码中,出错了不易发现和纠正。
事件
用来通知线程有一些事件已经发生,从而启动后续任务的开始。
优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同线程中的线程同步操作。
信号量与互斥量机制
四种信号量
-
整型信号量:定义为一个用于表示资源数据的整型量S,它与一般整型量不同,除了初始化之外,仅能通过两个标准的原子操作(waitS和signalS)来访问。waitS就是申请资源,signalS就是释放资源。
wait (S) {
while (S <= 0);
S--;
}
signal (S) {
S++
}
-
记录型信号量:整型信号量机制中,只要信号量S<=0就会不断的进行测试,因此该机制并未遵循让权等待的准则,而是使该进程处于忙等状态。而记录型信号量则是一种不存在忙等现象的进程同步机制:当S<=0时表示该类资源已经分配完毕,因此进程应该block进行自我阻塞,放弃处理机。
-
AND型信号量:将进程整个运行过程中所需的所有资源一次性全部分配给进程,待进程使用完再一起释放,只要有一个资源未能分配给进程,其他所有可能为之分配的资源也不分配给它。这样有效地避免了互相请求资源而导致的死锁的发生。
-
信号量集:前面的信号量机制中,PV操作(申请资源释放资源)只能对信号量施加加一或者减一的操作,意味着每次只能对某类临界资源进行一个单位的申请或者释放。当一次需要N个单位时,就得进行N次waitS操作,十分低效。此外在一些情况下,为了保证系统的安全性,当进程申请某类临界资源时,在每次分配之前,都必须测试资源的数量,判断是否大于可分配的下限值来决定是否予以分配。
- Swait(S, d, d):此时在信号量集中只有一个信号量S,但是允许它每次申请d个资源,当现有资源数小于d时不予分配。
- Swait(S, 1, 1):此时的信号量集已经变为一般的记录型信号量(S>1)或互斥信号量(S=1)
- Swait(S, 1, 0):一种特殊且有用的信号量操作,当S>=1时,允许多个进程进入某特定区;当S变为0后,将组织任何进程进入特定区。
互斥量
为了使多个进程能够互斥的访问某临界资源,只需要为该资源设置一互斥信号量mutex,并设置初始值为1,然后将各进程访问该资源的临界区至于wait(mutex)和singnal(mutex)操作之间即可。每个欲访问该临界资源的进程在进入临界区之前,都要先对mutex执行wait操作进行获取,若该资源此刻未被访问,本次wait成功,否则失败。
信号量 vs 互斥量
- 互斥量用于线程的互斥,对临界资源的保护;而信号量用于线程的同步:
- 互斥:指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排他性。但互斥无法限制访问者对资源的访问顺序。
- 同步:指在互斥的基础上,通过其他机制实现访问者对资源的有序访问。大多数情况下同步已经实现了互斥。
- 互斥量只能为01;而信号量可以为非负整数。
- 一个互斥量只能用于同一个资源的互斥访问,它不能实现多个资源的多进程互斥问题,该资源要么是1已经被访问,要么是0还未访问;而信号量可以实现多个同类资源的多进程互斥和同步。
- 互斥量的加锁和解锁必须由同一个线程分别对应使用;信号量可以由一个线程释放,另一个线程得到。
进程之间通信的几种方式
- 管道以及命名管道:管道可用于具有亲缘关系的父子进程间的通信,命名管道除了具有管道所具有的的功能外,它还允许无亲缘关系进程间的通信。
- 普通管道pipe:通常有两种限制:一种是单工用于单向传输;二是只能在父子或者兄弟进程间使用。
- 流管道s_pipe:去除了上面的第一种限制,为半双工,可以双向传输。
- 命名管道:name_pipe:去除了第二种限制,可以在许多并不相关的进程之间进行通讯。
- 信号:信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。
- 消息队列:消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点。具有写权限的进程可以按照一定的规则向消息队列中添加新信息;对消息队列有读权限的进程则可以从消息队列中读取信息。
- 共享内存:这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据的更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。
- 信号量:主要是作为进程之间及同一种进程的不同线程之间的同步和互斥手段。其可以很好的解决共享内存之间多进程之间的安全问题。
- 套接字:可用于网络中不同机器之间的进程间通信。
CPU(处理机)的调度
高级调度:将后备队列的作业放入就绪队列
高级调度又称长进程调度或者作业调度,它的调度对象是作业,主要功能是根据某种算法,决定将外存上处于后备队列中的哪几个作业调入内存,为它们创建进程、分配必要的资源,并将它们放入就绪队列。高级调度主要用于多道批处理系统中,而在分时和实时系统中不设置高级调度。
低级调度:决定哪一个进程获得处理机
低级调度又称为进程调度或者短程调度,调度的对象是进程(或者内核级线程)。主要功能是根据某种算法,决定就绪队列中的哪个进程应该获取处理机,并由分派程序将处理机分配给被选中的进程。进程调度是最基本的一种调度,在多道批处理、分时和实时三种类型的OS中,都必须配备这种调度
中级调度:将进程挂起或者让进程处于就绪队列
中级调度又称为内存调度,目的是提高内存利用率和系统吞吐量。为此应该把那些暂时不能运行的进程调至外存等待,此时进程的状态称为就绪驻外存状态(挂起状态)。当它们具备运行条件且内存又稍有空间时,由中级调度来决定,把外存上的那些已具备运行条件的就绪进程再重新调入内存,并修改其状态为就绪状态。
进程的调度算法
调度算法是指根据系统的资源分配策略所规定的资源分配算法。即进程切换时的算法。
- 批处理系统:没有太多的用户操作系统,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交和周转时间)
- 先来先去服务(FistComeFirstServerd)
- 最简单的调度算法,也称为先进先出或严格排队方案。当每个进程就绪后,它加入就绪队列。当前正在运行的进程停止执行时,选择在就绪队列中存在时间最长的进程执行。该算法即可用于作业调度,也可以用于进程调度。
- 先来先去服务比较适合于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长的时间,造成了短作业等待时间过长。
- 短作业优先(ShortestJobFirst)
- 非抢占策略,其原则是按照估计运行时间最短的顺序进行调度。
- 问题:长作业有可能会饿死,处于一直等待短作业执行完毕的状态。如果一直有短作业到来,那么长作业永远得不到调度。
- 最短剩余时间优先(ShortestRemainingTimeNext)
- 短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。当一个新作业到达时,其整个运行时间与当前作业的剩余时间做比较。如果新的进程需要的时间更少,则挂起当前作业,运行新作业。否则新的作业等待。
- 交互式系统:交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应。
- 时间片轮换调度(Round Robin)
- 将所有就绪进程按照FCFS的原则排成一个队列,每次调度时,把CPU时间分配给队首进程,该进程可以执行一个小时间片。当时间片用完时,有计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时把CPU时间分配给队首的进程,如此往复。
- 该算法的效率和时间片的大小有很大关系:因为进程切换都要保存进程的信息并且载入新进程的信息。如果时间片太小,会导致进程切换的太频繁,在进程切换上就会花很多时间;而时间片太长就不能保证实时性。
- 多级反馈队列
- 假设某个进程需要执行100个时间片,如果采用了时间片轮换调度算法,那么需要交换100次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片的大小都不同,例如1248。进程在第一个队列没有执行完,就会被移动到下一个队列,这种方式下,之前的进程只需要交换7次。
- 每个队列的优先级也不同,时间片越小的队列优先级越高。
- 优先级调度
- 为每个进程分配一个优先级,按照优先级进行调度。为了防止低优先级的进程永远得不到调度,可以随着时间的推移增加等待进程的优先级。
- 高响应比优先级调度
- 既考虑了作业的等待时间,又考虑作业运行时间的调度算法。这样做既照顾了短作业,又不至于使长作业的等待时间过长,从而改善了处理机的性能。
- 我们为每一个作业算出其的响应比 = (等待时间+要求服务时间)/要求服务时间。根据时间比去进行一个动态的分配。缺点就是每次进行调度之前,都需要先做响应比的计算,增加系统开销。
- 实时系统:要求一个请求在一个确定的时间内得到响应。分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
僵尸进程孤儿进程
孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程就称为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。
僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或者waitpid获取子进程的状态信息,那么子进程的进程描述符就仍然保存在系统中。这种进程称为僵尸进程。
- 产生原因:每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然为其保留一定的信息,例如进程号、退出状态、运行时间等,这些信息直到父进程通过wait/waitpid来取时才释放。
- 这就会导致问题:如果父进程不调用wait/waitpid,保留的那些信息就不会被释放,其进程号就会一直被占用。系统所能使用的进程号是有限的,如果大量产生僵尸进程,将会因为没有可用的进程号而导致系统不能产生新的进程。
- 查看僵尸进程:利用命令ps,标记为Z的就是僵尸进程
- 清除僵尸进程:改写父进程,在子进程死后为他收尸,具体做法就是接管SIGCHLD信号。子进程死后,会发送SIGCHLD信号给父进程,父进程收到此信号后,执行watipid()函数为其收尸。
进程与线程之间的关系与区别
关系:
- 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少拥有一个线程。
- 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
- 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
- 不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
- 处理机是分给线程的,正在在处理机上运行的是线程。
- 线程是指进程内的一个执行单元,也是进程内的可调度实体。
区别:
- 调度:进程是资源分配的基本单位;线程是独立调度的基本单位。
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
- 系统开销:在创建或者撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的开销大于创建或者撤销线程时的开销。
- 进程有独立的地址空间,进程奔溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时耗费的资源较大,效率要差些。
- 进程在执行过程中,每个独立的线程有一个程序运行的入口,顺序执行序列和程序的出口,但是线程不能够独立执行,必须依存于在应用程序中,有应用程序提供多个线程执行控制。从逻辑角度看,多线程的意义是在一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。
协程
实现协作式多任务,可以在程序执行内部中断转而执行其他协程。协程是比线程更小的一种执行单元,你可以认为是轻量级的线程,之所以说轻,其中一方面的原因是协程所持有的栈比线程要小很多。协程不是被操作系统内核所管理,而是完全由程序来控制(用户态执行)。
用户态和内核态
由于需要限制不同的程序之间的访问能力,防止其获取别的程序的内存数据,或者获取外围设备的数据并发送到网络,所以操作系统需要两种CPU状态:
- 内核态(Kernel Mode):运行操作系统程序,CPU可以访问内存的所有数据,包括外围设备。
- 用户态(User Mode):运行用户程序,只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU资源可以被其他程序获取。
指令划分:
- 特权指令:只能由操作系统使用、用户程序不能使用的指令,例如:启动IO、内存清零、修改程序状态字、设置时钟、允许/禁止终端、停机
- 非特权指令:用户程序可以使用的指令,例如:控制转移、算数运算、取数指令、访管指令(使用户程序从用户态陷入内核态)。
用户态切换到内核态的三种方式:
-
系统调用:用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统的服务程序完成工作,比如fork()实际上就是执行了一个创建新进程的系统调用。而系统调用的机制核心还是使用了操作系统为用户特别开放的一个中断来实现(陷入指令、又名访管指令,指当运行的用户进程或系统实用进程欲请求操作系统内核为其服务时,可以安排执行一条陷入指令引起一次特殊的异常)。
-
中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一跳即将要执行的指令转而去执行与中断信号对应的处理程序,如果先前执行的指令是用户态下的程序,那么这个转换的过程自然也就发生了由用户态到内核态的切换。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等。中断不一定会引起用户态到内核态,如果中断前的请求操作是内核态的,中断就不会引起改变了。
-
异常:当CPU在执行运行在用户态下的程序时,发生了某些实现不可知的异常,这时会触发由当前运行进程切换到处理此异常的内核相关程序中,也就转到了内核态,例如缺页异常。
内核态到用户态可以用设置程序状态字PSW来实现。
用户态和内核态的区别:
- 内核态与用户态是操作系统的两种运行级别。当程序运行在3级特权级上时,就可以称之为运行在用户态;当程序运行在0级特权级上时,就可以称之为运行在内核态。
- 运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权利和能力完成的工作时就会切换到内核态。
- 处于用户态执行时,进程所能访问的内存空间和对象受到限制,其所处于占有的处理机是可被抢占的;处于内核态执行中的进程,则能访问所有的内存空间和对象,且所占有的处理机是不允许被抢占的。
内存管理的方式
内存管理方式有三种方式:
- 分页存储管理:将用户程序的任一页面放入任一物理块中,实现了离散分配。
- 页面:内存划分成多少个小单元,每个单元大小为K,称为物理块。作业也按照K单位大小划分成片,称为页面。
- 物理划分块的大小等于逻辑划分块的大小
- 页面的大小要适中,太大容易导致最后一页内碎片太大;太小页面碎片总空间虽然小,提高了空间利用率,但是每个作业的页面数量较多,页表过长,反而增加了空间的使用。
- 页表:为了找到被离散分配到内存中的作业,记录每个作业各页映射到哪个物理块,形成的页面映射表,称为页表。
- 每个作业都有自己的页表。
- 页号到物理块号的地址映射要找到作业A,关键是找到页表(页表地址保存在进程管理块PCB中,页表保存在内存),根据页表来找物理块。
- 分段存储管理
- 作业的地址空间被划分为若干段,每个段定义了一组逻辑信息:主程序段MAIN、子程序段X、数据段D以及栈段S等。通常由一个段号来代替段名,每个段都从0开始编址,并采用了一段连续的地址空间。段的长度由相应的逻辑信息组的长度决定,因此各段的长度并不相等,整个作业的地址空间由于被分为多个段,所以呈现出二维的特性:每个段既包含了一部分地址空间,又标识了逻辑关系(逻辑地址由段号和段内地址组成)。
- 段表:类似页表,记录了段在内存中的起始地址和段的长度。
- 地址结构为段号+段内地址。
- 段页式存储管理
- 程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。
分页与分段的区别
- 需求:分页是出于系统管理的需要,是一种信息的物理划分单位;分段是出于用户应用的需要,是一种逻辑单位,通常包含一组意义相对完整的信息。
- 大小和空间利用率:页的大小是系统固定的;段的大小通常不固定。分段没有内碎片,但连续存放段会产生外碎片,可以通过内存紧缩来消除;相对而言分页空间利用率更高。
- 地址维度:分页是一维的,各个模块在链接时必须组织成同一个地址空间;而分段是二维的,各个模块在链接时可以每个段组成一个地址空间。
- 通常段比页大,因而段表比页表短,可以缩短查找时间,提高访问速度。分段模式下,还可针对不同类型采取不同的保护,按段为单位进行共享。
分段系统的优点:
- **易于实现共享 :**在分段系统中,实现共享十分容易,只需在每个进程的段表中为共享程序设置一个段表项。并且分段的空间管理更简单。
- **易于实现保护:**代码的保护和其逻辑意义有关,分页的机械式划分不容易实现。
虚拟内存的实现
虚拟存储器:指具有请求调入功能和置换功能,能从逻辑上对内存容量加以扩充的一种存储器系统。
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存(分硬盘的空间)。
基本思想
程序在运行时,如果它所访问的页(段)已调入内存,便可以继续执行下去;但如果程序所要访问的页(段)尚未调入内存(称为缺页或缺段),便发出缺页(段)中断请求,此时OS将利用请求调页(段)功能将它们调入内存,以使程序能继续执行下去。如果此时内存已满,无法再装入新的页(段),OS还须再利用页(段)的置换功能,将内存中暂时不用的页(段)调用至盘上,腾出足够的内存空间后,再将要访问的页(段)调入内存,使程序能继续执行下去。这样使得一个较大的用户程序在较小的内存空间中运行,也可以在内存中同时装入更多的进程,使它们并发执行。
虚拟存储器的特征
- 多次性:一个作业中的程序和数据无须在作业运行时一次性全部装入内存,而是允许被分成多次调入内存运行。即**只需要将当前要运行的那部分程序和数据装入内存即可开始运行,以后每当要运行到尚未调入的那部分程序时,再将它调入。**正是多次性才使得它具有从逻辑上扩大内存的功能。
- 对换性:一个作业中的程序和数据无须在作业运行时一直常驻内存,而是允许在作业的运行过程中进行换进和换出。即允许将那些暂不使用的代码和数据从内存调至外存的对换区(换出),待以后需要时再将它们从外存中调入内存(换进),甚至还允许将暂时不运行的进程调至外存,待它们又具备运行条件时再调入内存。
- 虚拟性:能够从逻辑上扩充内存容量,使用户所看到的内存容量远大于实际内存容量。这样就可以在小的内存中运行大的作业,或者能提高多道程序度。
页面置换算法
在程序运行过程中,如果要访问的页面不在内存中,就发生**缺页中断(page fault)**而将该页调入内存中。如果内存已经无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。页面置换算法和缓存淘汰策略类似,可以将内存看成是磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。而用来选择淘汰哪一页的规则就叫做页面置换算法。
页面置换算法主要目标就是使页面置换频率最低(缺页率最低)
- 最佳置换算法OPT:是理想的页面置换算法,发生缺页时,有些页面在内存中,其中有一页将很快被访问(包含紧跟着的下一条指令的那页),而其他的页面则可能要到10、100或者1000条指令后才会被访问,每个页面都可以在该页面首次被访问前所需要执行的指令数进行标记,标记最大的页应该被置换。这个算法唯一的问题是无法实现,因为在发生缺页时OS无法知道各个页面下一次是在什么时候被访问到。该算法可以用于对可实现算法的性能进行衡量比较。
- 先进先出置换算法FIFO:总是选择在主存中停留最长的的一页进行置换,先进入内存的页先退出内存。理由是:最早调入内存的页,其不再被使用的可能性比刚调入内存的可能性大。建立一个FIFO队列收容所有在内存中的页。被置换页面总是在队列头上进行,当一个页面被放入内存时,就把它插在队尾上。然而这种算法只在按线性顺序访问地址空间时才是理想的,否则效率不高,因为那些经常被访问的页往往在内存中也停留的最久。
- 最近最久未使用算法LRU:当必须置换一个页面时,LRU算法选择过去一段时间里最久未使用的页面进行置换。
- 计数器:让每个页表对应一个时间字段,并给CPU增加一个逻辑时钟或者计数器。每次存储访问,该时钟都加1。每当访问一个页面时,CPU的计数器就被复制到相应页表项的使用时间字段中。这样我们就可以保留着每个页面最后访问的时间。置换页面的时候选择该时间值最小的页面。这样做的问题是:不仅要查询表,而且当页表改变时(CPU调度)要维护每个页表中的时间,还要考虑到计数器溢出的问题。
- 栈:用一个栈保留页号,每当访问一个页面时,就把它从栈中取出放在栈顶上。这样一来,栈底总是放当前使用最少的页,可以直接得到。这样做的问题是:由于要从栈的中间移走一项,所以该栈需要具有头尾指针的双向链表连接。在最坏的情况下,移走一页并把它放在栈顶上需要改动6个指针,每次修改都有开销。
- 最近未使用算法NRU:在存储分块表的每一表项中增加一个引用位,操作系统定期将它们置为0。当某一页被访问时,由硬件将该位置置为1。过一段时间后,通过检查这些位可以确定哪些页使用过,哪些页自上次置0之后还没有使用过。就可以把该位是0的页淘汰出去。
- 改进:在NRU算法的基础上添加一个修改位,替换时根据访问位和修改位综合判断。优先替换访问位和修改位都是0的页面,其次是访问位为0修改位为1的页面。
- 最少使用算法LFU:给内存中的每个页面设置一个移位寄存器,用来记录该页面被访问的频率。该算法选择在之前时期使用最少的页面作为淘汰页。
IO
一次IO的两个步骤
当进程发起系统调用(进程想要获取磁盘的数据,而能取数据的只有内核)时,这个系统调用就进入内核模式,然后开始IO操作:
- 磁盘把数据装载到内核的内存空间
- 内核的内存空间的数据copy到用户的内存空间中(发生IO的地方)
系统调用的步骤
- 进程向内核发起一个系统调用
- 内核接收到系统调用,知道是对文件的请求,于是告诉磁盘,把文件读取出来
- 磁盘接收到来自内核的命令后,把文件载入到内核的内存空间里面
- 内核的内存空间接收到数据之后,把数据copy到用户进程的内存空间(IO发生的地方)
- 进程内存空间得到数据后,给内核发送通知
- 内核把接收到的通知回复给进程,此过程为唤醒进程,然后进程得到数据,进行下一步操作
IO发生的地方才会出现阻塞或者非阻塞
阻塞IO
进程发起IO调用,并且只能等待IO完成,此时CPU把进程切换出去,进程处于“睡眠”状态。IO完成后,系统直接通知进程,则进程被唤醒。
非阻塞IO
进程发起IO调用,IO过程需要一段时间完成,所以立刻通知进程可以去进行其他操作。
每隔一段时间,进程就会去询问内核数据是否准备完成,完成后则获取数据继续执行,否则重复。(盲等)
IO复用
IO多路复用的本质是通过一种机制(系统内核缓冲IO数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(读就绪或者写就绪),能够通知程序进行相应的读写操作。
-
select:基于数组实现,最大连接数为1024(86)或者2048(64)。时间复杂度ON,它仅仅知道了有IO事件发生了,并不知道是哪几个流(可能有一个、多个甚至全部)。我们只能无差别的轮询所有流,找出能读数据或者写数据的流,对他们进行操作。如果同时处理的流越多,轮询的时间就越长。
-
poll:基于链表实现,时间复杂度ON,本质和select无区别,但是它没有最大连接数的限制。
-
epoll:时间复杂度O1,epoll会把哪个流发生了怎样的IO事件通知给我们。
事件(信号)驱动IO
水平出发的事件驱动机制:内核通知进程来读取数据,进程没来读取数据,内核需要一次一次的通知进程。
边缘触发的事件驱动机制:内核只通知一次让进程来读取数据,进程可以在超时时间之内随时来读取数据。
异步IO
区别总结
对文件的读写一般要经过内核态和用户态的切换,正因为有切换才导致了IO有同步和异步的说法。
通常来讲IO可以分成两种:
并且完成IO操作可以简单的表述为两个步骤:
如何区分是同步IO还是异步IO呢?看执行IO操作是否阻塞:当请求被阻塞,就是同步IO,否则就是异步IO。比如在阻塞I/O中,进程需要发起系统调用,让内核去把磁盘的内容拷贝到内核的内存空间去,这还不够,还需要阻塞等待内核把内核的内存空间内容拷贝到用户内存空间,然后返回进行所需的数据,进程这时才得以继续运行下去,因此这个有阻塞的就是同步I/O。而异步I/O,比如调用aio_read函数,可以直接发起系统调用,等内核把磁盘数据拷贝到内核地址,再从内核地址拷贝到用户地址,进程才会过来处理这个东西,在此前这个进程都是不会被阻塞的,因此是异步I/O。
同步IO的特点:
- 同步IO指的是用户进程触发I/O操作并等待或者轮询的去查看I/O操作是否就绪。
- 同步IO的执行者是IO操作的发起者。
- 同步IO需要发起者进行内核态到用户态的数据拷贝过程,所以这里必须有阻塞
异步IO的特点:
- 异步IO是指用户进程触发I/O操作以后就立即返回,继续开始做自己的事情,而当I/O操作已经完成的时候会得到I/O完成的通知。
- 异步IO的执行者是内核线程,内核线程将数据从内核态拷贝到用户态,所以这里没有阻塞
如何区分是阻塞IO还是非阻塞IO呢?
看发起IO操作是否阻塞。如果阻塞直到完成,就是阻塞IO,否则就是非阻塞IO。
五种区别图解
- 阻塞I/O,第一阶段和第二阶段都被阻塞;
- 非阻塞I/O,第一阶段不阻塞,第二阶段阻塞。但是第一阶段需要轮训请求内核。
- I/O复用,第一阶段阻塞,但是不用进程阻塞,用的是select等阻塞,第二阶段阻塞。
- 事件驱动I/O,第一阶段不阻塞,通过回调函数或者信号实现,第二阶段阻塞
- 异步I/O,第一二阶段都不阻塞。
磁盘调度算法
为了减少对文件的访问时间,采用一种最佳的磁盘调度算法,使得各进程对磁盘的平均访问时间最小。
先来先去服务(FCFS)
根据进程请求访问磁盘的先后次序进行调度,此算法的优点就是公平和简单,且每个进程的请求都能依次的得到处理,不会出现某一进程的请求长期得不到满足的情况。但此算法未对寻道进行优化,致使平均寻道时间可能较长。
最短寻道时间优先(SSTF)
要求访问的磁道与当前磁头所在的磁道距离最近,以使得每次寻道时间最短,但该算法不能保证平均寻道时间最短。
扫描算法(SCAN)
不仅考虑到欲访问的磁道与当前磁道间的距离,更优先考虑到的是磁头当前的移动方向。当磁头正在自里向外移动时,SCAN算法所考虑的下一个访问对象应该是其欲访问的磁道既在当前磁道之外,又是距离最近的。
避免死锁:银行家算法
一种避免死锁的算法,当一个进程申请使用资源的时候,银行家算法通过先试探分配给该进程资源,然后通过安全性算法判断分配后的系统是否处于安全状态(即资源分配后,系统能按某种顺序来为每个进程分配其所需的资源,使每个进程都可以顺利地完成),若不安全则试探分配作废,让该进程继续等待。
-
四种数据结构:系统中可利用资源、所有进程对资源的最大需求、系统中的资源分配、所有进程还需要多少资源。
- 可利用资源(Available):含有m个元素的数组,其中每一个元素代表一类可利用的资源数目。初始值是系统中所配置的该类全部可用资源的数目,数值随该类资源的分配和回收而动态的改变。
- 最大需求矩阵(Max):一个n * m的矩阵,定义了系统中n个进程中的每个进程对m类资源的最大需求。
- 分配矩阵(Allocation):一个n * m的矩阵,定义了系统中每一类资源当前已分配给每一进程的资源数。
- 需求矩阵(Need):一个n * m的矩阵,用以表示每一个进程尚需的各类资源数。
上述三个矩阵间存在下述关系:Need[i, j] = Max[i, j] - allocation[i, j]
-
假设Request是进程P的请求向量,如果Request[j] = K,则表示进程P需要K个rj类型的资源。当p发出资源请求后,系统按照下述步骤进行检查:
- 如果Request[j] <= Need[i, j],跳转到下一步,否则出错,因为它所需要的资源数已经超过它所宣布的最大值。
- 如果Request[j] <= Available[j],跳转到下一步,否则表示无足够资源,P必须等待。
- 系统试探着将资源分配给进程P,并修改下面数据结构中的数值
- Available[j] -= Request[j];
- Allocation[i, j] += Request[j];
- Need[i, j] -= Request[j];
- 系统执行安全性算法,检查此次资源分配后系统是否处于安全状态。若安全才将资源分配给进程P,否则将本次的试探分配作废,恢复到原来资源分配状态,让进程P等待。
-
安全性算法:
- 1.设置两个向量:
- Work:表示系统可提供给进程继续运行所需的各类资源数目,它含有m个元素,在执行安全算法开始时Work = Available。
- Finish:表示系统是否有足够的资源分给进程使之运行完成,开始时Finish[i] = false,当有足够资源分配给进程时,再另Finish[i] = true。
- 2.从进程集合中找到一个能满足下述条件的进程:
- Finish[i] = false;
- Need[i, j] <= Work[j];
- 若找到了则执行步骤3,否则执行4
- 3.当进程P获得资源后,可顺利执行直到完成,并释放出分配给它的资源:
- Work[j] += Allocation[i, j];
- Finish[i] = true;
- 返回步骤2
- 4.如果所有进程的Finish[i] = true都满足,则表示系统处于安全状态,否则表示系统不安全。
-
安全性算法的问题:
- 对于一个安全的系统来说,步骤2完全满足,那么必然会将所有的Finish[i]设置为true。所以我们可以设置一个变量来计数,当该变量与进程数量相等的时候说明已经全部置为true了,终止循环。
- 对于一个不安全的系统,不可能将Finish[i]都置为true,必定存在false,那么我们就得去找一个退出while循环的条件(因为不达到条件就不断的去循环查找)。我们认为,当寻找一轮发现是不安全的,那么下一轮查找必定也是不安全的。
- 解决方案:我们可以记录一下Finish[i] = true的次数,然后在下次判断的时候,将Finish[i] = true的次数和上一轮的进行对比,如果相等就说明找到了不安全的进程,则跳出循环。
了解:程序的装入和链接
用户程序要在系统中运行,必须先将它装入内存,然后再将其转变为一个可以执行的程序:
- 编译:由编译程序对用户源程序进行编译,形成若干个目标模块。
- 链接:由连接程序将编译后形成的一组目标模块以及它们所需要的库函数链接在一起,形成一个完整的装入模块。
- 静态链接:程序运行之前,将各个目标模块及它们所需的库函数链接成一个完整的装配模块,以后不再拆开。(一次全部连接)
- 装入时的动态链接:将用于源程序编译后所得到的一组目标模块,在装入内存时,采用边装入边链接的链接方式。即在装入一个目标模块时,若发生一个外部模块调用时间,将引起装入程序去找出相应的外部目标模块,并将它装入内存。
- 运行时的动态链接:将对某些模块的链接推迟到程序执行时才进行。即在执行过程中,如果发现一个被调用模块尚未装入内存时,立刻由OS去找该模块,并将其装入内存,将其链接到调用者模块上。
- 装入:由装入程序将装入模块装入内存。
- 绝对装入方式:适用于单道程序,事先知道程序有可能装入的位置。
- 可重定位装入方式:多到程序下,根据内存的具体情况将装入模块装入到内存的适当位置。
- 动态运行时的装入方式:把装入模块装入内存后,并不立刻把装入模块中的逻辑地址转换为物理地址,而是把这种地址转换推迟到程序真正要执行时才进行。