1. 系统调用

以x86平台为例,分析系统调用:

  1. 应用程序通过调用 C 语言函数库中的外壳(wrapper)函数,来发起系统调用
  2. 对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器。
  3. 由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器(%eax)中。
  4. 外壳函数执行一条中断机器指令(int 0x80),引发处理器从用户态切换到核心态,并执行系统中断 0x80 (十进制数 128) 的中断矢量所指向的代码。

    较新的 x86-32 硬件平台实现了 sysenter 指令,较之传统的 int 0x80 中断指令,sysenter指令进入内核的速度更快。2.6 内核及 glibc 2.3.2 以后的版本都支持 sysenter 指令。

  5. 为响应中断 0x80,内核会调用 system_call () 例程(位于汇编文件 arch/i386/entry.S 中)来处理这次中断,具体如下:
    1. 在内核栈中保存寄存器值。
    2. 审核系统调用编号的有效性。
    3. 以系统调用编号对存放所有调用服务例程的列表(内核变量 sys_call_table)进行索引,发现并调用相应的系统调用服务例程。若系统调用服务例程带有参数,那么将首先检查参数的有效性。例如,会检查地址指向用户空间的内存位置是否有效。随后,该服务例程会执行必要的任务,这可能涉及对特定参数中指定地址处的值进行修改,以及在用户内存和内核内存间传递数据(比如,在 I/O 操作中)。最后,该服务例程会将结果状态返回给 system_call () 例程。
    4. 从内核栈中恢复各寄存器值,并将系统调用返回值置于栈中。
    5. 返回至外壳函数,同时将处理器切换回用户态。
  6. 若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 errno。然后,外壳函数会返回到调用程序,并同时返回一个整型值,以表明系统调用是否成功。

在 Linux 上,系统调用服务例程遵循的惯例是调用成功则返回非负值。发生错误时,例程会对相应 errno 常量取反,返回一负值。C 语言函数库的外壳函数随即对其再次取反(负负得正),将结果拷贝至 errno,同时以-1 作为外壳函数的返回值返回,向调用程序表明有错误发生。

上述惯例所依赖的前提条件是系统调用服务例程,若调用成功则不会返回负值。可是,对于少数例程来说,这一前提并不成立。一般情况下,这也不会有问题,因为取反的 errno值范围不会与调用成功返回负值的范围有交集。不过,有一种情况,沿用这个惯例确实会出问题:系统调用 fcntl () 的 F_GETOWN 操作。

2. 处理来自系统调用和库函数的错误

少数几个系统函数在调用时从不失败。例如,getpid() 总能成功返回进程的 ID,而 _exit() 总能终止进程。无需对此类系统调用的返回值进行检查。

处理系统调用错误

每个系统调用的手册页记录有调用可能的返回值,并指出了哪些值表示错误。通常,返回值为-1 表示出错。因此,可使用下列代码对系统调用进行检查:

// system call to open a file
fd = open(pathname, flags, mode);
if (fd == -1) {
    // Code to handle the error
}
// ...
if (close(fd) == -1) {
    // Code to handle the error
}

系统调用失败时,会将全局整形变量 errno 设置为一个正值,以标识具体的错误。如果调用系统调用和库函数成功,errno 绝不会被重置为 0,故此,该变量值不为 0,可能是之前调用失败造成的。因此,在进行错误检查时,必须坚持首先检查函数的返回值是否表明调用出错,然后再检查 errno 确定错误原因。

少数系统调用(比如,getpriority ())在调用成功后,也会返回−1。

系统调用失败后,常见的做法之一是根据 errno 值打印错误消息。提供库函数 perror()strerror (),就是出于这一目的。

  • 函数 perror() 会打印出其 msg 参数所指向的字符串,紧跟一条与当前 errno 值相对应的消息。
  • 函数 strerror() 会针对其 errnum 参数中所给定的错误号,返回相应的错误字符串。
#include<stdio.h>
void perror(const char *msg);

#include<string.h>
char *strerror(int errnum);

例如:

fd = open(pathname, flags, mode);
if (fd == -1) {
    perror("open");
    exit(EXIT_FAILURE);
}

3. 错误诊断函数

具体代码见项目,以下为部分函数及相关解释:

#include "tlpi_hdr.h"

void errMsg(const char *format, ...);
void errExit(const char *format, ...);
void err_exit(const char *format, ...);
void errExitEN(int errnum, const char *format, ...);
  • errMsg() :能够在标准错误设备上打印信息。会打印出与当前errno值相对应的错误文本,其中包含了错误名以及由 strerror() 返回的错误描述,外加由参数列表指定的格式化输出。
  • errExit() :操作方式与 errMsg() 相似,只是还会以如下两种方式之一来终止程序。
    • 其一,调用 exit () 退出。
    • 其二,若将环境变量 EF_DUMPCORE 定义为非空字符串,则调用 abort () 退出,同时生成核心转储(core dump)文件,供调试器调试之用。
  • err_exit() :类似于 errExit(),但存在两方面的差异
    • 打印错误消息之前,err_exit () 不会刷新标准输出。
    • err_exit () 终止进程使用的是_exit (),而非 exit ()。略去了对 stdio 缓冲区的刷新以及对退出处理程序(exit handler)的调用。
  • errExitEN() :与 errExit() 大体相同,区别仅仅在于:与 errExit() 打印与当前 errno 值相对应的错误文本不同,errExitEN() 只会打印与 errnum 参数中给定的错误号
#include"tlpi_hdr.h"

