关于Linux内核月报
Linux阅码场
Linux阅码场内核月报栏目,是汇总当月Linux内核社区最重要的一线开发动态,方便读者们更容易跟踪Linux内核的最前沿发展动向。
限于篇幅,只会对最新技术做些粗略概括,技术细节敬请期待后续文章,也欢迎广大读者踊跃投稿为阅码场社区添砖加瓦。
本期月报(总第2期)主要贡献人员:
陈玮、张健、廖威雄
(月报的完善和专业,离不开大牛们的持续贡献,欢迎更多大牛加入月报贡献团队)
第一期链接:
Linux阅码场 - Linux内核月报(2020年06月)
1. 虚拟化
1.1 Kishon Vijay Abraham I: Enhance VHOST to enable SoC-to-SoC communication
该PATCH系列对Linux vhost做了增强,从而让它支持 SoC与SoC之间基于MMIO的通信。该系列也对RPMSG进行了增强,让它可以支持两个SoC之间使用PCIe Root Complex<->Endpoint 连接方式和HOST1-NTB-HOST2连接方式的通信。
修改的内容包括:
1) 修改vhost,让它符合Linux标准驱动模型
2) 在vring中添加通过MMIO访问virtqueue支持
3) 在rpmsg中添加居于vhost的客户端驱动
4) 为通过PCIe Root Complex <->Endpoint 方式互相连接的两个SoC提供可以用于rpmsg通信的PCIe Root Complex驱动(使用virtio)和PCIe Endpoint 驱动(使用vhost)
案例1:
5) 为通过HOST1-NTB-HOST2 方式互相连接的两个SoC提供可以用于rpmsg通信的NTB Virtio 驱动和NTB Vhost 驱动
案例2:
6) 添加了一个configfs接口用于配置各个组件
软件层次结构:
一个比较High-Level软件层次结构应该如下图所示。本系列只增加了对RPMSG VHOST的支持,但是其他一些类似的修改还需要在网络和SCSI驱动中完成。当这些修改都完成后,任何的vhost设备(PCI, NTB,Platform,user)都可以使用任何的vhost客户端驱动程序。
原始讨论请看这里[1]
[1] -> https://lore.kernel.org/r/2cf00ec4-1ed6-f66e-6897-006d1a5
1.2 Andra Paraschiv: Add support for Nitro Enclaves
Nitro Enclaves (NE)是Amazon弹性计算云(EC2)的一种新功能。它允许客户在EC2实例[1]内部再分割出一块隔离的计算环境。
例如,一个在虚拟机中运行的用于处理敏感数据的应用程序,可以和运行在同一个虚拟机中的其它应用程序分离开。我们称呼这个运行EC2实例的虚拟机为主虚拟机。分离后,这个应用程序运行在和主虚拟机不同的另外一个单独虚拟机里,也可以叫做enclave。
Enclave与衍生它的虚拟机一起运行。这种设置满足低延迟应用程序的需求。为enclave分配的资源,例如内存和CPU等,都是从主虚拟机中分割出来的。每个enclave在主虚拟机中都会有一个进程和它一一对应。该进程通过NE驱动暴露的ioctl接口和内核NE驱动进行通信。
从这个意义而言,NE有两个组成模块:
1. Enclave的抽象进程 – 这是一个运行在主虚拟机里的用户态程序。它可以使用内核NE驱动提供的ioctl接口来产生一个Enclave虚拟机(也就是模块2)。
Nitro Enclave包含一个模拟的PCI设备,该PCI设备会暴露给主虚拟机。主虚拟机中运行的NE驱动已经包含了该模拟PCI设备的驱动。
NE驱动中的ioctl操作逻辑都会转换为该PCI设备命令操作,例如NE_START_ENCLAVE ioctl操作就会被映射到Enclave start这个PCI命令。然后该PCI设备会把命令转换为hypervisor一侧的实际操作。Nitro hypervisor和主虚拟机运行在同一主机上,它依然是一个KVM的核心技术作为基础。
2. Enclave本身 – 一个与产生他的主虚拟机运行在同一主机上的虚拟机。从主虚拟机里分割出来给Enclave使用的内存和CPU是Enclave专用的。但是将不会有持久性存储器(磁盘,FLASH等)分配给Enclave使用。从主虚拟机中分割出来给Enclave使用的内存区间要求是2MiB / 1GiB对齐的连续物理区间(或者是2MiB/1GiB的倍数,如8MiB)。这些内存可以在用户态通过hugetlbfs[2][3]进行分配。Enclave需要至少64 MiB的内存。而且Enclave所使用的内存和CPU都必须来自同一个NUMA节点。
Enclave运行在它专用的CPU核心上. CPU0和其它的CPU都需要保留在主虚拟机,让主虚拟机可以访问。为了Nitro Enclave,一个CPU资源池将被管理员用户建立。可以前往内核文档的CPU list章节[4]查看该CPU资源池的详细格式。
Enclave和主虚拟机之间的通信是通过一个本地通信通道,该通道采用virtio-vsock[5]实现。主虚拟机使用virtio-pci vsock设备,而enclave虚拟机将使用virtio-mmio vsock设备。这些vsock设备通过eventfd进行信号传递。Enclave虚拟机可以使用local APIC和IOAPIC,并从中获取virtio-vsock设备的中断信号。Virtio-mmio设备将被放置于典型的4GiB地址以下。
在Enclave虚拟机中运行的应用程序需要和OS(内核,ramdisk,init程序)一起被打包成Enclave镜像。Enclave虚拟机拥有它自己的内核,并且遵守Linux标准启动协议。内核bzImage镜像,内核启动命令行和ramdisk都是Enclave镜像格式(Enclave Image Format)的一部分。在EIF的头部还会添加一些元数据包括magic number,EIF版本信息,镜像大小和CRC校验码。
整个Enclave镜像(EIF),包括内核和ramdisk,会做hash计算。这些hash值可以用来保证Enclave镜像的完整性。可以用来检查我们加载到Enclave虚拟机里运行的镜像是不是我们想要运行的那个Enclave镜像,它有没有损坏或者被篡改等。
Nitro Hypervisor会生成一个签名的认证文件,该文件会包含这些加密信息的检验数据,这些信息在后面的使用中可以用来作为Enclave的身份证明。和Nitro Enclave集成在一起的KMS(AWS Key Management Service)服务就是一个示例,它会检查这些认证文件。
Enclave的镜像(EIF)会被加载到Enclave虚拟机内存的第一个8MiB偏移的位置。Enclave虚拟机的init进程会通过vsock CID和主虚拟机连接,并通过一个预先设定好的端口(9000)发送心跳数据(0xb7)。这个机制用于主虚拟机检测Enclave是不是已经启动成功了。
当Enclave虚拟机崩溃或者优雅退出,主虚拟机中的NE驱动会收到一个中断事件。主虚拟机中运行的Enclave抽象进程一直在轮询事件的通知,当该进程收到内核NE驱动转发的事件通知后,
该Enclave抽象进程也会退出。
1.3 Boqun Feng: Hyper-V: Support PAGE_SIZE larger than 4K
该PATCH系列为在虚拟机中运行的,使用页面大小大于4K的客户系统提供必要的修改。
Hyper-V一直采用4K作为它的页面大小,并且期望在和虚拟机中客户系统的通信中采用同样的页面大小。也就是说,在所有的hypervisor-guest通信协议中使用的“PFN”s,代表的是以HV_HYP_PAGE_SIZE为大小的页面的数量,而不是以PAGE_SIZE为大小的页面的数量。为了支持虚拟机客户系统使用更大的页面,我们需要在hypervisor-guest通信中正确地在这两个不同的页面大小间进行转换。这基本就是这个PATCH系列所做的工作。
在这个转换中,有一个挑战就是如何处理ringbuffer。一个ringbuffer有两个部分:头部和数据段,这两个部分在虚拟机客户系统中都是使用PAGE_SIZE进行对齐的。因为我们会使用“双重映射”的技巧在虚拟机客户系统的虚拟地址空间将数据段映射两次。这会为快速封装和方便数据就地访问提供便利。然而,Hyper-V hypervisor总是将ringbuffer头部视为4K大小的页面。为了克服这个分歧,我们扩大了hv_ring_buffer结构体,让它总是按PAGE_SIZE对齐。并引入gpadl类型概念,可以让
vmbus_establish_gpadl()针对ringbuffer的情况做特殊处理。但是请注意, gpadl类型只是对虚拟机客户系统而言,在Hyper-V hypervisor并没有这样的概念。
该系列由以下11个patch组成:
Patch 1~4: 介绍gpadl类型,这样在PAGE_SIZE != HV_HYP_PAGE_SIZE的情况下,我们就可以处理ringbuffer。同时修复了一些本该使用HV_HYP_PAGE_SIZE但却错误使用了PAGE_SIZE的地方。
Patch 5~6: 添加了一些辅助函数帮助计算hvpfn(以HV_HYP_PAGE_SIZE为单位的页面数量)和其它相关数据。因此我们可以在驱动代码中使用它们。
Patch 7~11: 修改驱动程序并使用相关辅助函数,使得对应的网络,输入,存储和工具类驱动程序可以在PAGE_SIZE != HV_HYP_PAGE_SIZE情况下工作。
结合Michael关于ARM64 Hyper-V guest support的补丁集[1], 已经使用PAGE_SIZE=64k和PAGE_SIZE=16k配置的虚拟机客户系统进行了一些测试。
[1]: https://lore.kernel.org/lkml/1584200119-18594-1-git-send-...
1.4 Liu Yi L: vfio: expose virtual Shared Virtual Addressing to VMs
共享虚拟地址(Shared Virtual Addressing SVA), 在Intel平台上又可以称作共享虚拟内存(Shared Virtual Memory SVM)。它允许在DMA设备和应用程序之间共享地址空间。SVA可以降低编程的复杂度同时增强程序的安全性。
该VFIO补丁系列打算将SVA暴露给虚拟机使用。比如让虚拟机里的应用程序和直通设备共享地址空间。在这个补丁系列中,我们称之为vSVA。完整的vSVA支持需要同时对QEMU/VFIO/IOMMU进行修改。这些修改都分别在独立的补丁系列里完成,我们可以在后面列出的“相关系列”里找到。
以下是一个比较高层次的SVA虚拟化架构图。vSVA的关键设计是利用HOST IOMMU中的两级地址转换功能(该功能也被称作IOMMU嵌套地址转换)
这个补丁集按照vSVA对PCI直通设备的基础支持,大略可以被分为4个部分:
1. vfio 支持虚拟机的PASID的分配和释放
2. vfio 支持虚拟机页表的绑定请求
3. vfio 支持虚拟机IOMMU cache invalidation
4. vfio 在IOMMU-backed的mdev设备的上使用vSVA
完整的vSVA上游内核补丁集可以被拆分为3个阶段:
1. 通用API和PCI设备直通
2. IOMMU-backed受控直通设备
3. Page Request Services (PRS) 支持
该补丁集以Jacob的补丁集(链接如下)为基础,主要负责阶段1和阶段2。
[PATCH V10 00/11] Nested Shared Virtual Address (SVA) VT-d support:
https://lkml.org/lkml/2020/3/20/1172
目前完整的vSVA补丁集可以在以下分支中获取。
https://github.com/luxis1999/linux-vsva.git: vsva-linux-5.6-rc6
对应的QEMU补丁系列可以在以下链接找到。
[PATCH v1 00/22] intel_iommu: expose Shared Virtual Addressing to VMs
完整的QEMU分支可以参考以下链接
https://github.com/luxis1999/qemu.git: sva_vtd_v10_v1
1.5 Yang Weijiang: Introduce support for guest CET feature
英特尔的控制流强制技术 (Control-flow Enforcement Technology, CET) 会提供一个机制来阻挠使用面向返回编程 (Return-orientated programming, ROP) 和面向跳转编程 (Jump-orientated programming, JOP) 的漏洞的攻击。CET技术又两个子类特性:影子栈(Shadow Stack,SHSTK)和间接跳转跟踪(Indirect Branch Tracking,IBT)。影子栈基于Shadow Stack Register的返回地址验证可以防止ROP编程攻击,而IBT支持粗颗粒度前向跳转地址验证,可以防止JOP编程攻击。
KVM已经更新了部分代码来添加虚拟机中CET支持。这些代码包括:CPUID/XSAVES配置, MSR直通, 用户态MSR访问接口,vmentry/vmexit配置, 嵌套虚拟化等。这些补丁都依赖于之前为内核添加的CET xsavs支持和CET定义补丁集,比如MSR和相关特性标志定义等。
这些CET内核补丁可以在以下地址找到:
https://lkml.kernel.org/r/20200429220732.31602-1-yu-cheng...
2. 文件系统和Block Layer
2.1 Support Age-Threshold based Garbage Collection for f2fs
这系列补丁为 F2FS 文件系统设计了一个新的垃圾回收算法 ATGC (Age Threshold based Garbage Collection)。这是基于年龄阈值的回收算法,主要解决了当前以下几个痛点:
1. 当前的回收算法只在意 segment 是否有最少的有效块,而不管它是不是比较新的数据或者属于 hot segment。迁移这些较新的数据没意义,因为这些数据大概率很快又会被更新。
2. 现在的回收算法不管数据的更新频率,可能会再次把冷热数据混在一起
3. 现在的GC分配器主要使用 LFS 类型的 segment,这加快空闲 segment 的使用
新的垃圾回收算法使用 SSR 分配器,优先回收最旧的数据。其大致逻辑如下:
1. 0表示最新,100表示最旧。通过设置的“年龄”阈值,算则大于阈值的“高龄”的且含有最少有效块的segment,作为待迁移segment。
2. 选择与待迁移segment “年龄” 相近的且拥有最多有效块的segment,作为迁移目标
3. 通过 SSR 分配器来实现把 待迁移segment 迁移到目标segment。
上述做法优先只在比较旧的segment之间进行合并,避免了新旧(冷热)数据的混合,还不额外消耗空闲segment。
在补丁作者的测试中,GC的次数从162次下降到了75次,移动的块从41454个下降到了12813个,GC的效率得到明显的提升。
2.2 fs: add mount_setattr()
此补丁为内核新设计的挂载相关的系列API,添加了新的系统调用mount_setattr()
。新的系统调用用于设置挂载扩展参数,例如private/shared/slave/unbindable。
在这里不得不提下内核新设计的挂载API。与旧的挂载 mount()
系统调用相比,新的 API 把一次挂载的操作细分成多个API。在提交的补丁中有讲到细分mount()
的原因 (https://patchwork.kernel.org/cover/10610615/),这里只列举其中几条:
- 传统的
mount()
实际上是把多个接口糅合成一个接口,导致挂载接口非常臃肿,也导致挂载失败时不容易判别是哪里出错。 - 挂载配置的字符串只能在1个page之内,当属性字符串非常长时会无法使用
- 不适用于类似 overlayfs 这样多个源的挂载情况
- 一个非法参数可能会导致挂载失败,但由于更早之前的设置已经生效了却无法回滚,导致超级块处于一个不一致的状态。
新设计的挂载API需要这么使用:
fd = fsopen("nfs"); fsconfig(fd, FSCONFIG_SET_STRING, "option", "val", 0); fsconfig(fd, FSCONFIG_CMD_CREATE, NULL, NULL, 0); mfd = fsmount(fd, MS_NODEV); move_mount(mfd, "", AT_FDCWD, "/mnt", MOVE_MOUNT_F_EMPTY_PATH);
以下是相关的资料:
《新一代mount系统调用》:https://zhuanlan.zhihu.com/p/93592262 《A new API for mounting filesystems》:,
https://lwn.net/Articles/753473/
新挂载API的提交补丁:
https://patchwork.kernel.org/cover/10610615/
2.3 debugfs: Add access restriction option
debugfs 为用户空间提供了非常多且非常实用的 debug 功能,但同时 debugfs 也包含一些敏感信息,因此需要谨慎对待。
这个补丁为 debugfs 提供了一个额外的保护措施,通过模块参数debugfs
来限制 debugfs 的功能。例如:
- debugfs=on:所有功能正常使用
- debugfs=no-mount:不允许挂载,但debugfs的API可以正常使用
- debugfs=off:不允许挂载,且模块注册文件或者目录都会直接返回-EPERM错误。
同时也提供了 Kconfig 上的配置,只用模块参数和 Kconfig 上的配置同时使能时,debugfs才能正常工作。
总的来说,此补丁为用户提供了一个动态开关 debugfs 的功能。
2.4 ext4: add fast commits feature
此系列补丁为 JBD2 日志系统提供了一个快速提交的功能,且在ext4上做了适配。
日志的快速提交是一个怎么样的逻辑?举个例子来说,如果 ext4 为某个文件新添加了一个 extent ,此时连带的 inode table、 block bitmap、group descriptor 和 superblock 系列元数据都需要更新,这就产生了大量的脏元数据。这些脏元数据都写入 到日志中。实际上连带的元数据改动都可以由新添加extent的记录推导出来。如果日志只记录原始数据和相关关系,在日志应用 时再根据信息推导和还原,就能大量减少日志的体量。
目前补丁作者已经在ext4上做了适配,在作者的测试中,性能提升34.4%至192.2%,效果还是挺明显的。
2.5 Mount notifications
此系列补丁为挂载拓扑事件做了个主动上报的机制,例如挂载、卸载、挂载重新配置等事件触发时向用户空间上报事件。
为什么需要内核主动上报呢?用户应用通过遍历 /proc/mount 知道我们的 namespace 出现了挂载状态的改动,还需要有 不少解析对比工作才能知道发生了什么变化,这效率太低下了。此外,访问 /proc/mount 会持有 namespace_sem 锁,而且 生成 /proc/mount 的文本信息也会非常复杂。如果内核对挂载事件主动做上报,上述提到的问题就能得到很好的解决。
用户空间可以通过新的 fsinfo()
系统调用来获取挂载事件。
2.6 VFS: Filesystem information
此系列补丁添加了个新的系统调用 fsinfo()
,除了可以获取挂载的拓扑结构变化,还可以获取文件系统超级块属性。例如:
- 通用的文件系统超级块属性
- 文件系统的id信息(UUID,卷标签,设备号等)
- 文件系统功能的限制情况
- 支持的statx字段和属性和IOC标志信息
- 支持的功能
- 空间使用情况(类似与statfs)
- 文件系统的特殊属性
- Superblock-level timestamps
- Cell name, workgroup or other netfs grouping concept.
- Server names and addresses.
- VFS的信息
- 挂载拓扑信息
- 挂载属性
- 挂载点路径
详细的功能可以看提交日志:https://lwn.net/Articles/826980/
3. 体系架构
本月,内核社区补丁很丰富,有不少大特性在讨论。例如安全方面有Intel的SGX和AMD的SEV。热门架构RISC-V也有很多大特性在review,例如uefi,KVM,kprobe/uprobe等。上月月报提到的ARM64特性MTE(Memory Tagging Extension),Catalin在等待最后五个补丁的ack。这次继续分享三个补丁集:
3.1 arm64: tlb: add support for TLBI RANGE instructions
TBLI RANGE是armv8.4扩展,该指令支持按范围invalidate TLB。从测试结果看,当page数量是256时,与不支持TLBI RANGE相比,性能有10倍提升。补丁由来自华为虚拟化团队的Zhenyu Ye提交。补丁地址:https://lwn.net/Articles/825747/
3.2 Use per-CPU temporary mappings for patching
内核中有些情况下需要动态修改代码(patching),例如kprobe,ftrace等。同时当内核打开了CONFIG_STRICT_KERNEL_RWX,内核的text和rodata会被设置为只读,非text段设置为不可执行。所以在打开CONFIG_STRICT_KERNEL_RWX后,为了patching,需要临时覆盖RWX对text段的保护。之前ppc的实现中,这个临时映射可以被其它CPU看到,这可能有安全风险。在这个补丁中,Christopher基于x86的“temporary mm“工作,该工作保证映射只有当前CPU可以访问。从而避免了上述安全风险。补丁地址:https://lwn.net/Articles/825757/
5.3 KVM RISC-V Support
RISC-V对于KVM的支持目前是v13(KVM RISC-V Support)[1],这个工作最早从RISC-V hypervisor extension 0.3开始[2],目前已经支持最小的上下文切换,中断(PLIC,IPI等)/timer/SBI 0.1等模块/机制的初步emulation等。如果希望用QEMU玩耍,可以参考这个链接[3]。
至于具体实现,不同架构对KVM的支持不太一样。x86中KVM与host kernel一起运行在root ring0。没有VHE支持的ARM/ARM64架构,KVM被分为两部分(low visor和high visor):非特权的KVM与内核运行在PL1(ARM架构)/EL1(ARM64架构),需要更高权限时,trap到PL2(ARM架构)/EL2(ARM64架构)。这个方式增加了KVM的开销。所以ARM64架构在v8.1引入了VHE,使KVM和host kernel一起运行在VHE mode,避免了上述trap。
RISC-V的实现和VHE有些类似。Ta的hypervisor extension把原有运行Kernel的System Mode(即S mode)拆分为VS和HS mode,参考下图(来源见[4])。
如果想撸代码,可以从RISC-V hypervisor extension[2]开始,并结合代码[5]。例如补丁的第一点“Minimal possible KVM world-switch which touches only GPRs and few CSRs.”,可以从Commit 542705c835bf ”RISC-V: KVM: Implement VCPU world-switch“,看到其实现,Host和Guest之间的world switch在__kvm_riscv_switch_to实现。该函数先保存了通用寄存器(GPR),再保存Host hypervisor相关的寄存器(Hypervisor CSR,CSR:Control and Status Register),设置Host异常返回地址,恢复Guest的CSR和GPR后,resume to guest。从Guest返回的过程相反。返回过程可以参考下图截图的__kvm_switch_return。
参考资料:
[1] https://lwn.net/Articles/825864/
[2] GitHub - riscv/riscv-isa-manual: RISC-V Instruction Set Manual,需要从这里下载latest privilege spec,里面有v0.6.1的hypervisor extension。
[3] https://github.com/kvm-riscv/howto/wiki/KVM-RISCV64-on-QEMU [4] RISC-V虚拟化技术和进展:https://static.sched.com/hostedfiles/kvmforum2019/d9/TheHypearoundtheRISCVHypervisorv11.pdf
[5] RISC-V KVM代码: https://github.com/kvm-riscv/linux.git branch riscvkvm_master