Skip to content

进程描述与控制 ​

进程的描述 ​

进程的并发执行 ​

  • 顺序执行: 顺序性,封闭性,可再现性
  • 并发执行:
    • 间断性 (以指令为单位,不是所有的异常都终止,比如缺页异常)
    • 多个程序共享系统资源
    • 不可再现性 (中断返回时重新调度 / 时间片用完了)
不可再现性示例

进程的概念 ​

  • 允许并发的程序在数据集合的执行过程
  • 用户数据,系统数据,程序构成的实体

特征: 并发,动态,独立,异步

进程可以看成有限状态机,程序是静态的,指令的集合 (一个程序对应多个进程,研究 fork())。

进程控制块(PCB) ​

控制进程用的数据结构,存 pid,地址,状态…

作用

例 1 进程优先级

进程调度 -> 优先级 (大 -> 小)

例 2 -- time_slice

时钟中断 -> time_slice -- -> if time_slice == 0 -> 进程调度 -> 切换

特征: 进程控制块是内核数据(系统数据),常驻内存,标识进程存在

Linux 进程描述符 task_struct

补充: 内核栈 / 用户栈相关
  • 用户栈(恢复现场用)每个进程独立
  • 内核栈

创建新进程 -> 分配 8KB 栈空间(物理连续,页大小 4KB) { 低地址 PCB,stack 从高到低 }

8KB -> 213 末 13 位一定为 0,用 ESP (栈指针寄存器) 前 19 位找 PCB

可以这样填,按 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: 磁盘引导程序和分区表(分区的位置,是否为活动分区)

活动分区

Active = 被 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 在取指、译码过程中确定当前指令长度,然后:

PCnext=PCcurrent+当前指令长度

例如一条指令 3 B,就 EIP += 3;下一条 7 B,就 EIP += 7。

现代 CPU 内部会用复杂的预取、译码、流水线提前确定指令边界,并不是软件手动算。

CPU 上下文存内存的位置

主要涉及两个地方:

  1. 内核栈(系统栈):中断、异常、系统调用进入内核时,硬件和 OS 会把 EIP、EFLAGS、通用寄存器等现场保存到当前进程的内核栈。
  2. PCB 等内核数据结构:保存进程调度所需的信息,尤其包括内核栈指针等,从而以后能找到保存的上下文。

可以粗略理解为:

text
PCB
 │
 └─ 保存内核栈位置等信息
          ↓
      内核栈
      ├─ 寄存器
      ├─ EIP
      ├─ EFLAGS
      └─ ...

所以不要简单记成“上下文全部存在 PCB”。

更准确:

CPU 上下文通常主要保存在进程内核栈中,PCB 保存恢复该进程所需的调度信息和栈指针等。

终端向量表(OS 初始化,建一个数组,8B / 元素(向量号是索引),起始地址 -> IDTR)

IDTR 中断描述符表寄存器,存中断处理程序入口地址

Linux

text
段选择符 保留位 / 存在位,特权级等 段内偏移
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 位系统逻辑地址

32 位系统逻辑地址:3G 用户空间、1G 内核空间,分别标注 3 和 0

一般函数调用 ​

  • 用 Jump(要用用户栈)
  • 编译的时候插进去

系统调用 ​

flowchart TD
    accTitle: 系统调用的执行路径
    instruction["int 0x80"] --> descriptor["IDT[0x80]"]
    descriptor --> entry["系统调用入口"]
    entry --> table["sys_call_table[eax]"]
    table --> handler["sys_write"]