Chinaunix首页 | 论坛 | 博客
  • 博客访问: 387941
  • 博文数量: 120
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 741
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-27 18:15
文章分类

全部博文(120)

文章存档

2016年(13)

2015年(41)

2014年(66)

我的朋友

分类: LINUX

2014-08-14 10:35:07

    内核函数printk的使用方法和用户printf相似,用于内核打印消息。printk根据日志级别(loglevel)对消息进行类。日志级别用宏定义,日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此printk函数中日志级别宏和格式字符串间不能有逗号。
    下面是两个printk的例子,一个用于打印调试信息,另一个用于打印临界条件信息。

点击(此处)折叠或打开

  1. printk(KERN_DEBUG "Here I am: %s:%i/n", __FILE__, __LINE__);
  2. printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);

printk的日志级别定义如下(在linux-3.4\include\linux\kernel.h中):

点击(此处)折叠或打开

  1. #define KERN_EMERG    "<0>"    /* system is unusable            */
  2. #define KERN_ALERT    "<1>"    /* action must be taken immediately    */
  3. #define KERN_CRIT    "<2>"    /* critical conditions            */
  4. #define KERN_ERR    "<3>"    /* error conditions            */
  5. #define KERN_WARNING    "<4>"    /* warning conditions            */
  6. #define KERN_NOTICE    "<5>"    /* normal but significant condition    */
  7. #define KERN_INFO    "<6>"    /* informational            */
  8. #define KERN_DEBUG    "<7>"    /* debug-level messages            */

  9. /* Use the default kernel loglevel */
  10. #define KERN_DEFAULT    ""
  11. /*
  12.  * Annotation for a "continued" line of log printout (only done after a
  13.  * line that had no enclosing \n). Only to be used by core/arch code
  14.  * during early bootup (a continued line is not SMP-safe otherwise).
  15.  */
  16. #define KERN_CONT    ""

  17. extern int console_printk[];

  18. #define console_loglevel (console_printk[0])
  19. #define default_message_loglevel (console_printk[1])
  20. #define minimum_console_loglevel (console_printk[2])
  21. #define default_console_loglevel (console_printk[3])


    日志级别的范围是0~7,没有指定日志级别的printk语句默认采用的级别是DEFAULT_MESSAGE_LOGLEVEL,其定义列出如下(在linux-3.4\linux\printk.c中):

点击(此处)折叠或打开

  1. /* printk's without a loglevel use this.. */
  2. #define DEFAULT_MESSAGE_LOGLEVEL CONFIG_DEFAULT_MESSAGE_LOGLEVEL

  3. /*autoconf.h中*/
  4. #define CONFIG_DEFAULT_MESSAGE_LOGLEVEL 7


    内核可把消息打印到当前控制台上,可以指定控制台为字符模式的终端或打印机等。默认情况下,“控制台”就是当前的虚拟终端。为了更好地控制不同级别的信息显示在控制台上,内核设置了控制台的日志级别console_loglevelprintk日志级别的作用是打印一定级别的消息(不一定能显示出来,只是输出到buffer),与之类似,控制台只显示一定级别的消息。当日志级别小于console_loglevel时,消息才能显示出来。控制台相应的日志级别定义如下:

点击(此处)折叠或打开

  1. /* printk's without a loglevel use this.. */
  2. #define DEFAULT_MESSAGE_LOGLEVEL CONFIG_DEFAULT_MESSAGE_LOGLEVEL

  3. /* We show everything that is MORE important than this.. */
  4. #define MINIMUM_CONSOLE_LOGLEVEL 1 /* Minimum loglevel we let people use */
  5. #define DEFAULT_CONSOLE_LOGLEVEL 7 /* anything MORE serious than KERN_DEBUG */

  6. DECLARE_WAIT_QUEUE_HEAD(log_wait);

  7. int console_printk[4] = {
  8.     DEFAULT_CONSOLE_LOGLEVEL,    /* console_loglevel */
  9.     DEFAULT_MESSAGE_LOGLEVEL,    /* default_message_loglevel */
  10.     MINIMUM_CONSOLE_LOGLEVEL,    /* minimum_console_loglevel */
  11.     DEFAULT_CONSOLE_LOGLEVEL,    /* default_console_loglevel */
  12. };


    如果系统运行了klogdsyslogd,则无论console_loglevel为何值,内核消息都将追加到/var/log/messages中。如果klogd没有运行,消息不会传递到用户空间,只能查看/proc/kmsg。

    变量console_loglevel的初始值是DEFAULT_CONSOLE_LOGLEVEL,可以通过sys_syslog系统调用进行修改。调用klogd时可以指定-c开关选项来修改这个变量。如果要修改它的当前值,必须先杀掉klogd,再加-c选项重新启动它。

