linux内核printk调试(摘录《Linux安全体系分析与编程》)

1  printk及控制台的日志级别

函数printk的使用方法和printf相似,用于内核打印消息。printk根据日志级别(loglevel)对消息进行分类。

日志级别用宏定义,日志级别宏展开为一个字符串,在编译时由预处理器将它和消息文本拼接成一个字符串,因此printk 函数中日志级别宏和格式字符串间不能有逗号。

下面是两个printk的例子,一个用于打印调试信息,另一个用于打印临界条件信息。

printk(KERN_DEBUG "Here I am: %s:%i/n", _ _FILE_ _, _ _LINE_ _);

printk(KERN_CRIT "I'm trashed; giving up on %p/n", ptr);

printk的日志级别定义如下(在linux26/includelinux/kernel.h中):

#defineKERN_EMERG"<0>"/*紧急事件消息,系统崩溃之前提示,表示系统不可用*/

#defineKERN_ALERT"<1>"/*报告消息,表示必须立即采取措施*/

#defineKERN_CRIT"<2>"/*临界条件,通常涉及严重的硬件或软件操作失败*/

#defineKERN_ERR"<3>"/*错误条件,驱动程序常用KERN_ERR来报告硬件的错误*/

#defineKERN_WARNING"<4>"/*警告条件,对可能出现问题的情况进行警告*/

#defineKERN_NOTICE"<5>"/*正常但又重要的条件,用于提醒。常用于与安全相关的消息*/

#defineKERN_INFO"<6>"/*提示信息,如驱动程序启动时,打印硬件信息*/

#defineKERN_DEBUG"<7>"/*调试级别的消息*/

extern int console_printk[];

#define console_loglevel  (console_printk[0])

#define default_message_loglevel (console_printk[1])

#define minimum_console_loglevel (console_printk[2])

#define default_console_loglevel (console_printk[3])

日志级别的范围是0~7,没有指定日志级别的printk语句默认采用的级别是DEFAULT_ MESSAGE_LOGLEVEL,其定义列出如下(在linux26/kernel/printk.c中):

/*没有定义日志级别的printk使用下面的默认级别*/

#define DEFAULT_MESSAGE_LOGLEVEL 4 /* KERN_WARNING 警告条件*/

内核可把消息打印到当前控制台上,可以指定控制台为字符模式的终端或打印机等。默认情况下,“控制台”就是当前的虚拟终端。

为了更好地控制不同级别的信息显示在控制台上,内核设置了控制台的日志级别console_loglevel。printk日志级别的作用是打印一定级别的消息,与之类似,控制台只显示一定级别的消息。

当日志级别小于console_loglevel时,消息才能显示出来。控制台相应的日志级别定义如下:

/* 显示比这个级别更重发的消息*/

#define MINIMUM_CONSOLE_LOGLEVEL  1   /*可以使用的最小日志级别*/

#define DEFAULT_CONSOLE_LOGLEVEL  7 /*比KERN_DEBUG 更重要的消息都被打印*/

int console_printk[4] = {

DEFAULT_CONSOLE_LOGLEVEL,/*控制台日志级别,优先级高于该值的消息将在控制台显示*/

/*默认消息日志级别,printk没定义优先级时,打印这个优先级以上的消息*/

DEFAULT_MESSAGE_LOGLEVEL,

/*最小控制台日志级别,控制台日志级别可被设置的最小值(最高优先级)*/

MINIMUM_CONSOLE_LOGLEVEL,

DEFAULT_CONSOLE_LOGLEVEL,/* 默认的控制台日志级别*/

};

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

变量console_loglevel的初始值是DEFAULT_CONSOLE_LOGLEVEL,可以通过sys_syslog系统调用进行修 改。调用klogd时可以指定-c开关选项来修改这个变量。如果要修改它的当前值,必须先杀掉klogd,再加-c选项重新启动它。
通过读写/proc/sys/kernel/printk文件可读取和修改控制台的日志级别。查看这个文件的方法如下:

#cat /proc/sys/kernel/printk

6   4  1   7

