第 3 章 系统编程概念 (System Programming Concepts)

      +

      核心结论

      • 系统调用机制:系统调用是「用户态→内核态」受控入口;通过 glibc 包装函数触发;CPU 状态从用户态切换到内核态;返回时把结果(含可能的 errno)传回用户态。

      • 系统调用 vs 库函数:系统调用是内核服务入口(如 readwriteopen);库函数是 C 标准库提供的函数(部分基于系统调用,如 fopen 基于 open,部分不基于,如 strcpy)。

      • 错误处理约定:系统调用失败时返回 -1 并设置全局 errno;成功调用不重置 errno;检查错误必须「先看返回值,再看 errno」。

      • errno 与诊断函数<errno.h> 定义错误常量(E 开头的常量);perror()strerror() 把 errno 翻译成可读字符串。

      • glibc:Linux 上最常用的 C 库实现是 GNU C Library (glibc);嵌入式替代品有 uClibc、diet libc;查询版本用 gnu_get_libc_version()confstr(_CS_GNU_LIBC_VERSION)

      • 可移植编程:feature test macros(_POSIX_C_SOURCE_XOPEN_SOURCE_GNU_SOURCE)控制可见的函数声明与类型;SUSv3 定义了标准系统数据类型(如 pid_tuid_t)。

      • 可重入函数getpwnamctimelocaltime 等返回静态分配结构的函数不可重入;多线程程序必须用 _r 版本(如 getpwnam_rctime_r)。

      本章主旨

      本章为后续所有系统编程章节奠定基础:(1) 系统调用是如何工作的(从 glibc 包装到内核服务例程);(2) 库函数与系统调用的区别;(3) 错误处理的统一约定(errno 与 perror/strerror);(4) 可移植编程的工具(feature test macros、SUSv3 数据类型)。本书的每个示例程序都会用 errExit()errMsg() 等错误处理辅助函数;这一章告诉你为什么必须这么做。

      一、核心概念

      本章围绕 6 个核心概念展开:从「系统调用如何工作」到「错误处理约定」,再到「可移植编程」。

      概念 定义 + 重要性 实现提示

      系统调用机制

      用户态→内核态的受控入口;通过 glibc 包装函数触发;每个系统调用有唯一编号;返回非负值表示成功,负值(取反后即 errno)表示失败。

      §3.1;调用 man 2 syscalls 查看所有 Linux 系统调用。

      库函数 vs 系统调用

      库函数属于 C 标准库;部分基于系统调用(如 fopenopen),部分不基于(如 strlen、字符串操作);库函数常提供更友好的接口(缓冲、格式化)。

      §3.2;不要混淆系统调用(man 2)与库函数(man 3)。

      errno 与错误处理

      <errno.h> 定义的全局整数;系统调用失败时设置;成功调用可能不改;必须「先看返回值」再「看 errno」。

      §3.4;perror(msg) 打印 msg: errno 对应字符串strerror(e) 返回 errno 对应的字符串。

      glibc (GNU C Library)

      Linux 上最常用的 C 标准库实现;包含系统调用包装、POSIX 扩展、线程支持等;版本号查询用 gnu_get_libc_version()

      §3.3;嵌入式替代品:uClibc、diet libc、musl。

      Feature Test Macros

      #include 之前定义,控制哪些函数声明、常量、类型可见;如 _POSIX_C_SOURCE=200809L_XOPEN_SOURCE=600_GNU_SOURCE

      §3.6;man page 的「Feature Test Macro Requirements」章节会说明需要哪个宏。

      可重入性 (Reentrancy)

      函数能否在「同一进程被同一信号的 handler 中断后再调用」时仍正确;返回静态分配结构的函数(getpwnamctimelocaltime)不可重入。

      §3.5;多线程/异步信号处理必须用 _r 后缀版本(如 getpwnam_r)。

      二、详细笔记

      3.1 系统调用机制

      What:系统调用是「进程请求内核服务」的受控入口;通过 glibc 包装函数触发;执行期间 CPU 切换到内核态;完成后返回用户态。

      Why:理解系统调用机制能解释「为什么系统调用有性能开销」「为什么 errno 是全局变量」「为什么 getpriority() 等函数需要特殊处理」。

      How:以 execve() 为例的系统调用执行步骤(x86-32,§3.1):

      1. 应用调用 glibc 包装函数 execve(path, argv, envp)

      2. 包装函数把参数复制到寄存器(内核期望的位置)。

      3. 包装函数把系统调用编号(__NR_execve = 11)存入 %eax

      4. 包装函数执行 int 0x80sysenter 指令——CPU 切换到内核态。

      5. 内核的 system_call() 例程检查编号合法性,索引 sys_call_table,调用 sys_execve()

      6. 服务例程执行,返回结果状态。

      7. 内核恢复寄存器,把返回值放在栈上。

      8. CPU 切回用户态;包装函数把返回值给应用;如果返回负数,把它取反写入 errno,并返回 -1 给应用。

      Linux 系统调用服务例程的命名约定:sys_xyz()(如 sys_execvesys_open);C 库包装函数同名(execveopen)。

      When

      • 需要直接与内核交互时(如绕过库函数缓冲)。

      • 调试性能问题时——系统调用开销约 0.3μs,比普通函数调用高一个数量级。

      • 理解 strace(附录 A)的输出——它能列出程序执行的所有系统调用。

      Example

      // 摘自《The Linux Programming Interface》第 3 章
      // 用户态调用 execve() —— 触发系统调用
      execve(path, argv, envp);
      // ↓ glibc 包装
      // sys_execve() 在内核执行
      // ↓ 成功:返回 0;失败:返回 -ENOENT 等
      if (execve(path, argv, envp) == -1) {
          if (errno == ENOENT)
              fprintf(stderr, "file not found: %s\n", path);
      }

      3.2 库函数与 glibc

      What:库函数属于 C 标准库;可能基于系统调用(如 fopenopen),也可能不基于(如 strlenmemcpy);glibc 是 Linux 上最常用的实现。

      Why:区分系统调用(man 2)与库函数(man 3)能避免「找不到手册页」「混淆两层错误处理」等问题。

      How

      类别 基于系统调用 纯用户态

      文件 I/O

      fopenopenfreadread

      字符串

      strlenstrcpystrcmp

      内存

      mallocbrk/mmap

      memcpymemset

      时间

      time → 系统调用

      strftime(格式化)

      数学

      sqrtsin

      glibc 是 Linux 默认 C 库;提供:

      • C 标准库(stdio.hstdlib.h 等)。

      • POSIX/SUS 扩展。

      • Linux 特有扩展。

      • 线程支持(NPTL)。

      • 国际化(locale)。

      When

      • 查询 glibc 版本——/lib/libc.so.6(直接运行);或 ldd myprog | grep libc;或程序内调用 gnu_get_libc_version()

      • 写嵌入式代码——考虑 uClibc、musl(更小)。

      • 在 man page 第 2 节看「系统调用」,第 3 节看「库函数」。

      Example

      // 摘自《The Linux Programming Interface》第 3 章
      // 查询 glibc 版本
      #include <gnu/libc-version.h>
      printf("glibc version: %s\n", gnu_get_libc_version());
      // 输出: "2.35"

      3.3 错误处理:errno 与诊断函数

      What:系统调用失败时设置全局 errnoperror()strerror() 把 errno 翻译为可读字符串;成功调用不重置 errno。

      Why:错误处理是系统编程的「卫生习惯」——跳过它会让 bug 难以诊断。

      How

      • errno<errno.h> 定义的 int;常见常量:EACCES(权限)、EBADF(坏 fd)、EFAULT(坏地址)、EINTR(被信号中断)、ENOENT(文件不存在)、ENOMEM(内存不足)。

      • 检查约定:调用返回 -1 → 看 errno;调用返回 0 → 成功;少数函数(如 getpriority)合法返回 -1——必须先 errno = 0 再调用。

      • perror(msg):打印 msg: errno 对应字符串 到 stderr。

      • strerror(e):返回 errno 对应字符串的指针。

      • strerror_r(e, buf, n):可重入版本。

      // 摘自《The Linux Programming Interface》第 3 章
      // 典型错误处理模式
      fd = open(pathname, O_RDONLY);
      if (fd == -1) {
          // 必须先看返回值!errno 在成功调用后可能仍然非零
          if (errno == ENOENT)
              fprintf(stderr, "file not found: %s\n", pathname);
          else
              errExit("open");  // 打印 msg + errno 描述 + exit(EXIT_FAILURE)
      }

      When

      • 几乎所有系统调用都应检查返回值。

      • 不要把 errno 当作「最后错误」——成功调用可能不改它。

      • 在多线程程序中——errno 是线程局部的(thread-local),可放心使用。

      Exampleperror("open") 输出形如 open: No such file or directory

      3.4 Feature Test Macros

      What:Feature test macros(特性测试宏)在 #include 之前定义,控制 <unistd.h> 等头文件暴露哪些声明与类型。

      Why:不同 UNIX 标准(POSIX、SUS、XPG)定义了不同的函数子集;通过宏告诉编译器「我要遵循哪个标准」。

      How:常见宏:

      暴露的标准

      _POSIX_C_SOURCE

      POSIX;200809L = POSIX.1-2008

      _XOPEN_SOURCE

      X/Open + SUS;600 = SUSv3

      _GNU_SOURCE

      GNU 扩展(Linux 特有)= _BSD_SOURCE + _SVID_SOURCE + 上面的所有

      _DEFAULT_SOURCE

      默认行为(包含 BSD/SVID)

      _BSD_SOURCE

      BSD 风格(已废弃,用 _DEFAULT_SOURCE

      When

      • 写可移植 UNIX 代码——_POSIX_C_SOURCE=200809L

      • 用 Linux 特有扩展——_GNU_SOURCE

      • man page 第 2/3 节通常会列出「Feature Test Macro Requirements」。

      Example

      // 摘自《The Linux Programming Interface》第 3 章
      #define _POSIX_C_SOURCE 200809L   // 启用 POSIX.1-2008
      #define _GNU_SOURCE                // 启用 GNU 扩展
      #include <unistd.h>
      #include <fcntl.h>
      // 现在可以使用 O_CLOEXEC 等 GNU 扩展
      int fd = open(path, O_RDONLY | O_CLOEXEC);

      3.5 可重入性 (Reentrancy)

      What:函数在「同一进程被信号 handler 中断,handler 又调用同一函数」时仍能正确工作——即可重入。

      Why:多线程程序 + 异步信号处理要求函数可重入;返回静态分配结构的函数不满足。

      How:不可重入函数通常返回指向静态缓冲区的指针——第二次调用会覆盖第一次的结果。

      • 不可重入:ctimelocaltimegmtimeasctimegetpwnamgetpwuidgetgrnamgetgrgidstrerror

      • 可重入版本:以上函数加 _r 后缀,如 ctime_r(buf, size, t)getpwnam_r(name, pwd, buf, buflen, &result)

      • errno 本身在 glibc 中是线程局部的(#define errno (*__errno_location()))。

      When

      • 写多线程程序——避免不可重入函数。

      • 信号 handler 内——只能用 async-signal-safe 函数(man 7 signal-safety);其余包括 printf、malloc 等都不可用。

      • 替代方案——拷贝返回值到本地缓冲区。

      Example

      // 摘自《The Linux Programming Interface》第 3 章
      // 不可重入版本(线程不安全)
      char *s = ctime(&t);    // 返回静态缓冲区指针
      // ... 第二次调用后 s 内容被覆盖
      
      // 可重入版本(线程安全)
      char buf[26];
      ctime_r(&t, buf);        // 结果写入调用者提供的 buf

      3.6 SUSv3 标准数据类型

      What:SUSv3 定义了一组标准系统数据类型(<sys/types.h>),如 pid_tuid_tgid_tmode_toff_t;目的是屏蔽不同实现的位宽差异。

      Why:使用 SUSv3 标准类型能让代码在不同 UNIX 实现间可移植;不直接假设 intlong 的位宽。

      How:常见类型:

      类型 用途

      pid_t

      进程 ID

      uid_t, gid_t

      用户 ID、组 ID

      mode_t

      文件权限位

      off_t

      文件偏移

      size_t

      无符号大小(sizeof

      ssize_t

      有符号大小(read/write 返回值)

      time_t

      时间(秒数)

      dev_t, ino_t

      设备 ID、i-node 号

      When

      • 函数参数与返回值涉及系统资源——使用 SUSv3 类型。

      • 打印这些类型——不要假设 int/long;用 (long)(long long) 强制转换(与 printf 格式匹配)。

      Example

      // 摘自《The Linux Programming Interface》第 3 章
      pid_t pid = getpid();
      printf("PID = %ld\n", (long) pid);  // 强制 long 转换以兼容 %ld

      三、关键图表

      非可视化条目(关键 API 对照)
      API / 概念 描述

      系统调用机制

      glibc 包装 → int 0x80/sysenter → 内核 sys_call_table → sys_xyz() 服务例程

      errno 检查

      返回 -1 → 看 errno;少数函数(getpriority)合法返回 -1 → 先 errno=0

      perror / strerror

      把 errno 翻译为可读字符串

      glibc 版本查询

      /lib/libc.so.6 直接运行;gnu_get_libc_version();confstr(_CS_GNU_LIBC_VERSION)

      Feature Test Macros

      _POSIX_C_SOURCE / _XOPEN_SOURCE / _GNU_SOURCE

      可重入 _r 函数

      ctime_r / localtime_r / getpwnam_r / strerror_r

      四、思维导图

      mindmap
        root((第 3 章 系统编程概念))
          系统调用机制
            包装函数
            int 0x80 sysenter
            sys_call_table
            返回值约定
          库函数与 glibc
            标准 C 库
            POSIX 扩展
            GNU 扩展
            版本查询
          errno 与错误
            全局 errno
            失败返回 -1
            perror strerror
            EINTR 处理
          可移植
            Feature Test Macros
            POSIX SUS XPG
            _GNU_SOURCE
            SUSv3 数据类型
          可重入性
            静态缓冲区陷阱
            _r 版本
            信号安全
          错误处理辅助
            errExit errMsg
            usageErr cmdLineErr
            fatal

      五、重点与易错点

      1. 「系统调用」vs「库函数」:man page 第 2 节是系统调用(open/read/fork),第 3 节是库函数(fopen/printf/malloc);不要混淆。

      2. errno 不在成功调用后被重置:所以必须「先看返回值再看 errno」——这是初学者最常犯的错误。

      3. 少数函数合法返回 -1getpriorityread(在 EOF 时返回 0)、write(short write)——必须先 errno = 0 再调用。

      4. glibc vs musl/uClibc:嵌入式系统或对二进制大小敏感时考虑替代品;多数服务器/桌面场景用 glibc 即可。

      5. Feature Test Macros 必须 #include 之前定义:否则头文件可能不暴露所需的声明,编译报「implicit declaration」。

      6. errno 是线程局部变量(glibc 实现):多线程中每个线程有独立的 errno,无需额外同步;但不要在不同线程间共享 errno 变量。

      7. 信号 handler 只能用 async-signal-safe 函数printfmallocexit 都不可用;详见 man 7 signal-safety

      8. strace 是排查系统调用问题的好工具strace -e trace=open,read,write ./prog 列出指定系统调用;详见附录 A。

      9. 跨章衔接:第 4-5 章的文件 I/O 大量使用系统调用;第 7 章 malloc 基于 brk/mmap;第 8-9 章用 SUSv3 类型;第 20-22 章的信号处理必须考虑可重入性。

      10. 避免 man 2man 3 编号混淆:用 man -a open(看所有节)、man 3 errno(库函数 errno 定义)。