----------------------------------------------------------------------------------------------------------------------------

内核版本:linux 5.2.8
根文件系统:busybox 1.25.0
u-boot:2016.05
----------------------------------------------------------------------------------------------------------------------------

一、printk介绍

我们在学习C语言的时候,经常使用printf函数将内容输出到控制台,printf 是格式化输出函数,主要功能是向标准输出设备按规定格式输出信息。printf是C语言标准库函数,定义于头文件 <stdio.h>。

而在linux内核中是无法使用printf 函数的,取而代之的是printk函数。printk在内核源码中用来记录日志信息的函数,只能在内核源码范围内使用,用法类似于printf函数。

一个较大的差别在于printk支持多种日志级别,从而允许printk根据消息的等级选择性进行打印。

printk函数主要做两件事情:

  • 将信息记录日志文件中(一般为/var/log/message);
  • 调用控制台驱动来将信息输出到控制台;

1.1 日志缓冲区

printk将内核信息输出到内核信息缓冲区中,内核缓冲区在kernel/printk/printk.c中定义:

#define LOG_ALIGN __alignof__(struct printk_log)
#define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT)
#define LOG_BUF_LEN_MAX (u32)(1 << 31)
static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN);

内核日志缓冲区__log_buf的大小为$2^{CONFIG_LOG_BUF_SHIFT}$ ,CONFIG_LOG_BUF_SHIFT默认为17,即128KB,可以通过make menuconfig配置,对应的配置项为LOG_BUF_SHIFT。

1.2 日志级别

日志级别用来控制printk打印的这条信息是否在控制台上显示,linux内核共提供了8种不同的日志级别,分为级别 0~7。数值越大,表示级别越低,对应的消息越不重要。

linux内核日志级别相应的宏定义在include/linux/kern_levels.h 文件中。

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

#define KERN_DEFAULT    ""              /* the default kernel loglevel */

各个日志级别含义如下:

  • KERN_EMERG 表示紧急事件,一般是系统崩溃之前提示的消息;
  • KERN_ALERT 表示必须立即采取行动的消息;
  • KERN_CRIT 表示临界状态,通常涉及严重的硬件或软件操作失败;
  • KERN_ERR 用于报告错误状态,设备驱动程序会经常使用该级别来报告来自硬件的问题;
  • KERN_WARNING 对可能出现问题的情况进行警告,这类情况通常不会对系统造成严重的问题;
  • KERN_NOTICE 表示有必要进行提示的正常情形,许多与安全相关的状况用这个级别进行汇报;
  • KERN_INFO 表示内核提示信息,很多驱动程序在启动的时候,用这个级别打印出它们找到的硬件信息;
  • KERN_DEBUG 用于调试信息;

1.3 函数使用

在使用printk时,会将日志级别放到最开始的位置,使用方式如下:

printk(log_level "message...");

例如:

printk(KERN_EMERG   "GetIot: KERN_EMERGn");
printk(KERN_ALERT   "GetIot: KERN_ALERTn");
printk(KERN_CRIT    "GetIot: KERN_CRITn");
printk(KERN_ERR     "GetIot: KERN_ERRn");
printk(KERN_WARNING "GetIot: KERN_WARNINGn");
printk(KERN_NOTICE  "GetIot: KERN_NOTICEn");
printk(KERN_INFO    "GetIot: KERN_INFOn");
printk(KERN_DEBUG   "GetIot: KERN_DEBUGn");

若未设置日志级别,printk默认使用内核定义的全局变量default_message_loglevel作为的默认打印的日志级别。

当printk中的消息日志级别小于当前控制台日志级别时,printk要打印的信息才会在控制台打印出来,否则不会显示在控制台。

printk的用法与printf基本一致, 最大的区别是printk对%p的扩展,在应用层使用printf打印一个指针的地址时直接使用%p即可, 但是内核为了防止泄漏内存布局的敏感信息,直接使用%p输出的地址是经过hash处理的, 如果想实现和printf的%p一样的效果, printk需要使用%px。

1.4 查看日志

无论当前控制台日志级别是何值,即使没有在控制台打印出来,都可以通过下面两种方法查看日志:

  • 第一种是使用dmesg命令查看日志;
  • 第二种是通过cat /proc/kmsg来查看日志;

