【Note】《深入理解Linux内核》Chapter 10 :Linux 内核中的系统调用机制全解析

《深入理解Linux内核》Chapter 10 :Linux 内核中的系统调用机制全解析

摘要:系统调用、内核态、用户态、中断门、syscall 表、寄存器传参、x86 调用号、调用路径、安全检查、性能优化


一、什么是系统调用?

系统调用(System Call)是操作系统为用户程序提供的 受控访问内核资源 的接口。

用户态程序不能直接访问内核内存、I/O、文件、进程管理等,必须通过系统调用请求服务。

典型的系统调用包括:

  • 文件操作:open(), read(), write(), close()
  • 进程控制:fork(), execve(), exit(), wait()
  • 内存管理:mmap(), brk()
  • 网络通信:socket(), bind(), recv(), send()

二、系统调用的实现机制总览

系统调用流程可分为以下几个步骤:

  1. 用户程序调用 glibc 封装函数(如 read());
  2. glibc 使用特定指令触发 CPU 进入内核态;
  3. 内核接收调用号和参数,执行对应服务;
  4. 系统调用完成后返回用户态,恢复现场。

三、系统调用号与调用表

每个系统调用对应一个唯一编号(call number),在源码中定义:

c 复制代码
// include/uapi/asm-generic/unistd.h
#define __NR_read 0
#define __NR_write 1
#define __NR_open 2
...

3.1 系统调用表

Linux 使用系统调用表(system call table)将编号映射为内核函数指针:

c 复制代码
// arch/x86/kernel/syscall_table.S
ENTRY(sys_call_table)
    .quad sys_read
    .quad sys_write
    .quad sys_open

当用户程序触发系统调用后,内核根据调用号(保存在寄存器中)查表找到实际执行函数。


四、用户态与内核态的切换机制

4.1 切换指令:int 0x80 与 syscall

指令 架构 说明
int 0x80 x86(32 位) 软中断,早期实现
syscall x86-64 新指令,更高效
sysenter Intel x86 特定快速调用方式

x86-64 上,glibc 封装函数使用 syscall 指令:

asm 复制代码
mov $60, %rax       ; syscall号:exit
mov $0, %rdi        ; 参数1:返回码
syscall             ; 进入内核
  • 系统调用号存入 %rax
  • 参数存入 %rdi, %rsi, %rdx, %r10, %r8, %r9
  • 返回值存入 %rax

五、系统调用的内核路径:从入口到处理函数

以 64 位系统为例,系统调用进入路径如下:

5.1 系统调用入口

c 复制代码
// arch/x86/entry/entry_64.S
entry_SYSCALL_64:
    ...
    call do_syscall_64

5.2 调用处理函数

c 复制代码
// arch/x86/kernel/syscall.c
asmlinkage __visible void do_syscall_64(struct pt_regs *regs)
{
    syscall_nr = regs->orig_rax;
    ...
    syscall_fn = syscall_table[syscall_nr];
    return syscall_fn(...);
}

5.3 参数获取与返回

  • 参数从 regs 中提取;
  • 执行调用逻辑;
  • 结果写回 %rax

六、内核中定义系统调用

6.1 定义系统调用函数

c 复制代码
// kernel/sys.c
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
    // 实现细节
}
  • SYSCALL_DEFINEn 宏自动生成封装函数,处理参数验证与展开;
  • 宏展开后生成 sys_read()、参数提取与类型转换逻辑。

6.2 注册系统调用

系统调用在构建过程中自动注册进 sys_call_table

asm 复制代码
sys_call_table:
    .quad sys_read
    .quad sys_write

修改或添加系统调用需更新 unistd.hsyscall_table.S 等。


七、系统调用与安全性机制

系统调用是用户空间进入内核的唯一合法入口,内核必须防止以下安全风险:

7.1 用户空间指针校验

  • copy_from_user()
  • copy_to_user()

防止内核直接访问非法用户地址。