注:#ps -e 查看所有进程PID,然后KILL。
通过读写/proc/sys/kernel/printk文件可读取和修改控制台的日志级别。查看这个文件的方法如下:

#cat /proc/sys/kernel/printk
6 4 1 7

    上面显示的4个数据分别对应控制台日志级别默认的消息日志级别最低的控制台日志级别默认的控制台日志级别。使用printk时,只有级别小于控制台日志级别才能显示出来。

可用下面的命令设置当前日志级别:

# echo 8 > /proc/sys/kernel/printk
printk打印消息机制

在内核中,函数printk将消息打印到环形缓冲区__log_buf中,并将消息传给控制台进行显示。控制台驱动程序根据控制台的日志级别显示日志消息。

应用程序通过系统调用sys_syslog管理环形缓冲区__log_buf,它可以读取数据、清除缓冲区、设置日志级别、开/关控制台等。

当系统调用sys_syslog从环形缓冲区__log_buf读取数据时,如果缓冲区没有数据,系统调用sys_syslog所在进程将被加入到 等待队列log_wait中进行等待。当printk将数据打印到缓冲区后,将唤醒系统调用sys_syslog所在进程从缓冲区中读取数据。等待队列 log_wait定义如下:

DECLARE_WAIT_QUEUE_HEAD(log_wait);//等待队列log_wait

环形缓冲区__log_buf在使用之前就是已定义好的全局变量,缓冲区的长度为1 << CONFIG_LOG_ BUF_SHIFT。变量CONFIG_LOG_BUF_SHIFT在内核编译时由配置文件定义,对于i386平台,其值定义如下(在 linux26/arch/i386/defconfig中):

CONFIG_LOG_BUF_SHIFT=18

在内核编译时,编译器根据配置文件的设置,产生如下的宏定义:

#define CONFIG_LOG_BUF_SHIFT 18

环形缓冲区__log_buf定义如下(在linux26/kernel/printk.c中):

#define __LOG_BUF_LEN(1 << CONFIG_LOG_BUF_SHIFT) //定义环形缓冲区的长度,i386平台为 
static char __log_buf[__LOG_BUF_LEN]; //printk的环形缓冲区
static char *log_buf = __log_buf;
static int log_buf_len = __LOG_BUF_LEN;
 
static DEFINE_SPINLOCK(logbuf_lock);

通过宏定义LOG_BUF,缓冲区__log_buf具备了环形缓冲区的操作行为。宏定义LOG_BUF得到缓冲区指定位置序号的字符,位置序号超过缓冲区长度时,通过与长度掩码LOG_BUF_MASK进行逻辑与操作,位置序号循环回到环形缓冲区中的位置。

宏定义LOG_BUF及位置序号掩码LOG_BUF_MASK的定义列出如下:

#define LOG_BUF_MASK (log_buf_len-1)
#define LOG_BUF(idx)  (log_buf[(idx) & LOG_BUF_MASK])

为了指明环形缓冲区__log_buf字符读取位置,定义了下面的位置变量:

static unsigned long log_start;
static unsigned long con_start;
static unsigned long log_end;
static unsigned long logged_chars;

任何地方的内核调用都可以调用函数printk打印调试、安全、提示和错误消息。函数printk尝试得到控制台信号量 (console_sem),如果得到,就将信息输出到环形缓冲区__log_buf中,然后函数release_console_sem()在释放信号 量之前把环形缓冲区中的消息送到控制台,调用控制台驱动程序显示打印的信息。如果没得到信号量,就只将信息输出到环形缓冲区后返回。函数printk的调 用层次如图1所示。

图1 函数printk的调用层次图

函数printk列出如下(在linux26/kernel/printk.c中):

asmlinkage int printk(const char *fmt, ...)
{
va_list args;
int r;
 
va_start(args, fmt);
r = vprintk(fmt, args);
va_end(args);
 
return r;
}
 
