鸿蒙轻内核A核源码分析系列五 虚实映射(2)虚实映射初始化

2、 虚拟映射初始化

在文件kernel/base/vm/los_vm_boot.c中的系统内存初始化函数OsSysMemInit()会调用虚实映射初始化函数OsInitMappingStartUp()。该函数代码定义在文件arch/arm/arm/src/los_arch_mmu.c,代码如下。⑴处函数使TLB失效,清理虚实映射缓存数据,涉及些cp15寄存器和汇编,后续再分析。⑵处函数切换到临时TTB。⑶处设置内核地址空间的映射。下面分别详细这些函数代码。

VOID OsInitMappingStartUp(VOID)
{
⑴   OsArmInvalidateTlbBarrier();

⑵   OsSwitchTmpTTB();

⑶  OsSetKSectionAttr(KERNEL_VMM_BASE, FALSE);
    OsSetKSectionAttr(UNCACHED_VMM_BASE, TRUE);
    OsKSectionNewAttrEnable();
}

2.1 函数OsSwitchTmpTTB

函数OsSwitchTmpTTB申请16KiB的内存存放L1页表项数据,把页表项数据从g_firstPageTable复制到申请的内存区域。⑴处获取内核地址空间。L1页表由4096个页表项组成,每个4 bytes,共需要16KiB大小。所以⑵处代码按16KiB对齐申请16KiB大小的内存区域存放L1页表项。⑶处设置内核虚拟内存地址空间的转换表基地址TTB。⑷处把g_firstPageTable页表数据复制到内核地址空间的转换表区域。如果复制失败,则直接使用g_firstPageTable。⑸处设置内核虚拟地址空间的TTB转换地址对应的物理内存地址,然后调用函数OsArmWriteTtbr0写入MMU寄存器。

STATIC VOID OsSwitchTmpTTB(VOID)
{
    PTE_T *tmpTtbase = NULL;
    errno_t err;
⑴   LosVmSpace *kSpace = LOS_GetKVmSpace();

    /* ttbr address should be 16KByte align */
⑵   tmpTtbase = LOS_MemAllocAlign(m_aucSysMem0, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,
                                  MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);
    if (tmpTtbase == NULL) {
        VM_ERR("memory alloc failed");
        return;
    }

⑶  kSpace->archMmu.virtTtb = tmpTtbase;
⑷  err = memcpy_s(kSpace->archMmu.virtTtb, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS,
                   g_firstPageTable, MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS);
    if (err != EOK) {
        (VOID)LOS_MemFree(m_aucSysMem0, tmpTtbase);
        kSpace->archMmu.virtTtb = (VADDR_T *)g_firstPageTable;
        VM_ERR("memcpy failed, errno: %d", err);
        return;
    }
⑸  kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);
    OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);
    ISB;
}

2.2 函数OsSetKSectionAttr

内部函数OsSetKSectionAttr用于设置内核虚拟地址空间的区间属性,分别针对内核虚拟地址空间的内核区间[KERNEL_ASPACE_BASE,KERNEL_ASPACE_BASE+KERNEL_ASPACE_SIZE]和未缓存区间[UNCACHED_VMM_BASE,UNCACHED_VMM_BASE+UNCACHED_VMM_SIZE]进行设置。内核虚拟地址空间是固定映射到物理内存的,内核地址空间的映射包含代码段、数据段、堆栈区间映射,如下示意图所示:

⑴处计算相对内核虚拟地址空间基地址KERNEL_VMM_BASE的偏移大小。⑵处先计算相对偏移值的text、rodata、data_bss段的虚拟内存地址,然后创建这些段的虚实映射关系数组mmuKernelMappings。⑶处设置内核虚拟地址区间的虚拟转换基地址TTB和物理转换基地址TTB。然后解除虚拟地址virtAddr的虚实映射,解除映射的长度就是代码段、只读数据段、数据BSS段这些内存段的长度。⑷处按指定的标签flags对text代码段之前的内存区间进行虚实映射。⑸处映射text代码段、rodata只读数据段、data_bss数据段的内存区间,并调用函数LOS_VmSpaceReserve在进程空间中预定地址区间。⑹是BSS段后面的heap区、stack区的映射,映射虚拟地址空间的内存堆栈区间到对应的物理内存区间。

