Linux内核调试技巧与最佳实践
1. 看门狗设备驱动
在用户空间,我们可以使用看门狗的sysfs接口来操作相关参数。例如,检查是否可以选择预超时调节器:
# cat /sys/class/watchdog/watchdog0/pretimeout_governor
panic
# echo -n noop > /sys/class/watchdog/watchdog0/pretimeout_governor
# cat /sys/class/watchdog/watchdog0/pretimeout_governor
noop
检查预超时值:
# cat /sys/class/watchdog/watchdog0/pretimeout
10
通过这些操作,我们可以在用户空间利用整个看门狗框架,灵活调整看门狗参数。
2. Linux内核调试基础
Linux内核调试具有一定挑战性,因为它是运行在操作系统最底层的独立软件。不过,大多数内核调试工具是内核自带的,无需额外工具。
2.1 技术要求
- 具备高级计算机架构知识和C编程技能。
- 拥有Linux内核v4.19.X源码,可从https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/refs/tags 获取。
2.2 理解Linux内核发布过程
Linux内核发布模型包含主线版本、稳定版本和长期支持(LTS)版本三种活跃内核发布类型。
-
主线版本
:子系统维护者收集和准备错误修复和新特性,提交给Linus Torvalds,集成到他的主线Linux树(即主Git仓库),这是所有稳定版本的源头。
-
稳定版本
:新内核版本发布前,会通过候选版本标签提交给社区,开发者测试并反馈。Linus根据反馈决定最终版本是否发布。稳定版本基于主线版本,由Greg Kroah - Hartman维护linux - stable树。
-
长期支持(LTS)版本
:提供长期的稳定性和安全更新。
| 版本类型 | 特点 | 维护者 |
|---|---|---|
| 主线版本 | 新特性和修复的源头 | Linus Torvalds |
| 稳定版本 | 基于主线版本,接收错误修复 | Greg Kroah - Hartman |
| 长期支持(LTS)版本 | 长期稳定性和安全更新 | 特定开发者 |
新主线内核通常每2 - 3个月发布一次。稳定版本的错误修复必须先在主线仓库中应用,再反向移植到稳定树。例如,4.9内核发布后,基于它的稳定版本编号为4.9.1、4.9.2等。
以下是Linux内核发布过程的mermaid流程图:
graph LR
A[子系统维护者收集修复和特性] --> B[提交给Linus Torvalds]
B --> C[集成到主线Linux树]
C --> D[发布候选版本标签]
D --> E[开发者测试和反馈]
E --> F{Linus决定发布最终版本?}
F -- 是 --> G[发布稳定版本]
F -- 否 --> D
G --> H[错误修复反向移植到稳定树]
重要链接:
- https://www.kernel.org/ :可下载内核归档文件。
- https://www.kernel.org/category/releases.html :获取最新LTS内核版本及支持时间线。
- https://patchwork.kernel.org/ :按子系统跟踪内核补丁提交情况。
3. Linux内核开发技巧
最佳的Linux内核开发实践源于现有内核代码。本章重点关注调试,常用的调试方法是日志记录和打印。
3.1 消息打印
在Linux内核中,
printk()
函数是事实上的内核消息打印函数,类似于C库中的
printf()
,但有日志级别概念。使用示例:
printk(<LOG_LEVEL> "printf like formatted message\n");
<LOG_LEVEL>
是在
include/linux/kern_levels.h
中定义的八种不同日志级别之一,用于指定错误消息的严重程度。
3.2 内核日志级别
| 日志级别 | 定义 | 描述 |
|---|---|---|
| KERN_EMERG | “0” | 紧急消息,系统即将崩溃或不稳定 |
| KERN_ALERT | “1” | 发生严重情况,需立即采取行动 |
| KERN_CRIT | “2” | 关键条件发生,如严重硬件/软件故障 |
| KERN_ERR | “3” | 错误条件,常用于驱动程序表示硬件问题或子系统交互失败 |
| KERN_WARNING | “4” | 警告信息,本身不严重,但可能暗示问题 |
| KERN_NOTICE | “5” | 不严重但值得注意,常用于报告安全事件 |
| KERN_INFO | “6” | 信息性消息,如驱动初始化时的启动信息 |
| KERN_DEBUG | “7” | 调试信息,只有在启用DEBUG内核选项时才有效 |
如果消息未指定日志级别,默认使用
DEFAULT_MESSAGE_LOGLEVEL
(通常为 “4” = KERN_WARNING),可通过
CONFIG_DEFAULT_MESSAGE_LOGLEVEL
内核配置选项设置。
对于新驱动,建议使用更方便的打印API,如
pr_emerg
、
pr_alert
等。示例:
#define pr_fmt(fmt) "%s:%s: " fmt, KBUILD_MODNAME, __func__
这样可以为每个
pr_*()
消息添加模块和函数名前缀。
对于设备驱动,应使用与设备相关的辅助函数,如
dev_emerg
、
dev_alert
等,这些函数会接受设备结构作为参数,并打印相关设备名称。
3.3 控制台日志级别
内核根据消息的日志级别和
console_loglevel
内核变量决定是否立即将消息打印到控制台。默认内核日志级别通常为 “4”,所以
pr_info()
、
pr_notice()
、
pr_warn()
等消息可能不会显示在控制台。
- 查看当前控制台日志级别 :
$ cat /proc/sys/kernel/printk
4 4 1 7
第一个数字是当前控制台日志级别,第二个是默认值,第三个是可设置的最小级别,第四个是启动时的默认级别。
-
更改控制台日志级别
:
# echo 8 > /proc/sys/kernel/printk
或者使用
dmesg -n
参数:
# dmesg -n 5
也可以在启动时使用
loglevel
启动参数指定日志级别。
此外,还有特殊的
KERN_CONT
和
pr_cont
,用于表示连续消息,仅在早期启动时的核心/架构代码中使用。
3.4 内核日志缓冲区
每个内核消息都会记录在一个固定大小的循环缓冲区中。如果缓冲区满了,可能会丢失消息。可以通过
LOG_BUF_SHIFT
选项或
log_buf_len
内核启动参数更改缓冲区大小。
3.5 添加时间信息
通过
CONFIG_PRINTK_TIME
选项启用
printk
时间功能,可为打印的消息添加时间戳。时间戳格式为秒和微秒,是自机器启动(或内核计时开始)以来的绝对时间。
可以通过向
/sys/module/printk/parameters/time
文件写入值来控制时间戳的打印:
# echo 1 >/sys/module/printk/parameters/time
# cat /sys/module/printk/parameters/time
Y
4. Linux内核跟踪与性能分析
虽然打印调试能满足大部分需求,但有时需要在运行时监控Linux内核,以跟踪异常行为。Ftrace是Linux内核内部的跟踪工具,是本节的重点。
4.1 使用Ftrace进行代码插桩
Ftrace自2008年的2.6.27版本开始包含在Linux内核中,提供调试环形缓冲区来记录数据。它基于debugfs文件系统,通常挂载在
/sys/kernel/debug/tracing/
目录。
启用Ftrace需要开启以下内核选项:
- CONFIG_FUNCTION_TRACER
- CONFIG_FUNCTION_GRAPH_TRACER
- CONFIG_STACK_TRACER
- CONFIG_DYNAMIC_FTRACE
这些选项依赖于架构支持相关的跟踪特性选项。
可以通过以下方式挂载tracefs目录:
- 在
/etc/fstab
文件中添加:
tracefs /sys/kernel/debug/tracing tracefs defaults 0 0
- 运行时挂载:
mount -t tracefs nodev /sys/kernel/debug/tracing
Ftrace目录下的部分重要文件及其功能如下:
-
available_tracers
:可用的跟踪程序列表。
-
tracing_cpumask
:指定要跟踪的CPU,以十六进制字符串格式表示。
-
current_tracer
:当前运行的跟踪程序。
-
tracing_on
:启用或禁用向环形缓冲区写入数据。
-
trace
:以人类可读格式保存跟踪数据的文件。
4.2 可用的跟踪程序
通过以下命令查看可用的跟踪程序:
# cat /sys/kernel/debug/tracing/available_tracers
blk function_graph wakeup_dl wakeup_rt wakeup irqsoff function nop
各跟踪程序特点如下:
| 跟踪程序 | 特点 |
| ---- | ---- |
| function | 无参数的函数调用跟踪器 |
| function_graph | 带子调用的函数调用跟踪器 |
| blk | 与块设备I/O操作相关的调用和事件跟踪器 |
| mmiotrace | 内存映射I/O操作跟踪器 |
| irqsoff | 跟踪禁用中断的区域,并保存最长延迟的跟踪信息 |
| preemptoff | 跟踪并记录禁用抢占的时间 |
| preemtirqsoff | 跟踪并记录禁用中断和/或抢占的最长时间 |
| wakeup | 跟踪最高优先级任务唤醒后调度的最大延迟 |
| wakeup_rt | 跟踪实时(RT)任务唤醒后调度的最大延迟 |
| nop | 最简单的跟踪器,仅显示
trace_printk()
调用的输出 |
4.3 使用function跟踪器
以下是使用function跟踪器的示例脚本:
# cd /sys/kernel/debug/tracing
# echo function > current_tracer
# echo 1 > tracing_on
# sleep 1
# echo 0 > tracing_on
# less trace
运行脚本后,输出包含缓冲区中的条目数量、写入的总条目数量,以及每个跟踪函数的信息,包括进程名、进程ID、CPU编号、函数开始时间、被跟踪函数名和调用它的父函数名。
4.4 使用function_graph跟踪器
function_graph跟踪器比function跟踪器更详细,会显示每个函数的入口和出口点,并能测量函数执行时间。
修改前面的脚本:
# cd /sys/kernel/debug/tracing
# echo function_graph > current_tracer
# echo 1 > tracing_on
# sleep 1
# echo 0 > tracing_on
# less trace
输出中,
DURATION
显示函数运行时间,
+
表示函数执行时间超过10微秒,
!
表示超过100微秒。函数调用使用C语言中的花括号
{}
表示开始和结束,无子调用的叶函数用分号
;
标记。
Ftrace还支持通过
tracing_thresh
选项限制只跟踪执行时间超过一定阈值的函数。可以在启动时通过内核命令行设置阈值:
tracing_thresh=200 ftrace=function_graph
也可以在运行时设置:
echo 200 > tracing_thresh
4.5 函数过滤
可以使用过滤器简化Ftrace的输出,只显示感兴趣的函数信息。
-
设置过滤函数
:
# echo kfree > set_ftrace_filter
- 禁用过滤器 :
# echo > set_ftrace_filter
- 排除特定函数 :
# echo kfree > set_ftrace_notrace
还可以使用
set_ftrace_pid
工具跟踪特定进程调用的函数。更多过滤选项可参考官方文档https://www.kernel.org/doc/Documentation/trace/ftrace.txt 。
4.6 跟踪事件
在介绍跟踪事件之前,先了解跟踪点。跟踪点是触发系统事件的特殊代码插入点,分为动态和静态两种。静态跟踪点对系统无影响,仅在被检测函数末尾添加少量函数调用字节和一个数据结构;动态跟踪点在相关代码片段执行时调用跟踪函数,并将跟踪数据写入环形缓冲区。
Linux内核提供了从用户空间操作跟踪点的特殊API,在
/sys/kernel/debug/tracing/events
目录中保存了系统事件。
- 查看可用事件列表 :
# cat /sys/kernel/debug/tracing/available_events
- 以结构化方式查看事件 :
# ls /sys/kernel/debug/tracing/events
以跟踪hrtimer相关内核函数为例,使用
nop
跟踪器:
# cd /sys/kernel/debug/tracing/
# echo 0 > tracing_on
# echo > trace
# echo nop > current_tracer
# echo 1 > events/timer/enable
# echo 1 > tracing_on;
# sleep 1;
# echo 0 > tracing_on;
# echo 0 > events/timer/enable
# less trace
更多关于事件跟踪配置的详细信息可参考https://www.kernel.org/doc/Documentation/trace/events.txt 。
Linux内核调试技巧与最佳实践
5. 内核调试实战案例分析
为了更好地理解上述的内核调试技巧,下面通过几个实战案例进行详细分析。
5.1 设备驱动调试案例
假设我们正在开发一个新的设备驱动,在设备初始化过程中遇到了问题。我们可以使用内核日志打印来定位问题。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
static struct device *my_device;
static int __init my_driver_init(void) {
pr_info("My driver is initializing...\n");
my_device = device_create(NULL, NULL, 0, NULL, "my_device");
if (IS_ERR(my_device)) {
pr_err("Failed to create device\n");
return PTR_ERR(my_device);
}
pr_info("Device created successfully\n");
return 0;
}
static void __exit my_driver_exit(void) {
pr_info("My driver is exiting...\n");
device_destroy(NULL, 0);
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
在这个案例中,我们使用
pr_info
和
pr_err
来打印初始化过程中的信息。如果设备创建失败,会打印错误信息,方便我们定位问题。
5.2 性能问题调试案例
假设系统出现了性能问题,CPU使用率过高。我们可以使用Ftrace来分析是哪些函数占用了大量时间。
# cd /sys/kernel/debug/tracing
# echo function_graph > current_tracer
# echo 1 > tracing_on
# sleep 10 # 运行一段时间来收集数据
# echo 0 > tracing_on
# less trace
通过分析
trace
文件的输出,我们可以找到执行时间较长的函数,例如带有
+
或
!
标记的函数。然后根据这些信息对代码进行优化。
6. 内核调试的注意事项
在进行内核调试时,需要注意以下几点:
-
编译选项
:在调试时,建议启用
DEBUG
选项,这样可以让
pr_debug
等调试信息生效。
-
日志级别
:合理设置日志级别,避免过多的日志信息影响系统性能。可以根据需要调整
console_loglevel
。
-
缓冲区大小
:如果需要记录大量的日志信息,适当增大内核日志缓冲区的大小,避免日志丢失。
-
性能影响
:一些调试工具,如Ftrace,可能会对系统性能产生一定影响。在生产环境中使用时,需要谨慎评估。
7. 总结
本文详细介绍了Linux内核调试的相关技巧和最佳实践,包括看门狗设备驱动的操作、内核发布过程、开发技巧、跟踪与性能分析等方面。通过掌握这些知识,我们可以更高效地进行Linux内核的开发和调试。
以下是本文内容的总结表格:
| 主题 | 主要内容 |
| ---- | ---- |
| 看门狗设备驱动 | 通过sysfs接口操作看门狗参数,如检查预超时调节器和预超时值 |
| 内核发布过程 | 包括主线版本、稳定版本和长期支持版本,介绍了发布流程和相关链接 |
| 内核开发技巧 | 消息打印、日志级别、控制台日志级别、日志缓冲区和时间信息的处理 |
| 内核跟踪与性能分析 | 使用Ftrace进行代码插桩、函数跟踪、事件跟踪和性能分析 |
| 调试实战案例 | 设备驱动调试和性能问题调试的案例分析 |
| 注意事项 | 编译选项、日志级别、缓冲区大小和性能影响等方面的注意事项 |
最后,为了更清晰地展示整个内核调试的流程,以下是一个mermaid流程图:
graph LR
A[开始调试] --> B{选择调试方法}
B -- 打印调试 --> C[使用printk或pr_*系列函数]
B -- 跟踪调试 --> D[使用Ftrace]
C --> E[设置日志级别和缓冲区]
D --> F[选择跟踪程序和过滤器]
E --> G[分析日志信息]
F --> H[分析跟踪数据]
G --> I{问题解决?}
H --> I
I -- 是 --> J[结束调试]
I -- 否 --> B
通过这个流程图,我们可以看到内核调试的基本流程,根据问题的特点选择合适的调试方法,然后进行分析和解决。希望这些内容能帮助你更好地进行Linux内核的开发和调试工作。
超级会员免费看
1万+

被折叠的 条评论
为什么被折叠?



