1. 系统调用概述

1.1 什么是系统调用

系统调用(System Call)是操作系统内核提供给用户空间程序的一组标准接口,是用户程序请求内核服务的唯一合法途径。当应用程序需要执行特权操作(如读写文件、创建进程、网络通信)时,必须通过系统调用陷入内核,由内核代表用户程序完成操作。

核心定义:系统调用是用户态与内核态之间的桥梁,是受控的特权操作入口。

1.2 系统调用的作用

系统调用在操作系统中承担着不可替代的核心作用,主要体现在以下五个方面:

作用维度 具体说明 典型示例
资源管理 统一管理 CPU、内存、I/O 设备等硬件资源 brk()、mmap()
安全隔离 防止用户程序直接访问硬件或破坏其他进程 fork()、execve()
抽象封装 将硬件差异隐藏,提供统一的编程接口 read()、write()
进程通信 提供进程间数据交换与同步机制 pipe()、shmget()
系统信息 获取或设置系统运行状态与参数 getpid()、time()

1.3 用户态与内核态

现代 CPU 通过硬件级特权级别(Privilege Level)实现用户态与内核态的隔离。以 x86 架构为例,CPU 提供 4 个特权环(Ring 0 ~ Ring 3),Linux 仅使用其中两个:

  • Ring 3(用户态):应用程序运行在此级别,只能访问受限的指令和地址空间。
  • Ring 0(内核态):操作系统内核运行在此级别,可执行所有 CPU 指令并访问全部内存。
关键理解:用户态程序无法直接执行特权指令(如 HLT、LGDT、端口 I/O),必须通过系统调用切换到内核态才能完成这些操作。
用户态与内核态特权级架构 Ring 0 — 内核态 完全特权:所有指令 + 全部内存访问 Ring 3 — 用户态 受限特权:仅可执行非特权指令 + 用户空间内存 系统调用 (陷入内核) 返回 (恢复用户态) 系统调用是用户态进入内核态的唯一合法通道 通过 SYSCALL/SYSENTER 指令触发,由内核完成特权操作后返回

1.4 用户态与内核态的切换流程

当用户程序发起系统调用时,CPU 和内核协同完成以下关键步骤:

  1. 触发陷阱:用户程序执行 SYSCALL(x86-64)或 SYSENTER(x86-32)指令,CPU 自动切换到 Ring 0。
  2. 保存上下文:CPU 将用户态的寄存器(RIP、CS、RSP、SS 等)压入内核栈。
  3. 查找系统调用表:内核根据系统调用号在 sys_call_table 中定位对应的处理函数。
  4. 执行内核函数:以内核态权限执行具体的系统调用服务例程。
  5. 返回用户态:执行 SYSRET 指令,恢复用户态上下文,返回 Ring 3。
性能注意:每次系统调用都涉及用户态与内核态的切换,存在上下文保存/恢复的开销。高频系统调用(如网络 I/O)可能成为性能瓶颈,现代内核通过 vDSO(虚拟动态共享对象)将部分系统调用(如 gettimeofday())直接映射到用户空间执行,避免陷入内核。
系统调用用户态↔内核态切换流程 用户态(Ring 3) 应用程序调用 read() / write() 等 CPU 硬件切换 执行 SYSCALL 指令,自动切换 Ring 内核态(Ring 0) 执行 sys_read() / sys_write() 等 恢复用户态 执行 SYSRET 指令,返回 Ring 3 整个切换过程由 CPU 硬件和内核共同完成 用户程序无需关心底层细节,只需调用标准 C 库函数

1.5 系统调用与标准 C 库的关系

应用程序通常不直接调用系统调用,而是通过标准 C 库(glibc、musl 等)提供的封装函数间接发起。C 库承担了以下职责:

  • 参数封装:将函数参数按照系统调用约定放入寄存器。
  • 陷入内核:执行 SYSCALL 指令触发上下文切换。
  • 错误处理:检查内核返回值,若为负值则设置 errno 并返回 -1。
  • 兼容性:屏蔽不同架构的系统调用差异,提供统一的 POSIX 接口。
示例对比:
C 库函数 fwrite() → 内部调用 write() → 触发系统调用 sys_write() → 内核执行文件写入。

1.6 系统调用编号与查找

每个系统调用在 Linux 内核中都有一个唯一的编号(System Call Number),定义在 arch/x86/entry/syscalls/syscall_64.tbl 中。用户程序通过将系统调用号放入 rax 寄存器来指定要调用的服务。

系统调用号 函数名 C 库封装 功能描述
0 sys_read read() 从文件描述符读取数据
1 sys_write write() 向文件描述符写入数据
2 sys_open open() 打开或创建文件
39 sys_getpid getpid() 获取当前进程 ID
56 sys_clone clone() 创建子进程
查看系统调用表:在 Linux 系统中,可以通过 ausyscall --dump 命令查看当前内核支持的所有系统调用及其编号。

1.7 本章小结