资讯动态

Linux内核模块与KVM虚拟化:原理、加载到排障实践

发布时间:2026/10/19 8:30:33 来源:尧图企业网站定制
这份笔记的起点其实是一次挺磨人的排障。之前帮某实验室搭虚拟化平台系统装好后执行modprobe kvm_intel一直报错虚拟机自然起不来。我一开始以为问题出在 QEMU 配置上来回改参数改了一天最后才发现是内核模块的加载环境、BIOS 开关和模块参数三者共同作祟。从那之后我就把 Linux 内核模块和 KVM 虚拟化放在同一份文档里做笔记越整理越觉得这两样东西必须一起理解。先弄清模块怎么编译、怎么加载、怎么和硬件打交道再去理解 KVM 如何借用这些机制去接管 CPU 和内存排障的时候才不至于绕着圈找问题。这份笔记适合三类人。第一类是在用 libvirt 或 KVM 创建虚拟机但遇到KVM: no hardware support、/dev/kvm Permission denied这类报错想知道问题出在哪的运维第二类是刚接触内核模块开发想找一个能落地的最小示例把编译、加载、卸载全流程跑通的开发者第三类是纯粹想搞懂“KVM 和 QEMU 到底谁负责什么”的虚拟化学习者。我会先从模块机制讲起再进到 KVM 的底层协作原理最后给完整的落地步骤和排障表。里面所有命令都是我实际敲过的踩过的坑也会明确标出来。1. 内核模块与KVM为何要放到一起学1.1 内核模块到底是个什么东西内核模块不是一个普通程序它是一份.ko文件在系统运行期间被内核直接加载进内核地址空间并执行。它和用户态程序最大的区别是权限等级模块里的代码运行在内核态可以调用任意内核函数、访问任意物理内存。你用一根 USB 线往主板上插外设内核模块就像主板上新插的扩展卡系统不用重启功能立刻生效。但代价也很直接。普通程序崩溃了内核会杀掉进程系统继续跑内核模块一旦指针写错、访问了非法地址后果经常是直接 panic整个机器重启。我见过有人写模块时用了一个未初始化的指针加载后宿主机瞬间失去响应连日志都没来得及留。这不是吓唬人使用模块前必须先把测试环境的快照做好。模块的生命周期很简单。加载时内核调用模块里的 init 函数卸载时调用 exit 函数。init 返回值 0 表示加载成功负数表示失败exit 函数在卸载阶段做清理工作比如释放内存、注销设备、关闭文件。模块之间的依赖关系由内核维护lsmod输出的第二列是使用计数第三列是依赖它的模块列表。你rmmod一个正在被别的模块引用的模块内核会直接拒绝。1.2 KVM虚拟化技术栈里的模块分工KVM 不是像 VirtualBox 那样的单一大程序它是一套内核机制加用户态工具的集合。内核侧通常涉及三个模块kvm.ko是核心框架负责虚拟机生命周期、vCPU 调度、内存管理等通用逻辑kvm_intel.ko和kvm_amd.ko分别封装 Intel VT-x 和 AMD SVM 的硬件特性把 CPU 虚拟化的能力暴露给上层。用户态那部分是 QEMU负责模拟磁盘、网卡、显示等设备以及处理各种策略问题。整个链路可以简写成CPUVT-x/SVM 硬件 ↓ kvm_intel / kvm_amd 模块 ↓ kvm.ko 核心框架 ↓ /dev/kvm 设备文件 ↓ QEMU 用户态进程没有 KVM 模块时QEMU 也不是完全不能用它会退回软件二进制翻译模式TCG每条客户机指令都要在宿主机上解释执行性能会差一个数量级。有了 KVM 模块客户机的 CPU 指令直接交给硬件虚拟化单元执行速度逼近原生。这也是为什么我们排查虚拟化问题第一反应不是去改 QEMU 参数而是先确认 KVM 模块是否加载成功。1.3 这份笔记能帮你解决什么我给自己定的目标是看完之后你能独立完成三件事。第一从零写一个最小内核模块编译加载再卸载并且知道每一步在做什么第二知道 KVM 的kvm.ko、kvm_intel、kvm_amd三个模块是怎么协作的为什么没有硬件虚拟化标志就一定起不来第三在真实机器上创建第一台 KVM 虚拟机遇到常见报错时能按图索骥。如果只是急着用虚拟化可以直接跳到第 4 节照着操作如果想搞明白原理从第 2 节开始顺着读逻辑是通的。2. 手写第一个内核模块从环境到加载验证2.1 环境准备内核头文件怎么对齐内核模块开发不像普通 C 程序那样装个 gcc 就行你必须有和当前运行内核严格匹配的头文件。实际项目中绝大多数加载失败问题根源都是头文件版本不匹配。先看内核版本uname -r在 Debian 系系统上用apt install linux-headers-$(uname -r)安装对应头文件。装完之后检查一下/lib/modules/$(uname -r)/build是否存在它通常是一个符号链接指向真正的内核源码目录或头文件目录。这一步很关键因为模块的 Makefile 最终就要用到这个路径。还有一类容易踩的坑测试机升级内核后头文件也跟着换版本了但你之前编译生成的.ko还留在工作目录里。这时候直接insmod大概率报Invalid module format因为模块的 vermagic 字符串对不上。解决方式是清理重编不要把旧文件留着。条件允许的话我建议模块实验放在独立虚拟机里做内核模块出问题重启恢复成本低不会影响工作环境。2.2 最小模块的代码结构拆解先放一个我实际用来验证环境的最简模块文件名叫hello.c#include linux/init.h #include linux/module.h #include linux/kernel.h static int __init hello_init(void) { printk(KERN_INFO hello: module loaded\n); return 0; } static void __exit hello_exit(void) { printk(KERN_INFO hello: module unloaded\n); } module_init(hello_init); module_exit(hello_exit); MODULE_LICENSE(GPL); MODULE_AUTHOR(A developer); MODULE_DESCRIPTION(Minimal demo module);很多刚接触内核模块的人会问main 函数去哪了用户态程序的入口是main内核模块没有这个概念。加载时内核调用的入口由module_init宏指定这里就是hello_init卸载时由module_exit宏指定。两个宏定义在linux/module.h里所以这个头文件必须包含。__init和__exit是内核里的特殊标记。带__init的函数在模块加载完成后占用的内存会被释放避免长期浪费带__exit的函数如果模块被编译进内核而不是作为可加载模块就会被忽略因为编译进内核的东西不需要卸载。printk和用户态的printf看着像但输出不进终端而是写到内核日志缓冲区用dmesg查看。它支持日志级别前缀比如KERN_INFO、KERN_ERR。我后面排查模块问题第一步永远是dmesg | tail比任何调试器都直接。还有一个细节值得提模块代码运行在内核栈上内核栈默认只有 8KB 左右别在函数里定义大数组否则栈溢出会让系统直接挂掉。我写模块时习惯动态分配内存而不是在栈上开大块缓冲区。2.3 Makefile、编译参数与常见报错同目录下的 Makefile 长这样obj-m : hello.o KDIR : /lib/modules/$(shell uname -r)/build PWD : $(shell pwd) all: $(MAKE) -C $(KDIR) M$(PWD) modules clean: $(MAKE) -C $(KDIR) M$(PWD) clean逐行看。obj-m : hello.o告诉内核构建系统我们要以可加载模块方式编译hello.o。KDIR指向运行内核的 build 目录这是编译头文件和配置的源头。PWD是当前模块源码目录。核心命令是$(MAKE) -C $(KDIR) M$(PWD) modules-C切换工作目录到内核树M$(PWD)告诉内核构建系统模块源码在哪modules表示生成模块目标。编译make成功后目录里会出现hello.ko。先用file hello.ko看一眼 ELF 格式是否正常再用modinfo hello.ko查看模块信息modinfo hello.ko你应该能看到vermagic字段它对应内核版本和 SMP 配置。如果构建时用的头文件与运行内核不一致这个字段会对不上加载时直接报错。常见报错是找不到内核构建树make: *** /lib/modules/6.8.0-.../build: No such file or directory这基本就是没装头文件。还有个习惯问题编译模块别用sudo make普通用户编译就行只有insmod/rmmod需要 root。2.4 insmod、rmmod 与日志查看编译完成后把模块加载进内核sudo insmod ./hello.ko检查是否加载成功lsmod | grep hello查看内核日志sudo dmesg | tail你应该能看到hello: module loaded。卸载sudo rmmod hello再看日志会看到hello: module unloaded。insmod和modprobe的区别值得说清楚。insmod只按你给的路径加载单个模块不管依赖modprobe会读取/lib/modules/$(uname -r)/modules.dep依赖关系自动加载依赖模块。自编译的.ko通常用insmod因为还没装进标准模块目录如果想把模块交给系统管理可以复制到/lib/modules/$(uname -r)/extra/然后运行depmod再modprobe hello。我第一次用modprobe加载自编译模块失败就是因为忘了depmod系统找不到模块路径。另外卸载前看下lsmod输出里的使用计数如果第二列不为 0rmmod会提示Module is in use先查是谁在用。3. KVM 与内核模块交互的底层原理3.1 CPU 虚拟化的 VMX 执行环这部分单独讲会太抽象我用一个场景来理解。Intel VT-x 引入了两种执行模式VMX root operation 和 VMX non-root operation。hypervisor这里就是 KVM 内核模块运行在 root 模式下客户机运行在 non-root 模式下。正常情况下客户机的指令直接在 CPU 上执行不经过任何翻译只有遇到敏感指令、中断或访问特殊寄存器时CPU 硬件会触发一次 VM exit自动切回 root 模式把控制权交给 KVM。KVM 处理完事件后再执行一次 VM entry把 CPU 切回 non-root 模式继续跑客户机。可以把这种机制类比成比赛里的裁判。球员正常比赛时裁判不干预只有出现犯规或界外球才吹哨暂停处理完再恢复比赛。这个“哨声”就是 VM exit/VM entry。硬件这么设计的价值在于绝大部分计算不需要额外付出虚拟化开销被压缩到了极小范围。早期纯软件虚拟化需要翻译每条指令性能因此差得离谱。AMD SVM 的设计思路和 VT-x 类似只是寄存器、控制数据结构名字不同。Intel 侧叫 VMCSAMD 侧叫 VMCB它们都保存了客户机状态和 hypervisor 状态保证模式切换时上下文不会丢。这些数据结构由kvm_intel或kvm_amd模块管理。所以看/proc/cpuinfo里的vmx或svm标志就是在确认当前 CPU 是否具备这个硬件能力。3.2 kvm.ko、kvm_intel、kvm_amd 是怎么协作的kvm.ko是 KVM 的核心框架但它不负责具体 CPU 厂商的硬件细节。真正和 VT-x/SVM 打交道的是kvm_intel.ko或kvm_amd.ko它们通过一组回调函数注册到核心框架里。简单说kvm.ko假设底层有kvm_x86_ops结构体里面是一堆函数指针比如创建 vCPU、执行 vCPU、切换模式Intel 或 AMD 模块在加载时把自己的实现填进去。设备层面kvm.ko加载后会在系统中创建/dev/kvm字符设备。QEMU 启动虚拟机时打开这个设备文件拿到文件描述符然后用ioctl往里发指令大致流程是打开 /dev/kvm ↓ KVM_CREATE_VM创建虚拟机对象 ↓ KVM_CREATE_VCPU创建虚拟 CPU ↓ KVM_RUN进入 vCPU 执行循环每个新的虚拟 CPU 都会对应内核里一个线程。内核线程再调度到物理 CPU 上运行这就是“虚拟机是内核管理的进程线程”这个说法的来源。你运行ps -ef | grep qemu看到的是 QEMU 进程但里面还有一堆kvm前缀的内核线程它们在真正驱动硬件加速。模块参数也很重要。比如nested表示是否支持嵌套虚拟化也就是虚拟机里再开虚拟机ept表示是否启用 Intel EPT 内存虚拟化。查看参数cat /sys/module/kvm_intel/parameters/nested cat /sys/module/kvm_intel/parameters/ept如果是Y说明对应的特性已经开启。我遇到过 BIOS 支持虚拟化但模块参数默认关闭的情况改模块参数后还要重新加载模块才生效。3.3 内存虚拟化与 I/O 路径上的模块角色内存虚拟化是整个 KVM 里最复杂也最值得理解的部分。客户机看到的物理地址并不是宿主机物理地址中间隔了一层。早期没有硬件辅助时KVM 要维护影子页表把客户机物理地址和宿主机物理地址的映射关系直接体现到 CPU 页表里每次客户机页表变化都可能触发同步开销很大。现代 Intel EPT 和 AMD NPT 解决了这个问题。CPU 硬件支持两阶段地址转换客户机 virtual address 先转成客户机 physical address再由 EPT/NPT 转成宿主机物理地址。整个过程对客户机是透明的KVM 只需要维护 EPT 页表缺页导致 VM exit 的次数大幅减少。这也是为什么现在 KVM 虚拟机内存性能可以接近原生。I/O 路径同样和内核模块深度相关。早期虚拟机的网卡、磁盘访问都会触发 VM exit然后回到用户态 QEMU 去模拟设备行为路径很长性能损耗明显。半虚拟化的思路是用 virtio 协议在共享内存里建立一个环形缓冲区客户机驱动把 I/O 请求直接丢进这个缓冲区QEMU 在另一头消费不用每次访问都 VM exit。网络方向还有vhost_net内核模块它把数据通路放进内核态减少用户态上下文切换。我实际测过虚拟机网卡用 virtio 和默认模拟网卡对比单纯小包转发的差距非常夸张。这个经验在后面性能调优部分会用到。4. 落地实践从检查硬件到创建第一台虚拟机4.1 检查虚拟化能力与加载模块这是创建 KVM 虚拟机前的必经环节。第一步确认 CPU 支持硬件虚拟化grep -E -c vmx|svm /proc/cpuinfo输出大于 0 表示支持。如果为 0大概率是 BIOS 里虚拟化开关没打开。不同厂商 BIOS 的菜单名字不一样一般在 Processor 或 Security 相关区域叫 Intel VT-d / VT-x 或 SVM Mode。改完 BIOS 要断电重启才生效。第二步加载对应厂商的 KVM 模块。Intel 加载kvm_intelAMD 加载kvm_amdsudo modprobe kvm_intel如果你在两家的 CPU 平台上乱加载比如 Intel 机器上加载kvm_amd会报错提示没有对应硬件。用lsmod | grep kvm确认加载结果kvm_intel 122880 0 kvm 692224 1 kvm_intel可以看到kvm.ko的使用计数是1被kvm_intel引用符合预期。再检查设备节点ls -l /dev/kvm如果/dev/kvm不存在说明 KVM 模块没正确加载或内核配置没开。接着确认嵌套虚拟化参数cat /sys/module/kvm_intel/parameters/nested4.2 创建磁盘镜像并安装虚拟机模块就绪后先从磁盘镜像开始。测试环境首选 qcow2 格式它按需增长创建 20G 的镜像文件初始很小只有数据写进去才膨胀而且支持快照非常适合做实验。对性能要求极高的生产存储raw 格式少了 qcow2 的元数据开销可以考虑。创建镜像qemu-img create -f qcow2 /var/lib/libvirt/images/demo.qcow2 20G如果装了 libvirt 工具集我习惯用virt-install创建虚拟机sudo virt-install \ --name demo \ --ram 2048 \ --vcpus 2 \ --disk path/var/lib/libvirt/images/demo.qcow2,size20,formatqcow2,busvirtio \ --network networkdefault,modelvirtio \ --os-variant ubuntu22.04 \ --cdrom /tmp/installer.iso \ --graphics vnc,listen0.0.0.0 \ --noautoconsole每个参数都有用意。--name demo是虚拟机名字--ram 2048分配 2G 内存--vcpus 2分配 2 个 vCPU--disk使用刚才创建的 qcow2 镜像并且明确busvirtio绕开慢速模拟磁盘--network networkdefault,modelvirtio走默认 NAT 网络并用 virtio 网卡--cdrom挂载系统安装镜像--noautoconsole表示安装时不自动接入虚拟控制台避免终端卡住。安装完成后virsh list --all virsh start demo我个人的建议是第一次操作不要跳过 virtio。很多人在虚拟机里看到网卡速率异常、磁盘 IO 差往往是因为默认用了模拟的 IDE 或 e1000 设备。后面重装驱动很麻烦不如建虚拟机时就直接指定 virtio。4.3 CPU、内存、磁盘与网络的性能参数这部分是我实际使用中最常调整的几组参数。CPU 方面KVM 允许 vCPU 超售但我一般不把超售比例压到 2 倍以上。宿主机在top里可以看到每个 vCPU 线程如果%steal数值很高说明 vCPU 在等待物理 CPU虚拟机里的负载会莫名其妙变慢。生产环境我建议用host-model或host-passthrough模型前者兼顾迁移兼容性后者性能最接近原生但迁移到不同 CPU 的机器时可能失败。内存方面大页是关键。默认 4K 内存页在虚拟机内存很大时TLB 命中率下降严重。启用 2MB HugeTLBecho 1024 /proc/sys/vm/nr_hugepages持久化写入/etc/sysctl.confvm.nr_hugepages1024QEMU 侧再用memory-backend-file指定 hpages 路径。开启后最直观的感受是内存密集型应用在虚拟机里的性能抖动明显减少代价是内存被大页占住不能像普通内存那样灵活回收。磁盘方面缓存策略要按业务场景选。数据库类应用我倾向cachenone绕过宿主 page cache用 O_DIRECT 直写减少意外断电时的数据不一致风险对性能要求高但能接受丢失风险的环境writeback会快不少。网络方面确认vhost_net模块加载这是内核态加速路径默认开着但如果你用了自定义内核可能没编进去。5. 常见问题速查与排障思路5.1 模块加载阶段的典型故障现象可能原因排查命令解决方式insmod: Invalid module format头文件版本与运行内核不一致modinfo hello.ko看 vermagic清掉旧.ko重新对版本编译modprobe: could not insert kvm_intel: Operation not permittedBIOS 未开启虚拟化或模块参数异常grep -E vmx|svm /proc/cpuinfo进 BIOS 开启 VT-x/SVM重启Unknown symbol报错依赖模块未加载或符号未导出nm hello.ko | grep U 改用modprobe自动拉依赖rmmod: Module hello is in use有其他模块或程序使用该模块lsmod看 Used by先停用引用方别用rmmod -f强卸内核模块排障第一入口永远是dmesg。加载失败时内核会把具体原因写进日志比如版本不匹配会直接显示模块 vermagic 和内核 vermagic这是最有价值的信息。我见过有人不看日志反复重新编译三四次后来一看日志问题根本不是代码是头文件路径指向了旧内核。5.2 虚拟机运行阶段的典型故障现象可能原因排查命令解决方式/dev/kvm Permission denied当前用户不在 kvm 组ls -l /dev/kvmsudo usermod -aG kvm $USER重新登录KVM acceleration not foundKVM 模块未加载或硬件不支持lsmod | grep kvmgrep vmx /proc/cpuinfo按 4.1 节流程检查模块与 BIOS虚拟机网络慢、ping 延迟高网卡模型用了模拟网卡客户机内lspci、ethtool -i eth0换成 virtio-net必要时重装驱动虚拟机卡顿top显示 %steal 高vCPU 超售过度或宿主负载高vmstat 1ps -eo pid,pcpu,comm | grep qemu降低 vCPU 数量、绑定 CPU 亲和、错峰调度qcow2 镜像越来越大qcow2 按写增长是正常机制qemu-img info demo.qcow2定期清理内部垃圾必要时转 raw虚拟机运行阶段的问题真正出在 KVM 模块本身的比例不大更多是配置、权限、设备模型和资源争抢。特别是%steal时间我在新手上路阶段曾以为它只是好看的数字后来才意识到它直接代表虚拟机在等宿主机资源比很多监控指标都真实。看到 steal 高第一反应应该是查宿主机负载而不是在虚拟机里调应用参数。5.3 一个简单的环境自检脚本为了减少重复劳动我写过一个环境自检脚本新机器到手先跑一遍再决定要不要继续折腾#!/usr/bin/env bash echo CPU if grep -qE (vmx|svm) /proc/cpuinfo; then echo OK: vmx/svm found else echo FAIL: enable VT in BIOS fi echo KVM modules sudo modprobe kvm_intel 2/dev/null || sudo modprobe kvm_amd 2/dev/null if lsmod | grep -q ^kvm; then echo OK: kvm module loaded else echo FAIL: no kvm module exit 1 fi echo /dev/kvm if [ -c /dev/kvm ]; then echo OK: /dev/kvm exists else echo FAIL: /dev/kvm missing fi echo permission if [ -w /dev/kvm ]; then echo OK: you can access /dev/kvm else echo FAIL: add user to kvm group fi echo nested for f in /sys/module/kvm_*/parameters/nested; do [ -f $f ] echo $f: $(cat $f) done这个脚本没有多复杂就是把我排障时的检查顺序固化了。遇到环境问题先跑一遍能省掉一半无效操作。比如权限问题有些人反复重装 libvirt其实一条usermod -aG kvm就能解决。6. 给正在入门的朋友几句经验话6.1 先从模块视角定位虚拟化问题我自己的体会是虚拟化环境出问题七成藏在内核模块、设备权限和 BIOS 开关里而不在 QEMU 复杂参数里。你看到KVM acceleration not found先别急着改虚拟机 XML按 CPU 标志、模块加载、/dev/kvm三个顺序查通常几分钟就能定位。内核日志里往往写着真正的答案dmesg看不明白的时候再放大某个模块的日志级别而不是盲目重启服务。还有一点QEMU 在 KVM 模块没加载时也会尝试启动不要看到虚拟机进程起来了就以为万事大吉。客户机里用lscpu查看 Hypervisor 信息确认是不是真的走了硬件加速。实际跑一个密集计算任务对比开启 KVM 前后的耗时差异你很快就能理解模块加载与否的差距有多大。6.2 踩过几次坑后的操作习惯我现在养成几个习惯。任何内核模块都先在测试机上验证包括自己写的hello.ko确认加载正常再考虑生产环境卸载模块前先看lsmod的使用计数绝不用rmmod -f强卸那是在制造未知风险加载 KVM 相关模块后立刻检查/sys/module/kvm_*/parameters/下的状态避免参数被发行版默认关闭。磁盘镜像也建议明确格式和缓存策略裸奔qcow2做高 IO 测试会吃不少亏。这份笔记目前能覆盖的是从模块基础到虚拟化落地的主链路。下一步我打算继续整理 virtio 队列机制和磁盘调度器对虚拟机 IO 的影响这部分踩过的坑也不少。如果你在实践里遇到了这里没提到的问题欢迎来交流排障经验本来就是互相补全的。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