STATIC VOID OsSetKSectionAttr(UINTPTR virtAddr, BOOL uncached)
{
⑴  UINT32 offset = virtAddr - KERNEL_VMM_BASE;
    /* every section should be page aligned */
⑵  UINTPTR textStart = (UINTPTR)&__text_start + offset;
    UINTPTR textEnd = (UINTPTR)&__text_end + offset;
    UINTPTR rodataStart = (UINTPTR)&__rodata_start + offset;
    UINTPTR rodataEnd = (UINTPTR)&__rodata_end + offset;
    UINTPTR ramDataStart = (UINTPTR)&__ram_data_start + offset;
    UINTPTR bssEnd = (UINTPTR)&__bss_end + offset;
    UINT32 bssEndBoundary = ROUNDUP(bssEnd, MB);
    LosArchMmuInitMapping mmuKernelMappings[] = {
        {
            .phys = SYS_MEM_BASE + textStart - virtAddr,
            .virt = textStart,
            .size = ROUNDUP(textEnd - textStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),
            .flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_EXECUTE,
            .name = "kernel_text"
        },
        {
            .phys = SYS_MEM_BASE + rodataStart - virtAddr,
            .virt = rodataStart,
            .size = ROUNDUP(rodataEnd - rodataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),
            .flags = VM_MAP_REGION_FLAG_PERM_READ,
            .name = "kernel_rodata"
        },
        {
            .phys = SYS_MEM_BASE + ramDataStart - virtAddr,
            .virt = ramDataStart,
            .size = ROUNDUP(bssEndBoundary - ramDataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),
            .flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE,
            .name = "kernel_data_bss"
        }
    };
    LosVmSpace *kSpace = LOS_GetKVmSpace();
    status_t status;
    UINT32 length;
    int i;
    LosArchMmuInitMapping *kernelMap = NULL;
    UINT32 kmallocLength;
    UINT32 flags;

    /* use second-level mapping of default READ and WRITE */
⑶  kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;
    kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);
    status = LOS_ArchMmuUnmap(&kSpace->archMmu, virtAddr,
                              (bssEndBoundary - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT);
    if (status != ((bssEndBoundary - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
        VM_ERR("unmap failed, status: %d", status);
        return;
    }

    flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE | VM_MAP_REGION_FLAG_PERM_EXECUTE;
    if (uncached) {
        flags |= VM_MAP_REGION_FLAG_UNCACHED;
    }
⑷  status = LOS_ArchMmuMap(&kSpace->archMmu, virtAddr, SYS_MEM_BASE,
                            (textStart - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
                            flags);
    if (status != ((textStart - virtAddr) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
        VM_ERR("mmap failed, status: %d", status);
        return;
    }

⑸  length = sizeof(mmuKernelMappings) / sizeof(LosArchMmuInitMapping);
    for (i = 0; i < length; i++) {
        kernelMap = &mmuKernelMappings[i];
        if (uncached) {
            kernelMap->flags |= VM_MAP_REGION_FLAG_UNCACHED;
        }
        status = LOS_ArchMmuMap(&kSpace->archMmu, kernelMap->virt, kernelMap->phys,
                                 kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT, kernelMap->flags);
        if (status != (kernelMap->size >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
            VM_ERR("mmap failed, status: %d", status);
            return;
        }
        LOS_VmSpaceReserve(kSpace, kernelMap->size, kernelMap->virt);
    }

⑹   kmallocLength = virtAddr + SYS_MEM_SIZE_DEFAULT - bssEndBoundary;
    flags = VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE;
    if (uncached) {
        flags |= VM_MAP_REGION_FLAG_UNCACHED;
    }
    status = LOS_ArchMmuMap(&kSpace->archMmu, bssEndBoundary,
                            SYS_MEM_BASE + bssEndBoundary - virtAddr,
                            kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
                            flags);
    if (status != (kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
        VM_ERR("mmap failed, status: %d", status);
        return;
    }
    LOS_VmSpaceReserve(kSpace, kmallocLength, bssEndBoundary);
}

2.3 函数OsKSectionNewAttrEnable

函数OsKSectionNewAttrEnable设置虚实地址的转换表基地址TTB并清楚TLB缓存。⑴处获取内核虚拟进程空间,⑵处设置进程空间MMU的虚拟地址转换表基地址TTB,然后查询到物理内存地址并设置物理内存地址转换表基地址。⑶处从CP15 C2寄存器读取TTB地址,取高20位。⑷处将内核物理内存页表基地址写入CP15 c2 TTB寄存器。⑸处清空TLB缓冲区,然后释放内存。涉及到了MMU寄存器,后续系列会专门详细讲解。

STATIC VOID OsKSectionNewAttrEnable(VOID)
{
⑴  LosVmSpace *kSpace = LOS_GetKVmSpace();
    paddr_t oldTtPhyBase;

⑵  kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;
    kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);

    /* we need free tmp ttbase */
⑶  oldTtPhyBase = OsArmReadTtbr0();
    oldTtPhyBase = oldTtPhyBase & MMU_DESCRIPTOR_L2_SMALL_FRAME;
⑷  OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);
    ISB;

    /* we changed page table entry, so we need to clean TLB here */
⑸  OsCleanTLB();

    (VOID)LOS_MemFree(m_aucSysMem0, (VOID *)(UINTPTR)(oldTtPhyBase - SYS_MEM_BASE + KERNEL_VMM_BASE));
}
<think>我们之前讨论了`phys_to_virt`的作用,现在用户的新问题是关于启动阶段物理地址的映射情况。用户询问:"正常物理地址在启动阶段是否都会进行映射"。我们需要结合Linux内核启动过程来回答这个问题。 根据引用[4],在Linux内核启动阶段,虚实地址映射是在MMU开启之前建立的。但在MMU开启之前,只能使用物理地址。开启MMU后,可以使用虚拟地址。然而,并不是所有的物理地址都会在启动阶段被映射。 具体来说: 1. 内核在启动的早期阶段(通常是汇编阶段)会建立初始页表。这个初始页表通常只映射内核运行所必需的部分物理内存,比如内核代码段、数据段、初始堆栈以及设备树(如使用)等。这些区域的物理地址会被映射到虚拟地址空间的内核部分(如线性映射区域)。 2. 初始页表通常不会映射所有的物理内存。它只映射了低端内存的一部分(足够内核启动使用),而高端内存可能不会被映射。 3. 在后续的内核初始化过程中(比如在`start_kernel`函数中),会调用`paging_init`等函数来建立完整的页表,这时才会建立所有物理内存的线性映射(对于低端内存)或者动态映射(对于高端内存)。 因此,在启动阶段(尤其是早期阶段),并不是所有的物理地址都会被映射。只有内核启动所必需的那些物理地址才会被映射。随着内核初始化的进行,映射会逐渐完善。 引用[4]提到:“至此,启动阶段的页表建立完成,但是由于目前并没有开启MMU,同时页表的物理地址并没有填充给TTBR0和TTBR1,所以在开启MMU之前,依然不能使用虚拟地址,只能使用物理地址。开始了MMU之后,可以使用虚拟地址。”这里说明初始页表建立后,开启MMU后就可以使用虚拟地址访问那些被映射的区域。 所以,回答用户的问题:在启动阶段(MMU开启之前),内核并没有使用虚拟地址,而是直接使用物理地址。在开启MMU的瞬间,初始页表开始生效,此时只有被初始页表映射的物理地址才能被正常访问。而完整的物理地址映射(尤其是线性映射)是在内核启动的后续阶段完成的,并不是在启动阶段一开始就全部映射的。 因此,正常物理地址在启动阶段(早期)并不会全部进行映射,而是逐步建立映射的。 </think>在 Linux 内核启动阶段,**并非所有物理地址都会自动进行映射**。这是一个关键特性,理解这一点对掌握内核启动机制至关重要: ### 1. **启动阶段映射的基本原则** - **初始映射有限性**:启动阶段(MMU 开启前)建立的页表仅包含**内核运行必需的最小物理地址映射**: - 内核代码段(`.text`) - 内核数据段(`.data`、`.bss`) - 初始堆栈空间 - 设备树(DTB)等关键数据结构 $$ \text{初始映射范围} \subseteq \text{完整物理地址空间} $$ 如图示(简化): ``` 物理地址空间: [0x00000000 - 0xFFFFFFFF] 初始映射区域: [0x80000000 - 0x81000000] // 仅映射内核心区域[^4] ``` - **映射必要性**:映射仅针对**当前执行流程直接访问的内存区域**。未映射的物理地址(如未初始化的设备寄存器或预留内存)在启动早期无法通过虚拟地址访问[^4]。 ### 2. **映射建立的动态过程** | 启动阶段 | 映射状态 | 关键技术机制 | |----------------------|----------------------------------|---------------------| | **MMU 关闭阶段** | 直接使用物理地址 | 无映射(纯物理寻址) | | **初始页表建立** | 仅映射内核心区域 | `early_pgtable` | | **MMU 开启瞬间** | 初始映射生效 | `TTBRx` 寄存器加载 | | **`start_kernel()`** | 扩展线性映射到所有低端内存 | `paging_init()` | | **完整初始化后** | 动态映射外设/高端内存 | `ioremap()`/`vmalloc()` | 关键点: - **线性映射延迟建立**:低端内存(Low Memory)的完整线性映射(如 `phys_to_virt` 依赖的 `PAGE_OFFSET` 区域)在 `paging_init()` 函数中完成,**早于大部分驱动初始化**但晚于内核心启动[^4]。 - **外设地址需动态映射**:设备寄存器等物理地址**不会自动映射**,需驱动通过 `ioremap()` 显式建立虚拟映射后才能访问[^1]。 ### 3. **未映射场景的典型案例** - **外设寄存器**:如 UART 控制器地址 `0x3F201000` 在启动早期无映射,需串口驱动调用: ```c void __iomem *uart_base = ioremap(0x3F201000, SZ_4K); ``` - **预留内存区域**:通过 `memreserve` 在设备树声明的保留内存不会被初始页表映射[^2]。 - **高端内存(HighMem)**:物理地址超过线性映射区的内存需动态映射[^4]。 ### 4. **映射验证与调试** 可通过内核启动参数 `earlycon` 验证早期映射: ```bash # 要求建立指定 UART 的早期映射 earlycon=uart8250,mmio,0x3F201000 ``` 若地址未映射,则输出失败,证明该物理地址未被初始页表覆盖[^4]。 ### 结论 正常物理地址在启动阶段**不会全部自动映射**。内核仅确保自身运行所需的心区域有初始映射,其他物理地址需通过以下方式建立映射: 1. **线性映射扩展**:通过 `paging_init()` 建立完整低端内存映射 2. **动态映射**:驱动使用 `ioremap()` 建立外设寄存器映射 3. **用户空间映射**:通过 `mmap()` 建立用户进程映射[^2] 此设计平衡了启动效率和灵活性,避免不必要的映射开销[^1][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值