第14章 Linux终端设备驱动之printk 和 early_printk console驱动

14.7printk early_printk console驱动

    在 Linux 内核中,printk()是最常用的调试手段。printk()的打印消息会放入一个环形缓冲区(RingBuffer),而/proc/kmsg 文件用于描述这个环形缓冲区。通过 dmesg 命令或 klogd 可以读取该环形缓冲区。如果用户空间的 klogd 守护进程在运行,klogd将获取内核消息并分发给 syslogd,syslogd 接着检查/etc/syslog.conf 来找出如何处理它们。

    内核 printk 信息支持 8 个级别,优先级从高到低(数值越高,级别越低,消息越不重要)分别是:KERN_EMERG(数值为0)、KERNEL_ALERT、KERN_CRIT、KERN_ERR、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG(数值为7)。当调用 printk()函数时指定的优先级小于指定的控制台优先级 console_loglevel 时,调试消息显示在控制台终端。默认的的 console_loglevel 值是 DEFAULT_CONSOLE_LOGLEVEL,用户可以使用系统调用 sys_syslog 或 klogd  -c 来修改 console_loglevel 值,也可以直接 echo 值到/proc/sys/kernel/printk。/proc/sys/kernel/printk文件包含四个整数值,第一个表示系统当前的优先级,第二个表示系统默认的优先级。

    在 Linux 中,用于 printk 输出的是内核 console,用 console 结构体来描述,如代码清单 14.19 所示。

代码清单 14.19 用于 printk 的 console 结构体

include/linux/console.h

struct console {
        char    name[16];
        void    (*write)(struct console *, const char *, unsigned);
        int     (*read)(struct console *, char *, unsigned);
        struct tty_driver *(*device)(struct console *, int *);
        void    (*unblank)(void);
        int     (*setup)(struct console *, char *);
        short   flags;
        short   index;
        int     cflag;
        void    *data;
        struct   console *next;

};

分析:

    其中,较关键的是 write()和 setup()成员函数,write()用于将打印消息写入 console,setup()用于设置 console 的特性,如波特率、停止位等。

    printk()函数经过重重调用,经过_ _call_console_drivers()函数,最终调用 console 的 write()成员函数将控制台消息打印出去,如代码清单 14.20 所示。

代码清单 14.20 printk()最终调用到 console 的 write()成员函数

kernel/printk.c

/*
 * Call the console drivers on a range of log_buf
 */
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);
        }
}

内核提供如下 API 用于注册和注销 console:

include/linux/console.h

kernel/printk.c

void register_console(struct console *console);

int unregister_console(struct console *console);

        在内核 init/main.c 文件中的 start_kernel()函数中,会调用 console_init()函数,该函数会调用位于内核存放 console 初始化函数的代码段,调用其中的每一个初始化 console 的函数,如代码清单 14.21。

代码清单 14.21 console_init()函数

drivers/char/tty_io.c

/*
 * Initialize the console device. This is called *early*, so
 * we can't necessarily depend on lots of kernel help here.
 * Just do some early initializations, and do the complex setup
 * later.
 */
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++;
        }
}

    对于任何一个初始化 console 的函数,只需要通过 console_initcall()进行包装,即可把它放入.con_initcall.init 段(起始地址为_ _con_initcall_start),如最常用的 8250 对应的 console 结构体以及初始化代码如清单 14.22。

代码清单 14.22 8250 的 console 及 console_initcall

drivers/serial/8250.c

static struct console serial8250_console = {
        .name           = "ttyS",
        .write          = serial8250_console_write,
        .device         = uart_console_device,
        .setup          = serial8250_console_setup, // 设置
        .flags          = CON_PRINTBUFFER,
        .index          = -1,
        .data           = &serial8250_reg,
};

static int __init serial8250_console_init(void)
{
        serial8250_isa_init_ports();
        register_console(&serial8250_console);
        return 0;
}

console_initcall(serial8250_console_init);

console_initcall()是一个宏,定义于 include/linux/init.h 文件。

