C 语言错误输出及日志
develop c
不要重复造轮子。
– 佚名
错误表述
C 语言没有异常机制。
使用异常或状态
别的编程语言的函数类似 y = f(x)
, 如果运行会出错就抛出一个异常。
C 很多本来可以没有返回值的函数有返回值 status = f(x, &y)
:
status
为 0 表示没有出错。
- 内核空间
-
status == 0
代表没有出错,status < 0
代表出错,-status
代表出错类型
-
- 用户空间
-
status == 0
代表没有出错,status > 0
代表出错。status
代表出错类型 -
status > 0
代表没有出错,status == -1
代表出错。- 通过设置
errno
来表示出错类型。用strerr(errno)
,,perror(NULL)
查看报错(单线程,多线程errno
是共享的不行)。 - 可以用非负数表示成功的某种状态(比如成功写入了多少个字符)。
- 通过设置
-
此类函数有:
-
close()
: -1 或 0 -
fclose()
: -1 或 0 -
write()
: -1 或 成功的状态 -
fwrite()
: -1 或 成功的状态 -
read()
: -1 或 成功的状态 -
fread()
: -1 或 成功的状态 -
fstat()
: -1 或 0 -
ioctl()
: -1 或 0
使用哨兵值
比如有的编程语言 y = f(x)
,如果 y
为 None
就是出错,否则就是正确的结果。
C 中可以使用 y = f(x)
其中:
- 如果
y
类型是指针,则哨兵值是NULL
- 如果
y
是int
,则哨兵值为 -1 。为此 C 还特意typedef
了ssize_t
来代表有哨兵值的size_t
。
此类函数有:
-
fopen()
:NULL
或指针 -
open()
: -1 或文件描述符fd
-
mmap()
: -1 的指针 或地址 -
malloc()
:NULL
或 指针
一般地(笔者观察的规律):
-
y
是指针的会用NULL
做哨兵值 -
y
是个结构体会用状态。 -
y
是个非负整数(例如fd
)会用 -1 做哨兵值
错误处理
无封装
因为 strerr()
会返回所有 errno
的错误信息,所以可以:
#if 0
bin="$(basename "$0")" && bin="${bin%%.*}" && cc "$0" -o"$bin" && exec ./"$bin" "$@"
#endif
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define FILE_NAME "non_existent_file"
int main(int argc, char *argv[]) {
FILE *fp = fopen(FILE_NAME, "r");
if (fp == NULL) {
fprintf(stderr, "%s: %s: %s\n", argv[0], FILE_NAME, strerror(errno));
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
$ chmod +x main.c
$ ./main.c
./main: non_existent_file: No such file or directory
这 fprintf()
明显可以封装一下。
第一次封装
stdio.h
提供封装好的 perror()
。
#if 0
bin="$(basename "$0")" && bin="${bin%%.*}" && cc "$0" -o"$bin" && exec ./"$bin" "$@"
#endif
#include <stdio.h>
#include <stdlib.h>
#define FILE_NAME "doesn't_exist_file"
int main(int argc, char *argv[]) {
FILE *fp = fopen(FILE_NAME, "r");
if (fp == NULL) {
fprintf(stderr, "%s: ", argv[0]);
perror(FILE_NAME);
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
第二次封装
如果不需要退出可以使用 perror()
。如果需要退出可以直接用 err()
。
#if 0
bin="$(basename "$0")" && bin="${bin%%.*}" && cc "$0" -o"$bin" && exec ./"$bin" "$@"
#endif
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
#define FILE_NAME "doesn't_exist_file"
int main(int argc, char *argv[]) {
FILE *fp = fopen(FILE_NAME, "r");
if (fp == NULL)
err(EXIT_FAILURE, FILE_NAME);
return EXIT_SUCCESS;
}
不过需要注意 err.h
是 POSIX C 的头文件不是 ANSI C 的。
日志
我们的更多需求:
- 每次
perror()
的信息能否保存下来以供日后查看?最好保存的时候再记录一些信息,比如时间戳、进程的 PID 等等。 - 每次
fprintf()
的信息能否设定一个等级:调试、信息、通知、警告、错误、致命错误等等?超过某个等级的信息才会被显示?
内核空间
linux/module.h
中的 printk("XXX")
的输入的第一个字符(不可见字符)用来表达等级 。如果第一个字符不是不可见字符,则使用默认的等级。
所以使用的时候经常 printk(LEVEL "XXX")
。
用户空间
POSIX C 的 syslog.h
中的 syslog(LEVEL, "XXX")
。默认不打印输出结果。日志的输出结果需要通过一个 syslogd
的守护进程记录。常见的 syslogd
包括:
-
journalctl
:systemd
自带。journalctl -fn0 -tprogram_name
。使用和systemd
一样的环境变量SYSTEMD_XXX
。例如SYSTEMD_COLORS=1
可以强制输出颜色无论输出是否是终端。 syslogd-ng