一、什么是中断

中断(Interrupt) 是指计算机在执行程序的过程中,遇到某些突发或紧急事件(如硬件故障、外部设备请求、程序异常等)时,CPU 暂停当前任务,转而去处理这些事件,处理完成后再返回原任务继续执行的机制。

简单来说,中断是计算机 “暂停当前工作、响应紧急事件、再恢复工作” 的一套标准化流程。它的核心价值是 打破 CPU “顺序执行指令” 的固有模式,让计算机能高效处理突发任务,同时兼顾 “常规工作” 和 “紧急事件”。

在计算机系统中,“中断” 是一种核心机制,它让计算机能够像人类一样 “灵活应对突发情况”。想象你正在写报告(CPU 执行任务),突然电话铃响(紧急事件),你暂停写作、接电话(处理事件),挂电话后继续写作(恢复任务)—— 这个过程,就是计算机中断机制的现实类比。

二、中断的分类:硬件 、 软件、时钟

2.1 硬件中断

2.1.1 前提引入

在之前进程控制的一章中我们学到当我们的程序中存在需要外部设备响应的代码片段如scanf等,CPU会检查该外部设备是否就绪,如果该外部设备未就绪操作系统会将该进程从运行队列链入到该外部设备的阻塞队列中,当外部设备就绪后再链入运行队列中等待CPU调度和执行。

而CPU如何知道某一个外部设备已经处于就绪状态呢(例如键盘已经输入数据)

在早期的计算机硬件设计中,硬件设备之间主要通过物理针脚与进行连接。

为了方便管理外部设备的中断请求(IRQ),包括接收多个外设的中断信号、优先级判断、向 CPU 转发中断等,我们引入中断控制器这一硬件设备。首先外部设备通过针脚向中断控制器发送中断信号,中断控制器再通过相应针脚通知CPU。例如当键盘处于就绪状态时会向中断控制器发送相应的“中断信号”,再由中断控制器通知CPU。

这里我们需要注意的是,外部设备给中断控制器发送的“中断信号”并不是我们之前讲的软件层面的信号,本质上是通过高低电平变化电平状态维持实现的电信号传递,这是硬件设备间最基础的通信方式。

2.1.1 硬件中断的流程

理解上述原理之后我们大致可以来理解CPU是如何知道外部设备是怎样就绪的了,但是CPU是一个硬件设备,它不能主动的决定或控制它要访问哪一些代码或者进行哪一些操作。例如当键盘设备就绪向CPU发出硬件中断时CPU只能知道键盘设备已就绪这一信息,并不能主动地执行后续的相关代码比如将键盘阻塞队列中的相应进程重新链入到运行队列调度等。

为了解决上面的问题,我们接着引入中断号和中断向量表这两个重要概念:

1. 中断号

前面我们知道当外部设备就绪时会给中断控制器发送中断信号并由中断控制器来通知CPU哪一个外部设备已处于就绪状态。除了这些操作,中断控制器还会生成一个中断号,中断号是系统分配给每个中断源的代号之后会被CPU所获取用于在计算机系统中识别和处理不同的中断请求。

  • 硬件中断中,同一中断控制器(如 x86 的 8259A、ARM 的 GIC)管理的 “可独立触发中断的设备 / 事件”,其中断号具有唯一性;

在采用向量中断方式的中断系统中,CPU 通过中断号来找到中断服务程序的入口地址,实现程序的转移。例如,在 PC 中,可通过中断号 (n)×4 得到一个指针,指向中断向量表中中断服务程序入口地址所在的位置,从中取出地址(CS:IP),装入代码段寄存器 CS 和指令指针寄存器 IP,从而转移到中断服务程序。

2. 中断向量表

