操作系统是如何运行的?

硬件中断

在我们使用键盘的时候,操作系统要怎么知道键盘上有数据了呢?硬件中断!

硬件中断过程如图所示:

按照图中所示,外设直接与CPU进行交互,但是之前对于冯诺依曼体系架构的学习可知,外设要和CPU交互必须要通过内存,那么怎么做到的呢?

有两个颜色的信号交互线路,分别是控制信号和数据信号。之前的描述中主要是对于数据的拷贝线路进行的学习,实际上还有一个控制信号的线路,只要进行传输控制信号即可,无需拷贝数据,所以外设可以和CPU进行交互。主要是传输信息

外设和中央处理器之间如何连接通信呢?

上图为CPU。会有很多针脚,针脚与主板相连,外设也会和主板连接。CPU的针脚有相当一部分是用来与外设交互的。


再了解一下寄存器的概念:

当我们向磁盘发送数据,想在磁盘进行存储,假如指令in 100(扇区编号) XXXX(数据)

磁盘要怎么往扇区存储呢?

实际上磁盘有磁盘控制器 ,里面会有各个不同功能的寄存器!

拓展一下,外设都有自己的控制器!


继续理解硬件中断的过程图示:

  1. **外设就绪:**硬件交互的信号也就是高低电平,外设准备就绪。
  2. **发起中断:**通过电平变化,在中断控制器产生中断,并且产生中断号(外设特有的编号)
  3. **通知CPU:**结合上述关于控制信号发送和寄存器的概念,通知CPU有中断产生
  4. **CPU得知中断,获取中断号:**CPU收到通知,但是要处理中断需要得到中断号。CPU在在中断控制器得到中断号,现在CPU就知道哪一个中断号准备好了。
  5. **根据中断号,执行中断处理方法:**现在已经得到哪一个设备中断了,需要具体的处理方法。操作系统提供了中断向量表,就是一个函数指针数组,中断号对应的就是不同下标,每个中断号有着不同的函数方法。根据中断号进行查找方法,然后执行。(中断向量表是操作系统的一部分,操作系统启动后记忆加载到内存中了)
  6. 中断完毕,继续之前的工作。

这一套过程可以得到:

  • 操作系统不会关注外设是否准备好,而是外设准备好之后通知操作系统。
  • 过程很熟悉,与信号的过程很像,发信号,信号编号,保存信号,处理信号

内核代码:

c 复制代码
// 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, &parallel_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 中断信号请求。
}

时钟中断

  • 在没有中断的时候,操作系统在做什么?
  • 进程可以在操作系统的指挥下,被调度,被执行,那么操作系统自己被谁指挥,被谁推动执行呢?
  • 外部设备可以触发硬件中断,但是这个是需要用户或者设备自己触发,有没有自己可以定期触发的设备?

在没有中断的时候,操作系统在做什么?

时钟中断怎么进行

  1. 运行 Idle 进程
    • 当所有可运行的用户/内核进程都已运行完时间片或被阻塞,没有其他事情可做时,CPU 会进入所谓的 "idle"(空闲)进程。
    • 在 x86 上,idle 里常见写法大致是:
c 复制代码
for (;;) {
  asm("hlt");      // 让 CPU 进入低功耗等待状态,直到下一个中断到来
}

或者:

c 复制代码
for (;;) {
  pause();         // 类似 hlt,也会在硬件中断到来前挂起
}
复制代码
- **目的**<font style="color:rgb(31,35,41);">:减少功耗,不浪费 CPU 周期在"忙等"上。</font>
cpp 复制代码
void main(void) /* 这里确实是 void,并没有错。 */
{
    /* 在 startup 程序(head.s)中就是这样假设的。 */
    
    // 进入无限循环,等待调度器决定是否需要执行其他任务
    for (;;)
        pause();  // 'pause()' 等待一个信号的到来(例如,任务切换或者硬件中断)

    /* 
    注意!! 对于任何其它的任务,'pause()' 将意味着我们必须等待收到一个信号才会返回就绪运行态。
    然而,任务0(task0)是唯一的例外情况(参见 'schedule()')。因为任务0在任何空闲时间都会被激活(当没有其它任务在运行时)。
    对于任务0,'pause()' 仅意味着我们返回来查看是否有其它任务可以运行,如果没有的话,我们就回到这里,继续循环执行 'pause()'。
    */
} // end main
  1. 后台守护与内核线程
    • 一些内核线程(如 kswapd、ksmd、kworker 等)会被唤醒去做后台清理、内存回收、延迟任务执行等。
    • 但如果它们也都阻塞起来,则真正就只剩下 idle 持续运行。

