本节学习目的
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[]全局数组中,那接下来来搜索该数组,看看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] == '>') 从上面的代码和注释来看,显然vprintk()的作用就是:
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) == '>') {
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) 显然得出结果,当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,便不会显示调试信息了) __FILE__, __FUNCTION__, __LINE__ 也可以用在应用层printf()里 |
|