上面显示的4个数据分别对应控制台日志级别、默认的消息日志级别、最低的控制台日志级别和默认的控制台日志级别。

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

# echo 8 > /proc/sys/kernel/printk

2  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;

/*互斥锁logbuf_lock保护log_buf、log_start、log_end、con_start和logged_chars */

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;/*系统调用syslog读取的下一个字符*/

static unsigned long con_start;/*送到控制台的下一个字符*/

static unsigned long log_end;/*最近已写字符序号加1 */

static unsigned long logged_chars; /*自从上一次read+clear 操作以来产生的字符数*/

任何地方的内核调用都可以调用函数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())

/*如果在printk运行时,这个CPU发生崩溃,

确信不能死锁,10秒1次初始化锁logbuf_lock和console_sem,留时间

给控制台打印完全的oops信息*/

zap_locks();

local_irq_save(flags);  //存储本地中断标识

lockdep_off();

spin_lock(&logbuf_lock);

printk_cpu = smp_processor_id(); 

/*将输出信息发送到临时缓冲区printk_buf */

printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args);

/*拷贝printk_buf数据到循环缓冲区,如果调用者没提供合适的日志级别,插入默认值*/

for (p = printk_buf; *p; p++) {

if (log_level_unknown) {

/* log_level_unknown signals the start of a new line */

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)) {

/*拥有控制台驱动程序,降低spinlock并让release_console_sem()打印字符 */

console_locked = 1;

printk_cpu = UINT_MAX;

spin_unlock(&logbuf_lock);

/*如果CPU准备好,控制台就输出字符。函数cpu_online检测CPU是否在线,

函数have_callable_console()检测是否

有注册的控制台启动时就可以使用*/

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 {

/*如果其他进程拥有这个驱动程序,本线程降低spinlock,

允许信号量持有者运行并调用控制台驱动程序输出字符*/

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;/* Flush */

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调用控制台驱动程序的写操作函数显示消息。其列出如下:

static void __call_console_drivers(unsigned long start, unsigned long 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); //调用驱动程序的写操作函数

}

}

3  sys_syslog系统调用

系统调用sys_syslog根据参数type的命令执行相应的操作。参数type定义的命令列出如下:

0 -- 关闭日志,当前没实现。
1 -- 打开日志,当前没实现。
2 -- 从环形缓冲区读取日志消息。
3 -- 读取保留在环形缓冲区的所有消息。
4 -- 读取并清除保留在环形缓冲区的所有消息。
5 -- 清除环形缓冲区。
6 -- 关闭printk到控制台的打印。
7 -- 开启printk到控制台的打印。
8 -- 设置打印到控制台的消息的日志级别。
9 -- 返回日志缓冲区中没读取的字符数。
10 -- 返回日志缓冲区的大小。

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

asmlinkage long sys_syslog(int type, char __user * buf, int len)

{

return do_syslog(type, buf, len);

}

int do_syslog(int type, char __user *buf, int len)