当没有任何外部中断(时钟、I/O、系统调用陷阱等)时,CPU 就一直跑到图最右侧的 <font style="color:rgb(31,35,41);">for(;;) pause()</font>,等待下一个"蓝色箭头"打来的时钟中断。

  1. 时钟源
  • 外部设备一般包括时钟源,时钟源会以特定的频率,向CPU发送特定的中断。
  • 一旦定时器中断到来,CPU 自动:
    1. 保存现场(用寄存器保存当前进程状态)
    2. 切换到内核栈
    3. 根据中断号查 IDT(Interrupt Descriptor Table)
    4. 跳转到对应的中断服务例程
  • IDT就是中断向量表,存放不同中断信号所对应的服务例程方法
  • 当中断后就会跳转到中断服务例程中的进程调度,专门用来为时钟中断服务,保证操作系统的持续运行,持续进行进程调度等一系列操作系统的基础操作。也就是在空闲的时候去以特定的频率在全局数据结构中扫一遍,查看有没有需要处理的事情
  • 也可以理解,操作系统就是:基于中断进行工作的软件。

时钟源以恒定频率发中断(比如 1 ns 一次),这个就是CPU的主频。每个进程被分配一个固定的"滴答数"作时间片(比如 10 个滴答 = 10 ns),每次中断既减少当前进程的剩余时间片,也把全局计数器 total(jiffies)加一,这样既能实现进程的公平调度,又能即使离线也可以精确地统计系统运行时间。

时钟中断和进程调度核心流程概述

  1. 硬件定时器中断的注册:
    • 内核在启动时设置好与时钟中断相关的处理程序。通过 set_intr_gate 将定时器中断(IRQ0)与处理函数(timer_interrupt)关联。这相当于告诉内核,当硬件定时器发出中断信号时,应该跳转到哪个函数进行处理。
  2. 中断处理入口:
    • 每次硬件定时器触发时,CPU 会进入中断处理程序。中断向量表将控制权传递给 timer_interrupt 入口,CPU 会保存现场,允许处理函数执行。此时的中断处理并不直接切换到其他任务,而是先通过汇编指令跳转到 C 语言的 do_timer 函数。
  3. 时间片管理:
