admin管理员组

文章数量:1576288

本文基于Linux-4.14

1.earlycon

early console,顾名思义,他表示的就是早期的console设备,主要用于在系统启动阶段的内核打印的输出,由于linux内核实际设备驱动模型还没有加载完成,所以早期的启动信息需要一个特殊的console用于输出log。

在系统初始化时通过cmdline参数来解析,代码如下:


./init/main.c:

static int __init do_early_param(char *param, char *val,

                 const char *unused, void *arg)

{

    const struct obs_kernel_param *p;



    for (p = __setup_start; p < __setup_end; p++) {

        if ((p->early && parameq(param, p->str)) ||

            (strcmp(param, "console") == 0 &&

             strcmp(p->str, "earlycon") == 0)

        ) {

            if (p->setup_func(val) != 0)

                pr_warn("Malformed early option '%s'\n", param);

        }

    }

    /* We accept everything at this stage. */

    return 0;

}



void __init parse_early_options(char *cmdline)

{

    parse_args("early options", cmdline, NULL, 0, 0, 0, NULL,

           do_early_param);

}

整个流程如下,系统启动阶段会读取cmdline,并且解析cmdline参数名字为earlycon的参数,然后执行do_early_param操作,其中的关键是调用p->setup_func,这个也就是earlycon驱动实现的内容,param_setup_earlycon函数:

首先来看内核实现的earlycon驱动:


./drivers/tty/serial/earlycon.c:

static int __init param_setup_earlycon(char *buf)

{

    int err;



    /*

     * Just 'earlycon' is a valid param for devicetree earlycons;

     * don't generate a warning from parse_early_params() in that case

     */

    if (!buf || !buf[0]) {

        if (IS_ENABLED(CONFIG_ACPI_SPCR_TABLE)) {

            earlycon_init_is_deferred = true;

            return 0;

        } else if (!buf) {

            return early_init_dt_scan_chosen_stdout();

        }

    }



    err = setup_earlycon(buf);

    if (err == -ENOENT || err == -EALREADY)

        return 0;

    return err;

}

early_param("earlycon", param_setup_earlycon);

上面的代码会创建一个如下结构体,用于和cmdline中的参数进行匹配:


struct obs_kernel_param {

    const char *str;

    int (*setup_func)(char *);

    int early;

};

然后我们来看关键的setup实现param_setup_earlycon->setup_earlycon:


./drivers/tty/serial/earlycon.c:

int __init setup_earlycon(char *buf)

{

    const struct earlycon_id **p_match;



    if (!buf || !buf[0])

        return -EINVAL;



    if (early_con.flags & CON_ENABLED)

        return -EALREADY;



    for (p_match = __earlycon_table; p_match < __earlycon_table_end;

         p_match++) {

        const struct earlycon_id *match = *p_match;

        size_t len = strlen(match->name);



        if (strncmp(buf, match->name, len))

            continue;



        if (buf[len]) {

            if (buf[len] != ',')

                continue;

            buf += len + 1;

        } else

            buf = NULL;



        return register_earlycon(buf, match);

    }



    return -ENOENT;

}

最后我们来看关键的setup实现setup_earlycon->register_earlycon:


static int __init register_earlycon(char *buf, const struct earlycon_id *match)

{

    int err;

    struct uart_port *port = &early_console_dev.port;



    /* On parsing error, pass the options buf to the setup function */

    if (buf && !parse_options(&early_console_dev, buf))

        buf = NULL;



    spin_lock_init(&port->lock);

    port->uartclk = BASE_BAUD * 16;

    if (port->mapbase)

        port->membase = earlycon_map(port->mapbase, 64);



    earlycon_init(&early_console_dev, match->name);

    err = match->setup(&early_console_dev, buf);

    if (err < 0)

        return err;

    if (!early_console_dev.con->write)

        return -ENODEV;



    register_console(early_console_dev.con);

    return 0;

}

