zoukankan      html  css  js  c++  java
  • 《驱动调试

    1.在驱动调试中,使用printk(),是最简单,最方便的办法

    当uboot的命令行里的“console=tty1”时,表示printk()输出在开发板的LCD屏上

    当uboot的命令行里的“console=ttySAC0,115200”时,表示printk()输出在串口UART0上,波特率=115200

    当uboot的命令行里的“console=tty1 console=ttySAC0,115200”时,表示printk()同时输出在串口上,以及开发板的LCD屏上

    显然printk(),还是根据命令行参数来调用不同控制台的硬件处理函数

    内核又是怎么根据上面命令行参数来确定printk()的输出设备?

    2.我们以“console=ttySAC0,115200”为例,进入linux-2.6.22.6kernelprintk.c

    找到以下一段:

    __setup("console=", console_setup);

    若uboot传递进来的命令行字符串里含有“console=”,便调用console_setup()函数,并对“console=”后面带的字符串"ttySAC0,115200"进行分析

    3.我们以*str= "ttySAC0,115200"为例,console_setup()函数如下所示

    static int __init console_setup(char *str)                    //*str="ttySAC0,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="ttySAC0, "
           }
           name[sizeof(name) - 1] = 0;            //*name="ttySA0"
           if ((options = strchr(str, ',')) != NULL)   //找到',',返回给options,所以options=",115200"
                  *(options++) = 0;                //*options="115200", *str="ttySAC0"
    #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;                                         //将"ttySAC0"中的"0"设为0,所以*name="ttySAC"
    
           add_preferred_console(name, idx, options);      
          //*name="ttySAC"
          // 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[]

    3)每次拷贝前都要检查打印级别,若没有打印级别,便插入默认值default_message_loglevel

    4)最后检查是否有注册的控制台,若有,便调用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,如下图所示:

    如上图所示,也可以向命令行里添加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
    ", __FILE__, __FUNCTION__, __LINE__);
    //__FILE__:    表示文件路径
    //__FUNCTION__: 表示函数名
    //__LINE__:    表示代码位于第几行
    //KERN_DEBUG:   等于7,表示打印级别为7

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

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

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

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

  • 相关阅读:
    【深度学习】吴恩达网易公开课练习(class1 week2)
    【深度学习】吴恩达网易公开课练习(class1 week3)
    【python】内存调试
    【python】threadpool的内存占用问题
    Druid: A Real-time Analytical Data Store
    Mesa: GeoReplicated, Near RealTime, Scalable Data Warehousing
    Presto: SQL on Everything
    The Snowflake Elastic Data Warehouse
    Guava 库
    Java Annotation
  • 原文地址:https://www.cnblogs.com/zhuangquan/p/11713737.html
Copyright © 2011-2022 走看看