34.Linux-printk分析、使用prink调试驱动
本节学习目的
- 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[].name)]; // char name[8]
char *s, *options;
int idx;
/*
* Decode str into name, index, options.
*/ if (str[] >= '' && str[] <= '') {
strcpy(name, "ttyS");
strncpy(name + , str, sizeof(name) - );
} else {
strncpy(name, str, sizeof(name) - ); //*name="ttySA0, "
}
name[sizeof(name) - ] = ; //*name="ttySA0"
if ((options = strchr(str, ',')) != NULL) //找到',',返回给options,所以options=",115200"
*(options++) = ; //*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 >= '' && *s <= '') || *s == ',')
break; idx = simple_strtoul(s, NULL, ); //和strtoul()一样,将s中的"0"提出来,所以idx=0
*s = ; //将"ttySA0"中的"0"设为0,所以*name="ttySA" add_preferred_console(name, idx, options);
//*name="ttySA"
// idx=0
//*options="115200"
return ;
}
通过上面的代码和注释得到, 最终调用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 = ; i < MAX_CMDLINECONSOLES && console_cmdline[i].name[]; i++)
if (strcmp(console_cmdline[i].name, name) == &&console_cmdline[i].index == idx)
// console_cmdline[]是一个全局数组,用来匹配要添加的控制台是否重复
{
selected_console = i;
return ; //在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) - ] = ;
c->options = options;
c->index = idx;
return ;
}
上面函数,最终将控制台的信息放到了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 ;
}
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 = -, /索引值
.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[]; //临时缓冲区
static int log_level_unknown = ;
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[] == '<' && p[] >='' && p[] <= '' && p[] == '>')
{ loglev_char = p[]; //获取打印级别字符,将级别放入 loglev_char中
p += ;
printed_len -= ; }
else
{ //若没有打印级别,便插入默认值,比如printk("abc"),会变为printk("<4>abc")
loglev_char = default_message_loglevel+ '';
} ... ... //开始拷贝到循环缓冲区log_buf[]
} /* cpu_online():检测CPU是否在线
have_callable_console():检测是否有注册的控制台*/
if (cpu_online(smp_processor_id()) || have_callable_console()) {
console_may_schedule = ;
release_console_sem(); //调用release_console_sem()向控制台打印信息
} else {
/*释放锁避免刷新缓冲区*/
console_locked = ;
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[] = {
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 < && ((end - cur_index) > ) &&LOG_BUF(cur_index + ) == '<' && LOG_BUF(cur_index + ) >= '' &&
LOG_BUF(cur_index + ) <= '' &&LOG_BUF(cur_index + ) == '>')
{ /* LOG_BUF (addr):获取addr地址上的数据 */
msg_level = LOG_BUF(cur_index + ) - ''; //msg_level等于打印级别,0~7
cur_index += ; //跳过前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 < ) { //若没有打印级别,便插入默认值,一般默认级别为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 < || 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()里
34.Linux-printk分析、使用prink调试驱动的更多相关文章
- linux 内核分析+使用SystemTap调试新增内核模块
http://blog.chinaunix.net/uid/14528823/list/1.html?cid=189394
- debian下使用dynamic printk分析usb转串口驱动执行流程
看了一篇文章<debug by printing>,文中提到了多种通过printk来调试驱动的方法,其中最有用的就是"Dynamic debugging". “Dyna ...
- Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动
姓名:江军 ID:fuchen1994 实验日期:2016.3.13 实验指导 使用实验楼的虚拟机打开shell cd LinuxKernel/ qemu -kernel linux-3.18.6/a ...
- debian下使用dynamic printk分析usb网卡驱动
在<debian下使用dynamic printk分析usb转串口驱动执行流程>中使用了usb转串口,当前例子使用usb网卡分析驱动(dm9601芯片). 仍然需要使能dynamic pr ...
- 【linux驱动分析】之dm9000驱动分析(三):sk_buff结构分析
[linux驱动分析]之dm9000驱动分析(一):dm9000原理及硬件分析 [linux驱动分析]之dm9000驱动分析(二):定义在板文件里的资源和设备以及几个宏 [linux驱动分析]之dm9 ...
- Linux内核分析(五)----字符设备驱动实现
原文:Linux内核分析(五)----字符设备驱动实现 Linux内核分析(五) 昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷, ...
- Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介
原文:Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介 Linux内核分析(四) 两天没有更新了,上次博文我们分析了linux的内存管理子系统,本来我不想对接下来的进程管理 ...
- TCPflow:在Linux中分析和调试网络流量的利器(转)
TCPflow是一款功能强大的.基于命令行的免费开源工具,用于在Unix之类的系统(如Linux)上分析网络流量.它可捕获通过TCP连接接收或传输的数据,并存储在文件中供以后分析,采用的格式便于协议分 ...
- [转]Linux芯片级移植与底层驱动(基于3.7.4内核)
1. SoC Linux底层驱动的组成和现状 为了让Linux在一个全新的ARM SoC上运行,需要提供大量的底层支撑,如定时器节拍.中断控制器.SMP启动.CPU hotplug以及底层的G ...
随机推荐
- 为Android设备添加A2SD支持
相信很多用Android设备的用户都有这个问题,内部存储太小导致应用只能装那么几个,虽然rom也有提供移动到sd卡的选项,但是仅仅是移动程序文件到sd卡,并不能解决多少问题,多装几个还是会 ...
- Redis 学习笔记-5种数据类型的基本操作
1.string类型 基本操作列表: GET 获取指定键对应的值 SET 设定键值 DEL 删除指定键对应的值(对所有数据类型都有效) > set hello world OK > get ...
- 如何用VS EF连接 Mysql,以及执行SQL语句 和存储过程?
VS2013, MySQL5.7.18 , MySQL5.7.14 执行SQL语句: ztp_user z = new ztp_user(); object[] obj = new object[] ...
- 一个简单的makefile文件编写
下午闲来无聊,就打开很久没动过的linux系统想熟悉熟悉在linux上面编译代码,结果一个makefile文件搞到晚上才搞定,哈哈! 先把代码简单贴上来,就写了一个冒泡排序: sort.h: #ifn ...
- 运行第一个Go Web框架
GO 语言的web框架很多,相对来说, Beego 框架,入门简单,文档齐全(中文),功能强大,本文以Beego 示例. Beego提供了详细的开发文档:http://beego.me/docs/in ...
- Java语言写出水仙花数,
package com.llh.demo;/** * 水仙花数 * @author llh * */public class Demo14 { public static void main(S ...
- Sudoku Killer
Problem Description 自从2006年3月10日至11日的首届数独世界锦标赛以后,数独这项游戏越来越受到人们的喜爱和重视. 据说,在2008北京奥运会上,会将数独列为一个单独的项目进行 ...
- Codeforces 376C. Socks
C. Socks time limit per test 2 seconds memory limit per test 256 megabytes input standard input outp ...
- 开发中关于Git那些事
如果你想精通Git,直接到 Git官网 把这本ProGit掌握已足以Pro Git 此文主要介绍一切开发中常用的git命令和一些配置技巧(诸如git别名配置,log打印技巧,版本回退以及分支管理等). ...
- 由浅入深学习springboot中使用redis
很多时候,我们会在springboot中配置redis,但是就那么几个配置就配好了,没办法知道为什么,这里就详细的讲解一下 这里假设已经成功创建了一个springboot项目. redis连接工厂类 ...