中断向量表(Interrupt Vector Table, IVT)是计算机系统中管理中断入口地址的核心数据结构,其本质是 “中断号与中断服务程序(Interrupt Service Routine, ISR)入口地址的映射表”。当硬件或软件触发中断时,CPU 会通过 “中断号” 快速查询该表,找到对应的 ISR 入口,进而跳转到 ISR 执行 —— 它是中断处理流程的 “导航目录”,直接决定了中断响应的效率和正确性。

术语 定义 作用
中断号(Interrupt Number) 中断的唯一标识(如 x86 的0x00~0xFF,ARM 的0~1019 作为 IVT 的索引,用于快速定位对应的中断向量
中断向量(Interrupt Vector) 即 ISR 的入口地址(或跳转指令),是 IVT 中存储的核心内容 指向中断发生时需要执行的程序(ISR),是 CPU 的 “中断目标地址”
中断向量表(IVT) 以中断号为索引、中断向量为元素的连续内存区域(本质是数组) 集中管理所有中断的入口,实现 “中断号→ISR 地址” 的快速映射

简言之,中断向量表类似一个函数指针数组。其中的元素就是各类中断对应的服务程序例如处理键盘,处理显示或者网卡等。

当一个硬件中断到来后CPU会快速获取该中断源所对应的中断号并以此来生成对应中断服务程序(ISR)的入口地址(IVT),有了入口地址CPU就可以快速定位对应的中断方法,也可以近似理解为函数指针数组的下标。之后CPU会将代码加载到各个寄存器开始执行中断方法。

2.2 时钟中断

2.2.1 基本定义

时钟中断是操作系统的 “心跳”,由硬件定时器(如可编程间隔定时器 PIT、高精度事件定时器 HPET)周期性触发,是操作系统实现多任务调度、精确计时、定时器管理等核心功能的基础。它本质是一种硬件中断,强制 CPU 暂停当前任务,转而去执行内核预设的中断服务程序(ISR),完成系统级的关键操作。在当代的CPU设计中时钟源通常集成在CPU中。

2.2.2 核心属性

  • 周期性:中断触发间隔固定,用 “时钟频率(Hz)” 表示(如 100Hz = 每 10ms 触发一次,1000Hz = 每 1ms 触发一次)。频率越高,系统响应越灵敏,但内核中断开销也越大(需频繁切换上下文)。
  • 强制性:无论 CPU 当前执行用户程序还是内核代码(非临界区),时钟中断都会强制打断当前流程(硬件级触发,不可被忽略)。
  • 高优先级:时钟中断优先级通常高于其他外设中断(如键盘、磁盘),确保系统计时和调度的准确性。

2.2.3 关键作用

1. 维护系统时间(计时功能)

时钟中断是系统时间的 “增量器”,负责维护两种关键时间:

  • 墙上时间(Wall Time):即用户看到的系统时间(年 / 月 / 日 时:分: 秒)。开机时,系统从实时时钟(RTC,硬件时钟) 读取初始时间;之后每触发一次时钟中断,内核就将 “系统时间” 增加一个固定单位(如 1ms,对应 1000Hz 频率),确保时间持续更新。
  • 进程时间(Process Time):统计进程在 CPU 上的实际运行时间(分为用户态时间和内核态时间)。每次时钟中断触发时,若当前进程正在运行,内核会为其 “时间计数器” 加 1,最终用于topps等命令展示 CPU 利用率。

2.实现进程调度(多任务核心)

现代操作系统的抢占式多任务完全依赖时钟中断实现:

内核为每个进程分配 “时间片”(如 100ms),表示进程可连续占用 CPU 的最大时长。

每次时钟中断触发时,内核会检查当前进程的 “剩余时间片”:

  • 若剩余时间片 > 0:将其减 1,继续执行当前进程;
  • 若剩余时间片 = 0:触发进程调度器(Scheduler),暂停当前进程(保存上下文),从 “就绪队列” 中选择下一个高优先级进程执行(切换上下文)。

3. 管理定时器与延迟任务

用户层和内核层的 “定时需求” 均通过时钟中断触发:

  • 用户层定时器:如sleep(1)(休眠 1 秒)、alarm(5)(5 秒后触发信号),内核会将这些定时任务加入 “定时器队列”,每次时钟中断时检查队列:若某任务的 “到期时间” 等于当前系统时间,就执行其预设回调(如唤醒进程、发送信号)。
  • 内核层延迟任务:如内核驱动的 “延迟 10ms 后执行初始化”,依赖时钟中断的周期性检查,确保延迟操作的准确性。

2.2.4 工作流程

与硬件中断的工作流程相似,唯一不同的是集成在CPU中的时钟源可以直接向CPU发送中断请求并生成中断号。CPU在接受中断请求后会暂停当前任务并保存上下文获取中断号后并以此来生成对应中断服务程序(ISR)的入口地址(IVT),然后根据 “中断向量表”(内核维护的中断号→处理函数映射表),对应的 “时钟中断处理函数”(如 Linux 中的timer_interrupt()),跳转到该函数执行。

这里我们详细来理解一下时钟中断下进程调度是如何发生的:

整个流程的核心函数调用链是:timer_interrupt (中断处理入口) -> do_timer -> schedule

timer_interrupt这个函数位于 kernel/system_call.s 中,是用汇编语言写的,因为它需要非常精细地处理寄存器状态。

do_timer这个函数位于 kernel/sched.c 中。

// 代码出处:kernel/sched.c
void do_timer(long cpl) {
    // cpl (Current Privilege Level) 在 timer_interrupt 中通过 regs->cs & 3 计算得来
    // 0 表示内核态,3 表示用户态

    // 1. 更新当前进程的时间片和CPU时间
    if (cpl)
        current->utime++; // 如果cpl>0(用户态),增加用户态时间
    else
        current->stime++; // 否则(内核态),增加内核态时间

    // 2. 处理定时器(内核态使用的定时功能,与进程调度无关)
    if (next_timer) {
        next_timer->jiffies--;
        while (next_timer && next_timer->jiffies <= 0) {
            void (*fn)(void);
            fn = next_timer->fn;
            next_timer->fn = NULL;
            next_timer = next_timer->next;
            (fn)();
        }
    }

    // 3. 更新当前进程的时间片 counter
    if ((--current->counter) > 0)
        return; // 如果时间片还没用完,直接返回

    // 如果时间片用完(counter <= 0),则将调度标志置位
    current->counter = 0; // 确保 counter 不为负
    need_resched = 1;     // 设置全局调度标志
}
// 注意:在 timer_interrupt 中调用 do_timer 时,传入的 cpl 参数是根据 CS 寄存器计算出的特权级。

其中主要完成以下任务:

  • 递减时间片:current->counter 是当前进程剩余的时间片(滴答数)。每次时钟中断都会将其减一。

  • 时间片未用完:如果 counter > 0,说明进程还可以继续运行,函数直接返回。

  • 时间片用完:如果 counter 减到 0 或以下,这是触发调度的关键条件。

    • 先将 counter 明确设置为 0。

    • 然后将全局调度标志 need_resched 设置为 1。

这个 need_resched 标志就是上面汇编代码 timer_interrupt 中检查的那个标志。一旦它被置位,汇编代码就会调用 schedule() 函数。

进程调度函数 (schedule)这个函数也位于 kernel/sched.c 中,是真正执行进程切换的地方。

// 代码出处:kernel/sched.c
void schedule(void) {
    int i, next, c;
    struct task_struct **p;

    // 1. 检查并处理信号(略)
    ...

    // 2. 核心算法:遍历所有任务,选择 counter 值最大的就绪态任务
    while (1) {
        c = -1;
        next = 0;
        i = NR_TASKS; // NR_TASKS = 64,最大任务数
        p = &task[NR_TASKS];

        // 从后往前遍历任务数组(从最后一个任务到第一个)
        while (--i) {
            if (!*--p)
                continue;
            // 如果任务就绪(TASK_RUNNING)并且 counter 值大于当前最大值 c
            if ((*p)->state == TASK_RUNNING && (*p)->counter > c)
                c = (*p)->counter, next = i; // 更新最大值 c 和任务号 next
        }

        // 3. 找到了一个 counter > 0 的就绪任务,跳出循环
        if (c)
            break;

        // 4. 如果没有找到 counter > 0 的就绪任务(所有进程的时间片都用完了)
        //    则重新计算所有进程的时间片:counter = counter/2 + priority
        for (p = &LAST_TASK; p > &FIRST_TASK; --p) {
            if (*p)
                (*p)->counter = ((*p)->counter >> 1) + (*p)->priority;
        }
    }

    // 5. 切换到选中的任务
    switch_to(next);
}

2.3 软中断  

2.2.1 用户态与内核态

操作系统将 CPU 的执行权限分为高权限(内核态) 和低权限(用户态),程序运行时只能处于其中一种状态。它们是操作系统为了隔离资源、保障系统安全而划分的两种程序执行状态,本质是对 CPU 权限的分级管控 —— 不同状态下的程序,能访问的硬件资源(如内存、CPU 寄存器)和执行的指令(如修改系统配置)存在严格限制。

维度 内核态(Kernel Mode) 用户态(User Mode)
权限等级 最高权限,可直接访问所有硬件资源(内存、CPU、I/O 设备),执行所有 CPU 指令(如修改页表、关闭中断) 低权限,仅能访问 “用户空间”(程序自身的内存区域),无法直接操作硬件或执行特权指令
运行的程序 操作系统内核(如进程调度、内存管理、文件系统、驱动程序) 普通应用程序(如浏览器、Office、终端命令行)
资源隔离性 共享系统内核资源,错误会直接导致系统崩溃(如内核 panic) 资源隔离,程序崩溃仅影响自身,不波及系统
典型操作场景 进程切换、内存分配、磁盘读写、网络数据包处理、系统调用响应 计算逻辑(如数据运算)、用户交互(如按钮点击)

1.为什么需要区分用户态与内核态!

核心目的是 “保护系统核心资源不被普通程序破坏”,避免因应用程序的错误或恶意行为导致整个系统崩溃。举个通俗的例子:

  • 内核态好比 “系统管理员”,能操作服务器机房的所有设备(电源、硬盘、网络交换机),权限无限制;
  • 用户态好比 “普通员工”,只能通过办公软件(如浏览器、文档)操作自己的电脑,无法直接触碰机房设备 —— 若想让机房做某事(如存储大文件到服务器硬盘),必须向管理员(内核)提交申请。

如果不区分状态,普通程序可能:

  1. 误修改内核内存中的进程调度表,导致所有程序无法执行;
  2. 直接操作磁盘控制器,覆盖系统分区数据,导致系统瘫痪;
  3. 恶意关闭 CPU 中断,让操作系统失去对硬件的控制。

2.重要概念:进程地址空间隔离

我们知道当用户创建一个进程的时候,一方面为了避免用户直接接触物理内存引发一系列安全问题另一方面为了高效解决物理内存分配的问题操作系统就会给其分配一套“虚拟地址空间”。该地址空间的虚拟地址通过页表与物理地址一一映射起来。

32 位 CPU 的地址总线可寻址范围是0~4GB,操作系统通常将分配的虚拟地址空间按 “3:1” 划分:

  • 用户空间(0~3GB):仅用户态程序可访问,每个应用程序拥有独立的 “虚拟用户空间”(通过虚拟内存技术隔离,避免程序间互相干扰);
  • 内核空间(3~4GB):仅内核态可访问,存储操作系统内核代码、内核数据结构(如进程控制块 PCB、页表),所有程序共享同一内核空间(但用户态程序无法直接读写)。

CPU在执行每一行代码段时都会进行“权限审查”,当用户尝试以用户空间的代码段直接访问内核空间的代码或数据的时候CPU会通过权限审查拦截用户的这一请求。比如用户直接解引用访问内核空间的指针时。那CPU是如何进行这一“权限审查”的呢?

给个结论:CPU主要是通过关键寄存器标志位来进行“权限审查”。

x86 架构: CS 寄存器的 CPL 标志位

  • CPL = 0:当前处于内核态
  • CPL = 3:当前处于用户态

CPU 通过代码段寄存器(CS) 的最低 2 位(bit 0 和 bit 1)记录当前特权级,称为 “当前特权级(Current Privilege Level, CPL) ”:

当我们自己的代码与数据加载到内存的时候,操作系统为了描述加载进来的代码段会有相应的段描述符表表示该代码段处于用户空间还是涉及内核态。当代码段地址交给CPU执行相应代码的时候CPU中的CS段寄存器的标志位会与该代码段的段描述符表对比,如果权相一致则执行代码否则就会报错等执行拦截操作。

3. 内核态与用户态的切换

1. 系统调用

系统调用是用户态程序与内核通信的 “官方接口”。当程序需要内核服务时,会通过特定指令(如 x86 的int 0x80syscall)触发切换,流程如下:

  1. 用户态程序准备参数(如要读写的文件名、数据缓冲区地址),存入 CPU 寄存器;
  2. 执行系统调用指令(如syscall),CPU 感知到该指令,自动切换到内核态;
  3. 内核根据 “系统调用号”(提前约定的接口编号,如read对应 3、write对应 4)找到对应的内核函数;
  4. 内核执行函数(如实际操作磁盘控制器),将结果存入寄存器;
  5. 执行完毕后,CPU 自动切换回用户态,用户程序从寄存器中读取结果,继续执行。

2. 中断(硬件触发)

当硬件设备完成操作后(如键盘按下、磁盘读写完成、网络数据包到达),会向 CPU 发送 “中断信号”,强制 CPU 暂停当前用户态程序,切换到内核态处理中断:

  1. 硬件(如键盘控制器)触发中断,CPU 暂停当前用户态程序,保存其执行上下文(寄存器值、程序计数器);
  2. CPU 根据 “中断号” 查找 “中断服务程序(ISR)” 的地址(内核提前注册的硬件处理函数);
  3. 切换到内核态,执行 ISR(如键盘中断的 ISR 会读取键盘扫描码,转换为字符存入内核缓冲区);
  4. ISR 执行完毕后,CPU 恢复之前保存的用户态程序上下文,切换回用户态,程序继续执行。

特点:中断是 “异步” 的,用户态程序无法预知中断何时发生(如你随时可能按下键盘)。

3. 异常(程序错误触发)

当用户态程序执行了 “非法操作”(如除以 0、访问无权限的内存、执行特权指令),CPU 会触发 “异常”,强制切换到内核态让内核处理错误:

  • 除以 0:触发 “算术异常”,内核可能终止该程序(如 Linux 下的Floating point exception);
  • 访问内核空间内存:触发 “页错误异常”,若内存确实不存在(如野指针),内核会终止程序(Segmentation fault);

特点:异常是 “同步” 的,由用户态程序的错误操作直接引发

2.2.2 主动中断(陷阱)

在这一小节我们主要讨论一下系统调用是怎么运行起来的以及其中所涉及的用户态到内核态之间的切换。

重点概念:系统调用表(sys_call_table)

操作系统为了保证操作安全性不会直接暴露内核让用户直接对内核进行操作,而是暴露一些接口。这些接口就是系统调用。

当操作系统加载到内存时同时还加载了一张系统调用表(sys_call_table),它的本质类似于函数指针数组,其中注册了各类系统调用的具体实现方法:

/* 在 kernel/sys_call_table.c (假设的文件) */
#include <linux/sys.h> // 包含 fn_ptr 类型和所有 sys_* 的声明

fn_ptr sys_call_table[] = {
    (fn_ptr)sys_setup,      /* 0  */
    (fn_ptr)sys_exit,       /* 1  */
    (fn_ptr)sys_fork,       /* 2  */
    (fn_ptr)sys_read,       /* 3  */
    (fn_ptr)sys_write,      /* 4  */
    (fn_ptr)sys_open,       /* 5  */
    (fn_ptr)sys_close,      /* 6  */
    (fn_ptr)sys_waitpid,    /* 7  */
    (fn_ptr)sys_creat,      /* 8  */
    (fn_ptr)sys_link,       /* 9  */
    (fn_ptr)sys_unlink,     /* 10 */
    (fn_ptr)sys_execve,     /* 11 */
    (fn_ptr)sys_chdir,      /* 12 */
    (fn_ptr)sys_time,       /* 13 */
    (fn_ptr)sys_mknod,      /* 14 */
    (fn_ptr)sys_chmod,      /* 15 */
    (fn_ptr)sys_chown,      /* 16 */
    (fn_ptr)sys_break,      /* 17 */
    (fn_ptr)sys_stat,       /* 18 */
    (fn_ptr)sys_lseek,      /* 19 */
    (fn_ptr)sys_getpid,     /* 20 */
    (fn_ptr)sys_mount,      /* 21 */
    (fn_ptr)sys_umount,     /* 22 */
    (fn_ptr)sys_setuid,     /* 23 */
    (fn_ptr)sys_getuid,     /* 24 */
    (fn_ptr)sys_stime,      /* 25 */
    (fn_ptr)sys_ptrace,     /* 26 */
    (fn_ptr)sys_alarm,      /* 27 */
    (fn_ptr)sys_fstat,      /* 28 */
    (fn_ptr)sys_pause,      /* 29 */
    (fn_ptr)sys_utime,      /* 30 */
    (fn_ptr)sys_stty,       /* 31 */
    (fn_ptr)sys_gtty,       /* 32 */
    (fn_ptr)sys_access,     /* 33 */
    (fn_ptr)sys_nice,       /* 34 */
    (fn_ptr)sys_ftime,      /* 35 */
    (fn_ptr)sys_sync,       /* 36 */
    (fn_ptr)sys_kill,       /* 37 */
    (fn_ptr)sys_rename,     /* 38 */
    (fn_ptr)sys_mkdir,      /* 39 */
    (fn_ptr)sys_rmdir,      /* 40 */
    (fn_ptr)sys_dup,        /* 41 */
    (fn_ptr)sys_pipe,       /* 42 */
};

当用户需要调用相应的系统调用时首先需要提供一个系统调用号,这个系统调用号可以理解为系统调用表的具体下标,系统调用表中的每一个系统调用只有唯一一个系统调用号,有了系统调用号操作系统就会根据系统调用表跳转到对应系统调用的具体实现方法并执行相应代码。

重点概念:指令集int 0x80与syscall

指令集(Instruction Set Architecture,ISA)是CPU 能够识别和执行的全部机器指令的集合,是软硬件交互的 “底层协议”:CPU 的硬件设计完全围绕指令集展开,操作系统、编译器等软件也必须基于指令集生成代码(否则 CPU 无法理解)。

可以把指令集理解为 “CPU 的母语”—— 只有用这套 “语言” 写的 “句子(指令)”,CPU 才能看懂并执行。也就是说我们编写的一系列代码加载到内存空间并交给CPU进行执行的时候CPU都会将其转化为相应的指令集。

其中int 0x80与syscall就是其中特定的一种指令集用于触发系统调用。

1. int 0x80

int 0x80中的 “int” 是 “interrupt(中断)” 的缩写0x80软中断号—— 它是 32 位 x86 架构(如早期 32 位 Linux、Windows)中,用户态程序请求内核服务的 “传统方式”,本质是通过 “软中断” 触发特权级切换。

当CPU收到int 0x80指令集时就会触发软中断,此时CPU会做3个核心操作:

  1. 保存用户态上下文:避免切换后数据丢失;
  2. 切换特权级:将当前特权级(CPL=3)改为内核态特权级(CPL=0);
  3. 查找中断处理程序:CPU 根据 “中断号 0x80”,查询内核中的中断描述符表,0x80对应的入口就是内核的 “系统调用总入口”(如 Linux 的system_call函数)。

在系统调用的中断服务system_call函数中主要完成下列重要操作:

  • 验证合法性:检查用户传递的系统调用号(存于eax寄存器中)是否在合法范围内
  • 调用对应的系统调用:扫描系统调用表(sys_call_table)调用对应的内核处理函数
  • 处理返回值并恢复现场
2. syscall

syscall是64 位架构(x86-64、ARMv8-A 等)专用的系统调用指令,设计目标是解决int 0x80的效率问题 —— 它跳过了 “软中断查表” 环节,直接通过专用寄存器跳转,大幅降低特权级切换开销。

当CPU收到syscall指令集时就会触发软中断,此时CPU会执行下列操作:

  1. 保存用户态上下文:避免切换后数据丢失;
  2. 切换特权级:自动将 CPL 从 3(用户态)切换到 0(内核态);
  3. 加载内核入口地址:从 C 的syscall_entry),并将其赋值给rip(CPU 的指令指针);
  4. 跳转到内核入口:CPU 开始执行内核的syscall_entry函数。

