#define KERN_EMERG "<0>" /* 系统不可使用 */
#define KERN_ALERT "<1>" /* 需要立即采取行动 */
#define KERN_CRIT "<2>" /* 严重情况 */
#define KERN_ERR "<3>" /* 错误情况 */
#define KERN_WARNING "<4>" /* 警告情况 */
#define KERN_NOTICE "<5>" /* 正常情况, 但是值得注意 */
#define KERN_INFO "<6>" /* 信息型消息 */
#define KERN_DEBUG "<7>" /* 调试级别的信息 */
printk 可以在内核的任意上下文中调用。这个调用从 ./linux/kernel/printk.c 中的 printk 函数开始,它会在使用 va_start 解析可变长度参数之后调用 vprintk(在同一个源文件)。
vprintk 函数执行了许多管理级检查(递归检查),然后获取日志缓冲区的锁(__log_buf)。接下来,它会对输入的字符串进行日志级别检查; 如果发现日志级别信息,那么对应的日志级别就会被设置。最后,vprintk
会获取当前时间(使用函数 cpu_clock)并使用 sprintf
(不是标准库版本,而是在 ./linux/lib/vsprintf.c 中实现的内部内核版本)将它转换成一个字符串。这个字符串会被传递给 printk,然后它会被一个管理缓冲边界(emit_log_char)的特殊函数复制到内核日志缓冲区中。这个函数最后将获取和释放执行控制台信号,并将下一条日志消息发送到控制台(在 release_console_sem 中执行)。内核缓冲缓冲区的大小初始值为 4KB,但是最新的内核大小已经升级到 16KB(在不同的体系架构上,这个值最高可以达到 1MB)。
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
/*
*启动KDB调试
*/
#ifdef CONFIG_KGDB_KDB
if (unlikely(kdb_trap_printk)) {
va_start(args, fmt);
r = vkdb_printf(fmt, args);
va_end(args);
return r;
}
#endif
va_start(args, fmt);
r = vprintk(fmt, args);//printk函数的核心步骤
va_end(args);
return r;
}
asmlinkage int vprintk(const char *fmt, va_list args)
{
int printed_len = 0;
int current_log_level = default_message_loglevel;//printk函数的默认输出级别
unsigned long flags;
int this_cpu;
char *p;
size_t plen;
char special;
boot_delay_msec();
printk_delay();
/* 当我们需要console_sem的持有者的时候 我们会停止这一步 */
local_irq_save(flags);
this_cpu = smp_processor_id();//获得cpu号
/* printk在错误使用下会引发panic */
if (unlikely(printk_cpu == this_cpu)) {
/*
* 如果在这个CPU上printk调用失败了,然后试着得到失败的信息但
* 需要保证我们不会引发死锁。否则直接返回来避免printk的递归
* 但是要标记已经发生递归,并在下一个合适的时刻将信息打印出来
*/
/* oops_in_progress只有在panic函数中才为1 */
if (!oops_in_progress && !lockdep_recursing(current)) {
recursion_bug = 1;
goto out_restore_irqs;
}
/*如果在printk运行时,这个CPU崩溃,确信不能死锁,
*10秒1次初始化锁logbuf_lock和console_sem,
*留时间给控制台打印完全的oops信息
*/
zap_locks();
}
lockdep_off();
raw_spin_lock(&logbuf_lock);
printk_cpu = this_cpu;
/* 因为第一次printk调用失败 所以这次需要把第一次失败的信息打印出来 */
if (recursion_bug) {
recursion_bug = 0;
strcpy(printk_buf, recursion_bug_msg);
printed_len = strlen(recursion_bug_msg);
}
/*将要输出的字符串按照fmt中的格式编排好,
*放入printk_buf中,并返回应该输出的字符个数
*/
printed_len += vscnprintf(printk_buf + printed_len,
sizeof(printk_buf) - printed_len, fmt, args);
p = printk_buf;
/* 读取日志等级 并且处理printk的特殊前缀 */
plen = log_prefix(p, ¤t_log_level, &special);
if (plen) {
p += plen;
switch (special) {
case 'c': /* Strip KERN_CONT, continue line */
plen = 0;
break;
case 'd': /* Strip KERN_DEFAULT, start new line */
plen = 0;
default:
if (!new_text_line) {
emit_log_char('\n');
new_text_line = 1;
}
}
}
/*
*拷贝printk_buf数据到环形缓冲区中,如果调用者没有提供合适的日志级别,
*则插入默认级别;拷贝的过程由函数emit_log_char实现,每次拷贝一个字节
*/
for (; *p; p++) {
if (new_text_line) {
new_text_line = 0;
if (plen) {
/* 保存初始的前缀 */
int i;
for (i = 0; i < plen; i++)
emit_log_char(printk_buf[i]);
printed_len += plen;
} else {
/* 如果不存在添加默认等级 */
emit_log_char('<');
emit_log_char(current_log_level + '0');
emit_log_char('>');
printed_len += 3;
}
/*如果设置了此选项,则在每一条printk信息前都要加上时间参数*/
if (printk_time) {
char tbuf[50], *tp;
unsigned tlen;
unsigned long long t;
unsigned long nanosec_rem;
t = cpu_clock(printk_cpu);
nanosec_rem = do_div(t, 1000000000);
tlen = sprintf(tbuf, "[%5lu.%06lu] ",
(unsigned long) t,
nanosec_rem / 1000);/* 对打印出来的时间进行格式化 */
for (tp = tbuf; tp < tbuf + tlen; tp++)
emit_log_char(*tp);
printed_len += tlen;
}
if (!*p)
break;
}
emit_log_char(*p);
if (*p == '\n')
new_text_line = 1;
}
/*
* 在acquire_console_semaphore_for_printk函数的注释中有这样一句话:
* 此函数被调用时拥有logbuf_lock的自旋锁,并且处于禁止中断状态
* 在返回时(无论成功get sem)应保证logbuf_lock的自旋锁被释放,但是仍然禁止中断
*/
if (console_trylock_for_printk(this_cpu))
/*此函数将log_buf中的内容发送给console,并且唤醒klogd*/
console_unlock();
lockdep_on();
out_restore_irqs:
local_irq_restore(flags);
return printed_len;
}
环形缓冲区在字面看来就是一个数组 static char __log_buf[__LOG_BUF_LEN]
;其长度一般为4096大小(内核可修改)。而log_end长度为unsigned long范围,远远大于数组的大小,对于每一个字符的赋值log_end则只管++,在加一之后进行判断,如果log_end的值大于log_start,则表示缓冲区的长度已经达到最大,下一次的写入就将覆盖之前最旧的位置,因此log_start = log_end - log_buf_len,将log_start的位置向后移一位(因为每次只写入一个字符,不可能超过一位)。log_end和log_start通过unsigned long的自然溢出来实现环形的判断,而对其中每一次赋值则不再考虑环形的实现形式。
static void emit_log_char(char c)
{
LOG_BUF(log_end) = c;
log_end++;
if (log_end - log_start > log_buf_len)
log_start = log_end - log_buf_len;
if (log_end - con_start > log_buf_len)
con_start = log_end - log_buf_len;
if (logged_chars < log_buf_len)
logged_chars++;
}
前面已经说明了printk的实现机制,而printk最终是要打印到控制台上,
所以我们来看一下在linux下控制台是如何实现打印信息的。
struct console_cmdline
{
char name[8]; /* 设备名称 */
int index; /* 可用的次设备号 */
char *options; /* 设备的一些选项 */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
char *brl_options; /* Options for braille driver */
#endif
};
/*
*通过传入的str来提取console_cmdline中的一些信息 并存放到buf中
*/
static int __init console_setup(char *str)
{
char buf[sizeof(console_cmdline[0].name) + 4]; /*即得到options */
char *s, *options, *brl_options = NULL;
int idx;
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
if (!memcmp(str, "brl,", 4)) {
brl_options = "";
str += 4;
} else if (!memcmp(str, "brl=", 4)) {
brl_options = str + 4;
str = strchr(brl_options, ',');
if (!str) {
printk(KERN_ERR "need port name after brl=\n");
return 1;
}
*(str++) = 0;
}
#endif
/*
* Decode str into name, index, options.
*/
if (str[0] >= '0' && str[0] <= '9') {
strcpy(buf, "ttyS");
strncpy(buf + 4, str, sizeof(buf) - 5);
} else {
strncpy(buf, str, sizeof(buf) - 1);
}
buf[sizeof(buf) - 1] = 0;
if ((options = strchr(str, ',')) != NULL)
*(options++) = 0;
#ifdef __sparc__
if (!strcmp(str, "ttya"))
strcpy(buf, "ttyS0");
if (!strcmp(str, "ttyb"))
strcpy(buf, "ttyS1");
#endif
for (s = buf; *s; s++)
if ((*s >= '0' && *s <= '9') || *s == ',')
break;
idx = simple_strtoul(s, NULL, 10);
*s = 0;
__add_preferred_console(buf, idx, options, brl_options);
console_set_on_cmdline = 1;
return 1;
}
static int __add_preferred_console(char *name, int idx, char *options,
char *brl_options)
{
struct console_cmdline *c;
int i;
/*
* 查看这个tty是否已登记, and
* if we have a slot free.
*/
for (i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++)
if (strcmp(console_cmdline[i].name, name) == 0 &&
console_cmdline[i].index == idx) {
if (!brl_options)
selected_console = i;//确定所选的终端号 默认为-1
return 0;
}
if (i == MAX_CMDLINECONSOLES)//所选终端号无对应终端 报错
return -E2BIG;
if (!brl_options)
selected_console = i;
c = &console_cmdline[i];//将所选的终端的对象进行初始化
strlcpy(c->name, name, sizeof(c->name));
c->options = options;
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
c->brl_options = brl_options;
#endif
c->index = idx;
return 0;
}
printk的代码中包含了对console的选择
以下为调用关系
printk -> vprintk -> console_unlock -> call_console_drivers
void console_unlock(void)
{
unsigned long flags;
unsigned _con_start, _log_end;
unsigned wake_klogd = 0, retry = 0;
/* 如果当前终端已被挂起 释放锁 并退出 */
if (console_suspended) {
up(&console_sem);
return;
}
console_may_schedule = 0;/* console需要调度 */
again:
for ( ; ; ) {
raw_spin_lock_irqsave(&logbuf_lock, flags);
wake_klogd |= log_start - log_end;/* 如果之间有数据需要打印,则添加标记 */
if (con_start == log_end)
break; /* 没有数据需要打印 */
_con_start = con_start;
_log_end = log_end;
con_start = log_end; /* “清空”缓存 */
raw_spin_unlock(&logbuf_lock);
stop_critical_timings(); /* don't trace print latency */
call_console_drivers(_con_start, _log_end);/* 将数据长度传入终端设备 */
start_critical_timings();
local_irq_restore(flags);
}
console_locked = 0;
/* 将数据输出到所有的终端设备 */
if (unlikely(exclusive_console))
exclusive_console = NULL;
raw_spin_unlock(&logbuf_lock);
up(&console_sem);
/*
* 有人可能又将缓存区填入了数据, 所以检查是否需要重新flush
* 万一我们没有获得console_sem锁,锁会有新的持有者
* 他们会调用console_unlock 并flush缓存区 不需要担心
*/
raw_spin_lock(&logbuf_lock);
if (con_start != log_end)/* 有人填充了数据 */
retry = 1;
raw_spin_unlock_irqrestore(&logbuf_lock, flags);
if (retry && console_trylock())/* 又有新的数据需要打印 */
goto again;
if (wake_klogd)/* 如果有等待的klogd 那么唤醒他 */
wake_up_klogd();
}
static void call_console_drivers(unsigned start, unsigned end)
{
unsigned cur_index, start_print;
static int msg_level = -1;/* 日志等级 */
BUG_ON(((int)(start - end)) > 0);
cur_index = start;
start_print = start;
while (cur_index != end) {
if (msg_level < 0 && ((end - cur_index) > 2)) {
/* 判断日志等级 */
cur_index += log_prefix(&LOG_BUF(cur_index), &msg_level, NULL);
start_print = cur_index;
}
while (cur_index != end) {
/* 这一段操作之将日志相关信息解析 */
char c = LOG_BUF(cur_index);/* 将缓存中的字符一个一个赋给c */
cur_index++;
if (c == '\n') {
if (msg_level < 0) {
msg_level = default_message_loglevel;
}
_call_console_drivers(start_print, cur_index, msg_level);
msg_level = -1;
start_print = cur_index;
break;
}
}
}
_call_console_drivers(start_print, end, msg_level);/* 传入打印数据 */
}
/*
* 从start到end一个字符一个字符输出
*/
static void _call_console_drivers(unsigned start,
unsigned end, int msg_log_level)
{
/* 如果当前日志等级高于设置打印的级别 则忽略掉 */
if ((msg_log_level < console_loglevel || ignore_loglevel) &&
console_drivers && start != end) {
if ((start & LOG_BUF_MASK) > (end & LOG_BUF_MASK)) {
/* 如果start低于end 则如下方式打印 */
__call_console_drivers(start & LOG_BUF_MASK,
log_buf_len);
__call_console_drivers(0, end & LOG_BUF_MASK);
} else {
__call_console_drivers(start, end);/* 正常区间打印 */
}
}
}
static void __call_console_drivers(unsigned start, unsigned end)
{
struct console *con;
/* 遍历终端的链表 把信息输出到每个终端上 */
for_each_console(con) {
if (exclusive_console && con != exclusive_console)/* 只输出选中的终端 */
continue;
if ((con->flags & CON_ENABLED) && con->write &&
(cpu_online(smp_processor_id()) ||
(con->flags & CON_ANYTIME)))
con->write(con, &LOG_BUF(start), end - start);/*调用每个终端的write接口*/
}
}
用户空间访问和控制内核日志有两个接口:
(1)通过glibc的klogctl函数接口调用内核的syslog系统调用 (2)通过fs/proc/kmsg.c内核模块中导出的procfs接口:/proc/kmsg
文件。 他们其实最终都调用了/kernel/printk.c中的do_syslog函数,实现对__log_buf的访问及相关变量的修改。但值得注意的是:从/proc/kmsg中获取数据,那么 __log_buf中被读取过的数据就不再保留(也就是会修改log_start指针), 然而 syslog 系统调用返回日志数据并保留数据给其他进程。读取/proc文件是 klogd的默认做法。dmesg命令可用来查看缓存的内容并保留它,其实它是将__log_buf中的所有内容返回给stdout,并不管它是否已经被读取过