另外如果配置好并运行了syslogd 或klogd,没有在控制台上显示的printk的信息也会追加到/var/log/messages.log中。

1.4.1 dmesg

执行dmesg查看日志:

[root@zy:/]# dmesg
Booting Linux on physical CPU 0x0
Linux version 5.2.8 (root@zhengyang) (gcc version 4.8.3 20140320 (prerelease) (Sourcery CodeBench Lite 2014.05-29)) #23 Sun Mar 26 14:53:14 CST 2023
...
1.4.2 /proc/kmsg

执行cat /proc/kmsg查看日志:

[root@zy:/]# cat /proc/kmsg
<6>Booting Linux on physical CPU 0x0
<5>Linux version 5.2.8 (root@zhengyang) (gcc version 4.8.3 20140320 (prerelease) (Sourcery CodeBench Lite 2014.05-29)) #23 Sun Mar 26 14:53:14 CST 2023
...

1.5 调整日志级别

linux系统支持在运行时,通过proc文件系统查看和调整内核日志的输出等级。查看当前控制台的打印级别的命令如下:

[root@zy:/]# cat /proc/sys/kernel/printk
7       4       1       7

该文件有4个数字值,含义如下:

  • 控制台日志级别:优先级高于该值的消息将被打印至控制台;
  • 默认的消息日志级别:将用该优先级来打印没有优先级的消息(即 printk 没有指定消息级别);
  • 最低的控制台日志级别:控制台日志级别可被设置的最小值(最高优先级);
  • 默认的控制台日志级别:控制台日志级别的缺省值,如果没有指定控制台日志级别,则使用这个;

这四个值是在 kernel/printk/printk.c 中被定义的,如下:

int console_printk[4] = {
        CONSOLE_LOGLEVEL_DEFAULT,       /* console_loglevel */
        MESSAGE_LOGLEVEL_DEFAULT,       /* default_message_loglevel */
        CONSOLE_LOGLEVEL_MIN,           /* minimum_console_loglevel */
        CONSOLE_LOGLEVEL_DEFAULT,       /* default_console_loglevel */
};
EXPORT_SYMBOL_GPL(console_printk);

修改日志级别有两种方式,配置menuconfig和修改/proc/sys/kernel/printk文件。

1.5.1 配置menuconfig

修改CONFIG_MESSAGE_LOGLEVEL_DEFAULT的值,然后重新编译,更新内核。menuconfig配置路径如下:

Kernel hacking  --->
    printk and dmesg options  --->
        (4) Default message log level (1-7)

如下图所示:

1.5.2 在系统中修改

在系统运行期间,可以通过执行以下命令,修改当前控制态的日志级别:

echo "新的打印级别  4    1    7" > /proc/sys/kernel/printk

如屏蔽掉所有的内核printk打印,只需要把第一个数值调到最小值1或者0,此时可以敲如下命令:

echo "1       4       1      7" > /proc/sys/kernel/printk
1.5.3 打开调试日志

内核中的大部分驱动都使用了dev_dbg接口打印调试信息,日志级别为7,默认是不会输出到控制台的。在需要打印dev_dbg调试信息的驱动文件开头定义DEBUG宏,注意必须是在include/linux/device.h前面:

#define DEBUG

打开DEBUG宏是第一步,这个时候还是不能输出到控制台的,还必须要修改控制台日志级别为8。

二、其他常见用法

为了方便开发者使用,在linux内核中除了直接使用printk加消息级别的方式,内核还提供了pr_xx和dev_xx系列的打印接口,本质上,它们都是基于printk实现的。其中:

  • pr_xx系列函数简化了日志级别的使用;
  • dev_xx系列函数可以用于设备驱动程序中,便于打印设备相关的信息;

2.1  pr_xx系列函数

在 <linux/printk.h> 中定义了 pr_notice、pr_info、pr_warn、pr_err 等接口。使用这些 pr_xxx 接口,就可以省去指定消息级别的麻烦。

