轉自: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