操作系统学习笔记 7进程管理
600 Words|Read in about 3 Min|本文总阅读量次
熟悉完操作系统的第六篇章,开始学习第七篇章,关于操作系统的进程管理。
0猜你喜欢
操作系统系列文章
7进程管理
- 进程(PROCESS)描述
- 进程状态(State)
- 线程(THREAD)
- 进程间通信(INTER-PROCESS COMMUNICATION)
- 进程互斥与同步
- 死锁(DEADLOCK)
7.1进程描述
- 进程定义
- 进程的组成
- 进程的特点
- 进程控制结构
在使用计算及过程中,可以看到进程的影子。如下图7-1所示,左边看到很多正在运行的程序,某种角度上可以把正在运行的程序看做是进程,代表程序在整个执行过程的描述。右边可以看到CPU的使用率、内存、网络一会高一会低,代表进程有时候占用CPU、内存、网络多,有时候占用CPU、内存、网络较少。
本来操作系统只能运行一个程序,随着计算机的发展,CPU能力越来越强,内存越来越多,可以在内存中放入更多的运行程序,这个时候再用程序表示各个运行的程序就不大合适了,可能一个程序会跑多个,多个程序的实例在内存中无法表示。通过引入进程,可以更好地表示程序的整个执行过程。
7.1.1进程
一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程。
一个静态的程序,通过操作系统在内存中让程序执行起来,形成动态的执行过程就是进程。
7.1.2 进程的组成
一个进程应该包括
- 程序的代码
- 程序处理的数据
- 程序计数器中的值,指示下一条将运行的指令
- 一组通用的寄存器的当前值,堆和栈
- 一组系统资源(如打开文件)
总之,进程包含了正在运行的一个程序的所有状态信息
7.1.2.1进程与程序的联系
- 程序是产生进程的基础
- 程序的每次运行构成不同的进程
- 进程是程序功能的体现
- 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。
7.1.2.2进程与程序的区别
- 进程是动态的,进程是程序的执行,进程有核心态/用户态;程序是静态的,程序是有序代码的集合
- 进程是暂时的的,程序是永久的;进程是一个状态变化的过程,程序可以长久保存
- 进程与程序的组成不同,进程的组成包括程序、数据和进程控制块(即进程状态信息)
举例说明
如下图7-2所示,进程是包括程序和其他部分,可以被其他优先级更高的进程中断,切换到优先级高的进程中来。
7.1.3进程的特点
动态性
可动态地创建、结束进程
并发性
进程可以被独立调度并占用处理机运行
独立性
不同进程的工作不互相影响(页表机制)
制约性
因访问共享数据/资源或进程间同步而产生制约
一段时间内有多个进程在执行,给人一种错觉像是并行执行。
并发
一段时间内有多个进程在执行
并行
一个时刻有多个进程在执行
如果目前只有一个CPU,是无法并行执行多个进程的。操作系统会不停调度不同的进程来执行,进程执行时间会受到其他进程的影响,但是独立性是不会受到影响。
独立性的保证
通过页表可以使得不同的程序访问不同的地址空间,这个过程中不可能越过当前程序的地址空间。越过这个地址空间会产生缺页异常,页错误等。
7.1.4进程控制结构
程序=算法+数据结构
描述进程的数据结构:进程控制块(Process Control Block,PCB)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息。
7.1.4.1进程控制块
操作系统管理控制进程运行所用的信息集合。操作系统用PCB来描述进程的基本情况以及运行变化的进程,PCB是进程存在的唯一标志。
7.1.4.2使用进程控制块
-
进程的创建
为该进程生成一个PCB
-
进程的终止
回收它的PCB
-
进程的组织管理
通过对PCB的组织管理来实现
7.1.4.3PCB含有以下三大类信息
-
进程标识信息
如本进程的标识,本进程的产生者标识(父进程标识);用户标识
-
处理机状态信息保存区
保存进程的运行现场信息;
用户可见寄存器,用户程序可以使用的数据,地址等寄存器
控制和状态寄存器,如程序计数器PC,程序状态字PSW
栈指针,过程调用/系统调用/中断处理和返回时需要用到它
-
进程控制信息
调度和状态信息
用于操作系统调度进程并占用处理机使用
进程间通信信息
为支持进程间的与通信相关的各种标识、信号、信件等,这些信息存在接收方的进程控制块中
存储管理信息
包含有指向本进程映像存储空间的数据结构
进程所用资源
说明由进程打开、使用的系统资源,如打开的文件等
有关数据结构连接信息
进程可以连接到一个进程队列中,或连接到相关的其他进程的PCB。
7.1.4.4PCB的组织方式
-
链表(更好的动态插入和删除)
同一状态的进程其PCB成一链表,多个状态对应多个不同的链表
各状态的进程形成不同的链表:就绪链表和阻塞链表
-
索引表(较少的动态插入和删除)
同一状态的进程归入一个index表(由index指向PCB),多个状态对应多个不同的index表。
各状态的进行形成不同的索引表:就绪索引表和阻塞索引表
7.2 进程状态
- 进程的生命期管理
- 进程状态变化模型
- 进程挂起模型
7.2.1进程的生命期管理
进程的生命期管理
- 进程创建
- 进程运行
- 进程等待
- 进程唤醒
- 进程结束
7.2.1.1进程创建
引起进程创建的3个主要事件
- 系统初始化时
- 用户请求创建一个新进程
- 正在运行的进程执行了创建进程的系统调用
7.2.1.2进程运行
内核选择一个就绪的进程,让它占用处理机并执行
为何选择?
如何选择?
7.2.1.3进程等待
在以下情况下,进程等待(阻塞)
- 请求并等待系统服务,无法马上完成
- 启动某种操作,无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己,因为只有进程自身才知道何时需要等待某种事件的发生。
7.2.1.4进程唤醒
唤醒进程的原因
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待事件到达
- 将该进程的PCB插入到就绪队列
进程只能被别的进程或操作系统唤醒。
7.2.1.5进程结束
在以下四种情况下,进程结束
- 正常退出(自愿的)
- 错误退出(自愿的)
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
7.2.2进程状态变化模型
进程的三种基本状态
进程在生命结束前处于且仅处于三种基本状态之一
不同系统设置的进程状态数目不同
-
运行状态
当一个进程正在处理机上运行时
-
就绪状态
一个进程获得了除处理机之外的一切所需资源,一旦得到处理机即可运行
-
等待状态
一个进程正在等待某一事件而暂停运行时。如等待某资源,等待输入/输出完成。
下图7-3所示为三种状态变化模型图
进程其他的基本状态
-
创建态
一个进程正在被创建,还没被转到就绪状态之前的状态
-
结束态
一个进程正在从系统中消失的状态,这是因为进程结束或由于其他原因所导致
如下所示为完整的状态变化图
可能的状态如下
NULL->New,一个新进程被产生出来执行一个程序
New->Ready,当进程被创建完成并初始化后,一切就绪准备运行时,就为就绪状态
Ready->Running,处于就绪状态的进程被进程调度程序选中后,就分配到处理机上来运行
Running->Exit,当进程表示它已经完成或者因出错,当前运行进程会由操作系统作结束处理
Running->Ready,处于运行状态的进程在其运行过程中,,由于分配给它的处理机时间片用完而让出处理机。
Running->Blocked,当进程请求某样东西且必须等待时
Blocked->Ready,当进程要等待某时间到来时,它从阻塞状态变到就绪状态
7.2.3进程挂起
7.2.3.1进程挂起
进程在挂起状态时,意味着进程没有占用内存空间,处于挂起状态的进程映像在磁盘上。
7.2.3.2挂起状态
-
阻塞挂起状态
进程在外存并等待某事件的出现
-
就绪挂起状态
进程在外存,但只要进入内存,即可运行
7.2.3.3与挂起相关的状态转换
-
挂起(Suspend):把一个进程从内存转到外存
阻塞到阻塞挂起
没有进程处于就绪状态或就绪进程要求更多内存资源时,会进行这种转换,以提交新进程或运行就绪进程
就绪到就绪挂起
当有高优先级阻塞(系统认为会很快就绪的)进程和低优先就绪进程时,系统会选择挂起低优先级就绪进程
运行到就绪挂起
对抢先式分时系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态
-
在外存时的状态转换
阻塞挂起到就绪挂起
当有阻塞挂起进程因相关事件出现时,系统会把阻塞进程转换为就绪挂起进程
-
解挂/激活(Activate)把一个进程从外存转到内存,可能有以下几种情况
就绪挂起到就绪
没有就绪进程或挂起就绪进程优先级高于就绪进程是,会进行这种转换
阻塞挂起到阻塞
当一个进程释放足够内存时,系统会把一个高优先级阻塞挂起(系统认为会很快出现所等待的事件)进程转化为阻塞进程
OS通过PCB和定义的进程状态来管理PCB,帮助完成进程的调度过程
7.2.3.4状态队列
- 由操作系统来维护一组队列,用来表示系统当中所有进程的当前状态
- 不同的状态分别用不同的队列来表示(就绪队列、各种类型的阻塞队列)
- 每个进程的PCB都根据它的状态加入到相应的队列当中,当一个进程的状态发生变化时,它的PCB从一个状态队列中脱离出来,加入到另一个队列。
如下图7-4所示为状态表示方法图
这里还区分进程的优先级,就绪队列1优先级最高,当就绪队列1为空的时候才会从就绪队列2调度进程。
如果事件1只能满足一个进程的话,那么只能将队列中的一个进程从阻塞态变成就绪态。如果事件1产生之后,所有等待事件一致的进程都能满足,那需要把所有进程从阻塞状态变成就绪状态。
7.3线程管理
线程
自从60年代提出进程概念以来,在操作系统中一直都是以进程作为独立运行的基本单位,直到80年代中期,人们又提出了更小的能独立运行的基本单位,线程。
- 为什么使用线程
- 什么是线程
- 线程的实现
- 多线程编程接口举例
7.3.1为什么使用线程
案例 编写一个MP3播放软件
核心功能模块有三个
- 从MP3音频文件当中读取数据
- 对数据进行解压缩
- 把解压后的音频数据播放出来
对于单个进程的方式,会出现一系列问题
- 播放的声音是否连贯
- 各个函数之间不是并发执行,影响资源的使用效率
使用多进程的方式,也会引入一些问题
- 进程之间如何通信,共享数据
- 维护进程的系统开销较大
- 创建进程时,分配资源、建立PCB,撤销进程时,回收资源、撤销PCB
- 进程切换时,保存当前进程的状态信息
我们需要提出一种新的实体。满足以下特征
- 实体之间可以并发的执行
- 实体之间共享相同的地址空间
7.3.2什么是线程
Thread
进程当中的一条执行流程
7.3.2.1从两方面来重新理解进程
-
从资源组合的角度
进程把一组相关的资源组合起来,构成了一个资源平台(环境),包括地址空间(代码段、数据段)、打开的文件等各种资源
-
从运行的角度
代码在这个资源平台上的一条执行流程(线程)
如下图7-5所示,线程有TCB(Thread Control Block,TCB)
TCB主要用于管理线程执行相关的一系列信息,包含PC(程序计数器)、SP(堆栈)还有一些寄存器的信息,由于有不同的控制流,需要一系列的寄存器来表示控制流的执行状态,这个是每个线程独立的信息。
但是内存空间,比如代码段、数据段,这些是所有线程所共享的。
线程=进程-共享资源
-
线程的优点
一个进程中可以同时存在多个线程
各个线程之间可以并发地执行
各个线程之间可以共享地址空间和文件等资源
-
线程的缺点
一个线程崩溃,会导致其所属进程的所有线程崩溃。
不同操作系统对线程的支持是不一样的。
7.3.2.2线程所需要的资源
对于单线程而言,整个进程里面只有一个控制流。对于多线程而言,整个程序会存在多个控制流,而且控制流的流程是不一样的,有各自独立的寄存器和堆栈。
7.3.2.3线程与进程的比较
-
进程是资源分配单位,线程是CPU调度单位
-
进程拥有一个完整的资源平台,而线程只独享必不可少的资源,如寄存器和栈
-
线程同样具有就绪、阻塞和执行三种基本状态,同样具有状态之间的转换关系
-
线程能减少并发执行的时间和空间开销
线程的创建时间比进程短
线程的终止时间比进程短
同一进程内的线程切换时间比进程短(同一个进程的页表线程不用换)
由于同一进程的各线程间共享内存和文件资源,可直接进行不通过内核的通信
7.3.3线程的实现
主要有三种线程的实现方式
-
用户线程(操作系统看不到的线程)
在用户空间实现(POSIX Pthreads, Mach C-threads, Solaris threads)
-
内核线程(操作系统管理起来的线程)
在内核空间实现(Windows, Solaris, Linux)
-
轻量级进程
在内核中实现,支持用户线程(LightWeight Process)
用户线程与内核线程的对应关系
多对一
一对一
多对多
7.3.3.1用户线程
用户态的库完成了线程控制管理,其中的TCB(Thread Control Block)这个是在库里面完成的。对于操作系统而言,看不到TCB,只能看到进程。所以线程的调度和管理,操作系统不直接参与,直接由线程库来完成。
在用户空间实现的线程机制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括线程的创建、终止、同步和调度等。
- 由于用户线程的维护由相应进程来完成(通过线程库函数),不需要操作系统内核了解用户线程的存在,可用于不支持线程技术的多进程操作系统
- 每个进程都需要它自己私有的线程控制块(TCB)列表,用来跟踪记录它的各个线程的状态信息(PC、栈指针、寄存器),TCB由线程库函数来维护
- 用户线程的切换也是由线程库函数来完成,无需用户态/核心态切换,所以速度特别快
- 允许每个进程拥有自定义的线程调度算法
用户线程缺点
- 阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程在等待(读文件)
- 当一个线程开始运行后,除非它主动地交出CPU的使用权,否则它所在的进程当中的其他线程将无法运行
- 由于时间片分配给进程,故与其他进程比,在多线程执行时,每个线程得到的时间片较少,执行会较慢
7.3.3.2内核线程
操作系统可以看得见的线程,也就是TCB是放到内核中的,WINDOW操作系统就是这么设计的。只要完成一次线程切换,就会有一次内核态到用户态的变化,开销比较大。
在操作系统的内核当中实现的一种线程机制,由操作系统的内核来完成线程的创建、终止和管理
- 在支持内核线程的操作系统中,有内核来维护进程和线程的上下文信息(PCB和TCB)
- 线程的创建、终止和切换都是通过系统调用/内核函数的方式来进行,由内核来完成,因此系统开销较大
- 在一个进程当中,如果某个内核线程发起系统调用而被阻塞,并不会影响其他内核线程的运行
- 时间片分配给线程,多线程的进程获得更多CPU时间
- Window NT/Windows 2000/XP支持内核线程
7.3.3.3轻量级进程
它是内核支持的用户线程。一个进程可有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来支持。(Solaris/Linux)
如下图7-6所示,多个轻量级的进程对应这一个内核中的线程这样的实现方式。相对而言,Solaris管理起来更加复杂,比较灵活;而Linux更加简洁,效率更高,灵活性没有Solaris高。
7.4进程切换
7.4.1上下文切换
-
停止当前运行进程(从运行状态改变成其他状态)并且调度其他进程(转变成运行状态)
必须在切换之前存储许多部分的进程上下文
必须能够在之后恢复它们,所以进程不能显示它曾经被暂停过
必须快速(上下文转换时非常频繁的)
-
需要存储什么上下文
寄存器(PC、SP。。。),CPU状态,。。。
有些时候可能会费时,所以我们应该尽可能避免
进程切换涉及到两个进程,进程P0和进程P1。进程P0在执行到一定程度的时候,这时候操作系统考虑让另一个进程P1来执行。进程P0会把context信息(寄存器信息)保存在PCB中,然后让另一个P1进程的PCB的context信息恢复到CPU中去,这样操作系统让进程来回切换的过程就是上下文切换。这个上下文信息和硬件有着紧密联系,实际操作系统实现这块代码是以汇编语言书写的。
7.4.2需要做上下文切换的进程
操作系统为活跃进程准备了PCB,操作系统将PCB放置到一个合适的队列里
- 就绪队列,能够在CPU上执行的进程它们会存放在就绪队列中,我们需要将就绪态进程放置到链表中,便于操作系统去寻址
- 等待I/O队列(每个设备的队列)
- 僵尸队列
7.5进程控制
7.5.1创建进程
7.5.1.1CreateProcess(filename)
Windows进程创建的API
- 创建时关闭所有在子进程里的文件描述符
CreateProcess(filename, CLOSE_FD)
- 创建时改变子进程的环境
CreateProcess(filename, CLOSE_FD, new_envp)
7.5.1.2fork/exec
Unix进程创建系统调用
fork()
把一个进程复制成两个进程,parent(old PID)
,child(new PID)
exec()
用新程序来重写当前进程,PID没有改变
用fork和exec创建进程的示例
1int pid = fork(); //创建子进程
2if(pid == 0) //子进程在这里继续
3{
4 //Do angthing(unmap memory, close net connections...)
5 exec("program", argc, argv0, argv1, ...);
6}
其中,fork()创建一个继承的子进程
- 复制父进程的所有变量和内存
- 复制父进程的所有CPU寄存器(有一个寄存器例外,是用来识别父进程和子进程的)
fork()的返回值
- 子进程的fork()返回0
- 父进程的fork()返回子进程标识符
- fork()返回值可方便后续使用,子进程可使用getpid()获取PID值
fork()的地址空间复制
-
fork()执行过程对于子进程而言,是在调用时间对父进程地址空间的一次复制
-
对于父进程fork()返回child PID,对于子进程返回值为0
7.5.2加载和执行进程
首先看一个例子,系统调用exec会加载程序取代当前运行的程序
1int main()
2{
3 ...
4 int pid = fork();
5 if(pid == 0)//子进程
6 {
7 exec_status = exec("calc", argc, argv0, argv1, ...);
8 printf("Why would I execute?");//正常不可能执行到,起到提醒作用,子进程去执行另一个程序了,不可能在切回来
9 }else if(pid > 0)//父进程
10 {
11 printf("Whose your daddy?");
12 ...
13 //wait代表子进程结束了
14 child_status = wait(pid);
15 }else
16 {
17 /*error occurred*/
18 }
19}
这里可以直观的看出执行fork和exec的变化
当执行exec的时候,程序和代码都会复制一份,
里面的pid没变,执行的代码会发生变化,如下图7-7所示,从本来的sh变成了calc程序
内存中的布局图,用户空间存入一些信息,shell的代码段和数据段(包含堆栈),在内核空间内放在PID和进程控制块的信息
fork会创建一个新的进程,子进程的地址是完全复制父进程的
执行exec之后,PCB信息发生变化,用户内存空间发生变化,代码段被新的程序calc替换,整个控制流发生变化
Exec()
-
调用允许一个进程“加载”一个不同的程序并且在main开始执行(事实上_start)
-
它允许一个进程指定参数的数量(argc)和它字符串参数数组(argv)
-
如果调用成功
它是相同进程,但是它运行了一个不同的程序
-
代码,stack(栈) & heap(堆)重写
fork的简单实现
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程里
- 开销昂贵(复制父进程的代码段和数据段)
在99%的情况里,我们调用fork()之后调用exec()
- 在fork()操作中内存复制是没有作用的
- 子进程可能关闭打开文件和连接
- 开销因此是高的
vfork
- 一个创建进程的系统调用,不需要创建一个同样的内存映像
- 一些时候称为轻量级fork
- 子进程应该几乎立刻调用exec
- 现在不在使用,使用Copy On Write技术(COW)
7.5.3等待和终止进程
7.5.3.1wait
1)系统调用是被父进程用来等待子进程的结束
2)一个子进程向父进程返回一个值,所以父进程必须接受这个值并处理
3)wait系统调用担任这个要求
- 它使父进程去睡眠来等待子进程的结束
- 当一个子进程调用exec时候,操作系统解锁父进程,并且将通过exit传递得到的返回值作为wait调用的一个结果(连同子进程呢个的pid一起)。如果这里没有子进程存活,wait立刻返回。
- 当然,如果这里有为父进程的僵尸等待,wait立刻返回其中一个值(并且解除僵尸状态)
7.5.3.2调用exit(进程结束执行之后)
这个系统调用
-
将这程序的结果作为一个参数
-
关闭所有打开的文件,连接等
-
释放内容
-
释放大部分支持进程的操作系统
-
检查是否父进程是存活着的
如果是的话,它保留结果的直到父进程需要它;在这种情况里,进程没有真正死亡,但是它进入了僵尸状态
如果没有,它释放所有的数据结构,这个进程死亡(Root进程会定期扫描僵尸队列 判断僵尸状态的子进程的父进程是否存在)
-
清除所有等待的僵尸进程
进程终止是最终的垃圾收集(资源回收)
子进程被fork出来,在执行exit退出的时候,会处于一个僵尸进程状态,PCB没有被回收。直到父进程执行wait状态,可以回收子进程的僵尸状态的PCB,最终退出程序。
执行exec的时候,在整个系统执行过程中,可能处于不同状态。首先执行exec,进程会进入running状态,exec会加载运行程序和执行运行程序。在加载的时候,程序在磁盘上,把磁盘读到内存中来,这个过程花的时间比较长。也就是会从running状态到blocked状态切换。