跳转至

原语

来源:操作系统 / note/进程管理/进程/原语.md

原语就是操作系统中一种不可被中断的操作序列

也就是说:

原语一旦开始执行,就必须执行完,中间不能被打断。

英文一般叫 primitive,在操作系统里常说的是 原子操作 atomic operation


1. 为什么需要原语?

操作系统里有很多共享资源,比如:

进程表
就绪队列
信号量
文件表
内存分配表

这些数据可能会被多个进程同时访问。

如果一个操作执行到一半被打断,另一个进程又来修改同一个数据,就可能出错。

比如两个进程同时申请一个资源:

资源数量 count = 1

进程 A 判断:

if (count > 0)

此时发现 count = 1,可以申请。

但是还没来得及执行:

count--;

CPU 切换到了进程 B。

进程 B 也判断 count > 0,也认为可以申请,于是也申请成功。

结果:

本来只有 1 个资源
却被两个进程同时申请成功

这就出错了。

所以某些关键操作必须设计成不可中断,这就是原语。


2. 原语的核心特点

原语有两个重要特点:

特点 含义
原子性 要么全部执行,要么完全不执行
不可中断性 执行过程中不能被调度、中断打断

比如:

申请资源
释放资源
阻塞进程
唤醒进程
P 操作
V 操作

这些通常都需要做成原语。


3. 举个信号量的例子

信号量里的 P 操作V 操作 就是经典原语。

P 操作,也叫 wait 操作

P(S) {
    S--;
    if (S < 0) {
        阻塞当前进程;
        把当前进程加入等待队列;
    }
}

意思是:申请一个资源。

V 操作,也叫 signal 操作

V(S) {
    S++;
    if (S <= 0) {
        从等待队列中唤醒一个进程;
    }
}

意思是:释放一个资源。

这两个操作必须是原语。

因为如果 S-- 执行完,还没来得及判断 S < 0 就被打断,其他进程也来操作 S,信号量的值和等待队列就可能乱掉。


4. 原语和普通函数的区别

普通函数可以在执行过程中被中断或被调度走。

比如:

void func() {
    a++;
    b++;
    c++;
}

执行完 a++ 后,CPU 可能切到别的进程。

但原语不允许这样。

原语开始
  执行若干关键操作
原语结束

中间不能被打断。

所以:

普通函数:可能执行到一半被切走
原语:必须一口气执行完

5. 原语怎么实现不可中断?

常见方法有两种。

第一种是关中断

关中断
执行关键操作
开中断

在单处理器系统中,关中断后,时钟中断不能打断当前操作,所以不会发生进程切换。

第二种是使用硬件提供的原子指令

比如:

Test-and-Set
Compare-and-Swap, CAS
Swap
Fetch-and-Add

这些指令本身由 CPU 保证不可分割。

在多核系统中,仅仅关当前 CPU 的中断不够,因为其他 CPU 核心还可能同时访问共享数据,所以更依赖锁和硬件原子指令。


6. 放到进程管理里理解

比如操作系统要把一个进程从运行态变成阻塞态:

修改进程状态
把进程从运行队列移走
加入阻塞队列
重新调度 CPU

这些步骤必须保持一致。

如果执行到一半被打断,就可能出现:

进程状态是阻塞态
但它还留在就绪队列里

这样调度器可能错误地再次调度它。

所以这种修改系统核心数据结构的操作通常要作为原语实现。


7. 一句话总结

原语就是:

操作系统中完成某个核心功能的一段不可中断程序,具有原子性,要么全部完成,要么不被执行到一半就停下。

你可以把它理解成操作系统里的“最小安全操作单位”。