上一讲我们系统梳理了C语言的ABI兼容性,包括结构体布局、符号修饰、调用约定和跨编译器/版本的不兼容风险及如何规避。
1. 主题原理与细节逐步讲解
系统调用(system call, syscall)是应用程序与操作系统内核进行交互的标准方式。例如,文件读写(read/write)、进程管理(fork/exec)、内存分配(mmap)、网络通信(socket)等,底层都依赖系统调用。
在C语言中,系统调用通常通过标准库函数间接封装。比如fopen、fread、close等,底层会转化为open、read、close系统调用。直接调用系统调用接口时(如open、read等),需要手动处理所有出错情况,因为系统调用会因各种原因失败,并通过返回值和错误码(通常是errno)反馈详细原因。
系统调用的错误码处理机制
- 绝大多数系统调用出错时返回-1(或NULL),并设置全局变量
errno为具体出错码。 errno由操作系统维护,仅在出错时有效,成功时不应依赖其值。- 错误码是标准化整数(如
EINTR、EAGAIN、ENOMEM等),在<errno.h>中定义。 - 错误码可用
strerror(errno)或perror()输出人类可读信息。
常见的系统调用错误码
| 错误码 | 意义 |
|---|---|
EINTR | 被信号中断 |
EAGAIN | 资源暂时不可用(如非阻塞IO) |
EIO | 设备I/O错误 |
ENOMEM | 内存不足 |
EBADF | 文件描述符无效 |
EEXIST | 文件已存在 |
ENOENT | 文件或目录不存在 |
2. 典型陷阱/缺陷说明及成因剖析
2.1 忽略返回值/errno
成因: 没有检查系统调用返回值,导致未发现文件没打开、数据没写成功、资源泄漏等严重后果。
2.2 errno用法错误
成因:
- 成功调用后读取
errno,其值已无意义; - 多线程程序中未使用线程安全的
errno; errno被后续函数覆盖,误判错误来源。
2.3 未处理EINTR/EAGAIN
成因: read/write等被信号中断(EINTR)或暂时不可用(EAGAIN),未重试,导致IO提前终止或丢数据。
2.4 错误码与平台差异
成因: 错误码值/含义在不同操作系统略有差别,移植时未做兼容处理。
3. 规避方法与最佳实践
- 始终检查所有系统调用的返回值。
- 出错时立即读取并妥善处理errno,不能跨多个API后再读。
- 对于可重试错误(EINTR、EAGAIN),用循环包裹调用,直到成功或遇到不可恢复错误。
- 多线程程序使用线程局部
errno(C11 thread_local,或POSIX每线程独立errno)。 - 错误处理逻辑要清晰:统一日志、分级处理,关键路径要fail fast。
- 移植代码时,查文档确认关键错误码是否一致,必要时用条件编译兼容处理。
4. 错误代码与正确代码对比
错误示例:未检查返回值/errno
int fd = open("data.txt", O_RDONLY);
read(fd, buf, 100); // 没检查返回值
close(fd);
优化后:
int fd = open("data.txt", O_RDONLY);
if (fd == -1) {
perror("open failed");
return -1;
}
ssize_t n;
while ((n = read(fd, buf, 100)) == -1 && errno == EINTR); // EINTR处理
if (n == -1) {
perror("read failed");
close(fd);
return -1;
}
close(fd);
错误示例:跨API读取errno,导致误判
if (read(fd, buf, 100) == -1) {
some_func(); // 可能修改errno
printf("error: %s\n", strerror(errno));
}
优化后:
if (read(fd, buf, 100) == -1) {
int saved_errno = errno;
some_func();
printf("error: %s\n", strerror(saved_errno));
}
错误示例:未处理EAGAIN
n = read(fd, buf, 100);
if (n == -1) {
perror("read failed");
// 实际上非阻塞IO下,EAGAIN应重试
}
优化后:
ssize_t n;
do {
n = read(fd, buf, 100);
} while (n == -1 && (errno == EINTR || errno == EAGAIN));
if (n == -1) {
perror("read failed");
}
5. 底层原理补充
errno常为线程局部存储,防止多线程冲突(如glibc实现),但极端移植场合应确认。- 系统调用失败后,内核会返回错误码,C库将其同步到errno。
- C标准规定errno仅在出错时有效,成功时其值未定义。
- 多数系统调用返回-1并设置errno,极个别API(如返回指针)用NULL表示失败。
6. 系统调用错误处理流程

7. 总结与实际建议
- C语言系统调用后必须检查返回值并妥善处理错误码,任何疏忽都可能导致资源泄漏、数据损坏或安全风险。
errno要就地保存、及时处理,避免后续API覆盖或跨线程污染。- 可重试错误(EINTR/EAGAIN)要循环处理,保证健壮性。
- 移植与多线程环境下,要关注
errno的存储模型和错误码兼容性。 - 错误处理推荐集中日志、分级反应,便于维护和调试。
核心建议:系统调用的错误检查与errno管理是C语言高质量工程的生命线,切勿掉以轻心。优雅而严谨的错误处理是健壮C程序的基础。
公众号 | FunIO
微信搜一搜 “funio”,发现更多精彩内容。
个人博客 | blog.boringhex.top
1万+

被折叠的 条评论
为什么被折叠?



