1.系统调用基础架构
1.1.系统调用入口
系统调用是用户态进入内核态的唯一受控通道。在 x86_64 架构下:
- 用户态:通过 syscall 指令触发(替代了旧版的 int 0x80)
- 系统调用号:存放在 rax 寄存器(如 read 是 0,write 是 1)
- 入口点:entry_SYSCALL_64(位于 arch/x86/entry/entry_64.S)
1.2.完整调用路径
c
用户程序 → glibc 封装 → syscall 指令 → CPU 特权级切换(Ring3→Ring0)
→ entry_SYSCALL_64 → do_syscall_64 → 系统调用表(sys_call_table)
→ 具体内核函数 → 返回用户态
2.参数传递机制
2.1.寄存器传参(x86_64)
| 寄存器 | 用途 |
|---|---|
rax |
系统调用号 |
rdi |
第 1 个参数 |
rsi |
第 2 个参数 |
rdx |
第 3 个参数 |
r10 |
第 4 个参数(注意:不是 rcx,因为 rcx 被 syscall 指令占用) |
r8 |
第 5 个参数 |
r9 |
第 6 个参数 |
示例:read(fd, buf, count) 调用时
c
mov rax, 0 # __NR_read
mov rdi, fd
mov rsi, buf
mov rdx, count
syscall
2.2.参数超过 6 个怎么办?
- 结构体指针:将多余参数打包成结构体,传递指针
- 不常见:Linux 系统调用设计原则上不超过 6 个参数
3.返回值处理
3.1.返回值规范
- 成功:返回值通过 rax 传回用户态
- 错误:返回负的错误码(如 -EINVAL,即 -22)
- glibc 转换:glibc 将负错误码转为正数存入 errno,并返回 -1
3.2.内核中的返回路径
c
// 内核态系统调用函数签名示例
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
// 返回实际读取的字节数,或负错误码
return retval;
}
3.3.错误码映射
| 内核返回 | 用户态 errno | 含义 |
|---|---|---|
-EPERM |
1 |
操作不允许 |
-ENOENT |
2 |
无此文件 |
-EAGAIN |
11 |
资源暂时不可用 |
-ENOMEM |
12 |
内存不足 |
-EINVAL |
22 |
无效参数 |
4.用户态/内核态内存访问
4.1.核心问题:地址空间隔离
- 用户态:每个进程独立的虚拟地址空间(
0~128TB) - 内核态:高地址空间(
128TB~256TB),所有进程共享内核页表 - 风险:内核直接解引用用户态指针可能导致崩溃或安全漏洞
4.2.安全拷贝接口
copy_from_user / copy_to_user
c
// 从用户空间拷贝到内核空间
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
// 从内核空间拷贝到用户空间
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);
返回值:未成功拷贝的字节数(0 表示完全成功)
c
SYSCALL_DEFINE2(my_syscall, char __user *, user_buf, size_t, len)
{
char *kernel_buf;
kernel_buf = kmalloc(len, GFP_KERNEL);
if (!kernel_buf)
return -ENOMEM;
// 安全拷贝:处理缺页、权限检查
if (copy_from_user(kernel_buf, user_buf, len)) {
kfree(kernel_buf);
return -EFAULT; // 用户地址无效
}
// ... 处理数据 ...
if (copy_to_user(user_buf, kernel_buf, len)) {
kfree(kernel_buf);
return -EFAULT;
}
kfree(kernel_buf);
return 0;
}
4.3.其他访问辅助函数
| 函数 | 用途 |
|---|---|
get_user(x, ptr) |
从用户空间读取基本类型(1/2/4/8 字节) |
put_user(x, ptr) |
向用户空间写入基本类型 |
strncpy_from_user |
安全拷贝用户态字符串 |
clear_user |
清零用户空间内存 |
access_ok(addr, size) |
检查用户地址是否合法(不检查页表映射) |
5.性能优化策略
5.1.减少系统调用次数
批量操作优于多次单操作:
c
// 差:1000 次 write 调用 = 1000 次上下文切换
for (i = 0; i < 1000; i++) write(fd, buf + i, 1);
// 优:1 次 write 调用
write(fd, buf, 1000);
实际应用:
- writev/readv:一次调用传输多个不连续缓冲区
- sendfile:内核态直接文件到 socket 拷贝,零用户态参与
- splice:管道与文件/socket 间的零拷贝传输
5.2.零拷贝技术 (Zero-Copy)
sendfile 系统调用
c
sendfile(out_fd, in_fd, &offset, count);
- 数据从文件页缓存直接拷贝到 socket 缓冲区
- 省去:磁盘 → 内核页缓存 → 用户缓冲区 → 内核 socket 缓冲
mmap + write:
c
buf = mmap(NULL, len, PROT_READ, MAP_PRIVATE, fd, 0);
write(socket_fd, buf, len);
- 文件内容映射到用户地址空间,省去 read 的数据拷贝
- 但仍有一次 write 的系统调用开销
5.3.vDSO (Virtual Dynamic Shared Object)
- 机制:将部分高频系统调用(如 gettimeofday、clock_gettime)映射到用户态共享库
- 效果:无需进入内核态,直接读取内核更新的内存页
- 适用:只读且无需内核实时参与的操作
5.4.批量系统调用:io_uring
Linux 5.1+ 引入的高性能异步 I/O 接口:
c
// 提交一批请求,一次系统调用
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, len, offset);
io_uring_submit(&ring); // 一次 syscall 提交多个操作
优势:
- 批量提交/收割,摊平系统调用开销
- 支持轮询模式(IORING_SETUP_IOPOLL),完全绕过系统调用
- 性能接近 DPDK/SPDK,但保持内核安全性
5.5.内核内部优化
| 技术 | 原理 |
|---|---|
| per-CPU 变量 | 避免缓存行 bouncing,减少锁竞争 |
| RCU 读路径 | 读操作无锁,零开销遍历链表/树 |
| Slab 分配器 | 对象缓存,减少内存分配碎片 |
| 页缓存预读 | 顺序读取时异步预加载下一页 |
| 直接 I/O (O_DIRECT) | 绕过页缓存,适合数据库等自缓存应用 |
5.6.上下文切换开销优化
系统调用本身开销约 100~300ns(现代 CPU),主要成本:
- 特权级切换(Ring3→Ring0→Ring3)
- 寄存器保存/恢复
优化方向:
- 使用 io_uring 将多次 I/O 合并为一次提交
- 使用 epoll/io_uring 替代 select/poll,减少每次等待的系统调用
- 用户态轮询(如 io_uring 的 polling 模式)
6.总结速查
| 维度 | 关键点 |
|---|---|
| 传参 | x86_64 用 rdi/rsi/rdx/r10/r8/r9,系统调用号在 rax |
| 返回值 | 内核返负错误码,glibc 转 errno;成功值在 rax |
| 内存访问 | 必须用 copy_*_user,用户指针标记 __user |
| 性能核心 | 减少切换次数 > 零拷贝 > 批量提交 > vDSO 捷径 |