zoukankan      html  css  js  c++  java
  • 34.Linux-printk分析、使用__FILE__, __FUNCTION__, __LINE__ 调试

    本节学习目的

    • 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.6kernelprintk.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.6kernelPrintk.c里的register_console(struct console *console)函数,有用到console_cmdline[]

    显然,register_console()函数就用来注册控制台的,继续搜索register_console

    如下图所示,找到很多CPU的控制台驱动初始化:

    5.我们以2410为例(linux-2.6.22.6driversserialS3c2410.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 == ' ') //判断打印的数据是否结尾 { 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 ()函数(位于kernelprintk.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
    ", __FILE__, __FUNCTION__, __LINE__);
    //__FILE__:    表示文件路径
    //__FUNCTION__: 表示函数名
    //__LINE__:    表示代码位于第几行
    //KERN_DEBUG:   等于7,表示打印级别为7

    然后在驱动中,可以通过上面代码插入到每行需要调试的地方,

    然后参考上面第9小节,设置console_loglevel值大于7(KERN_DEBUG)。

    (当调试完成后,再将console_loglevel设为7,便不会显示调试信息了)

    __FILE__, __FUNCTION__, __LINE__ 也可以用在应用层printf()里

  • 相关阅读:
    461. Hamming Distance
    342. Power of Four
    326. Power of Three
    368. Largest Divisible Subset java solutions
    95. Unique Binary Search Trees II java solutions
    303. Range Sum Query
    160. Intersection of Two Linked Lists java solutions
    88. Merge Sorted Array java solutions
    67. Add Binary java solutions
    14. Longest Common Prefix java solutions
  • 原文地址:https://www.cnblogs.com/lifexy/p/7993136.html
Copyright © 2011-2022 走看看