{

unsigned long i, j, limit, count;

int do_clear = 0;

char c;

int error = 0;

error = security_syslog(type);  //检查是否调用这个函数的权限

if (error)

return error;

switch (type) {

case 0:/* 关闭日志 */

break;

case 1:/* 打开日志*/

break;

case 2:/*读取日志信息*/

error = -EINVAL;

if (!buf || len < 0)

goto out;

error = 0;

if (!len)

goto out;

if (!access_ok(VERIFY_WRITE, buf, len)) { //验证是否有写的权限

error = -EFAULT;

goto out;

}

//当log_start - log_end为0时,表示环形缓冲区无数据可读,把当前进程放入

等待队列log_wait

error = wait_event_interruptible(log_wait, (log_start - log_end));

if (error)

goto out;

i = 0;

spin_lock_irq(&logbuf_lock);

while (!error && (log_start != log_end) && i < len) {

c = LOG_BUF(log_start); //从环形缓冲区得到读取位置log_start

log_start++;

spin_unlock_irq(&logbuf_lock);

error = __put_user(c,buf); //将c地址的字符传递到用户空间的buf中

buf++;

i++;

cond_resched();  //条件调度,让其他进程有运行时间

spin_lock_irq(&logbuf_lock);

}

spin_unlock_irq(&logbuf_lock);

if (!error)

error = i;

break;

case 4:/* 读/清除上一次内核消息*/

do_clear = 1;

/* FALL THRU */

case 3:/*读取上一次内核消息*/

error = -EINVAL;

if (!buf || len < 0) 

goto out;

error = 0;

if (!len)  //读取长度为0

goto out;

if (!access_ok(VERIFY_WRITE, buf, len)) { //验证有写权限

error = -EFAULT;

goto out;

}

count = len;

if (count > log_buf_len) 

count = log_buf_len;

spin_lock_irq(&logbuf_lock);

if (count > logged_chars) // logged_chars是上次读/清除以来产生的日志字符数

count = logged_chars;

if (do_clear)

logged_chars = 0;

limit = log_end; 

/* __put_user() 可以睡眠,当__put_user睡眠时,printk()可能覆盖写正在

拷贝到用户空间的消息,因此,这些消息被反方向拷贝,将buf覆盖部分的数据重写到buf的起始位置*/

for (i = 0; i < count && !error; i++) { //读取count个字符

j = limit-1-i;

if (j + log_buf_len < log_end)

break;

c = LOG_BUF(j); //从环形缓冲区得到读取位置j

spin_unlock_irq(&logbuf_lock);

   //将c位置的字符传递到用户空间的buf中,如果发生错误,将发生错误的c位置给error

error = __put_user(c,&buf[count-1-i]);

cond_resched();

spin_lock_irq(&logbuf_lock);

}

spin_unlock_irq(&logbuf_lock);

if (error)

break;

error = i;

if (i != count) { //表示__put_user没有拷贝完成

int offset = count-error;

/* 拷贝期间缓冲区溢出,纠正用户空间缓冲区*/

for (i = 0; i < error; i++) {

if (__get_user(c,&buf[i+offset]) ||

__put_user(c,&buf[i])) { //将覆盖部分的数据

重写到buf的起始位置

error = -EFAULT;

break;

}

cond_resched();

}

}

break;

case 5:/* 清除环形缓冲区*/

logged_chars = 0;

break;

case 6:/*关闭向控制台输出消息*/

console_loglevel = minimum_console_loglevel;

break;

case 7:/*开启向控制台输出消息*/

console_loglevel = default_console_loglevel;

break;

case 8:/* 设置打印到控制台的日志级别*/

error = -EINVAL;

if (len < 1 || len > 8)

goto out;

if (len < minimum_console_loglevel)

len = minimum_console_loglevel;

console_loglevel = len;

error = 0;

break;

case 9:/* 得到日志消息所占缓冲区的大小*/

error = log_end - log_start;

break;

case 10:/*返回环形缓冲区的大小*/

error = log_buf_len;

break;

default:

error = -EINVAL;

break;

}

out:

return error;

}

为了更好的管理日志,你可通过修改/etc/syslog.conf来满足你的需求。man syslog.conf获得更多关于syslog.conf的信息。当你在编写内核模块时,应该注意一个问题,你不能让那些消息困扰你,因此定义一些宏来 开关printk消息是很有必要的,毕竟printk是用来调试,或者显示设备、模块的一些状态。另外内核同时也提供了一些接口, 在<linux/kernel.h>中定义,他们是一些内联函数。int printk_ratelimit(void)。在打印一条可能被重复的信息之前,应调用该函数。如果它返回一个非零值,则可以继续打印消息,否则跳过。 printk_ratelimit通过跟踪发送到控制台的消息量,开避免消息的重复输出。可以通过修改/proc/sys/kernel /printk_ratelimit来设置重新打开消息应该等待的秒数。

