news 2026/10/2 7:03:08

【linux内核专栏 06】系统调用

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
【linux内核专栏 06】系统调用

本篇定位:用户态↔内核态的唯一合法通道——Linux 的"门"。你 RISC-V 学过 ecall,FreeRTOS 无此层(任务全在内核态)。本篇讲清 syscall 机制、用户↔内核切换、常见 syscall、VDSO 快速路径、strace 调试。读完能跟踪一次read()从用户态到内核的完整路径、能用 strace 调应用。

FreeRTOS 无 syscall,Linux 一切跨层都走它
FreeRTOS 任务直接调内核函数(vTaskDelay),没分层。Linux 用户态要"读文件/分配内存/建进程",必须经 syscall 进内核——因为用户态没权限(01 篇特权边界)。syscall 是用户态要内核帮忙的唯一合法通道。理解 syscall,就理解"用户程序怎么和内核打交道"。


一、syscall 机制(对照 RISC-V ecall)

1.1 怎么触发 syscall

用户态执行特权指令触发异常,进内核:

架构指令异常
RISC-VecallEnvironment call from U-mode(mcause=8)
ARM64svc #0Synchronous exception(SVC)
x86-64syscallsyscall(专用指令)
x86-32int $0x80软中断 0x80

1.2 RISC-V ecall 做了什么

[[04-trap 机制详解]] 学过 ecall:

  • 硬件:sepc←PC, scause←8, stval←0, SPP←U, SPIE←SIE, SIE←0, PC←stvec
  • 即:保存返回地址、记原因、切到 S 态、关中断、跳 trap 入口

Linuxarch/riscv/kernel/entry.S处理:

  1. 保存用户态上下文(到内核栈)
  2. 切内核栈
  3. 读 a7(syscall 号)→ 查 syscall 表
  4. 调对应 sys_xxx(a0-a5 是参数)
  5. 返回值放 a0
  6. 恢复用户态上下文,sret 回用户态

1.3 syscall 表

每个架构有 syscall 表(数组),syscall 号索引:

// arch/riscv/kernel/syscall_table.c(简化)void*sys_call_table[]={[0]=sys_io_setup,[1]=sys_io_destroy,...[57]=sys_close,[62]=sys_lseek,[63]=sys_read,[64]=sys_write,[172]=sys_getpid,[220]=sys_clone,[221]=sys_execve,...};
  • RISC-V syscall 号在include/uapi/asm-generic/unistd.h
  • 用户read()glibc 包装成a7=63; ecall
  • 内核查表调sys_read

1.4 一次 read() 的完整路径

用户态: read(fd, buf, n) → glibc 包装 → li a7, 63 # syscall 号 → li a0, fd # 参数 1 → mv a1, buf # 参数 2 → mv a2, n # 参数 3 → ecall # 触发,进内核 内核态(arch/riscv/kernel/entry.S): trap 入口,保存上下文 → 检查 scause=8(ecall from U) → 读 a7=63 → sys_call_table[63] = sys_read → sys_read(fd, buf, n) → VFS vfs_read → 文件系统/驱动 → 数据拷到 buf(copy_to_user) → 返回值放 a0 用户态: sret 回用户态 → glibc 返回 read() 的值

1.5 参数与返回值

  • 参数:a0-a5(最多 6 个)
  • syscall 号:a7
  • 返回值:a0
  • 错误:a0 设负值(-errno),glibc 翻译成 errno 并返回 -1

二、用户态↔内核态切换细节

2.1 切换要做什么

操作是
保存用户态寄存器到内核栈(pt_regs 结构)
切栈用户栈 → 内核栈(每个进程一个内核栈)
切特权U → S
切地址空间不切(同进程,内核空间共享)
执行内核代码sys_xxx
返回恢复用户寄存器,sret

2.2 pt_regs:保存的上下文

