1. 进程
1.1 进程的定义
- 是程序的一次执行
- 或者说,进程是程序执行的1个实例
- 每个进程都有自己的地址空间
- 消耗计算机资源(CPU、IO等)
- 一个具有一定独立功能的程序在一个数据集合上的一次动态执行过程
为什么要引入进程
从操作系统的历史来看,最初的操作系统只能支持跑一个程序,随着后来的发展,CPU越来越强,内存越来越大,内存中存放的程序越来越多,此时,如果一个程序跑了多次,这么多个程序实例还用程序来表示显然是不合适的。
1.2 进程和程序的关系和差异
- 程序是进程的基础,进程是程序的功能体现
- 通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包含多个程序
- 程序是进程的静态实体(即执行代码)
- 进程是动态的,程序是静态的
- 程序是有序代码的集合,进程是程序的执行,进程有核心态和用户态(为什么有核心态:若进程向操作系统请求读写一个文件,此时操作系统代表进程在内核中执行,此时处于核心态)
- 进程是暂时的,程序是永久的:进程是一个状态变化的过程,程序可长久保存
- 进程与程序的作用不同:进程的组成包括程序、数据和进程控制快
- 一个程序可以对应多个进程,每启动一次就产生一个进程
例:
食谱=程序,
厨师=CPU,
原料=数据,
做蛋糕的=进程
进程可以被切换
1.3 进程的组成
- 程序的代码(放在内存中)
- 程序处理的数据(放在内存中)
- 程序计数器中的值,指示下一条将运行的指令
- 一组通用寄存器的当前值,堆、栈
- 一组系统资源(如打开的文件)
总之,进程包含了正在运行的一个程序的所有状态信息
1.4 进程特点
- 动态性:可动态的创建、结束进程
- 并发性:进程可以被独立调度并占用处理机处理
- 独立性:不同进程的工作不互相影响(用页表保障)
- 制约性:因访问共享数据/资源或进程间同步而产生制约
并发与并行
并发:只有1个CPU时,在某一时刻只有一个程序被执行,当需要执行其他程序时,可以进行切换,在一段短的时间内可以有多个程序被执行
并行:当有多个CPU时,在一个时刻可以有多个程序被执行
程序=算法+数据结构
描述进程的数据结构:进程控制块(Process Control Block,PCB)
操作系统为每个进程都维护了一个PCB,用来保存与该进程有关的各种状态信息
1.5 进程控制结构
- 进程控制块:操作系统管理控制进程运行所用的信息集合
操作系统用PCB来描述进程的基本情况以及运行变化的过程
PCB是进程存在的唯一标志
PCB伴随进程的一生,进程产生PCB即产生,进程消亡PCB即消亡 - 使用进程控制块
进程的创建:为该进程生成一个PCB
进程的终止:回收它的PCB
进程的组织管理:通过对PCB的组织管理来实现 - PCB包含三大类信息
- PCB的组织方式
进程个数较为固定,可以采用索引的方式,一般采用链表,便于删除和添加
2 进程状态
2.1 进程的生命期管理
- 进程创建
- 进程运行
(调度算法) - 进程等待
如等待读取文件、和其他进程协同,而且他进程还未结束、数据等其他资源 - 进程唤醒
- 进程结束
2.2 进程状态变化模型
进程的三种状态
进程在生命结束前处于且仅处于
三种基本状态之一
不同系统设置的进程状态数目不同
三状态图
五状态图
运行->就绪:存在多个就绪进程,为保证每个就续进程均有可能执行,给每个就绪进程均分配一个时间片,执行完即可被换下,改另一个就绪进行执行
不会持续很久,是一个很快的过程,因为只是完成了PCB的初始化
操作系统来完成
等待读写文件、资源
操作系统来完成状态的转变
2.3 进程挂起
挂起状态
- 阻塞挂起状态(Blocked_suspend):进程在外存等待某事件的出现
- 就绪挂起状态(Ready_suspend):进程在外存,但只要进入内存,即可运行
与挂起相关的状态转换
- 挂起(Suspend):把一个进程从内存转到外存;可能有以下几种情况:
- 阻塞→阻塞挂起:没有进程处于就绪状态或就绪进程要求更多内存资源时,发生这种转换,以提交新进程或运行就绪进程(腾出更多的内存空间给就绪状态/进程)
- 就绪→就绪挂起:当有高优先级阻塞(系统认为会很快就绪的)进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程
- 运行→就绪挂起:对抢占式系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程转到就绪挂起状态
- 在外存时的状态转换:
- 阻塞挂起到就续挂起:当有阻塞挂起进程因相关事件出现时,系统会把阻塞挂起进程转换为就绪挂起进程(还是在硬盘,只是状态变了)
问题:OS怎么通过PCB和定义的进程状态来管理PCB,帮助完成进程的调度过程?
如果事件1只能满足一个阻塞进程,则只把这一个进程的状态由阻塞变为就绪
若1能满足所有的,则把所有的状态都变为就绪
3 线程(Thread)管理
自从60年代提出进程概念以来,在操作系统中一直都是以进程作为独立运行的基本单位,直到80年代中期,人们又提出了更小的能独立运行的基本单位——线程
3.1 为什么使用线程
存在的问题:
- 播放出来的声音能否连贯?
Read()表示要从硬盘中读取文件,而硬盘读取速度较慢,就会导致后面的解压缩不能执行,即各个函数之间不是并发执行,影响资源的使用效率(I/O和CPU不能重叠,不能并发执行)
一开始多读几块数据。当read()被阻塞时,执行其他的进程,保证高效的播放
存在的问题:
1、进程间如何通信,共享数据?(进程1的数据状态如何传给进程2?)
2、维护进程的系统开销较大(进程切换需要保存和恢复,占用资源较多)
开销:创建进程时,分配资源、建立PCB;撤销进程时,回收资源、撤销PCB;进程切换时,保存当前进程的状态信息
这个实体就是线程!
3.2 什么是线程
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可以打断,因为它有中断)
- 由于时间片分配给进程,故与其他进程相比,在多线程执行时,每个线程得到的时间片较少,执行会较慢
3.3.2 内核线程
(典型的windows)
os管理调度的基本单位变为线程
所有线程的创建调度都是由os来完成的
进程主要实现的是管理资源
一个PCB管理一系列的TCB的list,就具体的调度由TCB完成
每一次线程切换,就要有一次从用户态到内核态的切换,开销较大
3.3.3 轻量级进程
它是内核支持的用户线程,一个进程可有一个或多个轻量级进程,每个量级进程由一个单独的内核线程来实现(Solaris/Linux)
3.4 多线程编程接口举例
4 上下文(context)切换
进程切换(将context保存到PCB中)
(由汇编来实现)
操作系统为活跃进程准备了进程控制块(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态的转变