【Linux】二十三.《操作系统到底是怎么跑起来的?一次搞懂中断、软中断和内核态》
上一篇文章我们学习了,用户态和内核态相关基本概念,以及内核是如何实现信号的捕捉,这一片篇内容,我们将基于上回对相关基础知识,来进行本文章的讲解。
一. 操作系统是怎么运⾏的
1.1硬件中断
理解硬件中断简单来说:操作系统是怎么"知道"外部设备有数据的?
1.从键盘输入说起
你在键盘上敲了一个键,屏幕上显示出来。这个过程中有一个关键问题:CPU 怎么知道键盘上有数据了?
答案就是:硬件中断。
2.硬件中断的整体流程
- 外部设备(如键盘、磁盘、网卡等)数据准备就绪后,会通过中断控制器(8259A)向 CPU 发送中断信号。
- CPU 收到该信号后,获取对应的中断号,并依据此中断号在中断向量表中查找相应的中断处理程序,然后跳转执行以完成数据读取等逻辑处理。
- 处理结束后,CPU 恢复现场,继续执行被中断的任务。
- 整个流程完全由硬件中断驱动,操作系统无需主动轮询任何外设状态。


3.中断向量表
中断向量表本质上就是一个函数指针数组,存储在内存中的固定位置。操作系统启动的时候就把这个表加载到内存里了,每个中断号对应一个处理函数的地址。
比如:
| 中断号 | 对应的处理函数 |
|---|---|
| 0 | 除零异常处理 |
| 1 | 单步调试处理 |
| 2 | 非屏蔽中断 |
| 3 | 断点调试 |
| ... | ... |
| 0x20 | 时钟中断处理 |
| 0x21 | 键盘中断处理 |
| ... | ... |
| 0x80 | 系统调用入口 |
当 CPU 收到一个中断信号,它拿到中断号后,就去这个表里找到对应的函数地址,跳过去执行。
这些中断处理函数,就是操作系统的一部分。
更加通俗易懂的理解就是:
中断向量表是操作系统"提前注册"好的"事件处理函数列表"。硬件触发了哪个事件,CPU 就去表里找对应的函数来执行。OS 不用主动去问设备"你有没有数据",设备准备好了会主动"喊"OS。
4.8259A 中断控制器
介绍的8259A 是一个可编程中断控制器(PIC),它的作用是:
管理多个外部设备的中断请求
给每个设备分配中断号
按照优先级把中断请求转发给 CPU
鼠标 网卡 键盘 磁盘 → 8259A(中断控制器) → CPU
OS 不需要知道是"键盘"还是"鼠标"准备好了,它只需要知道"有一个中断来了,中断号是多少",然后根据中断号去处理对应的设备。外设数量远多于 CPU 的中断引脚,所以需要中断控制器做中转和仲裁。
有了硬件中断机制,操作系统再也不用对外设进行周期性检测或轮询(polling)了。
硬件中断机制的核心思想就是异步通知。在没有中断之前,操作系统想获知外设状态只能靠主动轮询——每隔一段时间挨个去问键盘、磁盘、网卡"你有数据了吗",大部分时间设备都没准备好,CPU 却在空转,白白浪费处理能力。中断机制彻底改变了这种模式,设备数据就绪后会主动通过中断控制器向 CPU 发信号,操作系统只管专心执行当前任务,等中断来了响应就行。就像你专心写代码,不用每隔几秒就问快递员"有我的快递吗",快递到了自然会打电话叫你下楼取。这就是异步通知的本质:生产者主动通知,消费者被动响应,双方都不浪费资源。
5.硬件中断与信号的相似性
硬件中断和信号在机制上非常相似,本质上是一种"通知"机制:
| 硬件中断 | 信号 |
|---|---|
| 外部设备发中断号 | 内核发信号编号 |
| CPU 查中断向量表 | 内核查 pending 位图 |
| 执行中断处理程序 | 执行信号处理函数 |
| 由硬件触发 | 由软件触发 |
所以可以这样理解:信号是纯软件的,本质是用软件的方式模拟硬件中断的行为。
6.知识补充
外设也有寄存器(控制寄存器、状态寄存器、数据寄存器)。
当 CPU 执行中断处理程序时,本质是在做三件事:
读外设的状态寄存器:确认数据是否就绪
读外设的数据寄存器:把数据取出来
写外设的控制寄存器:告诉外设"数据我读走了,你可以继续了"
代码如下(这一块了解即可):
// Linux 内核 0.11 源码:初始化中断向量表
void trap_init(void)
{
int i;
// 设置 CPU 异常处理函数
// 这些是 CPU 内部触发的异常(同步中断)
set_trap_gate(0, ÷_error); // 除零错误
set_trap_gate(1, &debug); // 调试/单步
set_trap_gate(2, &nmi); // 不可屏蔽中断
set_system_gate(3, &int3); // 断点 (允许用户态调用)
set_system_gate(4, &overflow); // 溢出 (允许用户态调用)
set_system_gate(5, &bounds); // 边界检查 (允许用户态调用)
set_trap_gate(6, &invalid_op); // 无效指令
set_trap_gate(7, &device_not_available); // 设备不可用(协处理器)
set_trap_gate(8, &double_fault); // 双重错误(连续出错导致)
set_trap_gate(9, &coprocessor_segment_overrun); // 协处理器段越界
set_trap_gate(10, &invalid_TSS); // 无效 TSS(任务状态段)
set_trap_gate(11, &segment_not_present); // 段不存在
set_trap_gate(12, &stack_segment); // 栈段错误
set_trap_gate(13, &general_protection); // 通用保护错误
set_trap_gate(14, &page_fault); // 缺页异常 ★
set_trap_gate(15, &reserved); // 保留
set_trap_gate(16, &coprocessor_error); // 协处理器错误
// 批量预留中断号 17~47
// 先全部设为 reserved(占位),等硬件初始化时再重新设置
for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
// 允许特定的硬件中断
set_trap_gate(45, &irq13); // 协处理器中断
outb_p(inb_p(0x21) & 0xf0, 0x21); // 允许 IRQ2(从片级联)
outb(inb_p(0xA1) & 0xdf, 0xA1); // 允许 IRQ13(协处理器)
// 设置并行口中断
set_trap_gate(39, ¶llel_interrupt); // 并行口中断
}
// 串口中断初始化
void rs_init(void)
{
// 绑定串口中断处理函数
set_intr_gate(0x24, rs1_interrupt); // 0x24 = 36号,对应 IRQ4(串口1)
set_intr_gate(0x23, rs2_interrupt); // 0x23 = 35号,对应 IRQ3(串口2)
// 初始化串口硬件
init(tty_table[1].read_q.data); // 初始化串口1
init(tty_table[2].read_q.data); // 初始化串口2
// 允许 8259A 接收 IRQ3、IRQ4 的中断信号
outb(inb_p(0x21) & 0xE7, 0x21); // 0xE7 = 11100111,打开 IRQ3、IRQ4
}
代码的内容是:
Linux 0.11 启动时,把 0~47 号中断都绑定了处理函数。CPU 异常(除零、缺页)用
set_trap_gate绑定,外部设备(串口、协处理器)用set_intr_gate绑定。绑定完成后,再通过写 8259A 的 I/O 端口把对应的中断开关打开,允许硬件中断信号进入 CPU。
1.2时钟中断
时钟中断:操作系统的心脏起搏器
1.操作系统被谁叫醒?
先回答一个根本问题:进程被操作系统调度,那操作系统自己呢?谁让它跑起来?
答案是:没人指挥操作系统。它平时是"死"的,只有中断来了才活过来干活。
中断就是操作系统的"起搏器"。没有中断脉冲,OS 就一直停着不动。
2.时钟中断
时钟中断:那个自己会跳的中断
外部设备(键盘、磁盘、网卡)的中断都得靠外部事件触发——键盘你得敲,磁盘你得读写。但有一种中断是自己定时触发的,不需要任何人操作它:时钟中断。
主板上有一个定时器芯片(PIT,可编程间隔定时器),每隔固定时间(比如 10ms)就往 CPU 发一个中断信号,频率固定,从不偷懒。这个中断叫时钟中断,中断号是
0x20。
内核初始化的时候,把中断号 0x20 和时钟中断处理函数绑定:
// Linux 内核初始化时设置时钟中断
set_intr_gate(0x20, &timer_interrupt);
3.时钟中断处理流程
硬件定时器每隔 10ms 发一次中断,CPU 收到后执行 timer_interrupt 处理函数:
// 时钟中断处理入口
timer_interrupt:
call do_timer // 调用 do_timer 函数
// ... 恢复现场
// do_timer:时钟中断的核心处理逻辑
void do_timer(long cpl)
{
// 1. 更新系统时间戳
total++; // 系统运行总滴答数,用来维护系统时间
// 2. 当前进程的时间片减 1
if (--current->counter > 0)
return; // 时间片还没用完,继续跑当前进程
// 3. 时间片用完了,调用调度器切换进程
schedule(); // 切换到下一个进程
}
current指向当前正在运行的进程,每个进程都有一个counter字段表示剩余时间片。每来一次时钟中断,counter就减 1。减到 0 就说明时间片用完了,调用schedule()切换到下一个进程。
4.时钟中断的三个作用
驱动进程调度:时间片到了就切换进程,这是分时操作系统的基础
维护系统时间:
total++累加时钟中断次数,换算成秒就是系统运行时间,再结合 RTC(实时时钟)就能让计算机离线也知道几点让操作系统"活"过来:时钟中断是唯一一种无需外部事件、固定频率触发的中断,保证 OS 能定期拿到 CPU 控制权
那如果没有时钟中断会怎样?
进程死循环 --> CPU 永远被占用 --> OS 永远抢不回控制权 -->整个系统卡死
没有系统时间 -->
time()函数没法用没有调度 --> 多进程变单进程,分时操作系统不复存在
所以时钟中断是操作系统能掌控全局的底线保障。

