Linux 系统编程 · 第 2 章:系统调用与库函数

Linux 系统编程 · 第 2 章:系统调用与库函数

本章深入剖析系统调用机制、glibc 封装原理与 errno 错误处理体系,是 Linux 系统编程的基石。


目录

  1. 系统调用机制(syscall)

  2. [C 标准库与 glibc](#C 标准库与 glibc)

  3. [errno 错误处理体系](#errno 错误处理体系)

  4. [系统调用 vs 库函数对比](#系统调用 vs 库函数对比)

  5. 综合实践


1. 系统调用机制(syscall)

1.1 什么是系统调用

系统调用(System Call)是用户态程序请求内核执行特权操作的唯一合法入口。用户程序不能直接访问硬件或内核数据结构,必须通过系统调用陷入内核态。

复制代码
用户态 (User Space)                    内核态 (Kernel Space)
─────────────────────────────────────────────────────────────
  应用程序
     │
     │  调用 write(fd, buf, n)
     ▼
  glibc 封装函数
     │
     │  设置寄存器(系统调用号 + 参数)
     │  执行 syscall 指令(x86_64)
     │  ─────────────────────────────► 陷入内核
     │                                    │
     │                                    ▼
     │                              系统调用分发表
     │                              (sys_call_table)
     │                                    │
     │                                    ▼
     │                              sys_write() 内核实现
     │                                    │
     │  ◄─────────────────────────────── 返回结果
     │  从内核态返回用户态
     ▼
  返回值(成功 ≥ 0,失败 = -1 并设置 errno)

1.2 系统调用号

每个系统调用都有唯一的调用号,存储在头文件中,内核通过调用号索引函数指针表。

复制代码
# 查看 x86_64 系统调用号表
cat /usr/include/asm/unistd_64.h | head -30
# 或
ausyscall --dump 2>/dev/null | head -20
​
# 常见系统调用号(x86_64)
# 0  read        1  write       2  open
# 3  close       4  stat        5  fstat
# 9  mmap       11  munmap     12  brk
# 39 getpid     57 fork        59 execve
# 60 exit       62 kill
复制代码
/* 文件名:syscall_numbers.c
 * 演示系统调用号与直接调用的关系
 */
#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>   /* 包含 SYS_xxx 宏定义 */
​
int main(void) {
    /* 打印常见系统调用号 */
    printf("系统调用号对照表(x86_64):\n");
    printf("  SYS_read    = %d\n", SYS_read);
    printf("  SYS_write   = %d\n", SYS_write);
    printf("  SYS_open    = %d\n", SYS_open);
    printf("  SYS_close   = %d\n", SYS_close);
    printf("  SYS_getpid  = %d\n", SYS_getpid);
    printf("  SYS_fork    = %d\n", SYS_fork);
    printf("  SYS_execve  = %d\n", SYS_execve);
    printf("  SYS_exit    = %d\n", SYS_exit);
​
    /* 方式1:通过 glibc 封装调用 */
    pid_t pid1 = getpid();
​
    /* 方式2:直接使用 syscall() 函数(绕过 glibc 封装) */
    pid_t pid2 = (pid_t)syscall(SYS_getpid);
​
    /* 方式3:内联汇编(最底层,了解原理用) */
    pid_t pid3;
    __asm__ volatile (
        "syscall"
        : "=a"(pid3)          /* 输出:rax 寄存器存放返回值 */
        : "0"(SYS_getpid)     /* 输入:rax = 系统调用号 */
        : "rcx", "r11", "memory"  /* 被破坏的寄存器 */
    );
​
    printf("\n三种方式获取 PID:\n");
    printf("  glibc getpid():    %d\n", pid1);
    printf("  syscall(SYS_getpid): %d\n", pid2);
    printf("  内联汇编 syscall:  %d\n", pid3);
    printf("  结果一致: %s\n",
           (pid1 == pid2 && pid2 == pid3) ? "✓ 是" : "✗ 否");
​
    return 0;
}
复制代码
gcc -o syscall_numbers syscall_numbers.c
./syscall_numbers
# 输出示例:
# 系统调用号对照表(x86_64):
#   SYS_read    = 0
#   SYS_write   = 1
#   SYS_open    = 2
#   SYS_close   = 3
#   SYS_getpid  = 39
#   SYS_fork    = 57
#   SYS_execve  = 59
#   SYS_exit    = 60
#
# 三种方式获取 PID:
#   glibc getpid():      12345
#   syscall(SYS_getpid): 12345
#   内联汇编 syscall:    12345
#   结果一致: ✓ 是

1.3 系统调用的参数传递(x86_64 ABI)

x86_64 Linux 使用寄存器传递系统调用参数,最多支持 6 个参数

复制代码
寄存器分配(x86_64 Linux syscall ABI):
┌──────────┬────────────────────────────────────────┐
│  寄存器  │  用途                                   │
├──────────┼────────────────────────────────────────┤
│  rax     │  系统调用号(输入)/ 返回值(输出)      │
│  rdi     │  第 1 个参数                            │
│  rsi     │  第 2 个参数                            │
│  rdx     │  第 3 个参数                            │
│  r10     │  第 4 个参数                            │
│  r8      │  第 5 个参数                            │
│  r9      │  第 6 个参数                            │
│  rcx,r11 │  被 syscall 指令破坏(保存 rip/rflags) │
└──────────┴────────────────────────────────────────┘
​
示例:write(1, "Hello\n", 6)
  rax = 1      (SYS_write)
  rdi = 1      (fd = STDOUT_FILENO)
  rsi = 地址   (buf 指针)
  rdx = 6      (count)
  → 执行 syscall 指令
  ← rax = 6   (实际写入字节数,或负数表示错误)
复制代码
/* 文件名:syscall_abi.c
 * 用内联汇编演示 write 系统调用的寄存器传参
 */
#include <stdio.h>
#include <sys/syscall.h>
​
/* 手动实现 write 系统调用(仅用于教学演示) */
static long my_write(int fd, const void *buf, unsigned long count) {
    long ret;
    __asm__ volatile (
        "syscall"
        : "=a"(ret)                          /* 输出:返回值在 rax */
        : "0"((long)SYS_write),              /* rax = 系统调用号 */
          "D"((long)fd),                     /* rdi = fd */
          "S"(buf),                          /* rsi = buf */
          "d"(count)                         /* rdx = count */
        : "rcx", "r11", "memory"
    );
    return ret;
}
​
int main(void) {
    const char msg[] = "通过内联汇编直接调用 write 系统调用!\n";
    long n = my_write(1, msg, sizeof(msg) - 1);
    /* 注意:这里不能用 printf(它也会调用 write),
     * 改用 syscall 版本打印结果 */
    char result[64];
    int len = __builtin_snprintf(result, sizeof(result),
                                 "写入了 %ld 字节\n", n);
    my_write(1, result, (unsigned long)len);
    return 0;
}
复制代码
gcc -o syscall_abi syscall_abi.c
./syscall_abi
# 输出:
# 通过内联汇编直接调用 write 系统调用!
# 写入了 38 字节

1.4 使用 strace 追踪系统调用

strace 是分析程序行为的核心工具,可以拦截并记录所有系统调用。

复制代码
# 基本用法:追踪程序的所有系统调用
strace ls /tmp 2>&1 | tail -20
​
# 只追踪特定系统调用
strace -e trace=read,write,open,close cat /etc/hostname
​
# 统计系统调用次数和耗时
strace -c ls /usr/bin 2>&1
​
# 追踪已运行的进程(需要权限)
strace -p $(pgrep bash | head -1)
​
# 追踪子进程
strace -f bash -c "ls && echo done"
​
# 输出到文件(-o)并显示时间戳(-t)
strace -o strace.log -t ls /etc
​
# 显示每个系统调用的耗时(微秒)
strace -T ls /etc 2>&1 | grep -v "^---"
复制代码
# 实战:分析 cat /etc/hostname 的系统调用流程
strace cat /etc/hostname 2>&1 | grep -E "open|read|write|close"
# 典型输出:
# openat(AT_FDCWD, "/etc/hostname", O_RDONLY) = 3
# read(3, "myhost\n", 131072)               = 7
# write(1, "myhost\n", 7)                   = 7
# close(3)                                  = 0

1.5 系统调用的开销与 vDSO 优化

频繁的系统调用会因用户态/内核态切换 产生较大开销。Linux 通过 vDSO(Virtual Dynamic Shared Object) 优化高频调用。

复制代码
/* 文件名:syscall_overhead.c
 * 对比系统调用与普通函数调用的性能差异
 */
#include <stdio.h>
#include <time.h>
#include <sys/syscall.h>
#include <unistd.h>

#define ITERATIONS 1000000   /* 100万次 */

int main(void) {
    struct timespec t1, t2;
    long long elapsed_ns;

    /* ── 测试1:glibc clock_gettime(通过 vDSO,无需陷入内核)── */
    clock_gettime(CLOCK_MONOTONIC, &t1);
    for (int i = 0; i < ITERATIONS; i++) {
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);  /* vDSO 优化,极快 */
    }
    clock_gettime(CLOCK_MONOTONIC, &t2);
    elapsed_ns = (t2.tv_sec - t1.tv_sec) * 1000000000LL
               + (t2.tv_nsec - t1.tv_nsec);
    printf("clock_gettime (vDSO):  %lld ns / 次  (总计 %lld ms)\n",
           elapsed_ns / ITERATIONS, elapsed_ns / 1000000);

    /* ── 测试2:直接 syscall(强制陷入内核,有切换开销)── */
    clock_gettime(CLOCK_MONOTONIC, &t1);
    for (int i = 0; i < ITERATIONS; i++) {
        syscall(SYS_gettimeofday, NULL, NULL);  /* 强制系统调用 */
    }
    clock_gettime(CLOCK_MONOTONIC, &t2);
    elapsed_ns = (t2.tv_sec - t1.tv_sec) * 1000000000LL
               + (t2.tv_nsec - t1.tv_nsec);
    printf("syscall gettimeofday:  %lld ns / 次  (总计 %lld ms)\n",
           elapsed_ns / ITERATIONS, elapsed_ns / 1000000);

    /* ── 查看 vDSO 映射 ── */
    printf("\n/proc/self/maps 中的 vDSO 映射:\n");
    FILE *fp = fopen("/proc/self/maps", "r");
    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        if (strstr(line, "vdso") || strstr(line, "vvar")) {
            printf("  %s", line);
        }
    }
    fclose(fp);

    return 0;
}
复制代码
gcc -O2 -o syscall_overhead syscall_overhead.c
./syscall_overhead
# 输出示例:
# clock_gettime (vDSO):    8 ns / 次  (总计    8 ms)
# syscall gettimeofday:  150 ns / 次  (总计  150 ms)
#
# /proc/self/maps 中的 vDSO 映射:
#   7fff...000-7fff...000 r-xp ... [vdso]
#   7fff...000-7fff...000 r--p ... [vvar]

💡 vDSO 原理 :内核将 clock_gettimegettimeofday 等高频调用的实现映射到用户空间,程序直接在用户态执行,避免了特权级切换,性能提升约 10~20 倍


2. C 标准库与 glibc

2.1 glibc 概述

glibc(GNU C Library) 是 Linux 上最主要的 C 标准库实现,提供:

  • POSIX API 封装 :对系统调用的封装(openreadwrite...)

  • 标准 C 函数printfmallocstrcpyqsort...

  • 线程支持:pthreads 实现

  • 数学库libm

  • 动态链接器ld-linux.so

复制代码
# 查看 glibc 版本
ldd --version
# 或
/lib/x86_64-linux-gnu/libc.so.6 --version 2>/dev/null | head -1

# 查看程序链接的库
ldd /bin/ls

# 查看 glibc 提供的符号(函数)
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " T " | head -20

# 查看程序实际调用了哪些库函数
objdump -d /bin/ls | grep "callq\|call " | head -20

2.2 系统调用封装原理

glibc 对系统调用的封装做了以下工作:

复制代码
glibc write() 封装的内部流程:
─────────────────────────────────────────────────────
1. 参数验证(可选)
2. 将参数放入对应寄存器
3. 将系统调用号放入 rax
4. 执行 syscall 指令
5. 检查返回值:
   - 若 rax ∈ [-4095, -1],说明出错:
     * 将 errno 设置为 -rax(错误码取反)
     * 返回 -1 给调用者
   - 否则直接返回 rax(成功值)
─────────────────────────────────────────────────────
复制代码
/* 文件名:glibc_wrapper.c
 * 手动实现一个简化版的 glibc open() 封装,理解其工作原理
 */
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/syscall.h>

/* 手动实现 open 的 glibc 封装(简化版,仅用于教学) */
int my_open(const char *pathname, int flags, ...) {
    long ret;

    /* 直接调用 openat 系统调用(open 在新内核中由 openat 实现) */
    ret = syscall(SYS_openat, AT_FDCWD, pathname, flags, 0666);

    /* glibc 的错误转换逻辑:
     * 内核返回负数 -errno_code 表示错误
     * glibc 将其转换为:返回 -1,并设置全局 errno
     */
    if (ret < 0) {
        errno = (int)(-ret);   /* 设置 errno(取反得到错误码) */
        return -1;             /* 返回 -1 表示失败 */
    }

    return (int)ret;           /* 返回文件描述符 */
}

int main(void) {
    /* 测试1:打开存在的文件 */
    int fd = my_open("/etc/hostname", O_RDONLY);
    if (fd == -1) {
        perror("my_open /etc/hostname");
    } else {
        printf("成功打开 /etc/hostname,fd = %d\n", fd);
        close(fd);
    }

    /* 测试2:打开不存在的文件,观察 errno */
    fd = my_open("/nonexistent/path/file.txt", O_RDONLY);
    if (fd == -1) {
        printf("打开失败,errno = %d,错误信息: %s\n",
               errno, strerror(errno));
    }

    /* 对比:使用标准 open() */
    fd = open("/nonexistent/path/file.txt", O_RDONLY);
    if (fd == -1) {
        printf("标准 open 失败,errno = %d,错误信息: %s\n",
               errno, strerror(errno));
    }

    return 0;
}
复制代码
gcc -o glibc_wrapper glibc_wrapper.c
./glibc_wrapper
# 输出:
# 成功打开 /etc/hostname,fd = 3
# 打开失败,errno = 2,错误信息: No such file or directory
# 标准 open 失败,errno = 2,错误信息: No such file or directory

2.3 标准 I/O 库(stdio)vs 系统调用 I/O

glibc 的 stdioFILE * 接口)在系统调用之上增加了用户空间缓冲区

