系统调用是通过软中断或syscall指令触发的特权态切换,而非普通函数调用;用户态经int 0x80或syscall陷入内核态,切换栈、保存上下文、查表执行对应内核函数,glibc仅做封装与错误处理。

系统调用不是函数调用,是软中断触发的特权切换
用户程序调用 open()、read() 这类“函数”时,实际执行的不是内核代码,而是 glibc 提供的封装——它最终通过 int 0x80(x86)或 syscall 指令(x86-64)主动陷入内核。这个过程强制 CPU 从用户态(ring 3)切到内核态(ring 0),并跳转到预设的中断处理入口。
关键点在于:用户栈被保留但不再可用;CPU 切换到内核栈;寄存器上下文被保存;内核根据传入的系统调用号(如 __NR_read = 0)查表找到对应内核函数(如 sys_read)。
- glibc 封装会做参数校验、错误码转换(
-1→errno),但不参与真正的 I/O 或内存管理 - 直接写汇编调用
syscall指令绕过 libc 是可行的,但需手动设置%rax(调用号)、%rdi/%rsi/%rdx(参数),且失去可移植性 - 现代 x86-64 推荐用
syscall指令而非int 0x80,后者在 64 位下可能截断指针或触发兼容模式开销
用户态和内核态的内存隔离靠页表和 CR0.WP 位保障
Linux 使用分页机制实现地址空间隔离:每个进程有独立的页表,用户态只能访问标记为 “user accessible” 的页表项(PT_USER 位)。当 CPU 处于用户态时,若尝试访问内核地址(如 0xffff888000000000 起始的直接映射区),会触发 #PF(page fault)异常,由内核的缺页处理程序拦截并终止进程。
内核自身也受保护:CR0 寄存器的 WP(Write Protect)位开启后,即使在内核态,对只读页(如代码段、常量数据)的写操作也会触发异常——这防止了模块或驱动意外覆写内核关键结构。
-
copy_to_user()和copy_from_user()不是简单memcpy,它们会先用access_ok()检查地址是否落在当前进程的用户地址空间范围内,再逐页检查页表权限 - 用户传入的指针(如
buf参数)必须是当前进程虚拟地址空间内的有效地址;传入内核地址(如&some_kernel_var)会导致-EFAULT - 内核态不能直接使用用户栈,所有系统调用入口都会立即切换到 per-CPU 内核栈(通常 16KB),避免栈溢出影响内核稳定性
strace 看到的 read(3, "...", 1024) 实际经历了三次上下文切换
运行 strace -e trace=read ./a.out 显示一行 read(3, "hello\n", 1024) = 6,但这背后至少发生三次 CPU 特权级切换:
user: call read() → enter kernel (1st switch) kernel: do_syscall_64() → sys_read() → vfs_read() → ... → copy_to_user() kernel: 返回前准备用户寄存器、恢复用户栈指针 → exit_to_user_mode() (2nd switch) user: read() 返回,但此时仍处于用户态;若后续有信号待投递,会再陷入内核处理(3rd switch)
每次切换涉及寄存器保存/恢复、TLB 刷新(部分架构)、栈切换,开销远高于普通函数调用。频繁小读写(如循环调用 read(fd, &c, 1))性能极差,本质是把 I/O 变成了系统调用风暴。
- 系统调用号本身不跨架构:x86-64 的
__NR_read是 0,arm64 也是 0,但寄存器传参约定不同(arm64 用x0~x7) -
strace依赖ptrace(PTRACE_SYSCALL)在每次进入/退出系统调用时暂停目标进程,因此本身会显著拖慢被跟踪程序 - 真正零拷贝路径(如
splice()、io_uring)的目标就是减少甚至消除用户/内核间的数据拷贝和上下文切换次数
自定义系统调用为什么现在几乎没人做
添加新系统调用需修改内核源码(arch/x86/entry/syscalls/syscall_table_64.c)、分配唯一调用号、提供稳定 ABI,并面临上游拒绝合入的风险。相比而言,字符设备驱动 + ioctl()、eBPF 程序、用户态协议栈(如 DPDK)、或 io_uring 提供的扩展接口更安全、灵活、无需重启内核。
- 新增系统调用一旦合入主线,就必须永久维护 ABI 兼容性,连参数语义都不能变——比如
stat()的 struct layout 锁死几十年 - Android 的
binder、Chrome OS 的minijail都没加新 syscall,而是基于现有机制(ioctl、seccomp、memfd_create)构建 - 真正需要内核介入的新功能(如 cgroup v2、landlock)都走 netlink、procfs、sysfs 等已有通道,而非塞进 syscall 表
用户态和内核态的边界清晰,但跨越它的成本比想象中高;多数优化方向不是“让系统调用更快”,而是“少调用几次”。