最终会调用register_console注册printk输出对应的console。代码跟踪到这里,会发现以上的处理还都是内核的通用框架性代码,那么到底如何与实际的设备硬件对应起来呢?看上面的setup_earlycon中的match操作,实际上是通过查找__earlycon_table来匹配实际硬件的。可以看到以上的代码都是内核的通用代码,不需要开发人员再做处理的,但是对于驱动人员来说,必须要实现硬件驱动,也就是要把自己硬件相关的操作注册到__earlycon_table中。

在include/linux/serial_core.h中定义了如下的宏定义来便于添加__earlycon_table:



#define _OF_EARLYCON_DECLARE(_name, compat, fn, unique_id)      \

    static const struct earlycon_id unique_id           \

         EARLYCON_USED_OR_UNUSED __initconst            \

        = { .name = __stringify(_name),             \

            patible = compat,               \

            .setup = fn  };                 \

    static const struct earlycon_id EARLYCON_USED_OR_UNUSED     \

        __section(__earlycon_table)             \

        * const __PASTE(__p, unique_id) = &unique_id



#define OF_EARLYCON_DECLARE(_name, compat, fn)              \

    _OF_EARLYCON_DECLARE(_name, compat, fn,             \

                 __UNIQUE_ID(__earlycon_##_name))

2.console

上面第一章节介绍early console,它是一种启动阶段前期的console,启动到后期会切换为real console。这两者都属于console,那么到底什么才是console呢?先看下内核的实现:


void register_console(struct console *newcon)

{

	int i;

	unsigned long flags;

	struct console *bcon = NULL;

	struct console_cmdline *c;

	static bool has_preferred;



	if (console_drivers)       //内核注册的所有console drivers,如果发现要注册的console已经被注册过了,直接return

		for_each_console(bcon)

			if (WARN(bcon == newcon,

					"console '%s%d' already registered\n",

					bcon->name, bcon->index))

				return;



	/*

	 * before we register a new CON_BOOT console, make sure we don't

	 * already have a valid console

	 */

	if (console_drivers && newcon->flags & CON_BOOT) { //如果real console已经注册过,那么boot console就不允许再被注册了

		/* find the last or real console */

		for_each_console(bcon) {

			if (!(bcon->flags & CON_BOOT)) {

				pr_info("Too late to register bootconsole %s%d\n",

					newcon->name, newcon->index);

				return;

			}

		}

	}



	if (console_drivers && console_drivers->flags & CON_BOOT) //是否已经注册过boot console

		bcon = console_drivers;



	if (!has_preferred || bcon || !console_drivers)

		has_preferred = preferred_console >= 0; //如果real console还没有注册过,那么判断是否存在prefer console



	/*

	 *	See if we want to use this console driver. If we

	 *	didn't select a console we take the first one

	 *	that registers here.

	 */

	if (!has_preferred) { //如果不存在prefer console,那么把当前这个作为preferred console

		if (newcon->index < 0)

			newcon->index = 0;

		if (newcon->setup == NULL ||

		    newcon->setup(newcon, NULL) == 0) {

			newcon->flags |= CON_ENABLED;

			if (newcon->device) {

				newcon->flags |= CON_CONSDEV;

				has_preferred = true;

			}

		}

	}



	/*

	 *	See if this console matches one we selected on

	 *	the command line.

	 */

	for (i = 0, c = console_cmdline;

	     i < MAX_CMDLINECONSOLES && c->name[0];

	     i++, c++) {  //查找cmdline中传递的console配置项,如果发现匹配项,就根据cmdline传入的options执行setup操作(可能包含波特率等配置)

		if (!newcon->match ||

		    newcon->match(newcon, c->name, c->index, c->options) != 0) { //match函数为空或者match函数不匹配,那么执行默认match操作

			/* default matching */

			BUILD_BUG_ON(sizeof(c->name) != sizeof(newcon->name));

			if (strcmp(c->name, newcon->name) != 0) //默认的match操作其实就是比较name字符串是否相同

				continue;

			if (newcon->index >= 0 &&

			    newcon->index != c->index)

				continue;

if (newcon->index < 0)

				newcon->index = c->index;



			if (_braille_register_console(newcon, c))

				return;



			if (newcon->setup &&

			    newcon->setup(newcon, c->options) != 0)

				break;

		}

//执行到这里,说明match都已经成功了,直接enable console,并且把prefer设置为它

		newcon->flags |= CON_ENABLED;

		if (i == preferred_console) {

			newcon->flags |= CON_CONSDEV;

			has_preferred = true;

		}

		break;

	}

//执行到此,他的逻辑:

//1.如果real console,只有match了cmdline中的配置,才会enable它,否则直接return。

//2.如果是boot console,此时也是enable状态,那么也会继续运行到下面

	if (!(newcon->flags & CON_ENABLED))

		return;



	/*

	 * If we have a bootconsole, and are switching to a real console,

	 * don't print everything out again, since when the boot console, and

	 * the real console are the same physical device, it's annoying to

	 * see the beginning boot messages twice

	 */

	if (bcon && ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV)) 

//运行到此说明是从boot console到real console的切换,那么要注销所有的boot console

		newcon->flags &= ~CON_PRINTBUFFER;



	/*

*	Put this console in the list - keep the

	 *	preferred driver at the head of the list.

	 */

	console_lock();

	if ((newcon->flags & CON_CONSDEV) || console_drivers == NULL) { //把对应的console driver加入到链表中,此链表在printk时会使用到

		newcon->next = console_drivers;

		console_drivers = newcon;

		if (newcon->next)

			newcon->next->flags &= ~CON_CONSDEV;

	} else {

		newcon->next = console_drivers->next;

		console_drivers->next = newcon;

	}



	if (newcon->flags & CON_EXTENDED)

		if (!nr_ext_console_drivers++)

			pr_info("printk: continuation disabled due to ext consoles, expect more fragments in /dev/kmsg\n");



	if (newcon->flags & CON_PRINTBUFFER) {

		/*

		 * console_unlock(); will print out the buffered messages

		 * for us.

		 */

		logbuf_lock_irqsave(flags);

		console_seq = syslog_seq;

		console_idx = syslog_idx;

		logbuf_unlock_irqrestore(flags);

		/*

		 * We're about to replay the log buffer.  Only do this to the

		 * just-registered console to avoid excessive message spam to

		 * the already-registered consoles.

		 */

		exclusive_console = newcon;

	}

	console_unlock();

	console_sysfs_notify();



	/*

	 * By unregistering the bootconsoles after we enable the real console

	 * we get the "console xxx enabled" message on all the consoles -

	 * boot consoles, real consoles, etc - this is to ensure that end

	 * users know there might be something in the kernel's log buffer that

	 * went to the bootconsole (that they do not see on the real console)

	 */

	pr_info("%sconsole [%s%d] enabled\n",

		(newcon->flags & CON_BOOT) ? "boot" : "" ,

		newcon->name, newcon->index);

	if (bcon &&

	    ((newcon->flags & (CON_CONSDEV | CON_BOOT)) == CON_CONSDEV) &&

	    !keep_bootcon) {

		/* We need to iterate through all boot consoles, to make

		 * sure we print everything out, before we unregister them.

		 */

		for_each_console(bcon)

			if (bcon->flags & CON_BOOT)

				unregister_console(bcon);

	}

}

EXPORT_SYMBOL(register_console);

不管是early console还是real console都是通过register_console注册到内核中的,如果它是early console,那么直接enable它,如果它是一个real console设备,那么要匹配cmdline中配置的“console=”配置,只有匹配了cmdline的console才会使能,使能后的real console会加入到统一管理的链表:console_drivers。 这里引申一个问题?如果我们要同时使能一个serial console和一个ram console,那么要怎么配置,实际上要在cmdline中配上两个cmdline console,传入cmdline时可以通过“,”分割即可。

下面来看下cmdline参数是如何传入的:



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

         i++, c++) {

        if (strcmp(c->name, name) == 0 && c->index == idx) {  //判断是否已经添加过此console,不再重复添加

            if (!brl_options)

                preferred_console = i;

            return 0;

        }

    }

    if (i == MAX_CMDLINECONSOLES)

        return -E2BIG;

    if (!brl_options)

        preferred_console = i;

    strlcpy(c->name, name, sizeof(c->name)); //如果还没有加入到cmdline cosole数组,那么这里进行加入操作

    c->options = options;

    braille_set_options(c, brl_options);

    

    c->index = idx;

    return 0;

}





