线上问题排查有个很经典的开场:同事在群里问"你这台机器的内核版本是多少",然后你会发现同一个环境里三个人报上来三个不一样的数字——一个说 3.10.0-1160.el7.x86_64,一个说 5.15.0-91-generic,还有一个在容器里敲了半天,反馈说"没有 uname 这个命令"。查看 Linux 内核这件事看着简单,实际上它牵扯到命令的适用边界、虚拟文件系统的组织方式、内核日志的读取权限,甚至是容器与宿主之间的内核共享关系。把这三个入口摸清楚,后面不管是判断某个特性支不支持、排查驱动加载失败,还是给编译内核做准备,都能少走弯路。下面就把我在实际运维和嵌入式调试里攒下来的三条查看路径拆开讲,每条都给可以直接抄的命令、输出解读方法,以及那些文档里不太会写、但一踩就疼的坑。
1. 动手之前先分清:你嘴里的"内核"到底是哪一层
很多人问"怎么看内核",其实心里想看的不是同一样东西,这直接决定了该敲哪条命令。最常见的四种诉求差别很大:想知道内核版本号,用来判断某个特性或驱动能不能用;想看内核日志,用来定位硬件识别、驱动报错、OOM 之类的问题;想看内核参数与配置,比如某个编选项有没有打开;还有一种是内核源码和模块,做裁剪、改驱动、写模块时才需要。
我见过最典型的一次误判:有人为了确认机器上有没有开启某个网络特性,敲了uname -a发现版本是 5.15,就断定"这个特性肯定支持"。结果实际跑起来功能不可用,最后查/boot/config-$(uname -r)才发现那个编选项CONFIG_XXX压根没开。版本号高不等于功能全,发行版在编译内核时会做大量裁剪,尤其是国产发行版和企业版,定制程度更高。
| 你想了解的内容 | 该看的位置 | 典型命令 |
|---|---|---|
| 内核版本与编译信息 | uname、/proc/version | uname -r |
| 内核启动日志 | 环形缓冲区、systemd journal | dmesg、journalctl -k |
| 内核编译配置 | /boot/config-* 或 /proc/config.gz | grep CONFIG_ /boot/config-$(uname -r) |
| 已加载模块 | /proc/modules、/sys/module | lsmod |
| 内核启动参数 | /proc/cmdline | cat /proc/cmdline |
这张表建议先存下来。我在带新人的时候发现,大部分"看错了"的案例不是命令敲错,而是一开始目标就定错了。你拿着一把量长度的尺子去量重量,再熟练也没用。下面三节分别对应上表里的前两行,也是实际工作中出现频率最高的两条路径。
2. 方法一:uname 家族——最快拿到版本号的那把钥匙
uname是coreutils里的东西,几乎任何发行版、任何精简镜像里都有,连很多嵌入式 rootfs 都不缺。它的好处是快、无依赖、不需要任何权限,缺点也明显:信息量有限,而且在容器里给出的答案未必是你以为的那台机器。
2.1 为什么老手只敲uname -r而不是uname -a
uname -a输出一行,看着信息齐全,但实际用起来会发现它把主机名、架构、构建时间全塞在一起,复制给别人时还要对方自己挑。真正需要版本号时,uname -r只吐一个字段,干净利落,特别适合塞进脚本里:
uname -r # 5.15.0-91-generic这条命令我在写自动化脚本时用过无数次,比如按内核版本号拼模块目录路径:
ls /lib/modules/$(uname -r)/kernel/drivers/net/要是这里用uname -a就得再切一刀,脚本可读性立刻下降。uname的几个参数分工其实很清晰:
-s:内核名称,Linux 上永远是Linux,看着没用,但在跨平台脚本里做判断很关键;-r:内核发行版本(release),日常最常用;-v:内核构建版本,包含编译时间和编译者,排查"这机器什么时候编译的内核"时有用;-m:机器硬件架构,比如x86_64、aarch64、armv7l,交叉编译场景必看;-n:主机名,等价于hostname;-o:操作系统名,GNU 扩展,部分非 GNU 环境不支持,写脚本要留意。
注意:
uname -o和uname -p在不同 libc/发行版上表现不一致,一个返回GNU/Linux,另一个可能直接报unknown。做兼容性脚本时别把它们当可靠字段用。
2.2 把3.10.0-1160.el7.x86_64逐段拆开读
光拿到字符串没用,得会读。以 CentOS 7 上常见的3.10.0-1160.el7.x86_64为例:
3是主版本号(major);10是次版本号(minor),在 2.6 之前的老传统里,偶数代表稳定分支、奇数代表开发分支,这个约定现在意义已经弱化,看它不如看发行版的实际维护情况;0是修订号(patchlevel);1160这一段属于发行版打包号,红帽系用来标记自己打了多少轮补丁,跟上游内核的补丁级别不是一回事;el7是发行版标识,表示 Enterprise Linux 7 系列;x86_64是目标架构。
再看 Ubuntu 上的5.15.0-91-generic:5.15.0是上游基线,91是 Ubuntu 自己的 ABI 序号,每推一次影响内核模块 ABI 的更新就会加一,generic是 flavour(口味),表明这是通用版本,同系列还有lowlatency、aws等不同口味。这个-91-generic后缀还有个实际影响:升级内核时模块目录名会跟着变,所以你自己编译的 DKMS 模块必须跟着重编,否则modprobe会直接报找不到模块。
2.3uname在容器、chroot 和 WSL 里会说谎
这是我踩过最多次的坑。容器和宿主共享同一个内核,所以你在容器里敲uname -r,拿到的是宿主的内核版本,不是"容器镜像的版本"。有一次排查一个镜像里的服务崩溃,同事坚持说"镜像里内核是 4.19",实际上那只是构建镜像时的宿主内核,运行时的机器内核是 5.10,两个环境的模块行为不完全一样。
要区分镜像本身的信息,应该看/etc/os-release:
cat /etc/os-release # PRETTY_NAME="Ubuntu 22.04.3 LTS"至于 WSL,uname -r会长成类似5.15.90.1-microsoft-standard-WSL2的样子,带明显的厂商后缀。看到这种带自定义后缀的版本号,就不要按上游内核的行为去推断特性了,一定要以实际测试为准。chroot 环境同理,uname走的是系统调用,反映的仍是当前运行的内核。
3. 方法二:从 /proc 虚拟文件系统里把内核信息读出来
/proc是内核向用户态暴露自己的一块窗口,里面的文件不是真实磁盘文件,而是内核在你读的时候现场生成的。所以它给出的信息比uname更细,也更能反映"内核自己怎么看自己"。
3.1/proc/version和uname到底差在哪
两者拿到的版本号一致,但/proc/version多带了编译器和构建环境的描述:
cat /proc/version # Linux version 5.15.0-91-generic (buildd@lcy02-amd64-045) # (gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0, GNU ld 2.38) #101-Ubuntu SMP ...那段 GCC 版本和链接器版本,在排查"内核模块编译报错"时特别值钱。因为内核模块的编译必须和构建该内核的编译器版本尽量对齐,GCC 大版本不一致时,某些头文件的结构体布局可能有细微差异,加载时就会出现invalid module format这类含糊的报错。我遇到过一回,模块编译全程无警告,加载却失败,最后对比/proc/version里的编译器版本,发现现场用的是完全不同的大版本,换工具链重编就解决了。
/proc/version还有一个用处是纯 shell 环境下没有uname的时候(极少数裁剪系统),读文件总归是能读的。
3.2 顺手把/proc/cmdline和/proc/sys一起看了
既然已经进到/proc,另外两个文件强烈建议一起看:
cat /proc/cmdline # BOOT_IMAGE=/vmlinuz-5.15.0-91-generic root=/dev/mapper/vg-root ro quiet splash/proc/cmdline是内核启动参数的原始记录。它比grub.cfg更可靠,因为那里可能有多份菜单项,你未必知道当时选的是哪一条。排查resume、crashkernel、iommu、isolcpus这类参数有没有真正生效,看这里最快。嵌入式场景更常用,因为设备树和 bootargs 的信息最终都会在这里体现。
ls /proc/sys/kernel/ sysctl kernel.dmesg_restrict/proc/sys下面挂的是可调内核参数。前面提到的kernel.dmesg_restrict就在这儿——它决定普通用户能不能读内核日志,后面讲dmesg的时候会再展开。
3.3 走到/boot下面看实物:vmlinuz、initramfs、config、System.map
/proc给的是运行时视图,/boot给的是磁盘上的实物文件,两者配合看能解决很多疑难:
| 文件 | 作用 | 什么时候用 |
|---|---|---|
vmlinuz-$(uname -r) | 压缩的内核镜像本体 | 确认当前运行的内核文件确实存在于磁盘 |
initrd.img-$(uname -r) | 启动初期用的临时根文件系统 | 排查存储驱动、LVM 解密类启动失败 |
config-$(uname -r) | 该内核的编译配置 | 确认某个 CONFIG 选项有没有开 |
System.map-$(uname -r) | 内核符号地址表 | 分析内核崩溃栈、写调试脚本 |
确认某个特性有没有编进内核,一条命令就够:
grep -E "CONFIG_IKCONFIG|CONFIG_BPF_SYSCALL" /boot/config-$(uname -r)如果/boot下没有config-*(有些发行版不装),还可以试试/proc/config.gz,前提是内核开启了CONFIG_IKCONFIG_PROC:
zcat /proc/config.gz | grep CONFIG_BPF_SYSCALL提示:
/boot一般需要 root 才能列目录,但很多发行版允许普通用户读里面的普通文件。如果你连ls都被拒,先确认是不是权限问题,别急着怀疑内核本身。
4. 方法三:读内核自己吐出来的日志(dmesg 与 journalctl -k)
前两种方法看的是"静态身份",真正做故障定位时,用得最多的其实是内核日志。内核从加电那一刻起就在往一个环形缓冲区写东西:内存条的容量、CPU 的型号、每个被识别到的硬盘、网卡固件的加载结果、驱动初始化失败的堆栈,全在这里。
4.1 先看版本行,再看错误行
dmesg输出的第一行往往就是内核版本,换句话说,dmesg | head -1本身也是一种查看内核版本的途径,而且带上了启动时刻的相对时间戳:
dmesg | head -3 # [ 0.000000] Linux version 5.15.0-91-generic ... # [ 0.000000] Command line: BOOT_IMAGE=/vmlinuz-5.15.0-91-generic ... # [ 0.000000] KERNEL supported cpus: ...但dmesg真正的价值在后面。常用的输出处理方式有这么几种:
dmesg -T # 时间戳转成人类可读的日期时间 dmesg -H # 分页显示,带颜色和相对时间 dmesg -w # 实时跟踪,插拔设备时很有用 dmesg -l err,warn # 只看错误和警告级别 dmesg | grep -i "error\|fail" # 老系统上更通用的过滤方式-T这个参数我需要额外提醒:它把"开机以来的毫秒数"换算成墙上时间,如果系统时钟在中途被 NTP 校准过,换算出来的时间可能是错的,甚至出现"未来时间"。做严格的时间线分析时,老老实实回到[ 12.345678]这种相对时间戳,自己按开机时间往上加更靠谱。
4.2dmesg被限制、日志被冲掉,是两件不同的事
新人最常卡的两个问题,本质完全不同。
第一个是权限。某些发行版默认把kernel.dmesg_restrict设为 1,普通用户执行dmesg会直接返回"Operation not permitted"。先确认:
sysctl kernel.dmesg_restrict # kernel.dmesg_restrict = 1 sudo dmesg | head -1需要长期放开的话,写进/etc/sysctl.d/下的配置文件再sysctl --system生效。不过说实话,生产机器上收紧这个开关是有道理的,因为内核日志里可能包含硬件地址、挂载路径等信息,能不开就不开,用sudo更稳妥。
第二个是环形缓冲区被覆盖。日志缓冲区大小是有限的,默认通常几十到几百 KB,机器跑久了、日志量大,早期启动信息就被新日志挤掉了。有一回我排查网卡固件加载失败,事后上去看,缓冲区里全是近几小时的重复报错,启动那几十行早没了。这种情况下要用 systemd 的日志:
journalctl -k # 等价于内核日志 journalctl -k -b # 只看本次启动 journalctl -k -b -1 # 看上一次启动的内核日志 journalctl -k --since "2 hours ago"journalctl -k -b -1这条命令是我认为排查"重启之后才能定位的问题"最重要的工具。机器刚重启过,缓冲区是空的,但上一次启动的日志如果被持久化了,仍然完整保留。
4.3 日志持久化的开关,值得花两分钟确认
journalctl默认可能只把日志写在/run/log/journal里,也就是内存中,一重启就没了。要让日志落盘,需要确保/var/log/journal目录存在且权限正确:
ls -d /var/log/journal # 目录不存在就创建,然后重启 systemd-journald sudo mkdir -p /var/log/journal sudo systemctl restart systemd-journald journalctl --disk-usage--disk-usage能告诉你日志实际占了多少空间。如果磁盘紧张,别直接删目录,用journalctl --vacuum-size=500M或--vacuum-time=7d做有序回收,删得更干净也更安全。
5. 三种方法之外的补充入口:从包管理器和模块目录反查
前面三种方法覆盖了九成场景,但有些问题它们答不上来。比如:"这台机器装了几个内核?""现在跑的是哪一个,另一个是什么版本?"这时候就要从包管理和模块目录去反查。
hostnamectl是最省事的一条捷径,它把主机名、发行版、内核、架构、虚拟化类型一次性列出来:
hostnamectl # Static hostname: web-01 # Kernel: Linux 5.15.0-91-generic # Architecture: x86-64这条命令在 systemd 系发行版上很稳,缺点是老旧的发行版或者精简容器里可能没有 systemd,那就回到前面三种方法。
查装了几个内核,按发行版家族分:
# Debian / Ubuntu dpkg -l | grep linux-image # RHEL / CentOS / 国产衍生版 rpm -qa | grep '^kernel' # 通用方式:直接看模块目录 ls -1 /lib/modules/ls /lib/modules/这招我很偏爱,因为它不依赖任何包管理器,直接反映"当前系统里有哪些内核版本对应的模块树"。目录名和内核版本是一一对应的,/lib/modules/$(uname -r)必须存在,否则modprobe会直接失败。有些自动化脚本会在内核更新后忘了跑depmod,表现就是新内核目录在,但模块依赖关系是旧的,排查时ls -l /lib/modules/$(uname -r)/modules.dep的时间戳能给出线索。
再往下一步是看模块本身:
lsmod # 当前加载了哪些模块、被谁依赖 modinfo e1000e # 某模块的版本、描述、依赖、参数 modinfo -F version e1000emodinfo输出的vermagic字段里带着内核版本和一大堆编译标记,做模块兼容性判断时看它比看uname更贴近事实。
6. 踩坑实录:五个真实场景的排查链路
理论说完了,讲几个我实际遇到过的、绕了弯路的案例,重点是排查顺序,而不是结论本身。
场景一:容器里看不到内核日志。在容器里执行dmesg报read kernel buffer failed。第一步去看sysctl kernel.dmesg_restrict,结果是 0,说明不是权限限制。第二步意识到容器默认只有有限的 capability,读取内核日志需要CAP_SYSLOG,而容器运行时默认不给。两个选择:临时用docker run --cap-add SYSLOG验证,或者干脆在宿主机上查。结论是这类操作本来就应该在宿主做,容器内看到的也是宿主日志,混淆边界只会让问题更难收口。
场景二:升级内核后模块加载失败。表现是某个 DKMS 模块在内核升级后modprobe报Module not found。排查顺序:先uname -r确认当前版本;再ls /lib/modules/看对新版内核的模块目录在不在;发现目录在,但里面没有那个模块;接着查 DKMS 状态dkms status,显示为新内核编译失败;最后看编译日志,定位到是头文件包没装。修复方式不是手工拷贝.ko,而是补上头文件包再让 DKMS 重新构建,否则每次内核更新都会重演一次。
场景三:日志里查不到启动信息。表现是想看固件加载失败的原因,但dmesg里翻不到启动那几行。这时候不要怀疑内核没打印,先看缓冲区是否被冲掉。做法是转到journalctl -k -b,如果还缺,看/var/log/journal是否存在。顺带说一下,缓冲区大小可以在启动参数里用log_buf_len=调整,但这是需要重启才生效的参数,临时排查用journalctl更实际。
场景四:脚本在 ARM 和 x86 之间结果不一致。一个采集脚本用uname -p判断架构,x86 机器上返回x86_64,ARM 机器上返回aarch64,但有台设备返回了unknown。原因是-p依赖 libc 实现,可靠性不如-m。改法很简单:统一用uname -m,并加一个兜底分支。
场景五:判断特性支持时版本号骗了人。前面提过一次,这里补完整链路。目标是确认某个流量控制特性是否可用。第一步uname -r看到版本够新,初步判断支持;第二步实际测试失败;第三步去看编译配置grep CONFIG_NET_SCH_ /boot/config-$(uname -r),发现相关模块被编译成m但没加载;第四步modprobe加载后测试通过。整个链路的核心教训是:版本号只能筛掉一部分可能,真正的结论要靠配置、模块和实测三者交叉验证。
提示:排查内核相关问题时,养成"先记录
uname -r和/proc/cmdline,再动手"的习惯。等改完参数重启之后,很多原始信息就找不回来了。
7. 把"查看"升级成"读懂":版本号背后的选型思路
拿到了版本号,只是拿到一个字符串,能读懂它才叫真的会看。我总结了几条在选型和维护时用得上的判断经验。
第一,主线版本号高不代表长期支持久。稳定分支的维护窗口长短不一,选型时更该关心发行版的维护策略和补丁回填情况,而不是单纯比数字大小。存量系统里还大量跑着 3.10、4.19 这类老基线,它们仍然在被持续打补丁,版本号看着旧,但对特定硬件和驱动的适配反而更成熟。嵌入式场景尤其如此,芯片厂商的 BSP 往往就锁在某个特定基线版本上,硬往上追新内核,光驱动适配就够喝一壶。
第二,比较版本号别用字符串直接比。sort按字典序会把5.9排在5.10后面,一定要用版本排序:
printf '%s\n' 5.9 5.10 5.15 4.19.100 | sort -V脚本里做版本判断时,sort -V或者按cut -d. -f1,2取主次版本号转成整数比较,都比直接比字符串靠谱。
第三,不同发行版的后缀含义完全不同,不要跨发行版猜。el7、el8、generic、amd64这些后缀只在各自的包管理体系里有意义。跨发行版交流时,最好把完整字符串连同/etc/os-release的内容一起贴出来,省掉一轮来回确认。
第四,同一台机器上有多个内核是正常的,要分清"默认启动"和"当前运行"。/boot里有一堆vmlinuz-*,当前运行的只有一个,用uname -r确认;而下次默认启动哪个,要看引导加载器的配置和启动顺序。做内核升级时我一般会保留一个可用版本作为回退点,同时在升级后手动确认一次/lib/modules/$(uname -r)是否完整,别等到真需要回退的时候才发现模块目录缺失。
第五,把所有"查看"动作沉淀成一条采集命令。我在自己的巡检脚本里放了这么一段,一次执行就能把版本、架构、启动参数、已加载的内核模块数量、内核日志的错误行数一起收上来:
{ echo "kernel: $(uname -r)" echo "arch: $(uname -m)" echo "cmdline: $(cat /proc/cmdline)" echo "modules: $(lsmod | wc -l)" echo "err_lines: $(dmesg -l err 2>/dev/null | wc -l)" } > /tmp/kernel-info.txt这段东西不复杂,但省事。以前每次排查都要一条条敲,现在直接跑一遍,把结果贴进工单,问题定位时间至少砍掉一半。
最后再分享一个小习惯:只要是内核相关的问题,第一件事永远是先把uname -r、/proc/version、/proc/cmdline三份原始信息完整存档,再开始改配置、重载模块或者重启。内核世界里的"现场"很容易被覆盖,日志缓冲会滚、模块会被卸载、启动参数会变,而这几条命令加起来不到三秒,却能让你在几小时之后依然拿得出证据。