#define pr_emerg(fmt, ...)     printk(KERN_EMERG pr_fmt(fmt), ##__VA_ARGS__)
#define pr_alert(fmt, ...)     printk(KERN_ALERT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_crit(fmt, ...)      printk(KERN_CRIT pr_fmt(fmt), ##__VA_ARGS__)
#define pr_err(fmt, ...)       printk(KERN_ERR pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warning(fmt, ...)   printk(KERN_WARNING pr_fmt(fmt), ##__VA_ARGS__)
#define pr_warn pr_warning
#define pr_notice(fmt, ...)    printk(KERN_NOTICE pr_fmt(fmt), ##__VA_ARGS__)
#define pr_info(fmt, ...)      printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__)

#ifdef DEBUG
#define pr_devel(fmt, ...) 
    printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_devel(fmt, ...) 
    no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif
#if defined(CONFIG_DYNAMIC_DEBUG)
#define pr_debug(fmt, ...)            
    dynamic_pr_debug(fmt, ##__VA_ARGS__)
#elif defined(DEBUG)
#define pr_debug(fmt, ...) 
    printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#else
#define pr_debug(fmt, ...) 
    no_printk(KERN_DEBUG pr_fmt(fmt), ##__VA_ARGS__)
#endif

如上各pr_xx定义,都是对printk+日志级别的封装,故都能和prink一样在linux内核中直接使用;

但是对于pr_devel需要在定义了DEBUG宏,pr_debug需要在定义了DEBUG或CONFIG_DYNAMIC_DEBUG宏才有实意。

2.2 dev_xx系列函数

在 <linux/dev_printk.h> 里也提供了一些驱动模型诊断宏,例如dev_err、dev_warn、dev_info等等。使用它们,不仅可以按标记的消息级别打印,还会打印对应的设备和驱动信息,这对于驱动调试来说相当重要。

#define dev_emerg(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_emerg, KERN_EMERG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_crit(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_crit, KERN_CRIT, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_alert(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_alert, KERN_ALERT, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_err(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_err, KERN_ERR, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_warn(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_warn, KERN_WARNING, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_notice(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_notice, KERN_NOTICE, dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_info(dev, fmt, ...) 
    dev_printk_index_wrap(_dev_info, KERN_INFO, dev, dev_fmt(fmt), ##__VA_ARGS__)

#if defined(CONFIG_DYNAMIC_DEBUG) || 
    (defined(CONFIG_DYNAMIC_DEBUG_CORE) && defined(DYNAMIC_DEBUG_MODULE))
#define dev_dbg(dev, fmt, ...)                        
    dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
#elif defined(DEBUG)
#define dev_dbg(dev, fmt, ...)                        
    dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#else
#define dev_dbg(dev, fmt, ...)                        
({                                    
    if (0)                                
        dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__); 
})
#endif

如上dev_xx定义,其和pr_xx类似,都可以直接理解为printk+日志级别的封装,一般在linux驱动程序中使用;

同样需要注意,dev_dbg也不是无条件打印的,除非定义了DEBUG或设定了CONFIG_DYNAMIC_DEBUG。一个相关约定是在已经开启了 DEBUG 时,使用VERBOSE_DEBUG来添加dev_vdbg。

2.3 测试

目前在内核驱动代码中,都不再建议直接使用printk直接添加打印信息,而是使用dev_info、dev_dbg、dev_err之类的函数代替,虽然这些 dev_xxx 函数的本质还是使用printk打印的,但是相比起printk,dev_xxx 的好处是:

  • 支持打印模块信息、dev信息;
  • 支持动态调试(dynamic debug)方式;

比如,如下代码:

dev_warn(&rtc->dev, "invalid alarm value: %ptRn", &alarm->time);  // %ptR输出时间

输出日志如下:

rtc rtc0: invalid alarm value: 1900-02-01T00:00:00

三、实现原理

之前我们说printk会将日志输出到控制台,比如我们在uboot的bootargs里配置“console=ttySA0,115200”,这样将会将日志信息输出在串口UART0上。

同样如果配置“console=tty1 console=ttySA0,115200”,将会同时将将日志信息输出到串口UART0以及LCD屏幕上。

显然printk根据命令行参数来调用不同控制台的硬件处理函数来输出日志。接下来我们研究一下printk函数的源码。

3.1 _setup

首先定位到kernel/printk/printk.c文件,以下代码:

_setup("console=", console_setup);

其中_setup宏的作用就是:若uboot传递进来的bootargs字符包含"console=",就调用console_setup函数,并对"console="后面的字符串“ttySAC0,115200”进行解析。

3.1.1 console_setup

console_setup函数定义如下:

/*
 * Set up a console.  Called via do_early_param() in init/main.c
 * for each "console=" parameter in the boot command line.
 */
static int __init console_setup(char *str)    // "ttySAC0,115200"
{
        char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for "ttyS" 声明一个字符数组,长度为20 */
        char *s, *options, *brl_options = NULL;
        int idx;

        if (_braille_console_setup(&str, &brl_options))
                return 1;

        /*
         * Decode str into name, index, options.
         */
        if (str[0] >= '0' && str[0] <= '9') {    // 首字符是数字,则将其驱动名设置为ttySn
                strcpy(buf, "ttyS");
                strncpy(buf + 4, str, sizeof(buf) - 5);
        } else {
                strncpy(buf, str, sizeof(buf) - 1);   // 直接拷贝 buf="ttySAC0,115200"
        }
        buf[sizeof(buf) - 1] = 0;
        options = strchr(str, ',');   // 返回第一个,字符位置 此时options=",115200"
        if (options)
                *(options++) = 0;     // options="115200" str="ttySAC0"
#ifdef __sparc__
        if (!strcmp(str, "ttya"))
                strcpy(buf, "ttyS0");
        if (!strcmp(str, "ttyb"))
                strcpy(buf, "ttyS1");
#endif
        for (s = buf; *s; s++)    
                if (isdigit(*s) || *s == ',')
                        break;                // s = ",115200"
        idx = simple_strtoul(s, NULL, 10);    // 执行完 idx=0  s="0,115200"
        *s = 0;                               // 将'0'修改为0 buf="ttySAC"

        __add_preferred_console(buf, idx, options, brl_options);
        console_set_on_cmdline = 1;
        return 1;
}

函数中console_cmdline是一个全局数组,数组长度为8,用来存放控制台的信息:

/*
 *      Array of consoles built from command line options (console=)
 */

#define MAX_CMDLINECONSOLES 8

static struct console_cmdline console_cmdline[MAX_CMDLINECONSOLES];

上面的代码最终调用__add_preferred_console("ttySAC", 0, "115200",...)。

补充:console_cmdline类型定义如下:

struct console_cmdline
{
        char    name[16];                       /* Name of the driver  驱动名称    */
        int     index;                          /* Minor dev. to use   次设备号    */
        char    *options;                       /* Options for the driver  选项 */
#ifdef CONFIG_A11Y_BRAILLE_CONSOLE
        char    *brl_options;                   /* Options for braille driver */
#endif
};
3.1.2 __add_preferred_console

__add_preferred_console函数定义如下,该函数将控制台信息添加到console_cmdline全局数组,该数组及全局preferred_console ,在register_console中会使用到。

static int __add_preferred_console(char *name, int idx, char *options,
                                   char *brl_options)
{
        struct console_cmdline *c;
        int i;

        /*
         *      See if this tty is not yet registered, and
         *      if we have a slot free.
         */
        for (i = 0, c = console_cmdline;
             i < MAX_CMDLINECONSOLES && c->name[0];  // MAX_CMDLINECONSOLES为8,表示最多添加8个控制台  判断要添加的控制台信息在console_cmdline数组是否已经存在
             i++, c++) {
                if (strcmp(c->name, name) == 0 && c->index == idx) {   // 比较名称和索引
                        if (!brl_options)
                                preferred_console = i; //设置全局selected_console索引号  
                        return 0;
                }
        }
        if (i == MAX_CMDLINECONSOLES)  //判断console_cmdline数组是否满了 
                return -E2BIG;
        if (!brl_options)
                preferred_console = i;
        strlcpy(c->name, name, sizeof(c->name));    // 添加控制台信息到console_cmdline数组第i个元素中
        c->options = options;
        braille_set_options(c, brl_options);

        c->index = idx;
        return 0;
}

3.2 控制台注册

register_console函数就用来注册控制台的,我们在linux驱动移植-UART设备驱动小节中介绍过uart_driver:

static struct uart_driver s3c24xx_uart_drv = {
        .owner          = THIS_MODULE,
        .driver_name    = "s3c2410_serial",
        .nr             = CONFIG_SERIAL_SAMSUNG_UARTS,  // 串口数量定义为4个,实际S3C2440只有3个
        .cons           = S3C24XX_SERIAL_CONSOLE,
        .dev_name       = S3C24XX_SERIAL_NAME,     // 设备名称ttySAC
        .major          = S3C24XX_SERIAL_MAJOR,    // 主设备号 204 
        .minor          = S3C24XX_SERIAL_MINOR,    // 次设备号 64
};

其中cons成员存储的是控制台,定义如下:

static struct console s3c24xx_serial_console = {
        .name           = S3C24XX_SERIAL_NAME,        // 控制台名称 ttySAC
        .device         = uart_console_device,        // 控制台设备
        .flags          = CON_PRINTBUFFER,            // 标志位
        .index          = -1,                         // 索引值
        .write          = s3c24xx_serial_console_write,  // 串口输出 
        .setup          = s3c24xx_serial_console_setup,  // 设置串口波特率、发送、接收等功能 
        .data           = &s3c24xx_uart_drv,             // 串口驱动uart_driver
};


#define S3C24XX_SERIAL_CONSOLE &s3c24xx_serial_console

控制台s3c24xx_serial_console的注册位于drivers/tty/serial/samsung.c:

static int __init s3c24xx_serial_console_init(void)
{
        register_console(&s3c24xx_serial_console);
        return 0;
}
console_initcall(s3c24xx_serial_console_init);

 在register_console函数里便会通过ttySAC来匹配console_cmdline[i]的名称,当匹配成功,printk调用的console结构体便是s3c24xx_serial_console了。

3.3 printfk

接下来我们看一下printk函数调用栈,首先定位到printk函数,函数定义在kernel/printk/printk.c

/**
 * printk - print a kernel message
 * @fmt: format string
 *
 * This is printk(). It can be called from any context. We want it to work.
 *
 * We try to grab the console_lock. If we succeed, it's easy - we log the
 * output and call the console drivers.  If we fail to get the semaphore, we
 * place the output into the log buffer and return. The current holder of
 * the console_sem will notice the new output in console_unlock(); and will
 * send it to the consoles before releasing the lock.
 *
 * One effect of this deferred printing is that code which calls printk() and
 * then changes console_loglevel may break. This is because console_loglevel
 * is inspected when the actual printing occurs.
 *
 * See also:
 * printf(3)
 *
 * See the vsnprintf() documentation for format string extensions over C99.
 */
asmlinkage __visible int printk(const char *fmt, ...)
{
        va_list args;
        int r;

        va_start(args, fmt);
        r = vprintk_func(fmt, args);
        va_end(args);

        return r;
}

该函数最终会调用vprintk_func,参数args和fmt的值就是我们printk传入的参数。

va_start和va_end宏定义如下:

#define va_start(v, l) __builtin_va_start(v, l)    
#define va_end(v) __builtin_va_end(v)             
3.3.1 vprintk_func

vprintk_func函数定义在kernel/printk/printk_safe.c

//关于vprintk_func()函数,首先来看如下的宏定义:
#define __printf(a, b) __attribute__((__format__(printf, a, b)))
//该宏定义主要通过__format__属性,来让编译器按照printf()函数的参数格式来对函数的参数进行检查。

__printf(1, 0) int vprintk_func(const char *fmt, va_list args)
{
        /*
         * Try to use the main logbuf even in NMI. But avoid calling console
         * drivers that might have their own locks.
         */
        if ((this_cpu_read(printk_context) & PRINTK_NMI_DIRECT_CONTEXT_MASK) &&
            raw_spin_trylock(&logbuf_lock)) {
                int len;

                len = vprintk_store(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args);
                raw_spin_unlock(&logbuf_lock);
                defer_console_output();
                return len;
        }

        /* Use extra buffer in NMI when logbuf_lock is taken or in safe mode. */
        if (this_cpu_read(printk_context) & PRINTK_NMI_CONTEXT_MASK)
                return vprintk_nmi(fmt, args);

        /* Use extra buffer to prevent a recursion deadlock in safe mode. */
        if (this_cpu_read(printk_context) & PRINTK_SAFE_CONTEXT_MASK)
                return vprintk_safe(fmt, args);

        /* No obstacles. */
        return vprintk_default(fmt, args);
}

可以看到,这个函数有三个分支:vprintk_nmi、vprintk_safe、vprintk_default。其中:

  • vprintk_default:正常走的分支;
  • vprintk_nmi:在nmi中断中调用printk走的分支;
  • vprintk_safe:在不安全的上下文中调用printk走的分支。

下面我们主要以vprintk_default为例分析。

3.3.2 vprintk_default

我们知道,printk最终会将输出信息保存在一个buffer中。如果多核同时调用printk,则最简单的情况,都走到vprintk_default分支,最终是由logbuf_lock_irqsave以及logbuf_unlock_irqrestore来完成并发处理的。

int vprintk_default(const char *fmt, va_list args)
{
    int r;
//假设配置中,未开启KGDB,则直接执行vprintk_emit函数。
#ifdef CONFIG_KGDB_KDB
    if (unlikely(kdb_trap_printk && kdb_print_cpu < 0)) {
        r = vkdb_printf(KDB_MSGRC_PRINTK, fmt, args);
    }
#endif
    r = vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args);
    //这里,默认日志等级为-1,即#define LOGLEVEL_DEFAULT -1。
    return r;
}

asmlinkage int vprintk_emit(int facility, int level, const char *dict, size_t ditclen, const char *fmt, va_list args)
{
    int printed_len;
    bool in_sched = false, pending_output;
    unsigned long flags;
    u64 curr_log_seq;

    if (unlikely(suppress_printk))
    //suppress_printk为全局只读变量,假设该值当前为0。
        return 0;
        
    if (level == LOGLEVEL_SCHED) {
    //在当前情况下,level并不为LOGLEVEL_SCHED,因此条件不成立
        level = LOGLEVEL_DEFAULT;
        in_sched = true;
    }
    //延时
    boot_delay_msce(level);
    prinkt_delay();
    
    logbuf_lock_irqsave(flags);
    curr_log_seq = log_next_seq;
    printed_len = vprintk_store(facility, level, dict, dictlen, fmt, args);
    //将所要输出的内容进行寄存
    pending_output = (curr_log_seq != log_next_seq)
    logbuf_unlock_irqrestore(flags);
    if (!in_sched && pending_output) {
    //in_sched为false,且pending_output为true。因此,该条件成立
        ...
        preempt_disable();
        if (console_trylock_spinning())
            console_unlock();
            //将日志缓冲中的内容进行打印
        premmpt_enable();
    } 
    ...
}

//寄存所要输出的内容
int vprintk_store(int facility, int level, const char *dict, size_t dictlen, const char *fmt, va_list args)
{
    static char textbuf[LOG_LINE_MAX];
    char *text = textbuf;
    size_t text_len;
    enum log_flags lflags = 0;

    text_len = vscnprintf(text, sizeof(textbuf), fmt, args);
    //该函数最终通过调用vsprintf()函数,vsprintf函数通过对fmt进行解析,将参数按照fmt格式填入,并将最终字符串写入text中。text_len表示写入的字节数。
    if (text_len && text[text_len - 1] == 'n') {
        text_len--;
        lflags |= LOG_NEWLINE;
    }

    if (facility == 0) {
        int kern_level;
        while ((kern_level = printk_get_level(text)) != 0) {
        //如果text的首字符为‘01’,且第二字符为0~7或c,则返回第二字符。否则,返回0。假设当前返回值为0。
            switch (kern_level) {
                case '0' ... '7':
                    if (level == LOGLEVEL_DEFAULT)
                        level = kern_level - '0';
                    break;
                case 'c':
                    lflags |= LOG_CONT; 
            }
            text_len -= 2;
            text += 2;
        }
    }

    if (level == LOGLEVEL_DEFAULT)
    //如果等级为默认的,则将其值更改为dafault_message_loglevel。
        level = default_message_loglevel;

    if (dict)
        lflags |= LOG_NEWLINE;
    //开始调用日志输出函数。
    return log_output(facility, level, lflags, dict, dictlen, text, text_len);
}

static size_t log_output(int facility, int level, enum log_flags lflags, const char *dict, size_t dictlen, char *text, size_t text_len)
{
    const u32 caller_id = printk_caller_id();
    //首先获取一个caller_id,即当前的进程号。
    
    if (cont.len) {
    //cont为类型为struct cont的全局变量,启动阶段cont.len为0。跳过该执行条件
        if (cont.caller_id == caller_id && (lflags & LOG_CONT)) {
            if (cont_add(caller_id, facility, level, lflags, text, text_len))
                return text_len;
        }
        cont_flush();
    }
    
    if (!text_len && (lflags & LOG_CONT))
        return 0;
    if (!(lflags & LOG_NEWLINE)) {
    //假设当前输出的内容为完整的一行内容,也会跳过该执行条件
        if (cont_add(caller_id, facility, level, lflags, text, text_len))
            return text_len;
    }
    //开始寄存所要输出的内容
    return log_store(caller_id, facility, level, lflags, 0, dict, dictlen, text, text_len);
}

static int log_store(u32 caller_id, int facility, int level, enum log_flags flags, u64 ts_nsec, const char *dict, u16 dict_len, const char *text, u16 text_len)
{
    ...
    msg = (struct printk_log *)(log_buf + log_next_idx);
    //申请struct printk_log结构体对象
    memcpy(log_text(msg), text, text_len);
    //将所要输出的内容复制到申请的struct printk_log结构体对象中
    msg->text_len = text_len;
    ...

    return msg->text_len;
    //结束整个操作
}
View Code

在上述代码中出现了两个结构体,这里对它们进行简单的说明:

struct printk_log {
    u64 ts_nsec;
    u16 len;
    u16 text_len;
    u16 dict_len;
    u8 facility;
    u8 flags:5;
    u8 level:3;
#iddef CONFIG_PRINTK_CALLER
    u32 caller_id
#endif
}
#ifdef CONFIG_HAVE_EFFICIENT_UNALIGNED_ACCESS
__packed __aligned(4)
#endif
;

static struct cont {
    char buf[LOG_LINE_MAX];
    size_t len;
    u32 caller_id;
    u64 ts_nesc;
    u8 level;
    u8 facility;
    enum log_flags flags;
} cont;

综上,可以知道printk函数将所要输出的内容全部存储到一段空间中。随后该段空间被使用,从而打印出内容。打印日志信息由上述解析过程中的console_unlock函数来完成。

void console_unlock(void)
{
    static char ext_text[CONSOLE_EXT_LOG_MAX];
    static char text[LOG_LINE_MAX + PREFIX_MAX];
    ...
    for (;;) {
        ...
        call_console_drivers(ext_text, ext_len, text, len);
        //调用控制台驱动
        ...
    }
    ...
}

static void call_console_drivers(const char *ext_text, size_t ext_len, const_char *text, size_t len)
{
    struct console *con;
    trace_console_rcuidle(text, len);
    for_each_console(con) {
    //遍历以console_drivers为表头的链表,访问每一个已经注册的console,并调用该console中定义的write方法来打印日志信息
        ...
        if (con->flags & CON_EXTENDED)
            con->write(con, ext_text, ext_len);
        else
            con->write(con, text, len);
            //通用模式下的打印日志信息方法
    }
}
3.3.3 early_console

关于上述代码中打印日志信息的方法,主要在注册console时来指定的,比如:在启动阶段的早期,如果内核中配置了CONFIG_EARLY_PRINTK选项,则会注册控制台early_console。如下:

static void early_console_write(struct console *con, const char *s, unsigned n)
{
    while (n-- && *s) {
        if (*s == 'n')
            prom_putchar('r');
        prom_putchar(*s);
        s++;
    }
}

struct console early_console_prom = {
    .name = "early",
    .write = early_console_write,
    .flags = CON_PRINTBUFFER | CON_BOOT,
    .index = -1
};

void __init setup_early_printk(void)
{
    if (early_console)
        return;
    early_console = &early_console_prom;
    register_console(&early_console_prom);
}

void register_console(struct console *newcon)
{
    ...
    struct console *bcon = NULL;
    ...
    for_each_console(bcon) {
    //遍历console链表,链表头为全局变量console_drivers,此时改变量为空,因此该条件并不成立
        if (WARN(bcon == newcon, "console '%s%d' already registeredn", bcon->name, bcon->index))
            return;
    }
    ...
    if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) {
    //该条件此时成立,因此执行该条件操作
        newcon->next = console_drivers;
        console_drivers = newcon;
    //上述两步操作,将新的console添加到console_driver链表中,且以其为表头
        if (newcon->next)
            newcon->next->flags &= ~CON_CONSDEV;
    } else {
        newcon->next = console_drivers->next;
        console_drivers->next = newcon;
    }
    ...
}
3.3.4 s3c24xx_serial_console_write