这样,有了时钟中断,操作系统不就在硬件的推动下,⾃动调度了么。
代码如下:
这段代码展示的是 Linux 0.11 内核如何通过时钟中断驱动进程调度,时钟中断 + 进程调度 完整代码与流程
1.内核初始化(main.c)
// main.c
void main(void)
{
// ... 各种初始化
sched_init(); // 调度程序初始化
// ...
while(1); // 死循环,等待中断
}
2.调度初始化(kernel/sched.c)
// kernel/sched.c
void sched_init(void)
{
// 1. 设置时钟中断门:中断号 0x20 → timer_interrupt
set_intr_gate(0x20, &timer_interrupt);
// 2. 允许时钟中断:修改 8259A 屏蔽寄存器,打开 IRQ0
outb(inb_p(0x21) & ~0x01, 0x21);
// 3. 设置系统调用中断门:0x80 → system_call
set_system_gate(0x80, &system_call);
}
3.时钟中断处理(kernel/sched.c + system_call.s)
汇编入口:
assembly
; kernel/system_call.s
_timer_interrupt:
; 保存现场(寄存器压栈)
call _do_timer ; 调用 do_timer 函数
; 恢复现场
iret ; 从中断返回
C 处理函数:
// kernel/sched.c
void do_timer(long cpl)
{
// cpl:当前进程特权级(0=内核态,3=用户态)
// 1. 更新系统时间
total++; // 系统总滴答数
// 2. 当前进程时间片减 1
if (--current->counter > 0)
return; // 时间片没完,继续执行当前进程
// 3. 时间片用完,调度
schedule();
}
4.调度器(kernel/sched.c)
// kernel/sched.c
void schedule(void)
{
int i, next;
struct task_struct **p;
// 1. 遍历所有进程,找 counter 最大的进程
next = 0;
for (p = &LAST_TASK; p > &FIRST_TASK; --p) {
if (*p && (*p)->counter > (*p)->counter) {
next = i;
}
}
// 如果所有进程 counter 都等于 0,重新计算
if (!next) {
for (p = &LAST_TASK; p > &FIRST_TASK; --p) {
if (*p) {
(*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
}
}
}
// 2. 切换到下一个进程
switch_to(next);
}
1.3死循环
操作系统平时在干啥?啥也不干。那它怎么工作的?靠中断。所以操作系统的本质可以理解为:一个被中断唤醒的死循环。
在 Linux 0.11 的 main.c 里,最后就是这样一个死循环:
void main(void)
{
// 各种初始化(内存管理、中断、调度等)
// ...
for (;;)
pause(); // 啥也不干,等中断唤醒
}
pause() 是一个系统调用,作用是让 CPU 进入暂停状态,直到有信号或中断到来才醒来。操作系统自己不做任何事情,需要什么功能就往中断向量表里添加对应的方法就行了。这样一来,操作系统就可以在硬件时钟中断的驱动下,自动完成进程调度。进程的时间片用完了,时钟中断会主动通知操作系统去切换进程,不需要操作系统主动去管。
补充两点:
时间片:就是每次时钟中断之间,进程可以连续执行的时间。时间片到了,时钟中断触发,操作系统就能拿走 CPU 控制权去调度别的进程。
CPU 主频:就是 CPU 每秒可以执行的指令数量。主频越快,单位时间内能执行的指令就越多,操作系统的调度响应速度也会更快,因为系统能更快地响应和处理各种中断事件。主频可以作为 OS 调度执行速度的参考之一。
1.4软中断
前面说的硬件中断,都是外部设备(键盘、磁盘、网卡)触发的。那有没有可能,因为软件原因也让 CPU 走类似的中断流程?有。就是软中断。
软中断就是软件主动触发的中断,目的是让 CPU 从用户态切到内核态,执行操作系统提供的服务。系统调用就是最典型的例子——用户程序想读写文件、创建进程,这些操作只有内核有权限,用户态干不了,必须靠软中断"陷"进内核里去。
系统调用号与系统调用表
系统调用号是内核提供的,不是 glibc 提供的。内核在源码里定义好每个系统调用对应的编号,比如:
// 源代码路径:linux-2.6.18/include/asm-x86_64/unistd.h
#define __NR_read 0
__SYSCALL(__NR_read, sys_read)
#define __NR_write 1
__SYSCALL(__NR_write, sys_write)
#define __NR_open 2
__SYSCALL(__NR_open, sys_open)
#define __NR_close 3
__SYSCALL(__NR_close, sys_close)
#define __NR_stat 4
__SYSCALL(__NR_stat, sys_newstat)
#define __NR_fstat 5
__SYSCALL(__NR_fstat, sys_newfstat)
#define __NR_lstat 6
__SYSCALL(__NR_lstat, sys_newlstat)
#define __NR_poll 7
__SYSCALL(__NR_poll, sys_poll)
#define __NR_lseek 8
__SYSCALL(__NR_lseek, sys_lseek)
#define __NR_mmap 9
__SYSCALL(__NR_mmap, sys_mmap)
#define __NR_mprotect 10
__SYSCALL(__NR_mprotect, sys_mprotect)
#define __NR_munmap 11
__SYSCALL(__NR_munmap, sys_munmap)
#define __NR_brk 12
__SYSCALL(__NR_brk, sys_brk)
#define __NR_rt_sigaction 13
__SYSCALL(__NR_rt_sigaction, sys_rt_sigaction)
// ... 几百个
__NR_xxx是系统调用号,sys_xxx是内核里对应的实现函数。系统调用表本质上就是个函数指针数组,系统调用号就是数组下标。
glibc 的封装
我们平时写代码调用 read()、write()、open(),从来没见过 int 0x80 或 syscall,因为 glibc 把几乎所有的系统调用都封装成了普通的 C 函数。glibc 内部有个宏:
#define SYS_ify(syscall_name) __NR_##syscall_name
作用是把函数名转成系统调用号。比如 SYS_ify(open) 展开后就是 __NR_open,也就是 2。
glibc 的封装层大概长这样(简化了一下):
int open(const char *pathname, int flags)
{
// 1. 把系统调用号 __NR_open 放到寄存器 EAX
// 2. 把参数放到其他寄存器
// 3. 执行 int 0x80 或 syscall 指令
// 4. 检查返回值,出错就设置 errno
// 5. 返回结果
}
整个过程:
你调用 open() 时,glibc 封装层先把函数名转成系统调用号(open 对应 __NR_open,数值是 2),然后把系统调用号放进寄存器(x86 是 EAX,x86_64 是 RAX),再执行 int 0x80(x86)或 syscall(x86_64)指令触发软中断。CPU 收到后自动从用户态切到内核态,内核根据寄存器里的系统调用号去系统调用表里找到对应的 sys_open 函数执行,干完活把结果返回给 glibc,glibc 再返回给你。
如果绕过 glibc 直接调
可以用 syscall() 函数:
#include <sys/syscall.h>
#include <unistd.h>
int main()
{
// 直接调 write,不经过 glibc 封装
syscall(__NR_write, 1, "hello\n", 6);
return 0;
}
或者直接写汇编:
// x86_64 下
asm volatile(
"mov $1, %%rax\n" // write 的系统调用号是 1
"mov $1, %%rdi\n" // fd = 1
"mov $msg, %%rsi\n" // buf
"mov $6, %%rdx\n" // count
"syscall\n" // 触发软中断
:
: "m"(msg)
);
不过一般没人这么干,glibc 封装得好好的,直接用就行。
软中断和硬件中断的对比
| 硬件中断 | 软中断 | |
|---|---|---|
| 谁触发的 | 外部设备(键盘、磁盘、网卡) | 软件主动触发 |
| 什么时候来 | 异步,设备就绪时 | 同步,执行到 int 0x80/syscall 时 |
| 用来干嘛 | 响应外部事件 | 从用户态切到内核态 |
信号的软中断本质
信号本质上也是一种软中断。进程调用
kill()发信号,内核修改目标进程的 pending 位图,等目标进程下次从内核态返回用户态时,发现有信号待处理,就"打断"正常执行流去执行信号处理函数。区别:系统调用是用户主动陷入内核,信号是内核主动打断用户。
小结:
软中断就是用软件模拟硬件中断。系统调用是最典型的例子——用户程序把系统调用号放进寄存器,执行
int 0x80/syscall,CPU 切到内核态,内核根据系统调用号查系统调用表执行对应函数,干完活再切回来。
图解如下:

1.5 缺页中断、除零、野指针错误
前面说的硬件中断是外部设备触发的,软中断是软件主动触发的。还有一类:CPU 执行指令的时候出错了,也会触发中断。Linux 内核把这叫陷阱(trap)或异常(exception),但它们走的流程和硬件中断一样——保存现场、查中断向量表、跳转到处理函数执行。
Linux 0.11 初始化的时候,把这些异常的处理函数都绑定了:
void trap_init(void)
{
set_trap_gate(0, ÷_error); // 除零
set_trap_gate(1, &debug); // 调试
set_trap_gate(2, &nmi); // 不可屏蔽中断
set_system_gate(3, &int3); // 断点
set_system_gate(4, &overflow); // 溢出
set_system_gate(5, &bounds); // 边界检查
set_trap_gate(6, &invalid_op); // 无效指令
set_trap_gate(7, &device_not_available);
set_trap_gate(8, &double_fault);
set_trap_gate(9, &coprocessor_segment_overrun);
set_trap_gate(10, &invalid_TSS);
set_trap_gate(11, &segment_not_present);
set_trap_gate(12, &stack_segment);
set_trap_gate(13, &general_protection);
set_trap_gate(14, &page_fault); // 缺页异常
set_trap_gate(15, &reserved);
set_trap_gate(16, &coprocessor_error);
for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
}
比如:
除零:CPU 发现除法指令分母是 0,触发 0 号中断,处理函数给进程发 SIGFPE 信号,进程默认终止。
缺页异常(14 号):进程访问的虚拟地址没有对应的物理页,处理函数去申请内存、填页表,然后重新执行刚才那条指令。虚拟内存的按需分配就是靠它实现的。
野指针:进程访问了非法地址,处理函数发现地址不合法,发 SIGSEGV 干掉进程,你就看到 "Segmentation fault"。
所以缺页、除零、野指针这些问题,最后都变成了 CPU 内部的异常,走中断向量表查到的处理例程来处理。有的申请内存填页表,有的直接杀进程。
三个概念的区别
| 术语 | 谁触发的 | 例子 |
|---|---|---|
| 硬件中断 | 外部设备 | 键盘、磁盘、时钟 |
| 陷阱(Trap) | 软件主动 | int 0x80、syscall |
| 异常(Exception) | CPU 执行指令出错 | 除零、缺页、野指针 |
内核源码里
trap_init()这个函数名,就是初始化这些 CPU 内部异常和陷阱的。缺页叫"缺页异常"不叫"缺页中断",因为它不是外部设备触发的,是 CPU 执行指令时发现的问题,所以名字里带"异常"。
小结
操作系统就是挂在中断向量表后面那堆处理函数。硬件中断管外设,陷阱管系统调用,异常管程序错误。不管哪种,流程都一样:保存现场、查表、执行、恢复。
二. 如何理解内核态和⽤户态
再谈,如何理解内核态和用户态
1.用户态和内核态的本质

Linux 每个进程都有 4GB 的虚拟地址空间(32 位),其中 0~3GB 是用户空间,3~4GB 是内核空间。
用户态和内核态的本质就是:
用户态:CPU 执行的代码在用户空间(0~3GB)里,只能访问自己的用户空间
内核态:CPU 执行的代码在内核空间(3~4GB)里,可以访问任何地址
CPU 怎么区分当前是用户态还是内核态?靠的是 CPL(Current Privilege Level,当前特权级别):
CPL = 0:内核态,最高权限
CPL = 3:用户态,受限权限
CPU 执行指令时会检查 CPL 和访问地址的权限,如果用户态程序访问内核空间,CPU 会触发保护异常(13号中断),走通用保护错误处理,干掉进程。
2.用户态怎么切到内核态?
用户程序想读写文件、创建进程,这些操作只有内核能做,用户态权限不够。怎么切?
通过 int 0x80(x86)或 syscall(x86_64)指令触发软中断,CPU 自动做几件事:
把当前 CPL 从 3 改成 0;
切换到内核栈;
跳转到中断向量表里对应的系统调用入口;
执行内核代码;;
干完活切换回用户态
这个切换过程叫 陷入内核。为啥叫陷入?因为程序主动钻进去了。
3.一个关键点
操作系统无论怎么切换进程,都能找到同一个操作系统。
因为所有进程的虚拟地址空间中,3~4GB 这块内核空间是共享的、相同的,所有进程看到的都是同一份内核代码和数据。
所以系统调用方法的执行,是在当前进程的地址空间中执行的——准确地说,是在进程地址空间的内核部分执行的。系统调用执行的时候,当前进程还在,只是 CPU 权限提升了,执行的是内核代码,改的是内核数据,但这些代码和数据映射在进程地址空间的 3~4GB 里。
一个你可能想过的问题
操作系统切换进程的时候,内核代码跑在哪?
答案:跑在当前进程的地址空间里。
所有进程的虚拟地址空间里,3~4GB 这块内核空间是共享的、一样的。不管切到哪个进程,内核代码都在同一个位置。
所以系统调用的本质就是:在某个进程的地址空间里,执行内核的那部分代码。 进程还是那个进程,只是 CPU 权限提升了,跑的是内核代码
换个角度理解:
进程 A 调用 read() -->
执行 int 0x80 / syscall -->
CPU 把 CPL 从 3 改成 0(切到内核态)-->
CPU 跳转到系统调用入口(查 0x80 中断向量表)-->
内核根据系统调用号(read=0)查系统调用表找到 sys_read -->
注意此时 CPU 依然在进程 A 的地址空间里,只是访问的是 3~4GB 那块内核空间(所有进程共享)-->
执行 sys_read 内核代码(读文件、拷数据)->
干完活 CPU 把 CPL 从 0 改回 3(切回用户态)-->
返回进程 A 的用户空间,继续执行 read 后面的代码。
系统调用执行期间,CPU 依然在进程 A 的地址空间里,只是访问的是 3~4GB 那一块。内核代码并不是独立存在的,而是映射到每个进程的高 1GB 空间里。
4.对比
| 用户态 | 内核态 | |
|---|---|---|
| 访问范围 | 0~3GB(用户空间) | 3~4GB(内核空间) |
| CPL 值 | 3 | 0 |
| 能做什么 | 普通计算,不能访问硬件 | 读写文件、管理内存、操作外设 |
| 怎么切换 | 执行 int 0x80 / syscall |
内核干完活自动切回来 |
5.关于特权级
CPU 用特权级来限制不同代码的权限,x86 有 0~3 四个级别,数字越小权限越大。Linux 只用两个:0 级(内核态)和 3 级(用户态)。
CPU 在执行代码时会检查几个东西:
CPL(Current Privilege Level):当前正在执行的代码在哪个特权级,也就是 CPU 当前处于用户态还是内核态。
DPL(Descriptor Privilege Level):某个资源(比如内存段、中断门)要求调用者必须具备的最低调权级。比如内核空间的 DPL 是 0,用户态(CPL=3)想访问就会被 CPU 拦住。
RPL(Requestor Privilege Level):访问某个资源时,当前进程实际持有的权限,用于更精细的权限校验,可以理解为“我声称自己是谁”的权限级别。
当一个用户程序(CPL=3)试图访问内核数据(DPL=0)时,CPU 会检查 CPL ≤ DPL 是否成立——不成立就触发保护异常(13号中断),把这个进程干掉,这就是“Segmentation Fault”的底层来源。
系统调用能成功,是因为
int 0x80/syscall对应的中断门 DPL 是 3,允许用户态程序调用,但一旦触发,CPU 自动把 CPL 从 3 切到 0,然后才能执行内核代码。段、段描述符这些是 x86 的历史包袱,为了兼容老代码搞得非常复杂,知道这些概念就够了,深究性价比不高。
6.小结
用户态就是执行用户空间代码时的状态,内核态就是执行内核空间代码时的状态。CPU 通过 CPL 来区分,用户态想切内核态就靠
int 0x80或syscall触发软中断。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)