简图记录总结~
在驱动开发过程中,常会用到一些打印做问题定位,无论是提前设计或者调试过程中添加,打印都是一种常用的手段。以下为打印相关问题总结。
一、常见驱动打印的添加与查看。
1、内核打印printk
概念:printk是内核中根据日志级别向控制台输出显示的函数。
用法:用法为printk(消息级别"a=%d\n",a);和printf用法类似。消息级别可省略。消息级别定义参考(0:系统崩溃前、1:系统必须响应的错误处理、2:严重错误、3:一般错误、4:警告、5:提醒、6:提示信息、7:调试信息)
查看打印等级:使用cat /proc/sys/kernel/printk---->7 4 1 7;第一个等级7表示 控制台当前日志级别(必须要更高的级别才能打印出来);第二个等级4表示默认的消息级别(当printk没有指定打印等级时默认的打印等级);第三个等级1表示最低控制台级别;第四个等级7表示默认中断控制级别;
设置打印等级:echo 目标级别 >/proc/sys/kernel/printk;
常见无法打印原因:1、打印级别低于系统设置 2、控制台输出未指定到串口上(boot下bootargs中未设置控制台到串口,如console=ttySAC0;kernel下可以通过cat /proc/cmdline查看bootargs)
2、查看内核缓存打印buffer信息dmesg
概念:linux系统中用于查看内核ringbuffer的信息(开机信息和printk信息都可以查看)
用法:(1)直接输入dmesg,打印buffer中缓存所有信息,利用管道组合grep可以查找带XXX信息打印,dmesg | grep XXX ;(2)dmesg -C 显示的同时清空buffer数据,重新开始记录 (3)dmesg -n 打印级别,设置记录控制台输出最低级别;
3、Android环境下日志控制logcat(用户态打印)
概念:Android环境下可以通过logcat进行用户态的打印控制;
用法:(1)直接输入logcat,默认持续打印所有信息(会阻塞输入),同样可以配合管道和grep指定输出打印,如logcat | grep XXX;(2)logcat -d,打印一次缓存信息到控制台输出(非阻塞);(3)logcat -c,清空打印缓存信息;(4)logcat 模块1:指定打印等级 模块2:指定打印等级,可以选择性的约束指定模块输出等级;
二、常见打印调试技巧
1、利用编译宏添加通用打印信息
如 __LINE__行号、__FUNCTION__函数名
2、通过current指针打印进程上下文信息
在用户态或者内核态的进程上下问中利用current信息,打印当前进程号和进程名:printk("the process name=%s pid=%i\n",current->comm,current->pid);
3、打印函数调用堆栈信息
(1)内核态直接在指定调用处添加dump_stack();
(2)用户态C/C++利用系统调用打印,参考实例如下(注意编译时带-rdynamic把符号带入动态链接标,否则只打印地址无 函数名称)
void mybacktrace()
{void *buffer[100];char **str=NULL;int i=0;int ptr_num = backtrace(buffer,100);//获取指定最大100个堆栈信息;str = backtrace_symbols(buffer,ptr_num);//转化为字符串if (NULL == str)return;for(i=0p;i<ptr_num;i++)printf("%s\n",str[i]);free(str);
}
(3)用户态java代码添加,利用Exception对象:
Exception e = new Exception("***dump stack****");
e.printStackTrace();
三、常见打印注意事项
1、注意打印会引入延时
特别是在中断等对延时敏感场景,打印信息更要简洁明了。。(调试一些耗时敏感的地方一定要注意打印注释掉后是否还符合预期)
2、频繁密集内核打印(如中断)可能挂死系统
不要一直在中断打印大量信息,在中断中过多打印信息会导致过长耗时引起系统响应缓慢、丢中断、甚至挂死系统等问题可以在状态变化时打印(可以加入计数信息),或者计数降频。
3、用户态和内核态打印是并发的
用户态和内核态打印是并发输出的,如果打印位置接近会出现A打印被B打印中断后继续输出甚至覆盖的的情况,在设计打印的时候要留意这一点。或者 使用过程中,直接把内核打印放到后台通过dmesg查看 并使用telnet循环抓取,用户态 直接串口输出记录(确定是无法体现两者同步关系)。
四、打印实现分析:
1、变长参数
概念:C语言规定变长参数至少存在一个明确的参数,且变长参数必须处在所有参数最后,如:
int printf(const char *format,...);--->尾部的...表示占位符
实现参考(用man查看帮助):
void va_start(va_list ap,last);在最开始变参处理前调用,ap指向最后一个明确参数,取得第一个变参起始地址
[type] va_arg(va_list ap, [type]);每次调用返回ap指向type类型数据,并将ap指向下一个参数地址
void va_end(va_list ap);结束变参访用调用(常为空)
typedef char* va_list;
#define va_size(type)\(((sizeof(type)+sizeof(long)-1/sizeof(long)*sizeof(long)) //引入long为编译对齐处理
#define va_start(ap,last)\
((ap)=(va_list)&(last)+va_size(last))
#define va_arg(ap,type)\
(*(type*)((ap)+=va_size(type),(ap)-va_size(type)))
#define va_end(ap)
变参用法
//使用va_arg逐个取出指定类型,打印n个int型变参
void test_print(int n,...)
{int i=0;va_list ap;va_start(ap,n);for (i=0;i<n;i++){int tmp = va_arg(ap, int);printf("%d num=%d\n",i,tmp);}va_end(ap)
}
\\结合vsprintf获取字符串打印,制作自己的打印函数
void my_printf(char *fmt,...)
{char tmp[100];va_list ap;va_start(ap,fmt);vsprintf(tmp,fmt,ap);va_end(ap);printf("%s\n",tmp);
}
2、C库printf族函数分析
/*1.<stdio.h>相关打印函数*/
int printf(const *fmt,...);//定义变参,使用vsprintf将变参转化为字符串,再利用终端write向标准输出写数据打印
int fprintf(FILE *stream,const *fmt,...);//定义变参,使用vfprintf将变参输出到文件
int sprintf(char *str,const *fmt,...);//定义变参,使用svprintf将变参转化字符串到buffer中
int snprintf(char *str,size_t size ,const *fmt,...);//定义变参,使用svnprintf将变参转化字符串到buffer中
/*2.<stdarg.h>相关变参处理函数*/
int vprintf(const *fmt,va_list ap);//利用vsprintf将变参转化为字符串,再利用fwrite向标准输出写数据打印
int vfprintf(FILE *stream,const *fmt,va_list ap);//使用vsnprintf将变参转化字符串,并同fwrite将buffer写入文件
int vsprintf(char *str,const *fmt,va_list ap);//解析fmt格式信息和变参ap拼接生成字符串
int svnprintf(char *str,size_t size , const *fmt,...);//类似vsprintf,加入长度校验