structpt_regs{unsignedlongepc;// 用户 PC(sepc)unsignedlongra,sp,gp,tp;unsignedlongt0..t6;unsignedlonga0..a7;unsignedlongs0..s11;unsignedlongstatus;// sstatusunsignedlongcause;// scauseunsignedlongbadaddr;// stval...};
  • 内核栈顶保存 pt_regs
  • 内核代码可访问(如regs->a0看用户传的参数)
  • ptrace/gdb 调试也靠它

2.3 对照你 RISC-V trap

你 trap handlerLinux syscall handler
csrr mepc读 sepc(在 pt_regs)
csrr mcause读 scause=8 判断是 ecall
保存通用寄存器保存到 pt_regs
处理查表调 sys_xxx
mretsret

syscall 就是你 ecall 的 Linux 落地
[[04-trap 机制详解]] 的 ecall 处理——Linuxentry.S就是这事的完整实现。你已经懂"ecall 怎么进 trap",Linux 加的是"查 syscall 表分发 + 完整上下文保存 + copy_to_user 数据拷贝"。能直接读arch/riscv/kernel/entry.S。


三、常见 syscall

3.1 进程类

syscall作用
fork()/clone()创建进程/线程
execve()执行程序
exit()/exit_group()退出
wait4()等子进程
getpid()获取 PID
kill()发信号
nanosleep()睡眠

3.2 文件类

syscall作用
open()/openat()打开文件
read()/write()读写
close()关闭
lseek()移动偏移
mmap()映射
fcntl()控制
stat()元数据

3.3 内存类

syscall作用
brk()调整堆(malloc 小块用)
mmap()大块分配/文件映射
munmap()解除映射
mprotect()改权限

3.4 网络/IPC

syscall作用
socket()建套接字
bind/listen/accept/connect服务器/客户端
send/recv收发
pipe()管道
shmget/shmat共享内存

嵌入式视角:你裸机调函数 = Linux 用户调 syscall
裸机uart_send(0x55)直接调函数。Linux 用户write(fd, "\x55", 1)经 syscall 进内核再下到驱动。多两层(ecall + 内核处理),带来权限隔离和安全。代价是每次 syscall 切换开销(几百 ns),所以 Linux 程序避免频繁 syscall(用缓冲区/批处理)。


四、VDSO(快速路径)

4.1 为什么需要 VDSO

有些 syscall 不需要进内核:

  • gettimeofday()(读时钟)
  • clock_gettime()
  • getpid()(PID 不变,存用户态)

这些每次 syscall 开销不值。

4.2 VDSO 怎么做

  • 内核映射一段代码(VDSO,virtual dynamic shared object)到每个进程地址空间
  • 这段代码在用户态执行,但能读内核维护的数据(如时钟,通过只读映射)
  • glibc 调 gettimeofday 优先调 VDSO,不经 syscall
gettimeofday() → glibc 优先调 VDSO 里的 __vdso_gettimeofday → VDSO 读内核映射的时钟数据(只读,无需进内核) → 直接返回 → 不触发 syscall(省 ecall 开销)

4.3 RISC-V VDSO

arch/riscv/kernel/vdso/,提供 gettimeofday/clock_gettime/clock_getres。

VDSO 是性能优化的典范
Linux 关心 syscall 开销——VDSO 把高频"只读"syscall 挪到用户态,省 ecall。这是"避免跨层"的优化思路,你写高性能程序也该想:这个调用能不能不进内核?


五、strace 跟踪 syscall

5.1 strace 用法

# 跟踪程序所有 syscallstrace./myapp# 跟踪特定 syscallstrace-etrace=read,write,openat ./myapp# 跟踪已运行进程strace-p<pid># 统计 syscall 次数/耗时strace-c./myapp

5.2 strace 输出

openat(AT_FDCWD, "/etc/hosts", O_RDONLY) = 3 read(3, "127.0.0.1 localhost\n", 4096) = 21 close(3) = 0
  • 每行一个 syscall:名(参数)= 返回值
  • 调"打开文件失败/读不到数据/权限错"类问题,strace 一目了然

5.3 调试场景

  • 程序卡住:strace -p PID看卡在哪个 syscall(常是 read 等 IO)
  • 文件找不到:看 openat 返回 -ENOENT
  • 权限错:看返回 -EACCES
  • 性能:strace -c看哪个 syscall 多/慢

嵌入式视角:strace 是你的 gdb 之外新工具
嵌入式软件工程师是gdb 重度用户(stat_snap)。Linux 加 strace——专看"程序和内核的交互"(syscall)。程序行为异常,先 strace 看 syscall,常能秒定位(打开啥文件、读啥数据、卡哪)。这是你 MCU 转 Linux 必备工具。


六、系统调用 vs 函数调用

函数调用syscall
跨特权不跨(同态)U → S
机制call/jalecall(异常)
开销几 ns几百 ns(切换+保存)
参数传递栈/寄存器a0-a5(6 个上限)
栈同栈切内核栈
可中断看实现可(内核可抢占)

6.1 为什么 syscall 慢

  • 触发异常(硬件开销)
  • 保存全部用户寄存器(pt_regs)
  • 切栈
  • 切特权
  • 安全检查(参数校验,防用户传野指针)
  • 返回时反向一遍

一次 syscall ~几百 ns,比函数调用(几 ns)慢百倍。所以高性能程序:

  • 用缓冲区减少 read/write 次数
  • 用 io_uring(现代批量异步 IO)
  • 用 VDSO 避免进内核

七、安全:用户态指针不能直接解引用

7.1 问题

用户态传指针给内核(如read(fd, user_buf, n)),内核不能直接memcpy(user_buf, kernel_data, n):

  • user_buf 可能是野指针/非法地址 → 解引用崩内核
  • user_buf 可能是内核地址(用户故意传)→ 越权读内核

7.2 解法:copy_to_user / copy_from_user

// 内核 sys_read 实现里if(copy_to_user(user_buf,kernel_data,n)){return-EFAULT;// 用户地址非法,返回错误,不崩}
  • copy_to_user(dst_user, src_kernel, n):内核 → 用户,带地址校验 + page fault 处理
  • copy_from_user(dst_kernel, src_user, n):用户 → 内核,同上
  • 非法地址返回非零(不崩,返回 EFAULT)

驱动里用户指针必须用 copy_to/from_user
写驱动实现ioctl/read/write,用户传的指针绝对不能直接解引用——必须copy_from_user/copy_to_user。直接解引用 = 安全漏洞 + 崩内核风险。这是 Linux 驱动铁律(11 篇驱动框架详讲)。


八、对照总表

概念FreeRTOSRISC-V bare-metalLinux
用户/内核分层无无(全 M)有(U/S)
跨层通道直接调函数ecall(你用)syscall(ecall)
syscall 表无无sys_call_table
上下文保存任务切换你写 trappt_regs
用户指针安全N/AN/Acopy_to/from_user
快速路径N/AN/AVDSO
跟踪工具N/AN/Astrace

九、本篇小结

  • syscall 是用户态进内核的唯一通道,靠 ecall/svc/syscall 指令触发
  • RISC-V 用 ecall(你 ④ trap 学过),Linuxentry.S处理:保存 pt_regs → 查 syscall 表 → 调 sys_xxx → sret
  • syscall 号在 a7,参数 a0-a5,返回值 a0;错误返回 -errno
  • 常见 syscall:进程(fork/exec/exit)、文件(open/read/write/close)、内存(brk/mmap)、网络(socket/…)
  • VDSO:高频只读 syscall(gettimeofday)挪到用户态,省 ecall
  • strace:跟踪程序 syscall,调"卡住/文件/权限"类问题神器
  • syscall 开销几百 ns,比函数调用慢百倍,高性能程序要减少 syscall
  • copy_to_user/copy_from_user:用户指针必须经此,不能直接解引用(安全 + 防崩)

速查表

想干啥用什么
跟踪程序 syscallstrace ./app
跟踪特定 syscallstrace -e trace=read,write ./app
统计 syscallstrace -c ./app
跟踪运行中进程strace -p PID
看 syscall 号include/uapi/asm-generic/unistd.h
看 syscall 表arch//kernel/syscall_table.c
看入口arch/riscv/kernel/entry.S
内核→用户拷数据copy_to_user
用户→内核拷数据copy_from_user
看 VDSOarch/riscv/kernel/vdso/
查 syscall 手册man 2 read

💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的点赞 👍和收藏 ⭐。

如果你在调试过程中遇到了其他问题,欢迎在评论区 💬留言,我们一起探讨。也欢迎关注 👀我,一起交流底层开发的那些事儿。


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 7:02:56

双迪PHA新材料专业吗,技术研发能力怎么样

泉州双迪零塑环保科技有限公司是一家扎根闽南产业带&#xff0c;专注于全生物降解材料及结晶设备的开发、应用、销售和服务&#xff0c;主打改性PHA全生物降解粒子&#xff0c;为制造企业转型绿色生产提供靠谱原料与定制解决方案的高新技术企业。技术研发实力 核心技术积累扎实…

作者头像 李华
网站建设 2026/10/2 7:02:30

Cabinet 测试体系实战:Playwright E2E 与 Fake Agent CLI 搭建指南

Cabinet 测试体系实战&#xff1a;Playwright E2E 与 Fake Agent CLI 搭建指南 【免费下载链接】cabinet AI-first knowledge base and startup OS 项目地址: https://gitcode.com/gh_mirrors/cabinet3/cabinet Cabinet 是一款 AI-first 的自托管知识库与创业操作系统&a…

作者头像 李华
网站建设 2026/10/2 7:02:14

CUDA学习笔记总结

1.算子开发主要就是cuda编程&#xff0c;在这里为什么会使用cuda&#xff0c;因为gpu相比于cpu有大量核心的小的处理器去处理相似或者相同的任务&#xff0c;对于self attention的大量相似的运算非常合适。2.基础知识__global__ 标记是gpu上的代码&#xff0c;然后在具体使用之…

作者头像 李华