Linux 内核开发笔记:休眠唤醒、Proc 文件系统、调试与性能工具
档记录了 Linux 内核开发中的关键技术点,涵盖系统休眠与唤醒机制、Proc 文件系统接口创建、内核堆栈追踪、Printk 日志等级及动态调试方法、Trace 追踪工具以及多种性能分析工具的使用。内容包括电源管理操作、等待队列应用、内核调试接口实现细节及系统性能瓶颈排查技巧,旨在帮助开发者理解内核行为并进行有效调试。

档记录了 Linux 内核开发中的关键技术点,涵盖系统休眠与唤醒机制、Proc 文件系统接口创建、内核堆栈追踪、Printk 日志等级及动态调试方法、Trace 追踪工具以及多种性能分析工具的使用。内容包括电源管理操作、等待队列应用、内核调试接口实现细节及系统性能瓶颈排查技巧,旨在帮助开发者理解内核行为并进行有效调试。

休眠状态指的是一种系统低功耗运行状态。在此状态下,各种支持休眠模式的外围设备也都全部进入休眠模式,CPU 挂起,所有用户态应用程序和内核态进程全部被冻结,内存处于自刷新模式。系统处于休眠状态会屏蔽所有除唤醒之外的所有命令,直到系统被某种原因唤醒才会解除此种状态。
// cat /sys/power/state【freeze: 可快速恢复,mem: 长时间省电】:支持哪些挂起模式,不是当前状态
// echo freeze/mem > /sys/power/state:触发休眠,命令敲完后,串口卡住没反应了(在/sys/power/autosleep【CONFIG_PM_AUTOSLEEP:自动休眠唤醒】是 off 状态才行)
// kernel/power/suspend.c: int pm_suspend(suspend_state_t state)
// autosleep 也是调用这行函数
{
error = enter_state(state);
// 进入 suspend 状态,具体的状态根据 state 值而定,如果执行成功的话,一直到系统退出 suspend 的状态后此函数才退出
}
// 如果外设没有实现 dev_pm_ops,那么外设对于休眠唤醒没有任何动作
struct dev_pm_ops {
int (*suspend)(struct device *dev); // 休眠,下电,与下面成对出现
int (*resume)(struct device *dev); // 唤醒,上电
...
int (*runtime_suspend)(struct device *dev); // 单设备:并不是要求设备一定要进入低功耗状态,而是要求设备在 suspend 后,不再处理数据,不再和 CPUs、RAM 进行任何的交互,直到设备的 runtime_resume 被调用
int (*runtime_resume)(struct device *dev);
};
如整机休眠时调用下面外设的.suspend。
如下查看哪个驱动模块当前有活跃锁,导致整机休眠不下去。自动休眠唤醒 suspend 控制器发现如果 /sys/kernel/debug/wakeup_sources 中 active_since 一列都为 0 即没有持锁(即没有唤醒)就休眠。
如下 __pm_relax 用来释放先前通过 __pm_stay_awake 增加的唤醒请求即 active_since 一列。
如按触屏和播放视频都不能休眠。
/* 有时候进程在读设备时,发现设备数据还没准备好,没办法正常读取设备。或在写设备时,发现设备缓冲区满,没办法正常写设备。进程在操作设备时,如果条件不满足,就让它进入休眠等待,直到条件满足,就可唤醒进程进行后面操作。
初始化:DECLARE_WAIT_QUEUE_HEAD() // 初始化等待队列头,宏的静态方式 或 wait_queue_head_t wq; // 动态方式 init_waitqueue_head(&wq);
休眠:wait_event() // 不可被打断,死等,直到满足条件为止
wait_event_interruptible() // 可使用信号打断它,常用
唤醒:wake_up() // 对应 wait_event()
wake_up_interruptible() // 对应上面可中断方式休眠的进程 wait_event_interruptible()
*/
#include <linux/wait.h>
DECLARE_WAIT_QUEUE_HEAD(wq);
{
// hc_read 函数中
printk(KERN_INFO "read hc_dev %p\n", hc_dev);
// wait_event(wq, hc_dev->c != NULL); // 第一个参数:等待队列头。第二个参数:等待条件:如果设备字符串区(即 echo 值进设备文件里)为空就进入等待(唤醒方法在 write 函数中)。
wait_event_interruptible(wq, hc_dev->c != NULL);
}
{
// hc_write 函数中
printk(KERN_INFO "%s write done", current->comm);
wake_up(&wq); // 当写入字符设备成功后调用 wakeup 函数唤醒等待队列上的进程
wake_up_interruptible(&wq);
return count;
}
如下等待条件满足,如上 卡住的即休眠的进程 会正常退出(两个进程 cat 卡住休眠,执行如下一行,两个进程都不会卡住)。
/* 新调试方法:利用 proc 文件系统在 pro 文件夹下创建接口,读写这个接口就可实现对内核的调试
struct proc_ops // pro 文件夹下创建接口第一种方式 proc_create()
struct seq_operations // 第二种方式 proc_create_seq()
remove_proc_entry // 移除接口
*/
#include <linux/module.h>
#include <linux/uaccess.h>
#include <linux/string.h>
#define PROC_DEBUG
#ifdef PROC_DEBUG
#include <linux/proc_fs.h> // 传统第一种方式
#include <linux/seq_file.h> // seq 第二种方式
#endif
char* str = "hello proc\n";
#ifdef PROC_DEBUG
int hp_open(struct inode *inode, struct file *filp) {
printk(KERN_INFO "open %ld\n", strlen(str));
return 0;
}
ssize_t hp_read(struct file *filp, char __user *buff, size_t count, loff_t *f_pos) {
ssize_t retval = 0;
int n = strlen(str); // 内容总长度(11 字节,含\n)
if (*f_pos >= n) goto out;
if (*f_pos + count > n) count = n - *f_pos; // 防止越界 count = n -*f_pos;调整读取长度
if (copy_to_user(buff, str, count)) { // 将字符串 str("hello proc\n") 赋值到 buff 用户空间
retval = -EFAULT;
goto out;
}
*f_pos += count; // 更新偏移量
return count;
out:
return retval;
}
struct proc_ops hp_ops = {
// 下面 __init 函数中 proc_create 调用 hp_ops 创建接口文件
.proc_open = hp_open,
.proc_read = hp_read,
};
void* hp_seq_start(struct seq_file *m, loff_t *pos) // pos 表示当前读到哪个位置或写到哪个位置了
{
printk(KERN_INFO "seq start\n");
if (*pos >= strlen(str)) // pos 指当前读到或写到哪个位置,索引
return NULL;
return &str[*pos]; // 拿出字符串中字符,将地址返回,这返回值将来作为下面其他函数的 v 传入
}
void hp_seq_stop(struct seq_file *m, void *v) {
printk(KERN_INFO "seq stop\n"); // 清除 start 函数一些工作,start 里开辟一些空间或申请一些锁,这里清除
}
void* hp_seq_next(struct seq_file *m, void *v, loff_t *pos) // 改变索引值
{
printk(KERN_INFO "seq next\n");
(*pos)++;
if (*pos >= strlen(str)) return NULL;
return &str[*pos];
}
int hp_seq_show(struct seq_file *m, void *v) {
printk(KERN_INFO "seq show\n");
seq_putc(m, *(char*)v); // 将获得到的字符一个一个打印出
return 0;
}
const struct seq_operations seq_ops = {
.start = hp_seq_start,
.stop = hp_seq_stop,
.next = hp_seq_next,
.show = hp_seq_show,
};
#endif
static int __init hello_init(void) {
printk(KERN_INFO "HELLO LINUX MODULE\n");
#ifdef PROC_DEBUG
proc_create("hello_proc", 0, NULL, &hp_ops); // 第一个参数即显示在 pro 目录下文件名称,第二个参数默认 0 只读权限。第三个参数父节点,null 默认 pro 目录。最后一个参数是操作的结构体地址
proc_create_seq("hello_seq_proc", 0, NULL, &seq_ops); // 就可在 pro 目录下创建对应节点
#endif
return 0;
}
static void __exit hello_exit(void) {
#ifdef PROC_DEBUG
remove_proc_entry("hello_proc", NULL); // 第二个参数是父节点
remove_proc_entry("hello_seq_proc", NULL);
#endif
printk(KERN_INFO "GOODBYE LINUX\n");
}
module_init(hello_init);
module_exit(hello_exit);
show next 调用了 11 次,最后一次返回 null 会调用 stop。cat 又会再调用一次,调 start 后直接返回 null 到 stop。
WARN_ON(condition) 函数实际也是调用 dump_stack,如 WARN_ON(1) 条件判断成功,类似 BUG_ON(1)。
cat /proc/cmdline 查看 console=ttyS0,如下图左边分支是远程登录,缺点是 log_buf 是环形 buff(会循环覆盖,日志不全)。左右分支都有 dmesg,左分支 dmesg 不受下面 x 控制,所以 dmesg 是最全的。
// Linux 内核为 printk 定义了 8 个打印等级,KERN_EMERG 等级最高,KERN_DEBUG 等级最低。在内核配置时,有一个宏来设定系统默认的打印等级 CONFIG_MESSAGE_LOGLEVEL_DEFAULT,通常该值设置为 4,那么只有打印等级高于 4 时才会打印到终端或者串口。
// kern_levels.h
#define KERN_EMERG KERN_SOH "0" /* system is unusable 紧急事件,一般是系统崩溃之前的提示消息 */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately 必须立即采取行动 */
#define KERN_CRIT KERN_SOH "2" /* critical conditions 临界状态,通常涉及严重的硬件或者软件操作失败 */
#define KERN_ERR KERN_SOH "3" /* error conditions 报告错误状态,经常用来报告硬件错误 */
#define KERN_WARNING KERN_SOH "4" /* warning conditions 对可能出现问题的情况进行警告,通常不会对系统造成严重问题 */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition 有必要的提示,通常用于安全相关的状况汇报 */
#define KERN_INFO KERN_SOH "6" /* informational 提示信息,驱动程序常用来打印硬件信息 */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages 用于调试信息 */
// printk("[me]%s[%d].\n", __func__, __LINE__);
// printk 调用 printk_safe_enter_irqsave --> local_irq_save【把当前的中断状态(开或关)保存到 flags 中,然后禁用当前处理器上的中断】,所以:板卡 --> 没有办法跟外界通信(I2C、UART、SPI 都用到中断)
printk 是全局的且只能设打印等级,动态打印可控制选择模块的打印,在内核配置打开 CONFIG_DYNAMIC_DEBUG: printk 会关中断影响性能,如果在 usb 的 read/write 里 printk,那么 usb 就没法直接用了。所以用动态打印,调试时才打开,control 节点默认不输出,如下操作才输出,+p 是将动态打印语句【pr_debug/dev_dbg()】转为 printk 语句 (相当于下面的 define dev_dbg …)。
ltrace,strace 参考 notes8 epoll,这俩底层实现是 ptrace(如下)。
ifconfig 和 ip 分别属于 net-tools 和 iproute2: 上面是统计信息,下面是网络吞吐:
可 echo 上面支持的策略 > scaling_governor。
access_ok:判断 arg 地址的数据长度是不是和 len 一样大: likely 一般用在 if 判断里,cpu 会把当前指令后面指令预取出来,等到执行时就去执行,效率提高,但是也要判断后面那条指令大概率执不执行,执行的话取出来,不执行则跳过。
debugfs.h 中 api 建立目录/sys/kernel/debug
内核开发会遇到内核崩溃,如空指针异常,内存访问越界,如下会打印出异常调用栈信息定位。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online