linux kernel调用efi runtime service时的内存上下文切换
本文以linux 6.5.2为基础,只讨论arm64平台。
当linux kernel从UEFI启动之后尽管boot service退出了但是仍然可以使用runtime service。这就引发了一个问题:存在于uefi内存空间的code如何被kernel调用。
首先找一个调用efi runtime service的例子:
static void efi_call_rts(struct work_struct *work) { ... switch (efi_rts_work.efi_rts_id) { case EFI_GET_TIME: status = efi_call_virt(get_time, (efi_time_t *)arg1, (efi_time_cap_t *)arg2); ...
这个函数算是调用runtime service的一个入口。在里面有所有可用的runtime service。调用入口是一个宏:
/* * Wrap around the new efi_call_virt_generic() macros so that the * code doesn't get too cluttered: */ #define efi_call_virt(f, args...) \ efi_call_virt_pointer(efi.runtime, f, args)
efi.runtime是一个包含efi runtime service相关的全局变量,
#define efi_call_virt_pointer(p, f, args...) \ ({ \ efi_status_t __s; \ unsigned long __flags; \ \ arch_efi_call_virt_setup(); \ \ __flags = efi_call_virt_save_flags(); \ __s = arch_efi_call_virt(p, f, args); \ efi_call_virt_check_flags(__flags, __stringify(f)); \ \ arch_efi_call_virt_teardown(); \ \ __s; \ })
arch_efi_call_virt_setup() 负责efi 内存上下文的切换。
#define arch_efi_call_virt_setup() \ ({ \ efi_virtmap_load(); \ __efi_fpsimd_begin(); \ raw_spin_lock(&efi_rt_lock); \ })
efi_virtmap_load最重要的操作就是switch_mm
void efi_virtmap_load(void) { preempt_disable(); efi_set_pgd(&efi_mm); } static inline void efi_set_pgd(struct mm_struct *mm) { __switch_mm(mm);
__switch_mm最重要的事就是切换页表也就是设置pgd,一旦pgd切换成功,当前的内存执行环境就切换成功了。那么页表创建是在什么时候完成的呢?
通过搜索efi_mm.pgd找到efi_virtmap_init。
static bool __init efi_virtmap_init(void) { efi_memory_desc_t *md; efi_mm.pgd = pgd_alloc(&efi_mm); 。。。 for_each_efi_memory_desc(md) { 。。。 ret = efi_create_mapping(&efi_mm, md); if (ret) {
这里分配了pgd的内存。看看efi_create_mapping做了什么:
int __init efi_create_mapping(struct mm_struct *mm, efi_memory_desc_t *md) { 。。。 create_pgd_mapping(mm, md->phys_addr, md->virt_addr, md->num_pages << EFI_PAGE_SHIFT, __pgprot(prot_val | PTE_NG), page_mappings_only);
可以看到,页表的创建依赖于从for_each_efi_memory_desc得到的md。md是efi内存映射的描述符:
typedef struct { u32 type; u32 pad; u64 phys_addr; u64 virt_addr; u64 num_pages; u64 attribute; } efi_memory_desc_t;
它描述了一个从虚拟内存到物理内存的连续映射的内存段。
看看md是如何赋值的:
#define for_each_efi_memory_desc(md) \ for_each_efi_memory_desc_in_map(&efi.memmap, md) #define for_each_efi_memory_desc_in_map(m, md) \ for ((md) = (m)->map; \ (md) && ((void *)(md) + (m)->desc_size) <= (m)->map_end; \ (md) = (void *)(md) + (m)->desc_size)
可知,efi.memmap是md的来源。继续探索它是如何设置的。通过搜索我们找到这里:
static int __init arm_enable_runtime_services(void) { ... if (efi_memmap_init_late(efi.memmap.phys_map, mapsize)) { //phys_map保存efi 内存描述符表的物理地址 pr_err("Failed to remap EFI memory map\n"); return 0; } ... } int __init efi_memmap_init_late(phys_addr_t addr, unsigned long size) { struct efi_memory_map_data data = { .phys_map = addr, .size = size, .flags = EFI_MEMMAP_LATE, }; ... return __efi_memmap_init(&data); } int __init __efi_memmap_init(struct efi_memory_map_data *data) { ... phys_map = data->phys_map; if (data->flags & EFI_MEMMAP_LATE) map.map = memremap(phys_map, data->size, MEMREMAP_WB); else map.map = early_memremap(phys_map, data->size); ... set_bit(EFI_MEMMAP, &efi.flags); efi.memmap = map;
这里稍微复杂一点,__efi_memmap_init函数负责赋值efi.memmap,这个map从phys_map起始的一段内存,这就是所有efi内存描述符表的存储地址。那么这个地址的来源又是哪里?
从source的搜索中发现,初始化efi.memmap的地方只有__efi_memmap_init一处。找到调用__efi_memmap_init的另一函数:efi_memmap_init_early
int __init efi_memmap_init_early(struct efi_memory_map_data *data) { /* Cannot go backwards */ WARN_ON(efi.memmap.flags & EFI_MEMMAP_LATE); data->flags = 0; return __efi_memmap_init(data); }
引用该函数的只有一处:
void __init efi_init(void) { struct efi_memory_map_data data; u64 efi_system_table; /* Grab UEFI information placed in FDT by stub */ efi_system_table = efi_get_fdt_params(&data); if (!efi_system_table) return; if (efi_memmap_init_early(&data) < 0) { 。。。
现在接近真相了,这些原始数据是从fdt里面获取的。似乎非常合理,但是等等,我好像知道一点fdt的秘密,那就是传给kernel的fdt也许根本就是空的,如果是这样,后续的操作还有什么意义?
其实fdt还有另一种途径生成,那就是在真正的kernel代码执行前的efi初始化阶段。
static efi_status_t allocate_new_fdt_and_exit_boot(void *handle, efi_loaded_image_t *image, unsigned long *new_fdt_addr, char *cmdline_ptr) { 。。。 status = efi_exit_boot_services(handle, &priv, exit_boot_func); 。。 } efi_status_t efi_exit_boot_services(void *handle, void *priv, efi_exit_boot_map_processing priv_func) { struct efi_boot_memmap *map; efi_status_t status; if (efi_disable_pci_dma) efi_pci_disable_bridge_busmaster(); status = efi_get_memory_map(&map, true); if (status != EFI_SUCCESS) return status; status = priv_func(map, priv); 。。 } static efi_status_t exit_boot_func(struct efi_boot_memmap *map, void *priv) { 。。。 efi_get_virtmap(map->map, map->map_size, map->desc_size, p->runtime_map, &p->runtime_entry_count); return update_fdt_memmap(p->new_fdt_addr, map); }
可以看到得到内存映射的关键函数是efi_get_memory_map
efi_status_t efi_get_memory_map(struct efi_boot_memmap **map, bool install_cfg_tbl) { ... status = efi_bs_call(get_memory_map, &tmp.map_size, NULL, &tmp.map_key, &tmp.desc_size, &tmp.desc_ver);
该函数通过get_memory_map这个efi的boot time service去获取内存映射的描述符表。至此我们算是从kernel层面清楚了efi runtime service调用所需内存上下文的设置。如果想了解uefi是怎么生成memory map的可以去看firmware的源码。
总结一下:
efi runtime service内存页表的切换流程:
1. 在efi初始化阶段通过调用get_memory_map boot time service将efi memory map信息放到fdt中;
2. efi_init中从fdt获取efi memory map其实地址信息,并存储到efi.memmap中;
3. arm_enable_runtime_services->efi_memmap_init_late->__efi_memmap_init将memory map信息remap并设置到efi.memmap中;
4. 在efi_virtmap_init中创建页表;
4. 通过efi_call_virt调用efi runtime service的时候切换页表执行service call;
总体来看,流程有点绕但是并不难理解,记录下来,留作以后回看。