在高版本linux6.12.7源碼中,early console介紹,可參考《riscv架構下linux6.12.7實現early打印》文章。
1 什么是early打印
適配內核到新的平臺,基本環境搭建好之后,首要的就是要調通串口,方便后面的信息打印。
正常流程 init/main.c 中 start_kernel 入口,要到 console_init 之后才能真正打印,前面的打印,都是緩存在 printk 的 ringbuffer 中的。
如果在 console_init 前就異常了,此時就看不到打印信息,為了調試 console_init 前的狀態,需要能更早的打印,內核提供了一種 early 打印的方式,尤其是 riscv 平臺我們可以直接 ecall 調用 opensbi 的打印,這樣 opensbi 適配好之后,這里就可以直接使用。
earlyprintk 的實現依賴于特定的硬件平臺,并且通常與特定固件配合使用。
earlyprintk 是一個高級功能,主要用于內核開發和調試。在生產環境中,通常不需要啟用此功能,因為它可能會干擾系統的正常啟動過程,或暴露潛在的敏感信息。
從 earlyprintk 到串行控制臺的轉換,通常發生在內核初始化過程中,特別是在 register_console 函數被調用之后。這個函數負責注冊串行控制臺,并使其成為內核默認的打印信息輸出設備。一旦串行控制臺被注冊,內核就會開始使用它來輸出打印信息,而 earlyprintk 則不再被需要。
2 printk函數實現
printk函數代碼實現,如下所示:
// 1.riscv-linux-4.15/include/linux/printk.h:
#define pr_info(fmt, ...) \printk(KERN_INFO pr_fmt(fmt), ##__VA_ARGS__) 調用==>// 2.riscv-linux-4.15/kernel/printk/printk.c:
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;
}// 3.riscv-linux-4.15/kernel/printk/printk_safe.c:
__printf(1, 0) int vprintk_func(const char *fmt, va_list args)
{...return vprintk_default(fmt, args); 調用==>
}// 4.riscv-linux-4.15/kernel/printk/printk.c:
int vprintk_default(const char *fmt, va_list args)
{...r = vprintk_emit(0, LOGLEVEL_DEFAULT, NULL, 0, fmt, args); 調用==>return r;
}// 5.riscv-linux-4.15/kernel/printk/printk.c:
asmlinkage int vprintk_emit(int facility, int level,const char *dict, size_t dictlen,const char *fmt, va_list args)
{...printed_len = log_output(facility, level, lflags, dict, dictlen, text, text_len);...console_unlock(); 調用==>...
}// 6.riscv-linux-4.15/kernel/printk/printk.c:
void console_unlock(void)
{...call_console_drivers(ext_text, ext_len, text, len); 調用==>...
}// 7.riscv-linux-4.15/kernel/printk/printk.c:
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) { // 遍歷console_drivers中每個consoleif (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);elsecon->write(con, text, len); // 通過console的write函數打印內容}
}
在代碼中,從printk開始,層層分析,最后在call_console_drivers函數中,會遍歷console_drivers中每個console,并調用console的write函數來完成內容打印。
pr_info ==>
printk ==>
vprintk_func ==>
vprintk_default ==>
vprintk_emit ==>
console_unlock ==>
call_console_drivers ==>
con->write
3 console注冊
console結構體定義:
// riscv-linux-4.15/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 *);int (*match)(struct console *, char *name, int idx, char *options);short flags;short index;int cflag;void *data;struct console *next;
};
通過register_console函數,可以將一個console進行注冊,放入console_drivers鏈表中,如下:
// riscv-linux-4.15/arch/riscv/kernel/setup.c:
void __init setup_arch(char **cmdline_p)
{
#if defined(CONFIG_EARLY_PRINTK)if (likely(early_console == NULL)) {early_console = &riscv_sbi_early_console_dev;register_console(early_console); // 注冊early console}
#endif*cmdline_p = boot_command_line;...
}// early console定義
struct console riscv_sbi_early_console_dev __initdata = {.name = "early",.write = sbi_console_write,.flags = CON_PRINTBUFFER | CON_BOOT | CON_ANYTIME,.index = -1
};// early console的write函數
static void sbi_console_write(struct console *co, const char *buf,unsigned int n)
{int i;for (i = 0; i < n; ++i) {if (buf[i] == '\n')sbi_console_putchar('\r');sbi_console_putchar(buf[i]);}
}// riscv-linux-4.15/arch/riscv/include/asm/sbi.h:
#define SBI_CALL(which, arg0, arg1, arg2) ({ \register uintptr_t a0 asm ("a0") = (uintptr_t)(arg0); \register uintptr_t a1 asm ("a1") = (uintptr_t)(arg1); \register uintptr_t a2 asm ("a2") = (uintptr_t)(arg2); \register uintptr_t a7 asm ("a7") = (uintptr_t)(which); \asm volatile ("ecall" \: "+r" (a0) \: "r" (a1), "r" (a2), "r" (a7) \: "memory"); \a0; \
})/* Lazy implementations until SBI is finalized */
#define SBI_CALL_0(which) SBI_CALL(which, 0, 0, 0)
#define SBI_CALL_1(which, arg0) SBI_CALL(which, arg0, 0, 0)
#define SBI_CALL_2(which, arg0, arg1) SBI_CALL(which, arg0, arg1, 0)static inline void sbi_console_putchar(int ch)
{SBI_CALL_1(SBI_CONSOLE_PUTCHAR, ch);
}static inline int sbi_console_getchar(void)
{return SBI_CALL_0(SBI_CONSOLE_GETCHAR);
}
4 SBI_CALL
console的write函數:先調用sbi_console_putchar,再調SBI_CALL。
SBI_CALL實現的功能,可大致理解為:
SBI_CALL(which, arg0, arg1, arg2)
{ a0寄存器 = (uintptr_t)(arg0); a1寄存器 = (uintptr_t)(arg1); a2寄存器 = (uintptr_t)(arg2); a7寄存器 = (uintptr_t)(which); 執行ecall指令;
}
SBI_CALL宏,通過在RISC-V處理器上,執行ecall指令來調用一個服務:
- 它通過將參數,放入特定的寄存器(a0、a1、a2);
- 并將服務標識符(調用號),放入a7寄存器;
- 然后,它執行ecall指令,并返回a0寄存器的值作為結果。
ecall系統調用,會觸發異常(mcause寄存器定義的異常8或9)。只不過這種異常,是由U或S模式下,程序通過ecall指令,軟件觸發的異常,主要用于系統調用,實現一些底層調用,例如輸出打印信息到串口等。
可以看到,這里定義了很多調用號Timer、Console、IPI、Shutdown等,如下:
// riscv-linux-4.15/arch/riscv/include/asm/sbi.h:
#define SBI_SET_TIMER 0
#define SBI_CONSOLE_PUTCHAR 1
#define SBI_CONSOLE_GETCHAR 2
#define SBI_CLEAR_IPI 3
#define SBI_SEND_IPI 4
#define SBI_REMOTE_FENCE_I 5
#define SBI_REMOTE_SFENCE_VMA 6
#define SBI_REMOTE_SFENCE_VMA_ASID 7
#define SBI_SHUTDOWN 8
在這里,就是:
- 將調用號1放入a7寄存器,將欲打印字符ch放入a0寄存器,然后CPU執行ecall指令,就會觸發一個異常;
- 然后CPU會處理該異常,由于當前kernel運行在S模式,因此CPU會進入M模式,并跳轉到M模式異常處理入口(Open SBI),在固件OpenSBI的處理代碼中,會判斷當調用號為1時,將字符ch打印出來(打印的方式,可以通過Uart或HTIF)。
在riscv-pk開源項目中,也支持通過ecall指令,來使用Uart或HTIF輸出打印信息。