几乎所有的系统函数和库函数在执行时都会通过返回特定的值来说明成功或出错。我们在调用它们后,必须马上对其返回值进行检测,如果调用出错则要进行相应的处理(一般是向终端输出错误信息并终止程序运行)。否则在今后程序出错时,如果通过调试去定位到该错误将会花费很长的时间。

当然也有某些系统调用从不失败(例如getpid()或_exit()等),在调用它们时可以不进行错误检测。

1.如何发现系统调用出错

  • 每个系统调用的manual page都会详细介绍可能的返回值。通常,用“-1”来表示出错,因此出错检测语句可以这样写:
fd = open(pathname, flags, mode);
if(fd == -1){
   
    /* code to handle the error */
}
...
    
if(close(fd) == -1){
   
    /* code to handle the error */
}

**注意:**不同于UNIX系统函数和C库函数,POSIX标准下的函数成功执行时,函数返回“0”,出错时设定errno为特定的非0整数值。

当一个系统调用失败时,内核会根据错误类型将error设定一个特定的非负整数。头文件<error.h>为每一个可能的error值定义了一个以“E”开头的宏名,在错误处理函数中可以通过检测这个特定的宏名来判断错误类型。

cnt = read(fd, buf, numtybes);
if(cnt == -1){
   
    if(errno == EINTR)
        fprintf(stderr, "read was interrupted by a signal\n");
    else{
   
        /*some other error occurred */
    }
}

当系统调用成功时,它并不会将error设定为0。因此,可能出现的一种情况是当前的系统函数成功执行了,但当前error值还是以前某次系统调用出错时设定的值,为此,在对error值进行检测以判定是哪种错误时,一定一定要先检查当前系统调用是否发生错误!

  • 有些系统函数在出错时并非返回“-1”,甚至在执行成功时返回“-1”。这种情况下,我们可以在系统调用前先将error设为0,待系统调用结束后,通过判定error是否为0就可知错误是否发生。

2.如何处理系统调用出错

对待系统调用出错的常见处理方法是基于error值输出不同的错误提示消息。库函数perror()strerror()提供类似的功能。

  • perror()函数先打印由用户自定义的字符串后,接着输出error值对应的错误提示字符串。
#include <stdio.h>
void perror(const char *msg);

一个使用它的简单的例子:

fd = open(pathname, flags, mode);
if(fd == -1){
   
    perror("open");
    exit(EXIT_FAILURE);
}
  • strerror()函数根据参数值返回特定的错误消息。
#include <string.h>
char *strerror(int errnum);
				//返回与errnum相应的错误消息的指针

因为strerror函数返回的错误消息字符串所在的空间是静态分配的,所以当前的错误消息会被下一次strerror函数调用所覆盖。

如果传给它的参数errnum是非法的,那么它会返回“Unknown error nnn”,在某些实现上也可能返回NULL指针。

3.介绍几种错误处理包装函数

  • 下文是即将介绍的错误处理的包装函数的头文件error_functions.h,里面声明了7个错误处理函数:
/* Func Name : error_functions.h Func Describe : Header file for error_functions.c. */
#ifndef ERROR_FUNCTIONS_H
#define ERROR_FUNCTIONS_H

/* Error diagnostic routines */

void errMsg(const char *format, ...);

#ifdef __GNUC__

    /* This macro stops 'gcc -Wall' complaining that "control reaches end of non-void function" if we use the following functions to terminate main() or some other non-void function. */

#define NORETURN __attribute__ ((__noreturn__))
#else
#define NORETURN
#endif

void errExit(const char *format, ...) NORETURN ;

void err_exit(const char *format, ...) NORETURN ;

void errExitEN(int errnum, const char *format, ...) NORETURN ;

void fatal(const char *format, ...) NORETURN ;

void usageErr(const char *format, ...) NORETURN ;

void cmdLineErr(const char *format, ...) NORETURN ;

