C 语言系统调用封装与错误处理
更新时间:2026-08-26。本文是
languages/c/主题专家层文档。C 程序最终都要落到系统调用上,而系统调用和"你以为的样子"常常不一样——getpid可能根本没发起 syscall、errno用错了位置就拿到错值。本文用 strace 实测把这些坑讲透。
本文要回答的问题
- 系统调用和库函数是一回事吗?
getpid()真的每次都进内核吗? errno什么时候读才对?多线程下为什么必须用errno宏而非全局变量?- 被信号打断(EINTR)的系统调用怎么处理才正确?
一、系统调用 vs 库函数
| 对比 | 系统调用(syscall) | 库函数(libc) |
|---|---|---|
| 例子 | read、write、open | printf、fread、getpid |
| 位置 | 内核态,syscall 指令陷入 | 用户态,可含缓存/缓冲 |
| 成本 | 高(模式切换 ~100ns+) | 低(纯用户态) |
| 是否必有 syscall | 是 | 不一定(可能纯用户态完成) |
关键认知:库函数是对系统调用的封装,可能做缓存、缓冲,甚至不发起真正的系统调用。
二、实测:getpid 被 glibc 缓存
写一个循环调用 getpid 100 万次,用 strace -c 统计:
for (int i = 0; i < 1000000; i++) getpid();实测结果(gcc -O2 + strace):
% time seconds usecs/call calls errors syscall
...
0.00 0.000000 0 1 getpid ← 只调用了 1 次!
...
100.00 0.000000 35 4 total100 万次 getpid(),strace 只捕获到 1 次系统调用! 因为 glibc 把 getpid() 的结果缓存了——进程 PID 不会变,第一次调用后就不再发起 syscall。
这带来一个实操结论:不要用 strace 统计"逻辑上会调 syscall"的函数,要看实际行为;反过来,像 getpid 这种被缓存的调用,性能上"零成本",不用担心。
同理被缓存/用户态优化的还有
getppid(部分版本)、time(vsyscall/vDSO 快速路径,见下文)。
三、errno 的正确读法
3.1 errno 是什么
系统调用失败时,内核通过 errno 返回错误码。库函数(如 fopen)失败时也会设置 errno:
FILE *f = fopen("/nonexistent/file.txt", "r");
if (f == NULL) {
printf("errno=%d (%s)\n", errno, strerror(errno));
// 实测输出:errno=2 (No such file or directory)
}常见错误码速查:
| errno | 宏 | 含义 |
|---|---|---|
| 2 | ENOENT | 文件不存在 |
| 13 | EACCES | 权限不足 |
| 11 | EAGAIN | 资源暂不可用(重试) |
| 4 | EINTR | 被信号打断 |
| 12 | ENOMEM | 内存不足 |
3.2 errno 的两个坑
坑 1:errno 只在失败后、下一次调用前有效
// 错误
errno = 0;
some_call();
if (errno != 0) { /* 判断 */ } // 中间若有别的调用,errno 可能被覆盖errno 是"最近一次失败"的错误码,任何成功的调用不会清零它,所以必须先判返回值、再读 errno。
坑 2:多线程下 errno 是线程局部的
errno 在现代 glibc 里是线程局部变量(每个线程独立一份)。这就是为什么必须用 #include <errno.h> 的 errno 宏,而不是自己声明 extern int errno(老代码的写法在多线程下会出错)。
四、EINTR 与重试
慢系统调用(如 read 阻塞读管道)可能被信号打断,返回 -1 且 errno=EINTR。正确处理是重试:
ssize_t ret;
do {
ret = read(fd, buf, sizeof(buf));
} while (ret == -1 && errno == EINTR); // 被信号打断,重试若 read 返回 -1 且 errno 是 EAGAIN(非阻塞 fd 无数据),则不该重试(否则忙等),应该去干别的或 poll。
五、系统调用封装的工程模式
// 模式 1:错误码包装成统一返回
int safe_read(int fd, void *buf, size_t n) {
ssize_t r;
do {
r = read(fd, buf, n);
} while (r == -1 && errno == EINTR);
if (r == -1) return -errno; // 把 errno 转成负错误码
return (int)r;
}
// 模式 2:vDSO/vsyscall 快速路径(gettimeofday 等免陷入内核)六、与本站主线衔接
- 系统调用内核机制:syscall 入口、模式切换、系统调用表,见 系统调用。
- strace 工具:观察系统调用的完整用法,见 strace。
- vDSO:gettimeofday 等免陷入的快速路径,见 系统调用详解。
- 信号:EINTR 与信号处理的关系,见 信号处理。
一句话总结
系统调用不等于库函数——getpid 被 glibc 缓存、100 万次只进 1 次内核;errno 要先判返回值再读、且多线程下是线程局部的;EINTR 要重试、EAGAIN 别忙等;看懂这些,才能写出正确又高效的系统调用封装。