系统调用是通过软中断或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)。
-1 → errno),但不参与真正的 I/O 或内存管理syscall 指令绕过 libc 是可行的,但需手动设置 %rax(调用号)、%rdi/%rsi/%rdx(参数),且失去可移植性syscall 指令而非 int 0x80,后者在 64 位下可能截断指针或触发兼容模式开销Linux 使用分页机制实现地址空间隔离:每个进程有独立的页表,用户态只能访问标记为 “user accessible” 的页表项(PT_USER 位)。当 CPU 处于用户态时,若尝试访问内核地址(如 0xffff888000000000 起始的直接映射区),会触发 #PF(pa
ge fault)异常,由内核的缺页处理程序拦截并终止进程。
内核自身也受保护:CR0 寄存器的 WP(Write Protect)位开启后,即使在内核态,对只读页(如代码段、常量数据)的写操作也会触发异常——这防止了模块或驱动意外覆写内核关键结构。
copy_to_user() 和 copy_from_user() 不是简单 memcpy,它们会先用 access_ok() 检查地址是否落在当前进程的用户地址空间范围内,再逐页检查页表权限buf 参数)必须是当前进程虚拟地址空间内的有效地址;传入内核地址(如 &some_kernel_var)会导致 -EFAULT
运行 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 变成了系统调用风暴。
__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 提供的扩展接口更安全、灵活、无需重启内核。
stat() 的 struct layout 锁死几十年binder、Chrome OS 的 minijail 都没加新 syscall,而是基于现有机制(ioctl、seccomp、memfd_create)构建用户态和内核态的边界清晰,但跨越它的成本比想象中高;多数优化方向不是“让系统调用更快”,而是“少调用几次”。
# linux
# 接口
# Struct
# binder
# 而非
# 切换到
# 也会
# 已有
# 都没
# 几次
# 这类
# 落在
# 分页
# 指针
# 循环
# int
# android
# access
# 栈
# ai
# switch
# linux系统
# 为什么
# 架构
# chrome
# 常量
# 封装
# errno
# 自定义