Linux 系统编程 · 第 2 章:系统调用与库函数
本章深入剖析系统调用机制、glibc 封装原理与 errno 错误处理体系,是 Linux 系统编程的基石。
目录
-
[C 标准库与 glibc](#C 标准库与 glibc)
-
[errno 错误处理体系](#errno 错误处理体系)
-
[系统调用 vs 库函数对比](#系统调用 vs 库函数对比)
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_gettime、gettimeofday等高频调用的实现映射到用户空间,程序直接在用户态执行,避免了特权级切换,性能提升约 10~20 倍。
2. C 标准库与 glibc
2.1 glibc 概述
glibc(GNU C Library) 是 Linux 上最主要的 C 标准库实现,提供:
-
POSIX API 封装 :对系统调用的封装(
open、read、write...) -
标准 C 函数 :
printf、malloc、strcpy、qsort... -
线程支持: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 的 stdio(FILE * 接口)在系统调用之上增加了用户空间缓冲区:
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!