《深入理解计算机系统》读书笔记15: 附录 B 错误处理
引言:为什么需要系统性错误处理?
作者: andylin02
学习章节: 附录 B 错误处理
关键词: 错误处理; errno; perror; strerror; 包装函数; csapp.h; Unix风格; Posix风格; DNS风格
“包装函数(Wrapper Function)是本书中几乎所有系统级代码都使用的通用错误处理技术。”——CSAPP 作者
在 CSAPP 的各个章节中,我们大量接触了 open_clientfd、open_listenfd、Fork、Pthread_create 等形如“首字母大写”的函数调用。这些函数正是附录 B 所阐释的错误处理包装函数的体现。它们在底层完成对实际函数的调用的同时,也完成了系统的错误检查处理工作,让我们的核心代码变得更简洁、更可靠。
正如本书所做的,明确对错误进行处理,并利用包装函数(wrapper function)将错误处理与核心逻辑分离,是在掌握 Unix/Linux 系统编程时的关键一步。
附录B与其他章节的关联:
- 与第8章异常控制流/第10章系统级I/O紧密相关——大量系统调用都需要错误检查和包装
- 与第11章网络编程关联——
gai_error专门处理getaddrinfo等 DNS 相关错误 - 贯穿全书所有实验(Shell Lab、Proxy Lab、Malloc Lab 等)
本章结构速览:
- B.1 Unix 系统中的错误处理:三种错误报告风格详解
- B.2 错误处理包装函数:包装函数的设计与实现
- B.3 csapp.h 中的错误报告函数:函数清单与使用示例
一、Unix 系统中的错误处理
在使用 Unix 系统级函数时,返回值是我们判断函数调用是否成功的主要依据。这些函数按照其返回值特征和错误报告方式可以分为三种不同类型。附录 B 将其归纳为三种风格:
1.1 三种错误处理风格的对比
┌─────────────────────────────────────────────────────────────────────┐
│ 三种错误处理风格的对比表 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Unix风格 Posix风格 DNS风格 │
│ (以fork/wait为例) (以pthread系列为例) (以gethostbyname为例)│
│ │
│ 成功: 返回有用结果 成功: 返回0 成功: 返回指针 │
│ 失败: 返回-1 失败: 返回非0 失败: 返回NULL │
│ errno被设置 错误码直接返回 h_errno被设置 │ │
│ │
│ 💡 工具函数: 💡 工具函数: 💡 工具函数: │
│ strerror(errno) strerror(rc) hstrerror(h_errno) │
│ perror(msg) │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 Unix 风格错误处理
Unix 风格的函数在成功时返回一个有用的结果(如文件描述符、进程 ID),在失败时返回 -1,并将全局变量 errno 设置为某个常量,以指示具体的错误原因。
// Unix 风格的错误处理示例(无包装)
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
if ((pid = wait(NULL)) < 0) {
fprintf(stderr, "wait error: %sn", strerror(errno));
exit(0);
}
return 0;
}
常用错误处理函数:
| 函数 | 原型 | 作用 |
|---|---|---|
perror |
void perror(const char *s); |
打印用户提供的字符串,后跟冒号、空格以及当前 errno 对应的错误描述 |
strerror |
char *strerror(int errnum); |
返回 errnum(通常是 errno 的值)对应的错误描述字符串 |
两者的核心区别在于:perror 直接将错误信息输出到标准错误(stderr),而 strerror 只返回描述字符串,允许开发者自定义输出行为。
💡 经验法则:Unix 风格的每个系统调用都应该检查返回值。工程实践中,应在错误返回后立即检查
errno,因为后续成功的系统调用可能会覆盖它。
1.3 Posix 风格错误处理
Posix 风格的函数只使用返回值来表示成功(0)或失败(非 0)。任何真正有含义的结果(如指向线程 ID 的指针)都通过函数参数(以引用方式传递)返回。
// Posix 风格的错误处理示例(无包装)
#include <stdio.h>
#include <string.h>
#include <pthread.h>
int main() {
pthread_t tid;
int retcode;
if ((retcode = pthread_create(&tid, NULL, thread, NULL)) != 0) {
fprintf(stderr, "pthread_create error: %sn", strerror(retcode));
exit(0);
}
return 0;
}
⚠️ 注意:Posix 函数的返回值即错误码,所以
strerror应该传入这个返回值,而不是全局errno。
1.4 DNS 风格错误处理
DNS 风格用于 gethostbyname 和 gethostbyaddr 等函数。它们在成功时返回指向结构体的指针,在失败时返回 NULL,并设置全局变量 h_errno。
// DNS 风格的错误处理示例(无包装)
#include <stdio.h>
#include <netdb.h>
int main() {
struct hostent *p;
char name[MAXLINE];
if ((p = gethostbyname(name)) == NULL) {
fprintf(stderr, "gethostbyname error: %sn", hstrerror(h_errno));
exit(0);
}
return 0;
}
💡 DNS 风格函数需要调用
hstrerror(h_errno)来获取可读的错误信息,而非strerror(errno)。
二、错误处理包装函数
2.1 包装函数的设计思想
直接从上面三种风格的代码中判断错误并进行处理是非常繁琐的,其代码会显得臃肿且可读性差。附录 B 提出了一种通用的处理方法:错误处理包装函数(Error-handling Wrapper Function)。
给定任何一个系统级函数 foo(任何错误风格),我们定义一个具有相同参数、但首字母大写的包装函数 Foo,它通过调用基本函数来检查错误。如果发现错误,则打印错误信息并终止进程;否则返回调用者。
包装函数封装在源文件 csapp.c 中,通过独立的头文件 csapp.h 提供给各个程序使用。这些函数包含了 Unix 文件 I/O、套接字、信号、线程和信号量的实用函数。
2.2 Unix 风格的包装函数示例:Wait
// 错误报告函数的通用基础
void unix_error(char *msg) {
fprintf(stderr, "%s: %sn", msg, strerror(errno));
exit(0);
}
// Unix 风格包装函数示例:Wait(首字母大写)
pid_t Wait(int *status) {
pid_t pid;
if ((pid = wait(status)) < 0)
unix_error("Wait error");
return pid;
}
// 未包装的代码
if ((pid = wait(NULL)) < 0) {
fprintf(stderr, "wait error: %sn", strerror(errno));
exit(0);
}
// 使用包装后的代码
pid = Wait(NULL);
2.3 Posix 风格的包装函数示例:Pthread_create
// Posix 错误报告函数
void posix_error(int code, char *msg) {
fprintf(stderr, "%s: %sn", msg, strerror(code));
exit(0);
}
// Posix 风格包装函数示例
void Pthread_create(pthread_t *tid, const pthread_attr_t *attr,
void *(*func)(void *), void *arg) {
int rc;
if ((rc = pthread_create(tid, attr, func, arg)) != 0)
posix_error(rc, "Pthread_create error");
}
2.4 DNS 风格的包装函数示例:Gethostbyname
// DNS 错误报告函数
void dns_error(char *msg) {
fprintf(stderr, "%s: %sn", msg, hstrerror(h_errno));
exit(0);
}
// DNS 风格包装函数示例
struct hostent *Gethostbyname(const char *name) {
struct hostent *p;
if ((p = gethostbyname(name)) == NULL)
dns_error("Gethostbyname error");
return p;
}
2.5 包装函数的定位
对于实际掌握系统编程的读者,包装函数在工程上是非常方便的。但对于理解错误处理的本质,unix_error 和 posix_error 等函数背后的机制更重要——理解了 errno、strerror 和系统调用的一般原理,就可以在原语之上构建自己的高级函数。
下图列出了 csapp.h 和附录 B 中出现的所有错误报告函数及其适用风格:
| 函数原型 | 适用风格 | 说明 |
|---|---|---|
void unix_error(char *msg); |
Unix | 打印 errno 对应信息并退出 |
void posix_error(int code, char *msg); |
Posix | 接收错误码 code,打印信息并退出 |
void dns_error(char *msg); |
DNS | 打印 h_errno 对应信息并退出 |
void gai_error(int code, char *msg); |
DNS/getaddrinfo | 处理 getaddrinfo 系列相关错误,使用 gai_strerror
|
void app_error(char *msg); |
应用层 | 打印用户传入的字符串并退出 |
其中 gai_error 专门针对网络编程中 getaddrinfo、getnameinfo 等函数返回的错误码,调用 gai_strerror 而非标准 strerror 来获取错误描述。app_error 则是为应用层错误退出的简便接口。
2.6 csapp 库的编译与配置示例
在实际实验中使用 csapp.c 时,需要链接 pthread 库(信号量函数可能还需 rt 库)。以下是三种编译配置的示例:
# 示例1:直接编译(简单使用)
gcc -O2 -lpthread -o sample sample.c csapp.c
# 示例2:仅当需要信号量但目标系统需单独指定运行库时(按需)
gcc -O2 -lpthread -lrt -o sample sample.c csapp.c
# 示例3:预编译 csapp.o 来加速后续编译
gcc -O2 -c csapp.c # 生成 csapp.o
gcc -O2 -lpthread -o sample sample.c csapp.o # 再与目标文件链接
# Makefile 示例(省略了示例4/5的多个程序场景)
sample: sample.c csapp.o
gcc -O2 -lpthread -o sample sample.c csapp.o
csapp.o: csapp.c
gcc -O2 -c csapp.c
2.7 为什么需要包装函数
| 优点 | 说明 |
|---|---|
| 简化代码 | 将 4 行错误处理缩减为 1 行函数调用 |
| 提高可读性 | 核心逻辑与错误处理分离 |
| 标准化 | 统一错误报告格式,便于调试和维护 |
| 支持多种风格 | 统一了 Unix、Posix、DNS 三种错误处理风格 |
| 便于调试 | 所有错误信息集中在一处,包含文件名与行号 |
三、csapp.h 错误处理函数清单
结合搜索结果与附录 B 的内容,csapp.h 中定义的主要错误报告函数汇总如下:
3.1 各风格专用函数
/* $begin errorhandling */
/* Unix-style error */
void unix_error(char *msg) {
fprintf(stderr, "%s: %sn", msg, strerror(errno));
exit(0);
}
/* Posix-style error */
void posix_error(int code, char *msg) {
fprintf(stderr, "%s: %sn", msg, strerror(code));
exit(0);
}
/* DNS-style error (h_errno) */
void dns_error(char *msg) {
fprintf(stderr, "%s: %sn", msg, hstrerror(h_errno));
exit(0);
}
/* Getaddrinfo-style (gai_strerror) */
void gai_error(int code, char *msg) {
fprintf(stderr, "%s: %sn", msg, gai_strerror(code));
exit(0);
}
/* Application error */
void app_error(char *msg) {
fprintf(stderr, "%sn", msg);
exit(0);
}
/* $end errorhandling */
3.2 使用场景对照
这些函数在 CSAPP 全书中的典型应用场景如下:
| 函数 | 典型调用场景 |
|---|---|
unix_error |
所有标准 Unix 系统调用出错时使用(fork、wait、open、read、write 等) |
posix_error |
线程相关函数(pthread_create、pthread_join、pthread_mutex_lock 等)出错时 |
dns_error |
DNS 解析函数 gethostbyname、gethostbyaddr 出错时 |
gai_error |
getaddrinfo、getnameinfo 网络地址转换函数出错时 |
app_error |
应用层自定义错误(如用户输入非法、业务逻辑错误),不经过系统调用 |
下图展示了本书实验(如 Shell Lab)中 csapp.c 提供的带错误检查的包装函数的典型调用上下文:
┌─────────────────────────────────────────────────────────────────────┐
│ 包装函数的使用场景 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 应用代码层 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ main() { │ │
│ │ pid_t pid = Fork(); ← 调用包装函数 │ │
│ │ Wait(NULL); ← 调用包装函数 │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ 包装函数层(csapp.c) │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ pid_t Fork(void) { │ │
│ │ pid_t pid; │ │
│ │ if ((pid = fork()) < 0) │ │
│ │ unix_error("Fork error"); ← 调用错误报告函数 │ │
│ │ return pid; │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ↓ │
│ 基础错误报告函数 │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ void unix_error(char *msg) { │ │
│ │ fprintf(stderr, "%s: %sn", msg, strerror(errno)); │ │
│ │ exit(0); │ │
│ │ } │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
四、本章知识点思维导图
附录 B 错误处理
│
├── 1. Unix 系统的三种错误处理风格
│ ├── Unix 风格(fork/wait/open/read/write)
│ │ ├── 成功:返回有用结果
│ │ ├── 失败:返回 -1,设置 errno
│ │ └── 工具:strerror(errno) / perror(msg)
│ ├── Posix 风格(pthread_create/pthread_join)
│ │ ├── 成功:返回 0
│ │ ├── 失败:返回非 0(错误码直接返回)
│ │ └── 工具:strerror(code)
│ └── DNS 风格(gethostbyname/gethostbyaddr)
│ ├── 成功:返回指针
│ ├── 失败:返回 NULL,设置 h_errno
│ └── 工具:hstrerror(h_errno)
│
├── 2. 错误处理包装函数
│ ├── 设计思想:封装错误检查,首字母大写
│ ├── 示例:
│ │ ├── pid_t Fork(void);
│ │ ├── void Pthread_create(...);
│ │ └── struct hostent *Gethostbyname(...);
│ └── 优点:简化代码、提高可读性、标准统一
│
├── 3. csapp.h 错误报告函数
│ ├── unix_error(char *msg)
│ ├── posix_error(int code, char *msg)
│ ├── dns_error(char *msg)
│ ├── gai_error(int code, char *msg)
│ ├── app_error(char *msg)
│ └── 根据错误风格选择合适的函数
│
└── 4. csapp 库的编译与引用
├── csapp.h / csapp.c —— 需要同步编译或提前构建 csapp.o
├── 需额外链接 -lpthread 库、-lrt 库(按目标系统决定)
└── Makefile 可简化重复编译步骤
五、本章小结
附录 B 短小精悍,却是理解 CSAPP 中所有系统编程示例和完成配套实验的关键基础。通过本章学习:
- ✅ 三种处理风格的识别:理解了 Unix 风格(-1 + errno)、Posix 风格(0/非0)和 DNS 风格(NULL + h_errno)的不同特征和适用场景
- ✅ 系统错误报告机制:掌握了
errno、perror、strerror、hstrerror、gai_strerror这些错误报告工具的正确使用 - ✅ 包装函数设计模式:学习了如何通过首字母大写的包装函数将错误检查与核心逻辑分离
- ✅ csapp.h 错误报告函数:了解了
unix_error、posix_error、dns_error、gai_error、app_error的作用与使用 - ✅ csapp 库的编译配置:掌握了在编译时如何正确链接
csapp.c与所需的线程库等
——至此,CSAPP 的核心章节与附录已全部学完,打通了从数据表示到处理器设计,再到系统级编程和网络并发等计算机系统核心领域的学习链路。
本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!