static int __init console_setup(char *str)

{

    char buf[sizeof(console_cmdline[0].name) + 4]; /* 4 for "ttyS" */

    char *s, *options, *brl_options = NULL;

    int idx;



    if (_braille_console_setup(&str, &brl_options))  //这里是针对盲人console设备的操作,不在进一步跟进,感兴趣的自己去看

        return 1;



    /*

     * Decode str into name, index, options.

     */

    if (str[0] >= '0' && str[0] <= '9') {

        strcpy(buf, "ttyS");

        strncpy(buf + 4, str, sizeof(buf) - 5);

    } else { 

        strncpy(buf, str, sizeof(buf) - 1);

    }

    buf[sizeof(buf) - 1] = 0; 

    options = strchr(str, ',');

    if (options)

        *(options++) = 0;

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

    idx = simple_strtoul(s, NULL, 10);

    *s = 0;



    __add_preferred_console(buf, idx, options, brl_options); //加入到cmdline console数组

    console_set_on_cmdline = 1;

    return 1;

}

__setup("console=", console_setup);

console_setup会把cmdline中的console参数解析并加入到一个数组中,在console_register时与这个数组中的console进行match操作,match成功才会enable使能它,并进行setup操作。

3.printk

console已经register注册成功了,那么printk的log是怎么输出到console的呢?


