本节学习目的

  • 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调试驱动的更多相关文章

  1. linux 内核分析+使用SystemTap调试新增内核模块

    http://blog.chinaunix.net/uid/14528823/list/1.html?cid=189394

  2. debian下使用dynamic printk分析usb转串口驱动执行流程

    看了一篇文章<debug by printing>,文中提到了多种通过printk来调试驱动的方法,其中最有用的就是"Dynamic debugging". “Dyna ...

  3. Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

    姓名:江军 ID:fuchen1994 实验日期:2016.3.13 实验指导 使用实验楼的虚拟机打开shell cd LinuxKernel/ qemu -kernel linux-3.18.6/a ...

  4. debian下使用dynamic printk分析usb网卡驱动

    在<debian下使用dynamic printk分析usb转串口驱动执行流程>中使用了usb转串口,当前例子使用usb网卡分析驱动(dm9601芯片). 仍然需要使能dynamic pr ...

  5. 【linux驱动分析】之dm9000驱动分析(三):sk_buff结构分析

    [linux驱动分析]之dm9000驱动分析(一):dm9000原理及硬件分析 [linux驱动分析]之dm9000驱动分析(二):定义在板文件里的资源和设备以及几个宏 [linux驱动分析]之dm9 ...

  6. Linux内核分析(五)----字符设备驱动实现

    原文:Linux内核分析(五)----字符设备驱动实现 Linux内核分析(五) 昨天我们对linux内核的子系统进行简单的认识,今天我们正式进入驱动的开发,我们今后的学习为了避免大家没有硬件的缺陷, ...

  7. Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介

    原文:Linux内核分析(四)----进程管理|网络子系统|虚拟文件系统|驱动简介 Linux内核分析(四) 两天没有更新了,上次博文我们分析了linux的内存管理子系统,本来我不想对接下来的进程管理 ...

  8. TCPflow:在Linux中分析和调试网络流量的利器(转)

    TCPflow是一款功能强大的.基于命令行的免费开源工具,用于在Unix之类的系统(如Linux)上分析网络流量.它可捕获通过TCP连接接收或传输的数据,并存储在文件中供以后分析,采用的格式便于协议分 ...

  9. [转]Linux芯片级移植与底层驱动(基于3.7.4内核)

      1.   SoC Linux底层驱动的组成和现状 为了让Linux在一个全新的ARM SoC上运行,需要提供大量的底层支撑,如定时器节拍.中断控制器.SMP启动.CPU hotplug以及底层的G ...

随机推荐

  1. sharepoint 创建个人网站

    One of the SharePoint 2013 puzzle pieces which got some major improvements are My Sites, User Profil ...

  2. ueditor编辑器插件 chrome中图片上传框延时问题

    最近在项目中使用ueditor插件进行文字的在线编辑功能时,发现这个插件的图片上传弹框在chrome浏览器延迟非常的厉害.经过多方搜索,终于解决.现将解决方案记录如下: 1.修改/Ueditor/di ...

  3. 本地idea调试spark2.x程序

    1.构建使用idea 构建maven 项目 选择org.scala-tools.archetypes:scala-archetype-simple,然后一直点next,maven最好选中本地配置国内源 ...

  4. MAC - PhpStorm安装调试环境xdebug

    今天下午一直在捣鼓如何用PhpStorm进行调试,查找了许多资料,零零碎碎的,所以自己弄篇文章记录一下步骤. 安装xdebug 使用brew安装xdebug,语法如下 brew install hom ...

  5. 高性能 Lua 技巧(译)

    高性能 Lua 技巧(译) 来源 https://segmentfault.com/a/1190000004372649 此为 Lua Programming Gems 一书的第二章:Lua Perf ...

  6. HDU1166 敌兵布阵(线段树)

    C国的死对头A国这段时间正在进行军事演习,所以C国间谍头子Derek和他手下Tidy又开始忙乎了.A国在海岸线沿直线布置了N个工兵营地,Derek和Tidy的任务就是要监视这些工兵营地的活动情况.由于 ...

  7. hdu 2196 Computer(树形DP经典)

    Computer Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  8. web实现数据交互的几种常见方式

    前言 在当今社会,作为一名前端程序猿,并不是一昧的去制作静态页面就可以满足滴:你说你会制作网页,好吧,只能说你算是一个前端程序猿.但这是你作为一个程序猿最基本的能力,并不会为你进行加分: 我们都明白, ...

  9. SpringMVC , Spring , MyBatis 文件上传

    学习一下文件上传下载,为图片上传做准备,感觉有一个世纪没玩过上传下载了,边敲代码边记录,请各路大神指教: 参考:http://blog.csdn.net/wjycgl/article/details/ ...

  10. empty()和remove()的区别

    这两个都是删除元素,但是两者还是有区别的. remove()这个方法呢是删除被选元素的所有文本和子元素,当然包括被选元素自己. 而empty()呢,被选元素自己是不会被删除的. 比如: <div ...