⛈️ 进程管理

吞佛童子2022年10月10日
  • os
  • 进程管理
大约 20 分钟

⛈️ 进程管理

1. 进程的状态

  • 运行中的程序,就是一个进程
  • 资源分配 & 调度的基本单位

状态转移

1) 创建 new

  1. 创建流程
    • 为新进程分配唯一进程标识号 & 空白的 PCB
    • 为新进程分配所需资源,例如内存资源
    • 初始化 PCB
      • 填入 进程标识号
      • 填入 CPU 相关信息
      • 填入 进程调度相关信息
    • 将新进程插入就绪队列,插入成功则处于就绪状态
  • 允许一个进程创建另外一个进程
  • 子进程继承父进程的所有资源;当子进程被终止时,继承的资源会归还给父进程;父进程终止时会终止所有子进程

2) 就绪 ready

  • 获得 CPU 时间片即可进入运行态

3) 运行 running

  • 获得 CPU 时间片

4) 阻塞 blocked

  1. 需要等待某个事件完成,此时即使给该进程时间片,也无法运行
  2. 一旦进入阻塞状态,必须由某个事件进程进行唤醒
  3. 发生场景
    • 请求共享资源失败,系统没有足够的资源可供分配
    • 等待某种操作完成
    • 新数据尚未到达
    • 等待新任务
  4. 阻塞流程
    • 找到该进程对应的 PCB
    • 将该进程由运行状态转换为阻塞状态
    • 将该进程的 PCB 插入到阻塞队列中
  5. 唤醒流程
    • 从阻塞队列中找到该进程对应的 PCB
    • 将该进程由阻塞状态转化为就绪状态
    • 将该 PCB 从阻塞队列中放入就绪队列

5) 结束 exit

  • 3 种结束方式
    • 正常结束
    • 异常结束
      • 越界错误
        • 访问超出该进程的存储区域
      • 保护错误
        • 访问了不被允许访问的区域
      • 非法指令
        • 使用了不存在的指令
      • 特权指令错误
        • 用户进程下指令了内核态才能执行的指令
      • 运行超时
        • 执行时间超过指定时间
      • 等待超时
        • 等待时间超过指定时间
      • 算法运算错误
        • 例如,除 0 操作
      • IO 故障
    • 外界干预
      • 操作员 | 操作系统干预
        • 例,kill 指令杀死进程
      • 父进程终止,所有子进程都会被结束
  • 结束流程
    • 查找该进程对应的 PCB,读取 PCB 中的进程状态
    • 若处于执行状态,终止该进程的执行,并将其拥有的 CPU 资源分配给其他进程
    • 若还有子进程,终止所有子进程的执行
    • 将该进程的所有资源归还给 父进程 | 操作系统
    • 将 PCB 从所在队列中移除

6) 就绪挂起

  • 进程被写入硬盘,一旦进入内存,即可进入就绪状态

7) 阻塞挂起

  • 进程被写入硬盘,并等待某个时间出现

挂起

  • 当大量进程均处于阻塞态时,由于该状态下进程需要等待某个事件完成才可继续执行
  • 因此若不将这部分进程换出到磁盘,则会占用大量物理内存
  • 因此采取挂起状态,用来描述一个进程没有存在于物理内存中,而是在磁盘等待调用
  • 挂起的几种情况:
    • 进程不占用物理内存
    • 调用 sleep() 函数
    • LinuxCtrl + Z 被用户挂起进程

2. PCB

1) 是什么

  1. PCB [进程控制块]
    • 是一种数据结构,是进程存在的唯一标识,为了描述 & 控制进程的执行,属于进程实体的一部分
  2. 作用
    • 标识一个进程
    • 提供进程调度相关信息
    • 提供进程所用资源相关信息
    • 提供进程 CPU 占用相关信息

2) 含有哪些信息

  1. 标识符
    • 进程标识符
      • 标识进程,每个进程都有一个 & 唯一的标识符
    • 用户标识符
      • 标识拥有该进程的用户,往往是用户进程在访问该进程时使用
  2. 进程调度信息
    • 进程当前状态
      • new ready running blocked exit
    • 进程优先级
      • 进程抢占 CPU 时的优先级
    • 与进程调度算法有关的信息
      • 进程已经等待 CPU 的时间总和
      • 进程已经执行的时间总和
    • 事件
      • 进程由 running 转换为 blocked 所等待的事件
  3. 资源信息
    • 占用的虚地址空间
    • 打开的文件列表
    • 使用的 IO 设备
  4. CPU 信息
    • 各个寄存器的值
      • 通用寄存器
      • 指令计数器
      • 程序状态字 PSW
      • 用户栈指针