printk->vprintk_emit:

asmlinkage int vprintk_emit(int facility, int level,

                const char *dict, size_t dictlen,

                const char *fmt, va_list args)

{

    int printed_len;

    bool in_sched = false;

    unsigned long flags;



    if (level == LOGLEVEL_SCHED) {

        level = LOGLEVEL_DEFAULT;

        in_sched = true;

    }



    boot_delay_msec(level);

    printk_delay();



    /* This stops the holder of console_sem just where we want him */

    logbuf_lock_irqsave(flags);

    printed_len = vprintk_store(facility, level, dict, dictlen, fmt, args);

    logbuf_unlock_irqrestore(flags);



    /* If called from the scheduler, we can not call up(). */

    if (!in_sched) {

        /*

         * Disable preemption to avoid being preempted while holding

         * console_sem which would prevent anyone from printing to

         * console

         */

        preempt_disable();

        /*

         * Try to acquire and then immediately release the console

         * semaphore.  The release will print out buffers and wake up

         * /dev/kmsg and syslog() users.

         */

        if (console_trylock())

            console_unlock();

        preempt_enable();

    }



    return printed_len;

}

EXPORT_SYMBOL(vprintk_emit);

在每次printk中会执行一个console_unlock操作,它会触发console对外输出数据:


console_unlock-->call_console_drivers:

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



    if (!console_drivers)

        return;



    for_each_console(con) {

        if (exclusive_console && con != exclusive_console)

            continue;

        if (!(con->flags & CON_ENABLED))

            continue;

        if (!con->write)

            continue;

        if (!cpu_online(smp_processor_id()) &&

            !(con->flags & CON_ANYTIME))

            continue;

        if (con->flags & CON_EXTENDED)

            con->write(con, ext_text, ext_len);

        else

            con->write(con, text, len);

    }

}

call_console_drivers函数会遍历console driver并且输出logbuffer。

3. real console

上面介绍了这么多,都是内核核心框架的内容,那么如何跟实际的硬件联系在一起呢?驱动工程师要做些什么才能输出log到串口呢?遵循如下两步骤:

  • 先要注册对应的uart driver,其中会注册tty驱动

uart_register_driver

  • 绑定一个port到指定的uart driver,其中会触发配置操作,如果这个设备指定配置为console,那么会执行register_console

uart_add_one_port->uart_configure_port–>register_console

本文标签: 内核详解设备LinuxConsole