硬件中断
在我们使用键盘的时候,操作系统要怎么知道键盘上有数据了呢?硬件中断!
硬件中断过程如图所示:
按照图中所示,外设直接与CPU进行交互,但是之前对于冯诺依曼体系架构的学习可知,外设要和CPU交互必须要通过内存,那么怎么做到的呢?
有两个颜色的信号交互线路,分别是控制信号和数据信号。之前的描述中主要是对于数据的拷贝线路进行的学习,实际上还有一个控制信号的线路,只要进行传输控制信号即可,无需拷贝数据,所以外设可以和CPU进行交互。主要是传输信息!
外设和中央处理器之间如何连接通信呢?
上图为CPU。会有很多针脚,针脚与主板相连,外设也会和主板连接。CPU的针脚有相当一部分是用来与外设交互的。
再了解一下寄存器的概念:
当我们向磁盘发送数据,想在磁盘进行存储,假如指令in 100(扇区编号) XXXX(数据)
。
磁盘要怎么往扇区存储呢?
实际上磁盘有磁盘控制器,里面会有各个不同功能的寄存器!
拓展一下,外设都有自己的控制器!
继续理解硬件中断的过程图示:
- **外设就绪:**硬件交互的信号也就是高低电平,外设准备就绪。
- **发起中断:**通过电平变化,在中断控制器产生中断,并且产生中断号(外设特有的编号)
- **通知CPU:**结合上述关于控制信号发送和寄存器的概念,通知CPU有中断产生
- **CPU得知中断,获取中断号:**CPU收到通知,但是要处理中断需要得到中断号。CPU在在中断控制器得到中断号,现在CPU就知道哪一个中断号准备好了。
- **根据中断号,执行中断处理方法:**现在已经得到哪一个设备中断了,需要具体的处理方法。操作系统提供了中断向量表,就是一个函数指针数组,中断号对应的就是不同下标,每个中断号有着不同的函数方法。根据中断号进行查找方法,然后执行。(中断向量表是操作系统的一部分,操作系统启动后记忆加载到内存中了)
- 中断完毕,继续之前的工作。
这一套过程可以得到:
- 操作系统不会关注外设是否准备好,而是外设准备好之后通知操作系统。
- 过程很熟悉,与信号的过程很像,发信号,信号编号,保存信号,处理信号
内核代码:
// Linux内核0.11源码
void trap_init(void)
{
int i;
set_trap_gate(0, &_error); // 设置除操作出错的中断向量值。以下雷同。
set_trap_gate(1, &debug);
set_trap_gate(2, &nmi);
set_system_gate(3, &int3); /* int3-5 can be called from all */
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);
// 下面将 int 17-47 的陷阱门先均设置为 reserved,以后每个硬件初始化时会重新设置自己的陷阱门。
for (i = 17; i < 48; i++)
set_trap_gate(i, &reserved);
set_trap_gate(45, &irq13); // 设置协处理器的陷阱门。
outb_p(inb_p(0x21) & 0xfb, 0x21); // 允许主 8259A 芯片的 IRQ2 中断请求。
outb(inb_p(0xA1) & 0xdf, 0xA1); // 允许从 8259A 芯片的 IRQ13 中断请求。
set_trap_gate(39, ¶llel_interrupt); // 设置并行口的陷阱门。
}
void rs_init(void)
{
set_intr_gate(0x24, &rs1_interrupt); // 设置串口 1 的中断门向量 (硬件 IRQ4 信号)。
set_intr_gate(0x23, &rs2_interrupt); // 设置串口 2 的中断门向量 (硬件 IRQ3 信号)。
init(tty_table[1].read_q.data); // 初始化串口 1 (.data 是端口号)。
init(tty_table[2].read_q.data); // 初始化串口 2。
outb(inb_p(0x21) & 0xE7, 0x21); // 允许主 8259A 芯片的 IRQ3,IRQ4 中断信号请求。
}
时钟中断
- 在没有中断的时候,操作系统在做什么?
- 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
- 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?
在没有中断的时候,操作系统在做什么?
时钟中断怎么进行
- 运行 Idle 进程
- 当所有可运行的用户/内核进程都已运行完时间片或被阻塞,没有其他事情可做时,CPU 会进入所谓的 “idle”(空闲)进程。
- 在 x86 上,idle 里常见写法大致是:
for (;;) {
asm("hlt"); // 让 CPU 进入低功耗等待状态,直到下一个中断到来
}
或者:
for (;;) {
pause(); // 类似 hlt,也会在硬件中断到来前挂起
}
- **目的**<font style="color:rgb(31,35,41);">:减少功耗,不浪费 CPU 周期在“忙等”上。</font>
void main(void) /* 这里确实是 void,并没有错。 */
{
/* 在 startup 程序(head.s)中就是这样假设的。 */
// 进入无限循环,等待调度器决定是否需要执行其他任务
for (;;)
pause(); // 'pause()' 等待一个信号的到来(例如,任务切换或者硬件中断)
/*
注意!! 对于任何其它的任务,'pause()' 将意味着我们必须等待收到一个信号才会返回就绪运行态。
然而,任务0(task0)是唯一的例外情况(参见 'schedule()')。因为任务0在任何空闲时间都会被激活(当没有其它任务在运行时)。
对于任务0,'pause()' 仅意味着我们返回来查看是否有其它任务可以运行,如果没有的话,我们就回到这里,继续循环执行 'pause()'。
*/
} // end main
- 后台守护与内核线程
- 一些内核线程(如 kswapd、ksmd、kworker 等)会被唤醒去做后台清理、内存回收、延迟任务执行等。
- 但如果它们也都阻塞起来,则真正就只剩下 idle 持续运行。
当没有任何外部中断(时钟、I/O、系统调用陷阱等)时,CPU 就一直跑到图最右侧的 <font style="color:rgb(31,35,41);">for(;;) pause()</font>
,等待下一个“蓝色箭头”打来的时钟中断。
- 时钟源
- 外部设备一般包括时钟源,时钟源会以特定的频率,向CPU发送特定的中断。
- 一旦定时器中断到来,CPU 自动:
- 保存现场(用寄存器保存当前进程状态)
- 切换到内核栈
- 根据中断号查 IDT(Interrupt Descriptor Table)
- 跳转到对应的中断服务例程
- IDT就是中断向量表,存放不同中断信号所对应的服务例程方法
- 当中断后就会跳转到中断服务例程中的进程调度,专门用来为时钟中断服务,保证操作系统的持续运行,持续进行进程调度等一系列操作系统的基础操作。也就是在空闲的时候去以特定的频率在全局数据结构中扫一遍,查看有没有需要处理的事情
- 也可以理解,操作系统就是:基于中断进行工作的软件。
时钟源以恒定频率发中断(比如 1 ns 一次),这个就是CPU的主频。每个进程被分配一个固定的“滴答数”作时间片(比如 10 个滴答 = 10 ns),每次中断既减少当前进程的剩余时间片,也把全局计数器
total
(jiffies)加一,这样既能实现进程的公平调度,又能即使离线也可以精确地统计系统运行时间。
时钟中断和进程调度核心流程概述
- 硬件定时器中断的注册:
- 内核在启动时设置好与时钟中断相关的处理程序。通过
set_intr_gate
将定时器中断(IRQ0)与处理函数(timer_interrupt
)关联。这相当于告诉内核,当硬件定时器发出中断信号时,应该跳转到哪个函数进行处理。
- 内核在启动时设置好与时钟中断相关的处理程序。通过
- 中断处理入口:
- 每次硬件定时器触发时,CPU 会进入中断处理程序。中断向量表将控制权传递给
timer_interrupt
入口,CPU 会保存现场,允许处理函数执行。此时的中断处理并不直接切换到其他任务,而是先通过汇编指令跳转到 C 语言的do_timer
函数。
- 每次硬件定时器触发时,CPU 会进入中断处理程序。中断向量表将控制权传递给
- 时间片管理:
void do_timer(void) {
/* 更新全局时钟节拍 */
total++; // jiffies++,记录自开机以来的中断总次数
// 让“脱机”(OS 未运行)时的时间也能被累计
/* 对当前进程的时间片计数 */
if (--current->counter > 0)
return; // 进程的时间片还没用完,直接退出中断
/* 时间片耗尽,进行进程调度 */
current->counter = DEFAULT_TIMESLICE; // 重置下次时间片(图中用 struct task_struct.count)
schedule(); // 进行真正的上下文切换
}
- 在 `do_timer` 函数中,内核会检查当前进程的剩余时间片(`current->counter`)。如果该进程的时间片还没用完,就直接返回继续执行当前进程。
- 如果时间片已经耗尽,内核会为进程重置时间片,并调用 `schedule()` 来触发进程调度。通过调度器选择下一个准备好的进程执行。
- 调度与上下文切换:
<font style="color:rgb(31,35,41);">schedule()</font>
会遍历就绪队列,挑选优先级最高或公平调度的下一个进程。<font style="color:rgb(31,35,41);">switch_to()</font>
保存当前进程的寄存器/栈指针,恢复下一个进程的上下文。- 上下文切换完成后,执行
<font style="color:rgb(31,35,41);">iret</font>
,回到新进程的用户态或内核态继续运行。
- 循环与响应:
- 该过程会不断循环进行,确保系统在执行多个进程时能合理地分配 CPU 时间。如果没有其他进程需要执行,CPU 将进入 idle 进程,并继续等待下一个时钟中断。
操作系统自己被谁指挥、被谁推动执行?
- “事件驱动”模型
- Linux 内核并不是一个自扫描的“大循环”,而是被各种“事件”唤醒执行。这些事件主要分三类:
- 硬件中断(timer、网卡、键盘、磁盘等)
- 异常/陷阱(系统调用
<font style="color:rgb(31,35,41);">int 0x80</font>
/<font style="color:rgb(31,35,41);">syscall</font>
、页错误、非法指令等) - 软中断/底半部(
<font style="color:rgb(31,35,41);">raise_softirq()</font>
、<font style="color:rgb(31,35,41);">tasklet</font>
、<font style="color:rgb(31,35,41);">workqueue</font>
等)
- Linux 内核并不是一个自扫描的“大循环”,而是被各种“事件”唤醒执行。这些事件主要分三类:
- 具体流程
- 进程态 → 内核态:当用户进程执行到
<font style="color:rgb(31,35,41);">syscall</font>
指令,CPU 会做一次“软中断”,转到内核的系统调用入口。 - 中断处理:当硬件发中断时,CPU 保存现场后,跳到对应的中断服务例程(IDT 中的相应中断门)。
- 中断/系统调用处理完毕,通过
<font style="color:rgb(31,35,41);">iret</font>
或<font style="color:rgb(31,35,41);">sysret</font>
返回到原来被抢占或陷入的进程。
- 进程态 → 内核态:当用户进程执行到
- 总结:
操作系统“自己”并没有一个独立的“调度者”,而是被外部与内部的“事件”驱动——每来了一个中断或陷阱,就把控制权交给内核。
有没有自己可以定期触发的设备?
- 定时器芯片(PIT、HPET、APIC timer)
- Linux 在启动时会编程硬件定时器,让它们以固定频率(HZ,通常 100、250 或 1000Hz)自动产生中断。
- 例如:
- PIT(Programmable Interval Timer)
- IO-APIC/LAPIC Timer(本地/APIC 定时器)
- HPET(High Precision Event Timer)
- 无需人为或 I/O 触发,它们自己“滴答”——并把 IRQ0(或 APIC 定时中断)发给中断控制器。
- RTC(Real-Time Clock)周期中断
- 另一种定期中断来源是实时时钟芯片(CMOS RTC),可以配置为每秒或每几分之一秒产生一次 IRQ8。
- Linux 也可以利用 RTC 来做秒级或更低频率的周期唤醒
这样操作系统就可以在硬件时钟的推动下进行自动调度了~
软中断
想象一下,你正在编写一个简单的程序,比如读取一个文件并打印其内容。这个看似简单的操作,背后却隐藏着操作系统内核的复杂工作。你的程序运行在用户空间 (User Space),一个相对受限的环境;而文件系统、硬件设备等核心资源则由内核空间 (Kernel Space) 掌控,拥有最高权限。那么,用户程序如何安全、可控地请求内核来完成这些特权操作呢?答案就是通过系统调用 (System Call),而系统调用的实现,很大程度上就依赖于我们今天要讲的软中断。
中断:CPU的“请注意”信号
在深入软中断之前,我们先快速回顾一下什么是“中断”。你可以把中断想象成一种信号,它会打断 CPU 当前正在执行的任务,要求 CPU 立即关注并处理一个更紧急或特殊的事件。
中断主要分为两类:
- 硬件中断 (Hardware Interrupt): 由外部硬件设备(如键盘敲击、鼠标移动、网卡收到数据包、硬盘完成读写)产生。这些事件是异步的,发生时间不可预测。
- 软中断 (Software Interrupt): 由 CPU 内部执行的软件指令触发。它们是同步的,发生时间点就在指令执行的那一刻。
上文已经讲解了关于硬件中断的相关知识点,我们今天要聚焦的就是第二种——软中断。
初步了解软中断
CPU 设计了对应的汇编指令 (
int
或者syscall
), 可以让 CPU 内部触发中断逻辑。
没错,软中断的核心就是 CPU 提供了一些特殊的指令,允许正在运行的程序主动“中断”自己,将控制权交给预先定义好的处理程序(通常是操作系统内核的一部分)。
- 在经典的 x86 架构上,
INT n
指令就是用来触发软中断的,其中n
是一个中断号(0-255)。Linux 早期广泛使用INT 0x80
作为系统调用的入口。 - 随着 CPU 发展,为了提高效率,引入了更快的专用指令,如
SYSENTER
(配合SYSEXIT
) 和SYSCALL
(配合SYSRET
)。
无论使用哪种指令,效果都是类似的:暂停当前的用户程序,切换到更高权限的内核模式,并跳转到指定的中断处理程序。
系统调用
软中断最广为人知的应用场景就是实现系统调用。让我们跟随一次典型的系统调用(比如 read
文件),来一场“从用户空间到内核再返回”的深度游:
第一站:用户空间 - 请求发起
- 应用程序员视角: 你在代码里调用了一个库函数,比如 C 语言的
read(fd, buffer, count);
。 - C库 (glibc) 视角: 你调用的
read()
函数并非直接操作硬件。它是一个封装层。它的主要工作是:- 确定
read
操作对应的系统调用号(这是一个事先约定好的数字,例如,在 x86-64 Linux 中,read
的号是 0)。 - 下图为调用系统调用是系统调用号对应的函数指针表:
- 确定
/* 系统调用函数指针表,用于系统调用中断处理程序 (int 0x80) 作为跳转表 */
static fn_ptr sys_call_table[] = {
/* 0 - 9 */
sys_setup, /* 0 */
sys_exit, /* 1 */
sys_fork, /* 2 */
sys_read, /* 3 */
sys_write, /* 4 */
sys_open, /* 5 */
sys_close, /* 6 */
sys_waitpid, /* 7 */
sys_creat, /* 8 */
sys_link, /* 9 */
/* 10 - 19 */
sys_unlink, /* 10 */
sys_execve, /* 11 */
sys_chdir, /* 12 */
sys_time, /* 13 */
sys_mknod, /* 14 */
sys_chmod, /* 15 */
sys_chown, /* 16 */
sys_break, /* 17 */
sys_stat, /* 18 */
sys_lseek, /* 19 */
/* 20 - 29 */
sys_getpid, /* 20 */
sys_mount, /* 21 */
sys_umount, /* 22 */
sys_setuid, /* 23 */
sys_getuid, /* 24 */
sys_stime, /* 25 */
sys_ptrace, /* 26 */
sys_alarm, /* 27 */
sys_fstat, /* 28 */
sys_pause, /* 29 */
/* 30 - 39 */
sys_utime, /* 30 */
sys_stty, /* 31 */
sys_gtty, /* 32 */
sys_access, /* 33 */
sys_nice, /* 34 */
sys_ftime, /* 35 */
sys_sync, /* 36 */
sys_kill, /* 37 */
sys_rename, /* 38 */
sys_mkdir, /* 39 */
/* 40 - 49 */
sys_rmdir, /* 40 */
sys_dup, /* 41 */
sys_pipe, /* 42 */
sys_times, /* 43 */
sys_prof, /* 44 */
sys_brk, /* 45 */
sys_setgid, /* 46 */
sys_getgid, /* 47 */
sys_signal, /* 48 */
sys_geteuid, /* 49 */
/* 50 - 59 */
sys_getegid, /* 50 */
sys_acct, /* 51 */
sys_phys, /* 52 */
sys_lock, /* 53 */
sys_ioctl, /* 54 */
sys_fcntl, /* 55 */
sys_mpx, /* 56 */
sys_setpgid, /* 57 */
sys_ulimit, /* 58 */
sys_uname, /* 59 */
/* 60 - 69 */
sys_umask, /* 60 */
sys_chroot, /* 61 */
sys_ustat, /* 62 */
sys_dup2, /* 63 */
sys_getppid, /* 64 */
sys_getpgrp, /* 65 */
sys_setsid, /* 66 */
sys_sigaction,/* 67 */
sys_sgetmask, /* 68 */
sys_ssetmask, /* 69 */
/* 70 - 77 */
sys_setreuid, /* 70 */
sys_setregid /* 71 */
/* 如果有更多 syscall,请在此继续添加并更新区间注释 */
};
- 将这个系统调用号放入指定的寄存器(通常是 `EAX` 或 `RAX`)。
- 将函数的参数(文件描述符 `fd`、缓冲区 `buffer` 的地址、要读取的字节数 `count`)按照 **ABI (Application Binary Interface)** 的约定,放入其他指定的寄存器(如 `RDI`, `RSI`, `RDX` 等)或压入堆栈。
- 执行触发软中断的指令,比如 `syscall`。
第二站:模式切换 - “陷阱”之门
系统调用的过程,其实就是先使用
int 0x80
、syscall
陷入 (Trap) 内核,本质就是触发软中断…
- 当 CPU 执行到
syscall
(或INT 0x80
) 指令时,奇妙的事情发生了:- 权限提升: CPU 的运行模式从用户模式 (Ring 3) 切换到内核模式 (Ring 0)(后续总结讲解用户态和内核态)。
- 状态保存: CPU 自动保存当前用户程序的一些关键状态,至少包括:下一条指令的地址 (Instruction Pointer, 如 RIP/EIP)、代码段寄存器 (CS)、标志寄存器 (RFLAGS/EFLAGS)。用户堆栈指针 (RSP/ESP) 和段寄存器 (SS) 通常也会被保存(或切换到内核堆栈时隐式保存)。
- 寻找处理程序: CPU 使用
syscall
指令(或INT 0x80
的中断号 0x80)作为索引,去查询一个特殊的数据结构——中断描述符表 (IDT - Interrupt Descriptor Table)。IDT 中存储了每个中断号对应的处理程序的入口地址和所需权限等信息。 - 跳转执行: CPU 加载 IDT 中找到的内核态代码段和指令指针,跳转到内核的系统调用入口处理程序 (System Call Entry Handler)。
(你可以想象图示中,有一条从用户态执行 int 0x80/syscall
指令,穿过用户态/内核态边界,指向 IDT,再由 IDT 指向内核中特定处理代码的路径。)
第三站:内核空间 - 请求处理
- 系统调用分发器 (Dispatcher): 内核的入口处理程序(如汇编代码
_system_call
)接管控制权。它的任务是:- 保存更完整的用户上下文:将用户态的通用寄存器(如 RBX, RCX, RDX, RDI, RSI, RBP 等)压入内核堆栈。
- 从
RAX
(或EAX
) 寄存器中读取之前 C 库放入的系统调用号。 - 查表: 使用这个系统调用号作为下标,在内核维护的系统调用表 (
sys_call_table
) 中查找。操作系统不会提供任何系统调用接口,只提供系统调用号。
• 系统调用号的本质:数组下标!
•fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read, ... };
•call [_sys_call_table + eax * 4]
(或call sys_call_table[,%rax,8]
在 64 位)- 上文已经提到过。
这个表里存放的是指向具体内核实现函数(如 sys_read
, sys_write
, sys_open
等)的指针。
- 执行内核函数: 分发器调用
sys_call_table
中找到的函数指针,也就是执行sys_read
。 - 过程也就是:当使用系统调用函数后,函数内会将该系统调用的系统调用号mov到eax寄存器,然后使用
int 0x80
或者syscall
陷入内核,在系统调用表找到对应的系统调用函数执行。 - 真正的内核工作:
sys_read
函数会执行真正的文件读取逻辑,这可能涉及到:- 检查文件描述符的有效性和权限。
- 与虚拟文件系统 (VFS) 层交互。
- 通过文件系统层找到数据在磁盘的位置。
- 与块设备层和磁盘驱动程序交互,发起 I/O 请求。
- 等待 I/O 完成(期间当前进程可能会被挂起,让 CPU 去做其他事)。
- 将读取到的数据从内核缓冲区拷贝到用户传入的
buffer
地址。 - 准备返回值(实际读取的字节数,或出错时的错误码)。
第四站:返回用户空间 - 功成身退
- 内核函数返回:
sys_read
执行完毕,将返回值(比如成功读取的字节数)放入RAX
(或EAX
) 寄存器。 - 恢复上下文: 系统调用分发器从内核堆栈中恢复之前保存的用户通用寄存器。
- 执行返回指令: 执行特殊的返回指令,如
sysret
(对应syscall
) 或iret
(对应INT
)。 - CPU 的返程操作:
- 权限降低: CPU 运行模式从内核模式切换回用户模式。
- 状态恢复: CPU 自动恢复之前保存的用户态指令指针、代码段、标志寄存器、堆栈指针和段寄存器。
- 回到 C 库: 控制权回到用户空间的 C 库函数 (
read
) 中,就在syscall
指令之后。 - 返回应用程序: C 库函数从
RAX
取出内核返回的结果,并将其作为read()
函数的返回值,返回给你的应用程序。
至此,一次完整的系统调用结束。整个过程虽然复杂,但通过软中断机制,实现了用户空间到内核空间的安全、受控的“穿越”。
为何我们感受不到 INT 0x80
/syscall
?
因为 Linux 的 GNU C 标准库,给我们把⼏乎所有的系统调⽤全部封装了。
正如课件所说,我们平时编程依赖的标准库(如 Linux 下的 glibc,Windows 下的 ntdll.dll 或 kernel32.dll)为我们隐藏了这些底层细节。库函数就像是“系统调用代理人”,负责处理参数传递、触发软中断、获取结果等所有繁琐步骤。这使得应用程序员可以专注于业务逻辑,而无需关心底层的中断和模式切换。
不仅仅是系统调用:CPU 的“异常”信号
软中断的范畴并不局限于程序员主动发起的系统调用。CPU 在执行指令时,如果遇到无法处理的错误或需要特殊处理的情况,也会触发内部中断。这些通常被称为异常 (Exceptions)。
缺页中断?内存碎片处理?除零野指针错误?这些问题,全部都会被转换成为CPU内部的软中断… CPU内部的软中断,比如除零/野指针等,我们叫做 异常 (Exception)… CPU内部的软中断,比如
int 0x80
或者syscall
,我们叫做 陷阱 (Trap)。
这个区分很有用。我们可以进一步细化一下:
- 陷阱 (Trap): 通常是有意触发的中断,用于调用某种服务或功能。
INT 0x80
/syscall
就是典型的陷阱。调试断点指令 (INT 3
) 也是陷阱。执行完陷阱处理程序后,通常会返回到陷阱指令的下一条指令继续执行。 - 故障 (Fault): 通常由错误条件引起,但可能是可恢复的。最典型的例子就是缺页故障 (Page Fault)。当程序访问一个有效但当前不在物理内存中的页面时,会触发 Page Fault。操作系统会介入,将页面从磁盘加载到内存,然后重新执行导致故障的那条指令。除零错误、无效操作码、段错误(访问非法内存地址)、保护错误(权限不足)等也常被归为故障。如果故障无法恢复,操作系统可能会终止进程。
- 中止 (Abort): 表示发生了严重的、通常不可恢复的错误,比如硬件错误或系统表不一致。程序无法继续执行,通常会被强制终止。
无论是陷阱、故障还是中止,它们都使用与系统调用类似的机制:CPU 检测到事件 -> 保存状态 -> 查 IDT -> 跳转到内核处理程序。操作系统在初始化时(如课件中的 trap_init
函数)会为这些预定义的异常编号设置好相应的处理函数入口。
// 示例:设置异常处理入口 (概念性)
set_trap_gate(0, ÷_error_handler); // 除零错误
set_trap_gate(3, &breakpoint_handler); // 断点陷阱 (INT 3)
set_trap_gate(6, &invalid_opcode_handler); // 无效指令
set_trap_gate(13, &general_protection_fault_handler); // 通用保护错误
set_trap_gate(14, &page_fault_handler); // 缺页故障
中断是操作系统的脉搏
操作系统就是躺在中断处理例程上的代码块!
这句话精辟地指出了中断(包括硬件中断和软中断)对于操作系统的核心意义。操作系统的大部分代码,无论是设备驱动、文件系统、内存管理还是进程调度,很多时候都是在响应某个中断事件。软中断提供了:
- 用户与内核的桥梁: 安全、受控地访问内核服务。
- 错误处理机制: 统一处理 CPU 内部产生的各种异常。
- 系统运行的基础: 驱动了虚拟内存、调试器、进程终止等关键功能的实现。
一点补充: 在 Linux 内核中,还有一个叫做 “softirq” 的机制,它与我们这里讨论的 CPU 级软中断(INT
/syscall
/异常)是不同的概念。Linux 的 softirq 主要用于将硬件中断处理中耗时较长的部分“延迟”到底半部(bottom half)异步执行,以尽快释放硬件中断上下文。这是一个内核内部的优化技术,不要与 CPU 指令触发的软中断混淆。
小结
软中断就像是操作系统这座大厦中隐藏的楼梯和电梯,连接着用户空间和内核空间,也连接着正常的程序执行与异常处理。理解了软中断,你就能更深刻地把握程序是如何与操作系统交互,以及操作系统是如何应对各种内部事件的。希望这次的深入探讨,能让你对这个“看不见”却至关重要的机制有更清晰的认识!