引言
想了好久,還是覺得這個標題才配得上printk!^_^
我相信,不管做什么開發,使用最多的調試手段應該就是打印了,從我們學習編程語言第一課開始,寫的第一段代碼,就是打印"Hello, world"。
內核也不例外,到處都有打印語句,只不過在內核中的打印函數是printk,printk作為內核中不可或缺的基礎工具,幾乎可以在內核的任何上下文中都可以使用,正應了那句話,它明明是那么普通,卻又那么強大~~
關于printk的內容還挺多,所以分多篇深入介紹,這是第一篇,介紹printk的基本用法,后面介紹printk的進階用法。
如果本篇你已經熟悉,可以直接閱讀后面的內容,快捷跳轉:
Linux Kernel調試:強大的printk(二):pr_xxx相關的內容
Linux Kernel調試:強大的printk(三):dev_xxx相關的內容,以及限制打印速率
printk的特性
printk是一個在內核中使用的打印函數,用于向內核日志系統輸出信息,其通過將消息寫入內核日志緩沖區,然后由不同的工具讀取,比如 dmesg 命令或者查看 /var/log/syslog 文件(根據系統配置不同而有所變化)。
它對于調試和報告錯誤很有用,并且可以在中斷上下文中使用,但是使用時要小心:如果機器的控制臺中充斥著printk消息則會無法使用。
相比于用戶空間的printf,printk 有一些獨特的特性和用途:
-
日志級別:printk 允許你指定日志級別,這可以控制該消息是否顯示到控制臺。例如,緊急錯誤可能需要立即顯示,而調試信息則不一定。
-
緩沖區:printk 的輸出首先被寫入一個環形緩沖區(ring buffer)中。這意味著即使沒有控制臺,或者控制臺驅動程序尚未初始化,也可以記錄消息。你可以通過讀取 /proc/kmsg 或者使用 dmesg 命令來查看這些消息。
-
時間戳:每個 printk 消息都會附帶一個時間戳,這個時間戳是從系統啟動開始計算的時間,單位是秒和微秒。這對于調試非常有用,因為它可以幫助確定事件發生的順序和間隔。
-
異步行為:與 printf 不同,printk 并不保證消息會立刻出現在控制臺上。這是因為內核代碼不能阻塞等待 I/O 完成,所以 printk 實際上將消息放入了一個隊列中,稍后由另一個內核線程負責將其實際輸出到控制臺或其他目標。
printk的函數原型
printk定義于include/linux/printk.h文件中,原型如下:
#define printk(fmt, ...) printk_index_wrap(_printk, fmt, ##__VA_ARGS__)#define printk_index_wrap(_p_func, _fmt, ...) \({ \__printk_index_emit(_fmt, NULL, NULL); \_p_func(_fmt, ##__VA_ARGS__); \})int _printk(const char *fmt, ...);
printk的用法
printk的調用方法如下:
printk(KERN_INFO "My printk message\n");
這里的 KERN_INFO 是一個宏,它實際上只是一個字符串常量,用來表示這條消息的日志級別。其他可用的日志級別稍后會講。我們可以使用dmesg(1)之類的查看日志的工具根據日志級別進行過濾。
盡管 printk 很方便,但在性能關鍵的路徑中應謹慎使用,因為即使是簡單的日志記錄操作也可能引入顯著的開銷。另外,在一些時序要求嚴格的場合,一定要在關閉日志的情況下充分測試,因為有時打印日志會改變時序,會出現關閉log時,程序反而不能正常工作。
日志級別
日志級別在include/linux/kern_levels.h文件中定義:
#define KERN_SOH "\001" /* ASCII Start Of Header */
#define KERN_SOH_ASCII '\001'#define KERN_EMERG KERN_SOH "0" /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */#define KERN_DEFAULT "" /* the default kernel loglevel *//** Annotation for a "continued" line of log printout (only done after a* line that had no enclosing \n). Only to be used by core/arch code* during early bootup (a continued line is not SMP-safe otherwise).*/
#define KERN_CONT KERN_SOH "c"
-
可以看到,日志級別有0~7種,從名稱也可以看得出來,數值越小越緊急,優先級越高
-
KERN_SOH是一個控制字符,用于標記日志級別的開始,當解析到KERN_SOH后,其后的一個字符就是日志級別,或者KERN_CONT
-
KERN_CONT的意思是這是前一條消息的續行消息,不會添加時間戳、日志級別等新行信息,也不會換行
-
如果沒有解析到KERN_SOH則使用默認級別,KERN_DEFAULT定義為空,這就可以通過編譯時配置或者運行時設置默認的日志級別了
printk輸出到哪里
以下是 printk 輸出信息可能到達的設備或位置:
內核日志緩沖區
printk 首先將消息寫入內核日志緩沖區,這是一個環形緩沖區,用于臨時存儲內核消息,直到它們被用戶空間的工具(如 dmesg 或日志守護進程)讀取。
控制臺(Console)
根據內核的配置和日志級別,printk 輸出的消息可能會直接顯示在控制臺上。
-
虛擬控制臺(TTY):在本地終端(如 /dev/tty1 或 /dev/ttyS0)上顯示消息。
-
串行控制臺:通過串行端口(如 COM 端口)輸出消息,常用于服務器或嵌入式設備的調試。
-
圖形控制臺:在圖形界面的終端窗口中顯示消息(如 X 終端)。
通過內核啟動參數(如 console=ttyS0,115200)或運行時配置(如 dmesg 的日志級別設置)來控制哪些消息會顯示在控制臺上。
日志文件
用戶空間的日志守護進程(如 systemd-journald 或 rsyslogd)會從內核日志緩沖區讀取消息,并將它們寫入日志文件。
常見日志文件:
-
/var/log/syslog 或 /var/log/messages:系統日志文件,存儲內核消息和其他系統日志。
-
/var/log/dmesg:專門存儲內核日志的文件,通常由 dmesg 命令更新。
日志守護進程的配置文件(如 /etc/rsyslog.conf 或 /etc/systemd/journald.conf)決定了日志的存儲位置和格式。
網絡日志服務器
在某些配置下,內核日志可以通過網絡發送到遠程日志服務器,便于集中管理和監控。
實現方式:通過日志守護進程(如 rsyslogd)配置網絡日志功能,將日志消息發送到指定的遠程服務器。
其他輸出目標
-
串行端口:除了作為控制臺外,串行端口也可以被配置為單獨的日志輸出目標,用于調試或日志備份。
-
USB 調試接口:在一些嵌入式設備中,printk 輸出可以通過 USB 調試接口發送到連接的主機。
-
自定義設備:通過內核模塊或驅動程序,可以將 printk 輸出重定向到其他自定義設備(如存儲設備或專用的調試接口)。
用戶空間程序
用戶空間程序可以通過 /dev/kmsg 或 dmesg 等接口直接讀取內核日志,并進行進一步處理或顯示。
-
dmesg:用于顯示內核日志的命令行工具。
-
journalctl(systemd 系統):用于查看和管理內核日志的工具。
Ubuntu控制臺日志級別配置
我們在ubuntu上,有時會發現,有些log不會顯示到控制臺上,通過dmesg命令才可以看到,這是因為內核向控制臺輸出的信息取決于其日志級別,通過 /proc/sys/kernel/printk 文件控制,我們查看該文件內容如下:
這4個數字的含義如下:
第一個數字是控制臺日志級別(console_loglevel),表示只有嚴重程度高于或等于這個級別的內核消息會被打印到控制臺上。
第二個數字是默認的消息日志級別(default_message_loglevel),它設定了當調用內核函數
printk()
而沒有明確指定優先級時,給消息分配的默認優先級。第三個數字是最低的控制臺日志級別(minimum_console_loglevel),系統允許的最小控制臺日志級別。例如,如果這個值為
1
,則控制臺日志級別不能設置為0
(KERN_EMERG
)。第四個數字是默認的控制臺日志級別(default_console_loglevel),代表啟動時的默認控制臺日志級別。它是一個參考值,在某些情況下,系統可能會重置控制臺日志級別回到這個默認值。
所以第一個數字是4,就表示比KERN_WARNING級別高的打印信息才會出現在控制臺上。這在一定程度上過濾了一些并不是很重要的信息。
printk格式占位符
一些常見的printk格式占位符:
-
對于size_t和ssize_t類型(分別表示有符號和無符號整數),請分別使用%zu和%zd格式說明符
-
在打印內核空間中的地址(指針)時:?
-
非常重要:出于安全考慮,請使用%pK(它只會輸出哈希值,有助于防止信息泄露,這是一個嚴重的安全問題)
-
對于實際的指針,請使用%px查看實際地址(不要在生產環境中這樣做!)
-
對于物理地址,請使用%pa
-
-
若要將原始緩沖區打印為十六進制字符串,請使用%*ph(其中*由字符數量代替;對于少于65個字符的緩沖區使用此方法,對于更多字符的緩沖區則使用print_hex_dump_bytes()函數)。有多種變體可用(參見隨后的內核文檔鏈接)
完整的printk格式占位符列表,包括使用示例,請查閱內核文檔:
中文:Documentation/translations/zh_CN/core-api/printk-formats.rst
英文:Documentation/core-api/printk-formats.rst
內核模塊中使用printk
雖然在內核模塊中使用printk顯得很low(一般都是使用pr_xxx或者dev_xxx),但是由于我們這一篇就是講printk的,所以還是以printk為例寫一個內核模塊,來實際使用一下printk,關于寫內核模塊的知識這里不會講解,后面看看,如有必要,會補上這部分知識,現在缺少這部分知識的同學需要自己學習一下
下面是代碼,共有兩個文件printk_usage.c和Makefile文件,可到這里獲取https://gitee.com/coolloser/linux-kerenl-debug:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>// 定義模塊加載函數
static int __init printk_module_init(void)
{printk(KERN_INFO "====printk模塊加載成功!====\n");printk(KERN_DEBUG "這是一個DEBUG級別的信息\n");printk(KERN_INFO "這是一個INFO級別的信息\n");printk(KERN_WARNING "這是一個WARNING級別的信息\n");printk(KERN_ERR "這是一個ERROR級別的信息\n");printk(KERN_CRIT "這是一個CRITICAL級別的信息\n");printk(KERN_ALERT "這是一個ALERT級別的信息\n");printk(KERN_EMERG "這是一個EMERGENCY級別的信息\n");// 使用KERN_CONT繼續上一條日志消息printk(KERN_INFO "這是一條需要繼續的信息...");printk(KERN_CONT "...這是繼續的部分\n");// 使用KERN_DEFAULT設置默認日志級別printk(KERN_DEFAULT "這是默認日志級別的信息\n");return 0; // 返回0表示模塊加載成功
}// 定義模塊卸載函數
static void __exit printk_module_exit(void)
{printk(KERN_INFO "====printk模塊卸載成功!====\n");
}// 注冊模塊加載和卸載函數
module_init(printk_module_init);
module_exit(printk_module_exit);// 模塊信息
MODULE_LICENSE("GPL");
MODULE_AUTHOR("your_name");
MODULE_DESCRIPTION("一個簡單的printk內核模塊示例");
MODULE_VERSION("0.1");
# 定義模塊名稱
MODULE_NAME := printk_usage# 定義內核構建目錄,替換成你自己的路徑
KERNEL_BUILD_DIR := /home/leo/debug_kernel/linux-6.12.28# 定義目標文件
obj-m += $(MODULE_NAME).o# 默認目標
all:@echo "Building the $(MODULE_NAME) kernel module..."$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) modules# 清理目標
clean:@echo "Cleaning up the build environment..."$(MAKE) -C $(KERNEL_BUILD_DIR) M=$(PWD) clean
直接執行make
命令進行編譯:
會生成printk_usage.ko
為了測試哪些log會直接顯示到終端上,我們需要輸入ctrl+alt+F3
打開一個虛擬終端(tty3),然后登錄
執行如下命令加載模塊:
cd modules/001_printk_usage
sudo insmod printk_usage.ko
會顯示如下內容:
忽略中文亂碼問題^_^,還是能看到ERROR,CRITICAL,ALERT,EMERGENCY級別的log直接顯示到終端上了,這符合/proc/sys/kernel/printk文件控制的log級別
然后我們回到圖形界面(ctrl+alt+F1
),輸入如下命令:
sudo dmesg
可以看到如下內容:
模塊中打印的所有內容都可以看到,可以看到KERN_CONT對應的log接續在上一條的后面
總結
本篇介紹了printk的基礎知識和基本用法,最后通過一個內核模塊演示了printk的級別等用法,以及如何查看這些log,由于printk篇幅較長,一篇寫完太長,閱讀起來比較累,所以分多篇進行介紹,后面會介紹printk的進階用法,如pr_xxx和dev_xxx,還有printk_ratelimited等等的
請繼續閱讀下一篇:
Linux Kernel調試:強大的printk(二)