Skip to content

Linux的系统调用讲解全过程

1. 用户程序发起系统调用

  • 用户代码: 应用程序(在用户空间运行)通常不会直接编写汇编代码来触发系统调用,而是调用标准C库(如glibc)提供的函数,例如调用 $\text{open()}$、$\text{read()}$、$\text{write()}$ 或 $\text{malloc()}$ 等。

  • C库封装: C库中的这些函数被称为封装函数(Wrapper Functions)。它们的作用是:

    1. 将用户提供的参数(例如 $\text{open}$ 的文件名和标志)准备好。

    2. 确定要执行的系统调用号(System Call Number,例如 $\text{open}$ 对应的号码)。

    3. 将系统调用号和参数放入预定的CPU寄存器中(这是用户空间与内核空间约定的传递方式)。

    4. 执行一个特殊的机器指令(例如 $\text{x86}$ 架构上的 $\text{int 0x80}$、$\text{syscall}$ 或 $\text{sysenter}$),这称为软中断陷阱(Trap)。

2. 从用户空间到内核空间

  • 软中断/陷阱: 当执行特殊的软中断或陷阱指令时,CPU硬件会将当前程序的状态(包括程序计数器 $\text{PC}$、寄存器等)保存到内核堆栈中,并将执行模式从用户态切换到内核态(Ring 3 $\to$ Ring 0)。

  • 向量表查找: 内核会根据触发陷阱的类型(例如软中断号 $\text{0x80}$)查找中断向量表(Interrupt Vector Table),找到对应的中断服务程序(Interrupt Handler),即系统调用入口点

3. 内核空间处理(系统调用入口)

  • 保存现场: 系统调用入口点的汇编代码会进一步保存用户程序所有的通用寄存器到内核堆栈,以确保系统调用完成后能够恢复用户程序的完整现场。

  • 系统调用分发: 内核根据步骤1中从特定寄存器中取出的系统调用号,在内核的系统调用表(System Call Table,也称 $\text{sys_call_table}$)中查找对应的内核函数地址。

  • 执行内核函数: 内核执行找到的特定系统调用内核函数,例如 $\text{sys_open()}$。这是执行实际功能的地方,所有的权限检查、资源分配、硬件交互等都在这里发生。

4. 系统调用执行与返回

  • 执行操作: $\text{sys_open}$ 会检查文件权限、在文件系统中找到文件、创建或获取一个文件描述符(File Descriptor)等。

  • 返回值处理: 内核函数执行完毕后,会将结果(例如 $\text{open()}$ 成功返回的文件描述符,或失败时的错误码)放入预定的寄存器中。

  • 恢复现场: 内核恢复之前保存的用户程序寄存器,并从内核堆栈中取出被保存的用户程序状态。

5. 从内核空间返回到用户空间

  • 模式切换: 内核执行特殊的返回指令(例如 $\text{iret}$ 或 $\text{sysexit}$)将CPU执行模式从内核态切换回用户态(Ring 0 $\to$ Ring 3)。

  • 返回C库: 程序的控制权返回到步骤1中C库封装函数内的紧接着陷阱指令后面的代码。

  • C库处理返回值: C库封装函数检查从寄存器中取出的返回值。如果返回值表示错误,C库通常会将错误码设置到全局变量 $\text{errno}$ 中,并向用户程序返回 $-1$。

  • 用户程序继续执行: 最终,$\text{open()}$ 或其他C库函数将最终结果返回给用户程序,用户程序从系统调用发起点继续向下执行。

流程图

$用户程序(User Space) \xrightarrow{1. 调用C库函数(\text{open()})} C库封装函数 \xrightarrow{2. 准备参数/系统调用号} C库封装函数 \xrightarrow{3. 触发陷阱/中断(\text{syscall})} 内核态入口 \xrightarrow{4. 查找系统调用表} 内核函数(\text{sys_open()}) \xrightarrow{5. 执行内核操作} 内核函数 \xrightarrow{6. 放置返回值} 内核态出口 \xrightarrow{7. 切换回用户态} C库封装函数 \xrightarrow{8. 返回最终结果} 用户程序(User Space)$