3) 管理方式

  1. 一个 PCB 为一个数据结构,多个 PCB 如何进行管理?
    • 线性方式
      • 所有 PCB 保存在一张线性表中,将该线性表放在内存的一个专用区域
      • 实现简单,但是每次找某个 PCB 时,需要线性扫描该表
      • 适用于进程数目不多的系统
    • 索引方式
      • 将同一状态的进程存在一张索引表中,索引项指向对应的 PCB
    • 链表方式
      • 将同一状态的 PCB 连接在一起,形成队列,例如就绪队列、阻塞队列等
      • 由于进程的创建、销毁、调度比较频繁,因此多采用该方式

3. 上下文切换

1) CPU 上下文切换

  • 保护哪些现场?
    • CPU 寄存器的值
    • 程序计数器的值
  • 什么情况下发生 CPU 的上下文切换?
    • 进程切换
    • 线程切换
    • 中断切换

2) 进程上下文切换

  • 保护哪些现场?
    • 用户空间资源
      • 虚拟内存
      • 全局变量
    • 内核空间资源
      • 内核堆栈
      • 寄存器
  • 发生在什么态下?
    • 进程由内核管理 & 调度,因此进程切换只能发生在内核态
  • 什么情况下会发生进程的上下文切换?
    • 该进程的时间片使用完
    • 进程调用 sleep() 函数
    • 系统资源不足,例如内存不足
    • 有更高优先级的进程运行
    • 发生硬件中断

3) 线程上下文切换

  • 保护哪些现场?
    • 当 2 个线程处于不同进程时,同进程的 保护现场 & 恢复现场
    • 当 2 个线程处于同一进程时,
      • 程序计数器
      • 虚拟机栈
      • 本地方法栈

4. 进程的调度

1) 是什么

  • 实现进程状态的切换
  • 主要分为 2 大类:
    • 抢占式调度
      • 每个进程只运行某段时间,当时间到达后,若进程仍在运行,则将其挂起,
      • 调度程序从就绪队列中挑选另外一个进程,让其运行,如此循环
    • 非抢占式调度
      • 每个进程只要让其开始运行,则直到其阻塞 | 结束,才会切换下一个进程

2) 调度原则

  1. CPU 利用率
    • CPU 利用率 = CPU 忙碌时间 / 总时间
    • 应尽可能使 CPU 利用率高
  2. 系统吞吐量
    • 吞吐量 = 单位时间内完成的进程数
    • 进程完成耗时越少,吞吐量越高
  3. 等待时间
    • 进程处于等待队列中的时间
    • 等待时间越短越好
  4. 响应时间
    • 用户第一次提交请求,到产生第一次响应的时间
    • 多用于交互系统
  5. 周转时间
    • 进程等待时间 + 运行时间 + 阻塞时间总和 == 进程完成时间 - 进程提交时间
    • 周转时间越短越好