#endif
  1. errMsg(): 向标准错误终端输出消息,格式为:“错误类型 + 错误信息 + 用户自定义消息 + 换行”。
  2. errExit(): 向标准错误终端输出消息并调用exit()函数或abort()函数(如果环境变量EF_DUMPCORE设置为非空,则会调用该函数生成核转储文件供调试用)终止程序,格式同上。
  3. err_exit(): 除了调用_exit()函数代替exit()函数和输出错误消息时不刷新标准输入输出缓存(stdio buffers),其余同errExit()函数。其主要用于当一个进程创建的一个子进程出错需要终止时,得避免子进程刷新从父进程那继承过来的stdio缓存。
  4. err_ExitEN(): 主要用于执行POSIX标准的程序出错处理,因为它们的返回值代表了errno。
/*此处不用errno的原因是errno其实代表一个宏调用, 这个宏调用会调用一个函数来返回其值,会影响运行效率。*/
int s;
s = pthread_creat(&thread, NULL, func, &arg);
if(s != 0)
    errExitEN(s, "pthread_creat");
  1. fatal(): 它可以用来诊断一般性错误,包括不设置errno值得库函数运行错误。其余同errExit()函数。
  2. usageErr(): 一般用来诊断命令行命令输入错误(参数使用),格式为“Usage: + 格式化的用户自定义字符串”,而后调用exit()函数终止程序。
  3. cmdLineErr(): 基本同上,其专指命令行错误,它的输出消息的格式为”Command-line usage error: + 格式化的用户自定义字符串“。

下面是它们的具体实现:

/* Func Name : error_functions.c Func Describe : Some standard error handling routines used by various programs. */

#include <stdarg.h>
#include "error_functions.h"
#include "tlpi_hdr.h"
#include "ename.c.inc" /* Defines ename and MAX_ENAME */

#ifdef __GNUC__ /* Prevent 'gcc -Wall' complaining */
__attribute__ ((__noreturn__))  /* if we call this function as last */
#endif /* statement in a non-void function */
static void
terminate(Boolean useExit3)
{
   
    char *s;

    /* Dump core if EF_DUMPCORE environment variable is defined and is a nonempty string; otherwise call exit(3) or _exit(2), depending on the value of 'useExit3'. */

    s = getenv("EF_DUMPCORE");

    if (s != NULL && *s != '\0')
        abort();
    else if (useExit3)
        exit(EXIT_FAILURE);
    else
        _exit(EXIT_FAILURE);
}

/* Diagnose 'errno' error by: * outputting a string containing the error name (if available in 'ename' array) corresponding to the value in 'err', along with the corresponding error message from strerror(), and * outputting the caller-supplied error message specified in 'format' and 'ap'. */

static void
outputError(Boolean useErr, int err, Boolean flushStdout,
        const char *format, va_list ap)
{
   
#define BUF_SIZE 500
    char buf[BUF_SIZE], userMsg[BUF_SIZE], errText[BUF_SIZE];

    vsnprintf(userMsg, BUF_SIZE, format, ap);

    if (useErr)
        snprintf(errText, BUF_SIZE, " [%s %s]",
                (err > 0 && err <= MAX_ENAME) ?
                ename[err] : "?UNKNOWN?", strerror(err));
    else
        snprintf(errText, BUF_SIZE, ":");

#if __GNUC__ >= 7
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wformat-truncation"
#endif
    snprintf(buf, BUF_SIZE, "ERROR%s %s\n", errText, userMsg);
#if __GNUC__ >= 7
#pragma GCC diagnostic pop
#endif

    if (flushStdout)
        fflush(stdout);       /* Flush any pending stdout */
    fputs(buf, stderr);
    fflush(stderr);           /* In case stderr is not line-buffered */
}

/* Display error message including 'errno' diagnostic, and return to caller */

void
errMsg(const char *format, ...)
{
   
    va_list argList;
    int savedErrno;

    savedErrno = errno;       /* In case we change it here */

    va_start(argList, format);
    outputError(TRUE, errno, TRUE, format, argList);
    va_end(argList);

    errno = savedErrno;
}

/* Display error message including 'errno' diagnostic, and terminate the process */

void
errExit(const char *format, ...)
{
   
    va_list argList;

    va_start(argList, format);
    outputError(TRUE, errno, TRUE, format, argList);
    va_end(argList);

    terminate(TRUE);
}

