系统概述
syslog系统是linux操作系统的日志收集机制,内核模块和用户进程都可以用syslog机制保存各自的日志信息。
syslog系统架构如下:
内核模块保存日志信息的过程:
- 内核用printk函数将日志信息保存到_log_buf环形缓冲区中。
- klogd后台进程通过sys_syslog系统调用从_log_buf环形缓冲区中获取日志信息。
- klogd后台进程利用syslog库函数将获取的日志信息发送给syslogd后台进程。
- syslogd进程将日志信息输出到控制台或者指定的log文件。
用户进程保存日志信息的过程:
- 用户进程利用利用syslog库函数将日志信息发送给syslogd后台进程。
- syslogd进程将日志信息输出到控制台或者指定的log文件。
klogd后台进程
klogd 是一个专门截获并记录 Linux 内核消息的守护进程。
命令说明
其命令行语法如下:
klogd [ -f file ] [ -iI ] [ -n ] [ -o ] [ -p ] [ -s ] [ -k file ] [ -v ] [ -x ] [ -2 ]
命令行参数说明:
-
-f file
- 将日志直接记录到指定的file中,而不是转发到 syslogd 进程。 -i
-
要求当前正在运行的 klogd 守护进程重新装载内核符号表。
-i 用于让守护进程重新装载内核模块符号。
-I 用于让守护进程重新装载静态内核符号和内核模块符号。
-n
- 禁止自动后台运行,在 klogd 由 init 启动并直接被 init 控制的情况下必须使用此开关。 -o
- klogd 在读取并记录所有内核消息缓冲区中的消息之后立即退出(不作为守护进程)。 -p
- 只要 klogd 检测到内核消息流中包含了一个 Oops 字符串,那么就重新加载内核符号表。 -s
- 可以通过两个途径获取内核消息: /proc 文件系统和 sys_syslog 系统调用接口。虽然两者本质上完全等价,但 klogd 会优先使用 /proc/kmsg 文件。这个开关则强制 klogd 使用系统调用获取内核消息。 -k file
- 将指定的 file 作为内核符号表文件,也就是System.map文件的位置。 -v
- 打印版本信息后退出。 -x
- 忽略 EIP 转换信息,这样就不需要读取 System.map 文件。 -2
-
当展开符号时打印两行,一行将地址转换为符号,一行是原始文本。这样就允许一些外部程序(比如ksymoops)在原始数据上做一些处理。
- klogd 默认首选的获取内核消息的来源,环形缓冲区挂载位置 /var/run/klogd.pid
- 保存 klogd 的 PID 的文件 /boot/System.map, /System.map, /usr/src/linux/System.map
-I
消息转发
如果 klogd 将内核消息转发给 syslogd 进程,那么它可以分拣出某些特定的消息。原始内核消息的格式如下:
<[0-7]>Something said by the kernel.
尖括号中的数字表示内核消息的优先级,这些数字的定义位于 kernel.h 文件中。当 klogd 收到内核消息之后,将会读取这个数字,并在将此消息转发给 syslogd 时按照这个数字分配适当的优先级。
如果使用 -f 将内核消息直接记录到特定的文件中,那么这条消息将保持原样。
内核地址解析
klogd 会尝试将内核地址解析为对应的符号,如果你想得到原始的地址信息,那么可以使用"-2"开关。如果没有使用"-k"选项,那么将会依次尝试下面的路径:
/boot/System.map
/System.map
/usr/src/linux/System.map
因为内核模块动态加载所以地址并不固定,这时就要使用"-i"/"-I"通知 klogd 内核模块的变化。这两个开关都将导致当前正在运行的 klogd 守护进程重新加载内核符号表。应当在每一次加载或者卸载内核模块后立即运行下列命令:
klogd -i
-p 开关也可以用于更新内核符号表。它会让 klogd 在检测到保护性错误的时候重新加载内核符号表。使用这个开关需要小心,因为操作系统在出现保护性错误(protection fault)的时候已经变得不稳定了,而 klogd 必须执行系统调用才能重新装载内核符号表,所以这样做可能会导致更糟糕的结果。
控制台日志等级
内核默认的控制台日志等级是"7"(debug),也就是等级数字小于等于6的消息(优先级更高)都会显示在控制台上。这些不同等级所代表的意思位于 kernel.h 文件内,这个包内的syslog.h 中也有一份拷贝。可以使用 sysctl 来指定控制台日志等级,这通常是在 /etc/sysctl.conf 文件中设置的,比如下面这一行:
kernel.printk = 4 4 1 7
就是把内核的控制台日志等级设为了"4"。
信号处理
klogd 可以响应8种信号: SIGHUP, SIGINT, SIGKILL, SIGTERM, SIGTSTP, SIGUSR1, SIGUSR2, SIGCONT 。SIGINT, SIGKILL, SIGTERM, SIGHUP 信号会让进程优雅的正常退出。SIGTSTP 信号会让进程停止记录日志并进入休眠状态;SIGCONT 信号会让处于休眠状态的进程重新开始记录日志。组合使用 SIGSTOP 和 SIGCONT 可以在不退出进程的情况下切换日志消息的来源。比如需要卸载 /proc 文件系统的时候,可以使用下面的命令:
# kill -TSTP pid
# umount /proc
# kill -CONT pid
SIGUSR1 和 SIGUSR2 用于加载/重新加载内核符号表。SIGUSR1 表示重新加载内核模块的符号信息;SIGUSR2 表示同时重新加载模块和静态内核的符号信息。如果System.map 文件位置正确,那么 SIGUSR1 信号将非常有用。特别是在内核模块改变的时候。
相关文件
-
/proc/kmsg
syslogd后台进程
syslogd 默认通过 /dev/log 这个 unix domain socket 来接收应用程序发送过来的消息,这个位置是由系统的基本C库决定的。
命令说明
这个程序的命令行参数如下:
syslogd [ -a socket ] [ -f config-file ] [ -h ] [ -l hostlist ] [ -m interval ]
[ -n ] [ -p socket ] [ -r ] [ -s domainlist ] [ -v ]
参数说明:
-
-a socket
- 指定额外需要监听的 socket ,最多指定19个,可以通过修改 syslogd.c 文件中的 MAXFUNIX 宏修改这个默认值。如果你将某些进程在chroot环境下运行,那么这个选项就很有用了。 -f config-file
- 指定配置文件的位置,默认是 /etc/syslog.conf 。 -h
- 默认情况下 syslogd 并不转发它接收到的远程主机消息。指定这个选项后,进程将会把它接收到的远程主机消息转发到另一个指定的远程主机。 -l hostlist
- 指定一个分号(:)分隔的主机名列表,只记录这些主机的 hostname 而不是全限定域名。 -m interval
- syslogd 默认每隔20分钟产生一个时间戳标记(-- MARK --)。这个选项用于修改这个默认值。设为零将关闭这个特性。 -n
- 避免自动作为后台进程运行。如果由 init 来直接启动和控制的话这个选项就必须使用。 -p socket
- 你可以指定一个 unix domain socket 来代替默认的 /dev/log [这个位置是由libc决定的] -r
- 从 internet domain socket 上接收远程消息,也就是监听从514端口上进来的UDP包。 默认不接受任何远程消息。 -s domainlist
-
指定一个分号(:)分隔的域名列表,这些域名在记录前都会被剥除。只能指定完整的域名。比如"-s north.de"并不会剥除"satu.infodrom.north.de"的尾巴,你必须这样写才行:
-s north.de:infodrom.north.de
-v
- 打印版本信息后退出。
信号
在运行时,syslogd 会将自己的进程号保存在 /var/run/syslogd.pid 文件中。所以可以使用下面的命令向运行中的进程发送信号:
kill -信号 `cat /var/run/syslogd.pid`
-
HUP
- 使得 syslogd 进程重新初始化。所有打开的文件都会被关闭,然后重新读取配置文件,重新开始记录。 TERM
- 安全退出 CHLD
- 如果当前正在向所有登录的用户发送消息,那么等待这些子进程结束。
安全问题
流氓程序可以通过向 syslogd 进程发送大量日志信息来淹没日志文件或者耗尽磁盘空间。下面是一些建议:
- 通过防火墙来限制仅允许某些特定的主机访问 514/UDP socket
- 将日志文件放在独立的分区上,这样即使磁盘满了也不会有很大的影响。
- ext2文件系统可以保留一定的空间仅给root用户使用。这要求将 syslogd 以非root身份运行。
- 禁止监听 inet domain sockets 将会减少不少风险。
相关文件
-
/etc/syslog.conf
- syslogd的配置文件 /dev/log
- 默认将从这个 Unix domain socket 读取本地 syslog 消息 /var/run/syslogd.pid
-
包含 syslogd 进程号的文件
printk打印消息机制
-
函数printk的使用方法和printf相似,用于内核打印消息。printk根据日志级别对消息进行分类。
printk函数在linux2.6/kernel/printk.c中。其函数声明为:
asmlinkage int printk(const char *fmt,……);
printk(日志级别 "消息文本");这里的日志级别通俗的说指的是对文本信息的一种输出范围上的指定。
日志级别一共有8个级别,printk的日志级别定义如下(在include/linux/kernel.h中):
#define KERN_EMERG "<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/
#define KERN_ALERT "<1>"/*报告消息,表示必须立即采取措施*/
#define KERN_CRIT "<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/
#define KERN_ERR "<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/
#define KERN_WARNING "<4>"/*警告条件,对可能出现问题的情况进行警告*/
#define KERN_NOTICE "<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/
#define KERN_INFO "<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/
#define KERN_DEBUG "<7>"/*调试级别的消息*/
没有指定日志级别的printk语句默认采用的级别是 DEFAULT_ MESSAGE_LOGLEVEL(这个默认级别一般为<4>,即与KERN_WARNING在一个级别上),其定义在kernel/printk.c中可以找到。
下面是一个比较简单的使用
printk(KERN_INFO "INFO\n"); //这里可以使用数字代替 KERN_INFO,即可以写成printk(<6> "INFO\n");
在这个格式的定义中,日志级别和信息文本之间不能够使用逗号隔开,因为系统在进行编译的时候,将日志级别转换成字符串于后面的文本信息进行连接。
sys_syslog系统调用
sys_syslog函数在linux2.6/kernel/printk.c中。其函数声明为:
asmlinkage long sys_syslog(int type,char _user * buf,int len);
系统调用sys_syslog根据参数type的命令执行相应的操作。参数type定义的命令列出如下:
0:关闭日志,当前没有实现。
1:打开日志,当前没有实现。
2:从环形缓冲区读取日志消息。
3:读取保留在环形缓冲区的所有消息。
4:读取并清除保留在环形缓冲区的所有消息。
5:清除环形缓冲区。
6:关闭printk到控制台的打印。
7:开启printk到控制台的打印。
8:设置打印到控制台的消息的日志级别。
9:返回日志缓冲区中没读取的字符数。
10:返回日志缓冲区的大小。
syslog库函数
syslog库API在syslog.h文件中定义,它提供了函数openlog、syslog和closelog用来将应用程序中的日志消息写入日志系统。
- #include <stdio.h>
- #include "system.h"
- # include <syslog.h>
- static void log_su (struct passwd const *pw, bool successful)
- {
- const char *new_user, *old_user, *tty;
- new_user = pw->pw_name;
- old_user = getlogin ();//通过日志文件utmp得到登录的用户
- if (!old_user) //如果没得到用户名,就通过uid获取
- {
- struct passwd *pwd = getpwuid (getuid ());
- old_user = (pwd ? pwd->pw_name : "");
- }
- tty = ttyname (STDERR_FILENO);//通过标准描述符STDERR_FILENO得到控制台名
- if (!tty)
- tty = "none";
- // base_name (program_name)表示用程序的基本名字作为消息标签头
- openlog (base_name (program_name), 0 , LOG_AUTH ); // LOG_AUTH表示安全或授权消息
- //打印消息
- syslog (LOG_NOTICE,//消息优先级
- "%s(to %s) %s on %s",
- successful ? "" : "FAILED SU ",
- new_user,
- old_user, tty);
- closelog (); //关闭
- }