操作系统(UNIX与Linux内核信息输出过程比较)[2]

类别:软件工程 点击:0 评论:0 推荐:

 

. 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复杂,但是两者相同的是,代码写的都很漂亮。逻辑控制无论简繁而总不失条理,充分的考虑了效率的要求,又没有失去代码的可读性。笔者称其为“清丽之意,乃介乎天生丽质,韵味内涵皆清晰可观“,即指此也。

 

本文地址:http://com.8s8s.com/it/it37648.htm