Linux 內核console設備實現詳解【轉】


轉自:https://blog.csdn.net/rikeyone/article/details/95482978

本文基於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), \

.compatible = 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
————————————————
版權聲明:本文為CSDN博主「程序猿Ricky的日常干貨」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/rikeyone/article/details/95482978


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM