本節學習目的 1)分析printk()函數 2)使用printk()調試驅動 1.在驅動調試中,使用printk(),是最簡單,最方便的辦法 當uboot的命令行里的“console=tty1”時,表示printk()輸出在開發板的LCD屏上 當uboot的命令行里的“console=ttySA0, ...
本節學習目的
- 1)分析printk()函數
- 2)使用printk()調試驅動
1.在驅動調試中,使用printk(),是最簡單,最方便的辦法
當uboot的命令行里的“console=tty1”時,表示printk()輸出在開發板的LCD屏上
當uboot的命令行里的“console=ttySA0,115200”時,表示printk()輸出在串口UART0上,波特率=115200
當uboot的命令行里的“console=tty1 console=ttySA0,115200”時,表示printk()同時輸出在串口上,以及開發板的LCD屏上
顯然printk(),還是根據命令行參數來調用不同控制台的硬體處理函數
內核又是怎麼根據上面命令行參數來確定printk()的輸出設備?
2.我們以“console=ttySA0,115200”為例,進入linux-2.6.22.6\kernel\printk.c
找到以下一段:
__setup("console=", console_setup);
其中__setup()的作用就是:
若uboot傳遞進來的命令行字元串里含有“console=”,便調用console_setup()函數,並對“console=”後面帶的字元串"ttySA0,115200"進行分析
3.我們以*str= "ttySA0,115200"為例,console_setup()函數如下所示
static int __init console_setup(char *str) //*str="ttySA0,115200" { char name[sizeof(console_cmdline[0].name)]; // char name[8] char *s, *options; int idx; /* * Decode str into name, index, options. */ if (str[0] >= '0' && str[0] <= '9') { strcpy(name, "ttyS"); strncpy(name + 4, str, sizeof(name) - 5); } else { strncpy(name, str, sizeof(name) - 1); //*name="ttySA0, " } name[sizeof(name) - 1] = 0; //*name="ttySA0" if ((options = strchr(str, ',')) != NULL) //找到',',返回給options,所以options=",115200" *(options++) = 0; //*options="115200", *str="ttySA0" #ifdef __sparc__ if (!strcmp(str, "ttya")) strcpy(name, "ttyS0"); if (!strcmp(str, "ttyb")) strcpy(name, "ttyS1"); #endif for (s = name; *s; s++) //*s="0" if ((*s >= '0' && *s <= '9') || *s == ',') break; idx = simple_strtoul(s, NULL, 10); //和strtoul()一樣,將s中的"0"提出來,所以idx=0 *s = 0; //將"ttySA0"中的"0"設為0,所以*name="ttySA" add_preferred_console(name, idx, options); //*name="ttySA" // idx=0 //*options="115200" return 1; }
通過上面的代碼和註釋得到, 最終調用add_preferred_console("ttySA", 0, "115200")函數來添加控制台
4.進入console_setup()->add_preferred_console()
int __init add_preferred_console(char *name, int idx, char *options) { struct console_cmdline *c; int i; /* MAX_CMDLINECONSOLES=8,表示最多添加8個控制台*/ for(i = 0; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[0]; i++) if (strcmp(console_cmdline[i].name, name) == 0 &&console_cmdline[i].index == idx) // console_cmdline[]是一個全局數組,用來匹配要添加的控制台是否重覆 { selected_console = i; return 0; //在console_cmdline[]中,已經存有要添加的控制台,所以return } if (i == MAX_CMDLINECONSOLES) //i==8,表示數組存滿了 return -E2BIG; selected_console = i;
/*將命令行的控制台信息存在console_cmdline[i]中*/ c = &console_cmdline[i]; memcpy(c->name, name, sizeof(c->name)); c->name[sizeof(c->name) - 1] = 0; c->options = options; c->index = idx; return 0; }
上面函數,最終將控制台的信息放到了console_cmdline[]全局數組中,那接下來來搜索該數組,看看printk()如何調用控制台的硬體處理函數的。
搜索到在linux-2.6.22.6\kernel\Printk.c里的register_console(struct console *console)函數,有用到console_cmdline[]
顯然,register_console()函數就用來註冊控制台的,繼續搜索register_console
如下圖所示,找到很多CPU的控制台驅動初始化:
5.我們以2410為例(linux-2.6.22.6\drivers\serial\S3c2410.c):
static int s3c24xx_serial_initconsole(void) { ... ... register_console(&s3c24xx_serial_console); return 0; } console_initcall(s3c24xx_serial_initconsole); //聲明控制台初始化函數
上面通過register_console()來註冊s3c24xx_serial_console結構體,該結構體成員如下所示:
static struct console s3c24xx_serial_console = { .name = S3C24XX_SERIAL_NAME, //控制台名稱 .device = uart_console_device, //tty驅動 .flags = CON_PRINTBUFFER, //標誌 .index = -1, /索引值 .write = s3c24xx_serial_console_write, //列印串口數據的硬體處理函數 .setup = s3c24xx_serial_console_setup //用來設置UART的波特率,發送,接收等功能 };
該結構體的名稱如下圖所示:
在register_console()里,便會通過“ttySAC”來匹配console_cmdline[i]的名稱,當匹配成功,printk()調用的console結構體便是s3c24xx_serial_console了
6.接下來,分析printk()又是如何調用s3c24xx_serial_console結構體的write(),來列印信息的
printk()函數如下所示
asmlinkage int printk(const char *fmt, ...) { va_list args; int r; va_start(args, fmt); r = vprintk(fmt, args); //調用vprintk() va_end(args); return r; }
其中args和fmt的值就是我們printk代入的參數
7.然後進入printk()->vprintk():
asmlinkage int vprintk(const char *fmt, va_list args) { unsigned long flags; int printed_len; char *p; static char printk_buf[1024]; //臨時緩衝區 static int log_level_unknown = 1; preempt_disable(); //關閉內核搶占 ... ... /*將輸出信息發送到臨時緩衝區printk_buf[] */ printed_len = vscnprintf(printk_buf, sizeof(printk_buf), fmt, args); /*拷貝printk_buf數據到迴圈緩衝區log_buf[],如果調用者沒提供合適的列印級別,插入預設值*/ for (p = printk_buf; *p; p++) { ... ... /*判斷printk列印的列印級別,也就是首碼值"<0>"至 "<7>"*/ if (p[0] == '<' && p[1] >='0' && p[1] <= '7' && p[2] == '>')
{ loglev_char = p[1]; //獲取列印級別字元,將級別放入 loglev_char中 p += 3; printed_len -= 3; }
else
{ //若沒有列印級別,便插入預設值,比如printk("abc"),會變為printk("<4>abc") loglev_char = default_message_loglevel+ '0'; } ... ... //開始拷貝到迴圈緩衝區log_buf[] } /* cpu_online():檢測CPU是否線上 have_callable_console():檢測是否有註冊的控制台*/ if (cpu_online(smp_processor_id()) || have_callable_console()) { console_may_schedule = 0; release_console_sem(); //調用release_console_sem()向控制台列印信息 } else { /*釋放鎖避免刷新緩衝區*/ console_locked = 0; up(&console_sem); } lockdep_on(); local_irq_restore(flags); //恢複本地中斷標識 } ... .... }
從上面的代碼和註釋來看,顯然vprintk()的作用就是:
- 1)將列印信息放到臨時緩衝區printk_buf[]
- 2)從臨時緩衝區printk_buf[]複製到迴圈緩衝區log_buf[]
- ->2.1)每次拷貝前都要檢查列印級別,若沒有列印級別,便插入預設值default_message_loglevel
- 3)最後檢查是否有註冊的控制台,若有,便調用release_console_sem()
7.1 那麼列印級別"<0>"至 "<7>"到底是什麼?
發現printk的列印級別 在include/linux/kernel.h中找到:
#define KERN_EMERG "<0>" // 系統崩潰 #define KERN_ALERT "<1>" //必須緊急處理 #define KERN_CRIT "<2>" // 臨界條件,嚴重的硬軟體錯誤 #define KERN_ERR "<3>" // 報告錯誤 #define KERN_WARNING "<4>" //警告 #define KERN_NOTICE "<5>" //普通但還是須註意 #define KERN_INFO "<6>" // 信息 #define KERN_DEBUG "<7>" // 調試信息
7.2 那麼,printk()又如何加入這些首碼值?
比如: printk列印級別0 ,可以輸入printk(KERN_EMERG "abc");或者printk( "<0>abc");
當printk()里沒有列印級別首碼,比如printk("abc "),便會加入預設值default_message_loglevel
7.3 那麼預設值default_message_loglevel到底又是定義的哪個級別?
找到:
#define MINIMUM_CONSOLE_LOGLEVEL 1 //列印級別"<1>" #define DEFAULT_CONSOLE_LOGLEVEL 7 //列印級別"<7>" #define DEFAULT_MESSAGE_LOGLEVEL 4 //列印級別"<4>" int console_printk[4] = { DEFAULT_CONSOLE_LOGLEVEL, //=列印級別"<7>" DEFAULT_MESSAGE_LOGLEVEL, // =列印級別"<4>" MINIMUM_CONSOLE_LOGLEVEL, // =列印級別"<1>" DEFAULT_CONSOLE_LOGLEVEL, }; #define console_loglevel (console_printk[0]) //信息列印最大值, console_printk[1]=7 #define default_message_loglevel (console_printk[1]) //信息列印預設值, console_printk[1]=4 #define minimum_console_loglevel (console_printk[2]) //信息列印最小值, console_printk[2]=1 #define default_console_loglevel (console_printk[3])
顯然預設值default_message_loglevel為列印級別"<4>":
當預設值default_message_loglevel大於console_loglevel時,表示控制台不會列印信息
而最小值minimum_console_loglevel,是用來判斷是否大於console_loglevel
8.接下來我們繼續進入release_console_sem(),來看看它在哪兒判斷列印級別和console_loglevel值的
8.1printk()->vprintk()->release_console_sem():
void release_console_sem(void) { ... ... call_console_drivers(_con_start, _log_end); //將剛剛保存在迴圈緩衝區log_buf[]里的數據,發送給命令行的控制台里 //_con_start:等於起始地址, _log_end:等於結束地址 }
8.2printk()->vprintk()->release_console_sem()->call_console_drivers():
static void call_console_drivers(unsigned long start, unsigned long end) { unsigned long cur_index, start_print; ... ... cur_index = start; start_print = start; while (cur_index != end) //當列印數據的地址,等於結束地址,便退出while { /*判斷printk的列印級別,也就是首碼值"<0>"至"<7>"*/ if (msg_level < 0 && ((end - cur_index) > 2) &&LOG_BUF(cur_index + 0) == '<' && LOG_BUF(cur_index + 1) >= '0' && LOG_BUF(cur_index + 1) <= '7' &&LOG_BUF(cur_index + 2) == '>') {
/* LOG_BUF (addr):獲取addr地址上的數據 */ msg_level = LOG_BUF(cur_index + 1) - '0'; //msg_level等於列印級別,0~7 cur_index += 3; //跳過前3個首碼值,比如: "<0>abc",變為"abc" start_print = cur_index; // start_print表示要列印數據的起始地址 } while (cur_index != end) //進入列印數據環節 { char c = LOG_BUF(cur_index); //獲取要列印的cur_index地址上的數據 cur_index++; if (c == '\n') //判斷列印的數據是否結尾 { if (msg_level < 0) { //若沒有列印級別,便插入預設值,一般預設級別為4 msg_level = default_message_loglevel; } _call_console_drivers(start_print, cur_index, msg_level); //調用_call_console_drivers() } } }
8.3 進入printk()->vprintk()->release_console_sem()->call_console_drivers()->_call_console_drivers():
static void _call_console_drivers(unsigned long start,unsigned long end, int msg_log_level) { /*判斷要列印數據的列印級別msg_log_level ,若小於console_loglevel 值便進行列印*/ if ((msg_log_level < console_loglevel || ignore_loglevel) &&console_drivers && start != end)
{ ... ... __call_console_drivers(start, end); } }
顯然得出結果,當printk("abc")無法列印時,可能是default_message_loglevel預設值>=console_loglevel 值
9.那麼我們又該如何修改console_loglevel 值?
有以下3種方法
9.1通過修改 /proc/sys/kernel/printk 來更改printk列印級別
如下圖所示,可以看到default_message_loglevel預設值小於console_loglevel 值,滿足列印條件
然後通過# echo "1 4 1 7" > /proc/sys/kernel/printk來將console_loglevel設為1,即可屏蔽列印
缺點就是內核重啟後, /proc/sys/kernel/printk的內容又會恢復初值,等於"7 4 1 7",可以參考方法2和3來彌補該缺點
9.2直接修改內核文件
直接修改_call_console_drivers ()函數(位於kernel\printk.c)
將上面函數里的console_loglevel值改為0:
if ((msg_log_level < 0 || ignore_loglevel) &&console_drivers && start != end)
就可以屏蔽列印了
9.3設置命令行參數
將uboot命令行里的“console=ttySA0,115200”改為“loglevel=0 console=ttySA0,115200”,表示設置內核的console_loglevel 值=0,如下圖所示:
如上圖所示,也可以向命令行里添加debug、quiet欄位
debug:表示將console_loglevel 值=10,表示列印內核中所有的信息,一般用來調試用(後面會講如何調試)
quiet:表示將console_loglevel 值=4
(*PS:雖然屏蔽列印了,但是列印還存在緩衝區log_buf[]里, 可以通過dmesg命令來查看log_buf[])
10.接下來繼續跟蹤:
printk()->vprintk()->release_console_sem()->call_console_drivers()->_call_console_drivers()->__call_console_drivers():
static void __call_console_drivers(unsigned long start, unsigned long end) { struct console *con; // console結構體 /*for迴圈查找console */ for (con = console_drivers; con; con = con->next) { if ((con->flags & CON_ENABLED) && con->write &&(cpu_online(smp_processor_id())||(con->flags & CON_ANYTIME))) con->write(con, &LOG_BUF(start), end - start); //調用控制台的write函數列印log_buf的數據 } }
最終,__call_console_drivers()會調用s3c24xx_serial_console結構體的write函數,來列印信息
11.printk()總結:
1)首先,內核通過命令行參數, 將console信息放入console_cmdline[]全局數組中
比如: “console=ttySA0,115200”
2)然後,通過console_initcall()來查找控制台初始化函數
比如: console_initcall(s3c24xx_serial_initconsole); //來找到s3c24xx_serial_initconsole()函數
3)在控制台初始化函數里,通過register_console()來註冊console結構體
比如: register_console(&s3c24xx_serial_console); //註冊s3c24xx_serial_console
4)在register_console()里,匹配console_cmdline[]和console結構體,通過命令行參數來找到硬體處理相關的console結構體
5)使用printk(),先將列印信息先存入迴圈緩衝區log_buf[],再判斷列印級別,是否調用console->write
( PS:可以通過 dmesg 命令來列印迴圈緩衝區log_buf[] )
12.printk()分析完後,接下來便來說說如何使用printk()來調試驅動
只需要一段代碼就ok:
printk(KERN_DEBUG"%s %s %d\n", __FILE__, __FUNCTION__, __LINE__); //__FILE__: 表示文件路徑 //__FUNCTION__: 表示函數名 //__LINE__: 表示代碼位於第幾行 //KERN_DEBUG: 等於7,表示列印級別為7
然後在驅動中,可以通過上面代碼插入到每行需要調試的地方,
然後參考上面第9小節,設置console_loglevel值大於7(KERN_DEBUG)。
(當調試完成後,再將console_loglevel設為7,便不會顯示調試信息了)