cpp 复制代码
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()` 来触发进程调度。通过调度器选择下一个准备好的进程执行。
  1. 调度与上下文切换:
  • <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>,回到新进程的用户态或内核态继续运行。
  1. 循环与响应:
    • 该过程会不断循环进行,确保系统在执行多个进程时能合理地分配 CPU 时间。如果没有其他进程需要执行,CPU 将进入 idle 进程,并继续等待下一个时钟中断。

操作系统自己被谁指挥、被谁推动执行?

  1. "事件驱动"模型
    • Linux 内核并不是一个自扫描的"大循环",而是被各种"事件"唤醒执行 。这些事件主要分三类:
      1. 硬件中断(timer、网卡、键盘、磁盘等)
      2. 异常/陷阱 (系统调用 <font style="color:rgb(31,35,41);">int 0x80</font>/<font style="color:rgb(31,35,41);">syscall</font>、页错误、非法指令等)
      3. 软中断/底半部<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> 等)
  2. 具体流程
    • 进程态 → 内核态 :当用户进程执行到 <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> 返回到原来被抢占或陷入的进程。
  3. 总结

操作系统"自己"并没有一个独立的"调度者",而是被外部与内部的"事件"驱动------每来了一个中断或陷阱,就把控制权交给内核。


有没有自己可以定期触发的设备?

  1. 定时器芯片(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 定时中断)发给中断控制器。
  2. RTC(Real-Time Clock)周期中断
    • 另一种定期中断来源是实时时钟芯片(CMOS RTC),可以配置为每秒或每几分之一秒产生一次 IRQ8。
    • Linux 也可以利用 RTC 来做秒级或更低频率的周期唤醒

这样操作系统就可以在硬件时钟的推动下进行自动调度了~

软中断

想象一下,你正在编写一个简单的程序,比如读取一个文件并打印其内容。这个看似简单的操作,背后却隐藏着操作系统内核的复杂工作。你的程序运行在用户空间 (User Space) ,一个相对受限的环境;而文件系统、硬件设备等核心资源则由内核空间 (Kernel Space) 掌控,拥有最高权限。那么,用户程序如何安全、可控地请求内核来完成这些特权操作呢?答案就是通过系统调用 (System Call) ,而系统调用的实现,很大程度上就依赖于我们今天要讲的软中断

中断:CPU的"请注意"信号

在深入软中断之前,我们先快速回顾一下什么是"中断"。你可以把中断想象成一种信号,它会打断 CPU 当前正在执行的任务,要求 CPU 立即关注并处理一个更紧急或特殊的事件。

中断主要分为两类:

  1. 硬件中断 (Hardware Interrupt): 由外部硬件设备(如键盘敲击、鼠标移动、网卡收到数据包、硬盘完成读写)产生。这些事件是异步的,发生时间不可预测。
  2. 软中断 (Software Interrupt): 由 CPU 内部执行的软件指令 触发。它们是同步的,发生时间点就在指令执行的那一刻。

上文已经讲解了关于硬件中断的相关知识点,我们今天要聚焦的就是第二种------软中断。

初步了解软中断

CPU 设计了对应的汇编指令 (int 或者 syscall), 可以让 CPU 内部触发中断逻辑。

没错,软中断的核心就是 CPU 提供了一些特殊的指令,允许正在运行的程序主动"中断"自己,将控制权交给预先定义好的处理程序(通常是操作系统内核的一部分)。

  • 在经典的 x86 架构上,INT n 指令就是用来触发软中断的,其中 n 是一个中断号(0-255)。Linux 早期广泛使用 INT 0x80 作为系统调用的入口。
  • 随着 CPU 发展,为了提高效率,引入了更快的专用指令,如 SYSENTER (配合 SYSEXIT) 和 SYSCALL (配合 SYSRET)。

无论使用哪种指令,效果都是类似的:暂停当前的用户程序,切换到更高权限的内核模式,并跳转到指定的中断处理程序。

系统调用

软中断最广为人知的应用场景就是实现系统调用。让我们跟随一次典型的系统调用(比如 read 文件),来一场"从用户空间到内核再返回"的深度游:

第一站:用户空间 - 请求发起

  1. 应用程序员视角: 你在代码里调用了一个库函数,比如 C 语言的 read(fd, buffer, count);
  2. C库 (glibc) 视角: 你调用的 read() 函数并非直接操作硬件。它是一个封装层。它的主要工作是:
    • 确定 read 操作对应的系统调用号 (这是一个事先约定好的数字,例如,在 x86-64 Linux 中,read 的号是 0)。
    • 下图为调用系统调用是系统调用号对应的函数指针表:
cpp 复制代码
/* 系统调用函数指针表,用于系统调用中断处理程序 (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 0x80syscall 陷入 (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 指向内核中特定处理代码的路径。)

第三站:内核空间 - 请求处理

  1. 系统调用分发器 (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 等)的指针。

  1. 执行内核函数: 分发器调用 sys_call_table 中找到的函数指针,也就是执行 sys_read
  2. 过程也就是:当使用系统调用函数后,函数内会将该系统调用的系统调用号mov到eax寄存器,然后使用int 0x80或者syscall陷入内核,在系统调用表找到对应的系统调用函数执行。
  3. 真正的内核工作: sys_read 函数会执行真正的文件读取逻辑,这可能涉及到:
    • 检查文件描述符的有效性和权限。
    • 与虚拟文件系统 (VFS) 层交互。
    • 通过文件系统层找到数据在磁盘的位置。
    • 与块设备层和磁盘驱动程序交互,发起 I/O 请求。
    • 等待 I/O 完成(期间当前进程可能会被挂起,让 CPU 去做其他事)。
    • 将读取到的数据从内核缓冲区拷贝到用户传入的 buffer 地址。
    • 准备返回值(实际读取的字节数,或出错时的错误码)。

第四站:返回用户空间 - 功成身退

  1. 内核函数返回: sys_read 执行完毕,将返回值(比如成功读取的字节数)放入 RAX (或 EAX) 寄存器。
  2. 恢复上下文: 系统调用分发器从内核堆栈中恢复之前保存的用户通用寄存器。
  3. 执行返回指令: 执行特殊的返回指令,如 sysret (对应 syscall) 或 iret (对应 INT)。
  4. CPU 的返程操作:
    • 权限降低: CPU 运行模式从内核模式切换回用户模式。
    • 状态恢复: CPU 自动恢复之前保存的用户态指令指针、代码段、标志寄存器、堆栈指针和段寄存器。
  5. 回到 C 库: 控制权回到用户空间的 C 库函数 (read) 中,就在 syscall 指令之后。
  6. 返回应用程序: 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 函数)会为这些预定义的异常编号设置好相应的处理函数入口。

c 复制代码
// 示例:设置异常处理入口 (概念性)
set_trap_gate(0, &divide_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);       // 缺页故障

中断是操作系统的脉搏

操作系统就是躺在中断处理例程上的代码块!

这句话精辟地指出了中断(包括硬件中断和软中断)对于操作系统的核心意义。操作系统的大部分代码,无论是设备驱动、文件系统、内存管理还是进程调度,很多时候都是在响应某个中断事件。软中断提供了:

  1. 用户与内核的桥梁: 安全、受控地访问内核服务。
  2. 错误处理机制: 统一处理 CPU 内部产生的各种异常。
  3. 系统运行的基础: 驱动了虚拟内存、调试器、进程终止等关键功能的实现。

一点补充: 在 Linux 内核中,还有一个叫做 "softirq" 的机制,它与我们这里讨论的 CPU 级软中断(INT/syscall/异常)是不同的概念。Linux 的 softirq 主要用于将硬件中断处理中耗时较长的部分"延迟"到底半部(bottom half)异步执行,以尽快释放硬件中断上下文。这是一个内核内部的优化技术,不要与 CPU 指令触发的软中断混淆。

小结

软中断就像是操作系统这座大厦中隐藏的楼梯和电梯,连接着用户空间和内核空间,也连接着正常的程序执行与异常处理。理解了软中断,你就能更深刻地把握程序是如何与操作系统交互,以及操作系统是如何应对各种内部事件的。希望这次的深入探讨,能让你对这个"看不见"却至关重要的机制有更清晰的认识!

相关推荐
萌萌哒草头将军13 分钟前
注意!⚠️ 🔥 🚧 PostgreSQL 存在安全漏洞,请及时更新版本或者停用删除相关服务,防止中招!🚀🚀🚀
服务器·postgresql·xss
曼岛_14 分钟前
[密码学基础]GMT 0029-2014签名验签服务器技术规范深度解析
运维·服务器·密码学·签名验签服务器
智者知已应修善业20 分钟前
2021-11-14 C++三七二十一数
c语言·c++·经验分享·笔记·算法·visual studio
小oo呆36 分钟前
【自然语言处理与大模型】Linux环境下Ollama下载太慢了该怎么处理?
linux·服务器·人工智能
熹乐互动44 分钟前
FileZilla“服务器发回了不可路由的地址,使用服务器地址代替
运维·服务器
star _chen1 小时前
如何优雅地实现全局唯一?深入理解单例模式
c++·单例模式·设计模式
爱吃涮毛肚的肥肥(暂时吃不了版)1 小时前
项目班——0408——qt的多线程开发
服务器·数据库·nginx
eli9601 小时前
LIB-ZC, 一个跨平台(Linux)平台通用C/C++扩展库, 网络socket
linux·c语言·c++
czxyvX1 小时前
016-C语言内存函数
c语言
我科绝伦(Huanhuan Zhou)1 小时前
网络设备基础运维全攻略:华为/思科核心操作与巡检指南
运维·服务器·网络