进程切换 (上下文切换)
- 暂停当前运行进程 从运行状态变成其他状态
- 调度另一个进程从就绪状态变成运行状态
进程切换的要求
- 切换前 保存进程上下文(寄存器 CPU状态 内存地址空间(大部分不用保存 因为两个进程的内存地址空间不会被覆盖))
- 切换后 恢复进程上下文
- 快速切换
1 | 进程 P0 因为时间片用完 进入时钟中断服务例程 |
进程状态记录 PCB 过程
- 内核为每个进程 维护 对应的进程控制块(PCB)
- 内核将相同状态的 进程的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() 执行过程对于 子进程来说 是在调用时间 对父进程地址空间的一次复制(父进程 fork() 返回值为 子进程标识符 子进程 fork() 返回值为 0)
fork 循环调用示例
1 | int main() { |
每次循环 都会 fork 一个新的子进程 同时原来被 fork 出来的子进程 也会去 fork 一个新的子进程 这里一共循环了 3次 共有 8个 进程
fork 的开销
- 对子进程分配内存
- 复制父进程的内存和CPU寄存器到子进程
在大多数情况下 调用了 fork() 以后 会调用 exec() 将 fork() 复制出来的子进程的内存给覆盖掉 fork() 复制父进程的内存的开销是可以节约的
因此 Windows 下 通过一个系统调用 来完成进程的创建和加载
早期 Unix的 vfork()
也做类似的事情 创建进程时 不再创建一个同样的内存映像 称之为轻量级 fork() 子进程应该立即调用 exec() 现在使用 Copy on Write (COW 写时拷贝) 技术
进程加载与执行 exec
系统调用 exec() 加载新程序取代当前运行程序
1 | fork() 创建一个继承的子进程 并复制 父进程的内存地址空间和CPU寄存器 |
代码段 堆栈 堆 完全重写
进程等待与退出
wait() 系统调用用于父进程等待子进程的结束
- 子进程结束时通过 exit() 向父进程返回一个值
- 父进程通过 wait() 接受并处理返回值
当 父进程 先 wait() 子进程 后 exit() 时
- 父进程进入等待状态 等待子进程的返回结果
- 当某子进程调用 exit() 时 唤醒父进程 将 exit() 返回值作为 父进程 wait() 的返回值
当 子进程 先 exit() 父进程 后 wait() 时
- 说明有僵尸子进程等待 wait() 立即返回其中一个值
当 无子进程存活 而 父进程 wait() 时
- wait() 立即返回
进程退出 exit
进程结束执行时 调用 exit() 完成进程资源回收
- exit() 系统调用的功能
- 将调用参数作为进程的 结果(返回值)
- 关闭所有打开的文件等占用资源
- 释放内存
- 释放大部分进程相关的内核数据结构
- 检查父进程是否还存活
- 存活 保留结果的值 直到父进程需要它 进入 僵尸(zombie/defunct)状态
- 非存活 释放所有的数据结构和结果
- 清理所有等待的僵尸进程
进程终止是最终的垃圾收集(资源回收)
进程控制与进程状态关系
其他进程控制系统调用
- 优先级控制
- nice() 指定进程的初始优先级
- Unix系统中 进程优先级会随着执行时间而衰减
- 进程调试支持
- ptrace() 允许一个进程控制另一个进程的执行
- 设置断点和查看寄存器等
- 定时
- sleep() 可以让进程在定时器的等待队列中等待指定的时间