在内核启动我们又注册了控制台s3c24xx_serial_console,其write函数为s3c24xx_serial_console_write。定义在drivers/tty/serial/samsung.c::

static int
s3c24xx_serial_console_txrdy(struct uart_port *port, unsigned int ufcon)
{
        struct s3c24xx_uart_info *info = s3c24xx_port_to_info(port);
        unsigned long ufstat, utrstat;

        if (ufcon & S3C2410_UFCON_FIFOMODE) {
                /* fifo mode - check amount of data in fifo registers... */

                ufstat = rd_regl(port, S3C2410_UFSTAT);
                return (ufstat & info->tx_fifofull) ? 0 : 1;
        }

        /* in non-fifo mode, we go and use the tx buffer empty */

        utrstat = rd_regl(port, S3C2410_UTRSTAT);
        return (utrstat & S3C2410_UTRSTAT_TXE) ? 1 : 0;
}

static bool
s3c24xx_port_configured(unsigned int ucon)
{
        /* consider the serial port configured if the tx/rx mode set */
        return (ucon & 0xf) != 0;
}


static void
s3c24xx_serial_console_putchar(struct uart_port *port, int ch)
{
        unsigned int ufcon = rd_regl(port, S3C2410_UFCON);

        while (!s3c24xx_serial_console_txrdy(port, ufcon))
                cpu_relax();
        wr_regb(port, S3C2410_UTXH, ch);    // UTXH UART发送缓冲区寄存器
}