void fatal(const char *format, ...);
void usageErr(const char *format, ...);
void cmdLineErr(const char *format, ...);
  • fatal() 用来诊断一般性错误,其中包括未设置 errno 的库函数错误。除了将一个终止换行符自动追加到输出字符串尾部以外,fatal () 的参数列表与 printf () 基本相同。该函数会在标准错误上打印格式化输出,然后,像 errExit () 那样终止程序。
  • usageErr() 用来诊断命令行参数使用方面的错误。其参数列表风格与 printf () 相同,并在标准错误上打印字符串“Usage:”,随之以格式化输出,然后调用 exit () 终止程序。
  • cmdLineErr() 酷似 usageErr (),但其错误诊断是针对于特定程序的命令行参数。

3.1 tlpi_hdr.h

#ifndef TLPI_HDR_H
#define TLPI_HDR_H

#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>  // Prototypes for many system calls
#include<errno.h>
#include<string.h>

#include"get_num.h"

#include"error_functions.h"

typedef enum {
    false,
    true
} boolean;

#define min(m, n) ((m) < (n) ? (m) : (n))
#define max(m, n) ((m) > (n) ? (m) : (n))

#endif

3.2 error_functions.h

#ifndef ERROR_FUNCTIONS_H
#define ERROR_FUNCTIONS_H

// errMsg() 在标准错误设备上打印信息
// 会打印出与当前errno值相对应的错误文本
void errMsg(const char *format, ...);

#ifdef __GNUC__     // 检查是否是gcc环境
#define NORETURN __attribute__ ((__noreturn__)) // 改变函数的属性,代表这个自己编写的函数在进行GCC编译的时候声明没有返回值
#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

3.3 error_functions.c

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

#ifdef __GUNC__
__attribute__((__noreturn))
#endif

static void
terminate(boolean useExit3)
{
    char *s;

    // if EF_DUMPCORE environment variable
    // is defined and is nonempty string
    s = getenv("EF_DUMPCORE");
    if (s != NULL && *s != '\0')
        abort(); // Dump core
    else if (useExit3)
        exit(EXIT_FAILURE); // exit(3)
    else
        _exit(EXIT_FAILURE); // _exit(2)

    // _exit(2) would cause exit handlers to be invoked
}

// Diagnoes 'errno' error:
// output the error name and the corresponding error message from strerror()
// output 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 massage including 'errno', and return to caller

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

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

    errno = savedErrno;
}

// display error massage including 'errno', 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 massage including 'errno', and _exit(2)

/*
 * 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);
}

// expects the error number in 'errnum', other same as errExit()

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

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

    terminate(false);
}

// 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);
}

3.4 get_num.h

#ifndef GET_NUM_H
#define GET_NUM_H

#define GN_NONNEG 01 /* Value must be >= 0 */
#define GN_GT_0 02   /* Value must be > 0 */

/* By default, integers are decimal */
#define GN_ANY_BASE 0100 /* Can use any base - like strtol(3) */
#define GN_BASE_8 0200   /* Value is expressed in octal */
#define GN_BASE_16 0400  /* Value is expressed in hexadecimal */

long getLong(const char *arg, int flags, const char *name);
int getInt(const char *arg, int flags, const char *name);

#endif

3.5 get_num.c

#include "get_num.h"
#include <errno.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Print a diagnostic message that contains a function name ('fname'),
   the value of a command-line argument ('arg'), the name of that
   command-line argument ('name'), and a diagnostic error message ('msg'). */

static void gnFail(const char *fname, const char *msg, const char *arg, const char *name)
{
    fprintf(stderr, "%s error", fname);
    if (name != NULL)
        fprintf(stderr, " (in %s)", name);
    fprintf(stderr, ": %s\n", msg);
    if (arg != NULL && *arg != '\0')
        fprintf(stderr, "        offending text: %s\n", arg);

    exit(EXIT_FAILURE);
}

/* Convert a numeric command-line argument ('arg') into a long integer,
   returned as the function result. 'flags' is a bit mask of flags controlling
   how the conversion is done and what diagnostic checks are performed on the
   numeric result; see get_num.h for details.

   'fname' is the name of our caller, and 'name' is the name associated with
   the command-line argument 'arg'. 'fname' and 'name' are used to print a
   diagnostic message in case an error is detected when processing 'arg'. */

static long getNum(const char *fname, const char *arg, int flags, const char *name)
{
    long res;
    char *endptr;
    int base;

    if (arg == NULL || *arg == '\0')
        gnFail(fname, "null or empty string", arg, name);

    base = (flags & GN_ANY_BASE) ? 0 : (flags & GN_BASE_8) ? 8
                                   : (flags & GN_BASE_16)  ? 16
                                                           : 10;

    errno = 0;
    res = strtol(arg, &endptr, base);
    if (errno != 0)
        gnFail(fname, "strtol() failed", arg, name);

    if (*endptr != '\0')
        gnFail(fname, "nonnumeric characters", arg, name);

    if ((flags & GN_NONNEG) && res < 0)
        gnFail(fname, "negative value not allowed", arg, name);

    if ((flags & GN_GT_0) && res <= 0)
        gnFail(fname, "value must be > 0", arg, name);

    return res;
}

/* Convert a numeric command-line argument string to a long integer. See the
   comments for getNum() for a description of the arguments to this function. */

long getLong(const char *arg, int flags, const char *name)
{
    return getNum("getLong", arg, flags, name);
}

/* Convert a numeric command-line argument string to an integer. See the
   comments for getNum() for a description of the arguments to this function. */

int getInt(const char *arg, int flags, const char *name)
{
    long res;

    res = getNum("getInt", arg, flags, name);

    if (res > INT_MAX || res < INT_MIN)
        gnFail("getInt", "integer out of range", arg, name);

    return res;
}