进程描述与控制
进程的描述
进程的并发执行
- 顺序执行: 顺序性,封闭性,可再现性
- 并发执行:
- 间断性 (以指令为单位,不是所有的异常都终止,比如缺页异常)
- 多个程序共享系统资源
- 不可再现性 (中断返回时重新调度 / 时间片用完了)
不可再现性示例展开收起
进程的概念
- 允许并发的程序在数据集合的执行过程
- 用户数据,系统数据,程序构成的实体
特征: 并发,动态,独立,异步
进程可以看成有限状态机,程序是静态的,指令的集合 (一个程序对应多个进程,研究 fork())。
进程控制块(PCB)
控制进程用的数据结构,存 pid,地址,状态…
作用
例 1 进程优先级
进程调度 -> 优先级 (大 -> 小)
例 2 -- time_slice
时钟中断 -> time_slice -- -> if time_slice == 0 -> 进程调度 -> 切换
特征: 进程控制块是内核数据(系统数据),常驻内存,标识进程存在
Linux 进程描述符 task_struct
补充: 内核栈 / 用户栈相关展开收起
- 用户栈(恢复现场用)每个进程独立
- 内核栈
创建新进程 -> 分配 8KB 栈空间(物理连续,页大小 4KB) { 低地址 PCB,stack 从高到低 }
8KB ->
可以这样填,按 x86 来理解:
表示 CPU 处于内核态的标志信息在哪里展开收起
在 CS(代码段寄存器)中的 CPL(Current Privilege Level),具体是 CS 的最低 2 位。
CPL = 0:内核态CPL = 3:用户态
所以“用户态/内核态”本质上是 CPU 当前特权级。
标志更新由硬件还是 OS 完成展开收起
硬件完成特权级的实际切换,OS 负责提前配置切换规则和入口。
例如发生系统调用/中断时,CPU 按 OS 配置好的 IDT(中断描述符表) 等信息自动进入内核态;返回时执行 iret 等指令,由 CPU 恢复到用户态。
所以不能简单理解成 OS 自己“修改一个内核态标志”。
什么时候用户态 → 内核态,内核态 → 用户态展开收起
用户态 → 内核态:
- 系统调用(
syscall/int 0x80) - 中断(如时钟、I/O 中断)
- 异常(如缺页异常、除零、非法指令)
统称为陷入内核。
内核态 → 用户态:
内核处理完系统调用 / 中断 / 异常后,通过 iret / sysret 等机制返回用户程序。
注意:进程切换 ≠ 一定发生用户态/内核态切换。比如内核正在运行 A,又调度到 B 的内核上下文,前后都可以是内核态。
进程状态
基本状态: 就绪、运行、阻塞
flowchart TB
accTitle: 进程状态转换
accDescr: 新创建的进程进入就绪态;分配 CPU 后运行,时间片用完后回到就绪态;等待 I/O 时阻塞,等待的事件发生后唤醒。原图保留“一定伴随中断(?)”的疑问。
created["新创建的进程"] --> ready["就绪"]
ready -->|"分配 CPU(调度)"| running["执行"]
running -->|"时间片用完"| ready
running -->|"等待(如 I/O)· 阻塞"| blocked["阻塞"]
blocked -->|"等待的事件发生 · 唤醒<br/>(一定伴随中断(?))"| ready
数据结构为算法服务
Linux 的进程状态
当前进程指针 current
进程的组织
就绪队列 / 阻塞队列(可能有多个因为不同原因的阻塞队列)
- 链接方式: 这个链表叫进程队列,但是不是数据结构意义上的队列
- Linux: 进程链表, Pidhash 表及链表
- 进程树
- 结构数组
进程的控制
进程的创建
用户登录,作业调度,提供服务,应用请求
flowchart LR
accTitle: 进程的创建
allocate["申请空白 PCB"] --> resources["分配资源"]
resources --> initialize["初始化 PCB"] --> ready["把新进程插入就绪队列"]
研究 fork()展开收起
进程的终止
正常结束,异常结束,外接干预(kill)
flowchart TD
accTitle: 进程的终止
state["从 PCB 读状态"] --> stop["终止执行(若正在执行)"]
stop --> children["若有子孙进程终止子孙进程<br/>(不一定)"]
children --> resources["释放资源"] --> pcb["释放 PCB"]
托管展开收起
孤儿进程:父进程先于子进程退出后,子进程成为孤儿进程。
Unix/Linux 中,孤儿进程会被重新托管(reparent),传统上由 1 号进程 init 接管,负责在其退出后回收。
现代 Linux 中也可能由 subreaper(如 systemd 等)接管,不一定直接连到 PID 1。
进程的阻塞
系统调用,I/O,等待数据
flowchart TD
accTitle: 进程的阻塞
block["暂停执行,改成阻塞态 (PCB.state)"]
queue["将进程存入相应阻塞队列"]
schedule["执行进程调度程序<br/>(从就绪队列选分配 CPU 的进程)"]
block --> queue --> schedule
进程的唤醒
内核函数 wakeup
flowchart LR
accTitle: 进程的唤醒
remove["移出阻塞队列"] --> ready["改为就绪态"] --> insert["插入就绪队列"]
新创建的进程能不能先执行一下再进入就绪队列展开收起
理论上能,但这实际上意味着调度器直接把 CPU 给了新进程。
经典模型是:
flowchart TD
accTitle: 新进程的创建与调度
created["创建进程"] --> initialized["初始化 PCB、分配资源"]
initialized --> ready["就绪态"]
ready -->|"调度器选中"| running["运行态"]
“就绪态”表示:运行所需条件都具备,只差 CPU。
因此如果创建完马上运行,本质上可以理解为:
flowchart LR
accTitle: 创建后立即运行的过程
created["创建"] --> ready["就绪"] --> scheduled["立即被调度"] --> running["运行"]
其中“进入就绪队列”这一步甚至可能在具体实现中非常短暂,不能理解成一定要在那里排很久。
有没有 OS 这样干展开收起
有类似的实际机制。
现代 OS 的实现并不一定严格按照教材的几个框框机械执行。例如 Linux 创建新进程后,会把新任务交给调度器管理;调度器可能很快让子进程先于父进程运行。
甚至历史上的 Linux vfork() 就特别典型:父进程会暂停,子进程先运行,直到子进程 exec 或退出。
但从状态模型来看,仍然可以认为:
flowchart LR
accTitle: 新进程的抽象状态模型
created["创建态"] --> ready["就绪态"] --> running["运行态"]
只是中间的“就绪”可能极短。
操作系统原理为什么这么讲展开收起
因为教材讲的是抽象状态模型,而不是某个 OS 的具体代码实现。
关键逻辑是:
进程只有获得 CPU 才能执行。
新进程创建完成时,它只是具备了运行资格,并没有天然获得 CPU,所以定义为就绪态。
之后由调度器统一决定:
flowchart LR
accTitle: 就绪与运行之间的调度
ready["就绪"] -->|"进程调度/获得 CPU"| running["运行"]
这样把“创建进程”和“决定谁使用 CPU”两个机制分开,模型非常清晰。
所以考试里按教材答:新进程创建完成 → 进入就绪队列 → 等待调度 → 获得 CPU 后运行。
引导过程: 系统什么时候创建进程?新进程完成什么功能?展开收起
传统的多阶段引导过程
引导过程: 开机 -> BIOS(主板) -> boot(MBR) -> loader(磁盘) -> kernel -> 启动操作系统
主引导记录 ≈ 主引导扇 MBR: 磁盘引导程序和分区表(分区的位置,是否为活动分区)
活动分区
BIOS -执行-> 主引导扇区中的引导程序 -> 活动分区中的 BOOT,和内核 loader -> 执行 OS 内核
BIOS 和 DRAM 统一编址
操作系统内核
中断
什么是中断
复习计组
中断: 产生信号 -> 传送信号 -> CPU 从一个执行流转到另一个执行流
硬件上下文: 保证程序正确执行需要的寄存器的值 (OS 负责)
断点 PC 的值 (硬件负责)
为什么 OS 负责 ctx,硬件负责 PC展开收起
因为两者的目的和时机不同。
- 断点(返回地址/PC)必须硬件存:中断/异常可能在任意一条指令之后突然发生。CPU 要保证以后能准确回来,所以在进入中断处理程序的瞬间,硬件必须自动保存 PC/EIP 等最基本的现场。否则 OS 的第一条代码都还没执行,就已经不知道从哪返回了。
- 上下文 ctx 主要由 OS 存:完整上下文包括大量寄存器、调度信息等。到底要保存哪些、保存到哪个 PCB、是否真的需要进程切换,属于 OS 的调度策略和软件实现,没必要全部固化进硬件。
为什么要中断
并行工作,提升性能
中断类型
- 内部中断(同步中断或异常)
- 外部中断
- 可屏蔽(I/O 设备产生的中断)
- 不可屏蔽(紧急实践,如阴间故障)
怎么屏蔽中断
用程序状态字,Intel 体系结构的 CPU 中 eflags 寄存器中的 IF 0/1,屏蔽/打开
用内核指令 cli/sti 设置 0/1
引起中断的原因
(略)
中断响应
响应外部中断
- 条件: 开中断
- 时机: 执行完每一条指令
常见的信号
键盘: 1, 时钟: 0
中断向量: 帮助 OS 找地址的整数
单重中断的执行
flowchart TD
accTitle: 单重中断的执行
subgraph instructions[" "]
direction LR
fetch["取指令"] --> execute["执行指令"] --> check{"检查中断"}
check -->|"else"| fetch
end
instructions -->|"是"| hardware
subgraph hardware["硬件"]
response["响应:关中断,保存断点,<br/>中断处理 → PC"]
end
hardware --> os
subgraph os["OS"]
direction LR
save["保护现场"] --> service["中断服务程序"]
service --> restore["恢复现场"] --> enable["开中断"] --> back["中断返回"]
end
不定长指令,how PC++展开收起
对于 x86 这种变长指令集,PC(x86 中即 EIP/RIP)并不是简单固定 +4。
CPU 在取指、译码过程中确定当前指令长度,然后:
例如一条指令 3 B,就 EIP += 3;下一条 7 B,就 EIP += 7。
现代 CPU 内部会用复杂的预取、译码、流水线提前确定指令边界,并不是软件手动算。
CPU 上下文存内存的位置展开收起
主要涉及两个地方:
- 内核栈(系统栈):中断、异常、系统调用进入内核时,硬件和 OS 会把 EIP、EFLAGS、通用寄存器等现场保存到当前进程的内核栈。
- PCB 等内核数据结构:保存进程调度所需的信息,尤其包括内核栈指针等,从而以后能找到保存的上下文。
可以粗略理解为:
PCB
│
└─ 保存内核栈位置等信息
↓
内核栈
├─ 寄存器
├─ EIP
├─ EFLAGS
└─ ...所以不要简单记成“上下文全部存在 PCB”。
更准确:
CPU 上下文通常主要保存在进程内核栈中,PCB 保存恢复该进程所需的调度信息和栈指针等。
终端向量表(OS 初始化,建一个数组,8B / 元素(向量号是索引),起始地址 -> IDTR)
IDTR 中断描述符表寄存器,存中断处理程序入口地址
Linux
段选择符 保留位 / 存在位,特权级等 段内偏移
48-63. 32-47. 0-31在中断机制中OS需要完成什么功能?硬件完成什么功能?展开收起
时钟管理
计算机系统中的时钟
- RTC 时钟: CMOS 时钟,时钟新品啊,靠电池供电
- OS 时钟: PC 主板上的定时 / 计数芯片,开机时有效
应用程序 <-> OS 时钟 <-OS装入的时候-> 芯片(BIOS 对 RTC 有关操作的固化程序) <-> RTC
操作系统的时钟机制
- 时钟硬件(可编程间隔定时器) PIT OS 往保持寄存器写值 -> [保存寄存器 -> 计数器] -> 脉冲,计数器 - 1 -> 计数器 = 0 产生中断 -> 回 2
- 时钟软件(时钟驱动程序 <=> 时钟中断处理程序)
- 维护时间
- time_slice-- & 判 0
- 对 CPU 使用情况记账
- 递减报警寄存器 (一个存储“什么时候提醒 CPU”的硬件寄存器)
系统调用
与一般函数的区别
- 内核态 / 用户态
- 执行过程不同(系统调用的实现例程 - 系统调用的代码)
- 系统调用需要中断,系统开销
32 位系统逻辑地址
一般函数调用
- 用 Jump(要用用户栈)
- 编译的时候插进去
系统调用
flowchart TD
accTitle: 系统调用的执行路径
instruction["int 0x80"] --> descriptor["IDT[0x80]"]
descriptor --> entry["系统调用入口"]
entry --> table["sys_call_table[eax]"]
table --> handler["sys_write"]