asmlinkage int vprintk(const char *fmt, va_list args)
{
unsigned long flags;
int printed_len;
char *p;
static char printk_buf[1024];
static int log_level_unknown = 1;
 
preempt_disable(); //关闭内核抢占
if (unlikely(oops_in_progress) && printk_cpu == smp_processor_id())
 
zap_locks();
 
local_irq_save(flags); //存储本地中断标识
lockdep_off();
spin_lock(&logbuf_lock);
printk_cpu = smp_processor_id(); 
 
 
printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);
 
 
for (p = printk_buf; *p; p++) {
if (log_level_unknown) {
 
if (printk_time) {
int loglev_char;
char tbuf[50], *tp;
unsigned tlen;
unsigned long long t;
unsigned long nanosec_rem;
 
 
if (p[0] == '<' && p[1] >='0' &&
p[1] <= '7' && p[2] == '>') {
loglev_char = p[1]; //获取日志级别字符
p += 3;
printed_len -= 3;
} else {
loglev_char = default_message_loglevel
+ '0';
}
t = printk_clock();//返回当前时钟,以ns为单位
nanosec_rem = do_div(t, 1000000000);
tlen = sprintf(tbuf,
"<%c>[%5lu.%06lu] ",
loglev_char,
(unsigned long)t,
nanosec_rem/1000);//写入格式化后的日志级别和时间
 
for (tp = tbuf; tp < tbuf + tlen; tp++) 
emit_log_char(*tp);  //将日志级别和时间字符输出到循环缓冲区
printed_len += tlen;
} else {
if (p[0] != '<' || p[1] < '0' ||
p[1] > '7' || p[2] != '>') {
emit_log_char('<');
emit_log_char(default_message_loglevel
+ '0');  //输出字符到循环缓冲区
emit_log_char('>');
printed_len += 3;
}
}
log_level_unknown = 0;
if (!*p)
break;
}
emit_log_char(*p);//将其他printk_buf数据输出到循环缓冲区
if (*p == '/n')
log_level_unknown = 1;
}
 
if (!down_trylock(&console_sem)) {
 
console_locked = 1;
printk_cpu = UINT_MAX;
spin_unlock(&logbuf_lock);
 
 
if (cpu_online(smp_processor_id()) || have_callable_console()) {
console_may_schedule = 0;
release_console_sem();
} else {
 
console_locked = 0;
up(&console_sem);
}
lockdep_on();
local_irq_restore(flags); //恢复本地中断标识
} else {
 
printk_cpu = UINT_MAX;
spin_unlock(&logbuf_lock);
lockdep_on();
local_irq_restore(flags); //恢复本地中断标识
}
 
preempt_enable(); //开启抢占机制
return printed_len;
}

函数release_console_sem()给控制台系统开锁,释放控制台系统及驱动程序调用者持有的信号量。持有信号量时,表示printk 已在缓冲区存有数据。函数release_console_sem()在释放信号量之前将这些数据送给控制台显示。如果后台进程klogd在等待环形缓冲 区装上数据,它唤醒klogd进程。

函数release_console_sem列出如下(在linux26/kernel/printk.c中):

void release_console_sem(void)
{
unsigned long flags;
unsigned long _con_start, _log_end;
unsigned long wake_klogd = 0;
 
for ( ; ; ) {
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;
spin_unlock_irqrestore(&logbuf_lock, flags);
//调用控制台driver的write函数写入到控制台
call_console_drivers(_con_start, _log_end);
}
console_locked = 0;
console_may_schedule = 0;
up(&console_sem);
spin_unlock_irqrestore(&logbuf_lock, flags);
if (wake_klogd && !oops_in_progress && waitqueue_active(&log_wait))
wake_up_interruptible(&log_wait);//唤醒在等待队列上的进程
}

函数_call_console_drivers将缓冲区中从start到end - 1的数据输出到控制台进行显示。在输出数据到控制台之前,它检查消息的日志级别。只有日志级别小于控制台日志级别console_loglevel的消 息,才能交给控制台驱动程序进行显示。

函数_call_console_drivers列出如下:

static void _call_console_drivers(unsigned long start,
unsigned long 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)) {
 
__call_console_drivers(start & LOG_BUF_MASK, log_buf_len);
__call_console_drivers(0, end & LOG_BUF_MASK);
} else {
__call_console_drivers(start, end);
}
}
}

函数__call_console_drivers调用控制台驱动程序的写操作函数显示消息。其列出如下:



点击(此处)折叠或打开

  1. /*
  2.  * Call the console drivers on a range of log_buf
  3.  */
  4. static void __call_console_drivers(unsigned start, unsigned end)
  5. {
  6.     struct console *con;

  7.     for_each_console(con) {
  8.         if (exclusive_console && con != exclusive_console)
  9.             continue;
  10.         if ((con->flags & CON_ENABLED) && con->write &&
  11.                 (cpu_online(smp_processor_id()) ||
  12.                 (con->flags & CON_ANYTIME)))
  13.             con->write(con, &LOG_BUF(start), end - start);
  14.     }
  15. }

阅读(685) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~