错误处理与包裹函数
参考书籍:《UNIX网络编程》《UNIX环境高级编程》
errno
只要在一个 系统调用 中有错误发生,全局变量 errno 就会被自动设置为一个特定的正值,用来反馈具体错误,而函数本身则会返回 -1,以说明函数发生了错误;如果函数返回正值,即没有错误发生,则 errno 没有定义。errno 包含在 errno.h
中。
-
如果函数没有出错,则之前的 errno 可能不会被清除(未定义);只有发生错误时,才会覆盖之前的错误。
-
errno 不会为 0,这与多线程的 errno 处理有关。
这是因为:线程函数(以 pthread_ 开头的函数)遇到错误时不会设置标准 Unix 的 errno 变量,而是将 errno 的值以函数返回值的形式交给调用者。也就是说,返回值大于 0 则说明发生了错误,那没有发生错误呢?自然也就是返回 0 了。
1
2
3
4int n;
if((n=pthread_mutex_lock(&ndoen_mutex))!=0){
fprintf(stderr,"error:%s",strerror(n));
}你看,这意味着我们每次调用 pthread_ 函数时,都要事先分配一个整形来保存错误值,这很麻烦,所以我们可以把错误处理和 pthread_ 函数包裹起来以简化代码:
1
2
3
4
5void Pthread_mutex_lock(pthread_mutex_t *mptr){
int n;
if((n=pthread_mutex_lock(mptr))!=0)
fprintf(stderr,"error:%s",strerror(n));
} -
虽然 errno 是一个全局变量,但是在多线程环境中,每个线程都会有自己独立的 errno 副本,这是通过线程本地存储(Thread-Local Storage,TLS)实现的,因此一般不用担心多线程会相互竞争 errno,可参见《APUE》P358
-
虽然在多线程下不用担心 errno 的竞争问题,不过单线程下 errno 仍可能出现问题,比如在信号处理函数中被修改 。当发生信号时,执行流会跳转到信号处理函数,这感觉就像是多线程,但实际上它和之前的执行流位于同一个上下文,也就是说信号处理函数并不是新开的线程。因此,如果之前的执行流在系统调用出错后修改 errno,接着被信号中断,进入了信号处理函数,而信号处理函数中也调用了某些系统函数如 write,如果此时这个系统调用出错,那么 errno 就会被修改,这就使得之前的 errno 被覆盖! 因此,作为一个通用的规则: 在信号处理函数中,应首先保存 errno,退出时再恢复:
1
2
3
4
5
6
7void sig_alarm(int signo)
{
int errno_cpy = errno;
//do something...
write(....);
errno = errno_cpy;
}
perror、strerror
C 库函数 void perror(const char *str) 把一个描述性错误消息输出到标准错误 stderr,其中 str 是自定义内容。该函数先输出 str,再输出错误描述符:
1 | if(-1 == connect(fd, addr, len)) |
C 库函数 char *strerror(int errnum) 从内部数组中搜索错误号 errnum,并返回一个指向错误消息字符串的指针。
1 | if(-1 == connect(fd, addr, len)) |
包裹函数
网络编程很多时候都会遇到一些网络问题,并通过函数返回值或 errno 反馈错误,因此绝不能忽略对错误的处理。包裹函数一般用来处理致命性错误,此时能干的也就只有打印错误然后退出,对于非致命性错误,比如 EINTR 错误,就需要我们自己来处理失败情况:
1 | while(1){ |
这里我们需要自己重启被中断的系统调用。关于中断的系统调用,参见《APUE》第 260 页。
一些包裹函数如下:
1 | void Bind(int fd, const struct sockaddr *sa, socklen_t salen) |
其他包裹函数可参考UNP-sockwarp.c