1. 进程
1.1 进程的定义
- 是程序的一次执行
- 或者说,进程是程序执行的1个实例
- 每个进程都有自己的地址空间
- 消耗计算机资源(CPU、IO等)
- 一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
为什么要引入进程
从操作系统的历史来看,最初的操作系统只能支持跑一个程序,随着后来的发展,CPU越来越强,内存越来越大,内存中存放的程序越来越多,再用程序的概念表示os中各个运行的程序就不合适了,因为有可能存在一个程序跑多份的情况。如果一个程序跑了多次,这么多个程序实例还用程序来表示显然是不合适的。
进程能够更好的表示整个程序的执行过程
1.2 进程的组成
- 程序的代码(放在内存中)
- 程序处理的数据(放在内存中)
- 程序计数器中的值,指示下一条将运行的指令
- 一组通用寄存器的当前值,堆、栈
- 一组系统资源(如打开的文件)
总之,进程包含了正在运行的一个程序的所有状态信息
1.3 进程和程序的关系和差异
联系
区别
- 程序是进程的基础,进程是程序的功能体现
- 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包含多个程序
- 程序是进程的静态实体(即执行代码)
- 进程是动态的,程序是静态的
- 程序是有序代码的集合,进程是程序的执行,进程有核心态和用户态(为什么有核心态:进程在执行过程中需要去完成某些特定的功能,比如说读写文件,这涉及到硬盘,需要操作系统来完成,进程只需要给操作系统发出请求,操作系统代表进程在内核中执行,此时进程处于核心态
- 进程是暂时的,程序是永久的:进程是一个状态变化的过程,程序可长久保存
- 进程与程序的作用不同:进程的组成包括程序、数据和进程控制快
- 一个程序可以对应多个进程,每启动一次就产生一个进程
例:
食谱=程序,
厨师=CPU,
原料=数据,
做蛋糕的过程=进程
进程可以被切换
进程的切换体现了动态性
1.4 进程特点
并发与并行
并发:只有1个CPU时,在某一时刻只有一个程序被执行,当需要执行其他程序时,可以进行切换,在一段短的时间内可以有多个程序被执行
并行:当有多个CPU时,在一个时刻可以有多个程序被执行
进程的特点
- 动态性:可动态的创建、还可以进行状态切换,从一个状态切到另一个状态,最后结束进程
- 并发性:进程可以被独立调度并占用CPU处理
- 独立性:不同进程的工作不互相影响(用页表保障,给不同进程分配不同的页表,让每个进程在一个独立的空间执行,彼此的代码和数据不受影响)
- 制约性:因访问共享数据/资源或进程间同步而产生制约。一个进程可能和另一个进程有交互,或者说一个进程要等另一个进程执行到一定阶段后才会继续执行,他们之间有个时序的关系,这需要操作系统协调这些进程的执行
程序=算法+数据结构
那么描述进程的数据结构:进程控制块(Process Control Block,PCB)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息
1.5 进程控制结构
1. 进程控制块:操作系统管理控制进程运行所用的信息集合
操作系统用PCB来描述进程的基本情况以及运行变化的过程
PCB是进程存在的唯一标志
PCB伴随进程的一生,进程产生PCB即产生,进程消亡PCB即消亡
2. 使用进程控制块
进程的创建:为该进程生成一个PCB
进程的终止:回收它的PCB
进程的组织管理:通过对PCB的组织管理来实现
3. PCB包含三大类信息
4. PCB的组织方式
进程个数较为固定,可以采用索引的方式,
一般采用链表,便于删除和添加
2 进程状态
2.1 进程的生命期管理
1. 进程创建
操作系统初始化时创建第一个进程——MIT进程,这个进程负责创建其他新的进程
- 用户发起创建进程的请求,由操作系统去创建
- 正在运行的进程有时也需要创建新的进程,也会向操作系统发起创建进程的请求
2. 进程运行
创建出的新进程不一定就会被执行,还需要让操作系统选择其中一个可以执行的进程进行执行,把可以执行的进程称为就绪进程,
就绪进程有多个,操作系统还需要选择让哪个就绪进程占用CPU进行执行,由就绪态变为运行态,这就涉及到后面的调度算法
3. 进程等待
如等待读取文件、和其他进程协同,而且他进程还未结束、数据等其他资源
如果某个进程需要去硬盘读取某一文件,此时CPU的处理速度要远高于从硬盘中读取文件的速度,CPU不能一直等待这个资源,这样会很浪费这个CPU资源,因此会把这个进程处于等待状态,让其他就绪的进程执行
某个进程状态由运行转为等待,这个进程就不占用CPU资源了
4. 进程唤醒
5. 进程结束
致命错误:访问了其他进程的内存空间
2.2 进程状态变化模型
进程的三种状态
进程在生命结束前处于且仅处于
三种基本状态之一
不同系统设置的进程状态数目不同
三状态图
PCB彻底消失了,进程彻底结束了
五状态图
运行->就绪:存在多个就绪进程,为保证每个就续进程均有可能执行,给每个就绪进程均分配一个时间片,执行完即可被换下,改另一个就绪进行执行,这样能使每个就绪态的进程公平的占用CPU
不会持续很久,是一个很快的过程,因为只是完成了PCB的初始化
操作系统来完成,程序本身很难自己让出CPU
等待定时器的到达、读写文件、资源
操作系统来完成这2个状态的转变
2.3 进程挂起
进程的程序和数据被切换到了硬盘上去,这时的进程被称为挂起的进程
挂起状态
- 阻塞挂起状态(Blocked_suspend):进程在
外存
等待某事件的出现,进程处于阻塞状态
- 就绪挂起状态(Ready_suspend):进程在
外存
,但只要进入内存,即可运行,进程属于就绪状态
与挂起相关的状态转换
- 挂起(Suspend):把一个进程从内存转到外存;可能有以下几种情况:
- 阻塞→阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,发生这种转换,以提交新进程或运行就绪进程(腾出更多的内存空间给就绪状态/进程)
- 就绪→就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程
- 运行→就绪挂起:对抢占式系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态
- 在外存时的状态转换:
- 阻塞挂起到就续挂起:当有阻塞挂起进程因相关事件出现时,系统会把阻塞挂起进程转换为就绪挂起进程(还是在硬盘,只是状态变了)
问题:OS怎么通过PCB和定义的进程状态来管理PCB,帮助完成进程的调度过程?
OS负责选择某一个进程占用CPU执行,同时进程状态的转化
状态队列是OS管理进程很重要的数据结构
当事件1得到满足后,所有处于等待事件1上的进程就有可能从阻塞态变为就绪态
- 如果事件1只能满足一个阻塞进程,则只把这一个进程的状态由阻塞变为就绪
- 若1能满足所有的,则把所有的状态都变为就绪
小结
3 线程(Thread)管理
自从60年代提出进程概念以来,在操作系统中一直都是以进程作为独立运行的基本单位,直到80年代中期,人们又提出了更小的能独立运行的基本单位——线程
3.1 为什么使用线程
存在的问题:
- 播放出来的声音能否连贯?
CPU的能力不够强的话,声音可能就是断断续续的。Read()表示要从硬盘中读取文件,而硬盘读取速度较慢,就会导致后面的解压缩不能执行,即各个函数之间不是并发执行,影响资源的使用效率(I/O和CPU不能重叠,不能并发执行)
进程执行Read()时,如果数据还没有到来的话,就让其他的进程执行。一开始多读几块数据。当read()被阻塞时,执行其他的进程,保证高效的播放
存在的问题:
1、进程间如何通信,共享数据?(进程1的数据状态如何传给进程2?)
2、维护进程的系统开销较大(进程创建、进程切换需要保存和恢复,占用资源较多)
开销:创建进程时,分配资源、建立PCB;撤销进程时,回收资源、撤销PCB;进程切换时,保存当前进程的状态信息
与进程不同,每个进程间是独立的,不可以相互访问地址空间,如果要互相访问,需要进程间通信,即OS把数据从一个进程传到另一个进程,开销比较大
这个实体就是线程!
3.2 什么是线程
进程2个功能:
- 管理资源
- 执行功能,把执行状态交给线程来管理
之前默认一个进程只有一个线程
现在一个进程可以有多个线程,这些线程共享进程提供的资源,即线程可以共享访问进程的代码、数据、文件等
因为一个线程可以访问进程所有的资源,那么它把某个数据破坏了,而其他线程也需要这个数据,从而导致所有线程崩溃,这个进程也就结束了
强调性能
时,所执行的代码相对统一,不会出现一个代码执行不同控制的流程,如天气预报,一些复杂的水利计算等,需要线程
Internet的服务,打开浏览器打开网页可以用线程实现,但是某个网页被攻击了,就会引起这个线程的崩溃,那么所有打开的网页都崩溃了,所以打开一个网页使用进程实现,需要考虑安全性
,而不用过于担心线程
线程所需的资源
不同线程有自己的堆栈来保证相对独立
线程与进程的比较
线程创建时间和终止时间较短:因为进程在创建过程中还要创建一些其他的管理的信息,比如要创建内存怎么去管理,创建打开的文件怎么去管理
而线程会直接使用进程创建好的,线程终止时也不需要考虑这些资源的释放
同一进程的线程切换时间较短:因为同一个进程的线程具有同一个地址空间,所以这些线程拥有同一个页表,切换时不需要切换页表。而切换进程需要切换页表,切换页表开销比较大
所以线程的时间效率和空间效率比进程高
3.3 线程的实现
主要有三种线程的实现方式:
- 用户线程:在用户空间实现(os看不到,由用户线程库管理)(user thread)
POSIX Pthreads,Mach C-threads,Solaris threads - 内核线程:在内核中实现(由os管理)
Windows,Solaris,Linux(kernel thread) - 轻量级进程:在内核中实现,支持用户线程(LightWeight Process)
Solaris
用户线程和内核线程的对应关系
-
多对一(多个用户线程对一个内核线程)
-
一对一
-
多对多
3.3.1 用户线程
os只能看到PCB,看不到里面的TCB,TCB是由线程管理库管理的
用户线程缺点:
- 阻塞性的系统调用如何实现?如果一个线程发起系统调用而阻塞,则整个进程都需要等待(即使其他线程没有发出系统调用,那么它们也会等待。因为os看不到线程,只能看到进程,发生系统调用,就只会把进程阻塞)
- 当一个线程开始运行后,除非它主动交出CPU的使用权,否则它所在的进程当中的其他线程将无法进行(因为用户态的线程库无法主动打断当前用户线程的执行,虽然os可以打断(因为它会管理中断,一旦产生中断,CPU的控制权就到OS手里了,就可以对中断进行处理),但是用户态的线程OS打断不了,只能靠线程自己放弃)
- 由于时间片分配给进程,故与其他进程相比,在多线程执行时,每个线程得到的时间片较少,执行会较慢
3.3.2 内核线程
(典型的windows)
os管理调度的基本单位变为线程
所有线程的创建调度都是由os来完成的
进程主要实现的是管理资源
一个PCB管理一系列的TCB的list,就具体的调度由TCB完成
每一次线程切换,就要有一次从用户态到内核态的切换,开销较大
3.3.3 轻量级进程
它是内核支持的用户线程,一个进程可有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来实现(Solaris/Linux)
3.4 多线程编程接口举例
4 上下文切换(context switch)
各个进程间共享CPU资源,需要进程切换(将context保存到PCB中)让不同的进程占用CPU,这个切换的过程称为进程上下文切换
进程运行的时候需要程序计算器PC(指出进程在什么地方)、栈指针(知道调用关系以及相应的局部变量所在位置)等
需要把这些信息保存到进程控制块PCB的某一个位置
当此时运行进程P0,要运行另一个进程P1时,需要把context保存到P0进程的PCB中即PCB0中,同时把寄存器等context信息从那个进程P1的PCB1中取出来,恢复到CPU里去
(因为保存到PCB里去和恢复到CPU中都与硬件有关,所以由汇编语言来实现)
需要上下文切换的开销越小越好,使得大部分时间都用于进程运行本身
操作系统为活跃进程准备了进程控制块(PCB)
操作系统将PCB放置在一个合适的队列中
5 进程控制()
5.1 进程创建(未找到视频)
- 创建一个PCB
- 赋予一个唯一的进程标识符pid
- 初始化PCB
- 设置相应的链接,如:把新进程加到就绪队列中
fork()后出现2个PCB
pid=127:父进程,直接执行
pid=128:子进程,加载(exec(“/bin/calc”))变为下图
fork()第一次循环产生2个,第二次循环就是第一次循环产生的2个接着fork()产生4个。。。如下图:
并不是顺序再加,而是创建一个新进程,就把它加到就绪队列,由于调度算法的影响,新创建的进程并不是严格按照次序执行
左图为fork()调用的函数
右图为fork()内部实现
当用户的所有程序都执行完毕,CPU也没闲着,它还在进行空闲进程的创建
5.2 进程加载和执行进程
exec():让当前程序执行新的程序
当执行新的程序后,所有的数据代码等都会变成新的存的那些内容
上图红色段:如果exex(“calc”,…)执行了,则代码段会被覆盖,就不会执行printf(“why would I execute?”)
fork()执行后可能会返回<0,=0,>0
由上图可知,fork()后,代码数据全都复制了一份,复制后的子进程pid不变,但是要执行的代码会变,变化代码部分可变下图:
实际内存中(执行fork()后,完全复制):
执行exec()后:
exec():加载不同的程序,执行新的程序,执行后,代码堆栈都会被覆盖
fork()要把代码堆栈内存什么的全复制一份,而执行exec()后会被覆盖,白复制了,如何优化?
- vfork():只复制一部分必要的,之后立即执行exec()
- 利用虚存,使用Copy on Write,写的时候再复制。当父进程创建子进程时,复制地址空间时,只复制父进程地址空间所必须的原数据(即页表,他们指向同一块地址空间),父进程或子进程对某一个地址单元进行写操作时,会触发异常,使得父进程或子进程要完成一个操作,把触发异常的页复制成2份,使得父进程子进程分别有不同的地址,实现按需写的复制,如果全都是只读,就没有必要复制。无论后面执不执行exec,fork()后还是一样的
5.3 等待和终止进程
父进程要等待子进程的结束?
当一个进程执行结束,会调用exit(),但是这个进程需要的所有资源不会全部回收,内存空间、打开的文件等在用户态的资源会被回收,但os内部代表当前进程存在的唯一标识——PCB(在内核中的资源)不会被回收,这个自身难以回收,只能借助父进程,子进程执行exit()后,通知在wait该子进程的父进程帮助子进程释放掉在内核的资源
子进程的用户资源被释放后,但内核资源未被释放前,处于僵尸状态,无法正常工作,只能等着被回收
父进程先于子进程退出(死亡),那子进程的该怎么回收?
这样会导致僵尸进程越来越多,进程都有父子关系,最先的进程,即祖先进程会定期扫描进程控制块的链表,如果有进程处于僵尸状态,则代替父进程完成回收资源的相应操作
exec():加载一个执行程序;运行一个执行程序。
加载时假定程序存在磁盘上,会把它从磁盘加载到内存,这个过程会比较长,会出现从running态到block态的转变