原文:https://github.com/angrave/SystemProgramming/wiki/POSIX%2C-Part-1%3A-Error-handling
在其他语言中,您可能会看到使用异常实现的错误处理。虽然你在技术上可以在 c 中使用它们(你保留一堆非常 try / catch 块并使用setjmp
和longjmp
分别转到这些块),但是 C 中的错误处理通常是通过 posix 错误处理代码来完成的看起来像这样。
int ret = some_system_call()
if(ret == ERROR_CODE){
switch(errno){
// Do different stuff based on the errno number.
}
}
在内核中,goto
的使用被大量用于清理应用程序的不同部分。 你不应该使用 gotos 因为它们使代码更难阅读。内核中的 getos 是不必要的,所以不要上课。
POSIX 定义了一个特殊的整数errno
,它在系统调用失败时设置。 errno
的初始值为零(即没有错误)。当系统调用失败时,它通常会返回-1 表示错误并设置errno
每个线程都有自己的errno
副本。这非常有用;否则一个线程中的错误会干扰另一个线程的错误状态。
除非你专门将它重置为零,否则它不会!当系统调用成功时,他们执行 _ 而不是 _ 重置errno
的值。
这意味着如果您知道系统调用失败(例如,它返回-1),您应该只依赖于 errno 的值。
当复杂的错误处理使用库调用或系统调用可能会改变errno
的值时要小心。实际上,将 errno 的值复制到 int 变量更安全:
// Unsafe - the first fprintf may change the value of errno before we use it!
if (-1 == sem_wait(&s)) {
fprintf(stderr, "An error occurred!");
fprintf(stderr, "The error value is %d\n", errno);
}
// Better, copy the value before making more system and library calls
if (-1 == sem_wait(&s)) {
int errno_saved = errno;
fprintf(stderr, "An error occurred!");
fprintf(stderr, "The error value is %d\n", errno_saved);
}
同样,如果您的信号处理程序进行任何系统或库调用,那么最好保存 errno 的原始值并在返回之前恢复该值:
void handler(int signal) {
int errno_saved = errno;
// make system calls that might change errno
errno = errno_saved;
}
使用strerror
获取错误值的简短(英文)描述
char *mesg = strerror(errno);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, mesg);
在之前的页面中,我们使用 perror 将错误打印到标准错误。使用strerror
,我们现在可以编写perror
的简单实现:
void perror(char *what) {
fprintf(stderr, "%s: %s\n", what, strerror(errno));
}
不幸的是strerror
不是线程安全的。换句话说,两个线程不能同时调用它!
有两种解决方法:首先,我们可以使用互斥锁定义临界区和本地缓冲区。所有调用strerror
的地方的所有线程都应该使用相同的互斥锁
pthread_mutex_lock(&m);
char *result = strerror(errno);
char *message = malloc(strlen(result) + 1);
strcpy(message, result);
pthread_mutex_unlock(&m);
fprintf(stderr, "An error occurred (errno=%d): %s", errno, message);
free(message);
或者使用较不便携但线程安全的strerror_r
当信号(例如 SIGCHLD,SIGPIPE,...)传递给过程时,某些系统调用可能会中断。此时系统调用可能会返回而不执行任何操作!例如,字节可能未被读/写,信号量等待可能没有等待。
可以通过检查返回值以及errno
是否为 EINTR 来检测此中断。在这种情况下,应重试系统调用。通常会看到包含系统调用的以下类型的循环(例如 sem_wait)。
while ((-1 == systemcall(...)) && (errno == EINTR)) { /* repeat! */}
小心写== EINTR
,而不是= EINTR
。
或者,如果结果值需要稍后使用...
while ((-1 == (result = systemcall(...))) && (errno == EINTR)) { /* repeat! */}
在 Linux 上,将read
和write
调用到本地磁盘通常不会返回 EINTR(而是自动为您重新启动该功能)。但是,在对应于网络流 _ 的文件描述符上调用read
和write
可以 _ 返回 EINTR。
使用 man 页面!手册页包括可由系统调用设置的错误列表(即错误值)。经验法则是“慢”(阻塞)调用(例如,写入套接字)可能会被中断,但快速非阻塞调用(例如 pthread_mutex_lock)则不会。
来自 linux 信号 7 手册页。
“如果在阻止系统调用或库函数调用时调用信号处理程序,则:
- 信号处理程序返回后,调用自动重启;要么
- 调用失败并显示错误 EINTR。发生这两种行为中的哪一种取决于接口以及是否使用 SA_RESTART 标志建立了信号处理程序(请参阅 sigaction(2))。 UNIX 系统的细节各不相同;下面是 Linux 的详细信息。
如果对信号处理程序中断对以下某个接口的阻塞调用,则在使用 SA_RESTART 标志后,如果信号处理程序返回,则将自动重新启动该调用。否则呼叫将失败并显示错误 EINTR:
- read(2),readv(2),write(2),writev(2)和 ioctl(2)调用“慢”设备。 “慢”设备是 I / O 调用可能无限期阻塞的设备,例如终端,管道或套接字。 (根据此定义,磁盘不是慢速设备。)如果慢速设备上的 I / O 调用在信号处理程序中断时已经传输了某些数据,则该调用将返回成功状态(通常,传输的字节数)。 “
注意,很容易相信设置'SA_RESTART'标志足以使整个问题消失。不幸的是,这不是真的:仍有系统调用可能提前返回并设置EINTR
!有关详细信息,请参见信号(7)。
有些 POSIX 实用程序每个人都有自己的错误。一种是当你调用getaddrinfo
时检查错误并转换为字符串的函数是 gai_strerr 。不要混淆他们!