系统调用机制
系统调用(System Call)是用户空间程序访问内核服务的唯一合法途径。本章深入探讨系统调用的实现机制和使用方法。
系统调用原理
用户空间与内核空间切换
用户空间 (Ring 3)
↓
用户程序调用库函数
↓
库函数封装系统调用
↓
执行 syscall/int 0x80 指令
↓
CPU 切换到内核模式 (Ring 0)
↓
内核空间
↓
系统调用分发器
↓
执行对应的内核函数
↓
返回结果
↓
切换回用户模式
↓
用户程序继续执行
x86-64 系统调用实现
调用约定:
// 系统调用号存放在 rax 寄存器
// 参数依次存放在:rdi, rsi, rdx, r10, r8, r9
// 返回值存放在 rax
// 示例:sys_write(fd, buf, count)
mov rax, 1 ; 系统调用号 1 (write)
mov rdi, 1 ; 参数1: fd = 1 (stdout)
mov rsi, buffer ; 参数2: buf 地址
mov rdx, length ; 参数3: count
syscall ; 执行系统调用
内核入口点:
// arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
// 保存用户态寄存器
swapgs
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
// 保存所有寄存器到栈
pushq %rax
pushq %rdi
pushq %rsi
// ...
// 调用 C 函数
call do_syscall_64
// 恢复寄存器
popq %rsi
popq %rdi
popq %rax
// ...
// 返回用户空间
swapgs
sysretq
END(entry_SYSCALL_64)
系统调用表
// arch/x86/entry/syscalls/syscall_64.tbl
0 common read sys_read
1 common write sys_write
2 common open sys_open
3 common close sys_close
4 common stat sys_newstat
5 common fstat sys_newfstat
...
查看系统调用号:
# 查看系统调用定义
cat /usr/include/x86_64-linux-gnu/asm/unistd_64.h | grep __NR
# 或使用 ausyscall
ausyscall --dump
常用系统调用
文件操作
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
int main() {
// open - 打开文件
int fd = open("/tmp/test.txt", O_RDWR | O_CREAT, 0644);
if (fd == -1) {
perror("open");
return 1;
}
// write - 写入数据
const char *data = "Hello, Linux!\n";
ssize_t written = write(fd, data, 14);
printf("Written %zd bytes\n", written);
// lseek - 移动文件指针
off_t pos = lseek(fd, 0, SEEK_SET);
// read - 读取数据
char buffer[100];
ssize_t nread = read(fd, buffer, sizeof(buffer));
buffer[nread] = '\0';
printf("Read: %s", buffer);
// close - 关闭文件
close(fd);
return 0;
}
进程管理
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main() {
pid_t pid = fork(); // 创建子进程
if (pid == -1) {
perror("fork");
return 1;
} else if (pid == 0) {
// 子进程
printf("Child PID: %d\n", getpid());
// execve - 执行新程序
char *args[] = {"/bin/ls", "-l", NULL};
execve("/bin/ls", args, NULL);
// 如果 execve 失败才会执行到这里
perror("execve");
return 1;
} else {
// 父进程
printf("Parent PID: %d, Child PID: %d\n", getpid(), pid);
// wait - 等待子进程结束
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("Child exited with status %d\n", WEXITSTATUS(status));
}
}
return 0;
}
内存管理
#include <sys/mman.h>
#include <stdio.h>
#include <string.h>
int main() {
// mmap - 内存映射
size_t size = 4096;
void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (addr == MAP_FAILED) {
perror("mmap");
return 1;
}
// 使用映射的内存
strcpy((char *)addr, "Memory mapped data");
printf("Data: %s\n", (char *)addr);
// munmap - 解除映射
munmap(addr, size);
return 0;
}
直接系统调用
使用 syscall() 函数
#include <unistd.h>
#include <sys/syscall.h>
#include <stdio.h>
int main() {
// 方式1:使用库函数
pid_t pid1 = getpid();
// 方式2:直接系统调用
pid_t pid2 = syscall(SYS_getpid);
printf("PID (library): %d\n", pid1);
printf("PID (syscall): %d\n", pid2);
// 自定义系统调用
long ret = syscall(SYS_write, 1, "Hello\n", 6);
return 0;
}
内联汇编调用
#include <stdio.h>
ssize_t my_write(int fd, const void *buf, size_t count) {
ssize_t ret;
__asm__ volatile (
"syscall"
: "=a" (ret) // 输出:rax
: "0" (1), // rax = 1 (sys_write)
"D" (fd), // rdi = fd
"S" (buf), // rsi = buf
"d" (count) // rdx = count
: "rcx", "r11", "memory" // 破坏的寄存器
);
return ret;
}
int main() {
my_write(1, "Direct syscall!\n", 16);
return 0;
}
系统调用性能
性能开销
开销来源:
- 上下文切换(用户态 → 内核态)
- 寄存器保存和恢复
- 参数验证和复制
- TLB 刷新
测量性能:
#include <time.h>
#include <unistd.h>
#include <stdio.h>
#define ITERATIONS 1000000
int main() {
struct timespec start, end;
// 测量 getpid() 性能
clock_gettime(CLOCK_MONOTONIC, &start);
for (int i = 0; i < ITERATIONS; i++) {
getpid();
}
clock_gettime(CLOCK_MONOTONIC, &end);
long ns = (end.tv_sec - start.tv_sec) * 1000000000L +
(end.tv_nsec - start.tv_nsec);
printf("Average syscall time: %ld ns\n", ns / ITERATIONS);
return 0;
}
vDSO(Virtual Dynamic Shared Object)
优化机制:
- 某些系统调用在用户空间执行
- 避免内核态切换
- 提高性能
vDSO 提供的函数:
// 无需陷入内核的系统调用
gettimeofday()
time()
clock_gettime()
getcpu()
查看 vDSO:
# 查看进程的 vDSO 映射
cat /proc/self/maps | grep vdso
# 示例输出
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0 [vdso]
跟踪系统调用
strace 工具
# 跟踪程序的所有系统调用
strace ls
# 只显示特定系统调用
strace -e open,read,write ls
# 统计系统调用
strace -c ls
# 跟踪正在运行的进程
strace -p <PID>
# 跟踪子进程
strace -f ./program
# 保存到文件
strace -o trace.log ls
输出示例:
execve("/bin/ls", ["ls"], 0x7ffd...) = 0
brk(NULL) = 0x55f0...
access("/etc/ld.so.preload", R_OK) = -1 ENOENT
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=83090, ...}) = 0
ltrace 工具
# 跟踪库函数调用
ltrace ls
# 只跟踪特定函数
ltrace -e malloc,free ls
实践:自定义系统调用监控
// syscall_monitor.c
#define _GNU_SOURCE
#include <dlfcn.h>
#include <stdio.h>
#include <unistd.h>
// 保存原始 write 函数指针
static ssize_t (*original_write)(int, const void *, size_t) = NULL;
// 初始化函数
__attribute__((constructor))
static void init(void) {
original_write = dlsym(RTLD_NEXT, "write");
}
// 拦截 write 系统调用
ssize_t write(int fd, const void *buf, size_t count) {
fprintf(stderr, "[MONITOR] write(%d, %p, %zu)\n", fd, buf, count);
return original_write(fd, buf, count);
}
编译和使用:
# 编译为共享库
gcc -shared -fPIC syscall_monitor.c -o libmonitor.so -ldl
# 使用 LD_PRELOAD 加载
LD_PRELOAD=./libmonitor.so ls
系统调用安全
seccomp(Secure Computing Mode)
限制系统调用:
#include <seccomp.h>
#include <stdio.h>
int main() {
// 创建过滤器
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_KILL);
// 允许特定系统调用
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
// 加载过滤器
seccomp_load(ctx);
// 现在只能使用允许的系统调用
printf("This will work\n");
// 尝试使用被禁止的系统调用(会被杀死)
// getpid(); // 这会导致进程终止
return 0;
}
编译:
gcc -o seccomp_demo seccomp_demo.c -lseccomp
下一步:学习 内核空间与用户空间 章节。