剛剛開始做Linux相關(guān)開發(fā)工作時,深感Linux內(nèi)核代碼龐大,要加些自己的驅(qū)動進內(nèi)核代碼樹,常常深陷bug的泥沼難以自拔,今天來分享一下內(nèi)核調(diào)試利器printk的使用心得。
前面一段時間很忙,后期更文頻率會漸漸回歸正常頻率,盡量會保證每周一到兩更。感謝各位朋友的關(guān)注而沒有棄我而去,我定不負厚愛,會持續(xù)輸出些日常技術(shù)工作中的心得體會,如對朋友們有些許幫助,也煩請幫忙點個贊或者在看(這并不會對各位有何不利的影響哈~~~),這也是對我堅持持續(xù)輸出的大大激勵!
printk初接觸
Linux內(nèi)核啟動之后常會看見很多信息打印出來,這在底層是printk子系統(tǒng)實現(xiàn)的,其實現(xiàn)代碼在./kernel/printk/中實現(xiàn)的。
一個小小的打印,對于內(nèi)核而言也需要考慮很多方面,需要考慮到多核、中斷、緩沖以及用戶空間接口。對于用戶空間接口很多朋友或許會很疑惑。
其中/dev/kmsg字符設(shè)備就是printk子系統(tǒng)實現(xiàn)的內(nèi)核打印字符設(shè)備。如果利用文件操作寫這個設(shè)備就最終會以printk形式輸出,如果讀這個設(shè)備最終就會返回printk歷史,你如不信不妨用這個命令試試:
cat /dev/kmsg
看到這里或許有朋友會問,為啥有的文章提到用/proc/kmsg去讀取內(nèi)核打印緩沖區(qū)的日志用以調(diào)試。來分析一下:
/proc/kmsg
/proc/kmsg僅為root用戶提供內(nèi)核日志緩沖區(qū)的只讀操作。等效于通過SYSLOG_ACTION_READ
操作調(diào)用[syslog(2)
]。
一個進程必須具有超級用戶特權(quán)才能讀取此文件,并且只有一個進程應(yīng)讀取該文件。如果正在運行使用syslog(2)系統(tǒng)調(diào)用記錄內(nèi)核消息的syslog進程,則不應(yīng)讀取該文件。
這里補充說一點是,/proc文件系統(tǒng)本質(zhì)上是偽文件系統(tǒng),它提供了內(nèi)核數(shù)據(jù)結(jié)構(gòu)的接口。它一般掛載在/proc上。通常情況下,它是由系統(tǒng)自動掛載的的,但是也可以使用以下命令手動安裝:
mount -t proc proc /proc
大部分位于/proc下的文件屬于只讀特性,但也有少部分是可寫的。但是對于/proc/kmsg而言則是只讀的。
/dev/kmsg
/dev/kmsg提供對同一內(nèi)核日志緩沖區(qū)的訪問,但以一種更易于使用的方式。每次打開都會對讀取進行跟蹤,因此可以并行讀取多個進程,并且在讀取條目時不會將其從緩沖區(qū)中刪除。/dev/kmsg還提供對日志緩沖區(qū)的寫訪問權(quán),因此可用于將條目添加到日志緩沖區(qū)。
那么為什么兩者都存在,以及為什么一個存在于/proc中和而另一個存在于/dev中,/proc/kmsg是歷史設(shè)計,而/dev/kmsg是較新引入的,被設(shè)計為日志緩沖區(qū)的可用接口。該接口也實現(xiàn)了用戶空間添加記錄進內(nèi)核日志系統(tǒng)的可能。
其代碼實現(xiàn)也可以簡單來瞅瞅:
printk使用
printk怎么打印的呢?想必做嵌入式開發(fā)的一定熟悉printf函數(shù),那么從范式上printk也比較類似,但也有很多不同。且看:
內(nèi)核打印,界定了日志級別,其語法范式:
printk([KERN_LOG_LEVEL] "Message: %s\n", arg);
比如:
printk(KERN_DEBUG “Here is: %s:%i\n”, __FILE__, __LINE__);
那么有哪些日志級別,又各有何區(qū)別呢?
日志級別
對于這個表,或許剛使用時會不知所措,這么多級別到底該傳入什么級別呢?我的理解如果是自己定義的驅(qū)動按照字面意思理解,靈活使用即可。唯一需要注意的時候,不同的級別打印或許在控制臺會有不同的體現(xiàn),這取決于控制臺打印的配置。
格式化
下面內(nèi)容來源于./Documentation/printk-formats.txt,整理于此方便使用:
- 基本變量
注意:內(nèi)核打印不支持浮點,%n也不支持,%e, %f, %g, %a也不支持,如使用了會導(dǎo)致WARN。
- 指針
除上面描述的這些格式化,printk還支持格式化打印塊設(shè)備名、IPv4、IPv6地址、網(wǎng)絡(luò)設(shè)備屬性、MAC/FDDI地址、UUID/GUID地址等等。如需要用到可查閱該文檔獲取更為詳細的信息。
修改控制臺打印級別
運行時修改
在調(diào)試過程中,或許會發(fā)現(xiàn)有的printk信息沒有打印出來,那么肯定是默認運行中內(nèi)核控制臺printk打印級別低于代碼中使用的級別,那么如果不想重新編譯內(nèi)核,有沒有辦法動態(tài)修改呢?來看看怎么修改:
在/proc/sys/kernel/printk文件中,有4個屬性分別對應(yīng):
- 當前控制臺日志級別
- 默認日志級別
- 最小日志級別
- 啟動階段默認日志級別
使用下面命令可以當前控制臺printk日志級別:
echo 6 > /proc/sys/kernel/printk
這里傳入6,表示小于6級別的打印都將會被打印出來。這里可以根據(jù)需要傳入不同的值。取值參見前表<日志級別>。如想將所有的信息都打印出來,傳入8即可,如:
echo 8 > /proc/sys/kernel/printk
如果你想將這些打印記錄進一個文件,則可以使用klogd進行重定向,比如:
klogd -o -f ./kernel.msg
編譯修改
如果你想將某一模塊的內(nèi)核打印在編譯時使能,這樣做的好處是在模塊加載過程中的所有的信息在控制臺都可以看到,你還可以增加你感興趣的代碼添加打印信息,用以輔助調(diào)試。這怎么實現(xiàn)呢?
這里需要去看看你的內(nèi)核模塊代碼是以何種方式去調(diào)用printk的,比如有的代碼這樣調(diào)用:
static int tea5764_i2c_probe(struct i2c_client *client,
const struct i2c_device_id *id)
{
struct tea5764_device *radio;
struct v4l2_device *v4l2_dev;
struct v4l2_ctrl_handler *hdl;
struct tea5764_regs *r;
int ret;
PDEBUG("probe");
.....
這里的PDEBUG其實就是printk的一種宏重包裝:
#define PINFO(format, ...)\
printk(KERN_INFO KBUILD_MODNAME ": "\
DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
#define PWARN(format, ...)\
printk(KERN_WARNING KBUILD_MODNAME ": "\
DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
# define PDEBUG(format, ...)\
printk(KERN_DEBUG KBUILD_MODNAME ": "\
DRIVER_VERSION ": " format "\n", ## __VA_ARGS__)
還有的是這樣:
static int ad9467_spi_read(struct spi_device *spi, unsigned reg)
{
unsigned char buf[3];
int ret;
if (spi) {
buf[0] = 0x80 | (reg >> 8);
buf[1] = reg & 0xFF;
ret = spi_write_then_read(spi, &buf[0], 2, &buf[2], 1);
dev_dbg(&spi->dev, "%s: REG: 0x%X VAL: 0x%X (%d)\n",
__func__, reg, buf[2], ret);
if (ret < 0)
{
dev_dbg(&spi->dev, "spi_write_then_read failed %s: REG: 0x%X VAL: 0x%X (%d)\n",
__func__, reg, buf[2], ret);
return ret;
}
return buf[2];
}
return -ENODEV;
}
dev_dbg其本質(zhì)上也是調(diào)用的printk,來看看,在./include/linux/device.h中
#if defined(CONFIG_DYNAMIC_DEBUG)
#define dev_dbg(dev, fmt, ...) \
dynamic_dev_dbg(dev, dev_fmt(fmt), ##__VA_ARGS__)
#elif defined(DEBUG)
#define dev_dbg(dev, fmt, ...) \
dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__)
#else
#define dev_dbg(dev, fmt, ...) \
({ \
if (0) \
dev_printk(KERN_DEBUG, dev, dev_fmt(fmt), ##__VA_ARGS__); \
})
#endif
要把這些調(diào)試信息從控制臺給打印出來,可以這樣做:
- 修改一下默認打印機別,在./inlcude/linux/printk.h中,直接修改其默認值,8表示全放出來。
#define CONSOLE_LOGLEVEL_DEFAULT 8//CONFIG_CONSOLE_LOGLEVEL_DEFAULT
#define CONSOLE_LOGLEVEL_QUIET CONFIG_CONSOLE_LOGLEVEL_QUIET
- 在模塊頂端添加宏定義
/*添加宏定義DEBUG開關(guān)*/
#define DEBUG
#include <linux/module.h>
#include <linux/device.h>
#include <linux/kernel.h>
當然,你也可以通過makefile來定義這個宏,找到你模塊所在的模塊,添加如下語句:
DEBUG = y
推薦使用device.h中的定義的一系列宏,對應(yīng)了不同日志級別。
#define dev_emerg(dev, fmt, ...) \
_dev_emerg(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_crit(dev, fmt, ...) \
_dev_crit(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_alert(dev, fmt, ...) \
_dev_alert(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_err(dev, fmt, ...) \
_dev_err(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_warn(dev, fmt, ...) \
_dev_warn(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_notice(dev, fmt, ...) \
_dev_notice(dev, dev_fmt(fmt), ##__VA_ARGS__)
#define dev_info(dev, fmt, ...) \
_dev_info(dev, dev_fmt(fmt), ##__VA_ARGS__)
這有什么好處呢,因為這樣可以將模塊的設(shè)備名給打印出來。比如我在調(diào)試一個IIO設(shè)備時,其關(guān)聯(lián)的SPI控制接口到底發(fā)了些什么控制命令,通過這種方式就可以非常清楚的看到驅(qū)動調(diào)用了什么設(shè)備,寫了哪些寄存器,寫的什么值。
ad9467 spi1.0: ad9467_spi_write: REG: 0x5 VAL: 0x1 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0xD VAL: 0x0 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0x5 VAL: 0x3 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0xFF VAL: 0x1 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0xFF VAL: 0x0 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0x5 VAL: 0x2 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0xD VAL: 0x0 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0x5 VAL: 0x3 (0)
ad9467 spi1.0: ad9467_spi_write: REG: 0xFF VAL: 0x1 (0)
仍然沒有看到?
如果你配了這些,甚至編譯了,可是你還是沒有看到打印信息,那么可能printk沒有使能,在哪里使能呢?
CONFIG_PRINTK宏是內(nèi)核打印的編譯開關(guān),大概率是這個配置沒有使能。
總結(jié)一下
內(nèi)核模塊的調(diào)試還有很多其他的手段,printk則是一個非常高效的調(diào)試手段,所有如何比較好的利用printk進行打印調(diào)試,是做內(nèi)核模塊調(diào)試一個必要掌握的手段,至于printk的內(nèi)部實現(xiàn)其實也較為復(fù)雜,這塊代碼則沒有必要深究,當然如果從學(xué)習(xí)的角度去分析分析其代碼如何實現(xiàn)的,也是不錯的。好了,本期就分享到這里,咱們下期見~