⛈️ 进程管理
2022年10月10日
- os
⛈️ 进程管理
1. 进程的状态
- 运行中的程序,就是一个进程
- 资源分配 & 调度的基本单位
状态转移
1) 创建 new
- 创建流程:
- 为新进程分配唯一进程标识号 & 空白的 PCB
- 为新进程分配所需资源,例如内存资源
- 初始化 PCB
- 填入 进程标识号
- 填入 CPU 相关信息
- 填入 进程调度相关信息
- 将新进程插入就绪队列,插入成功则处于就绪状态
- 允许一个进程创建另外一个进程
- 子进程继承父进程的所有资源;当子进程被终止时,继承的资源会归还给父进程;父进程终止时会终止所有子进程
2) 就绪 ready
- 获得 CPU 时间片即可进入运行态
3) 运行 running
- 获得 CPU 时间片
4) 阻塞 blocked
- 需要等待某个事件完成,此时即使给该进程时间片,也无法运行
- 一旦进入阻塞状态,必须由某个事件进程进行唤醒
- 发生场景:
- 请求共享资源失败,系统没有足够的资源可供分配
- 等待某种操作完成
- 新数据尚未到达
- 等待新任务
- 阻塞流程:
- 找到该进程对应的 PCB
- 将该进程由运行状态转换为阻塞状态
- 将该进程的 PCB 插入到阻塞队列中
- 唤醒流程:
- 从阻塞队列中找到该进程对应的 PCB
- 将该进程由阻塞状态转化为就绪状态
- 将该 PCB 从阻塞队列中放入就绪队列
5) 结束 exit
3
种结束方式- 正常结束
- 异常结束
- 越界错误
- 访问超出该进程的存储区域
- 保护错误
- 访问了不被允许访问的区域
- 非法指令
- 使用了不存在的指令
- 特权指令错误
- 用户进程下指令了内核态才能执行的指令
- 运行超时
- 执行时间超过指定时间
- 等待超时
- 等待时间超过指定时间
- 算法运算错误
- 例如,除 0 操作
- IO 故障
- 越界错误
- 外界干预
- 操作员 | 操作系统干预
- 例,kill 指令杀死进程
- 父进程终止,所有子进程都会被结束
- 操作员 | 操作系统干预
- 结束流程:
- 查找该进程对应的 PCB,读取 PCB 中的进程状态
- 若处于执行状态,终止该进程的执行,并将其拥有的 CPU 资源分配给其他进程
- 若还有子进程,终止所有子进程的执行
- 将该进程的所有资源归还给 父进程 | 操作系统
- 将 PCB 从所在队列中移除
6) 就绪挂起
- 进程被写入硬盘,一旦进入内存,即可进入就绪状态
7) 阻塞挂起
- 进程被写入硬盘,并等待某个时间出现
挂起
- 当大量进程均处于阻塞态时,由于该状态下进程需要等待某个事件完成才可继续执行
- 因此若不将这部分进程换出到磁盘,则会占用大量物理内存
- 因此采取挂起状态,用来描述一个进程没有存在于物理内存中,而是在磁盘等待调用
- 挂起的几种情况:
- 进程不占用物理内存
- 调用
sleep()
函数 Linux
中Ctrl + Z
被用户挂起进程
2. PCB
1) 是什么
PCB
[进程控制块]- 是一种数据结构,是进程存在的唯一标识,为了描述 & 控制进程的执行,属于进程实体的一部分
- 作用
- 标识一个进程
- 提供进程调度相关信息
- 提供进程所用资源相关信息
- 提供进程 CPU 占用相关信息
2) 含有哪些信息
- 标识符
- 进程标识符
- 标识进程,每个进程都有一个 & 唯一的标识符
- 用户标识符
- 标识拥有该进程的用户,往往是用户进程在访问该进程时使用
- 进程标识符
- 进程调度信息
- 进程当前状态
- new ready running blocked exit
- 进程优先级
- 进程抢占 CPU 时的优先级
- 与进程调度算法有关的信息
- 进程已经等待 CPU 的时间总和
- 进程已经执行的时间总和
- 事件
- 进程由 running 转换为 blocked 所等待的事件
- 进程当前状态
- 资源信息
- 占用的虚地址空间
- 打开的文件列表
- 使用的 IO 设备
- CPU 信息
- 各个寄存器的值
- 通用寄存器
- 指令计数器
- 程序状态字 PSW
- 用户栈指针
- 各个寄存器的值
3) 管理方式
- 一个 PCB 为一个数据结构,多个 PCB 如何进行管理?
- 线性方式
- 所有 PCB 保存在一张线性表中,将该线性表放在内存的一个专用区域
- 实现简单,但是每次找某个 PCB 时,需要线性扫描该表
- 适用于进程数目不多的系统
- 索引方式
- 将同一状态的进程存在一张索引表中,索引项指向对应的 PCB
- 链表方式
- 将同一状态的 PCB 连接在一起,形成队列,例如就绪队列、阻塞队列等
- 由于进程的创建、销毁、调度比较频繁,因此多采用该方式
- 线性方式
3. 上下文切换
1) CPU 上下文切换
- 保护哪些现场?
- CPU 寄存器的值
- 程序计数器的值
- 什么情况下发生 CPU 的上下文切换?
- 进程切换
- 线程切换
- 中断切换
2) 进程上下文切换
- 保护哪些现场?
- 用户空间资源
- 虚拟内存
- 栈
- 全局变量
- 内核空间资源
- 内核堆栈
- 寄存器
- 用户空间资源
- 发生在什么态下?
- 进程由内核管理 & 调度,因此进程切换只能发生在内核态
- 什么情况下会发生进程的上下文切换?
- 该进程的时间片使用完
- 进程调用 sleep() 函数
- 系统资源不足,例如内存不足
- 有更高优先级的进程运行
- 发生硬件中断
3) 线程上下文切换
- 保护哪些现场?
- 当 2 个线程处于不同进程时,同进程的 保护现场 & 恢复现场
- 当 2 个线程处于同一进程时,
- 程序计数器
- 虚拟机栈
- 本地方法栈
4. 进程的调度
1) 是什么
- 实现进程状态的切换
- 主要分为 2 大类:
- 抢占式调度
- 每个进程只运行某段时间,当时间到达后,若进程仍在运行,则将其挂起,
- 调度程序从就绪队列中挑选另外一个进程,让其运行,如此循环
- 非抢占式调度
- 每个进程只要让其开始运行,则直到其阻塞 | 结束,才会切换下一个进程
- 抢占式调度
2) 调度原则
- CPU 利用率
- CPU 利用率 = CPU 忙碌时间 / 总时间
- 应尽可能使 CPU 利用率高
- 系统吞吐量
- 吞吐量 = 单位时间内完成的进程数
- 进程完成耗时越少,吞吐量越高
- 等待时间
- 进程处于等待队列中的时间
- 等待时间越短越好
- 响应时间
- 用户第一次提交请求,到产生第一次响应的时间
- 多用于交互系统
- 周转时间
- 进程等待时间 + 运行时间 + 阻塞时间总和 == 进程完成时间 - 进程提交时间
- 周转时间越短越好
3) 常见调度算法
- 先来先服务
- 非抢占式
- 每次从就绪队列中选取最先进入队列的进程让其运行,直到其阻塞 | 退出
- 若某个进程运行时间过长,将导致后面的进程等待时间过长,不利于短作业,适用于全是长作业的任务
- 最短作业优先
- 非抢占式
- 每次从就绪队列中优先选取运行时间最短的进程让其运行,直到其阻塞 | 退出
- 适用于全是短作业的任务,若某个进程运行时间长,则一直得不到运行,不利于存在长作业的任务
- 最短剩余时间优先
- 抢占式
- 当一个新就绪的进程比当前运行进程拥有更短的运行时间时,系统挂起当前进程,让这个新的进程运行
- 适用于全是短作业的任务,不适用于存在长作业的任务
- 高响应比优先
- 非抢占式
- 响应比 = (进程等待时间 + 进程运行时间) / 进程运行时间
- 每次从就绪队列中优先选取响应比高的进程让其运行,直到其 阻塞 | 退出
- 兼顾了短作业 & 长作业
- 当进程等待时间相同时,运行时间短的进程优先运行,利于短作业的优先进行
- 当进程运行时间相同时,等待时间长的进程优先运行,长作业由于一直被等待,等待时间变长,将会得到更大的几率被运行,利于长作业的运行
- 时间片轮转调度
- 抢占式调度
- 每个进程均被要求运行相同的时间,当时间片耗尽时,将发生进程的切换
- 时间片的选取:
- 一般 20 ms - 50 ms
- 时间片太短,将导致频繁的进程上下文切换,降低 CPU 效率
- 时间片太长,将导致短作业进程的响应时间变长
- 最高优先级调度
- 抢占式 & 非抢占式
- 每次从就绪队列中优先选取优先级高的进程让其运行
- 优先级是否固定?
- 静态优先级
- 进程优先级在进程创建时已经确定,直到结束都不能改变
- 动态优先级
- 进程优先级可以随着时间的推移不断提高
- 静态优先级
- 将导致低优先级的进程迟迟无法运行
- 多级反馈队列调度
- 抢占式
- 设置多个队列,每个队列有不同的优先级,优先级越高的队列时间片越短
- 优先执行高优先级队列中的进程,当高优先级队列中没有进程时,执行次优先级队列中的进程,以此类推
- 当一个新的进程加入时,会被放入最高优先级队列的队尾
- 若最高优先级队列中前面还有进程,则等待前面进程运行
- 若最高优先级队列中不存在进程,当前运行的进程非最高优先级,则将当前运行的进程停止运行,并放入所在优先级队列的队尾,然后运行新加入的拥有最高优先级的进程
- 若在时间片内进程没有执行完成,则会移到低一级的队列末尾
- 利于短作业,因为短作业可以在高优先级快速完成;利于长作业,因为越移到次优先级队列,当被执行时,分配到的时间片越长,越容易完成
5. 进程的通信
- 每个进程的用户地址空间是独立的,不可以直接访问;而内核空间是所有进程共享的,因此进程之间的通信必须通过内核
1) 管道
- 如何使用
- 匿名管道
- Linux 中的
|
就是匿名管道,意思是 将前面指令的输出作为后一个指令的输入
- Linux 中的
- 命名管道
- 首先创建一个管道
mkfifo myPipe
- 向管道中写入信息
echo "liuxianzhishou" > myPipe
echo
命令用于字符串的输出
- 从管道中读取信息并打印
cat < myPipe
- 首先创建一个管道
- 匿名管道
- 特点
- 简单,存储的是无格式的字节流数据
- 数据传输为单向,遵循 FIFO 原则
- 通信效率低,不适合进程间的频繁数据交换
- 大小受限
- 生命周期随进程的创建而建立,随进程的结束而销毁
- 匿名管道底层原理
- 依赖于
int pipe(int fd[2])
函数fd[0]
读取端描述符fd[1]
写入端描述符
- 本质:
- 在内核中创建一块内存作为缓存
- 如何实现进程间的通信?
fork
一个子进程,创建的子进程会复制父进程的文件描述符,这样就使得父子进程各有自己的fd[0]
&fd[1]
- 当父子进程需要通信时,只需要父进程写入管道,子进程就可从管道中读取信息
- Linux 中
|
的底层原理- 通过 shell fork 多个子进程,来实现子进程间的通信,子进程间不存在父子关系
- 依赖于
- 命名管道底层原理
- 提前创建一个管道,可以实现多个不相关的子进程间的通信
父子进程单向通信
Shell 中
A | B
的实现
2) 消息队列
- 特点
- 保存在内核中的消息链表
- 双方约定好消息体进程发送和接收,存储方式为约定好的消息体的大小,一个个存放
- 生命周期与内核有关,只要没有关闭操作系统 && 没有释放掉该消息队列,那么该消息队列始终存在
- 优点
- 效率提高
- 缺点
- 消息队列存放于内核,每个消息体大小受限制 && 一个消息队列中消息体总个数受限制,不适合大数据的传输
- 消息队列存放于内核,写入消息时,需要从用户态切换到内核态;接收消息时,也需要从内核态读取,来回切换开销大
3) 共享内存
- 实现原理
- 不同进程中,有一块特殊的虚拟内存,使得不同进程的虚拟内存最终可以映射到同一块物理内存
- 特点
- 解决消息队列中用户态 & 内核态来回切换的问题,一个进程写入,另一个进程读取,可以立马看到该块物理内存数据的变动
- 多进程并发下存在数据安全问题,可能一个进程改动了数据,另一个进程还未读取到,就被其他的进程覆盖了新数据
4) 信号量
- 作用
- 解决共享内存下多进程竞争共享数据造成的安全问题
- 保证共享资源的数据安全,任意时刻只能有 一个 | 特定几个 进程对共享资源进行操作
- 实际上是用于进程间的互斥 & 同步,而非用于传输相关通信数据,从而实现进程的通信,但是信号量也属于进程通信需要考虑的一个点
- 底层数据结构
- 信号量 [Semaphore] 是一个整型的计数器,可 ++ | --
- 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 编程
- 作用
- 可实现不同主机间的进程通信
- 当然同主机不同进程也可以实现通信
- 底层原理
// domain 协议族 - [IPv 4, IPv 6, 本机]
// type 通信类型 - [基于字节流 (tcp), 基于数据报 (udp), 原始套接字]
// protocal 基本废弃 - 0
int socket(int domain, int type, int protocal)
- 3 种形式
- TCP
- UDP
- 本机通信
- 调用
bind()
时,无需绑定 ip & port,只需要绑定一个本地文件 type
参数支持 基于字节流 & 基于数据报
- 调用
6. 线程
1) 是什么
- 线程是比进程更小的单位,是 CPU 运行的基本单位
- 一个进程中可以有多个线程
- 线程间共享 Java 的 堆 & 方法区
- 线程私有程序计数器、操作数栈、本地方法栈
- 优点:
- 线程创建、切换开销比进程小
- 可实现多线程并发
- 缺点:
- 当进程中的一个线程崩溃时,会导致该进程的其他线程也崩溃[针对 C/C ++,不包含 Java]
2) 进程 VS 线程
- 相同点
- 均具有 创建、运行、阻塞、终止等基本状态,也具有相应状态转换
- 均有不同进程、线程之间私有的部分
- 不同点
- 进程是资源调度 & 分配的基本单位;线程是 CPU 调度的基本单位
- 进程之间资源完全隔离;线程之间除了私有部分,堆 & 方法区是共享的
- 进程占有的页表不同;线程共享同一份页表 & 同一份虚拟内存
- 进程间数据传递需要经过内核;线程间的数据传递可以通过线程共享区
- 进程切换由内核态管理;线程切换取决于线程的实现方式
- 进程创建、切换、结束开销较大;线程创建、切换、结束开销较小
- 一个进程可以拥有多个线程;线程不能独立于进程而存在
3) 如何实现
- 用户态线程
- 在用户空间实现,不经过内核,通过用户态的线程库实现线程的创建 & 管理等,线程对应的 PCB 在用户态实现
- 一个内核线程 对应 多个用户线程
- 优点:
- 通过用户态的线程库函数实现线程的切换,无需经过用户态 & 内核态的切换,切换速度快
- 缺点:
- 操作系统将多个用户线程当做一个进程,当某个用户线程阻塞时,该进程下的所有线程均被阻塞
- 操作系统将多个用户线程当做一个进程,当多线程执行时,每个线程得到的时间片比其他进程更短
- 用户态线程无法中断当前运行中的线程,只有操作系统才有这个权限,因此,当某个线程开始运行后,除非自己交出 CPU 的使用权,否则其他用户线程无法运行
- 内核态线程
- 在内核空间实现,有内核管理,线程对应的 PCB 在内核态
- 一个内核线程 对应 一个用户线程
- 优点:
- 在一个进程中,当某个线程阻塞时,不会影响其他内核线程的运行
- 一个线程对应一个时间片
- 缺点:
- 线程调度开销大,线程创建、切换、终止均需要通过系统调用切换到内核态执行
- 内核线程创建的数量有限
- 轻量级进程
- 内核支持的用户线程,是基于内核线程的高级抽象
- 一个进程可拥有多个轻量级进程,一个轻量级进程对应一个内核线程
- 轻量级进程本质还是进程,与普通进程的区别在于它的轻量性,只有一个最小的执行上下文 & 调度程序所需的统计信息,只记录执行相关的信息
- 轻量级进程 & 用户线程的关系为
1 : n
n : 1
m : n
4) Linux 中线程的实现
- Linux 中没有专门的线程数据结构,在内核看来,只有进程而没有线程,在线程调度时,也采用进程调度的方式处理
- Linux 的线程是与其他进程共享资源的进程,但是 Windows 中有线程
- Linux 中线程相当于轻量级进程的实现方式
5) 一个进程可以创建多少线程
- 受操作系统虚拟空间内存影响
- 32 位操作系统,用户虚拟地址空间 = 3G
- 64 位操作系统,用户虚拟地址空间 = 128T
- 受创建一个线程需要多大虚拟内存空间影响
ulimit -a
命令可以查看进程创建线程默认分配的栈空间大小- 大概 [8MB, 10MB]
- 受相关系统内核参数影响
/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) 线程崩溃,进程是否崩溃?
- 什么情况下线程会崩溃?
- 尝试对只读内存写入数据
- 尝试访问没有权限访问的内存地址
- 尝试访问不存在的内存
- 线程崩溃后,会发生什么?
- 线程崩溃后,会发出信号,通知操作系统进行处理
- 操作系统受到信号后,执行相应的信号处理函数,执行完后,通知该进程退出
- 可以看出,若进程没有注册自己的信号处理函数,那么操作系统会执行默认信号处理程序,最后结果一般是会让该进程退出
- 但若注册了自己的信号处理函数,就会执行自己的信号处理函数,从而可以避免让进程退出
- 线程崩溃后,不会导致 JVM 进程的崩溃
- JVM 自定义了自己的信号处理函数,拦截了相关信号,因此不会导致进程的崩溃
7. 协程
1) 是什么
- 比线程更加轻量级的微线程,一个线程中可以有多个协程
- 协程的切换在用户态完成,因此切换开销小,速度快
- 并没有增加线程数量,而是在线程基础上,通过分时复用的方式运行多个协程
2) 特点
- 优点
- 协程切换快,开销小
- 适用于 IO 密集型任务,和 异步 IO 结合发挥作用
- 缺点
- 一条协程阻塞,整条线程也会阻塞
- 需要用户在用户态调度
8. 其他问题
1) fork()
- 作用:
- 当前进程下创建一个子进程
- 是原有进程的一个副本,包含所有的文件描述符、寄存器等内容,所有变量父子进程相同
fork()
完成后,父子进程就不相关了,其中一个进程的相关修改不会影响另一个进程相应参数fork()
函数的返回值,在父进程中可以看到,返回的是 子进程的pid