2. Linux 内核日志机制:printk原理、内核日志缓冲区、dmesg命令详解、klogd与syslogd关系
内核日志是Linux系统故障排查的“第一现场”。当系统崩溃、驱动异常或硬件故障时,用户态进程可能已经无法响应,但内核仍然可以通过其独立的日志机制记录关键信息。本章深入剖析内核日志的完整生命周期:从内核空间的printk函数,到内核日志缓冲区(kernel ring buffer),再到用户态的dmesg命令以及klogd与syslogd的协作关系。
2.1 printk 原理与日志级别
printk是内核空间的“printf”,但它比用户态的printf复杂得多。它不仅要输出文本,还要携带日志级别(loglevel),用于控制消息的紧急程度和输出目标。
2.1.1 printk 函数原型与日志级别宏
// 内核源码 include/linux/kern_levels.h
#define KERN_EMERG "<0>" /* 系统不可用 */
#define KERN_ALERT "<1>" /* 必须立即采取行动 */
#define KERN_CRIT "<2>" /* 严重条件 */
#define KERN_ERR "<3>" /* 错误条件 */
#define KERN_WARNING "<4>" /* 警告条件 */
#define KERN_NOTICE "<5>" /* 正常但重要的条件 */
#define KERN_INFO "<6>" /* 信息性消息 */
#define KERN_DEBUG "<7>" /* 调试级别消息 */
// 使用示例
printk(KERN_ERR "e1000: transmit queue %d is full\n", queue_id);
printk(KERN_INFO "CPU: %d package(s) x %d cores\n", packages, cores);
2.1.2 printk 输出目标决策流程
printk并非简单地将消息写入缓冲区,它需要根据控制台日志级别(console_loglevel)、默认消息日志级别(default_message_loglevel)和最小控制台日志级别(minimum_console_loglevel)三个内核变量决定消息的去向。
/proc/sys/kernel/printk 文件可以动态调整控制台日志级别。例如 echo "4 4 1 7" > /proc/sys/kernel/printk 表示只允许级别高于4(即0-3)的消息输出到控制台,减少控制台I/O干扰。
2.2 内核日志缓冲区(Kernel Ring Buffer)
内核日志缓冲区是一个固定大小的环形缓冲区,位于内核空间。它的设计目标是:在内存受限的环境下,始终保留最新的日志记录,避免因日志量过大导致内存耗尽。
2.2.1 缓冲区结构与关键参数
| 参数 | 默认值 | 说明 |
|---|---|---|
__LOG_BUF_LEN |
1 << CONFIG_LOG_BUF_SHIFT | 缓冲区总大小,通常为 128KB(CONFIG_LOG_BUF_SHIFT=17) |
log_buf |
内核全局数组 | 实际存储日志的字符数组 |
log_end |
0 | 下一条日志写入位置的偏移量 |
logged_chars |
0 | 当前已使用的字节数 |
2.2.2 环形缓冲区工作原理
环形缓冲区采用“写覆盖”策略:当写入位置到达缓冲区末尾时,自动回绕到开头继续写入。这意味着最旧的日志会被最新的日志覆盖。缓冲区中始终保留的是最近产生的日志记录。
2.3 dmesg 命令详解
dmesg是用户态读取内核日志缓冲区的主要工具。它通过 /dev/kmsg 设备文件或 syslog() 系统调用获取内核日志。
2.3.1 常用操作与参数
| 命令 | 功能 | 典型场景 |
|---|---|---|
dmesg |
打印所有内核日志 | 查看系统启动以来的所有内核消息 |
dmesg -T |
显示可读时间戳 | 将内核时间戳转换为人类可读的日期时间 |
dmesg -l err |
按级别过滤(err/warn/info等) | 只查看错误级别的消息,快速定位问题 |
dmesg -c |
读取并清空缓冲区 | 在脚本中获取当前日志后清空,便于后续增量分析 |
dmesg -w |
实时监控新日志 | 类似 tail -f,观察实时产生的内核消息 |
dmesg -n 3 |
设置控制台日志级别 | 临时降低控制台输出级别,减少串口/屏幕干扰 |
2.3.2 实际排查案例:磁盘I/O错误定位
# 场景:系统出现间歇性磁盘错误,应用报错"Input/output error"
# 步骤1:使用 dmesg 过滤错误级别消息
dmesg -T -l err | tail -20
# 输出示例:
[Mon Jan 15 14:23:45 2024] ata3.00: status: { DRDY ERR }
[Mon Jan 15 14:23:45 2024] ata3.00: error: { UNC }
[Mon Jan 15 14:23:45 2024] sd 2:0:0:0: [sdb] Unrecovered read error
# 步骤2:查看与 sdb 相关的所有消息
dmesg -T | grep sdb | tail -30
# 步骤3:实时监控新错误
dmesg -w | grep -i "error\|fail\|timeout"
dmesg -T -l err,warn 组合过滤,只显示错误和警告级别的消息,避免被大量INFO级别消息淹没。对于存储相关故障,重点关注 ata、sd、nvme 前缀的消息。
2.4 klogd 与 syslogd 的关系
内核日志从内核空间传递到用户空间的日志文件(如 /var/log/messages),需要经过 klogd 和 syslogd 两个守护进程的协作。
2.4.1 架构与数据流
2.4.2 职责分工与交互流程
| 组件 | 运行位置 | 核心职责 | 数据来源 |
|---|---|---|---|
| klogd | 用户空间守护进程 | 1. 从 /dev/kmsg 读取内核日志 2. 通过 syslog() 系统调用转发给 syslogd 3. 可选:直接写入日志文件(-f 选项) |
内核日志缓冲区 |
| syslogd/rsyslogd | 用户空间守护进程 | 1. 接收来自 klogd 和其他应用的日志 2. 根据配置文件(/etc/rsyslog.conf)过滤和路由 3. 写入本地文件或转发到远程服务器 |
klogd、本地应用、网络 |
2.4.3 现代系统中的变化:systemd-journald
在采用 systemd 的现代 Linux 发行版(RHEL 7+/Ubuntu 15.04+)中,systemd-journald 取代了传统的 klogd + syslogd 组合。journald 直接通过 /dev/kmsg 读取内核日志,并将其与用户态日志统一存储在二进制日志文件(journal)中。
# 使用 journalctl 查看内核日志(替代 dmesg)
journalctl -k # 显示所有内核日志
journalctl -k -p err # 只显示错误级别内核日志
journalctl -k --since "1 hour ago" # 最近1小时的内核日志
# 查看内核日志与用户日志的关联
journalctl -u sshd -k # 显示 sshd 服务相关的内核消息
dmesg 命令仍然可用,因为它直接读取内核缓冲区。但 klogd 通常不再需要运行,因为 journald 已经接管了内核日志的读取和转发工作。如果需要将日志转发到远程 syslog 服务器,通常通过 rsyslogd 从 journald 中读取日志后再转发。
2.5 本章小结
- printk 是内核日志的源头,通过日志级别控制消息的紧急程度和输出目标。
- 内核日志缓冲区是固定大小的环形缓冲区,保留最新日志,但崩溃时可能丢失数据。
- dmesg 是读取内核缓冲区的主要工具,支持级别过滤、时间戳转换和实时监控。
- klogd 作为内核与 syslogd 之间的桥梁,将内核日志注入用户态日志系统。
- 现代系统使用 systemd-journald 统一管理日志,但传统工具链仍然兼容。