相比int 0x80的system_call函数,syscall_entry函数也会根据系统调用号(存于rax寄存器中)扫描系统调用表并调用对应的内核处理函数,但是但syscall_entry优化了上下文处理,使得调用过程更高效,且在表的访问更具灵活性。

认识了两个指令集int 0x80与syscall与系统调用表(sys_call_table)我们来详细解释一下我们的系统调用是如何运行起来的:

在用户层面,用户不能直接接触系统调用表调用系统调用。操作系统为用户提供了大量的用户层面的系统调用接口,这些接口通过标准库(如 C 标准库、Windows API) 封装为统一的函数形式,提供明确的 “输入参数 - 返回值” 语法,确保不同用户程序调用方式一致,降低开发成本。

系统调用                                                                   函数原型
fork pid_t fork(void)
execve int execve(const char *pathname, char *const argv[], char *const envp[])
exit void exit(int status)
waitpid pid_t waitpid(pid_t pid, int *wstatus, int options)
getpid pid_t getpid(void)

当我们调用一个系统调用接口时会完成以下准备工作:以x86_64(64 位)为例

1. 确定系统调用号并放入专用寄存器:用户程序首先通过头文件(如unistd.h)获取目标系统调用的编号(例如write的调用号是1),然后将其写入rax寄存器。

2. 按顺序将参数放入对应的参数寄存器:以write(int fd, const void *buf, size_t count)为例(3 个参数)

  • 第一个参数fd(文件描述符)→ %rdi
  • 第二个参数buf(数据缓冲区地址)→ %rsi
  • 第三个参数count(要写入的字节数)→ %rdx

3. 寄存器准备就绪后,执行架构专用的触发指令:使 CPU 从用户态(低特权级)切换到内核态(高特权级)

  • x86_64:syscall指令;
  • x86:int $0x80(软中断指令,触发 0x80 号中断);

接收到int 0x80或者syscall指令集后CPU会从用户态切换到内核态并保护现场,因为操作系统已经将用户想要调用系统调用的系统调用号以及所用到的参数都放入了指定寄存器,此时CPU会从寄存器拿到系统调用号与参数扫描系统调用表调用对应的系统调用。

我们把通过指令集int 0x80与syscall主动中断是CPU从用户态切换到内核态的现象叫做触发陷阱(使用户陷入内核)。

Logo

一起探索未来云端世界的核心,云原生技术专区带您领略创新、高效和可扩展的云计算解决方案,引领您在数字化时代的成功之路。

更多推荐