1. KVM_MEMORY_ENCRYPT_OP初始化
main() // QEMU 入口函数
qemu_init() // 初始化虚拟机
configure_accelerators
kvm_init
sev_guest_init
KVM_SEV_INIT
sev_launch_start
KVM_SEV_LAUNCH_START
ram_block_notifier_add(&sev_ram_notifier); //sev_ram_block_added KVM_MEMORY_ENCRYPT_REG_REGION
kvm_state->memcrypt_encrypt_data = sev_encrypt_data;
kvm_arch_init
kvm_vm_enable_cap
kvm_memory_listener_register
memory_listener_register
cpus_register_accel
machine_run_board_init() //pc_init_isa
pc_init1()
pc_memory_init
pc_system_firmware_init
pc_system_flash_map
kvm_memcrypt_encrypt_data
kvm_state->memcrypt_encrypt_data
1.1、代码逻辑解析
此代码段属于 KVM 对 KVM_MEMORY_ENCRYPT_OP
ioctl 的处理核心逻辑,其作用是为内存加密操作提供统一的入口,但实际功能由架构相关代码(如 AMD SVM)实现。以下为逐行分析:
case KVM_MEMORY_ENCRYPT_OP: {
r = -ENOTTY; // 默认返回“不支持该操作”
if (kvm_x86_ops->mem_enc_op) // 检查架构是否实现内存加密操作
r = kvm_x86_ops->mem_enc_op(kvm, argp); // 调用具体实现(如 AMD SEV)
break;
}
1.2、关键行为解读
1. 兼容性控制机制
-
-ENOTTY
的深层含义:表示当前内核或硬件不支持内存加密功能,可能原因包括:- 未启用
CONFIG_KVM_AMD_SEV
内核编译选项 - 硬件无 SEV 支持(如非 AMD EPYC CPU)
- 内核版本过旧(<4.16)或过新(≥5.10,此接口已废弃)
- 未启用
-
kvm_x86_ops->mem_enc_op
的动态绑定:- AMD 实现:指向
svm_mem_enc_op()
(定义于arch/x86/kvm/svm/sev.c
) - Intel 实现:始终为
NULL
(Intel 无等效 SEV 功能)
- AMD 实现:指向
2. 参数传递流程
// 用户态(QEMU)调用示例:
struct kvm_sev_cmd cmd = {
.id = KVM_SEV_INIT, // 子命令类型
.data = (unsigned long)&init_params // 具体参数结构体
};
ioctl(kvm_fd, KVM_MEMORY_ENCRYPT_OP, &cmd);
// 内核态处理(svm_mem_enc_op):
static int svm_mem_enc_op(struct kvm *kvm, void __user *argp)
{
struct kvm_sev_cmd cmd;
copy_from_user(&cmd, argp, sizeof(cmd)); // 从用户空间复制命令
return sev_handle_cmd(kvm, cmd.id, cmd.data); // 分发到 SEV 子处理函数
}
1.3、与新版接口的差异
旧版(KVM_MEMORY_ENCRYPT_OP) vs 新版(KVM_SEV_*)
特性 | 旧版接口 | 新版接口(≥5.10) |
---|---|---|
命令结构 | 统一入口 + 子命令 ID | 独立 ioctl 命令(如 KVM_SEV_INIT ) |
可维护性 | 参数解析耦合度高 | 类型安全,减少错误传递 |
功能扩展性 | 需修改共用结构体 | 独立命令互不影响 |
典型调用栈 | ioctl(KVM_MEMORY_ENCRYPT_OP) → svm_mem_enc_op() → sev_handle_cmd() | ioctl(KVM_SEV_INIT) → sev_guest_init() |
淘汰原因分析(内核 ≥5.10)
- 安全性:旧接口允许任意
cmd.id
传递,存在非法命令注入风险 - 性能:新版省去多层间接跳转,调用路径更短
- 代码清晰度:每个 SEV 操作有独立入口,便于维护
1.4、实际应用场景验证
若在 5.4 内核 中执行以下 QEMU 命令:
qemu-system-x86_64 -machine confidential-guest-support=sev0...
内核日志将出现:
kvm_amd: SEV supported: 255 ASIDs
kvm_amd: SEV-ES supported: 255 ASIDs
svm_mem_enc_op: handling command KVM_SEV_INIT
此时旧接口仍有效,但 5.10+ 内核会拒绝此调用,需改用 KVM_SEV_INIT
。
1.5、调试技巧
-
动态追踪调用路径:
echo 'p:svm_mem_enc_op arch/x86/kvm/svm/sev.c:1800 cmd_id=+0(%dx):u32' > /sys/kernel/debug/tracing/kprobe_events echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
可捕获所有经过
svm_mem_enc_op
的命令 ID。 -
错误码解读:
-ENOTTY
→ 检查硬件/Kconfig/内核版本-EINVAL
→ 参数结构体不匹配(如 32/64 位模式混合)
1.6、迁移到新接口的代码示例
// 旧版(废弃):
struct kvm_sev_cmd cmd = {.id = KVM_SEV_INIT};
ioctl(kvm_fd, KVM_MEMORY_ENCRYPT_OP, &cmd);
// 新版(推荐):
ioctl(kvm_fd, KVM_SEV_INIT, &launch_params); // 直接使用独立命令
该代码段是 KVM 内存加密功能演进的历史产物,理解其工作原理有助于调试旧版本兼容性问题,但在新开发中应遵循 AMD 官方推荐使用 KVM_SEV_*
系列专用命令。
2 qemu中设置逻辑
kvm_init
kvm_memory_listener_register
memory_listener_register
listener_add_address_space
kvm_region_add (kml->listener.region_add = kvm_region_add;)
kvm_set_phys_mem
3 KVM_SET_USER_MEMORY_REGION
3.1、函数调用链路解析
1. 内存槽操作核心流程
2. 关键函数映射关系
用户预期名称 | 内核实际名称/行为 | 所属文件 |
---|---|---|
memslot_region_add | kvm_set_memslot | virt/kvm/kvm_main.c |
__kvm_set_memory_region | virt/kvm/kvm_main.c | |
sev_mem_enc_region_add | arch/x86/kvm/svm/sev.c |
3.2、调试与验证方法
1. 动态追踪技术验证
# 使用 ftrace 跟踪内存槽添加操作
echo 'p:kvm_set_memslot kvm_set_memslot' > /sys/kernel/debug/tracing/kprobe_events
echo 1 > /sys/kernel/debug/tracing/events/kprobes/enable
# 观察输出示例:
# qemu-system-x86-3248 [002] ...1 5678.901234: kvm_set_memslot: (kvm_set_memslot+0x0/0x3c0)
2. 代码交叉引用检查
- 在
sev.c
中搜索region_add
的引用:git grep -n "region_add" arch/x86/kvm/svm/sev.c # 输出:sev.c:1873: .region_add = sev_mem_enc_region_add,
- 确认
sev_mem_enc_region_add
的实现细节:static int sev_mem_enc_region_add(struct kvm *kvm, u64 addr, u64 size) { // 具体实现:遍历 PFN 设置 C-bit for (pfn = addr >> PAGE_SHIFT; pfn < (addr + size) >> PAGE_SHIFT; pfn++) set_page_encryption_mask(pfn); }
3.3、版本兼容性说明
内核版本 | 关键变化点 | 影响分析 |
---|---|---|
Linux 4.19 | 使用 kvm_set_memory_region 旧接口 | 直接操作 kvm_memory_slot |
Linux 5.4+ | 引入 kvm_set_memslot 统一接口 | 抽象化内存槽操作 |
Linux 5.15 | 加密回调通过 kvm_memory_encrypted_ops | 明确分离加密相关操作 |
3.4、开发者操作建议
-
正确跟踪回调函数:
// 在 sev.c 中添加调试输出 static int sev_mem_enc_region_add(struct kvm *kvm, u64 addr, u64 size) { pr_info("SEV: Encrypting memory region 0x%llx-0x%llx\n", addr, addr + size); // ...原有代码... }
通过
dmesg
观察加密区域触发情况。 -
API 变更应对策略:
// 兼容多版本内核的代码示例 #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,4,0) ret = kvm_set_memslot(kvm, &old, &new, KVM_MR_MOVE); #else ret = kvm_set_memory_region(kvm, &mem); #endif
-
文档查阅指引:
- 官方内核文档:
Documentation/virt/kvm/api.rst
(搜索KVM_SET_USER_MEMORY_REGION
) - AMD SEV 白皮书:AMD Secure Encrypted Virtualization API 第 4.2 节 “Memory Encryption”
- 官方内核文档:
4 KVM_MEMORY_ENCRYPT_REG_REGION
memory_region_init_ram_from_file
qemu_ram_alloc_from_file
mr->ram_block = qemu_ram_alloc_from_fd (qemu)
qemu_ram_alloc_from_fd (qemu)
qemu_ram_alloc_internal (qemu)
ram_block_add (qemu)
ram_block_notify_add (qemu)
ram_block_added = sev_ram_block_added (qemu)
sev_ram_block_added (qemu)
kvm_arch_vm_ioctl (KVM_MEMORY_ENCRYPT_REG_REGION)
svm_x86_ops->mem_enc_reg_region
svm_register_enc_region
qemu_ram_alloc_internal
函数解析
qemu_ram_alloc_internal
是 QEMU 内存管理子系统中的核心内部函数,负责为虚拟机分配物理内存块(RAM Block)。它是 QEMU 内存初始化和动态扩展的关键环节,直接关联虚拟机的内存布局与性能优化。以下是其详细说明:
4.1. 函数定义与参数
函数原型
RAMBlock *qemu_ram_alloc_internal(
ram_addr_t size, // 需要分配的内存大小(字节)
ram_addr_t max_size, // 最大可扩展大小(用于动态内存扩展)
void (*resized)(const char*, uint64_t, void*), // 内存大小调整回调函数
void *resized_opaque, // 回调函数的用户参数
uint32_t flags, // 内存属性标志(如共享、只读等)
const char *name, // 内存块名称(调试用)
Error **errp // 错误处理指针
);
参数解析
size
初始分配的物理内存大小(例如 4GB 的虚拟机内存)。max_size
允许动态扩展的最大内存大小。若无需扩展,通常与size
相同。resized
回调
当内存块大小调整时触发(例如通过 balloon 驱动收缩内存)。flags
内存属性标志,常见值包括:RAM_SHARED
: 内存可共享(用于多进程或热迁移)。RAM_PREALLOC
: 预分配物理内存(避免惰性分配)。RAM_NORESERVE
: 不预留宿主内存(允许 overcommit)。
name
唯一标识符,如"pc.ram"
或"virtio-mmio.region"
。
4.2. 核心功能
内存分配流程
-
创建 RAMBlock 结构体
初始化RAMBlock
对象,记录内存块的元数据(大小、宿主指针、映射关系等)。RAMBlock *block = g_new0(RAMBlock, 1); block->used_length = size; block->max_length = max_size; block->flags = flags; block->idstr = g_strdup(name);
-
宿主内存分配
调用宿主系统的内存分配接口(如mmap
或memfd
),根据flags
选择策略:- 私有匿名映射(默认):
ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
- 大页内存(若启用):
ptr = qemu_memfd_alloc(name, size, F_SEAL_GROW | F_SEAL_SHRINK, errp);
- 共享内存(
RAM_SHARED
):ptr = shm_open(name, O_CREAT | O_RDWR, 0666); ftruncate(fd, size);
- 私有匿名映射(默认):
-
注册到全局链表
将新分配的RAMBlock
添加到 QEMU 的全局 RAM 列表ram_list
,供地址空间映射使用:QLIST_INSERT_HEAD_RCU(&ram_list.blocks, block, next);
-
通知内存监听器
触发MEMORY_DEVICE_PREPLUG
事件,通知设备模型(如 VGA、virtio-balloon)内存已更新。
4.3. 调用场景
场景 1:虚拟机启动初始化
在 QEMU 启动参数 -m 4G
时,主函数调用链为:
main() → qemu_init() → pc_memory_init() → memory_region_allocate_system_memory()
→ qemu_ram_alloc_internal()
- 作用:分配虚拟机的主内存(
pc.ram
)。
场景 2:热插拔内存设备
通过 QMP 命令 memory-device-add
动态添加 DIMM:
qmp_memory_device_add() → pc_dimm_plug() → pc_dimm_memory_plug()
→ qemu_ram_alloc_internal()
- 作用:为热插拔的内存条分配新的 RAMBlock。
场景 3:内存动态调整
virtio-balloon 驱动收缩内存时:
virtio_balloon_handle_output() → qemu_balloon() → ram_block_discard_range()
→ 触发 resized 回调
- 作用:释放部分内存并更新
RAMBlock
的used_length
。
4.4. 关键数据结构
RAMBlock
结构(部分)
struct RAMBlock {
struct rcu_head rcu; // RCU 同步保护
ram_addr_t offset; // 在全局地址空间中的偏移
ram_addr_t used_length; // 当前使用的内存大小
ram_addr_t max_length; // 允许的最大内存大小
void *host; // 宿主内存指针
uint32_t flags; // 内存属性标志
char idstr[256]; // 唯一标识名称
QLIST_ENTRY(RAMBlock) next; // 全局链表指针
// ...(其他字段省略)
};
ram_list
全局变量
RAMList ram_list = {
.blocks = QLIST_HEAD_INITIALIZER(ram_list.blocks),
.mru_block = NULL,
};
4.5. 性能优化机制
-
内存预分配(
RAM_PREALLOC
)
避免宿主系统的惰性分配(lazy allocation),减少虚拟机运行时缺页中断的开销。 -
透明大页对齐
若宿主支持 THP(Transparent Huge Pages),qemu_ram_alloc_internal
会确保分配的内存按大页对齐(如 2MB),提升 TLB 命中率。 -
内存锁(mlock)
在实时性要求高的场景(如 vhost-user),调用mlock
锁定物理内存,防止被换出。
4.6. 错误处理
- 内存不足:返回
NULL
并通过error_setg(errp, "Cannot allocate memory")
设置错误信息。 - 重复名称:若
name
已存在,触发assert
(调试模式)或忽略(生产环境可能不同)。
4.7. 代码示例(简化版)
// 分配 1GB 内存,允许扩展到 2GB
RAMBlock *ram = qemu_ram_alloc_internal(
1 * GiB,
2 * GiB,
NULL, // 无需 resized 回调
NULL,
RAM_SHARED, // 共享内存
"guest.ram",
&error_fatal
);
// 将 RAMBlock 映射到地址空间
MemoryRegion *mr = g_new(MemoryRegion, 1);
memory_region_init_ram_ptr(mr, NULL, "sysmem", ram->max_length, ram->host);
address_space_add_memory(&address_space_memory, mr, 0);
小结
qemu_ram_alloc_internal
是 QEMU 内存管理的底层基石,负责:
- 物理内存分配:对接宿主系统的内存分配机制。
- 元数据管理:维护
RAMBlock
的全局状态。 - 扩展性支持:为热插拔、动态调整提供基础设施。
5. KVM_MEMORY_ENCRYPT_OP和KVM_MEMORY_ENCRYPT_REG_REGION区别
5. 1、 KVM_MEMORY_ENCRYPT_OP
功能目标
- 通用加密操作:执行与内存加密相关的全局性操作,例如初始化加密上下文、密钥管理(如生成/注入密钥)、加密状态控制等。
- 示例:
- AMD SEV 中的
KVM_SEV_INIT
(初始化安全加密虚拟化环境) KVM_SEV_LAUNCH_START
(启动虚拟机加密流程)
- AMD SEV 中的
参数处理
- 直接传递指针:通过
argp
直接传递操作类型和参数(通常是架构定义的联合体或结构体)。 - 无显式数据拷贝:依赖底层实现解析
argp
指针,操作可能直接修改内核或硬件状态。
操作层级
- 架构相关实现:通过
kvm_x86_ops->mem_enc_op
调用具体硬件(如 AMD SEV 或 Intel TDX)的加密操作接口。 - 影响范围:通常作用于整个虚拟机或加密会话。
5.2、 KVM_MEMORY_ENCRYPT_REG_REGION
功能目标
- 内存区域注册:将特定内存区域标记为加密/受保护区域,或从加密状态中解除注册。
- 示例:
- AMD SEV 的
KVM_SEV_LAUNCH_UPDATE_DATA
(标记某块内存需加密) - 解除内存区域的加密保护(如迁移完成后)。
- AMD SEV 的
参数处理
- 结构化数据拷贝:从用户空间复制
kvm_enc_region
结构体,明确包含内存区域的物理地址和长度。struct kvm_enc_region { __u64 addr; // 内存区域起始地址 __u64 size; // 内存区域大小 };
- 安全性检查:通过
copy_from_user
验证用户空间数据的合法性,避免非法内存访问。
操作层级
- 内存粒度操作:通过
kvm_x86_ops->mem_enc_reg_region
针对特定物理地址范围进行加密配置。 - 影响范围:仅作用于单个内存区域,用于精细化管理。
关键差异总结
维度 | KVM_MEMORY_ENCRYPT_OP | KVM_MEMORY_ENCRYPT_REG_REGION |
---|---|---|
操作类型 | 全局性加密控制(如初始化、密钥管理) | 内存区域级加密配置(注册/解注册) |
参数形式 | 可能为复合结构体(通过 argp 直接解析) | 固定结构体(明确地址和大小) |
数据传递 | 无显式用户空间拷贝 | 需从用户空间拷贝 kvm_enc_region |
影响范围 | 虚拟机级别或加密会话级别 | 特定物理内存区域 |
典型应用场景
-
虚拟机启动加密:
- 调用
KVM_MEMORY_ENCRYPT_OP
初始化 SEV 环境。 - 多次调用
KVM_MEMORY_ENCRYPT_REG_REGION
逐块注册内存到加密区域。
- 调用
-
热迁移保护:
- 迁移前通过
KVM_MEMORY_ENCRYPT_OP
导出加密密钥。 - 迁移后通过
KVM_MEMORY_ENCRYPT_REG_REGION
重新绑定内存区域。
- 迁移前通过
-
动态内存加密:
- 运行时按需加密敏感内存区域(如 GPU 显存),通过
KVM_MEMORY_ENCRYPT_REG_REGION
动态注册。
- 运行时按需加密敏感内存区域(如 GPU 显存),通过
底层实现依赖
- 硬件支持:两者最终通过
kvm_x86_ops
调用具体实现(如 AMD 的sev_mem_enc_op
和sev_mem_enc_reg_region
)。 - 安全性约束:内存加密操作通常需要 Hypervisor 与安全处理器(如 AMD PSP)协同完成,确保密钥不泄露。
此设计体现了 KVM 对内存加密的灵活支持:既提供全局控制接口,又允许细粒度内存管理,适配不同硬件加密方案。