1 进程概述
进程: 一个具有一定的功能的程序在一个数据集合上的一次动态执行过程.
1.1 进程组成
一个进程应该包括:
- 程序的代码
- 程序处理的数据
- 程序计数器中的值, 指使下一条将运行的指令
- 一组通用的寄存器的当前值, 堆, 栈
- 一组系统资源(如打开的文件)
总之, 进程包含了正在运行的一个程序的所有状态信息.
1.2 进程和程序的联系
- 程序是产生进程的基础
- 程序的每次运行构成不同的进程
- 进程是程序功能的体现
- 通过多次执行, 一个程序可对应多个进程, 通过调用关系, 一个进程可包括多个程序.
进程和程序的区别:
- 进程是动态的, 程序是静态的, 程序时有序代码的集合, 进程是程序的执行, 进程有核心态/用户态.
- 进程是暂时的, 程序是永久的, 进程是一个状态变化的过程, 程序可长久保存
- 进程与程序的组成不同, 进程的组成包括程序, 数据和进程控制块(如进程状态信息)
1.3 进程的特点
- 动态性: 可动态地创建, 结束进程
- 并发性: 进程可以被独立调度并占用处理器运行: 并发并行.
- 独立性: 不同进程的工作不相互影响.
- 制约性: 因访问共享数据/资源或进程间同步而产生制约
1.4 进程控制结构
进程控制块: 操作系统管理控制进程运行所用的信息集合. 操作系统用PCB(process control block)来描述进程的基本情况以及运行变化的过程, PCB是进程存在的唯一标志.
进程控制块的使用:
- 进程的创建: 为该进程生成一个PCB
- 进程的终止: 回收它的PCB
- 进程的组织管理: 通过对PCB的组织管理来实现
PCB含有一下三大类信息:
1. 进程标识信息. 如本进程的标识, 本进程的产生者标识(父进程标识), 用户标识.
2. 处理器状态信息保存区. 保存进程的运行现场信息:
- 用户可见寄存器, 用户程序可以使用的数据, 地址等寄存器.
- 控制和状态寄存器, 如程序计数器(PC), 程序状态字(PSW).
- 栈指针, 过程调用/系统调用/中断处理和返回时都需要用到它.
3. 进程控制信息:
- 调度和状态信息, 用于操作系统调度进程并占用处理器使用.
- 进程间通信信息, 为支持进程间的与通信相关的各种标识, 信号, 信件等, 这些信息存在接收方的进程控制块中.
- 存储管理信息, 包含有指向本进程映像存储空间的数据结构.
- 进程所用资源, 说明由进程打开, 使用的系统资源, 如打开的文件等.
- 有关数据结构连接信息, 进程可以连接到一个进程队列中, 或者连接到相关的其他进程的PCB.
PCB的组织方式:
链表: 同一状态的进程其PCB成一链表, 多个状态对应多个不同的链表. 各状态的进程形成不同的链表: 就绪链表, 阻塞链表.
索引表: 同一状态的进程归入一个index表(由index指向PCB), 多个状态对应多个不同的index表. 各状态的进程形成不同的索引表: 就绪索引表, 阻塞索引表.
2 进程状态
2.1 进程的生命周期管理
2.1.1 进程创建
引起进程创建的三个主要条件:
- 系统初始化时
- 用户请求创建一个新进程
- 正在运行的进程执行了创建进程的系统调用
2.1.2 进程运行
内核选择一个就绪的进程, 让它占用处理器并执行
2.1.3 进程等待
在以下情况下, 进程等待(阻塞):
- 请求并等待系统服务, 无法马上完成
- 启动某种操作, 无法马上完成
- 需要的数据没有到达
进程只能自己阻塞自己, 因为只有进程自身才知道何时需要等待某种事件发生.
2.1.4 进程唤醒
唤醒进程的原因:
- 被阻塞进程需要的资源可被满足
- 被阻塞进程等待的事件到达
- 将该进程的PCB插入到就绪队列
进程只能被别的进程或操作系统唤醒.
2.1.5 进程结束
在以下四种情形下, 进程结束:
- 正常退出(自愿的)
- 错误退出(自愿的)
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
2.2 进程状态变化模型
进程的三种基本状态:
进程在生命周期结束前处于且仅处于三种基本状态之一, 不同系统设置的进程状态数目不同:
- 运行状态: 当一个进程正在处理器上运行时.
- 就绪状态: 一个进程获得了除处理器之外的一切所需资源, 一旦得到处理器即可运行.
- 等待状态: 一个进程正在等待某一事件而暂停运行时. 如等待某资源, 等待输入/输出完成.
进程其他的基本状态:
- 创建状态: 一个进程正在被创建, 还没被转到就绪状态之前的状态.
- 结束状态: 一个进程正在从系统中消失时的状态, 这是因为进程结束或由于其他原因所导致.
状态变化:
- NULL->New: 一个新进程被产生出来执行一个程序.
- New->Ready: 当进程被创建完成并初始化后, 一切就绪准备运行时, 变为就绪状态.
- Ready->Running: 处于就绪状态的进程被进程调度程序选中后, 就分配到处理器上来运行.
- Runing->Exit: 当进程表示它已经完成或者因出错, 当前运行进程会由操作系统结束处理.
- Runing->Ready: 处于运行状态的进程在其运行过程中, 由于分配给它的处理器时间片用完而让出处理器.
- Runing->Blocked: 当进程请求某样东西且必须等待时.
- Blocked->Ready: 当进程要等待的某事件到来时, 它从阻塞状态变到就绪状态.
2.3 进程挂起模型
进程在挂起状态时, 意味着进程没有占用内存空间, 处在挂起状态的进程映像在磁盘上.
2.3.1 进程的挂起状态
挂起状态有两种:
- 阻塞挂起状态: 进程在外存并等待某事件的出现.
- 就绪挂起状态: 进程在外存, 但只要进入内存, 即可运行.
2.3.2 挂起相关的状态转换
把一个进程从内存转到外存(挂起), 可能有以下几种情况:
- 阻塞到阻塞挂起: 没有进程处于就绪状态或就绪进程进程要求更多内存资源时, 会进行这种转换, 以提交新进程或进行就绪进程.
- 就绪到就绪挂起: 当有高优先级阻塞(系统认为会很快就绪的)进程和低优先就绪进程时, 系统会选择挂起低优先级就绪进程.
- 运行到就绪挂起: 对抢先式分时系统, 当有高优先级阻塞挂起进程因事件出现而进入就绪状态时, 系统可能会把运行进程转到就绪挂起状态.
在外存时的状态转换:
- 阻塞挂起到就绪挂起: 当有阻塞挂起进程因相关事件出现时, 系统会把阻塞挂起进程转换为就绪挂起进程.
解挂/激活(把一个进程从外存转到内存)的情况:
- 就绪挂起到就绪: 没有就绪进程或挂起进程优先级高于就绪进程时, 会进行这种转换.
- 阻塞挂起到阻塞: 当一个进程释放足够内存时, 系统会把一个高优先级阻塞挂起(系统认为会很快出现所等待的事件)进程转换为阻塞进程.
2.4 状态队列
- 由操作系统来维护一组队列, 用来表示系统当中所有进程的当前状态
- 不同的状态分别用不同的队列来表示(就绪队列, 各种类型的阻塞队列)
- 每个进程的PCB都根据他的状态加入到相应的队列当中, 当一个进程的状态发生变化时, 它的PCB从一个状态队列中脱离出来, 加入到另外一个状态队列.
(此处为个人理解, 应该只是PCB数据结构的指针进行各个队列的出队/入队.)
3 线程
3.1 线程的使用意义
需要提出一种新的实体, 满足以下特性:
- 实体之间可以并发地执行
- 实体之间共享相同的地址空间
3.2 线程
3.2.1 线程特性
线程可以定义为: 进程当中的一条执行流程.
从两个方面来重新理解进程:
- 从资源组合的角度: 进程把一组相关的资源组合起来, 构成了一个资源平台(环境), 包括地址空间(代码段, 数据段), 打开的文件等各种资源.
- 从运行的角度: 代码在这个资源平台上的一条执行流程(线程).
线程的优点:
- 一个进程中可以同时存在多个线程
- 各个线程之间可以并发地执行
- 各个线程之间可以共享地址空间和文件等资源
线程的缺点:
- 一个线程崩溃, 会导致其所属进程的所有线程崩溃.
3.2.2 不同操作系统对线程的支持
3.2.3 线程所需的资源
3.2.4 线程与进程的比较
- 进程是资源分配单位, 线程是CPU调度单位
- 进程拥有一个完整的资源平台, 而线程只独享必不可少的资源, 如寄存器和栈
- 线程同样具有就绪, 阻塞和执行三种基本状态, 同样具有状态之间的转换关系
- 线程能减少并发执行的时间和空间开销:
- 线程的创建时间比进程短
- 线程的终止时间比进程短
- 同一进程内的线程切换时间比进程短
- 由于同一进程的各线程见共享内存和文件资源, 可直接进行不通过内核的通信.
3.3 线程的实现
主要有三种线程的实现方式:
- 用户线程: 在用户空间实现
- 内核线程: 在内核中实现
- 轻量级进程: 在内核中实现, 支持用户线程
用户线程和内核线程的对应关系:
- 多对一
- 一对一
- 多对多
3.3.1 用户线程
在用户空间实现的线程机制, 它不依赖于操作系统的内核, 由一组用户级的线程库函数来完成线程的管理, 包括进程的创建, 终止, 同步和调度等.
- 由于用户线程的维护由相应进程来完成(通过线程库函数), 不需要操作系统内核了解用户线程的存在, 可用于不支持线程技术的多进程操作系统.
- 每个进程都需要它私有的线程控制块(TCB)列表, 用来跟踪记录它的各个线程的状态信息(PC, 栈指针, 寄存器), TCB由线程库函数来维护.
- 用户线程的切换也是由线程库函数来完成, 无需用户态/核心态切换, 所以速度特别快.
- 允许每个进程拥有自定义的线程调度算法.
用户线程的缺点:
- 阻塞性的系统调用如何实现? 如果一个线程发起系统调用而阻塞, 则整个进程在等待.
- 当一个线程开始运行后, 除非它主动地交出CPU的使用权, 否则它所在的进程当中的其他线程将无法运行.
- 由于时间片分配给进程, 故与其他进程比, 在多线程执行时, 每个线程得到的时间片较少, 执行会较慢.
3.3.2 内核线程
是指在操作系统的内核当中实现的一种线程机制, 由操作系统的内核来完成线程的创建, 终止和管理.
- 在支持内核线程的操作系统中, 由内核来维护进程和线程的上下文信息(PCB和TCB)
- 线程的创建, 终止和切换都是通过系统调用/内核函数的方式来进行, 由内核来完成. 因此系统开销较大.
- 在一个进程当中, 如果某个内核线程发起系统调用而被阻塞, 并不会影响其他内核线程的运行.
- 时间片分给线程, 多线程的进程获得更多CPU时间
- Windows NT和Windows 2000/XP支持内核线程.
3.3.3 轻量级进程
它是内核支持的用户线程, 一个进程可有一个或者多个轻量级进程, 每个轻量级进程由一个单独的内核线程来支持.(Solaris/Linux)
3.4 多线程编程接口举例
4 进程控制
4.1 进程切换
停止当前运行进程(从运行状态改变成其他状态)并且调度其他进程(转变成运行状态)
- 必须在切换之前存储许多部分的进程上下文
- 必须能够在之后恢复他们, 所以进程不能显示它曾经被暂停过
- 必须快速(上下文转换是非常频繁的)
需要存储的上下文:
- 寄存器(PC, SP, ..), CPU状态, ...
- 一些时候可能会费时, 所以我们应该尽可能避免
进程控制块PCB: 内核的进程状态记录
- 内核为每个进程维护了对应的进程控制块
- 内核将相同状态的进程的PCB放置在同一个队列(就绪队列, I/O等待队列--每一个设备一个队列, 僵尸队列等)
4.2 进程创建
进程创建是操作系统提供给用户使用的系统调用, 完成新进程的创建工作.
不同系统的进程创建API不同, 如下:
- Windows进程创建API: CreateProcess(filename)
- Unix/Linux进程创建系统调用: fork/exec, 其中fork完成把一个进程复制成两个进程, 两个进程只有进程ID不同(PID), 复制完成后exec把新程序加载进来重写当前进程(PID没有改变)
Unix/Linux进程创建系统调用示例如下:
int pid = fork(); //创建子进程//父进程执行完这一行之后, 创建出来的子进程和本身的父进程都会//继续向下执行, 只不过此时子进程的pid是0, 父进程返回的是子//的pid if (pid == 0) {//此时说明是子进程, 就掉用exec执行想要执行的文件 }
fork()创建一个继承的子进程:
- 复制父进程的所有变量和内存
- 复制父进程的所有CPU寄存器(有一个寄存器例外)
fork()的地址空间复制:
- fork()执行过程对于子进程而言, 是在调用时间对父进程地址空间的一次复制(对于父进程fork()返回child PID, 对于子进程返回值为0).
- 系统调用exec()加载新程序取代当前运行进程.
4.3 进程加载
在Linux中会调用exec来加载新程序取代当前运行进程:
- exec()调用允许一个进程"加载"一个不同的程序并且在main开始执行(事实上 _start)
- 它允许一个进程指定参数的数量(argc: argument count)和它字符串参数数组(argv).
- 如果调用成功, 进程PID还是原来的PID, 但是运行的程序改变了(原来运行的是父进程程序, 调用后运行加载的程序)
- 其代码段, stack, heap都会重写
fork()的优化:
首先由前面已知, fork()的简单实现:
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程里
- 开销昂贵
但是, 在99%的情况里, 我们在调用fork()之后会继续调用exec(), 也就意味着:
- 在fork()操作中内存复制是没有作用的
- 子进程将可能关闭打开的文件和链接
- 开销因此是高的
- 为什么不能结合它们在一个调用中
vfork() --- 早期Unix系统提供
- 一个创建进程的系统调用, 不需要创建一个同样的内存映像
- 一些时候称为轻量级fork()
- 子进程应该几乎立即调用exec()
- 现在不再使用如果我们使用Copy on Write(COW)技术
4.4 进程的等待与退出
wait()系统调用是被父进程用来等待子进程的结束.
当一个进程结束之后, 一些资源(比如PCB等)是无法进行自我回收的.
一个子进程向父进程返回一个值, 所以父进程必须接受这个值并处理, wait()系统调用担任这个要求
- 它使父进程去睡眠来等待子进程的结束
- 当一个子进程调用exit()的时候, 操作系统解锁父进程, 并且将通过exit()传递得到的返回值作为wait调用的一个结果(连同子进程的pid一起), 如果这里没有子进程存在, wait()立即返回.
- 当然, 如果这里有为父进程的僵尸等待, wait()立即返回其中一个值(并且解除僵尸状态).
具体步骤如下:
- 进程结束执行之后, 它调用exit() -- 系统调用
- 这个系统调用:
- 将这个程序的"结果"作为一个参数
- 关闭所有打开文件, 连接等等
- 释放内存
- 释放大部分支持进程的操作系统结构
- 检查是否父进程是存活着的, 如果存活, 它保留结果的值直到父进程需要它: 在这种情况里, 进程没有真正死亡, 但是它进入了僵尸状态. 如果父进程没有存活 它释放所有的数据结构, 这个进程死亡.
- 清理所有等待的僵尸进程
- 进程终止是最终的垃圾收集(资源回收)
注意:
操作系统根进程(初始进程)会定期扫描僵尸进程, 并代替僵尸进程的父进程对其进行释放.