进程
进程的概念
进程是一个具有一定独立功能的程序在一个数据集合上的一次动态执行的过程
这里特别注意程序和进程之间的关系:
- 程序=文件(静态的可执行文件)
- 进程=程序+执行过程
- 进程是程序的超集,还包括数据和进程控制块。
程序和进程之间的区别:
- 同一个程序的多次执行过程对应为不同进程
- 进程是动态的,程序是静态的
- 进程是暂时的,程序是永久的
进程的特征:
- 动态性:动态创建、结束
- 并发性:可以独立调度并占用处理器运行
- 独立性:不同的进程的工作互不影响
- 制约性:因访问共享数据、资源或进程间同步产生制约关系
共享和独立需要有一定限度,进程之间不仅需要去耦合,但不能忘记相互协作的初衷
执行进程需要内存和CPU共同工作:
- 内存负责保存代码和数据
- CPU负责执行指令
进程控制块 (PCB Process Control Block)
操作系统管理控制进程运行所用的信息集合 (操作系统用 PCB 来描述进程的基本情况以及运行变化的过程)
- PCB 是进程存在的唯一标识(每个进程都在操作系统中有一个对应的 PCB)
进程控制块存些什么?
- 进程标识信息 (PID)
- 处理机现场保存 (调度之前需要 保存上下文 保存其他寄存器 以便下次调度回来的时候使用)
- 进程控制信息 (调度优先级)
- 调度和状态信息 (调度进程和处理机使用情况)
- 进程间通信信息
- 存储管理信息 (指向进程映像存储空间数据结构 占用的存储空间用完以后还给操作系统)
- 进程所用资源 (进程使用的系统资源 打开文件)
- 有关数据结构连接信息 (与PCB相关的进程队列 不同的状态处于不同的进程队列)
**进程控制块的组织 (数据结构实现)**:
链表:各种状态形成不同的链表,多个状态对应多个不同的链表
索引表:同一个状态的进程归入一个索引表(由索引指向)
进程状态
- 进程创建 操作系统分配给进程所需要的资源 构建 PCB
- 系统初始化时
- 用户请求创建一个新进程
- 正在运行的进程执行了创建进程的系统调用
- 进程执行
- 进程创建完后 会放入就绪队列 等待 CPU调度
- 内核选择一个就绪的进程 让它占用处理机并执行
- 进程等待(阻塞) 进程等待是进程本身发起的 不是外部让它去等待的
- 请求并等待的系统服务 无法马上完成
- 启动某个操作 无法马上完成
- 需要的数据没有到达
- 进程抢占
- 高优先级进程就绪
- 进程执行时间片用完
- 进程唤醒 进程唤醒是由别的进程或操作系统唤醒 不是自己唤醒自己
- 被阻塞的进程需要的资源可被满足
- 被阻塞的进程等待的事件到达
- 进程结束 将进程所占用的资源还给操作系统
- 正常退出
- 错误退出
- 致命错误(强制性的)
- 被其他进程所杀(强制性的)
三状态进程模型
- 当程序被创建完成之后,一切就绪准备运行时,变为就绪状态
- 处于就绪状态的进程被进程调度程序选中后,就分配到处理器上来运行,进入运行状态。
- 处于运行状态的进程在其运行过程中,由于分配给它的时间片用完,让出处理器,返回就绪状态。
- 当进程请求某资源且必须等待时,进入等待状态。
- 进程要等待某时间到来时,它从阻塞状态变到就绪状态。
- 当进程表示它已经完成或者因出错,进程由运行过程退出。
进程切换
- 进程 1 执行 sleep() 进入 内核 内核调用函数设置定时器
- 进行调度 保存现场(到 PCB 中) 切换到 进程 2
- 进程 2 开始执行 若此时定时器时间到了 定时器产生 中断
- 中断服务例程 让 进程 2 暂停下来 并保护 进程 2 的现场 恢复 进程 1 的现场
- 让 进程 1 继续执行 进程 1 执行结束 再回到 操作系统
挂起进程模型
在上述的三状态模型的基础上,我们引入外存,增加一系列挂起状态,包括就绪挂起和等待挂起,从而进一步缓解内存的空间压力。
将优先级较低的的进程挂起,是通用的策略。
几个典型的状态变迁如下:
- 等待到等待挂起:没有进程出于就绪态,或就绪过程需要挤占更多内存
- 就绪到就绪挂起:高优先级等待进程遇到低优先级就绪进程,认为前者将更快就绪,所以后者就绪挂起
- 运行到就绪挂起:高优先级等待挂起因事件出现而进入就绪挂起。(我的理解是一种特殊的插队机制)
- 等待挂起到就绪挂起:当有等待挂起进程因相关事件出现而发生转换
- 就绪挂起到就绪:没有就绪进程或挂起就绪进程优先级高于就绪进程
- 等待挂起到等待:当一个进程释放足够内存,并有高优先级等待挂起进程
状态队列
由操作系统来维护一组队列 表示系统中所有进程的当前状态
- 不同队列表示不同状态
- 根据进程状态不同 进程PCB 加入相应队列
线程(Thread)
背景和需求
如果要实现可同步进行且相互通信的三个流程,用进程就不行了。
进程可以实现并行化,但是
- 进程间相对隔离的性质和这个目标相悖
- 使用进程的开销过大,需要各自开辟一块相类似的进程控制块
所以我们引入在同一个进程当中并行设计的线程机制。
线程的概念
线程是什么:
- 进程的一部分
- 描述指令流执行状态,是进程中指令执行流的最小单元
- 是CPU调度的基本单位。
- 线程之间可以并发执行,其之间共享相同的地址空间。
线程的优缺点:
线程 = 进程 - 共享资源 = 执行流
- 线程的优点
- 一个进程中可以有多个线程
- 各个线程之间可并发执行
- 各个线程之间可以共享地址空间和文件等资源
- 线程的缺点
- 一个线程崩溃 会导致其所属进程的所有线程崩溃
线程与进程的区别:
- 进程是资源分配单位,线程是CPU调度单位
- 进程拥有一个完整的资源平台,而线程只独享指令流执行的必要资源 (寄存器 栈)
- 线程能减少并发执行的时间和空间开销
- 线程的创建和结束时间比进程短,且同一进程内的线程切换时间比进程短
- 同一进程的 各线程间共享内存和文件资源,可不通过内核进行直接通信(省执行时间)
线程实现的三种方式
- 用户线程(在用户空间实现)
- 内核线程(在内核中实现)
- 轻权进程(LightWeight Process 在内核中实现 支持用户线程 结合了用户线程和内核线程的优点)
用户线程
用户线程的特征:
- 不依赖操作系统内核:内核不了解用户线程的存在;可用于不支持线程的OS
- 在用户空间实现线程机制:每个进程由私有的线程控制块(TCB)列表,TCB由线程库函数维护
- 同一进程内的用户线程切换速度快:无需OS内核特权级转换等等开销
- 允许每个进程拥有自己的线程调度算法:程序员可以根据实际情况设计更适合程序的调度
当然也有缺点:
- 线程发起系统调用而阻塞时,则整个进程进入等待。
- 由于不和内核作用,所以不支持基于线程的处理器抢占。
- 只能按进程分配CPU时间:多个线程的进程中,每个线程所能分到的时间片较少
上述的缺点都是由于用户态的设计不与内核作用的结果,一方面不依赖内核,但一方面也不能实现最好的优化。
内核线程
由于上述缺点,所以如果在内核当中实现线程将会更加合适。在内核中直接用PCB链接TCB并进行操作,就可以让处理器更加了解当前工作的线程机制。从而克服上述的问题。
内核线程的特征:
- 由内核来维护 PCB 和 TCB
- 线程执行系统调用而被阻塞 不影响 其他线程
- 线程的创建 终止 切换 开销大(因为要通过系统调用在内核中走一趟)
- 以线程为单位进行 CPU时间分配(多线程的进程可获得更多的CPU时间)
轻权进程(LightWeight Process)
Solaris提出的轻权进程能进一步解决内核线程开销大的问题,一个进程可有一个或多个轻权进程,每个轻权进程由一个单独的内核线程来支持。但后续由于过于复杂,轻权进程的实际表现并不理想。
- 永久绑定线程 可看作是 内核支持的线程
- 未绑定的轻权进程 可由用户态来给出一些策略 提高应用的效率;未绑定的轻权进程 就像用户线程实现的多线程 可以自己决定调度算法之类的
- 但同时 永久绑定线程 又是 内核原生就支持的线程 所以轻权进程 是融合了 内核线程和用户线程的优点
内核线程和用户线程和轻权进程的对应关系
1 | 一对一 可看作是 多进程多线程操作系统 |
进程控制
进程切换 (上下文切换)
暂停当前运行进程,从运行状态变为其他状态;调度另一个进程从就绪状态变成运行状态。
重要的上下文信息包括:
- 寄存器
- CPU状态
- 内存地址空间
进程切换的要求:
- 切换前,保存进程上下文
- 切换后,恢复进程上下文
- 要求快速切换
切换的模式图如下:
内核为每个进程维护了对应的进程控制块,内核将相同状态的进程的PCB放置在同一队列。
进程创建
- Windows进程创建Api CreateProcess()
- Unix进程创建系统调用 fork/exec
- fork() 把一个进程复制成两个进程 父子进程的PID不同
- exec() 用新程序来重写当前进程 PID 不变
进程复制 fork
1 | int pid = fork(); // 创建子进程 |
- fork() 创建一个继承的子进程
- 复制父进程的所有变量和内存
- 复制父进程的所有 CPU 寄存器(一个寄存器例外 是用来识别父进程和子进程的)
- fork() 的返回值
- 子进程的 fork() 返回值 为 0
- 父进程的 fork() 返回值为 子进程标识符
- 子进程可使用 getpid() 获取 PID
fork执行过程对于子进程而言,是在调用时刻对父进程地址空间的一次复制。fork得到的子进程和父进程只有上述的返回值不同。利用这个特点就可以进行多进程操作:
1 | main() |
这里是一个创建的实例:
1 | int main() |
这个代码开始运行之后,每一个现存的进程都会进行fork复制。如下:
进程加载与执行 exec
执行系统调用exec()时,进行程序加载操作。
- exec(),允许程序加载一个完全不同的程序,并从main开始执行(和操作系统启动时的思路类似)
- 运行进程加载时指定启动参数。(argc,argv)
- exec调用成功时,还是相同的进程,但是运行了不同的程序。
- 代码段、堆栈和堆都完全重写。
在99%的情况下,我们在调用fork()之后,我们都会使用系统调用exec(),加载新程序取代当前运行进程。
因而
- 在fork操作中内存复制是没有作用的
- 子进程将可能关闭打开的文件和链接
因而在创建进程时,可以不再创建一个同样的内存映像,将fork和exec结合起来,成为轻量级fork,接口为vfork()。现在vfork使用Copy on write技术实现。
进程等待与退出
进程等待:
wait() 系统调用用于父进程等待子进程的结束
- 子进程结束时通过 exit() 向父进程返回一个值
- 父进程通过 wait() 接受并处理返回值
wait()系统调用的功能:
当父进程先 wait() 子进程后 exit() 时
- 父进程进入等待状态,等待子进程的返回结果
- 当某子进程调用 exit() 时 唤醒父进程,将 exit() 返回值作为 父进程 wait() 的返回值
当子进程先 exit() 父进程后 wait() 时
- 说明有僵尸子进程等待 ,wait() 立即返回其中一个值
当 无子进程存活 而 父进程 wait() 时
- wait() 立即返回
进程退出:
进程结束执行时 调用 exit() 完成进程资源回收
- exit() 系统调用的功能
- 将调用参数作为进程的 结果(返回值)
- 关闭所有打开的文件等占用资源
- 释放内存
- 释放大部分进程相关的内核数据结构
- 检查父进程是否还存活
- 存活 保留结果的值 直到父进程需要它 进入 僵尸(zombie/defunct)状态
- 非存活 释放所有的数据结构和结果
- 清理所有等待的僵尸进程
因此,进程终止是最终的垃圾收集(资源回收)
进程控制与进程状态关系
其他进程控制系统调用
- 优先级控制
- nice() 指定进程的初始优先级
- Unix系统中 进程优先级会随着执行时间而衰减
- 进程调试支持
- ptrace() 允许一个进程控制另一个进程的执行
- 设置断点和查看寄存器等
- 定时
- sleep() 可以让进程在定时器的等待队列中等待指定的时间