7.2 访问控制

  • 文件权限检查;
  • 信号发送需检查目标进程权限;
  • 特权系统调用如 mount() 要求 CAP_SYS_ADMIN;

7.3 seccomp 限制调用集

c 复制代码
prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

或使用 BPF 定义调用白名单:

c 复制代码
struct sock_filter prog[] = {
    BPF_STMT(BPF_LD + BPF_W + BPF_ABS, syscall_nr_offset),
    BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_KILL)
};

八、系统调用的性能优化

8.1 syscall vs 用户空间函数

系统调用涉及:

  • 用户态到内核态切换;
  • TLB 刷新;
  • 中断门上下文保护。

因此较 memcpy() 等纯用户函数慢数十倍。

8.2 减少系统调用开销的方法

  • 合并调用(如 readv()/writev());
  • 避免频繁 gettimeofday()
  • 使用 mmap() 替代频繁读写;
  • 使用 epoll() 替代反复 poll()

九、系统调用与 glibc 的关系

glibc 是用户程序的标准 C 库,它通过封装系统调用提供 API 接口:

c 复制代码
ssize_t read(int fd, void *buf, size_t count) {
    return syscall(SYS_read, fd, buf, count);
}

glibc 可提供缓存(如 malloc())、错误处理、异步支持。

一些函数并非系统调用,而是用户空间实现(如 printf())。


十、系统调用调试与分析工具

工具 用途
strace 捕捉系统调用及其参数、返回值
perf trace 性能事件与系统调用时间分析
ltrace 跟踪动态库函数调用
gdb 可设置断点在 syscall 封装层或内核
/proc/<pid>/syscall 显示进程当前正在执行的系统调用
BPF + bcc 工具 内核层系统调用跟踪

十一、Linux 的系统调用分类

类型 说明 示例函数
进程管理 创建、终止、等待、执行 fork(), execve(), wait()
文件系统 打开、读写、关闭、seek open(), read(), close()
内存管理 分配、映射、调整 mmap(), brk(), mprotect()
网络通信 套接字、连接、接收发送 socket(), send(), recv()
IPC 管道、共享内存、信号、消息队列 pipe(), shmget(), kill()
时间 计时、延迟 nanosleep(), gettimeofday()
特权操作 装载模块、挂载文件系统 mount(), reboot()

十二、系统调用兼容性与体系结构差异

Linux 针对不同体系结构定义不同调用号及参数寄存器规范:

架构 系统调用号寄存器 参数传递顺序
x86-64 rax rdi, rsi, rdx, r10, r8, r9
x86-32 eax ebx, ecx, edx, esi, edi, ebp
ARM r7 r0-r6

不同架构维护各自的 syscall 表和入口代码。


十三、自定义系统调用实验建议(内核开发者)

  1. 修改 arch/x86/entry/syscalls/syscall_64.tbl 添加自定义 syscall 编号;
  2. kernel/sys.c 添加实现函数(SYSCALL_DEFINEx);
  3. 编译内核并加载;
  4. 使用内核模块或测试程序调用 syscall;
  5. 使用 strace 或 BPF 验证调用行为;

十四、与中断/异常/陷阱机制的关系

系统调用本质上是一种用户主动触发的陷阱(trap):

  • 与硬件异常(page fault)不同,系统调用是显式行为;
  • 使用 int 0x80syscall 指令建立陷阱门;
  • 进入中断门时自动保存上下文并切换内核栈。

十五、总结与对比表

项目 描述
定义方式 使用 SYSCALL_DEFINE() 宏定义内核函数
注册机制 添加到 sys_call_table 映射中
调用方式 通过特定 CPU 指令触发陷阱进入内核
参数传递方式 寄存器传参(架构相关)
安全检查 检查用户空间指针、权限、合法性等
性能特点 上下文切换、用户态-内核态来回切换
扩展机制 可用 BPF 或 seccomp 进行控制或过滤