3) 常见调度算法

  1. 先来先服务
    • 非抢占式
    • 每次从就绪队列中选取最先进入队列的进程让其运行,直到其阻塞 | 退出
    • 若某个进程运行时间过长,将导致后面的进程等待时间过长,不利于短作业,适用于全是长作业的任务
  2. 最短作业优先
    • 非抢占式
    • 每次从就绪队列中优先选取运行时间最短的进程让其运行,直到其阻塞 | 退出
    • 适用于全是短作业的任务,若某个进程运行时间长,则一直得不到运行,不利于存在长作业的任务
  3. 最短剩余时间优先
    • 抢占式
    • 当一个新就绪的进程比当前运行进程拥有更短的运行时间时,系统挂起当前进程,让这个新的进程运行
    • 适用于全是短作业的任务,不适用于存在长作业的任务
  4. 高响应比优先
    • 非抢占式
    • 响应比 = (进程等待时间 + 进程运行时间) / 进程运行时间
    • 每次从就绪队列中优先选取响应比高的进程让其运行,直到其 阻塞 | 退出
    • 兼顾了短作业 & 长作业
      • 当进程等待时间相同时,运行时间短的进程优先运行,利于短作业的优先进行
      • 当进程运行时间相同时,等待时间长的进程优先运行,长作业由于一直被等待,等待时间变长,将会得到更大的几率被运行,利于长作业的运行
  5. 时间片轮转调度
    • 抢占式调度
    • 每个进程均被要求运行相同的时间,当时间片耗尽时,将发生进程的切换
    • 时间片的选取:
      • 一般 20 ms - 50 ms
      • 时间片太短,将导致频繁的进程上下文切换,降低 CPU 效率
      • 时间片太长,将导致短作业进程的响应时间变长
  6. 最高优先级调度
    • 抢占式 & 非抢占式
    • 每次从就绪队列中优先选取优先级高的进程让其运行
    • 优先级是否固定?
      • 静态优先级
        • 进程优先级在进程创建时已经确定,直到结束都不能改变
      • 动态优先级
        • 进程优先级可以随着时间的推移不断提高
    • 将导致低优先级的进程迟迟无法运行
  7. 多级反馈队列调度
    • 抢占式
    • 设置多个队列,每个队列有不同的优先级,优先级越高的队列时间片越短
    • 优先执行高优先级队列中的进程,当高优先级队列中没有进程时,执行次优先级队列中的进程,以此类推
    • 当一个新的进程加入时,会被放入最高优先级队列的队尾
      • 若最高优先级队列中前面还有进程,则等待前面进程运行
      • 若最高优先级队列中不存在进程,当前运行的进程非最高优先级,则将当前运行的进程停止运行,并放入所在优先级队列的队尾,然后运行新加入的拥有最高优先级的进程
    • 若在时间片内进程没有执行完成,则会移到低一级的队列末尾
    • 利于短作业,因为短作业可以在高优先级快速完成;利于长作业,因为越移到次优先级队列,当被执行时,分配到的时间片越长,越容易完成

5. 进程的通信

  • 每个进程的用户地址空间是独立的,不可以直接访问;而内核空间是所有进程共享的,因此进程之间的通信必须通过内核

1) 管道

  1. 如何使用
    • 匿名管道
      • Linux 中的 | 就是匿名管道,意思是 将前面指令的输出作为后一个指令的输入
    • 命名管道
      • 首先创建一个管道 mkfifo myPipe
      • 向管道中写入信息 echo "liuxianzhishou" > myPipe
        • echo 命令用于字符串的输出
      • 从管道中读取信息并打印 cat < myPipe
  2. 特点
    • 简单,存储的是无格式的字节流数据
    • 数据传输为单向,遵循 FIFO 原则
    • 通信效率低,不适合进程间的频繁数据交换
    • 大小受限
    • 生命周期随进程的创建而建立,随进程的结束而销毁
  3. 匿名管道底层原理
    • 依赖于 int pipe(int fd[2]) 函数
      • fd[0] 读取端描述符
      • fd[1] 写入端描述符
    • 本质:
      • 内核中创建一块内存作为缓存
    • 如何实现进程间的通信?
      • fork 一个子进程,创建的子进程会复制父进程的文件描述符,这样就使得父子进程各有自己的 fd[0] & fd[1]
      • 当父子进程需要通信时,只需要父进程写入管道,子进程就可从管道中读取信息
    • Linux 中 | 的底层原理
      • 通过 shell fork 多个子进程,来实现子进程间的通信,子进程间不存在父子关系
  4. 命名管道底层原理
    • 提前创建一个管道,可以实现多个不相关的子进程间的通信

父子进程单向通信

Shell 中 A | B 的实现

2) 消息队列

  1. 特点
    • 保存在内核中的消息链表
    • 双方约定好消息体进程发送和接收,存储方式为约定好的消息体的大小,一个个存放
    • 生命周期与内核有关,只要没有关闭操作系统 && 没有释放掉该消息队列,那么该消息队列始终存在
  2. 优点
    • 效率提高
  3. 缺点
    • 消息队列存放于内核,每个消息体大小受限制 && 一个消息队列中消息体总个数受限制,不适合大数据的传输
    • 消息队列存放于内核,写入消息时,需要从用户态切换到内核态;接收消息时,也需要从内核态读取,来回切换开销大

3) 共享内存

  1. 实现原理
    • 不同进程中,有一块特殊的虚拟内存,使得不同进程的虚拟内存最终可以映射到同一块物理内存
  2. 特点
    • 解决消息队列中用户态 & 内核态来回切换的问题,一个进程写入,另一个进程读取,可以立马看到该块物理内存数据的变动
    • 多进程并发下存在数据安全问题,可能一个进程改动了数据,另一个进程还未读取到,就被其他的进程覆盖了新数据

