《深入理解计算机系统》读书笔记15: 附录 B 错误处理

AI2天前发布 beixibaobao
3 0 0

引言:为什么需要系统性错误处理?

作者: andylin02
学习章节: 附录 B 错误处理
关键词: 错误处理; errno; perror; strerror; 包装函数; csapp.h; Unix风格; Posix风格; DNS风格


“包装函数(Wrapper Function)是本书中几乎所有系统级代码都使用的通用错误处理技术。”——CSAPP 作者

在 CSAPP 的各个章节中,我们大量接触了 open_clientfdopen_listenfdForkPthread_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 风格用于 gethostbynamegethostbyaddr 等函数。它们在成功时返回指向结构体的指针,在失败时返回 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_errorposix_error 等函数背后的机制更重要——理解了 errnostrerror 和系统调用的一般原理,就可以在原语之上构建自己的高级函数。

下图列出了 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 专门针对网络编程中 getaddrinfogetnameinfo 等函数返回的错误码,调用 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 系统调用出错时使用(forkwaitopenreadwrite 等)
posix_error 线程相关函数(pthread_createpthread_joinpthread_mutex_lock 等)出错时
dns_error DNS 解析函数 gethostbynamegethostbyaddr 出错时
gai_error getaddrinfogetnameinfo 网络地址转换函数出错时
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 中所有系统编程示例和完成配套实验的关键基础。通过本章学习:

  1. 三种处理风格的识别:理解了 Unix 风格(-1 + errno)、Posix 风格(0/非0)和 DNS 风格(NULL + h_errno)的不同特征和适用场景
  2. 系统错误报告机制:掌握了 errnoperrorstrerrorhstrerrorgai_strerror 这些错误报告工具的正确使用
  3. 包装函数设计模式:学习了如何通过首字母大写的包装函数将错误检查与核心逻辑分离
  4. csapp.h 错误报告函数:了解了 unix_errorposix_errordns_errorgai_errorapp_error 的作用与使用
  5. csapp 库的编译配置:掌握了在编译时如何正确链接 csapp.c 与所需的线程库等

——至此,CSAPP 的核心章节与附录已全部学完,打通了从数据表示到处理器设计,再到系统级编程和网络并发等计算机系统核心领域的学习链路。


本文为个人学习笔记,仅用于知识分享。如有错误,欢迎指正。
👍🏻 点赞 + 收藏 + 分享,让更多开发者看到这篇深度解析!❤️ 如果觉得有用,请给个赞支持一下作者!

© 版权声明

相关文章