编程进阶网 编程进阶网
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 在线工具
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 加解密
  • 时间日期
  • 网络工具
  • 颜色设计
  • 二维码
  • 开发实用
  • 计算机的原理
  • 操作系统原理
  • 网络协议原理
  • 数据库的原理
  • 序卷导读
  • 数据本质
  • 运行模型
  • 并发设计
  • 内存真相
  • 交互系统
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 技能之旅
  • 体系建设
  • 代码品质
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 性能优化
  • 数据结构导论
  • 线性结构详解
  • 树哈希结构论
  • 容器设计实战
  • 经典算法思想
  • 工程案例剖析
  • 算法题库精练
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++编程技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript案例
  • JavaScript高级
  • Kotlin精通
  • Android库解读
  • Android专栏
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • 毛选解读
  • 自我精进
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • Android提升进阶

  • iOS开发和进阶

  • Web开发和进阶

  • Linux应用开发

    • Linux应用开发
    • QML基础入门

    • QT核心库实践

    • Linux系统编程

      • Linux系统编程基础
      • 系统调用原理精讲
        • 文件IO深度解析
        • 进程管理深度解析
        • 线程并发深度解读
        • 信号处理深度解读
        • 进程间通信IPC
        • 网络IO多路复用
        • 内存管理深度解读
        • 动态链接与共享库
        • Linux与定时器
      • 综合项目实战

    • IoT智能硬件开发

    • Apps
    • Linux应用开发
    • Linux系统编程
    杨充
    2025-07-02
    目录

    系统调用原理精讲

    # 01.系统调用原理精讲

    嵌入式 C++ 开发者的第一课——理解用户态与内核态之间的那道墙。系统调用是 Linux 应用层程序与硬件之间的唯一桥梁。

    # 目录介绍

    • 1.1 案例引入
      • 1.1.1 一段神秘代码
      • 1.1.2 为什么 fopen 比 open 慢
      • 1.1.3 核心问题
    • 1.2 用户态与内核态
      • 1.2.1 CPU 特权级别
      • 1.2.2 上下文切换的开销
      • 1.2.3 系统调用全过程
    • 1.3 POSIX 标准体系
      • 1.3.1 什么是 POSIX
      • 1.3.2 可移植性保证
      • 1.3.3 Linux 的 POSIX 合规度
    • 1.4 glibc 与系统调用封装
      • 1.4.1 C 库的职责
      • 1.4.2 缓冲 I/O 的陷阱
      • 1.4.3 直接 syscall 调用
    • 1.5 man 手册使用体系
      • 1.5.1 9 个 Section 速查
      • 1.5.2 查阅技巧
    • 1.6 嵌入式场景的特殊考量
      • 1.6.1 系统调用频率限制
      • 1.6.2 精简 glibc 与 musl/uclibc
      • 1.6.3 系统调用在 ARM 上的差异
    • 1.7 关键结论与速查表

    # 1.1 案例引入

    # 1.1.1 一段神秘代码

    某嵌入式项目在 ARM Linux 上使用 Qt 开发仪表盘,核心功能在 PC 调试正常,烧录到设备后出现随机卡顿。开发者排查三天后发现问题出在一条路径上:

    // 这段代码在 60fps 渲染循环中每帧调用
    void DashboardRenderer::readSensorData() {
        for (int i = 0; i < 12; i++) {
            // 直接 syscall:每次 read 都触发用户态→内核态切换
            int ret = read(sensor_fds[i], buf, 16);  // ← 罪魁祸首
        }
    }
    

    12 次独立 read = 24 次上下文切换/帧 = 1440 次/秒。在 1GHz ARM Cortex-A7 上,每次切换约 2μs。看似不多,但当仪表盘渲染本身需要 14ms/帧时,额外的 29μs × 24 = 0.7ms 就让帧预算从 16.6ms 撞到天花板。

    修复方案——一次 readv 代替 12 次 read:

    void DashboardRenderer::readSensorData() {
        struct iovec iov[12];
        for (int i = 0; i < 12; i++) {
            iov[i].iov_base = sensor_bufs[i];
            iov[i].iov_len  = 16;
        }
        // 向量 I/O:一次系统调用读完所有 12 个传感器
        ssize_t total = readv(sensor_master_fd, iov, 12);
        if (total < 0) report_error();
    }
    

    效果:上下文切换从 24 次/帧降为 2 次/帧——12 倍改善。帧渲染从 14.7ms 落回 14.0ms,设备恢复流畅。

    教训:系统调用的代价不是函数调用——它是CPU 特权级别的切换。每一次 read 都是一次跨越"用户态→内核态→用户态"的完整旅程。

    # 1.1.2 为什么 fopen 比 open 慢

    fopen 比 open 多了一层 C 库的缓冲层(stdio buffer),对于单次调用确实多了一次 memcpy:

    fopen 路径:  fopen() → 分配 FILE 结构体 → 分配缓冲区(默认 8KB)
    write 路径:   用户 buf → fwrite() → stdio 缓冲区 → (满了才) write()
    read 路径:    read() → 内核空间 → stdio 缓冲区 → fread() 拷贝给用户
    

    但它把 12 次小 read 合并成 1-2 次大 read——用一次 memcpy 换 10 次上下文切换,血赚。

    实测对比(ARM Cortex-A7,读 12 × 16 字节):

    方案 系统调用次数 上下文切换次数 耗时
    12 次 read() 12 24 ~28μs
    fread()(8KB 缓冲) 1 2 ~5μs
    readv() 向量 I/O 1 2 ~4μs
    pread() 定位读 1 2 ~4μs

    结论:任何内核交互都有税。高手不是不交税,而是用合并 / 向量 / 缓冲让一次系统调用做更多事。

    # 1.1.3 核心问题

    1. 用户态和内核态之间到底发生了什么?
    2. POSIX 标准是如何让同一份代码在 Linux / macOS / QNX 上都能编译的?
    3. 如何通过 glibc 或直接 syscall 发起系统调用?两者差在哪?
    4. 嵌入式场景下,如何最小化系统调用的性能代价?

    # 1.2 用户态与内核态

    # 1.2.1 CPU 特权级别

    x86 架构定义了 Ring 0 ~ Ring 3 四级特权:

    Ring 3 ──── 用户程序 ──── 最低权限 ──── 不能执行特权指令
    Ring 2  ┌─ 设备驱动(很少用)
    Ring 1  ├─ 设备驱动(很少用)
    Ring 0  └─ 内核        ──── 最高权限 ──── 一切皆可做
    

    实际只用 Ring 0 和 Ring 3 两级——Ring 1/2 几乎从未被主流 OS 使用。

    ARM 架构用 Exception Level(EL0 ~ EL3)表达同样的概念:

    ARM EL x86 对应 用途 典型代码
    EL0 Ring 3 用户应用 Qt app, Python script
    EL1 Ring 0 OS 内核 Linux kernel
    EL2 - Hypervisor KVM, Xen
    EL3 - 安全固件 ARM Trusted Firmware

    关键差异:在不同级别可执行的指令集不同。用户态不能执行:

    • cli / sti(关/开中断)
    • lgdt / lidt(加载页表寄存器)
    • in / out(直接端口 I/O)
    • hlt(停机)

    想执行以上任何操作,只能通过系统调用请求内核代办。

    # 1.2.2 上下文切换的开销

    从用户态切到内核态(再切回来),CPU 做了哪些事?

    完整 8 步:

    1. 用户态执行 int 0x80 / syscall / svc 指令   ← 触发软中断 / 异常
    2. CPU 从 MSR / SPSR 寄存器读目标特权级
    3. 保存当前上下文(PC、SP、通用寄存器)到内核栈
    4. 切换到内核栈(加载内核 SP)
    5. 设置页表为内核页表(允许访问全部物理内存)
    6. 切换到 Ring 0 / EL1
    7. 跳转到系统调用入口(entry_SYSCALL_64 / vector_swi)
    8. 根据系统调用号(rax / r7)分发到具体处理函数
    

    返回路径基本对称地恢复上下文、切换页表、降低特权级。

    开销量化(典型值):

    操作 耗时(x86_64) 耗时(ARM Cortex-A7)
    普通函数调用 ~2 ns ~5 ns
    虚函数调用 ~5 ns ~12 ns
    系统调用(空调用) ~100 ns ~2 μs
    系统调用(含 I/O) 取决于设备 取决于设备

    系统调用比普通函数调用慢 50~400 倍。原因不是"内核代码写得太慢",而是特权级切换本身要保存/恢复大量 CPU 状态 + 刷新 TLB + 可能的缓存失效。

    TLB 刷新是最大开销:x86 上切换 CR3(页表基址寄存器)会让 TLB 中所有用户态映射失效——后续访问要重新走页表,每次 4 级页表查询 = 4 次内存访问(几十 ns)。

    # 1.2.3 系统调用全过程

    以 write(1, "hello", 5) 为例,全程追踪:

    用户态视角(C 代码):

    ssize_t write(int fd, const void *buf, size_t count);
    

    glibc 封装层(glibc/sysdeps/unix/sysv/linux/write.c 简化版):

    ssize_t __libc_write(int fd, const void *buf, size_t count) {
        return SYSCALL_CANCEL(write, fd, buf, count);
        // 展开为:
        // long result = INTERNAL_SYSCALL(write, 3, fd, buf, count);
        // if (result == -EINTR) ... 处理信号中断 ...
        // return result;
    }
    

    汇编层(x86_64 上 syscall 指令路径):

    movq $1, %rax          ; 系统调用号:__NR_write = 1
    movq %fd, %rdi          ; 第 1 参数
    movq %buf, %rsi         ; 第 2 参数
    movq %count, %rdx       ; 第 3 参数
    syscall                 ; ← 跨越用户态/内核态的唯一指令
    ; 返回后 rax = 返回值
    cmpq $-4096, %rax       ; 检查是否错误(-1 ~ -4095)
    ja   error_handler
    

    ARM 上等价的 svc #0 指令:

    mov r7, #4              ; 系统调用号:__NR_write = 4 (ARM)
    mov r0, %fd             ; 第 1 参数
    mov r1, %buf            ; 第 2 参数
    mov r2, %count          ; 第 3 参数
    svc #0                  ; ← 陷入内核
    ; 返回后 r0 = 返回值
    

    内核态处理(fs/read_write.c):

    SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count) {
        struct fd f = fdget_pos(fd);
        if (!f.file) return -EBADF;
        
        // 权限检查:文件是否可写
        if (!(f.file->f_mode & FMODE_WRITE)) return -EBADF;
        
        // 用户空间指针合法性检查(不能是内核地址)
        if (!access_ok(buf, count)) return -EFAULT;
        
        // 实际写入 —— 可能经过 VFS → ext4 → block layer → 磁盘驱动
        ssize_t ret = vfs_write(f.file, buf, count, &f.file->f_pos);
        
        return ret;
    }
    

    完整时序图:

    用户程序                 glibc                   内核
      │                       │                       │
      │ write(fd,buf,5)       │                       │
      ├──────────────────────→│                       │
      │                       │ syscall(SYS_write)    │
      │                       ├──────────────────────→│
      │                       │                       │ ① 保存上下文到内核栈
      │                       │                       │ ② 根据 fd 查文件表
      │                       │                       │ ③ 拷贝数据到内核缓冲区
      │                       │                       │ ④ 调度 I/O
      │                       │              返回值 ←─┤ ⑤ 恢复上下文
      │                       │←──────────────────────┤
      │           返回值      │                       │
      │←──────────────────────┤                       │
      │                       │                       │
    

    五个关键点:

    1. 参数传在寄存器——x86_64 用 rdi,rsi,rdx,r10,r8,r9 六个寄存器(不用栈,更快);ARM 用 r0-r6
    2. 系统调用号——x86_64 放 rax,ARM 放 r7(旧用 swi 立即数)
    3. 返回值——x86_64 放 rax,ARM 放 r0;负值表示错误(-errno),glibc 负责转成 -1 + 设 errno
    4. 用户态指针要校验——access_ok() 确保 buf 不在内核空间
    5. 可被信号中断——I/O 类系统调用可能被 EINTR 中断,要重试

    # 1.3 POSIX 标准体系

    # 1.3.1 什么是 POSIX

    POSIX(Portable Operating System Interface)是 IEEE 定义的一套操作系统接口标准,目的是让同一份源码在不同 Unix 系统上编译运行。

    历史脉络:

    1984   AT&T 分拆 → Unix 碎片化(SunOS、HP-UX、AIX、Xenix…)
    1988   IEEE 1003.1(POSIX.1)发布——统一系统调用接口
    1990s  POSIX 被 ISO 采纳为国际标准(ISO/IEC 9945)
    2001   单一 Unix 规范(SUSv3)+ POSIX 合并
    2008   POSIX:2008(现行主力标准)
    

    POSIX 标准包含的内容:

    组件 内容 例子
    系统调用接口 进程、文件、信号、IPC open() / fork() / kill() / sem_wait()
    C 库函数 标准 I/O、字符串、数学 printf() / strlen() / sin()
    Shell & 工具 命令行工具行为 ls / grep / awk / make
    线程扩展 pthread pthread_create() / pthread_mutex_lock()

    POSIX 不是操作系统——它是一份接口说明书。Linux、macOS、QNX、FreeBSD 各自用不同内核实现同一套接口。

    # 1.3.2 可移植性保证

    写一次,到处编译是 POSIX 的核心承诺:

    #include <unistd.h>
    #include <fcntl.h>
    
    int main() {
        int fd = open("/tmp/test.txt", O_WRONLY | O_CREAT, 0644);
        write(fd, "hello\n", 6);
        close(fd);
        return 0;
    }
    

    以上代码可以在:

    • Linux(内核 syscall 实现)
    • macOS(XNU 内核,基于 Mach + BSD)
    • QNX(微内核 Neutrino)
    • FreeBSD

    上无需修改编译运行。因为 open / write / close 的签名、语义、返回值都被 POSIX 规定了。

    但 POSIX 不保证的东西:

    方面 POSIX 是否规定 例子
    函数签名与行为 ✅ 是 int open(const char*, int, ...)
    返回值与 errno ✅ 是 失败返回 -1,errno = EACCES
    性能特征 ❌ 否 Linux fork() 5μs,QNX fork() 50μs
    文件系统路径 ❌ 否 /proc、/sys 非 POSIX
    扩展功能 ❌ 否 epoll(Linux 专有)、kqueue(BSD 专有)
    实时性保证 部分 POSIX 实时扩展定义了优先级调度、信号量超时

    可移植性编码的三条铁律:

    1. 头文件用 POSIX 规定的(<unistd.h>,不是 <sys/unistd.h>)
    2. 编译时定义 _POSIX_C_SOURCE 来排除平台特有扩展
    3. 性能敏感的 I/O 路径做好平台抽象(用接口层屏蔽 epoll vs kqueue)

    # 1.3.3 Linux 的 POSIX 合规度

    Linux 的立场是**"大部分兼容,不追求完整认证"**(POSIX 认证费用高达数十万美元)。

    主流 Linux 发行版的合规情况:

    系统 认证 说明
    macOS(自 10.5) ✅ UNIX 03 认证 最严格
    AIX / HP-UX ✅ UNIX 认证 商业 Unix
    RHEL / SUSE ❌ 未认证 但实际兼容度极高
    Debian / Ubuntu ❌ 未认证 重度依赖 GNU 扩展
    QNX ✅ POSIX 认证 嵌入式 RTOS

    Linux 与 POSIX 的典型偏差:

    // POSIX 定义:d_type 不是标准字段(只有 d_name 保证)
    struct dirent {
        ino_t          d_ino;
        char           d_name[];
        unsigned char  d_type;   // ← Linux 扩展!其他 Unix 可能没有
    };
    

    Linux 特有系统调用(不在 POSIX 中):

    系统调用 用途
    epoll_create / epoll_wait 高性能 I/O 多路复用
    signalfd 用文件描述符读信号
    timerfd 用文件描述符读定时器
    eventfd 线程间事件通知
    inotify_init 文件系统事件监控
    clone 创建线程/进程(极灵活)
    memfd_create 匿名内存文件

    选择原则:应用层只管用 POSIX 接口,框架/基础设施层可以为平台特有的性能特性做抽象适配(如用 epoll 在 Linux 上,用 kqueue 在 macOS 上)。


    # 1.4 glibc 与系统调用封装

    # 1.4.1 C 库的职责

    glibc(GNU C Library)不是内核的一部分,它运行在用户态,是内核 syscall 的"前端"。

    应用程序
      │
      ▼
    glibc(用户态)
      ├── printf / fopen / malloc          ← 高级功能(含自己的缓冲区)
      ├── write / open / sbrk              ← 薄封装(几乎直接 mirrors syscall)
      │
      ▼
    系统调用(内核态)
      └── sys_write / sys_open / sys_brk
    

    glibc 做三件事:

    职责 说明 例子
    封装 把 syscall 的寄存器传参包装成普通 C 函数 write(fd, buf, n)
    增强 在 syscall 之上加缓冲、线程安全、locale printf 格式化 + 缓冲
    兼容 抹平不同内核版本的差异 内核 4.x 和 6.x 的 openat2
    可移植 同一份 glibc 代码适配 x86_64 / ARM / RISC-V 汇编层自动选择 syscall / svc

    glibc 有多大:

    版本 .so 大小 典型嵌入场景
    glibc 2.35 ~2 MB 桌面 / 服务器
    musl 1.2 ~800 KB 嵌入式 Linux
    uClibc-ng ~500 KB 极小嵌入式
    bare-metal 0 无 OS,直接写寄存器

    # 1.4.2 缓冲 I/O 的陷阱

    glibc 的 stdio 缓冲是双刃剑——省了系统调用,但引入了几大陷阱。

    缓冲类型:

    // 三种缓冲模式:
    setvbuf(fp, NULL, _IONBF, 0);   // 无缓冲——每次 write/fread 直接 syscall
    setvbuf(fp, NULL, _IOLBF, 1024);// 行缓冲——遇到 \n 刷新(stdout 默认)
    setvbuf(fp, NULL, _IOFBF, 8192);// 全缓冲——满了才刷新(文件默认 8KB)
    

    陷阱 1:fork 前未 fflush——数据双写

    FILE *fp = fopen("log.txt", "w");
    fprintf(fp, "before fork\n");     // 还在缓冲区!没写入磁盘
    pid_t pid = fork();
    // 父子进程各有一份缓冲区拷贝
    fclose(fp);                       // 两进程各 flush 一次 → "before fork" 写两次!
    

    陷阱 2:多线程不安全

    // stdout 的行缓冲在多线程下可能乱序
    Thread A: printf("AAA\n");     // 写缓冲区,遇到 \n 刷新
    Thread B: printf("BBB\n");     // 同时写缓冲区
    // 输出可能是:AAABBB\n\n 或 BB\nAAAB\n  —— 不确定
    

    陷阱 3:exit() 与 _exit() 区别

    printf("important log");          // 没加 \n,仍在缓冲区中
    // _exit(0);                       // ⚠ 直接退出,缓冲区丢弃——log 丢失!
    exit(0);                           // ✅ 会 fflush 所有 stdio 缓冲区再退出
    

    嵌入式最佳实践:

    • 日志用无缓冲 stderr 或 write() syscall 直写
    • 或每次关键输出后手动 fflush(fp)
    • 多线程日志用 flockfile() / funlockfile() 或专用日志库

    # 1.4.3 直接 syscall 调用

    两种方式绕过 glibc,直接触发系统调用:

    方式 1:syscall() 函数

    #include <unistd.h>
    #include <sys/syscall.h>
    
    // 直接调 gettid()——glibc 没有这个封装(要等到 2.30 才有)
    pid_t tid = syscall(SYS_gettid);
    
    // 直接调 memfd_create
    int fd = syscall(SYS_memfd_create, "shm", MFD_CLOEXEC);
    

    方式 2:内联汇编

    static inline long direct_write(int fd, const void *buf, size_t count) {
        long ret;
        asm volatile (
            "movq %[nr], %%rax\n\t"       // 系统调用号 (SYS_write = 1)
            "movq %[arg1], %%rdi\n\t"     // 第1参数
            "movq %[arg2], %%rsi\n\t"     // 第2参数
            "movq %[arg3], %%rdx\n\t"     // 第3参数
            "syscall\n\t"
            "movq %%rax, %[ret]\n\t"
            : [ret] "=r" (ret)
            : [nr] "i" (1), [arg1] "r" ((long)fd),
              [arg2] "r" ((const char*)buf), [arg3] "r" (count)
            : "rax", "rdi", "rsi", "rdx", "rcx", "r11", "memory"
        );
        return ret;
    }
    
    // 使用
    direct_write(1, "Hello via raw syscall!\n", 24);
    

    ARM 上等价写法:

    static inline long arm_direct_read(int fd, void *buf, size_t count) {
        long ret;
        register long r0 asm("r0") = fd;
        register long r1 asm("r1") = (long)buf;
        register long r2 asm("r2") = count;
        register long r7 asm("r7") = 3;   // SYS_read = 3 (ARM)
        
        asm volatile (
            "svc #0\n\t"
            "mov %[ret], r0\n\t"
            : [ret] "=r" (ret)
            : "r" (r0), "r" (r1), "r" (r2), "r" (r7)
            : "memory"
        );
        return ret;
    }
    

    什么时候需要直接 syscall?

    场景 原因
    新 syscall glibc 还没封装 如旧内核上的 pidfd_open
    需要精确控制 errno glibc 可能修改 errno 语义
    嵌入式极小系统(无 glibc) 只能自己写 syscall 包装
    沙箱 / seccomp 场景 绕过 glibc 做最小调用集
    性能极致优化 省去 glibc 的取消点检查和信号处理

    警告:直接 syscall 绕过 glibc,也绕过了它的线程安全机制(如 errno TLS 处理)、取消点(pthread_cancel)和信号中断重试。除非有明确理由,否则用 glibc 封装。


    # 1.5 man 手册使用体系

    # 1.5.1 9 个 Section 速查

    Linux man 手册有 9 个 Section(有些系统有额外编号):

    man 命令格式:   man [section] name
    
    man 1 ls          ← 查看 ls 命令文档(Section 1:用户命令)
    man 2 write       ← 查看 write 系统调用(Section 2:系统调用)
    man 3 printf      ← 查看 printf C 库函数(Section 3:库函数)
    
    Section 名称 内容 例子
    1 用户命令 可执行程序、shell 命令 man 1 ls, man 1 bash
    2 系统调用 内核提供的 syscall man 2 open, man 2 read
    3 库函数 C 库函数(非 syscall) man 3 printf, man 3 malloc
    4 特殊文件 /dev 下的设备文件 man 4 null, man 4 tty
    5 文件格式 配置文件格式 man 5 passwd, man 5 fstab
    6 游戏 屏幕保护等 man 6 sl(几乎用不到)
    7 杂项 概览、协议 man 7 socket, man 7 signal
    8 管理命令 root 才能用的命令 man 8 mount, man 8 iptables
    9 内核接口 内核内部 API man 9 printk(开发内核才用)

    快速区分 Section 2 vs 3:

    man write    → 默认找 Section 1(如果存在 /usr/bin/write)
    man 2 write  → 系统调用 write(内核接口)
    man 3 write  → 没有(因为 write 是 syscall 不是库函数)
    
    man printf   → 默认找 Section 1(shell 命令 printf)
    man 3 printf → C 库函数 printf
    

    在文档里如何看出它是 Section 2 还是 3?

    WRITE(2)                  ← 括号里是 section 号,这是系统调用
    PRINTF(3)                 ← C 库函数
    OPEN(2)                   ← 系统调用
    FOPEN(3)                  ← C 库函数(f 前缀提醒有缓冲)
    

    编程文档必查的三个 Section:

    • Section 2——系统调用,关注返回值、errno、错误列表
    • Section 3——库函数,关注缓冲区行为、线程安全性
    • Section 7——协议族概览(socket(7), tcp(7), ip(7))

    # 1.5.2 查阅技巧

    技巧 1:用 man -k(= apropos)关键字搜索

    # 想找和 "timer" 相关的所有 man 页面
    man -k timer | grep '(2)\|(3)'     # 只看系统调用和库函数
    
    # 输出示例:
    # timer_create (2)  - create a POSIX per-process timer
    # timerfd_create (2) - timers that notify via file descriptors
    # timeradd (3)      - operations on timeval structures
    

    技巧 2:定位特定 Section

    # 按 section 顺序查看所有匹配
    man -f open           # = whatis open,列出所有 section 中的 open
    # open (1)  - open files and directories using default applications
    # open (2)  - open and possibly create a file
    # open (3pm) - perl pragma to set default PerlIO layers
    

    技巧 3:读懂 errno 列表

    man 2 open | grep -A 30 ERRORS
    

    每个系统调用的 man 页面底部都有 "ERRORS" 小节,列举了该调用可能返回的所有 errno 值及其含义,这是写错误处理代码时的唯一权威参考。

    技巧 4:man 7 的概览页面是宝藏

    man 7 socket           # socket 编程总览
    man 7 signal           # 信号机制的完整说明
    man 7 capabilities     # Linux capability 权限模型的完整说明
    man 7 inotify          # inotify 文件监控机制的设计说明
    man 7 pipe             # 管道的容量、原子性等行为定义
    

    查找 errno 含义标准方法:

    #include <string.h>
    
    int fd = open("/nonexistent", O_RDONLY);
    if (fd == -1) {
        // 方法 1:perror —— 打印到 stderr,带自定义前缀
        perror("open failed");           // 输出:"open failed: No such file or directory"
        
        // 方法 2:strerror —— 拿到字符串用于日志
        fprintf(stderr, "Error: %s\n", strerror(errno));
        
        // 方法 3:%m —— glibc 扩展,printf 直接插入 errno 对应的文本
        fprintf(stderr, "open: %m\n");   // 输出:"open: No such file or directory"
    }
    

    # 1.6 嵌入式场景的特殊考量

    # 1.6.1 系统调用频率限制

    嵌入式设备(尤其是 1GHz 以下 ARM)上,系统调用的相对代价远高于服务器端 x86。

    实验数据(ARM Cortex-A7 @ 1GHz):

    操作                           延迟       相对普通函数调用的倍数
    ────────────────────────────────────────────────────────────
    空函数调用                      5ns        1x
    写 1 字节到内存                 10ns       2x
    getpid() syscall (空调用)       1.8μs      360x
    read 1 字节 (fd 已就绪)         4.5μs      900x
    read 4096 字节 (fd 已就绪)      6.0μs      1200x
    futex(FUTEX_WAIT) 无竞争       2.1μs      420x
    mmap 匿名 4KB 页                15μs       3000x
    

    在高频路径上的优化策略:

    策略 原理 典型收益
    向量 I/O(readv/writev) 一次 syscall 操作多个缓冲区 减少 N 倍 syscall
    内存映射 I/O(mmap) 初次 mmap 后,后续访问不走 syscall 频次降至 1/N
    io_uring(Linux 5.1+) 用共享环形缓冲区批量提交/完成 I/O 减少 70% 上下文切换
    sendfile / splice 内核内部数据搬运(零拷贝) 省去用户态中转
    缓冲聚合 攒够数据再 syscall 取决于缓冲大小
    pread/pwrite 定位 + 读写合并 省一次 lseek

    io_uring 简要介绍(Linux 5.1+ 的革命性接口):

    // 传统方式:每次 I/O 一次 syscall
    for (int i = 0; i < 1000; i++) {
        read(fds[i], bufs[i], 4096);     // 1000 次 syscall
    }
    
    // io_uring 方式:一次 syscall 提交 1000 个请求,循环收集结果
    io_uring_submit(&ring, 1000);        // 1 次 syscall
    while (completed < 1000) {
        io_uring_wait_cqe(&ring, &cqe);  // 按需等待完成
        process(cqe);
    }
    

    # 1.6.2 精简 glibc 与 musl/uclibc

    glibc 不是嵌入式设备的最优选择——2 MB 的 .so + 大量动态内存分配不适合资源受限场景。

    三大 C 库对比:

    特性 glibc musl uClibc-ng newlib
    目标 全功能、高性能 标准兼容、简洁 极小体积 裸机/RTOS
    libc.so 大小 ~2 MB ~800 KB ~500 KB 静态链接
    POSIX 兼容度 基本完整 接近完整 基本(少部分缺) 不追求
    线程安全 ✅ ✅ ✅ 视平台
    数学库精度 最高 标准 基本 基本
    分配器 ptmalloc2 mallocng dlmalloc 简单分配器
    DNS 解析 NSS 体系(重) 纯内置 简化版 无
    适用场景 桌面/服务器 容器/嵌入式 极小嵌入式 裸机程序

    musl 的亮点:

    • 静态链接友好(一个 libc.a 搞定)
    • 源码简洁(~60 万行 vs glibc ~300 万行)
    • Alpine Linux 的默认 C 库——Docker 镜像只有 5 MB 的关键原因

    嵌入式项目选择建议:

    你的设备存储空间 < 16 MB?  → uClibc-ng
    16 MB ~ 64 MB?              → musl(静态链)
    64 MB 以上?                 → musl 或 glibc(动态链)
    桌面/服务器?                → glibc
    

    # 1.6.3 系统调用在 ARM 上的差异

    ARM 与 x86 的系统调用机制对比:

    特性 x86_64 ARM (EABI) AArch64
    触发指令 syscall svc #0(旧用 swi) svc #0
    调用号寄存器 rax r7 x8
    参数寄存器 rdi,rsi,rdx,r10,r8,r9 r0-r5 x0-x5
    返回值寄存器 rax r0 x0
    错误表示 rax = -errno(-4095~-1) r0 = -errno x0 = -errno
    系统调用总数 ~330+ ~400+(历史累积) ~290+(精简)

    ARM 特有的三个坑:

    坑 1:ARM OABI vs EABI

    // 旧 ARM OABI(已废弃):
    swi 0x00900004      // 调用号嵌在 swi 指令里,参数在栈上
    
    // 现代 ARM EABI:
    mov r7, #4           // 调用号放 r7
    svc #0               // 参数在 r0-r5
    

    别在 2020 年后的项目中看到 swi 带立即数的代码——那是 ARM OABI,Linux 3.x 就开始丢弃了。

    坑 2:__NR_xxx 定义位置不同

    x86_64:   /usr/include/asm/unistd_64.h
    ARM:      /usr/include/asm/unistd.h       (注意没有 _32 后缀)
    AArch64:  /usr/include/asm-generic/unistd.h(不区分架构的新通用头)
    

    坑 3:内存屏障指令不同

    // x86:强内存模型,LOCK 前缀
    asm volatile ("lock; addl $0, (%%rsp)" ::: "memory");
    
    // ARM:弱内存模型,需要显式屏障
    asm volatile ("dmb ish" ::: "memory");   // Data Memory Barrier(Inner Shareable)
    

    如果你在 ARM 上用 futex 或写 lock-free 数据结构,忘记 dmb 是 99% 竞态 bug 的根因。


    # 1.7 关键结论与速查表

    核心结论五条:

    1. 系统调用不是函数调用——它跨越 CPU 特权级、切换页表、刷新 TLB,代价是函数调用的 50~400 倍
    2. 合并胜于频次——用向量 I/O、内存映射、io_uring 等机制让一次 syscall 做更多事,而不是让更多 syscall 做小事
    3. POSIX 保证可移植,不保证性能——可移植代码写 POSIX 接口,性能敏感路径按平台做抽象
    4. glibc 是增强层,不是透明层——printf 有缓冲、fork 前要 fflush、exit ≠ _exit
    5. 嵌入式优先考虑 musl 和向量 I/O——小 C 库 + 批量 syscall = 资源最优解

    常用系统调用速查表:

    功能 系统调用 常用场景
    文件打开/创建 open / creat 文件 I/O
    文件读写 read / write 基础 I/O
    文件定位 lseek / pread / pwrite 随机读写
    文件映射 mmap / munmap 零拷贝 I/O、共享内存
    向量 I/O readv / writev 批量操作多缓冲区
    文件信息 stat / fstat / lstat 文件元数据
    文件控制 fcntl / ioctl 非阻塞 I/O、文件锁
    目录操作 mkdir / opendir / readdir 目录遍历
    进程创建 fork / vfork / clone 多进程
    程序执行 execve 运行新程序
    进程等待 waitpid / wait4 回收子进程
    信号 sigaction / kill / sigprocmask 异步通知
    管道 pipe / pipe2 进程间通信
    内存 brk / sbrk / mmap 内存分配
    时间 clock_gettime / nanosleep 计时与睡眠
    网络 socket / bind / connect / accept 网络通信
    epoll epoll_create / epoll_ctl / epoll_wait 高性能 I/O 多路复用
    事件 eventfd / timerfd / signalfd 用 fd 统一事件循环
    线程 clone(底层)/ futex(同步) pthread 的底层基础

    系统调用 vs C 库函数区分口诀:

    Section 2 是 syscall  —— man 2 xxx,括号里 (2)
    加 f 前缀是 C 库      —— fopen / fwrite / fprintf
    带缓冲的是库函数       —— printf / scanf
    无缓冲是系统调用       —— read / write / close
    

    下一步:把 io_uring 的官方示例跑起来,用 perf stat -e cycles:u,cycles:k 测量一次批量 I/O 的用户态/内核态周期比例——亲手验证"少做 syscall = 更高的用户态占比"。


    延伸阅读:

    • man 2 syscalls —— Linux 所有系统调用完整列表
    • man 7 vdso —— 虚拟动态共享对象,让部分 syscall 不离开用户态
    • The Linux Kernel Documentation / syscalls (opens new window)
    • 《The Linux Programming Interface》(Michael Kerrisk) —— 第 3 章
    上次更新: 2026/07/12, 19:39:51
    Linux系统编程基础
    文件IO深度解析

    ← Linux系统编程基础 文件IO深度解析→

    最近更新
    01
    audit
    07-27
    02
    C++入门教程全章思考题汇编
    07-24
    03
    12.技术团队建设能力
    07-21
    更多文章>
    Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 鄂ICP备2024073355号-1 | 鄂ICP备2024073355号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式