4) 信号量

  1. 作用
    • 解决共享内存多进程竞争共享数据造成的安全问题
    • 保证共享资源的数据安全,任意时刻只能有 一个 | 特定几个 进程对共享资源进行操作
    • 实际上是用于进程间的互斥 & 同步,而非用于传输相关通信数据,从而实现进程的通信,但是信号量也属于进程通信需要考虑的一个点
  2. 底层数据结构
    • 信号量 [Semaphore] 是一个整型的计数器,可 ++ | --
  3. 2 种原子操作
    • P
      • 进程在操作共享资源之前,将 s --
      • s < 0 说明资源被占用,进程阻塞等待
      • s >= 0 说明资源可以被本进程访问,正常执行
    • V
      • 进程在离开共享资源之后,将 s ++
      • s <= 0 说明当前存在阻塞进程,唤醒阻塞进程
      • s > 0 说明当前不存在阻塞进程,继续执行

5) 信号

  • 作用于进程异常状态
  • 进程通信中唯一的 异步通信 机制
  • Linux 常用信号
    • 硬件方式
      • Ctrl + C 产生 SIGINT 信号,强制终止该进程
      • Ctrl + Z 产生 SIGSTOP 信号,暂停进程并放入后台
    • 软件方式
      • kill -9 pid 给某个进程发送 SIGKILL 信号,终止该进程
  • 对信号的响应方式
    • 执行默认操作
      • Linux 中为众多信号配置专门的处理操作
    • 捕捉信号
      • 可以为信号自定义信号处理函数,当该信号产生时,会进入对应的信号处理函数中
    • 忽略信号
      • 当不希望处理某些信号时,可以对改信号进行忽略,不作处理
      • SIGSTOP & SIGKILL 无法被忽略

6) socket 编程

  1. 作用
    • 可实现不同主机间的进程通信
    • 当然同主机不同进程也可以实现通信
  2. 底层原理
// domain 协议族 - [IPv 4, IPv 6, 本机]
// type 通信类型 - [基于字节流 (tcp), 基于数据报 (udp), 原始套接字]
// protocal 基本废弃 - 0
int socket(int domain, int type, int protocal)
  1. 3 种形式
    • TCP
    • UDP
    • 本机通信
      • 调用 bind() 时,无需绑定 ip & port,只需要绑定一个本地文件
      • type 参数支持 基于字节流 & 基于数据报

6. 线程

1) 是什么

  1. 线程是比进程更小的单位,是 CPU 运行的基本单位
    • 一个进程中可以有多个线程
    • 线程间共享 Java 的 堆 & 方法区
    • 线程私有程序计数器、操作数栈、本地方法栈
  2. 优点:
    • 线程创建、切换开销比进程小
    • 可实现多线程并发
  3. 缺点:
    • 当进程中的一个线程崩溃时,会导致该进程的其他线程也崩溃[针对 C/C ++,不包含 Java]

2) 进程 VS 线程

  1. 相同点
    • 均具有 创建、运行、阻塞、终止等基本状态,也具有相应状态转换
    • 均有不同进程、线程之间私有的部分
  2. 不同点
    • 进程是资源调度 & 分配的基本单位;线程是 CPU 调度的基本单位
    • 进程之间资源完全隔离;线程之间除了私有部分,堆 & 方法区是共享的
    • 进程占有的页表不同;线程共享同一份页表 & 同一份虚拟内存
    • 进程间数据传递需要经过内核;线程间的数据传递可以通过线程共享区
    • 进程切换由内核态管理;线程切换取决于线程的实现方式
    • 进程创建、切换、结束开销较大;线程创建、切换、结束开销较小
    • 一个进程可以拥有多个线程;线程不能独立于进程而存在

