“系统调用究竟是如何工作的?”
这是我刚入门编程时,最让我困惑的问题之一。
乍一看,它似乎特别简单。
比如:
write(fd, buf, size);
程序调用一个 write(),内核做点事情,返回结果,然后结束。
但真正让我开始深入思考的是:
内核又是怎么知道这次我要调用的是 write(),而不是 read()?
原来,这个看似“简单”的函数调用背后,隐藏着一段出人意料的复杂过程。
今天,让我们借助这篇文章,一步一步地来了解它。
什么是系统调用?
你的程序,看似能操作整个计算机,但实际上,它一直被困在一个受限空间里,它不能自行操作硬件、读取文件或发送网络数据包——这些都由内核控制。
系统调用(syscall)就是你的程序在告诉内核:“嘿,内核,我需要你帮我做点什么。”,它也是用户态代码进入内核态的唯一合法入口。
C 语言中read()、write()、open()、fork() 等函数,最终都会触发系统调用。C 标准库仅对系统调用做了上层封装,底层始终依赖用户态与内核态的交互协作。
两大空间
要理解系统调用存在的意义,必须了解 CPU 的两种运行模式:
- • 用户空间(非特权模式 ):应用程序在此运行。它们的访问权限受到限制。它们不能直接访问硬件、操作其他进程拥有的内存或禁用中断。
- • 内核空间(特权模式:):这是操作系统核心运行的地方。它拥有对所有硬件(CPU、内存、磁盘、网卡)和指令的完全访问权限。
┌─────────────────────────────────────────────────────────┐│ USER SPACE ││ ││ Your App │ libc (glibc) │ Other Libraries ││ │├─────────────────────────────────────────────────────────┤│ <<< SYSCALL BOUNDARY >>> │├─────────────────────────────────────────────────────────┤│ KERNEL SPACE ││ ││ Syscall Handler → Syscall Table → Service Routines ││ │└─────────────────────────────────────────────────────────┘
系统调用是用户空间中的代码进入内核空间执行特定任务的唯一合法途径。
三大关键角色
在前面内容中,我们提到了用户态通过系统调用进入内核空间执行特定任务,也就是user → kernel → 返回结果,但内核内部到底是谁在接待 syscall?
其实当一个 syscall 指令进入内核时,会依次经过 三位关键角色。
syscall instruction │ ▼System Call Handler(entry_SYSCALL_64) │ ▼System Call Table(sys_call_table[]) │ ▼System Call Routine(e.g. sys_write)
当 CPU 执行 syscall 指令后,第一站并不是 sys_write(),而是entry_SYSCALL_64。
这个阶段做的事情,本质上只有一句话:
先确认你是谁,再决定把你带去哪里
接着就是查座位表(sys_call_table),它维护了一个巨大函数指针数组:
sys_call_table[1] → sys_writesys_call_table[0] → sys_read...
所以流程变成:
rax = 1→ sys_call_table[rax]→ 找到 sys_write→ 跳转
这一步的本质:
用数组索引做 O(1) 跳转
rax 中保存的,其实就是 syscall number。
最后才是真正的调用系统函数,sys_write、sys_read或者sys_open等。
不过,到这里还有一个问题没有解决。
我们已经知道:
- • 内核会通过 syscall table 找到对应函数
- • 最终执行 sys_write()、sys_read()
但:
一个最普通的 write(), 到底是如何一步步进入内核的?
比如:
- • syscall number 是谁放进寄存器的?
- •
syscall 指令执行后,CPU 内部发生了什么? - • entry_SYSCALL_64 又到底干了哪些事情?
这些问题,才是真正有意思的地方。
而下一篇文章,我们就会真正跟着一次 write() 调用:
从用户态一路走进 Linux 内核。