由于前两天在看netconsole的源码实现中,发现其跟printk的实现机制相关,加之之前一直是很普通的使用printk,从不清楚printk到底是怎样工作的,因此就趁这个机会把printk的实现代码也给大致看了一下,代码流程并不复杂,下面就简要说明一下。
printk在内核中的实现代码如下。
asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
/*将fmt后的参数信息保存到args中*/
va_start(args, fmt);
/*处理printk流程的主要函数*/
r =
vprintk(fmt, args);
/*va_end函数貌似是空函数*/
va_end(args);
return r;
}
下面就接着看vprintk函数的处理流程,vprintk实现printk的主要操作。
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; boot_delay_msec(); /*禁止内核抢占*/ preempt_disable(); /* This stops the holder of console_sem just where we want him */ raw_local_irq_save(flags); this_cpu = smp_processor_id(); /* * Ouch, printk recursed into itself! */ /*这一段的代码没有理解的很明白。。。*/ /*printk的递归调用 ? 是在说printk的时候发生oops吗? */ if (unlikely(printk_cpu == this_cpu)) { /* * If a crash is occurring during printk() on this CPU, * then try to get the crash message out but make sure * we can't deadlock. Otherwise just return to avoid the * recursion and return - but flag the recursion so that * it can be printed at the next appropriate moment: */ //oops_in_progress只有在panic函数中才为1 if (!oops_in_progress) { recursion_bug = 1; goto out_restore_irqs; } /*如果在printk运行时,这个CPU崩溃,确信不能死锁,10秒1次初始化锁logbuf_lock和console_sem,留时间给控制台打印完全的oops信息*/ zap_locks(); } lockdep_off(); spin_lock(&logbuf_lock); printk_cpu = this_cpu; if (recursion_bug) { recursion_bug = 0; strcpy(printk_buf, recursion_bug_msg); printed_len = strlen(recursion_bug_msg); } /* Emit the output into the temporary buffer */ /*将要输出的字符串按照fmt中的格式编排好,放入printk_buf中,并返回应该输出的字符个数*/ printed_len += vscnprintf(printk_buf + printed_len, sizeof(printk_buf) - printed_len, fmt, args); /* * Copy the output into log_buf. If the caller didn't provide * appropriate log level tags, we insert them here */ /*拷贝printk_buf数据到环形缓冲区中,如果调用者没有提供合适的日志级别,则插入默认级别*/ /*拷贝的过程由函数emit_log_char实现,每次拷贝一个字节*/ for (p = printk_buf; *p; p++) { if (new_text_line) { /* If a token, set current_log_level and skip over */ if (p[0] == '<' && p[1] >= '0' && p[1] <= '7' && p[2] == '>') { current_log_level = p[1] - '0'; p += 3; printed_len -= 3; } /* Always output the token */ emit_log_char('<'); emit_log_char(current_log_level + '0'); emit_log_char('>'); printed_len += 3; new_text_line = 0; /*如果设置了此选项,则在每一条printk信息前都要加上时间参数*/ if (printk_time) { /* Follow the token with the 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; } /* * Try to acquire and then immediately release the * console semaphore. The release will do all the * actual magic (print out buffers, wake up klogd, * etc). * * The acquire_console_semaphore_for_printk() function * will release 'logbuf_lock' regardless of whether it * actually gets the semaphore or not. */ /* * 网上有这样一句话,要想对console进行操作,必须获取console结构的信号量。如果获取信号量,则可以通过log the output and call the console * drivers. 反之,则place the output into the log buff and return. 现有的信号量holeder在release函数中将会注意到 * 新的output,在释放信号量前将会把output信息发送给console */ /* * 在acquire_console_semaphore_for_printk函数的注释中有这样一句话:此函数被调用时拥有logbuf_lock的自旋锁,并且处于禁止中断状态 * 在返回时(无论成功get sem)应保证logbuf_lock的自旋锁被释放,但是仍然禁止中断 */ if (acquire_console_semaphore_for_printk(this_cpu)) /*此函数将log_buf中的内容发送给console,并且唤醒klogd*/ release_console_sem(); lockdep_on(); out_restore_irqs: raw_local_irq_restore(flags); preempt_enable(); return printed_len; } |
(自己直接贴代码总是不能对齐,还在抱怨uc对于代码支持不好呢。。第一次用这个粘贴代码。。。)
对于printk来说,一共有两个缓冲区printk_buf以及log_buf,前者有种临时缓冲的意思,后者用来存储最终要输出的字符串。后面将详细说一下其中最主要的log_buf。
对于vscnprintf函数来说,其就是最终通过vsnprintf()函数将printk的参数根据fmt格式进行转换,并将转换的结果暂存到printk_buf中,最终又将printk_buf中的数据保存到log_buf中。
下面在讨论往log_buf缓冲区写数据的函数emit_log_char之前,先简要说一下printk中的log_buf缓冲区。
/* * The indices into log_buf are not constrained to log_buf_len - they * must be masked before subscripting */ static unsigned log_start; /* Index into log_buf: next char to be read by syslog() */ static unsigned con_start; /* Index into log_buf: next char to be sent to consoles */ static unsigned log_end; /* Index into log_buf: most-recently-written-char + 1 */ |
其中的log_end标志着下一个写入的位置,其是上一次写的末尾+1;而log_start和con_start则是syslog和consoles读取数据的起始位置。在缓冲区写入的时候正是通过这三个变量以及C语言的特性完成环形的实现。
下面看一下写缓冲区的具体函数实现。
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++; } |
这个写入满足每次只写入一个字符,通过LOG_BUF将字符c赋值给缓冲区,通过后面的长度变化来实现环形的概念。其中的LOG_BUF是这样定义的:
#define LOG_BUF(idx) (log_buf[(idx) & LOG_BUF_MASK])
在写入时根据log_end的大小mod缓冲区长度,获取最终的写入位置。
环形缓冲区在字面看来就是一个数组 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的自然溢出来实现环形的判断,而对其中每一次赋值则不再考虑环形的实现形式。(罗里啰嗦了这么多,也不知道能不能看明白,不过我是明白了。。。感觉方法挺巧的。。)
函数的最后,则是release_console_sem函数,在此函数中完成console相关的操作。主要过程就是将con_start与log_end间的数据通过call_console_drivers函数来完成数据往控制台的传递,并且在最后环形klogd进程。
而call_console_drivers函数则是遍历内核中的console链表console_driver,对于其中的每一个console结构,调用其注册的write函数。
这两个函数的代码都比较简单,就不再多说了。
void release_console_sem(void) { unsigned long flags; unsigned _con_start, _log_end; unsigned wake_klogd = 0; if (console_suspended) { up(&console_sem); return; } console_may_schedule = 0; for ( ; ; ) { spin_lock_irqsave(&logbuf_lock, flags); wake_klogd |= log_start - log_end; if (con_start == log_end) break; /* Nothing to print */ _con_start = con_start; _log_end = log_end; con_start = log_end; /* Flush */ 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; up(&console_sem); spin_unlock_irqrestore(&logbuf_lock, flags); if (wake_klogd) wake_up_klogd(); } |
call_console_drivers函数在最终是通过__call_console_drivers函数来实现的。
/* * Call the console drivers on a range of log_buf */ static void __call_console_drivers(unsigned start, unsigned end) { struct console *con; for (con = console_drivers; con; con = con->next) { if ((con->flags & CON_ENABLED) && con->write && (cpu_online(smp_processor_id()) || (con->flags & CON_ANYTIME))) con->write(con, &LOG_BUF(start), end - start); } } |
至此,整个printk的实现流程就已经结束了,并不复杂,流程比较清晰