天天看点

Linux内核之调试、移植、编码

调试知识

1、通过内核格式化打印函数printk()

  • 该函数健壮性比较强,几乎可以随时随地调用它,只有在系统启动过程中,终端还没有初始化之前,在某些地方不能使用
  • 如果要调试启动过程中最开始的那些步骤,有些时候可以依靠能够工作的硬件设备与外界通信
  • printk()函数可以指定日志级别,内核把级别比某个特定值低的所有消息显示在终端上,默认等级是 KERN_WARNING
记录等级 描述
KERN_EMERG 紧急情况
KERN_ALERT 警惕性错误
KERN_CRIT 临界情况
KERN_ERR 错误
KERN_WARNING 警告
KERN_NOTICE 通知信息
KERN_INFO 常规信息
KERN_DEBUG 调试信息
  • 终端记录等级console——loglevel也可以控制打印输出,默认是DEFAULT_MESSAGE_LOGLEVEL
  • 内核消息都被保存在一个LOG_BUF_LEN大小的环形队列中,可以通过设置CONFIG_LOG_BUF_SHIFT进行调整,如果消息队列满,新消息将覆盖队列中的老消息
  • 用户空间的守护进程klogd(会阻塞,直到有新的内核消息可供读出)从记录缓冲区中(/proc/kmsg文件中或通过syslog()系统调用)获取内核消息,再通过syslogd守护进程将它们保存在系统日志文件(默认/var/log/messages)中,可以通过/etc/syslog.conf配置文件重新指定输出文件

2、内核发布oops

  • 向终端输出错误消息,输出寄存器中保存的信息并输出可供跟踪的回溯线索
  • ksymoops命令可以对oops进行解码,编程我们易懂的信息
  • 2.5内核引入kallsyms特性,通过定义CONFIG_KAIISYMS配置选项启用(内核变大一些),内核可以打印解码好的跟踪线索

3、内核调试配置选项

  • 在编译的时候,为了方便调试和测试内核代码,内核提供了许多配置选项:slab层调试选项、高端内存调试选项、I/O映射调试选项、自旋锁调试选项、栈溢出检查选项等

4、引发BUG并打印信息

  • 提供断言并输出信息,常用的有BUG()和BUG_ON()
  • BUILD_BUG_ON()仅在编译时使用
  • panic()引发更严重的错误,打印错误消息,挂起整个系统
  • 打印一下栈的回溯信息来帮忙调试,可以使用dump_stack()

5、SysRq(系统请求)键

  • 通过定义CONFIG_MAGIC_SYSRQ配置选项启用
  • 该键是调试和挽救垂危系统所必需的一种工具,该功能对终端上任何用户都提供服务,要慎重使用

6、gdb

  • gdb vmlinux /proc/kcore //vmlinux 未压缩内核映像
  • 使用-g参数在编译时提供调试信息
  • kgdb补丁,支持远端主机上通过串口利用gdb的所有功能对内核进行调试

7、使用探测系统的小技巧调试

  • 利用选择条件实现执行不同分支或算法
  • 使用全局变量作为条件选择开关
  • 使用统计量
  • 重复频率限制控制输出,控制打印间隔,控制打印次数等

8、二分搜索比较获取BUG变更分支

9、社区求助:Linux内核邮件列表(LKML)

可移植操作系统知识

1、字长和数据类型

  • 能够由机器一次完成处理的数据称为字,处理器通用寄存器(GPR)的大小和它的字长是相同的,c语言定义的long类型总是对等于机器的字长
  • 一般有如下准则:char类型长度是1字节 int类型长度是32位,short类型是16位,指针不应假定为long的长度,sizeof(int)不应假设等于sizeof(long)
  • 不透明类型:不要假设该类型的长度,该类型的大小可能被任意修改;不要将该类型转化回其对应的C标准类型使用;编程时要保证该类型的实际存储空间或格式变化时代码不受影响
  • 指定数据类型:注意对应的类型后再使用
  • 长度明确的类型:对标准的C类型进行映射得到
  • char类型的符号问题:char某些体系结构上是默认带符号,有些事默认不带符号,如果能明确使用哪个,直接声明好

