34.Linux-printk分析、使用printk調試驅動

来源:http://www.cnblogs.com/lifexy/archive/2017/12/06/7993136.html
-Advertisement-
Play Games

本節學習目的 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,如下圖所示:

 

如上圖所示,也可以向命令行里添加debugquiet欄位

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,便不會顯示調試信息了)


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在上章34.Linux-printk分析、使用printk調試驅動里講述了: printk()會將列印信息存在內核的環形緩衝區log_buf[]里, 可以通過dmesg命令來查看log_buf[] 1.環形緩衝區log_buf[]又是存在內核的哪個文件呢? 位於/proc/kmsg里,所以除了dme ...
  • 花了一下午時間,感覺最坑的是,書寫代碼的個數和編譯器的坑比較多,還各種版本的編譯器! 會讓人“眼花繚亂”! 主要代碼 將文件保存為*.asm 對應的軟體,放在大工院ftp://10.84.0.90上的asm中,只有在機房或者學校內網才能訪問到哦! 或者訪問https://files.cnblogs. ...
  • 1.1 資料庫對比 緩存: 將數據存儲到記憶體中,只有當磁碟勝任不了的時候,才會啟用緩存 缺點:斷電數據丟失(雙電),用緩存存儲數據的目的只是為了應付大併發的業務。 資料庫: mysql(關係型資料庫,能夠保證數據一致性,保證數據不丟失,當因為功能太多,導致性能不高) 數據參考 緩存資料庫: memc ...
  • 話不多說,直接上代碼 分割線 [root@ip-172-31-9-121 ec2-user]# yum install mysql [root@ip-172-31-9-121 ec2-user]# yum install mysql-server tips:如果出現以下代碼,就需要下載mysql-s ...
  • 對於android調用JS代碼的方法有2種: 1. 通過WebView的loadUrl() 2. 通過WebView的evaluateJavascript() 對於JS調用Android代碼的方法有3種: 1. 通過WebView的addJavascriptInterface()進行對象映射 2. ...
  • 要使用Linux先要瞭解什麼是Linux,這篇隨筆簡單介紹一個Linux的起源與現狀。(挖坑填) ...
  • 第一次進入博客園 2017年12月7日 之前使用dos視窗時都輸入的是簡短的指令,今天突然感覺小框看著不舒服,就找了一下度娘,在這裡感謝萬能的百度,一鞠躬. 1.win+r打開dos命令視窗 2.cmd+ENTER 3.quit退出wmic模式 退出dos命令視窗時需要重新進行上述操作! ...
  • 1、下載需要的echo模塊https://github.com/openresty/echo-nginx-module/tags# wget https://github.com/openresty/echo-nginx-module/archive/v0.60.tar.gz# tar zxvf v ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...