浅析2.6.24内核printk函数
文章来源:http://gliethttp.cublog.cn
【浅析printk的emit_log_char()算法具体实现】
有段时间没有看内核了从2月1号进入新的工作以后,虽然工作中使用的都是ubuntu,但是一方面以前从来没有在纯linux下工作过,另一方面对于
linux下各种应用软件和vim编辑器都很生疏,加上工作性质并不是对内核,所以这2个多月都在掌握linux的基本操作和vim的使用,应该说经过这
2个多月锻炼,已经能够在linux下轻松的工作了,尤其发现vim是一个超级好用功能强大的编辑软件,wmii是一个超级好用的窗口管理器,现在连
windows我也装上了gvim,因为linux环境已经熟悉了,所以以后可以多花些时间在我感兴趣的内核上了,下面进入主题:
printk是内核向外界打log的函数,对于2.6.24内核,一次性输入给printk函数的数据不能超过1k,在printk()-> vprintk()函数中使用了一个static char printk_buf[1024];临时缓冲区,用来处理一次传给printk的数据的格式化操作,最后将格式化了的临时存储到printk_buf数组中的数据通过emit_log_char函数一个一个的输送到log_buf[]--真正的所有待打印数据的缓冲区,这个缓冲区的大小在编译内核执行 make menuconfig时指定,我的CONFIG_LOG_BUF_SHIFT=14即16k,通过for循环将printk_buf中的所有待发数据输送到log_buf[]之后,紧接着执行
if (!down_trylock(&console_sem))
{
...
release_console_sem();
...
}
如果console驱动已经安装,那么将在执行release_console_sem()的时候调用console驱动,
static void call_console_drivers(unsigned long start, unsigned long end)
{
unsigned long cur_index, start_print;
static int msg_level = -1;
BUG_ON(((long)(start - end)) > 0);
cur_index = start;
start_print = start;
while (cur_index != end) {
if (msg_level < 0 && ((end - cur_index) > 2) &&
LOG_BUF(cur_index + 0) == '<' &&
LOG_BUF(cur_index + 1) >= '0' &&
LOG_BUF(cur_index + 1) <= '7' &&
LOG_BUF(cur_index + 2) == '>') {
msg_level = LOG_BUF(cur_index + 1) - '0';
cur_index += 3;
start_print = cur_index;
}
while (cur_index != end) {
char c = LOG_BUF(cur_index);
cur_index++;
if (c == '\n') {//gliethttp_20080428以\n换行符为组,调用一次console驱动,来发送数据
if (msg_level < 0) {
/*
* printk() has already given us loglevel tags in
* the buffer. This code is here in case the
* log buffer has wrapped right round and scribbled
* on those tags
*/
msg_level = default_message_loglevel;
}
_call_console_drivers(start_print, cur_index, msg_level);
msg_level = -1;
start_print = cur_index;
break;
}
}
}
_call_console_drivers(start_print, end, msg_level);
}
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)) {
/* wrapped write */
__call_console_drivers(start & LOG_BUF_MASK,
log_buf_len);
__call_console_drivers(0, end & LOG_BUF_MASK);
} else {
__call_console_drivers(start, end);
}
}
}
对于console的注册登记,由void register_console(struct console *console)函数完成,
比如对于driver驱动的注册由module_init()完成,而对于通过module_init方式编译进内核的驱动来说,
会在start_kernel()->rest_init()->kernel_thread建立内核线程kernel_init()->do_basic_setup()->do_initcalls()->中通过
for (call = __initcall_start; call < __initcall_end; call++)循环方式依次调用编译进内核的驱动初始化模块函数!
arch\arm\kernel\vmlinux.lds.s 中定义了
// __initcall_start = .;
// INITCALLS
// __initcall_end = .;
include\asm-generic\vmlinux.lds.h 中定义了这些东西
//#define INITCALLS \
// *(.initcall0.init) \
// *(.initcall0s.init) \
// *(.initcall1.init) \
// *(.initcall1s.init) \
// *(.initcall2.init) \
// *(.initcall2s.init) \
// *(.initcall3.init) \
// *(.initcall3s.init) \
// *(.initcall4.init) \
// *(.initcall4s.init) \
// *(.initcall5.init) \
// *(.initcall5s.init) \
// *(.initcallrootfs.init \
// *(.initcall6.init) \
// *(.initcall6s.init) \
// *(.initcall7.init) \
// *(.initcall7s.init
不过为了能够提早的将内核的log数据打印出来,2.6.24在start_kernel()中及早的调用console_init();函数来注册登记了打印输出log的专用console;
void __init console_init(void)
{
initcall_t *call;
/* Setup the default TTY line discipline. */
(void) tty_register_ldisc(N_TTY, &tty_ldisc_N_TTY);
/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
__con_initcall_start在arch\arm\kernel\vmlinux.lds.s 中定义
...
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
...
在include\linux\init.h中有如下定义
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
而console驱动们就是通过console_initcall来及早注册登记自己的,如:drivers\serial\atmel_serial.c
static int __init atmel_console_init(void)
{
if (atmel_default_console_device) {
add_preferred_console(ATMEL_DEVICENAME, atmel_default_console_device->id, NULL);
atmel_init_port(&(atmel_ports[atmel_default_console_device->id]), atmel_default_console_device);
register_console(&atmel_console);
}
return 0;
}
console_initcall(atmel_console_init);
又如:drivers/serial/pxa.c
static int __init
serial_pxa_console_init(void)
{
register_console(&serial_pxa_console);
return 0;
}
console_initcall(serial_pxa_console_init);
printk函数是原子的操作,一进入printk函数,就调用preempt_disable()来禁止内核调度器对当前执行printk函数调用的内核线程对自己进行调度,不让自己让出cpu,切换到其他内核线程,也同时禁止了irq中断函数可能引发的更高优先级的内核线程被调度,因为 preempt_disable()已经锁住了调度器,直到printk函数执行完毕之后,调用preempt_enable();内
核才重新获得可抢占性!从这里我们可以看出,如果printk数据量很大,那么内核调度器虽然能够在irq之类的地方根据优先级调度之类的算法登记需要立
即执行的内核线程,但是因为preempt_disable的原因,不能立即执行内核调度进行线程切换,所以本来需要立即执行的一个高优先级内核线程就只
能等待,等到 printk调用console->write驱动函数将所有数据发送完毕执行preempt_enable()之
后,内核调度器才会使本该早早获得cpu的执行权的更高优先级的内核线程推迟到现在才获得cpu,如果printk数据量很大,再加上串口输出速率比较
低,所以等待这些log数据发送完毕是需要几十个ms的,实际应用中需要注意,当然现在很多arm处理器都使用了dma传输,比如at91rm9200使
用uart的pdc模式,它的
dma缓冲区为4k,这样对于小于4k的数据传输,也就可以交给pdc来完成,cpu不需要block在printk中,这样的话,禁止内核抢占的时间也
就可以大为缩短了,但是我看到的console内核驱动程序并没有这样来实现,因为内核在console这方面想的更多的是通用,而console驱动设
计者自己也并没有把这个作为必须要实现的咚咚,除非到了必须,否则肯定是能省事就省事!(gliethttp_20080428)
|