1 Linux 系统调用与接口层
本文说明 Linux 上用户进程如何请求内核服务 :从应用程序看到的「接口」一直讲到内核里如何分派到具体实现。侧重点在 机制与语义 (什么保证成立、什么随 ABI/架构变化),便于你建立稳定的心智模型;具体寄存器与调用约定因 CPU 架构 而异,文中在「架构相关」处会显式标注。
1.1 术语与边界
-
系统调用(system call) :用户态进入内核、以编号 + 约定好的参数 请求一项受控操作的机制。它是用户态与内核态之间的主通道之一 (此外还有异常、中断、内存映射的
mmap区域配合等)。 -
接口层在本文里指三层叠在一起的东西,不要混为一谈:
-
C 库封装 (典型为 glibc / musl ):
open(3)、read(3)等,负责把内核返回的错误码转换成-1+errno等 POSIX 语义。 -
ABI 调用约定 :哪些寄存器传参、哪个寄存器放系统调用号、返回值放在哪里(随 x86_64、ARM64、riscv 等变化)。
-
内核入口与分派 :从 CPU 陷入点到
sys_*/SYSCALL_DEFINE*处理器,再到具体子系统(VFS、网络、进程管理等)。
1.2 用户进程里看到的「第一层」:C 库与手册页
1.2.1 手册节号(准确性细节)
-
open(2)、read(2)这类数字 2 表示 系统调用包装(由内核提供语义,经 libc 暴露)。 -
printf(3)数字 3 表示库函数 ,不一定对应单次系统调用;例如printf常缓冲,未必每次调用都write。
因此「read(3) 还是 read(2)」在不同系统上排版习惯略有差异;Linux 上讨论「系统调用」时以 man 2 syscalls 的列表为准更稳妥。
1.2.2 errno 与返回值(必须搞清的语义)
在 Linux 上,内核侧系统调用实现通常 成功返回非负整数或指针 cast 的 long ;失败返回负的 errno 值 (例如 -EINVAL)。
glibc 等 C 库的薄包装一般会:
- 若内核返回表示错误,则把
errno设为对应的正值(如EINVAL),并向调用者返回-1(对返回指针的 API 则为NULL等由 API 规定的失败形态)。
结论 :你在应用里看到的「-1 + errno」是 libc 合同 ;内核里看到的是 负 errno 的 long 。调试时如果绕过 libc 直接 syscall(),就要自己处理内核返回的 long。
1.3 从 read() 到内核:一次请求的典型路径(概念)
以「读文件」为例,概念链为:
应用调用 read(fd, buf, count) → libc 中的封装 (可能一条或少量指令序列触发陷入)→ CPU 模式切换 (用户态 → 内核态,保存寄存器/栈)→ 架构相关的入口桩代码 → 通用层根据系统调用号查表 → 具体实现 (如 vfs_read 路径上的逻辑)→ 返回,逆向恢复用户态。
其中「查表」在 Linux 上常见形态是 每个架构维护自己的系统调用表(名称、编号、参数寄存器布局都可能不同)。
1.4 架构相关:陷入机制(读到这里要切换「架构脑」)
1.4.1 x86_64(常见服务器与部分桌面)
-
历史上存在
int 0x80(32 位兼容路径)、sysenter等;64 位 Linux 主流 使用syscall指令 进入内核(配合 MSR 如STAR/LSTAR/SFMASK等由内核启动时配置)。 -
用户态调用约定与内核入口约定由 ABI 文档规定(System V AMD64 ABI 与内核文档共同约束「哪个寄存器是 syscall 号、哪些是 arg1...」)。
1.4.2 ARM AArch64(ARM64,常见嵌入式与手机服务器)
- 典型使用
SVC #0(supervisor call)触发异常进入内核,系统调用号与参数在约定寄存器中传递。
1.4.3 重要结论(避免学错)
-
系统调用号在 ARM64 与 x86_64 上不相同;同一段 C 代码跨架构编译没问题,但「编号」不可移植。
-
参数类型在 32/64 位之间 有额外陷阱(
long长度、off_t与off64_t、结构体按值传递等),这也是存在openat等较新接口的原因之一。
1.5 vDSO:看起来像系统调用、但常常不「进内核慢路径」
内核会把一页用户态可执行 的共享库映射进每个进程:vDSO(virtual dynamic shared object)。
1.5.1 作用(准确表述)
对某些极高频且内核可提供纯用户态安全实现的查询,Linux 通过 vDSO 提供函数入口,使得:
gettimeofday/clock_gettime(单调时钟等) 、getcpu等在常见配置下可走 用户态快速路径,避免完整系统调用的模式切换成本。
1.5.2 你需要建立的判断
-
strace里看不到某些调用 ,不一定表示「没发生系统调用级别的工作」,也可能是 vDSO 命中 或 libc 优化路径。 -
仍有一些
clock_gettime场景会回落到真实系统调用(取决于时钟类型、硬件、内核配置)。
1.6 直接系统调用:syscall(2) 与 SYS_ 常量
Linux 提供 syscall(2)(libc 函数)用于按编号直接发起调用。用途包括:
-
极新、你使用的 glibc 尚未封装 的系统调用;
-
测试、沙箱、语言运行时实现;
-
需要精细控制错误处理的场景。
注意:
-
必须包含正确架构下的
SYS_xxx常量 (来自unistd.h/asm/unistd.h体系)。 -
可变参数与寄存器传递规则容易写错;跨架构可移植性差。
1.7 内核侧:从入口到 SYSCALL_DEFINE
1.7.1 分派模型(Linux 通用心智)
内核在架构相关入口完成:
-
保存用户上下文(寄存器等);
-
读取系统调用号;
-
做合法性检查(范围、seccomp 等);
-
调用 通用分派函数,最终进入具体 handler。
1.7.2 SYSCALL_DEFINE 宏族(阅读源码的导航)
内核子系统里的处理器常见写法是:
SYSCALL_DEFINE3(name, type1, arg1, ...):生成带元数据的sys_name之类符号,并与 tracepoint、ftrace、audit 等基础设施挂钩。
你在 fs/read_write.c、kernel/fork.c 等文件里看到的定义,多数属于这一族。
1.7.3 asmlinkage(历史与阅读线索)
在老式内核接口描述里常看到 asmlinkage :提示该函数参数来自寄存器/栈的特定调用约定(与架构入口桩衔接)。现代源码阅读时把它当作「这是系统调用处理器」的线索即可,不必背所有架构细节。
1.8 兼容层:compat_sys* 与 32/64 位
在 64 位内核运行 32 位用户态程序时,常存在 compat 系统调用:把 32 位用户参数布局、结构体对齐、指针宽度转换成内核 64 位侧可消费的形式。
这解释了很多「同名系统调用为何有两套入口」的现象。
1.9 与「不是系统调用」的接口对比(建立边界)
1.9.1 /proc 与 /sys
- 多数是 虚拟文件系统 接口:用户通过
read/write与内核交互,但底层不一定是「一次一个 syscall 号」的映射关系;它走 VFS 与各类seq_file、属性回调。
1.9.2 ioctl(2)
- 仍是系统调用,但子命令由驱动/子系统解释;ABI 与兼容性风险更高,属于「宽接口」。
1.9.3 mmap + 协议(共享内存 IPC)
mmap本身是系统调用;后续通信可能主要在用户态完成,内核只负责页错误、映射关系等。
1.9.4 io_uring(现代高吞吐 I/O)
- 仍依赖系统调用完成 ring 设置 ,但大量提交/完成走 共享内存 ring ,属于「系统调用 + 批处理接口」的演进方向;学习传统 syscall 模型后再学
io_uring会更稳。
1.10 安全与策略:seccomp、namespace、capabilities(接口层的「守门人」)
系统调用是攻击面与隔离策略的关键锚点:
-
seccomp:按规则允许/拒绝系统调用(及参数过滤,取决于模式与内核版本能力)。
-
capabilities:把传统 root 权力拆分;许多操作不再要求 UID 0,而要求特定 capability(与 syscall 检查点绑定)。
-
namespaces / cgroups:改变「对象可见性」与资源限额;系统调用在内核里访问对象时会受这些上下文约束。
准确结论 :「能 open 成功」不仅取决于路径权限,还取决于 挂载命名空间 、AppArmor/SELinux 、Landlock(若启用)等多层策略栈。
1.11 观测与实验(把理解落到可验证事实)
1.11.1 strace
-
跟踪系统调用(默认路径);对 vDSO 快速路径可能不完整显示。
-
常用:
strace -f -e trace=file,desc -o log.txt your_cmd。
1.11.2 perf / ftrace
- 观察 syscall 进入次数、延迟分布、内核侧 handler 耗时(需要合适权限与配置)。
1.11.3 /usr/include/asm/unistd_64.h(名称随发行版略有差异)
- 可查看 x86_64 上的
__NR_常量(学习用;应用代码应优先用 libc 常量体系)。
1.12 常见误解纠正(准确性清单)
-
误解 :「系统调用 = libc 函数」。正解 :多数 libc 函数是包装;不少库函数不发 syscall;少数
syscall()绕过 libc 语义糖。 -
误解 :「系统调用编号跨平台一致」。正解 :编号是 ABI/架构 属性。
-
误解 :「
strace列出的就是程序所有内核交互成本」。正解:还有 vDSO、页错误路径、中断驱动逻辑等。 -
误解 :「内核返回
-1表示错误」。正解 :内核通常返回 负 errno ;-1常见于 libc 包装层。
1.13 建议阅读顺序(从手册到源码)
-
man 2 syscalls:总览 Linux 提供的调用集合(随版本增长)。 -
man 2 intro:错误码与返回值惯例。 -
man 2 syscall:直接调用注意事项。 -
内核源码:从你关心的一个
SYSCALL_DEFINE*开始向下跟到 VFS/网络/调度子系统。
1.14 版本说明
系统调用集合、seccomp、io_uring 能力与 vDSO 覆盖范围会随 内核主版本 变化;学习时以你机器上的 man 与 uname -r 对应内核文档为准核对细节。本文刻意少绑定具体小版本号,以降低「一句话在 5.x 真、在 6.x 变」带来的不精确风险。