linux内核printk调试相关推荐

  1. linux内核printk调试手段,[Mini2440] 内核调试手段之 printk

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 一. 内核打印函数 printk 介绍 1.1 前言 内核提供了 printk 函数在内核运行时打印信息,类似于 C 语 ...

  2. linux内核printk调试手段,linux内核printk调试

    通过printk打你希望追踪的消息.从它的名字可以看出,这个东西有点类似与gnu c中的printf.不过用于内核的东西总该有些特色,printk添加了一些日志级别(loglevel),具体日志级别定 ...

  3. linux安全体系分析与编程pdf下载,linux内核printk调试(摘录《Linux安全体系分析与编程》)...

    asmlinkage long sys_syslog(int type, char __user * buf, int len) { return do_syslog(type, buf, len); ...

  4. Android Linux内核编译调试

    对于在Windows上写代码写习惯的人,调试是必不可少的手段,但是转到Android以后,发现调试手段异常简陋,跟Windows简直不是一个级别,特别是Android的内核调试,网上资料也相对较少,不 ...

  5. 学内核之二:基于QEMU搭建Linux内核运行调试环境

    目录 一 接续上文 二 编译根文件系统 三 构建完善根文件系统 四 内核中指定根文件系统 五 带根文件系统启动内核 一 接续上文 在上一篇文章中,我们展示了通过QEMU仿真软件来运行Linux内核的过 ...

  6. linux内核 printk实现,Linux内核调试技术之printk

    1.简介(基于s3c2440 linux) 在内核调试技术之中,最简单的就是printk的使用了,它的用法和C语言应用程序中的printf使用类似,在应用程序中依靠的是stdio.h中的库,而在lin ...

  7. linux 内核 printk 使用

    在linux 中,内核打印语句 printk() 会将内核信息输出到内核信息缓冲区中.内核信息缓冲区是一个环形缓冲区(ring buffer),因此,如果插入的信息过多,就会将之前的信息冲刷掉. pr ...

  8. linux内核单步调试,linux内核调试之KDB(1)

    说到linux调试人们第一想到的就是GDB,GDB是GNU开源组织发布的一个强大的UNIX下的程序调试工具,想必大家都比较熟悉,但是在调试模块时缺少一些至关重要的功能,它可用来查看内核的运行情况,包括 ...

  9. Linux 内核 printk打印

    调整内核打印信息级别: cat  /proc/sys/kernel/printk 7       7       1       7          其中第一个"7"表示内核打印 ...

最新文章

  1. minicom使用总结
  2. 整合用户篇—Oauth2理解与构造简单的系统
  3. 首届全球互联网通信云大会重磅来袭 融云邀您洞见通信未来
  4. 决策树 算法原理及代码
  5. ubuntu关闭服务需要身份验证
  6. 项目Beta冲刺(团队)总结
  7. 使用loadrunner录制winsock协议的程序(原创)
  8. 拓端tecdat|R语言基于Garch波动率预测的区制转移交易策略
  9. Filter_Listener:过滤器和监听器
  10. window下开启mysql慢查询和分割慢查询日志
  11. centos7配置IP地址
  12. 服务器的原理,服务器原理
  13. Tomcat安装配置及IDEA配置方法【亲测有效】
  14. 计算机二级VEP考试内容,2017计算机二级VEP知识点:报表设计与应用
  15. mac 解压rar压缩文件
  16. A Game of Thrones(23)
  17. 2022年最新全国城市/县/区天气查询API接口分享
  18. 一图秒懂:打开oracle归档模式,rman备份的前提条件
  19. 计算机网络之网络安全
  20. 11g 配置 dgmgrl 以及报错 DataGuard ORA-00313,

热门文章

  1. 内置函数、匿名函数,递归函数
  2. Lucene搜索引擎例子demo
  3. SEO是企业发展的永恒主题
  4. .net中关键字new的用法
  5. [深入浅出Cocoa]之消息(二)-详解动态方法决议(Dynamic Method Resolution)
  6. .NET不用代码生成器自己写一个生成Code的DLL 自动动态生成三层架构(一)概况...
  7. 苛评VCL: 接口与TObject
  8. Python学习笔记:数据库2
  9. STM32—— AHB、APB详解
  10. 【Python】if else 一行写完