static void
s3c24xx_serial_console_write(struct console *co, const char *s,
                             unsigned int count)
{
        unsigned int ucon = rd_regl(cons_uart, S3C2410_UCON);

        /* not possible to xmit on unconfigured port */
        if (!s3c24xx_port_configured(ucon))
                return;

        uart_console_write(cons_uart, s, count, s3c24xx_serial_console_putchar);
}

3.4 总结

综上分析,可知当执行printk函数时:

  • 首先会将所要输出的信息寄存到日志缓冲区;
  • 随后遍历所有的控制台,检查其是否满足当前要求,如果满足,则调用该控制台所指定的write函数,从而打印信息;

所以,关于内核启动时的日志打印,也需要在注册某个console之后,再次调用printk函数来进行日志的输出。

因此,内核在启动阶段首先注册了用于启动阶段的console,即early_console。随后,又初始化了内核启动之后的console,即s3c24xx_serial_console。与此同时,将前边注册的early_console进行了注销。

参考文章

[1]34.Linux-printk分析、使用__FILE__, __FUNCTION__, __LINE__ 调试

[2]Linux内核之 printk 打印(转)

[3]内核printk原理介绍

[4]How to get printk format specifiers right

[5]【Linux内核】printk日志输出格式控制

[6]linux console驱动详解

[7]printk()函数分析(转)

内容来源于网络如有侵权请私信删除

文章来源: 博客园

原文链接: https://www.cnblogs.com/zyly/p/17259493.html

你还没有登录,请先登录注册
  • 还没有人评论,欢迎说说您的想法!

相关课程