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),必须通过系统调用切换到内核态才能完成这些操作。
1.4 用户态与内核态的切换流程
当用户程序发起系统调用时,CPU 和内核协同完成以下关键步骤:
- 触发陷阱:用户程序执行
SYSCALL(x86-64)或SYSENTER(x86-32)指令,CPU 自动切换到 Ring 0。 - 保存上下文:CPU 将用户态的寄存器(RIP、CS、RSP、SS 等)压入内核栈。
- 查找系统调用表:内核根据系统调用号在
sys_call_table中定位对应的处理函数。 - 执行内核函数:以内核态权限执行具体的系统调用服务例程。
- 返回用户态:执行
SYSRET指令,恢复用户态上下文,返回 Ring 3。
性能注意:每次系统调用都涉及用户态与内核态的切换,存在上下文保存/恢复的开销。高频系统调用(如网络 I/O)可能成为性能瓶颈,现代内核通过
vDSO(虚拟动态共享对象)将部分系统调用(如 gettimeofday())直接映射到用户空间执行,避免陷入内核。
1.5 系统调用与标准 C 库的关系
应用程序通常不直接调用系统调用,而是通过标准 C 库(glibc、musl 等)提供的封装函数间接发起。C 库承担了以下职责:
- 参数封装:将函数参数按照系统调用约定放入寄存器。
- 陷入内核:执行
SYSCALL指令触发上下文切换。 - 错误处理:检查内核返回值,若为负值则设置
errno并返回 -1。 - 兼容性:屏蔽不同架构的系统调用差异,提供统一的 POSIX 接口。
示例对比:
C 库函数
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 命令查看当前内核支持的所有系统调用及其编号。