《Linux 设备驱动开发详解:基于最新的 Linux 4.0 内核》
第 24 章 Linux 设备驱动的调试进阶
参考:宋宝华 著,机械工业出版社,2015年版
24.1 动态调试(Dynamic Debug)
24.1.1 动态调试的概念
动态调试(Dynamic Debug)是 Linux 内核提供的一种运行时按需开启调试输出 的机制,无需重新编译内核或驱动即可控制 pr_debug()、dev_dbg() 等调试消息的输出:
动态调试 vs 传统调试方式:
传统方式(#define DEBUG):
需要在源码中添加 #define DEBUG
需要重新编译驱动
所有调试信息同时输出,难以过滤
生产环境不能使用
动态调试:
无需修改源码,无需重新编译
运行时精确控制哪些文件/函数/行的调试信息输出
可以按文件、函数、行号、模块过滤
生产环境可以按需开启,不影响性能
支持显示文件名、行号、函数名、线程 ID 等附加信息
24.1.2 动态调试的配置
内核配置:
bash
# 内核配置(make menuconfig)
# → Kernel hacking
# → [*] Dynamic printk() support (CONFIG_DYNAMIC_DEBUG=y)
# 验证配置
grep CONFIG_DYNAMIC_DEBUG /boot/config-$(uname -r)
# CONFIG_DYNAMIC_DEBUG=y
挂载 debugfs:
bash
# 挂载 debugfs(通常已自动挂载)
mount -t debugfs none /sys/kernel/debug
# 验证动态调试控制文件存在
ls /sys/kernel/debug/dynamic_debug/
# control
24.1.3 动态调试控制语法
bash
# 控制文件格式:
# echo "查询条件 操作" > /sys/kernel/debug/dynamic_debug/control
# 操作符:
# +p:开启打印(print)
# -p:关闭打印
# +f:显示函数名
# +l:显示行号
# +m:显示模块名
# +t:显示线程 ID
# =p:只开启打印(清除其他标志)
# 查询条件关键字:
# file <文件名>:按文件名过滤
# func <函数名>:按函数名过滤
# line <行号>:按行号过滤
# module <模块名>:按模块名过滤
# format <格式字符串>:按消息内容过滤
# all:匹配所有
24.1.4 动态调试的使用案例
bash
# ── 查看当前动态调试状态 ──────────────────────────────────────
# 查看所有可调试的打印点
cat /sys/kernel/debug/dynamic_debug/control | head -20
# drivers/i2c/i2c-core.c:1234 [i2c_core]i2c_transfer =_ "i2c transfer\n"
# drivers/gpio/gpio-imx.c:56 [gpio_imx]imx_gpio_get =_ "gpio get\n"
# ...
# 格式:文件:行号 [模块]函数名 标志 "消息格式"
# 标志:_ 表示未开启,p 表示已开启打印
# 统计可调试点数量
wc -l /sys/kernel/debug/dynamic_debug/control
# 12345 /sys/kernel/debug/dynamic_debug/control
# ── 按文件开启调试 ────────────────────────────────────────────
# 开启特定文件的所有调试输出
echo "file drivers/i2c/i2c-core.c +p" > \
/sys/kernel/debug/dynamic_debug/control
# 开启多个文件
echo "file drivers/gpio/gpio-imx.c +p" >> \
/sys/kernel/debug/dynamic_debug/control
# 关闭特定文件的调试输出
echo "file drivers/i2c/i2c-core.c -p" > \
/sys/kernel/debug/dynamic_debug/control
# ── 按模块开启调试 ────────────────────────────────────────────
# 开启整个模块的调试输出
echo "module my_driver +p" > /sys/kernel/debug/dynamic_debug/control
# 开启模块调试并显示文件名、行号、函数名
echo "module my_driver +pflm" > /sys/kernel/debug/dynamic_debug/control
# p:打印
# f:函数名
# l:行号
# m:模块名
# 关闭模块调试
echo "module my_driver -p" > /sys/kernel/debug/dynamic_debug/control
# ── 按函数开启调试 ────────────────────────────────────────────
# 开启特定函数的调试输出
echo "func my_driver_probe +p" > /sys/kernel/debug/dynamic_debug/control
echo "func my_driver_read +p" >> /sys/kernel/debug/dynamic_debug/control
# ── 按行号开启调试 ────────────────────────────────────────────
# 开启特定行的调试输出
echo "file my_driver.c line 45 +p" > \
/sys/kernel/debug/dynamic_debug/control
# 开启行范围的调试输出
echo "file my_driver.c line 40-60 +p" > \
/sys/kernel/debug/dynamic_debug/control
# ── 按消息内容过滤 ────────────────────────────────────────────
# 只开启包含特定字符串的调试消息
echo "format \"i2c transfer\" +p" > \
/sys/kernel/debug/dynamic_debug/control
# ── 开启所有调试输出 ──────────────────────────────────────────
# 开启所有模块的调试输出(谨慎使用,输出量极大)
echo "all +p" > /sys/kernel/debug/dynamic_debug/control
# 关闭所有调试输出
echo "all -p" > /sys/kernel/debug/dynamic_debug/control
# ── 通过内核启动参数开启 ──────────────────────────────────────
# 在 U-Boot 中设置内核启动参数
# bootargs = "... dyndbg=\"module my_driver +p\""
# 或在 /etc/modprobe.d/ 中配置
echo "options my_driver dyndbg=+p" > /etc/modprobe.d/my_driver.conf
24.1.5 在驱动中使用动态调试
c
/*
* 驱动中使用动态调试的正确方式
*/
#include <linux/kernel.h>
#include <linux/device.h>
/* pr_debug:支持动态调试(需要 CONFIG_DYNAMIC_DEBUG=y)*/
pr_debug("驱动初始化,参数 val=%d\n", val);
/* dev_dbg:带设备信息的调试输出(推荐在驱动中使用)*/
dev_dbg(&pdev->dev, "设备 %s 初始化完成\n", dev_name(&pdev->dev));
dev_dbg(&client->dev, "I2C 读取寄存器 0x%02x = 0x%02x\n", reg, val);
/*
* 注意:
* 1. pr_debug 和 dev_dbg 在 CONFIG_DYNAMIC_DEBUG=y 时支持动态控制
* 2. 在 CONFIG_DYNAMIC_DEBUG=n 时,如果没有定义 DEBUG 宏,这些调用被编译掉
* 3. 不要使用 printk(KERN_DEBUG ...) 代替,它不支持动态调试
*/
/* 验证动态调试效果 */
/*
* 加载驱动后:
* echo "module my_driver +pflm" > /sys/kernel/debug/dynamic_debug/control
* dmesg | grep my_driver
* 输出示例:
* [ 5.123] my_driver my_driver_probe:45 my_driver: 设备 my_device 初始化完成
* 格式:[时间] 模块名 函数名:行号 消息
*/
24.2 ftrace
24.2.1 ftrace 简介
ftrace(Function Tracer)是 Linux 内核内置的函数级跟踪框架,可以跟踪内核函数调用、中断、调度等事件,是分析内核行为和性能问题的强大工具:
ftrace 的主要功能:
1. 函数跟踪(Function Tracer)
跟踪内核函数的调用和返回
可以过滤特定函数
2. 函数图跟踪(Function Graph Tracer)
显示函数调用关系(树形结构)
显示每个函数的执行时间
3. 事件跟踪(Event Tracer)
跟踪内核预定义的事件(中断、调度、系统调用等)
支持自定义事件
4. 延迟跟踪(Latency Tracer)
跟踪中断延迟、调度延迟
用于实时性分析
5. 唤醒延迟跟踪(Wakeup Tracer)
跟踪进程从唤醒到运行的延迟
24.2.2 ftrace 的基本使用
bash
# ftrace 通过 debugfs 控制
cd /sys/kernel/debug/tracing
# 查看可用的跟踪器
cat available_tracers
# blk function_graph wakeup_dl wakeup_rt wakeup function nop
# 查看当前跟踪器
cat current_tracer
# nop ← 默认不跟踪
# 查看可用的事件
ls events/
# block ext4 gpio i2c irq kmem net sched signal skb ...
# ── 函数跟踪器 ────────────────────────────────────────────────
# 设置函数跟踪器
echo function > current_tracer
# 设置要跟踪的函数(支持通配符)
echo "my_driver_*" > set_ftrace_filter
echo "i2c_transfer" >> set_ftrace_filter
# 开始跟踪
echo 1 > tracing_on
# 执行要调试的操作
cat /dev/my_device
# 停止跟踪
echo 0 > tracing_on
# 查看跟踪结果
cat trace
# # tracer: function
# #
# # TASK-PID CPU# |||| TIMESTAMP FUNCTION
# # | | | |||| | |
# cat-1234 [000] .... 1234.567890: my_driver_open <-do_dentry_open
# cat-1234 [000] .... 1234.567891: my_driver_read <-vfs_read
# cat-1234 [000] .... 1234.567892: my_driver_release <-__fput
# 清空跟踪缓冲区
echo > trace
# ── 函数图跟踪器(显示调用关系和耗时)────────────────────────
# 设置函数图跟踪器
echo function_graph > current_tracer
# 设置要跟踪的函数
echo "my_driver_read" > set_graph_function
# 开始跟踪
echo 1 > tracing_on
cat /dev/my_device
echo 0 > tracing_on
# 查看结果
cat trace
# # tracer: function_graph
# #
# # CPU DURATION FUNCTION CALLS
# # | | | | | | |
# 0) | my_driver_read() {
# 0) 0.123 us | mutex_lock();
# 0) | copy_to_user() {
# 0) 0.456 us | __copy_to_user();
# 0) 0.789 us | }
# 0) 0.234 us | mutex_unlock();
# 0) 1.602 us | }
# ── 事件跟踪 ──────────────────────────────────────────────────
# 查看 I2C 相关事件
ls events/i2c/
# i2c_read i2c_result i2c_write i2c_reply
# 使能 I2C 事件跟踪
echo 1 > events/i2c/enable
# 使能特定事件
echo 1 > events/i2c/i2c_write/enable
echo 1 > events/i2c/i2c_read/enable
# 开始跟踪
echo 1 > tracing_on
# 执行 I2C 操作...
echo 0 > tracing_on
# 查看结果
cat trace
# cat-1234 [000] .... 1234.567: i2c_write: i2c-1 #0 a=048 f=0000 l=1 [00]
# cat-1234 [000] .... 1234.568: i2c_read: i2c-1 #1 a=048 f=0001 l=2
# ── 中断延迟跟踪 ──────────────────────────────────────────────
# 跟踪中断禁止的最大延迟
echo irqsoff > current_tracer
echo 1 > tracing_on
# 运行一段时间...
echo 0 > tracing_on
cat trace
# 显示中断被禁止的最长时间段的调用栈
# ── 使用 trace-cmd 工具(更方便)────────────────────────────
# 安装 trace-cmd
sudo apt-get install trace-cmd
# 记录函数调用
sudo trace-cmd record -p function -l "my_driver_*" cat /dev/my_device
# 分析记录
trace-cmd report
# 记录事件
sudo trace-cmd record -e i2c cat /dev/my_device
trace-cmd report
24.2.3 ftrace 在驱动调试中的应用
bash
# 案例一:分析驱动函数调用链
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo "my_driver_*" > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 触发驱动操作
echo "test" > /dev/my_device
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# 案例二:分析中断处理时间
echo function_graph > /sys/kernel/debug/tracing/current_tracer
echo "my_irq_handler" > /sys/kernel/debug/tracing/set_graph_function
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 等待中断触发...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# 查看中断处理函数的执行时间
# 案例三:跟踪内存分配
echo 1 > /sys/kernel/debug/tracing/events/kmem/kmalloc/enable
echo 1 > /sys/kernel/debug/tracing/events/kmem/kfree/enable
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 加载驱动...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace | grep "my_driver"
# 案例四:分析调度延迟
echo wakeup > /sys/kernel/debug/tracing/current_tracer
echo 1 > /sys/kernel/debug/tracing/tracing_on
# 运行实时任务...
echo 0 > /sys/kernel/debug/tracing/tracing_on
cat /sys/kernel/debug/tracing/trace
# 显示最大唤醒延迟
# 清理
echo nop > /sys/kernel/debug/tracing/current_tracer
echo > /sys/kernel/debug/tracing/set_ftrace_filter
echo > /sys/kernel/debug/tracing/trace
24.3 perf 性能分析
24.3.1 perf 简介
perf 是 Linux 内核自带的性能分析工具,基于硬件性能计数器(PMU)和内核跟踪点,可以分析 CPU 使用率、缓存命中率、函数调用热点等:
perf 的主要功能:
perf stat:统计程序运行期间的性能事件
perf record:采样记录性能数据
perf report:分析 perf record 的结果
perf top:实时显示 CPU 热点函数
perf trace:跟踪系统调用(类似 strace)
perf annotate:显示带注释的汇编代码
perf script:将 perf.data 转换为文本格式
perf diff:比较两次 perf record 的结果
24.3.2 perf 的安装和基本使用
bash
# 安装 perf
sudo apt-get install linux-tools-$(uname -r) linux-tools-generic
# 验证安装
perf --version
# perf version 4.0.0
# ── perf stat:性能统计 ────────────────────────────────────────
# 统计程序运行期间的性能事件
perf stat ./my_app
# Performance counter stats for './my_app':
#
# 1,234.56 msec task-clock # 0.987 CPUs utilized
# 5 context-switches # 0.004 K/sec
# 0 cpu-migrations # 0.000 K/sec
# 123 page-faults # 0.100 K/sec
# 3,456,789,012 cycles # 2.800 GHz
# 2,345,678,901 instructions # 0.68 insn per cycle
# 456,789,012 branches # 369.999 M/sec
# 1,234,567 branch-misses # 0.27% of all branches
# 统计特定事件
perf stat -e cache-misses,cache-references,instructions,cycles ./my_app
# 统计内核模块的性能(需要 root)
sudo perf stat -e cycles,instructions insmod my_driver.ko
# ── perf top:实时热点分析 ────────────────────────────────────
# 实时显示 CPU 热点函数
sudo perf top
# 只分析特定进程
sudo perf top -p 1234
# 只分析内核函数
sudo perf top -K
# 显示特定事件的热点
sudo perf top -e cache-misses
# ── perf record + report:详细分析 ───────────────────────────
# 记录性能数据(采样)
sudo perf record -g ./my_app # -g:记录调用栈
sudo perf record -g -a sleep 10 # -a:记录所有 CPU,持续 10 秒
# 分析记录结果
sudo perf report
# 交互式界面,显示热点函数和调用链
# 以文本格式输出
sudo perf report --stdio
# 生成火焰图
sudo perf record -g -F 99 ./my_app # -F 99:99Hz 采样频率
sudo perf script | ./FlameGraph/stackcollapse-perf.pl | \
./FlameGraph/flamegraph.pl > flamegraph.svg
# ── perf trace:系统调用跟踪 ─────────────────────────────────
# 跟踪系统调用(类似 strace,但开销更小)
sudo perf trace ./my_app
# 只跟踪特定系统调用
sudo perf trace -e open,read,write,ioctl ./my_app
# ── perf annotate:汇编级分析 ────────────────────────────────
# 显示热点函数的汇编代码(带性能注释)
sudo perf annotate my_driver_read
24.3.3 perf 在驱动调试中的应用
bash
# 案例一:分析驱动的 CPU 使用率
sudo perf record -g -p $(pgrep my_app) sleep 10
sudo perf report --stdio | head -50
# 案例二:分析 Cache 命中率
sudo perf stat -e cache-misses,cache-references \
-e L1-dcache-loads,L1-dcache-load-misses \
./my_app
# 输出:
# 1,234,567 cache-misses # 12.34% of all cache refs
# 9,876,543 cache-references
# 案例三:分析中断频率
sudo perf stat -e irq:irq_handler_entry -a sleep 5
# 统计 5 秒内的中断次数
# 案例四:分析 DMA 传输性能
sudo perf record -e dma:dma_map_sg,dma:dma_unmap_sg -a sleep 5
sudo perf report
# 案例五:比较优化前后的性能
sudo perf record -o before.data ./my_app_v1
sudo perf record -o after.data ./my_app_v2
sudo perf diff before.data after.data
# 案例六:分析内核驱动函数热点
sudo perf record -g -e cycles:k -a sleep 10 # 只记录内核态
sudo perf report --stdio | grep "my_driver"
24.4 内存检测工具(KASAN、Valgrind)
24.4.1 KASAN(Kernel Address Sanitizer)
KASAN 是 Linux 内核内置的内存错误检测工具,可以检测以下问题:
KASAN 能检测的内存错误:
1. 越界访问(Out-of-bounds access)
访问了分配内存之外的区域
例:char buf[10]; buf[10] = 'x'; ← 越界
2. 释放后使用(Use-after-free)
访问了已经释放的内存
例:kfree(ptr); *ptr = 1; ← 危险!
3. 释放后再次释放(Double-free)
对同一块内存调用两次 kfree
例:kfree(ptr); kfree(ptr); ← 危险!
4. 栈越界(Stack out-of-bounds)
访问了栈变量之外的区域
5. 全局变量越界(Global out-of-bounds)
访问了全局数组之外的区域
KASAN 的配置和使用:
bash
# 内核配置(make menuconfig)
# → Kernel hacking
# → Memory Debugging
# → [*] KASAN: runtime memory debugger (CONFIG_KASAN=y)
# → KASAN mode (Inline instrumentation)
# → [*] KASan stack instrumentation
# 编译内核(KASAN 会显著增加内存使用和降低性能,仅用于调试)
make -j$(nproc)
# 加载有内存错误的驱动
sudo insmod buggy_driver.ko
# KASAN 检测到错误时的输出示例:
# ==================================================================
# BUG: KASAN: slab-out-of-bounds in my_driver_write+0x24/0x80 [my_driver]
# Write of size 1 at addr ffff880007a80010 by task cat/1234
#
# CPU: 0 PID: 1234 Comm: cat Tainted: G B O 4.0.0 #1
# Hardware name: QEMU Standard PC
# Call Trace:
# dump_stack+0x4d/0x65
# print_address_description+0x1f4/0x290
# kasan_report+0x138/0x180
# my_driver_write+0x24/0x80 [my_driver]
# vfs_write+0x8c/0x17c
# sys_write+0x44/0x74
#
# Allocated by task 1234:
# kmalloc+0x1b/0x30
# my_driver_open+0x34/0x80 [my_driver]
#
# The buggy address belongs to the object at ffff880007a80000
# which belongs to the cache kmalloc-16 of size 16
# ==================================================================
# 分析 KASAN 报告:
# 1. 错误类型:slab-out-of-bounds(堆越界)
# 2. 出错位置:my_driver_write+0x24
# 3. 内存分配位置:my_driver_open+0x34
# 4. 使用 addr2line 定位具体代码行
arm-linux-gnueabihf-addr2line -e my_driver.ko 0x24
制造和检测内存错误的示例:
c
/*
* 演示 KASAN 检测各种内存错误
*/
/* 错误1:堆越界写 */
static int test_heap_oob_write(void)
{
char *buf = kmalloc(10, GFP_KERNEL);
if (!buf) return -ENOMEM;
buf[10] = 'x'; /* ← KASAN 会在此处报告 slab-out-of-bounds */
kfree(buf);
return 0;
}
/* 错误2:释放后使用 */
static int test_use_after_free(void)
{
char *buf = kmalloc(10, GFP_KERNEL);
if (!buf) return -ENOMEM;
kfree(buf);
buf[0] = 'x'; /* ← KASAN 会在此处报告 use-after-free */
return 0;
}
/* 错误3:栈越界 */
static int test_stack_oob(void)
{
char buf[10];
buf[10] = 'x'; /* ← KASAN 会在此处报告 stack-out-of-bounds */
return 0;
}
/* 正确的内存使用 */
static int test_correct(void)
{
char *buf = kmalloc(10, GFP_KERNEL);
if (!buf) return -ENOMEM;
memset(buf, 0, 10); /* 正确:在分配范围内操作 */
buf[9] = 'x'; /* 正确:最后一个有效字节 */
kfree(buf);
return 0;
}
24.4.2 Valgrind(用户空间内存检测)
Valgrind 是用户空间程序的内存检测工具,不能直接用于内核驱动,但可以用于测试驱动的用户空间测试程序:
bash
# 安装 Valgrind
sudo apt-get install valgrind
# ── Memcheck:内存错误检测(最常用)────────────────────────────
# 检测内存错误
valgrind --leak-check=full --show-leak-kinds=all \
--track-origins=yes --verbose \
./test_my_driver
# 输出示例:
# ==1234== Memcheck, a memory error detector
# ==1234== Invalid write of size 1
# ==1234== at 0x10001234: test_heap_oob (test_driver.c:45)
# ==1234== by 0x10001567: main (test_driver.c:100)
# ==1234== Address 0x5204e8a is 0 bytes after a block of size 10 alloc'd
# ==1234== at 0x4C2FB0F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
# ==1234== by 0x10001200: test_heap_oob (test_driver.c:43)
#
# ==1234== LEAK SUMMARY:
# ==1234== definitely lost: 1,024 bytes in 1 blocks
# ==1234== indirectly lost: 0 bytes in 0 blocks
# ==1234== possibly lost: 0 bytes in 0 blocks
# ==1234== still reachable: 0 bytes in 0 blocks
# ── Callgrind:函数调用分析 ────────────────────────────────────
# 记录函数调用数据
valgrind --tool=callgrind ./test_my_driver
# 分析结果
callgrind_annotate callgrind.out.1234
# 使用 KCachegrind 可视化
kcachegrind callgrind.out.1234
# ── Helgrind:线程错误检测 ────────────────────────────────────
# 检测数据竞争和死锁
valgrind --tool=helgrind ./test_my_driver_threaded
# ── Massif:内存使用分析 ──────────────────────────────────────
# 分析内存使用情况
valgrind --tool=massif ./test_my_driver
# 可视化内存使用
ms_print massif.out.1234
24.4.3 kmemleak ------ 内核内存泄漏检测
bash
# 内核配置
# CONFIG_DEBUG_KMEMLEAK=y
# 使用 kmemleak
mount -t debugfs none /sys/kernel/debug
# 触发扫描
echo scan > /sys/kernel/debug/kmemleak
# 查看泄漏报告
cat /sys/kernel/debug/kmemleak
# unreferenced object 0xffff880007a80000 (size 1024):
# comm "insmod", pid 1234, jiffies 4294967295
# hex dump (first 32 bytes):
# 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|
# backtrace:
# [<ffffffff811234ab>] kmalloc+0x1b/0x30
# [<ffffffffc0001234>] my_driver_init+0x34/0x80 [my_driver]
# [<ffffffff810abcde>] do_one_initcall+0x3e/0x170
# 清除已知泄漏(重新开始检测)
echo clear > /sys/kernel/debug/kmemleak
# 禁用 kmemleak(减少性能影响)
echo off > /sys/kernel/debug/kmemleak
24.5 lockdep 死锁检测
24.5.1 lockdep 的概念
lockdep(Lock Dependency Validator)是 Linux 内核内置的锁依赖关系验证器,可以在运行时检测潜在的死锁和锁使用错误:
lockdep 能检测的问题:
1. 死锁(Deadlock)
线程A持有锁1,等待锁2
线程B持有锁2,等待锁1
→ 循环等待,永远无法解锁
2. 锁顺序违反(Lock Order Violation)
代码路径1:先获取锁A,再获取锁B
代码路径2:先获取锁B,再获取锁A
→ 可能导致死锁(即使当前没有死锁)
3. 中断上下文中使用睡眠锁
在中断处理函数中调用 mutex_lock()
→ 可能导致死锁(中断上下文不能睡眠)
4. 递归锁(Recursive Lock)
同一线程对同一个非递归锁加锁两次
→ 死锁
5. 锁的不正确使用
在持有自旋锁时调用可能睡眠的函数
24.5.2 lockdep 的配置
bash
# 内核配置(make menuconfig)
# → Kernel hacking
# → Lock Debugging (spinlocks, mutexes, etc...)
# → [*] RT Mutex debugging, deadlock detection (CONFIG_DEBUG_RT_MUTEXES=y)
# → [*] Spinlock and rw-lock debugging: basic checks (CONFIG_DEBUG_SPINLOCK=y)
# → [*] Mutex debugging: basic checks (CONFIG_DEBUG_MUTEXES=y)
# → [*] Lock debugging: detect incorrect freeing of live locks (CONFIG_DEBUG_LOCK_ALLOC=y)
# → [*] Lock debugging: prove locking correctness (CONFIG_PROVE_LOCKING=y)
# → [*] Lock usage statistics (CONFIG_LOCK_STAT=y)
24.5.3 lockdep 的使用和报告分析
bash
# lockdep 自动运行,检测到问题时打印警告
# 示例:循环锁依赖警告
# ======================================================
# WARNING: possible circular locking dependency detected
# 4.0.0 #1 Not tainted
# ------------------------------------------------------
# my_app/1234 is trying to acquire lock:
# (&dev->mutex_b){+.+.+.}, at: [<ffffffffc0001234>] my_func_b+0x24/0x80 [my_driver]
#
# but task is already holding lock:
# (&dev->mutex_a){+.+.+.}, at: [<ffffffffc0001567>] my_func_a+0x17/0x40 [my_driver]
#
# which lock already depends on the new lock.
#
# the existing dependency chain (in reverse order) is:
#
# -> #1 (&dev->mutex_b){+.+.+.}:
# lock_acquire+0x9e/0x1c0
# mutex_lock_nested+0x5b/0x3b0
# my_func_b+0x24/0x80 [my_driver]
# my_func_a+0x17/0x40 [my_driver]
#
# -> #0 (&dev->mutex_a){+.+.+.}:
# lock_acquire+0x9e/0x1c0
# mutex_lock_nested+0x5b/0x3b0
# my_func_a+0x17/0x40 [my_driver]
#
# other info that might help us debug this:
# Possible unsafe locking scenario:
#
# CPU0 CPU1
# ---- ----
# lock(&dev->mutex_a);
# lock(&dev->mutex_b);
# lock(&dev->mutex_a);
# lock(&dev->mutex_b);
#
# *** DEADLOCK ***
# ======================================================
24.5.4 常见死锁场景及解决方法
c
/*
* 场景一:循环锁依赖(最常见的死锁原因)
*/
/* ❌ 错误:两个函数以不同顺序获取锁 */
static void func_a(struct my_dev *dev)
{
mutex_lock(&dev->mutex_a); /* 先获取 mutex_a */
mutex_lock(&dev->mutex_b); /* 再获取 mutex_b */
/* ... */
mutex_unlock(&dev->mutex_b);
mutex_unlock(&dev->mutex_a);
}
static void func_b(struct my_dev *dev)
{
mutex_lock(&dev->mutex_b); /* 先获取 mutex_b */
mutex_lock(&dev->mutex_a); /* 再获取 mutex_a ← 与 func_a 顺序相反!*/
/* ... */
mutex_unlock(&dev->mutex_a);
mutex_unlock(&dev->mutex_b);
}
/* ✅ 正确:所有代码路径以相同顺序获取锁 */
static void func_a_fixed(struct my_dev *dev)
{
mutex_lock(&dev->mutex_a); /* 始终先获取 mutex_a */
mutex_lock(&dev->mutex_b); /* 再获取 mutex_b */
/* ... */
mutex_unlock(&dev->mutex_b);
mutex_unlock(&dev->mutex_a);
}
static void func_b_fixed(struct my_dev *dev)
{
mutex_lock(&dev->mutex_a); /* 始终先获取 mutex_a */
mutex_lock(&dev->mutex_b); /* 再获取 mutex_b */
/* ... */
mutex_unlock(&dev->mutex_b);
mutex_unlock(&dev->mutex_a);
}
/*
* 场景二:中断上下文中使用睡眠锁
*/
/* ❌ 错误:在中断处理函数中使用 mutex */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
mutex_lock(&dev->mutex); /* ← 危险!中断上下文不能睡眠 */
/* ... */
mutex_unlock(&dev->mutex);
return IRQ_HANDLED;
}
/* ✅ 正确:中断上下文使用自旋锁 */
static irqreturn_t my_irq_handler_fixed(int irq, void *dev_id)
{
struct my_dev *dev = dev_id;
unsigned long flags;
spin_lock_irqsave(&dev->spinlock, flags); /* 自旋锁,不会睡眠 */
/* ... */
spin_unlock_irqrestore(&dev->spinlock, flags);
return IRQ_HANDLED;
}
/*
* 场景三:持有自旋锁时调用可能睡眠的函数
*/
/* ❌ 错误:持有自旋锁时调用 kmalloc(GFP_KERNEL) */
static void my_func(struct my_dev *dev)
{
spin_lock(&dev->lock);
void *buf = kmalloc(1024, GFP_KERNEL); /* ← 危险!GFP_KERNEL 可能睡眠 */
spin_unlock(&dev->lock);
kfree(buf);
}
/* ✅ 正确:持有自旋锁时使用 GFP_ATOMIC */
static void my_func_fixed(struct my_dev *dev)
{
spin_lock(&dev->lock);
void *buf = kmalloc(1024, GFP_ATOMIC); /* GFP_ATOMIC 不会睡眠 */
spin_unlock(&dev->lock);
kfree(buf);
}
/*
* 场景四:递归锁
*/
/* ❌ 错误:同一线程对同一 mutex 加锁两次 */
static void my_func(struct my_dev *dev)
{
mutex_lock(&dev->mutex);
/* ... */
my_sub_func(dev); /* 如果 my_sub_func 也调用 mutex_lock,则死锁 */
mutex_unlock(&dev->mutex);
}
static void my_sub_func(struct my_dev *dev)
{
mutex_lock(&dev->mutex); /* ← 死锁!mutex 已被同一线程持有 */
/* ... */
mutex_unlock(&dev->mutex);
}
/* ✅ 正确方法一:重构代码,避免递归加锁 */
static void my_sub_func_nolock(struct my_dev *dev)
{
/* 假设调用者已持有锁 */
/* ... */
}
static void my_func_fixed(struct my_dev *dev)
{
mutex_lock(&dev->mutex);
/* ... */
my_sub_func_nolock(dev); /* 调用不加锁的版本 */
mutex_unlock(&dev->mutex);
}
24.5.5 lockdep 的调试命令
bash
# 查看锁统计信息
cat /proc/lock_stat
# lock_stat version 0.4
# ---------------------------------------------------------------
# class name con-bounces contentions waittime-min waittime-max waittime-total acq-bounces acquisitions holdtime-min holdtime-max holdtime-total
# ---------------------------------------------------------------
# &dev->mutex: 0 0 0 0 0 0 5 0 0 0
# 重置锁统计
echo 0 > /proc/lock_stat
# 查看 lockdep 状态
cat /proc/lockdep_stats
# lock-classes: 123
# direct dependencies: 456
# indirect dependencies: 789
# all direct dependencies: 1234
# dependency chains: 567
# in-hardirq chains: 12
# in-softirq chains: 34
# in-process chains: 521
# stack-trace entries: 5678
# combined max dependencies: 12345
# hardirq-safe locks: 10
# hardirq-unsafe locks: 20
# softirq-safe locks: 15
# softirq-unsafe locks: 25
# irq-safe locks: 30
# irq-unsafe locks: 40
# 查看所有锁类
cat /proc/lockdep | head -50
# 查看锁依赖关系
cat /proc/lockdep_chains | head -20
本章小结
| 章节 | 核心知识点 | 关键工具/命令 |
|---|---|---|
| 24.1 动态调试 | 动态调试概念(运行时按需开启);控制语法(file/func/line/module/format);+p/+f/+l/+m/+t标志;驱动中使用pr_debug/dev_dbg;内核启动参数配置 | /sys/kernel/debug/dynamic_debug/control、echo "module my_driver +pflm" |
| 24.2 ftrace | ftrace功能(函数/函数图/事件/延迟跟踪);函数跟踪器(set_ftrace_filter);函数图跟踪器(显示调用关系和耗时);事件跟踪(I2C/中断/调度);trace-cmd工具 | /sys/kernel/debug/tracing/、trace-cmd record、trace-cmd report |
| 24.3 perf性能分析 | perf stat(性能统计);perf top(实时热点);perf record+report(详细分析);perf trace(系统调用);火焰图生成;Cache命中率分析 | perf stat、perf top、perf record -g、perf report |
| 24.4 内存检测工具 | KASAN(越界/UAF/double-free/栈越界);KASAN报告解读;Valgrind(Memcheck/Callgrind/Helgrind/Massif);kmemleak内核内存泄漏检测 | CONFIG_KASAN=y、valgrind --leak-check=full、/sys/kernel/debug/kmemleak |
| 24.5 lockdep死锁检测 | lockdep检测的5类问题;循环锁依赖报告解读;4种常见死锁场景及修复(循环依赖/中断上下文/自旋锁+GFP_KERNEL/递归锁);锁统计命令 | CONFIG_PROVE_LOCKING=y、/proc/lock_stat、/proc/lockdep_stats |
调试工具选择指南
问题类型 → 推荐工具:
调试信息输出控制 → 动态调试(Dynamic Debug)
无需重编译,运行时按需开启 pr_debug/dev_dbg
函数调用链分析 → ftrace(function_graph)
查看函数调用关系和执行时间
性能热点分析 → perf(perf top / perf record)
找出 CPU 使用最多的函数
内存越界/UAF → KASAN(内核)/ Valgrind(用户空间)
检测内存访问错误
内存泄漏 → kmemleak(内核)/ Valgrind --leak-check(用户空间)
检测未释放的内存
死锁/锁顺序 → lockdep
检测潜在的死锁和锁使用错误
系统调用跟踪 → strace / perf trace
分析用户空间与驱动的交互
综合性能分析 → PowerTOP / perf
分析系统整体性能和功耗
参考文献:宋宝华《Linux设备驱动开发详解:基于最新的Linux 4.0内核》,机械工业出版社,2015年