Ⅱ . Linux部分(printk)
在Linux中,完成类似工作的,是 linux/kernel/printk.c 中的过程 printk。这个过程,相对来讲,就复杂的多。其中程序代码为(在过程代码之前,有些全局变量的定义省略,需要指出时我们再指出):
/*
* linux/kernel/printk.c
*
* Copyright (C) 1991, 1992 Linus Torvalds
*
* Modified to make sys_syslog() more flexible: added commands to
* return the last 4k of kernel messages, regardless of whether
* they've been read or not. Added option to suppress kernel printk's
* to the console. Added hook for sending the console messages
* elsewhere, in preparation for a serial line console (someday).
* Ted Ts'o, 2/11/93.
* Modified for sysctl support, 1/8/97, Chris Horn.
*/
spinlock_t console_lock;/*结构体变量,控制台锁*/
001 asmlinkage int printk(const char *fmt, ...)
002 {
003 va_list args;
004 int i;
005 char *msg, *p, *buf_end;
006 int line_feed;
007 static signed char msg_level = -1; /* 当前消息的日志等级*/
008 long flags;
009
010 spin_lock_irqsave(&console_lock, flags);
011 va_start(args, fmt);
012 i = vsprintf(buf + 3, fmt, args); /* hopefully i < sizeof(buf)-4 */
013 buf_end = buf + 3 + i;
014 va_end(args);
015 for (p = buf + 3; p < buf_end; p++) {
016 msg = p;
017 if (msg_level < 0) {
018 if (
019 p[0] != '<' ||
020 p[1] < '0' ||
021 p[1] > '7' ||
022 p[2] != '>'
023 ) {
024 p -= 3;
025 p[0] = '<';
026 p[1] = default_message_loglevel + '0';
027 p[2] = '>';
028 } else
029 msg += 3;
030 msg_level = p[1] - '0';
031 }
032 line_feed = 0;
033 for (; p < buf_end; p++) {
034 log_buf[(log_start+log_size) & (LOG_BUF_LEN-1)] = *p;
035 if (log_size < LOG_BUF_LEN)
036 log_size++;
037 else {
038 log_start++;
039 log_start &= LOG_BUF_LEN-1;
040 }
041 logged_chars++;
042 if (*p == '\n') {
043 line_feed = 1;
044
break;
045 }
046 }
047 if (msg_level < console_loglevel && console_drivers) {
048 struct console *c = console_drivers;
049 while(c) {
050 if ((c->flags & CON_ENABLED) && c->write)
051 c->write(c, msg, p - msg + line_feed);
052 c = c->next;
053 }
054 }
055 if (line_feed)
056 msg_level = -1;
057 }
058 spin_unlock_irqrestore(&console_lock, flags);
059 wake_up_interruptible(&log_wait);
060 return i;
061 }
062
这一过程的设计,有些知识需要加以介绍。
首先,printk是内核内部消息日志记录函数。调用printk的一般为紧急事件、调试或普通信息。它与过程printf的参数比较类似,都是一个格式化字符串,并把其后若干个参数(包括0个)加入此字符串中。在printk中,格式化字符串可能是以一组“〈N〉”开始,其中0<=N<=7,而该数字区分了消息的日志等级(log level),只有当日志等级高于当前控制台定义的日志等级(console_loglevel)时,才会打印输出消息。
关于程序,其中要说明的是,spin_lock_irqsave是一个宏,定义如下:
/* 在文件 spinlock.h 中*/
#define spin_lock_irqsave(lock, flags)
do { save_flags(flags); cli(); } while (0)
这个宏定义很有意思,使用do-while结构把目标封装起来。这是一个技巧,可以避免宏带来的歧义。而作用是,获取控制态锁,并初始化flags。va_start和va_end也是宏,与一个特殊类型va_list进行处理,把printk参数中的“…”部分存入args(011至014)。
全局变量给出如下几个定义:
#define LOG_BUF_LEN (16384)/* log_buf 's length */
static char buf[1024];
unsigned long log_size = 0;
struct console *console_drivers = NULL;
static char log_buf[LOG_BUF_LEN];
static unsigned long log_start = 0;
static unsigned long logged_chars = 0;
而其中console是一个结构体,定义如下:
/* 在文件 console.h 中*/
struct console
{
void (*write)(struct console *, const char *, unsigned);
}
如此,您便可以暂时回去继续看一看这段程序,以己之力,来辨伊人。
对于Linux内核的这个过程,下面我们来进行详细的分析。
为方便讨论,我们给出下图,作为叙述的参考:
014结束时,我们已经获得控制台锁,并且,printk的参数“…”部分(如果有)与fmt部分都装入buf[]内。其中012行的vsprintf为Linux内核自己所实现的过程,向buf中写入格式化字符串并返回所写入字符串的长度(不包括最后终止字符0字节),由调用情况来看,忽略了buf的前三个字符,分析后面的代码就会知道原因。
015 至 057 外层for循环,具体的处理消息字符串。
016 msg记录 buf+3的位置。
017 当前消息日志的等级msg_level是静态变量,被初始化为-1。所以首次调用printk时,017至031肯定会被执行的。
如果快速的扫描程序,您会发现改变msg_level值的机会只有两个,一是在030,最后一次在056,如果line_feed的值为1,则msg_level的值又赋值为-1。
018 至 029 这个if判断非常有意思。这里重新审视buf[]的前三个字符(一个子串)。只要不是“〈N〉”,其中0<=N<=7,就需要对这个子串重新设置,消息日志的等级被置为default_message_loglevel(024至027);否则,msg后移三位。
030 msg_level记录当前消息日志的等级(为1到7的数字之一)!
032 line_feed初始化为0。line_feed其实记录的是当前已处理消息的行数。
033 至 046 是内层for循环。这里更有意思,也凸现出c语言的能力,及Linux内核代码编写者运用语言的能力。注意此时开始p和msg的指向,前者指向日志等级序列,后者指向紧跟其后的消息文本。
034 缓冲区log_buf[]是不是看起来容易让人糊涂?这里的按位与(&)运算是什么意思?是快速求模运算(%)!当然,需要有个保障,就是LOG_BUF_LEN的值要为2的幂。这样,log_buf[]其实是个循环缓冲器。它将记录最新的内部消息的一行。
035 至 040 就是对循环缓冲器访问机制的重新设置,使log_start+log_size在循环(取模LOG_BUF_LEN-1)的意义上后移一位。
041 logged_chars是全局静态变量,记录的是机器启动后由printk写入的所有字符的个数,这里自增1。
042 至 045 是内循环退出的另一出口,如果当前p所指内容为‘\n’,表明消息一行即将结束。则line_feed记录为1,退出内层for循环。
从这里可以回头看看这个内层for循环所做的工作。可以知道,每次内层for循环开始,都开始一个新的打印行。只不过通常printk只用于打印一行,所以内层for循环通常也只执行一次。
047 至 054 打印一行消息的工作。047的判断若为真,则表明当前消息的日志等级高于当前控制台定义的日志等级,且控制台驱动链表console_drivers可以打开。其后就是遍历console_drivers告知每一个控制台驱动去打印当前行。我们不讨论这个过程的详细情况,只提醒您051传入的第二个参数是msg而不是p,这样就在没有日志等级序列的情况下写入了消息。以后若需要日志等级,可以访问log_buf[]。
055 和 056 如果line_feed为1,则一行已经结束。无论是否存在,则都可视为新的一行开始,msg_level又置为-1。
现在,可以看看msg_level的控制逻辑。如下图(是简略图,只涉及msg_level):
对照printk,脉络就会更清楚些。其中包括017到031部分的执行,现在可以说是在每一新行的消息处理时。
058 释放控制台锁。
059 唤醒等待被写入控制台日志的所有进程。
060 返回I。
061 结束。
这个过程虽较UNIX的printf复杂,但是两者相同的是,代码写的都很漂亮。逻辑控制无论简繁而总不失条理,充分的考虑了效率的要求,又没有失去代码的可读性。笔者称其为“清丽之意,乃介乎天生丽质,韵味内涵皆清晰可观“,即指此也。