/* Display error message including 'errno' diagnostic, and terminate the process by calling _exit(). The relationship between this function and errExit() is analogous to that between _exit(2) and exit(3): unlike errExit(), this function does not flush stdout and calls _exit(2) to terminate the process (rather than exit(3), which would cause exit handlers to be invoked). These differences make this function especially useful in a library function that creates a child process that must then terminate because of an error: the child must terminate without flushing stdio buffers that were partially filled by the caller and without invoking exit handlers that were established by the caller. */

void
err_exit(const char *format, ...)
{
   
    va_list argList;

    va_start(argList, format);
    outputError(TRUE, errno, FALSE, format, argList);
    va_end(argList);

    terminate(FALSE);
}

/* The following function does the same as errExit(), but expects the error number in 'errnum' */

void
errExitEN(int errnum, const char *format, ...)
{
   
    va_list argList;

    va_start(argList, format);
    outputError(TRUE, errnum, TRUE, format, argList);
    va_end(argList);

    terminate(TRUE);
}

/* Print an error message (without an 'errno' diagnostic) */

void
fatal(const char *format, ...)
{
   
    va_list argList;

    va_start(argList, format);
    outputError(FALSE, 0, TRUE, format, argList);
    va_end(argList);

    terminate(TRUE);
}

/* Print a command usage error message and terminate the process */

void
usageErr(const char *format, ...)
{
   
    va_list argList;

    fflush(stdout);           /* Flush any pending stdout */

    fprintf(stderr, "Usage: ");
    va_start(argList, format);
    vfprintf(stderr, format, argList);
    va_end(argList);

    fflush(stderr);           /* In case stderr is not line-buffered */
    exit(EXIT_FAILURE);
}

/* Diagnose an error in command-line arguments and terminate the process */

void
cmdLineErr(const char *format, ...)
{
   
    va_list argList;

    fflush(stdout);           /* Flush any pending stdout */

    fprintf(stderr, "Command-line usage error: ");
    va_start(argList, format);
    vfprintf(stderr, format, argList);
    va_end(argList);

    fflush(stderr);           /* In case stderr is not line-buffered */
    exit(EXIT_FAILURE);
}

此处ename是一个包含所有错误类型简称字符的数组,定义于ename.c.inc文件中。其用errno值作为索引存储对应的错误类型简称。同时由于不同内核版本即架构的区别,错误类型名称即数值也不同,所以此处提供了一个脚本Build_ename.sh用以产生ename.c.inc文件。

#!/bin/sh
#
# Create a new version of the file ename.c.inc by parsing symbolic
# error names defined in errno.h
# echo '#include <errno.h>' | cpp -dM | 
sed -n -e '/#define *E/s/#define *//p' |sort -k2n |
awk '
BEGIN {
   
        entries_per_line = 4
        line_len = 68;
        last = 0;
        varname =" enames";
        print "static char *ename[] = {";
        line =  " /* 0 */ \"\"";
}
 
{
   
    if ($2 ~ /^E[A-Z0-9]*$/) {
         # These entries are sorted at top
        synonym[$1] = $2;
    } else {
   
        while (last + 1 < $2) {
   
            last++;
            line = line ", ";
            if (length(line ename) > line_len || last == 1) {
   
                print line;
                line = " /* " last " */ ";
                line = sprintf(" /* %3d */ ", last);
            }
            line = line "\"" "\"" ;
        }
        last = $2;
        ename = $1;
        for (k in synonym)
            if (synonym[k] == $1) ename = ename "/" k;
 
            line = line ", ";
            if (length(line ename) > line_len || last == 1) {
   
                print line;
                line = " /* " last " */ ";
                line = sprintf(" /* %3d */ ", last);;
            }
            line = line "\"" ename "\"" ;
    }
}
END {
   
    print  line;
    print "};"
    print "";
    print "#define MAX_ENAME " last;
}
'
  • 下面显示的数组为在环境”Linux 2.6/x86-32“环境下运行脚本产生的ename.c.inc中ename数组的值及errno最大值:

  • 有些项为空,代表其对应的errno值未使用;有些项包含两个错误名,代表这两种错误对应同一个errno值。

获取更多知识,请点击关注:
嵌入式Linux&ARM
CSDN博客
简书博客
知乎专栏