#define console_initcall(fn) \
        static initcall_t __initcall_##fn \

        __attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn

展开为:

#define console_initcall(serial8250_console_init) \
        static initcall_t __initcall_serial8250_console_init \

        __attribute_used__ __attribute__((__section__(".con_initcall.init")))=serial8250_console_init

__section(.con_initcall.init),是一个链接阶段的指示,表明将指定的函数放入.con_initcall.init 段

console_init()是由 init/main.c 文件中的 start_kernel()函数调用的,在 console_init ()被调用前,还执行了一系列的操作。为了在 console_init()被调用前就能使用 printk(),可以使用内核的“early printk”支持,该选项位于内核配置菜单“Linux Kernel Configuration”下的“ Kernel hacking”菜单之下。

    对于 early printk 的 console 的注册通过解析内核的 early_param 完成,例如对于 8250,定义“earlycon”一个内核参数,当解析这个内核参数时,相应地被 early_param ()绑定的函数 setup_early_serial8250_console()被调用,此函数将注册一个用于 early printk 的 console。

代码清单 14.23 8250 的 early printk console

static struct console early_serial8250_console __initdata = {
         .name = "uart",
         .write = early_serial8250_write,
         .flags = CON_PRINTBUFFER | CON_BOOT,
         .index = -1,

 };

 int _ _init setup_early_serial8250_console(char *cmdline)
 {
         char *options;
         int err;

         options = strstr(cmdline, "uart8250,");
         if (!options) {
                 options = strstr(cmdline, "uart,");
         if (!options)
                 return 0;
         }

         options = strchr(cmdline, ',') + 1;

         err = early_serial8250_setup(options);

        if(err < 0)
                 return err;

         register_console(&early_serial8250_console);
         return 0;
 }

early_param("earlycon", setup_early_serial8250_console);//注册一个用于 early printk 的 console

例如,在 Linux 启动的 command line 中设置如下参数,将使能 8250 作为 early printk 的 console。

earlycon=uart8250,mmio,0xff5e0000,115200n8

earlycon=uart8250,io,0x3f8,9600n8

总结:

    代码清单 14.22 第 7 行的 flags 和代码清单 14.23 第 4 行的 flags 的区别,后者多出一个CON_BOOT 属性。

    所有的具有CON_BOOT 属性的console 都会在内核初始化至late initcall 阶段的时候被注销,注销函数是disable_boot_consoles(),其定义如代码清单14.24。

代码清单 14.24 disable_boot_consoles()函数

 static int _ _init disable_boot_consoles(void)
 {
         if (console_drivers != NULL) {
                 if (console_drivers->flags & CON_BOOT) {
                         printk(KERN_INFO "turn off boot console %s%d\n",
                         console_drivers->name, console_drivers->index);
                         return unregister_console(console_drivers);
                 }
         }
         return 0;
 }

 late_initcall(disable_boot_consoles);

disable_boot_consoles()被 late_initcall()修饰,因此被放入到.initcall7.init 段中。

补充知识:

     内核的 initcall 分成 8 级,对应的段分别为.initcall0.init、.initcall1.init、

.initcall2.init、.initcall3.init、.initcall4.init、.initcall5.init、.initcall6.init、.initcall7.init,分别通过 

pure_initcall(fn)、core_initcall(fn) 、postcore_initcall(fn) 、arch_initcall(fn)、subsys_initcall(fn)、fs_initcall(fn)、device_initcall(fn)、late_initcall(fn)可将指定的函数放入对应的段。对于 pure_initcall(),指
定的 initcall 不依赖于任何其他部分,因此,其指定函数只能 built-in(编入镜像),不能在模块中。对于 1~7
级,还存在对应的 sync 版本,分别通过 core_initcall_sync(fn)、postcore_initcall_sync(fn)、arch_
initcall_sync(fn)、subsys_initcall_sync(fn)、fs_initcall_sync(fn)、device_initcall_sync(fn)、late_initcall_sync(fn)修饰。


  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值