这两个函数是经常用到的函数,闲暇之余,剖析下这两个函数的原理。这两个函数都是把字符串打印到终端上。其最终所要做的就是把存放在缓存区里的内容输出到串口。
printk
printk函数在kernel/printk.c中,其把主要工作交给了vprintk。vprintk经过vscnprintf把要打印的数据格式化 后存放到printk_buf缓存区中,然后通过emit_log_char把要打印的数据放到__log_buf里。emit_log_char保证了 __log_buf不会下标越界——因为每次到了缓存区末又从头开始存放数据。代码中使用new_text_line变量来判断当前字符是不是行首,因为 内核在配置下可能会在行首打印时间或者当前打印的级别。
真正调用打印的函数在console_unlock里面,在该函数里会执行call_console_drivers(_con_start, _log_end).接下来的调用流程是:
_call_console_drivers(start_print, end, msg_level);
-> __call_console_drivers(start, end);
-> for_each_console(con) { ... con->write(con, &LOG_BUF(start), end - start); ... }
for_each_console展开就是for (con = console_drivers; con != NULL; con = con->next),设置这个console_drivers的write方法流程如下所示:
start_kernel //init/main.c
-> console_init //drivers/tty/tty_io.c 所有和console相关的初始化函数在链接脚本里指定好了放在.con_initcall.init区
-> serial8250_console_init //drivers/tty/serial/8250.c
-> register_console(&serial8250_console); // 关键代码在于console_drivers = newcon;
这时就可以看到con->write其实就是serial8250_console.write,即serial8250_console_write。这个函数所做的就是对硬件进行操作。就不继续往下说了。
但是在调用console_init之前调用printk也能打印出信息,这是為什麼呢?在start_kernel函数中很早就调用了 parse_early_param函数,该函数会调用到链接脚本中.init.setup段的函数。其中就有 setup_early_serial8250_console函数。该函数通过 register_console(&early_serial8250_console);注册了一个比较简单的串口设备。可以用来打印内核启 动早期的信息。
printf
printf其本质就是通过write系统调用完成的。如果感兴趣可以用strace观察下。那么就从sys_write这个系统调用开始分析吧。该系统调用的定义位于fs/read_write.c 中:SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf, size_t, count)。打印出信息所要经过的流程如下:
vfs_write
-> redirected_tty_write // tty_io.c:tty_init 中设置file->f_op->write指向该函数
-> tty_write // 关键在于调用 ret = do_tty_write(ld->ops->write, tty, file, buf, count);
-> n_tty_write
-> process_output_block
-> uart_write
-> uart_start
-> __uart_start
-> serial8250_start_tx
-> transmit_chars
光从这个调用流程来看,就足够复杂了。可以用户态要打印一个字符可真不容易。
redirected_tty_write函数判断终端重定向(通过ioctl的TIOCCONS控制字)。
tty_write做一些检测,把任务交给了do_tty_write。
do_tty_write通过copy_from_user(tty->write_buf, buf, size)把要打印的字符拷贝到内核空间,再调用ld->ops->write函数。(注册ldisc还是在上文中的 console_init函数中。tty_ldisc_begin函数完成ldisc的设置。)
接下来的函数都可以顾名思义的。
為什麼用户程序的打印如此复杂呢?内核在用户和硬件中间加了一个tty层以保证设备驱动可以专心处理和硬件相关的事。而不必考虑复杂的数据格式化。