3) 如何实现

  1. 用户态线程
    • img_9.png
    • 在用户空间实现,不经过内核,通过用户态的线程库实现线程的创建 & 管理等,线程对应的 PCB 在用户态实现
    • 一个内核线程 对应 多个用户线程
    • 优点:
      • 通过用户态的线程库函数实现线程的切换,无需经过用户态 & 内核态的切换,切换速度快
    • 缺点:
      • 操作系统将多个用户线程当做一个进程,当某个用户线程阻塞时,该进程下的所有线程均被阻塞
      • 操作系统将多个用户线程当做一个进程,当多线程执行时,每个线程得到的时间片比其他进程更短
      • 用户态线程无法中断当前运行中的线程,只有操作系统才有这个权限,因此,当某个线程开始运行后,除非自己交出 CPU 的使用权,否则其他用户线程无法运行
  2. 内核态线程
    • img_10.png
    • 在内核空间实现,有内核管理,线程对应的 PCB 在内核态
    • 一个内核线程 对应 一个用户线程
    • 优点:
      • 在一个进程中,当某个线程阻塞时,不会影响其他内核线程的运行
      • 一个线程对应一个时间片
    • 缺点:
      • 线程调度开销大,线程创建、切换、终止均需要通过系统调用切换到内核态执行
      • 内核线程创建的数量有限
  3. 轻量级进程
    • img_11.png
    • 内核支持用户线程,是基于内核线程的高级抽象
    • 一个进程可拥有多个轻量级进程,一个轻量级进程对应一个内核线程
    • 轻量级进程本质还是进程,与普通进程的区别在于它的轻量性,只有一个最小的执行上下文 & 调度程序所需的统计信息,只记录执行相关的信息
    • 轻量级进程 & 用户线程的关系为 1 : n n : 1 m : n

4) Linux 中线程的实现

  1. Linux 中没有专门的线程数据结构,在内核看来,只有进程而没有线程,在线程调度时,也采用进程调度的方式处理
  2. Linux 的线程是与其他进程共享资源的进程,但是 Windows 中有线程
  3. Linux 中线程相当于轻量级进程的实现方式

5) 一个进程可以创建多少线程

  1. 操作系统虚拟空间内存影响
    • 32 位操作系统,用户虚拟地址空间 = 3G
    • 64 位操作系统,用户虚拟地址空间 = 128T
  2. 创建一个线程需要多大虚拟内存空间影响
    • ulimit -a 命令可以查看进程创建线程默认分配的栈空间大小
    • 大概 [8MB, 10MB]
  3. 受相关系统内核参数影响
    • /proc/sys/kernel/threads-max
      • 系统支持的最大线程数
      • default = 14553
    • /proc/sys/kernel/pid_max
      • 全局 pid 最大数量,每个线程 | 进程均需要有一个 Pid
      • default = 32768
    • /proc/sys/vm/max_map_count
      • 一个进程可以拥有的虚拟内存区域的数量
      • default = 65530

6) 线程崩溃,进程是否崩溃?

  1. 什么情况下线程会崩溃
    • 尝试对只读内存写入数据
    • 尝试访问没有权限访问的内存地址
    • 尝试访问不存在的内存
  2. 线程崩溃后,会发生什么?
    • 线程崩溃后,会发出信号,通知操作系统进行处理
    • 操作系统受到信号后,执行相应的信号处理函数,执行完后,通知该进程退出
    • 可以看出,若进程没有注册自己的信号处理函数,那么操作系统会执行默认信号处理程序,最后结果一般是会让该进程退出
    • 但若注册了自己的信号处理函数,就会执行自己的信号处理函数,从而可以避免让进程退出
  3. 线程崩溃后,不会导致 JVM 进程的崩溃
    • JVM 自定义了自己的信号处理函数,拦截了相关信号,因此不会导致进程的崩溃

7. 协程

1) 是什么

  • 比线程更加轻量级的微线程,一个线程中可以有多个协程
  • 协程的切换在用户态完成,因此切换开销小,速度快
  • 并没有增加线程数量,而是在线程基础上,通过分时复用的方式运行多个协程

2) 特点

  1. 优点
    • 协程切换快,开销小
    • 适用于 IO 密集型任务,和 异步 IO 结合发挥作用
  2. 缺点
    • 一条协程阻塞,整条线程也会阻塞
    • 需要用户在用户态调度

8. 其他问题

1) fork()

  1. 作用:
    • 当前进程下创建一个子进程
    • 是原有进程的一个副本,包含所有的文件描述符、寄存器等内容,所有变量父子进程相同
    • fork() 完成后,父子进程就不相关了,其中一个进程的相关修改不会影响另一个进程相应参数
    • fork() 函数的返回值,在父进程中可以看到,返回的是 子进程的 pid
上次编辑于: 2022/10/10 下午8:43:48
贡献者: liuxianzhishou