2、数据对齐

  • 如果一个变量的内存地址正好是它长度的整数倍,那么是自然对齐。编写高可移植性代码要避免对齐问题,保证所有的类型都能够自然对齐
  • 一个数据类型长度较小,本来是对齐的,如果你用一个指针进行类型转换,转换后的类型长度较大,那么通过改指针进行数据访问时就可能会引发对齐问题
  • 非标准类型C数据类型对齐:对于数组,只要按照基本数据类型进行对齐即可;对于联合体,只要它包含的长度最大的数据类型对齐即可;对于结构体,只要结构体中每个元素能够正确对齐即可
  • 结构体填补:为了保证结构体中每一个成员都能够自然对齐,结构体要被填补,不同体系结构之间所需的填补也不尽相同(结构体重新排序有时可以避免填补,但是某些有固定次序要求的会有问题)

3、字节顺序

  • 字节顺序是指一个字中各个字节的顺序,处理器对字取值时既可能将最低有效位所在的字节当做第一个字节,也可能将其当做最后一个字节,这就是大端小端问题。
  • Linux支持每一种体系结构,定义了一组宏命令用于字节顺序之间的转换

4、时间

时间测量是一个内核概念,绝对不要假定时钟中断发生的频率,应该使用HZ(每秒产生的jiffies数目)来正确计量时间

5、页长度

绝对不能假设页长度,当处理用页组织管理的内存时,通过PAGE_SIZE以字节数来表示页长度

6、处理器排序

有些处理器严格限制指令排序,有些体系结构对排序要求很弱(可以用内存屏障来保证一定的指定执行顺序)

7、SMP、内核抢占、高端内存

  • 假设代码在SMP系统上运行,要正确选择和使用锁
  • 假设代码在支持内核抢占情况下运行,要正确选择和使用锁和内核抢占语句
  • 假设代码运行在使用高端内存(非永久映射内存)的系统上,必要时使用kmap()

Linux编码风格

1、缩进

  • 缩进风格是用制表位每次缩进8个字符长度
  • switch语句下的case标记应该缩进到和switch声明对齐
  • 空格放在关键字周围,函数名和圆括号之间无空格;函数、宏以及与函数相像的关键字在关键字和圆括号之间没有空格;对于大多数二元或者三元操作符,在操作符的两边加上空格,对于大多数一元操作符,在操作符和操作数之间不加空格
  • 花括号的左括号紧跟在语句的最后,与语句在相同的一行,而右括号要新起一行,作为该行的第一个字符,函数不采用这样的书写方式,重起一行;不需要一定使用括号的语句可以忽略
  • 每行代码的长度不超过80个字符

2、 命名规范

  • 名称中不容许使用混合的大小写字符
  • 局部变量能清楚表明它的用途即可,不接受冗长繁复的名字,在变量名称中加入变量的类型是不必要的
  • 全局变量和函数应该选择包含描述性内容的名称,并且使用小写字母,必要时加上下划线区分单词

3、函数

  • 函数代码长度不应该超过两屏,局部变量不应超过10个
  • 一个函数应该功能单一并且实现精准,担心函数调用开销,可以使用inline关键字

4、注释

-c风格注释 代码注释一般描述代码要做什么和为什么要做即可

/*
*
*/
           

5、typedef

尽量少用typedef,除非有必要,比如隐藏变量与体系结构相关的实现细节,某种类型将来有可能发生变化等

6、多使用内核本身提供的一些通用函数,尽量少自己重复实现,减少一些问题比如移植性问题等

7、要在合适的地方使用ifdef预处理指令

8、结构初始化

结构初始化的时候必须在它的成员前加上结构标识符

9、代码格式化

使用一些工具按照内核编码风格对代码进行格式化

继续阅读