复制代码
stdio 缓冲层示意:
─────────────────────────────────────────────────────────────
  fprintf(fp, "data")
       │
       ▼
  ┌─────────────────────────────────────────┐
  │  用户空间缓冲区(FILE 结构体内部)        │
  │  [d][a][t][a][...........空闲空间......]  │
  │  ← 数据先写入缓冲区,不立即触发系统调用  │
  └──────────────────┬──────────────────────┘
                     │ 缓冲区满 / fflush() / fclose() / 换行符(行缓冲)
                     ▼
              write() 系统调用
                     │
                     ▼
                  内核缓冲区
                     │
                     ▼
                  磁盘/设备
─────────────────────────────────────────────────────────────

三种缓冲模式:
  全缓冲(_IOFBF):缓冲区满才刷新(普通文件默认)
  行缓冲(_IOLBF):遇到换行符刷新(终端默认)
  无缓冲(_IONBF):立即写入(stderr 默认)
复制代码
/* 文件名:stdio_vs_syscall.c
 * 对比 stdio 缓冲 I/O 与系统调用直接 I/O 的行为差异
 */
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <time.h>

#define ITERATIONS  100000
#define MSG         "Hello\n"
#define MSG_LEN     6

/* 计时辅助函数 */
static long long now_ns(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

int main(void) {
    long long t1, t2;

    /* ── 测试1:stdio 带缓冲写入(写到 /dev/null)── */
    FILE *fp = fopen("/dev/null", "w");
    t1 = now_ns();
    for (int i = 0; i < ITERATIONS; i++) {
        fwrite(MSG, 1, MSG_LEN, fp);
    }
    fflush(fp);
    t2 = now_ns();
    fclose(fp);
    printf("stdio fwrite  (%d次): %lld ms\n",
           ITERATIONS, (t2 - t1) / 1000000);

    /* ── 测试2:系统调用直接写入(写到 /dev/null)── */
    int fd = open("/dev/null", O_WRONLY);
    t1 = now_ns();
    for (int i = 0; i < ITERATIONS; i++) {
        write(fd, MSG, MSG_LEN);   /* 每次都触发系统调用 */
    }
    t2 = now_ns();
    close(fd);
    printf("syscall write (%d次): %lld ms\n",
           ITERATIONS, (t2 - t1) / 1000000);

    /* ── 演示缓冲行为 ── */
    printf("\n--- 缓冲行为演示 ---\n");

    /* stderr 是无缓冲的,立即输出 */
    fprintf(stderr, "stderr(无缓冲): 立即显示\n");

    /* stdout 是行缓冲(终端)或全缓冲(文件) */
    printf("stdout 第1行(有换行,行缓冲立即刷新)\n");
    printf("stdout 无换行,可能不立即显示...");
    fflush(stdout);   /* 手动刷新 */
    printf(" 已手动 fflush\n");

    /* 修改缓冲模式 */
    setvbuf(stdout, NULL, _IONBF, 0);   /* 改为无缓冲 */
    printf("改为无缓冲后,每个字符立即输出\n");

    return 0;
}
复制代码
gcc -O2 -o stdio_vs_syscall stdio_vs_syscall.c
./stdio_vs_syscall
# 输出示例:
# stdio fwrite  (100000次):  2 ms
# syscall write (100000次): 45 ms
# (stdio 因缓冲减少了系统调用次数,速度快约 20 倍)

2.4 动态链接与库加载

复制代码
# 查看可执行文件依赖的动态库
ldd /bin/bash
# 输出示例:
#   linux-vdso.so.1 (0x00007fff...)
#   libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6
#   libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6
#   /lib64/ld-linux-x86-64.so.2

# 查看库的搜索路径
ldconfig -v 2>/dev/null | grep "^/" | head -10
cat /etc/ld.so.conf
echo $LD_LIBRARY_PATH

# 查看函数来自哪个库
nm -D /lib/x86_64-linux-gnu/libc.so.6 | grep " T printf"
复制代码
/* 文件名:dynamic_link.c
 * 演示运行时动态加载库(dlopen/dlsym)
 */
#include <stdio.h>
#include <dlfcn.h>    /* dlopen, dlsym, dlclose */
#include <math.h>

int main(void) {
    /* 运行时动态加载 libm(数学库) */
    void *handle = dlopen("libm.so.6", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen 失败: %s\n", dlerror());
        return 1;
    }

    /* 清除之前的错误 */
    dlerror();

    /* 获取 sin 函数的地址 */
    double (*my_sin)(double) = dlsym(handle, "sin");
    const char *err = dlerror();
    if (err) {
        fprintf(stderr, "dlsym 失败: %s\n", err);
        dlclose(handle);
        return 1;
    }

    /* 调用动态加载的函数 */
    printf("动态加载 libm.so.6 成功\n");
    printf("sin(π/6) = %.4f(期望 0.5000)\n", my_sin(3.14159265 / 6));
    printf("sin(π/2) = %.4f(期望 1.0000)\n", my_sin(3.14159265 / 2));

    /* 查看库的加载地址 */
    printf("\n库加载信息:\n");
    Dl_info info;
    if (dladdr(my_sin, &info)) {
        printf("  函数名:   %s\n", info.dli_sname);
        printf("  库路径:   %s\n", info.dli_fname);
        printf("  函数地址: %p\n", info.dli_saddr);
    }

    dlclose(handle);
    return 0;
}
复制代码
gcc -o dynamic_link dynamic_link.c -ldl
./dynamic_link
# 输出:
# 动态加载 libm.so.6 成功
# sin(π/6) = 0.5000(期望 0.5000)
# sin(π/2) = 1.0000(期望 1.0000)
#
# 库加载信息:
#   函数名:   sin
#   库路径:   /lib/x86_64-linux-gnu/libm.so.6
#   函数地址: 0x7f...

3. errno 错误处理体系

3.1 errno 机制原理

errno 是 C 标准库定义的线程局部全局变量,用于报告系统调用和库函数的错误原因。

复制代码
错误处理流程:
─────────────────────────────────────────────────────────────
  系统调用失败
       │
       ▼
  内核返回负数(如 -2 表示 ENOENT)
       │
       ▼
  glibc 封装层:
    errno = 2;    ← 设置线程局部 errno(取反)
    return -1;    ← 返回 -1 给调用者
       │
       ▼
  用户代码检查返回值:
    if (ret == -1) {
        // 读取 errno 获取具体原因
        perror("操作失败");
    }
─────────────────────────────────────────────────────────────

⚠️  重要规则:
  1. 只有当函数返回值表示失败时,errno 才有意义
  2. 成功的调用可能会修改 errno(某些函数)
  3. errno 是线程局部的(Thread-Local Storage),多线程安全
  4. 必须在调用失败后立即检查 errno,后续调用可能覆盖它

3.2 常见 errno 错误码

复制代码
/* 文件名:errno_table.c
 * 打印常见 errno 错误码及其含义
 */
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main(void) {
    /* 常见错误码对照表 */
    struct { int code; const char *name; const char *desc; } errors[] = {
        { EPERM,        "EPERM",        "操作不允许(权限不足)" },
        { ENOENT,       "ENOENT",       "文件或目录不存在" },
        { ESRCH,        "ESRCH",        "进程不存在" },
        { EINTR,        "EINTR",        "系统调用被信号中断" },
        { EIO,          "EIO",          "I/O 错误" },
        { EBADF,        "EBADF",        "无效的文件描述符" },
        { ENOMEM,       "ENOMEM",       "内存不足" },
        { EACCES,       "EACCES",       "权限被拒绝" },
        { EFAULT,       "EFAULT",       "无效的内存地址" },
        { EBUSY,        "EBUSY",        "设备或资源忙" },
        { EEXIST,       "EEXIST",       "文件已存在" },
        { ENODEV,       "ENODEV",       "设备不存在" },
        { EISDIR,       "EISDIR",       "是一个目录" },
        { EINVAL,       "EINVAL",       "无效参数" },
        { EMFILE,       "EMFILE",       "进程打开文件数超限" },
        { ENFILE,       "ENFILE",       "系统打开文件数超限" },
        { ENOSPC,       "ENOSPC",       "设备空间不足" },
        { EPIPE,        "EPIPE",        "管道破裂(写端无读者)" },
        { ERANGE,       "ERANGE",       "结果超出范围" },
        { EAGAIN,       "EAGAIN",       "资源暂时不可用(非阻塞)" },
        { EWOULDBLOCK,  "EWOULDBLOCK",  "操作会阻塞(同 EAGAIN)" },
        { EINPROGRESS,  "EINPROGRESS",  "操作正在进行中" },
        { ETIMEDOUT,    "ETIMEDOUT",    "连接超时" },
        { ECONNREFUSED, "ECONNREFUSED", "连接被拒绝" },
        { EADDRINUSE,   "EADDRINUSE",   "地址已被使用" },
        { 0, NULL, NULL }
    };

    printf("%-6s  %-16s  %-35s  %s\n",
           "编号", "宏名", "strerror() 输出", "中文说明");
    printf("%s\n", "──────────────────────────────────────────────────────────────────");

    for (int i = 0; errors[i].name; i++) {
        printf("%-6d  %-16s  %-35s  %s\n",
               errors[i].code,
               errors[i].name,
               strerror(errors[i].code),
               errors[i].desc);
    }

    return 0;
}
复制代码
gcc -o errno_table errno_table.c
./errno_table
# 输出示例:
# 编号    宏名              strerror() 输出                      中文说明
# ──────────────────────────────────────────────────────────────────
# 1       EPERM             Operation not permitted              操作不允许(权限不足)
# 2       ENOENT            No such file or directory            文件或目录不存在
# 4       EINTR             Interrupted system call              系统调用被信号中断
# ...

3.3 错误处理函数

复制代码
/* 文件名:error_handling.c
 * 演示三种错误报告方式:perror、strerror、strerror_r
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

/* ── 方式1:perror() ─────────────────────────────────────
 * 格式:[prefix]: [strerror(errno)]\n
 * 输出到 stderr,自动读取当前 errno
 */
void demo_perror(void) {
    printf("\n=== perror() 演示 ===\n");
    int fd = open("/root/secret_file", O_RDONLY);
    if (fd == -1) {
        perror("open /root/secret_file");
        /* 输出:open /root/secret_file: Permission denied */
    }
}

/* ── 方式2:strerror() ───────────────────────────────────
 * 返回错误码对应的字符串(非线程安全,共享静态缓冲区)
 */
void demo_strerror(void) {
    printf("\n=== strerror() 演示 ===\n");
    /* 手动构造错误信息,更灵活 */
    int fd = open("/nonexistent", O_RDONLY);
    if (fd == -1) {
        int saved_errno = errno;   /* 立即保存 errno! */
        fprintf(stderr, "错误 [%d]: %s(尝试打开 /nonexistent)\n",
                saved_errno, strerror(saved_errno));
    }
}

/* ── 方式3:strerror_r() ─────────────────────────────────
 * 线程安全版本,将结果写入用户提供的缓冲区
 */
void demo_strerror_r(void) {
    printf("\n=== strerror_r() 演示(线程安全)===\n");
    char errbuf[128];
    /* GNU 版本:返回 char*(可能是 errbuf 或静态字符串) */
    int fd = open("/nonexistent", O_RDONLY);
    if (fd == -1) {
        int saved_errno = errno;
        /* XSI 版本(POSIX 标准):返回 int */
        if (strerror_r(saved_errno, errbuf, sizeof(errbuf)) == 0) {
            fprintf(stderr, "线程安全错误信息: %s\n", errbuf);
        }
    }
}

/* ── 方式4:自定义错误处理函数(推荐实践)────────────────
 * 统一的错误处理,支持格式化输出和可选退出
 */
void err_exit(const char *fmt, ...) {
    int saved_errno = errno;   /* 保存 errno,因为 va_list 操作可能修改它 */
    va_list ap;
    char buf[512];

    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    fprintf(stderr, "错误: %s: %s\n", buf, strerror(saved_errno));
    exit(EXIT_FAILURE);
}

/* 不退出的版本 */
void err_msg(const char *fmt, ...) {
    int saved_errno = errno;
    va_list ap;
    char buf[512];

    va_start(ap, fmt);
    vsnprintf(buf, sizeof(buf), fmt, ap);
    va_end(ap);

    fprintf(stderr, "警告: %s: %s\n", buf, strerror(saved_errno));
    errno = saved_errno;   /* 恢复 errno */
}

int main(void) {
    demo_perror();
    demo_strerror();
    demo_strerror_r();

    printf("\n=== 自定义错误处理演示 ===\n");
    int fd = open("/tmp/test_errno_demo.txt",
                  O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) {
        err_exit("创建文件 /tmp/test_errno_demo.txt 失败");
    }
    printf("文件创建成功,fd = %d\n", fd);
    close(fd);
    unlink("/tmp/test_errno_demo.txt");

    return 0;
}
复制代码
gcc -o error_handling error_handling.c
./error_handling
# 输出示例:
# === perror() 演示 ===
# open /root/secret_file: Permission denied
#
# === strerror() 演示 ===
# 错误 [2]: No such file or directory(尝试打开 /nonexistent)
#
# === strerror_r() 演示(线程安全)===
# 线程安全错误信息: No such file or directory
#
# === 自定义错误处理演示 ===
# 文件创建成功,fd = 3

3.4 EINTR 与系统调用重启

某些系统调用在被信号中断 时会返回 EINTR,需要手动重试:

复制代码
/* 文件名:eintr_retry.c
 * 演示 EINTR 处理与系统调用自动重启封装
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>

/* 信号处理函数(会中断阻塞的系统调用) */
static void sig_handler(int signo) {
    /* 注意:信号处理函数中只能调用异步信号安全函数 */
    const char msg[] = "收到信号,系统调用被中断\n";
    write(STDERR_FILENO, msg, sizeof(msg) - 1);
}

/* ── 封装1:带 EINTR 重试的 read ─────────────────────────
 * 自动处理信号中断,对调用者透明
 */
ssize_t read_restart(int fd, void *buf, size_t count) {
    ssize_t ret;
    do {
        ret = read(fd, buf, count);
    } while (ret == -1 && errno == EINTR);
    /* 其他错误(如 EIO)直接返回 -1 */
    return ret;
}

/* ── 封装2:带 EINTR 重试的 write(处理部分写入)─────────
 * write 可能只写入部分数据,需要循环直到全部写完
 */
ssize_t write_all(int fd, const void *buf, size_t count) {
    const char *ptr = (const char *)buf;
    size_t remaining = count;
    ssize_t written;

    while (remaining > 0) {
        written = write(fd, ptr, remaining);
        if (written == -1) {
            if (errno == EINTR) continue;   /* 信号中断,重试 */
            return -1;                       /* 其他错误 */
        }
        ptr       += written;
        remaining -= (size_t)written;
    }
    return (ssize_t)count;
}

int main(void) {
    /* 注册 SIGALRM 信号处理函数 */
    struct sigaction sa = {
        .sa_handler = sig_handler,
        .sa_flags   = 0   /* 不设置 SA_RESTART,让系统调用返回 EINTR */
    };
    sigemptyset(&sa.sa_mask);
    sigaction(SIGALRM, &sa, NULL);

    /* 创建测试文件 */
    int fd = open("/tmp/eintr_test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return 1; }

    /* 写入测试数据 */
    const char *data = "Hello, EINTR test!\n";
    if (write_all(fd, data, strlen(data)) == -1) {
        perror("write_all");
        close(fd);
        return 1;
    }
    lseek(fd, 0, SEEK_SET);   /* 回到文件开头 */

    /* 设置 1 秒后发送 SIGALRM(模拟信号中断) */
    alarm(1);

    printf("开始读取(1秒后会被信号中断,自动重试)...\n");
    char buf[64] = {0};
    ssize_t n = read_restart(fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        printf("读取成功(%zd 字节): %s", n, buf);
    } else {
        perror("read_restart");
    }

    close(fd);
    unlink("/tmp/eintr_test.txt");

    /* ── SA_RESTART 标志:让内核自动重启被中断的系统调用 ── */
    printf("\n--- SA_RESTART 演示 ---\n");
    sa.sa_flags = SA_RESTART;   /* 设置自动重启标志 */
    sigaction(SIGALRM, &sa, NULL);
    printf("SA_RESTART 已设置:信号不会中断系统调用\n");

    return 0;
}
复制代码
gcc -o eintr_retry eintr_retry.c
./eintr_retry
# 输出示例:
# 开始读取(1秒后会被信号中断,自动重试)...
# 收到信号,系统调用被中断
# 读取成功(19 字节): Hello, EINTR test!

3.5 errno 的线程安全性

复制代码
/* 文件名:errno_thread_safe.c
 * 演示 errno 是线程局部变量(Thread-Local Storage)
 */
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <pthread.h>
#include <fcntl.h>
#include <unistd.h>

/* 每个线程独立设置 errno,互不干扰 */
void *thread_func(void *arg) {
    int thread_id = *(int *)arg;

    /* 触发不同的错误 */
    if (thread_id == 1) {
        open("/nonexistent_file_1", 0);   /* ENOENT = 2 */
    } else {
        open("/root/no_permission", 0);   /* EACCES = 13 */
    }

    int my_errno = errno;   /* 读取本线程的 errno */

    /* 模拟耗时操作(此时另一个线程可能修改其 errno) */
    usleep(100000);   /* 100ms */

    /* errno 应该仍然是本线程设置的值 */
    printf("线程 %d: errno = %d (%s),未被其他线程修改: %s\n",
           thread_id, my_errno, strerror(my_errno),
           errno == my_errno ? "✓" : "✗");

    return NULL;
}

int main(void) {
    pthread_t t1, t2;
    int id1 = 1, id2 = 2;

    printf("errno 是线程局部变量(TLS),每个线程独立:\n\n");

    pthread_create(&t1, NULL, thread_func, &id1);
    pthread_create(&t2, NULL, thread_func, &id2);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);

    /* 验证 errno 的 TLS 实现 */
    printf("\n验证 errno 的 TLS 地址(不同线程地址不同):\n");
    printf("主线程 &errno = %p\n", &errno);

    return 0;
}
复制代码
gcc -o errno_thread_safe errno_thread_safe.c -lpthread
./errno_thread_safe
# 输出示例:
# errno 是线程局部变量(TLS),每个线程独立:
#
# 线程 1: errno = 2 (No such file or directory),未被其他线程修改: ✓
# 线程 2: errno = 13 (Permission denied),未被其他线程修改: ✓
#
# 验证 errno 的 TLS 地址(不同线程地址不同):
# 主线程 &errno = 0x7f...

4. 系统调用 vs 库函数对比

4.1 核心区别

复制代码
┌─────────────────┬──────────────────────────┬──────────────────────────┐
│  对比维度        │  系统调用(syscall)       │  库函数(glibc)          │
├─────────────────┼──────────────────────────┼──────────────────────────┤
│ 执行位置         │  内核态                   │  用户态                   │
│ 性能开销         │  较大(特权级切换)        │  较小(无切换)            │
│ 缓冲机制         │  无(直接操作内核缓冲)    │  有(用户空间缓冲区)       │
│ 可移植性         │  Linux 特定               │  跨平台(POSIX/C标准)     │
│ 错误报告         │  返回负数(-errno)        │  返回 -1,设置 errno       │
│ 典型函数         │  read/write/open/close    │  fread/fwrite/fopen/fclose │
│ 头文件           │  <unistd.h> <sys/...>     │  <stdio.h> <stdlib.h>      │
│ 手册页           │  man 2                    │  man 3                     │
└─────────────────┴──────────────────────────┴──────────────────────────┘

4.2 文件 I/O 对比实战

复制代码
/* 文件名:syscall_vs_libc.c
 * 完整对比系统调用 I/O 与 stdio 库函数 I/O
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>

#define TEST_FILE "/tmp/io_compare_test.txt"
#define CONTENT   "Linux系统编程:系统调用 vs 库函数\n第二行内容\n第三行内容\n"

/* ── 方式A:系统调用 I/O ─────────────────────────────────── */
void syscall_io_demo(void) {
    printf("\n=== 系统调用 I/O(man 2)===\n");

    /* open:返回文件描述符(非负整数) */
    int fd = open(TEST_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    if (fd == -1) { perror("open"); return; }
    printf("open() 返回 fd = %d\n", fd);

    /* write:直接写入内核缓冲区 */
    ssize_t n = write(fd, CONTENT, strlen(CONTENT));
    printf("write() 写入 %zd 字节\n", n);

    /* lseek:移动文件偏移量 */
    off_t pos = lseek(fd, 0, SEEK_CUR);
    printf("当前文件偏移: %ld\n", (long)pos);

    close(fd);

    /* 重新打开读取 */
    fd = open(TEST_FILE, O_RDONLY);
    char buf[256] = {0};
    n = read(fd, buf, sizeof(buf) - 1);
    printf("read() 读取 %zd 字节:\n%s", n, buf);
    close(fd);
}

/* ── 方式B:stdio 库函数 I/O ─────────────────────────────── */
void stdio_io_demo(void) {
    printf("\n=== stdio 库函数 I/O(man 3)===\n");

    /* fopen:返回 FILE* 指针(封装了 fd + 缓冲区) */
    FILE *fp = fopen(TEST_FILE, "w");
    if (!fp) { perror("fopen"); return; }
    printf("fopen() 返回 FILE* = %p\n", (void *)fp);
    printf("对应的 fd = %d\n", fileno(fp));   /* fileno 获取底层 fd */

    /* fprintf:格式化写入(先写缓冲区) */
    int n = fprintf(fp, "%s", CONTENT);
    printf("fprintf() 写入 %d 字节(可能在缓冲区中)\n", n);

    /* ftell:获取当前位置 */
    long pos = ftell(fp);
    printf("当前文件位置: %ld\n", pos);

    fclose(fp);   /* fclose 会自动 fflush */

    /* 重新打开读取 */
    fp = fopen(TEST_FILE, "r");
    char buf[256] = {0};
    char line[64];
    int line_num = 0;
    while (fgets(line, sizeof(line), fp)) {
        printf("第%d行: %s", ++line_num, line);
    }
    fclose(fp);
}

/* ── 混合使用:FILE* 与 fd 互转 ─────────────────────────── */
void mixed_io_demo(void) {
    printf("\n=== 混合使用 FILE* 与 fd ===\n");

    /* 用系统调用打开,转为 FILE* 使用 stdio */
    int fd = open(TEST_FILE, O_RDONLY);
    FILE *fp = fdopen(fd, "r");   /* fd → FILE* */
    if (!fp) { perror("fdopen"); close(fd); return; }

    char line[64];
    if (fgets(line, sizeof(line), fp)) {
        printf("通过 fdopen 读取第一行: %s", line);
    }
    fclose(fp);   /* fclose 会同时关闭底层 fd */

    /* 用 fopen 打开,获取 fd 使用系统调用 */
    fp = fopen(TEST_FILE, "r");
    fd = fileno(fp);              /* FILE* → fd */
    char buf[32] = {0};
    lseek(fd, 0, SEEK_SET);
    read(fd, buf, 10);
    printf("通过 fileno 读取前10字节: %.10s\n", buf);
    fclose(fp);
}

int main(void) {
    syscall_io_demo();
    stdio_io_demo();
    mixed_io_demo();
    unlink(TEST_FILE);
    return 0;
}
复制代码
gcc -o syscall_vs_libc syscall_vs_libc.c
./syscall_vs_libc
# 输出示例:
# === 系统调用 I/O(man 2)===
# open() 返回 fd = 3
# write() 写入 54 字节
# 当前文件偏移: 54
# read() 读取 54 字节:
# Linux系统编程:系统调用 vs 库函数
# 第二行内容
# 第三行内容
#
# === stdio 库函数 I/O(man 3)===
# fopen() 返回 FILE* = 0x...
# 对应的 fd = 3
# fprintf() 写入 54 字节(可能在缓冲区中)
# 当前文件位置: 54
# 第1行: Linux系统编程:系统调用 vs 库函数
# 第2行: 第二行内容
# 第3行: 第三行内容

5. 综合实践

5.1 健壮的文件操作封装库

复制代码
/* 文件名:robust_io.h
 * 生产级别的 I/O 封装,处理 EINTR、部分读写等边界情况
 */
#ifndef ROBUST_IO_H
#define ROBUST_IO_H

#include <sys/types.h>

/* 健壮的 read:自动重试 EINTR,读满 count 字节或到 EOF */
ssize_t robust_read(int fd, void *buf, size_t count);

/* 健壮的 write:自动重试 EINTR,保证写完 count 字节 */
ssize_t robust_write(int fd, const void *buf, size_t count);

/* 带错误检查的 open */
int safe_open(const char *path, int flags, mode_t mode);

/* 带错误检查的 close */
int safe_close(int fd);

#endif
复制代码
/* 文件名:robust_io.c */
#include "robust_io.h"
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>

ssize_t robust_read(int fd, void *buf, size_t count) {
    char *ptr = (char *)buf;
    size_t remaining = count;
    ssize_t total = 0;

    while (remaining > 0) {
        ssize_t n = read(fd, ptr, remaining);
        if (n == 0) break;           /* EOF */
        if (n == -1) {
            if (errno == EINTR) continue;   /* 信号中断,重试 */
            return -1;                       /* 真正的错误 */
        }
        ptr       += n;
        remaining -= (size_t)n;
        total     += n;
    }
    return total;
}

ssize_t robust_write(int fd, const void *buf, size_t count) {
    const char *ptr = (const char *)buf;
    size_t remaining = count;

    while (remaining > 0) {
        ssize_t n = write(fd, ptr, remaining);
        if (n == -1) {
            if (errno == EINTR) continue;
            return -1;
        }
        ptr       += n;
        remaining -= (size_t)n;
    }
    return (ssize_t)count;
}

int safe_open(const char *path, int flags, mode_t mode) {
    int fd;
    do {
        fd = open(path, flags, mode);
    } while (fd == -1 && errno == EINTR);
    return fd;
}

int safe_close(int fd) {
    int ret;
    do {
        ret = close(fd);
    } while (ret == -1 && errno == EINTR);
    return ret;
}
复制代码
/* 文件名:robust_io_test.c
 * 测试健壮 I/O 封装库
 */
#include "robust_io.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>

/* 统一错误处理宏 */
#define CHECK(expr, msg) \
    do { \
        if ((expr) == -1) { \
            fprintf(stderr, "[%s:%d] %s: %s\n", \
                    __FILE__, __LINE__, (msg), strerror(errno)); \
            exit(EXIT_FAILURE); \
        } \
    } while (0)

int main(void) {
    const char *path = "/tmp/robust_io_test.bin";
    const char data[] = "系统调用与库函数综合测试数据\n";
    char readbuf[128] = {0};

    /* 写入测试 */
    int fd = safe_open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
    CHECK(fd, "safe_open for write");

    ssize_t n = robust_write(fd, data, strlen(data));
    CHECK((int)n, "robust_write");
    printf("写入 %zd 字节到 %s\n", n, path);
    CHECK(safe_close(fd), "safe_close after write");

    /* 读取测试 */
    fd = safe_open(path, O_RDONLY, 0);
    CHECK(fd, "safe_open for read");

    n = robust_read(fd, readbuf, sizeof(readbuf) - 1);
    CHECK((int)n, "robust_read");
    printf("读取 %zd 字节: %s", n, readbuf);
    CHECK(safe_close(fd), "safe_close after read");

    /* 验证数据一致性 */
    printf("数据一致性验证: %s\n",
           memcmp(data, readbuf, strlen(data)) == 0 ? "✓ 通过" : "✗ 失败");

    unlink(path);
    printf("\n所有测试通过!\n");
    return 0;
}
复制代码
gcc -o robust_io_test robust_io_test.c robust_io.c
./robust_io_test
# 输出:
# 写入 43 字节到 /tmp/robust_io_test.bin
# 读取 43 字节: 系统调用与库函数综合测试数据
# 数据一致性验证: ✓ 通过
#
# 所有测试通过!

5.2 系统调用性能分析脚本

复制代码
#!/bin/bash
# 文件名:syscall_profile.sh
# 功能:分析目标程序的系统调用分布

set -euo pipefail

TARGET="${1:-ls}"
echo "═══════════════════════════════════════════"
echo "  系统调用性能分析:$TARGET"
echo "═══════════════════════════════════════════"

# 使用 strace -c 统计系统调用
echo ""
echo "【系统调用统计(按耗时排序)】"
strace -c -S time "$TARGET" /usr/bin > /dev/null 2> strace_output.txt
cat strace_output.txt

# 提取调用次数最多的系统调用
echo ""
echo "【调用次数 Top 5】"
grep -v "^%" strace_output.txt | \
    grep -v "^-\|^calls\|strace\|total" | \
    awk 'NF>=5 {print $4, $NF}' | \
    sort -rn | head -5 | \
    awk '{printf "  %-20s %s 次\n", $2, $1}'

# 检查是否有失败的系统调用
echo ""
echo "【失败的系统调用】"
strace "$TARGET" /usr/bin 2>&1 | grep " = -1" | \
    awk '{print $1}' | sort | uniq -c | sort -rn | head -5 | \
    awk '{printf "  %-20s 失败 %d 次\n", $2, $1}' || \
    echo "  无失败的系统调用"

rm -f strace_output.txt
echo ""
echo "分析完成。"
复制代码
chmod +x syscall_profile.sh
./syscall_profile.sh ls

知识点总结

复制代码
第 2 章 核心知识图谱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌──────────────────────────────────────────────────────────┐
│              系统调用与库函数                              │
└────────────┬─────────────────┬────────────────────────────┘
             │                 │                    │
    ┌────────▼───────┐ ┌───────▼──────┐  ┌─────────▼──────┐
    │  syscall 机制   │ │    glibc     │  │  errno 体系     │
    └────────┬───────┘ └───────┬──────┘  └─────────┬──────┘
             │                 │                    │
    • 系统调用号       • 封装原理          • 错误码含义
    • 寄存器传参       • stdio 缓冲层      • perror/strerror
    • 用户/内核切换    • 动态链接 dlopen   • EINTR 重试
    • vDSO 优化        • fdopen/fileno     • 线程安全 TLS
    • strace 追踪      • 缓冲模式控制      • 自定义错误处理

关键对比:
  系统调用  →  man 2  →  内核态  →  fd(整数)  →  无缓冲
  库函数    →  man 3  →  用户态  →  FILE*       →  有缓冲

黄金法则:
  ① 调用失败后立即保存 errno(后续调用会覆盖)
  ② 循环处理 EINTR(或使用 SA_RESTART)
  ③ write 可能部分写入,需循环直到写完
  ④ 高频 I/O 优先用 stdio(利用缓冲减少系统调用)
  ⑤ 需要精确控制时用系统调用(如 O_SYNC、O_DIRECT)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📚 参考资料

  • man 2 syscall --- 系统调用通用接口

  • man 2 intro --- 系统调用错误处理说明

  • man 3 errno --- errno 详细说明

  • man 3 perror / man 3 strerror --- 错误报告函数

  • man 1 strace --- 系统调用追踪工具

  • 《Linux/UNIX 系统编程手册》第 3、4、5 章 --- Michael Kerrisk

  • glibc 源码:Making sure you're not a bot!

相关推荐
坤昱1 小时前
cfs调度类深入解刨——psi科普篇
linux·cfs·psi·cfs调度·eevdf·psi详细分析·linux系统资源监控
骑上单车去旅行1 小时前
openEuler 22.03 离线源码编译 Zabbix 7.0.27 完整最终整合手册
linux·运维·服务器·zabbix
向日葵.2 小时前
linux & qnx & git 命令 1
linux·运维·服务器
2023自学中2 小时前
Linux 内核与用户空间 内存管理详解(堆与栈篇)
linux·嵌入式·内存·开发板
似水এ᭄往昔2 小时前
【Linux系统编程】--虚拟地址空间
linux·服务器
不会C语言的男孩2 小时前
Linux 系统编程 · 第 3 章:文件 I/O 基础
linux·服务器
Luminous.2 小时前
C语言--day29
c语言·开发语言
十月的皮皮3 小时前
C语言学习笔记20260612-菱形图案打印(两种写法)
c语言·笔记·学习
AI科技星3 小时前
第三卷:质数王朝志(全卷定稿)
c语言·开发语言·汇编·electron·概率论