syslog
Syslog
1 内核日志基本框架
内核日志是通过printk函数实现的,它与用户空间对应的函数printf具有同样的作用。
内核创建一个名为_log_buf的Ring Buffer(环型缓冲区)来保存内核中打印的内核日志信息。
而用户态可以通过Syslog相关的系统调用或者/proc文件以及/dev/kmsg设备节点来查看_log_buf的信息,这些操作都是通过do_syslog的系统调用接口来实现的。
内核日志框架
2 Printk
在内核中,printk可以使用和prinf一样的格式将格式化消息输入到缓冲区。
Printk的格式如下:
int printk( const char * fmt, ... );
其中,fmt是一个定义文本和格式的字符串,其后面的参数列表是输出格式中需要输入的可变个数参数。
通过 printk 实现的日志是通过内核配置选项CONFIG_PRINTK**的。虽然 CONFIG_PRINTK一般都是**的,但是不包含这个选项的系统对内核的调用会返回一个ENOSYS 错误返回值。
2.1 日志级别
内核允许每一个消息根据日志级别(定义不同消息重要必的八种级别之一)来分类。这些级别可以用来判断系统是否不可用(紧急消息)、是否发现严重状况(严重消息)或者是否为简单报告消息。这个内核代码直接将日志级别定义消息的第一个参数。
例如下面的代码:
printk( KERN_CRIT "Error code %08x.\n", val );
KERN_CRIT只是一个普通的字符串,作为预处理程序的一部分,C会自动地使用一个名为 字符串串联的功能将这两个字符串组合在一起。组合的结果是将日志级别和用户指定的格式字符串包含在一个字符串中。如果调用者未将日志级别提供给printk,那么系统就会使用默认值KERN_WARNING(表示只有KERN_WARNING 级别以上的日志消息会被记录。)
内核的日志级别包括:
标识符 | 字符串 | 使用方法 |
KERN_EMERG | <0> | 紧急消息(导致系统崩溃) |
KERN_ALERT | <1> | 必须立即处理的错误 |
KERN_CRIT | <2> | 严重错误(硬件或软件) |
KERN_ERR | <3> | 错误状况(一般出现在驱动程序上) |
KERN_WARNING | <4> | 警告状况(可能导致错误) |
KERN_NOTICE | <5> | 不是错误,但是一个重要状况 |
KERN_INFO | <6> | 报告消息 |
KERN_DEBUG | <7> | 仅用于调试的消息 |
KERN_DEFAULT | <d> | 默认内核日志级别 |
KERN_CONT | <c> | 日志行继续(避免增加新的时间截) |
2.2 Printk的实现
2.2.1 日志缓冲区_log_buf
内核中,printk将日志信息打印到环型缓冲区(Ring Buffer)_log_buf中。
环形缓冲区__log_buf在使用之前就是已定义好的全局变量,缓冲区的长度为1 <<CONFIG_LOG_ BUF_SHIFT。变量CONFIG_LOG_BUF_SHIFT在内核编译时由配置文件定义,一般为18。在内核编译时,编译器根据配置文件的设置,产生如下的宏定义:
#define CONFIG_LOG_BUF_SHIFT 18
环型缓冲区的定义如下:
2.2.2 主要数据结构
1、Cont:
Cont结构用来保存多行需要打印在一起的信息的,使这些信息不会因为外部的进程或者其他因素致使这些信息不能连续打印。
这里的len字段表示当前cont中的已有信息的长度,如果len==0表示没有要连续打印的多行数据。
Owner字段表示cont是属于哪个进程的,如果进程不同,那么多行打印的数据不能同时保存在cont中。
Ts_nsec是第一次加入的日志信息的时间戳,level是级别,flushed表示已经疏导log_buf中。
2、log
保存当前这条日志的头信息,这些头信息是内核的管理数据,并不会通过print打印到用户态的终端上,而是会通过/dev/kmsg输出。
3、log_flags
这四个flag用来控制日志打印的方式。
4、buffer控制字段
分别记录log_buf的头尾的计数和索引。
2.2.3 Printk的处理流程
主要操作步骤为:
1) va_start获取参数。以下的操作通过vprintk_emit函数实现。
2) 调用函数boot_delay_msec()忙等待一段时间,这段时间的大小时由内核启动参数boot_delay指定的;
3) 获取当前CPU的编号;
4) vscnprintf()函数,将输出的字符串按fmt中的格式编排好,放入text中,并返回应该输出的字符的个数;
5) 如果日志信息的最后一个字符是\n,表示该日志之后要新起一行,将lflags加上LOG_NEWLINE标识;
6) 从text中取出输入的日志的级别level;
7) 如果lflags没有LOG_NEWLINE标志,如果cont中有信息(前一次),并且新的信息有前缀或者新的信息是另外的进程打印的,这是需要将cont中的信息写到log_buf中。否则,将需要打印的新信息加入到cont中,如果添加失败(主要由于cont的可用大小不够),直接调用log_store将text信息添加到log_buf中;
8) Lflags有LOG_NEWLINE标志,如果cont中有前一次的信息,这个信息也是属于当前进程的日志,如果新加入的信息没有前缀,则直接将信息加到cont中,然后再将cont刷到缓冲区。如果信息没有被加入到cont中,那么直接调用log_store将text信息添加到log_buf中
9) 获取终端信号量,打印终端。
2.3 Printk与终端console
Printk首先将要打印的信息放到buffer中,然后调用release_console_sem最后调用到相关驱动的write函数,如果你设定了CONFIG_CMDLINE="console=ttySL0,19200,那么printk信息就会调用ttySL这个驱动的write函数,也就是从串口输出数据了。在__call_console_drivers里面有一个很重要的变量console_drivers,它决定调用哪支driver输出printk信息。
为了更好的控制不同级别的日志显示在终端上,内核设置了控制台的日志级别,console_loglevel。printk日志级别的作用是打印一定级别的消息,与之类似,控制台只显示一定级别的消息。
当日志级别小于console_loglevel时,消息才能显示出来。
3 Syslog
3.1 Syslog系统调用
Syslog系统调用(在内核中调用 ./linux/kernel/printk.c 的 do_syslog)是一个相对较小的函数,它能够读取和控制内核环缓冲区。注意在 glibc 2.0 中,由于词汇 syslog 使用过于广泛,这个函数的名称被修改成 klogctl,它指的是各种调用和应用程序。syslog 和 klogctl(在用户空间中)的原型函数定义为:
int syslog( int type, char *bufp,int len );
int klogctl( int type, char *bufp, int len );
type参数用来传递所要执行的命令,其主要的参数值包括:
命令/操作代码 | 作用 |
SYSLOG_ACTION_CLOSE (0) | 关闭日志(未实现) |
SYSLOG_ACTION_OPEN (1) | 打开日志(未实现) |
SYSLOG_ACTION_READ (2) | 从日志读取 |
SYSLOG_ACTION_READ_ALL (3) | 从日志读取所有消息(非破坏地) |
SYSLOG_ACTION_READ_CLEAR (4) | 从日志读取并清除所有消息 |
SYSLOG_ACTION_CLEAR (5) | 清除环缓冲区 |
SYSLOG_ACTION_CONSOLE_OFF (6) | Disable printks to the console |
SYSLOG_ACTION_CONSOLE_ON (7) | **控制台printk |
SYSLOG_ACTION_CONSOLE_LEVEL (8) | 将消息级别设置为控制接受 |
SYSLOG_ACTION_SIZE_UNREAD (9) | 返回日志中未读取的字符数 |
SYSLOG_ACTION_SIZE_BUFFER (10) | 返回内核环缓冲区大小 |
3.2 do_syslog
3.2.1 syslog操作
SYSLOG_ACTION_READ
1、 如果log_buf中的数据都已经读完,则阻塞进程,将进程放入log_wait等待队列中;
2、 调用syslog_print打印局部信息。
SYSLOG_ACTION_READ_ALL
调用syslog_print_all打印全部信息。
SYSLOG_ACTION_CLEAR
调用syslog_print_all函数清空数据。
3.2.2 syslog_print
根据参数,向相应的buf中写入要输出的局部日志信息。
3.2.3 syslog_print_all
1、 根据参数,向相应的buf中写入要输出全部日志信息。
2、 参数clear为ture,需要将clear的索引和计数指向log_buf的尾部。
4 /proc/kmsg
1、/proc/kmsg初始化proc_kmsg_init中通过proc文件系统文件创建的接口proc_create创建proc文件类型,并注册对应的file_operations的proc_kmsg_operations。
2、proc_kmsg_operations的定义如下:
kmsg_open、kmsg_release、kmsg_read、kmsg_poll的实现都是通过给do_syslog的接口传递不同的参数来实现的。如下:
5 Syslog隔离方案
5.1 架构图
- 在容器环境下实现syslog的容器隔离,从而在一个容器中只能看到当前容器中的进程执行所产生的系统日志信息。
- 容器中的系统日志用户态接口和内核态的系统调用API等使用接口使用内核原有的实现。
- 进程上下文的nsproxy信息中添加syslog的命名空间信息,每一个syslog命名空间包含仅属于自己的日志缓冲区。
- 当内核通过printk、do_syslog以及/dev处理系统日志时,通过当前进程上下文的nsproxy信息获取该容器中进程所属的syslog命名空间,从而对属于这个命名空间的系统日志缓冲区读写等操作。
5.2 隔离数据
1、Log_buf
内核实现syslog的主要数据结构就是log_buf这个Ring Buffer,因此要实现syslog的隔离,主要要实现包含一个log_buf的命名空间。这个命名空间包含log_buf的控制参数。
对于log_buf的控制参数包括:
2、console
向终端写入数据时通过console_seq、console_idx控制在缓冲区log_buf的索引和计数,针对隔离的log_buf需要有对应的控制标识符。
3、syslog/kmsg
通过syslog(/proc/kmsg)读log_buf时,同样由syslog_idx、syslog_seq、syslog_prev、syslog_partial这样的参数来控制,因此对于隔离的log_buf,这些参数也要隔离。
4、/dev/kmsg
对设备节点的操作,通过直接操作log_buf来获取日志信息,实现kmsg_fops中的devkmsg_open、devkmsg_read、 devkmsg_writev、devkmsg_llseek、
devkmsg_poll、devkmsg_release不需要隔离相关控制字段。
5、log_wait
通过/dev/kmsg、syslog或者/proc/kmsg从log_buf中获取日志时,内核通过建立一个等待队列来唤醒因为暂时没有需要读出日志信息而阻塞的进程。这个结构同log_buf强相关,针对每一个命名空间中的log_buf,对应要有一个这个队列。
以上是syslog隔离过程中需要隔离的主要数据和字段。在操作这些数据和字段值时,都要从对应的命名空间中获取相应数据。