1. 系统调用
以x86平台为例,分析系统调用:
- 应用程序通过调用 C 语言函数库中的外壳(wrapper)函数,来发起系统调用
- 对系统调用中断处理例程来说,外壳函数必须保证所有的系统调用参数可用。通过堆栈,这些参数传入外壳函数,但内核却希望将这些参数置入特定寄存器。因此,外壳函数会将上述参数复制到寄存器。
- 由于所有系统调用进入内核的方式相同,内核需要设法区分每个系统调用。为此,外壳函数会将系统调用编号复制到一个特殊的 CPU 寄存器(%eax)中。
- 外壳函数执行一条中断机器指令(int 0x80),引发处理器从用户态切换到核心态,并执行系统中断 0x80 (十进制数 128) 的中断矢量所指向的代码。
较新的 x86-32 硬件平台实现了 sysenter 指令,较之传统的 int 0x80 中断指令,sysenter指令进入内核的速度更快。2.6 内核及 glibc 2.3.2 以后的版本都支持 sysenter 指令。
- 为响应中断 0x80,内核会调用 system_call () 例程(位于汇编文件 arch/i386/entry.S 中)来处理这次中断,具体如下:
- 在内核栈中保存寄存器值。
- 审核系统调用编号的有效性。
- 以系统调用编号对存放所有调用服务例程的列表(内核变量 sys_call_table)进行索引,发现并调用相应的系统调用服务例程。若系统调用服务例程带有参数,那么将首先检查参数的有效性。例如,会检查地址指向用户空间的内存位置是否有效。随后,该服务例程会执行必要的任务,这可能涉及对特定参数中指定地址处的值进行修改,以及在用户内存和内核内存间传递数据(比如,在 I/O 操作中)。最后,该服务例程会将结果状态返回给 system_call () 例程。
- 从内核栈中恢复各寄存器值,并将系统调用返回值置于栈中。
- 返回至外壳函数,同时将处理器切换回用户态。
- 若系统调用服务例程的返回值表明调用有误,外壳函数会使用该值来设置全局变量 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;
}