排查一台机器莫名其妙的卡顿,我第一个敲的命令往往不是看应用日志,而是dmesg -T | tail -n 50。dmesg命令读的是内核环形缓冲区,硬件被识别、驱动加载失败、磁盘报错、内存耗尽触发 OOM、USB 反复重连,这些线索都会先落在这里,然后才轮到上层日志系统去抄一遍。很多人对它的印象停留在“开机时刷屏的那堆东西”,实际上它是你在现场唯一能直接看到内核说话的窗口,尤其是当系统还没完全起来、或者磁盘已经挂掉写不进日志文件的时候,它的价值会被放大好几倍。
这篇内容适合三类人:刚接触 Linux、想弄明白开机日志到底在说什么的新手;需要定位硬件与驱动问题的运维和嵌入式开发者;以及写自动化巡检脚本、想把内核异常做成告警的工程师。我会从环形缓冲区的机制讲起,把参数按照真实排障场景拆开,再完整复现一遍 USB 设备识别、磁盘 I/O 报错、内存溢出这三类现场,最后把踩过的坑整理成一张速查表。参数部分我会解释每个选项为什么存在、什么时候该用、什么时候用了反而误导,这些是手册里通常不会写的部分。
1. 先搞清楚 dmesg 到底在看什么
1.1 内核环形缓冲区不是普通日志文件
dmesg 的名字来自 display message,它做的事情非常单纯:把内核环形缓冲区里的内容打印到标准输出。这个缓冲区是内核在启动早期就分配好的一块固定内存,不是磁盘上的文件,也不是由某个守护进程维护的队列。内核在初始化阶段还没有挂载根文件系统,中断上下文里也不允许阻塞式地写磁盘,所以它只能在内存里留一块自留地,把消息按顺序塞进去。
固定大小带来一个必然结果:写满之后新消息会覆盖最旧的消息。这跟行车记录仪的循环录像是一个道理,卡满了就顶掉最早的片段。所以你在一个跑了半年的机器上执行 dmesg,看到的很可能是最近几小时甚至几分钟的内容,而不是“自开机以来的全部历史”。这一点特别容易误导人,很多人以为 dmesg 是完整流水账,结果去查三天前的硬件报错,什么都找不到。
缓冲区大小在编译内核时由CONFIG_LOG_BUF_SHIFT决定,单位是 2 的幂次,常见配置落在 128KB 到 1MB 多这个区间,具体取决于内存规模和发行版的取舍。运行时也能用启动参数log_buf_len=8M覆盖,但那要改引导配置并重启,属于“计划内动作”,不适合救火现场。你得先接受一个现实:dmesg 是短时记忆,要长期留存就必须靠外部手段转存,后面我会给具体做法。
1.2 一条记录里到底有什么
随便挑一行输出拆开看:[ 12.345678] usb 1-1: new high-speed USB device number 5 using xhci_hcd。方括号里是从内核启动开始算的秒数,精确到微秒,注意它跟墙上时钟没有直接关系,是单调递增的启动计时。后面是子系统前缀usb 1-1,说明这条消息来自 USB 子系统,设备挂在 1 号总线 1 号端口。冒号之后才是真正的消息体。
消息本身还隐含两个属性:facility 和 level。facility 表示消息来源类别,常见的有 kern(内核自身)、user(用户空间)、daemon、auth 等;level 表示严重程度,从 0 到 7 共八档,数字越小越紧急。这两个属性在默认输出里是被隐藏的,只有加上-x才会以前缀形式显示出来,比如kern :err :。理解这两个维度很关键,因为后面所有的过滤参数都是围绕它们设计的。
| 级别数字 | 名称 | 典型内容 |
|---|---|---|
| 0 | emerg | 系统不可用,内核panic前兆 |
| 1 | alert | 必须立即处理,如根文件系统只读 |
| 2 | crit | 严重硬件或驱动故障 |
| 3 | err | 设备报错、I/O失败、驱动加载失败 |
| 4 | warn | 警告,如时钟源不稳定、固件版本不匹配 |
| 5 | notice | 正常但值得注意的事件,设备注册、链路状态变化 |
| 6 | info | 常规信息,驱动初始化打印 |
| 7 | debug | 调试输出,默认多半被编译选项关掉 |
实际排障时,3 到 5 这三档是你最该盯住的区间。0 到 2 出现通常意味着机器已经或即将不可用,4 和 5 则经常藏着“看起来没坏但性能异常”的根因,比如网卡协商速率掉到百兆、磁盘链路反复重置。
1.3 谁有资格看:权限模型与 dmesg_restrict
在老系统上,普通用户直接执行 dmesg 通常就能看到全部内容。后来的内核引入了kernel.dmesg_restrict这个开关,把它设成 1 之后,只有具备 CAP_SYSLOG 能力的进程(基本等同于 root)才能读取缓冲区。收紧的原因不复杂:内核消息里可能包含内存地址、调用栈、模块基址,这些信息对攻击者有参考价值,配合kernel.kptr_restrict一起用能把泄漏面压下去。
所以你会在不少机器上撞见这个报错:
$ dmesg dmesg: read kernel buffer failed: Operation not permitted处理方式有三条路,按推荐程度排序。第一条是临时提权,sudo dmesg或sudo dmesg -T,一次性排障够用;第二条是调整 sysctl,把kernel.dmesg_restrict改成 0,但这是降低安全边界的操作,只建议在受控的测试机或内网调试环境里做;第三条是走日志服务,用journalctl -k读取由日志守护进程转存的内核消息,权限由日志系统自己的组策略管理,通常比直接放开内核开关更稳妥。
# 临时查看当前值 sysctl kernel.dmesg_restrict # 仅本次开机生效(测试环境使用) sudo sysctl -w kernel.dmesg_restrict=0 # 持久化需要写入 /etc/sysctl.d/ 下的配置文件在容器环境里还有一层麻烦:容器共享宿主内核,但/dev/kmsg通常不会挂进去,dmesg 要么直接报权限错误,要么只能看到容器命名空间内可见的那部分。要在容器里排查硬件问题基本是徒劳的,得回到宿主机上做。
2. 参数别死记,按排障场景拆开用
2.1 时间线三件套:-T、-H 与 --time-format
默认输出的秒数时间戳对人很不友好。[ 12.345678]这个数字要换算成“几点几分”,得先知道系统启动时间,再心算加法,现场根本来不及。-T就是干这个的,它读/proc/uptime拿到当前运行秒数,再结合当前墙上时钟反推出每条消息的绝对时间,输出成人类能读的日期格式。
但-T有个必须知道的副作用:它算出来的时间是基于“现在的时钟”倒推的,如果系统启动之后时钟被校正过(比如 NTP 在开机几分钟后把时间往前或往后拨了一大截),那么所有历史消息的时间都会整体偏移。我遇到过一次,机器时钟慢了四十多分钟,-T打出来的日志时间对不上业务日志,白查了半天。所以在时间精度要求高的场景,我更倾向用--time-format=iso输出 ISO 8601 格式,或者干脆直接用journalctl -k,后者在消息进入日志系统时就带上了接收时间戳,不受后续时钟校正影响。
-H是另一个提升可读性的选项,它会开启分页器、加颜色、把 facility 和 level 用彩色前缀标出来。适合人工翻阅,不适合管道处理,因为颜色转义字符会污染 grep 结果。我自己的习惯是:人眼看用dmesg -H,进脚本一律用dmesg --time-format=iso或者纯默认输出。
# 人工翻阅,带颜色和分页 dmesg -H # 脚本采集,时间格式稳定 dmesg --time-format=iso # 只看最近一段时间的消息(需要较新的 util-linux) dmesg --since "2024-05-01 10:00:00" --until "2024-05-01 10:05:00" dmesg --since "1 hour ago"--since和--until这两个参数依赖读取/dev/kmsg的新路径,老版本工具链上可能不认,报参数错误。遇到这种情况别硬扛,退回到dmesg -T | awk的时间过滤写法,或者升级工具包。
2.2 级别与设施过滤:-l、-f、-x,还有最容易踩的 -n
-l(等价--level)按级别过滤,可以逗号分隔多个值,比如dmesg -l err,warn。这里有个极其常见的误解:很多人以为-l err会显示“err 及以上”,实际上它是精确匹配列出的级别,crit 和 alert 不会自动包含进来。想覆盖 0 到 3 全部紧急级别,得写全:
dmesg -l emerg,alert,crit,err如果不确定某台机器上到底有哪些级别在刷,先加-x看前缀,再决定过滤条件。
dmesg -x | head -n 20 # 输出形如: # kern :info : [ 0.000000] Linux version ... # kern :warn : [ 12.345678] ...-f(等价--facility)按来源过滤,排查内核自身问题时用-f kern,排查用户空间往内核日志里写消息的情况用-f user。这两个参数可以叠加,dmesg -f kern -l err,crit就是“内核来源且严重”的组合拳。
真正需要单独拎出来说的是-n。这个参数不控制输出过滤,它设置的是console_loglevel,也就是“哪些级别的消息允许打到物理控制台上”。dmesg -n 1的效果是让控制台只显示 emerg,屏幕立刻清净;dmesg -n 8则把所有级别的消息都放出来。它跟你看不看得到历史日志毫无关系。我见过有人在排障时执行dmesg -n 1想“只看最重要的日志”,结果发现 dmesg 输出一点没变,还以为命令坏了。想静音控制台刷屏用-n,想过滤输出内容用-l,这两件事千万别搞混。
与之配套的还有-D(关闭向控制台打印)和-E(重新打开),在串口控制台被日志刷得没法输入命令时特别管用:
# 控制台被刷屏,先静音,腾出手来操作 dmesg -D # 处理完再打开 dmesg -E2.3 实时跟踪与缓冲区操作:-w、-W、-c、-C、-s
-w(follow)是排障时最实用的参数之一,它先输出历史内容,然后持续等待新消息,行为和tail -f类似。插拔一个 USB 设备、拔掉一块硬盘、触发一次网卡重新协商,另开一个窗口跑着dmesg -w,事件发生的瞬间你就能看到完整过程,不用反复执行命令去比对。
-W(follow-new)是更克制的版本,只跟踪命令执行之后产生的新消息,不打印历史。在日志已经刷了几千行的机器上,-W能帮你把噪声挡在外面。
-c和-C这一对经常被误用。-c是“读取并清空”,执行完输出缓冲区内容,同时内核缓冲区被清掉;-C是“只清空不输出”。它们的原始设计意图是给一次性读取的场景用的,比如嵌入式设备上空间紧张,读完就清。但在生产环境我强烈建议别碰这两个参数,因为内核缓冲区是共享的,你清空的同时把别人正在排查的线索也一起抹掉了。真要做基线对比,用dmesg > /tmp/base.log保存快照,而不是清空。
-s指定读取缓冲区的大小。在老版本工具上,dmesg 通过系统调用读取,一次能读的长度受参数限制,缓冲区大的时候需要手动指定一个更大的值才能读全。新版工具改走/dev/kmsg,这个限制基本消失了。如果你在某台老机器上发现 dmesg 输出明显比预期短,末尾还缺内容,可以试着加-s给一个足够大的值:
# 一次性读取最多 8MB 的缓冲区内容 dmesg -s 8388608要注意-s只是“读多少”,不改变内核缓冲区本身的大小,别指望用它扩容。
3. 从零复现一个硬件排障现场
3.1 前置准备与基线快照
任何排障都应该先留基线,否则你没法区分“本来就有”和“刚刚出现”。我的标准动作是三步:记录系统信息、保存当前 dmesg 全量、记录关键设备状态。
# 建立工作目录 mkdir -p /tmp/diag && cd /tmp/diag # 系统与内核版本 uname -a > sysinfo.txt cat /etc/os-release >> sysinfo.txt 2>/dev/null # 全量内核日志快照 dmesg --time-format=iso > dmesg-base.log 2>/dev/null || dmesg -T > dmesg-base.log # 设备与块设备状态 lsblk -o NAME,SIZE,TYPE,MOUNTPOINT > lsblk.txt lspci -nn > lspci.txt 2>/dev/null留快照这一步看着繁琐,但它决定了后面能不能做“差异对比”。我习惯在复现问题之前存一份,复现之后再存一份,用diff一比,新出现的行一目了然,比在几千行里肉眼找高效太多。
# 问题复现后 dmesg --time-format=iso > dmesg-after.log # 只看新增的行(配合 -u 忽略时间戳差异效果更好,但需要处理格式) diff dmesg-base.log dmesg-after.log | grep '^>'实际用的时候要注意,两次快照之间的时间戳肯定不同,直接 diff 会把每一行都标成变化。更实用的做法是用子系统关键词做二次过滤,比如grep -Ei 'usb|sd[a-z]|nvme|error|fail|reset',把关注点收敛到可疑范围内。
3.2 抓取 USB 设备插拔全过程
插拔 USB 是最适合入门的复现场景,因为它是完全可控的输入,日志输出完整且结构清晰。开一个终端跑:
dmesg -w --time-format=iso | grep -i usb然后插上设备,你会看到类似这样一组消息:
[2024-05-01T10:12:03,123456+08:00] usb 1-1: new high-speed USB device number 5 using xhci_hcd [2024-05-01T10:12:03,234567+08:00] usb 1-1: New USB device found, idVendor=1234, idProduct=5678, bcdDevice= 1.00 [2024-05-01T10:12:03,234789+08:00] usb 1-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 [2024-05-01T10:12:03,345678+08:00] usb-storage 1-1:1.0: USB Mass Storage device detected [2024-05-01T10:12:03,456789+08:00] scsi host6: usb-storage 1-1:1.0 [2024-05-01T10:12:04,567890+08:00] scsi 6:0:0:0: Direct-Access Vendor Product 1.00 PQ: 0 ANSI: 6 [2024-05-01T10:12:04,678901+08:00] sd 6:0:0:0: [sdb] 31266816 512-byte logical blocks: (16.0 GB/14.9 GiB) [2024-05-01T10:12:04,789012+08:00] sd 6:0:0:0: [sdb] Attached SCSI removable disk这组日志的价值在于它把整条链路串起来了:USB 控制器枚举 → 设备描述符读取 → 存储类驱动接管 → SCSI 层分配主机号 → 块设备层注册sdb。任何一环断掉,你都能从“最后出现的那条消息”反推问题卡在哪。比如只打印到new high-speed USB device就没有后续,说明枚举阶段失败,问题在供电、线材或者控制器兼容性;如果跳到usb-storage之后没了,说明设备被识别成存储但 SCSI 层没响应,多半是设备固件问题。
拔掉设备再看一遍,会看到usb 1-1: USB disconnect, device number 5以及块设备卸载消息。如果反复看到device not accepting address或者reset high-speed USB device,那基本可以判定是供电不足或线材接触不良,换口、换线、加供电的 HUB 逐个排除。
3.3 磁盘 I/O 错误与 OOM 的定位路径
磁盘报错是最典型的“先看 dmesg 才知道”的场景,因为应用层往往只报一个Input/output error,没有任何细节。这时候直接按关键词捞:
dmesg -T | grep -Ei 'i/o error|blk_update_request|ata.*error|nvme.*error|reset'典型输出长这样:
[May 1 10:20:11] blk_update_request: I/O error, dev sda, sector 2048 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0 [May 1 10:20:11] Buffer I/O error on dev sda1, logical block 0, async page readsector 2048这个数字很关键,它对应设备上的具体扇区位置。如果同一块盘反复在相近扇区报错,基本可以判定是介质坏块;如果扇区位置满盘乱跳,反而更要怀疑控制器、线缆、供电或者背板。把这个规律记住,能省下很多返工。
文件系统层的问题也会在这里留痕,比如EXT4-fs error、XFS (sda1): Corruption detected,这类消息出现时不要急着跑修复工具,先把日志完整备份,因为修复操作会覆盖现场。
内存溢出同理,内核 OOM killer 动手之前一定会先打印一条记录:
dmesg -T | grep -iE 'out of memory|killed process|oom'[May 1 10:25:33] Out of memory: Killed process 12345 (java) total-vm:8392740kB, anon-rss:4123456kB, file-rss:1024kB, shmem-rss:0kB这条日志能告诉你三件事:被杀进程的 PID 和名字、它占用多少虚拟内存、实际驻留多少。anon-rss是匿名页,通常是堆内存,这个值接近物理内存总量的话,就是典型的应用内存泄漏或者没设上限。搞清楚是谁吃掉了内存,比事后去调vm.overcommit_memory有意义得多。
3.4 让日志可回溯:采集脚本与持久化方案
环形缓冲区的短时记忆特性决定了你必须在问题发生前就做好准备。最省事的做法是利用日志守护进程的转发机制,内核消息会被自动分发到/dev/log或者通过 netlink 进入 journald,最终落到/var/log/kern.log、/var/log/messages或者 journal 的二进制文件里。机器上如果已经有这些文件,dmesg的丢失风险就小很多,直接查历史即可。
但总有环境不具备条件,比如嵌入式板子没有完整的日志服务,或者容器里读不到宿主日志。这时我会放一个轻量采集脚本,定期把缓冲区增量落盘:
#!/usr/bin/env bash set -euo pipefail OUT_DIR="/var/log/kmsg-snap" mkdir -p "$OUT_DIR" STAMP="$(date +%Y%m%d-%H%M%S)" TARGET="$OUT_DIR/dmesg-$STAMP.log" { echo "=== captured at $(date -Is) ===" uname -a echo "=== kernel buffer ===" dmesg --time-format=iso 2>/dev/null || dmesg -T } > "$TARGET" # 只保留最近 7 天的快照,防止把磁盘写满 find "$OUT_DIR" -name 'dmesg-*.log' -mtime +7 -delete用 cron 每十分钟跑一次,代价是每次全量落盘,磁盘增长大约是缓冲区大小乘以运行次数。如果缓冲区按 1MB 估算,十分钟一次、保留七天,最坏情况占用 1MB × 6 × 24 × 7 ≈ 1GB,对大多数机器可以接受,紧张的话把保留周期压到两天。
想要更精细就做增量。dmesg 提供了-w的流式输出,直接接管道给日志轮转工具即可:
dmesg -w --time-format=iso | logger -t kernel-watchlogger会把内容写进系统日志,后续由日志服务的轮转机制统一管理,不用自己写清理逻辑。这个方法有个前提:日志服务本身得健康,如果磁盘满到日志写不进去,那就又回到 dmesg 单打独斗的局面了。
4. 常见问题与坑位实录
4.1 权限、缓冲区与容器边界
权限问题前面已经说过,这里补几个容易忽略的变体。一是sudo dmesg在某些加固过的系统上仍然失败,因为提权后虽然有了 CAP_SYSLOG,但/dev/kmsg的设备权限被改过,或者它被 SELinux/AppArmor 策略挡住,这时候要去看审计日志而不是继续加权限。二是脚本里用sudo要小心,cron 环境下没有 TTY,sudo可能因为 requiretty 配置直接失败,得改用sudo -n或者直接在 root 的 crontab 里跑。
缓冲区相关的坑主要有两个。一是“日志看起来太短”,有人以为是 dmesg 坏了,实际上就是缓冲区被写满了,旧记录已经被覆盖,这属于正常行为。二是在虚拟化环境里,宿主机的内核日志和虚拟机的是两套,虚拟机里看不到宿主的硬件报错,物理层的故障必须在宿主机上查。
容器边界那个坑值得单独强调:Kubernetes 的 Pod 里执行 dmesg,看到的可能是宿主内核消息(如果/dev/kmsg被挂载且权限放开),也可能是空,取决于运行时配置。别在容器里对着 dmesg 输出下结论说“机器没问题”,那可能只是你什么都看不到。
4.2 时间错乱、日志丢失与格式陷阱
时间错乱前面提过-T的时钟校正问题,还有个更隐蔽的来源:系统从休眠恢复后,启动计时和墙上时钟的关系会发生变化,某些内核版本上-T换算出来的时间会有偏差。如果日志时间跟业务日志对不上,先怀疑这一点,用journalctl -k交叉验证。
日志丢失除了缓冲区覆盖,还有一种情况是启动早期的消息根本没进缓冲区。内核在极早期(比如解压阶段、内存初始化前半段)用的是另一套早期控制台输出,那些内容未必会同步到 log buffer 里。所以排查“开机就挂”的问题,光看 dmesg 不够,还得看串口输出、pstore或者ramoops保存的崩溃现场。
格式陷阱主要出现在管道处理上。dmesg -H带颜色转义,grep出来的内容粘着 ANSI 码,写进文件后很难看;-T输出的日期格式受 locale 影响,不同机器上可能不一样,做脚本解析时最好统一用--time-format=iso或者干脆只取消息体部分。还有个细节:-x输出的前缀里冒号数量和对齐方式在不同版本上有差异,如果你写脚本去解析 level,用--level过滤比正则会话可靠得多。
4.3 常见问题速查表
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
read kernel buffer failed: Operation not permitted | kernel.dmesg_restrict=1且非 root | 用sudo dmesg,或改走journalctl -k |
| 输出内容明显不全,只有最近一小段 | 环形缓冲区被写满覆盖 | 调大log_buf_len启动参数,或做外部转存 |
-T显示的时间对不上业务日志 | 系统启动后时钟被校正过 | 改用--time-format=iso或journalctl -k交叉验证 |
-l err没显示 crit 级别消息 | 该参数是精确匹配,不是“及以上” | 写全-l emerg,alert,crit,err |
执行dmesg -n 1后输出没变化 | -n控制的是控制台级别,不是输出过滤 | 过滤用-l,静音控制台才用-n |
| 容器内 dmesg 为空或报错 | 未挂载/dev/kmsg或权限受限 | 回到宿主机排查,容器内不下结论 |
| 日志被误清空 | 误用了-c或-C | 改用重定向保存快照,禁止在生产机上清空 |
老机器上--since报参数错误 | util-linux 版本过旧 | 用-T配合 awk 做时间过滤,或升级工具 |
除了表里这些,再补一个我自己的经验:排障时永远先dmesg -T | tail -n 200扫一眼,再决定要不要上参数过滤。上来就grep error很容易漏掉关键上下文,因为很多故障的第一条线索并不带 error 字样,比如一条普通的link is not ready或者device descriptor read/64, error -71,看着不起眼,实际上是整条故障链的起点。
5. 我自己长期用下来的几个习惯
第一个习惯是永远不在生产机上执行带清空功能的参数,-c和-C在我手里基本等于禁用。需要对比就存快照,需要长期留存就上采集脚本,缓冲区那点空间不值得用“抹掉证据”的方式去省。
第二个习惯是把dmesg -w当成常驻窗口。调硬件相关的问题时,我会在 tmux 里固定开一个 pane 跑着它,用grep -Ei 'error|fail|reset|timeout'过滤,剩下的噪声交给时间。这样问题一复现,屏幕上立刻滚动出对应记录,不用事后再去翻,也不会有“刚才那条是什么来着”的遗憾。
第三个习惯是遇到任何“硬件疑似故障”的结论,都要找到至少两个独立证据。单看一条 I/O error 就换盘太草率,得结合smartctl的属性、同一扇区是否重复报错、换线换口之后是否复现。dmesg 是线索的起点,不是判决书。
最后一个习惯可能有点反直觉:越是着急的时候,越要先把日志落盘再说。我吃过亏,一次现场排查来回折腾了半小时,最后想起来要保存日志,结果缓冲区早就被后续的驱动日志冲干净了,只剩一句“之前好像报过 reset”。从那以后,dmesg > /tmp/x.log这一行成了我登录问题机器后的肌肉记忆,比任何技巧都管用。