0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

鸿蒙内核源码分析:物理地址的映射

鸿蒙系统HarmonyOS 来源:oschina 作者:深入研究鸿蒙 2020-11-03 10:28 次阅读

MMU的本质

虚拟地址(VA):就是线性地址,鸿蒙内存部分全是VA的身影,是由编译器和链接器在定位程序时分配的,每个应用程序都使用相同的虚拟内存地址空间,而这些虚拟内存地址空间实际上分别映射到不同的实际物理内存空间上。CPU只知道虚拟地址,向虚拟地址要数据,但在其保护模式下很悲催地址信号在路上被MMU拦截了,MMU把虚拟地址换成了物理地址,从而拿到了真正的数据。

物理地址(PA):程序的指令和常量数据,全局变量数据以及运行时动态申请内存所分配的实际物理内存存放位置。

MMU采用页表(page table)来实现虚实地址转换,页表项除了描述虚拟页到物理页直接的转换外,还提供了页的访问权限(读,写,可执行)和存储属性。MMU的本质是拿虚拟地址的高位(20位)做文章,低12位是页内偏移地址不会变。也就是说虚拟地址和物理地址的低12位是一样的,本篇详细讲述MMU是如何变戏法的。

MMU是通过两级页表结构:L1和L2来实现映射功能的,鸿蒙内核当然也实现了这两级页表转换的实现。本篇是系列篇关于内存部分最满意的一篇,也是最不好理解的一篇,强烈建议结合源码看,鸿蒙内核源码注释中文版 【 Gitee仓|CSDN仓|Github仓|Coding仓 】内存部分的注释已经基本完成 .

一级页表L1

L1页表将全部的4G地址空间划分为4096个1M的节,页表中每一项(页表项)32位,其内容是L2页表基地址或某个1M物理内存的基地址。虚拟地址的高12位用于对页表项定位,也就是4096个页面项的索引,L1页表的基地址,也叫转换表基地址,存放在CP15的C2(TTB)寄存器中,鸿蒙内核源码分析(内存汇编篇)中有详细的描述,自行翻看。

L1页表项有三种描述格式,鸿蒙源码如下。

/* L1 descriptor type */
#define MMU_DESCRIPTOR_L1_TYPE_INVALID                          (0x0 << 0)
#define MMU_DESCRIPTOR_L1_TYPE_PAGE_TABLE                       (0x1 << 0)
#define MMU_DESCRIPTOR_L1_TYPE_SECTION                          (0x2 << 0)
#define MMU_DESCRIPTOR_L1_TYPE_MASK                             (0x3 << 0)

第一种:Fault(INVALID)页表项,表示对应虚拟地址未被映射,访问将产生一个数据中止异常。

第二种:PAGE_TABLE页表项,指向L2页表的页表项,意思就是把1M分成更多的页(256*4K)

第三种:SECTION页表项 ,指向1M节的页表项

页表项的最低二位[1:0],用于定义页表项的类型,section页表项对应1M的节,直接使用页表项的最高12位替代虚拟地址的高12位即可得到物理地址。还是直接看鸿蒙源码来的清晰,每一行都加了详细的注释。

LOS_ArchMmuQuery通过虚拟地址查询物理地址和flags

//通过虚拟地址查询物理地址
STATUS_T LOS_ArchMmuQuery(const LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T *paddr, UINT32 *flags)
{//archMmu->virtTtb:转换表基地址
    PTE_T l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);//获取PTE vaddr右移20位 得到L1描述子地址
    PTE_T l2Entry;
    PTE_T* l2Base = NULL;

    if (OsIsPte1Invalid(l1Entry)) {//判断L1描述子地址是否有效
        return LOS_ERRNO_VM_NOT_FOUND;//无效返回虚拟地址未查询到
    } else if (OsIsPte1Section(l1Entry)) {// section页表项: l1Entry低二位是否为 10
        if (paddr != NULL) {//物理地址 = 节基地址(section页表项的高12位) + 虚拟地址低20位
            *paddr = MMU_DESCRIPTOR_L1_SECTION_ADDR(l1Entry) + (vaddr & (MMU_DESCRIPTOR_L1_SMALL_SIZE - 1));
        }

        if (flags != NULL) {
            OsCvtSecAttsToFlags(l1Entry, flags);//获取虚拟内存的flag信息
        }
    } else if (OsIsPte1PageTable(l1Entry)) {//PAGE_TABLE页表项: l1Entry低二位是否为 01
        l2Base = OsGetPte2BasePtr(l1Entry);//获取L2转换表基地址
        if (l2Base == NULL) {
            return LOS_ERRNO_VM_NOT_FOUND;
        }
        l2Entry = OsGetPte2(l2Base, vaddr);//获取L2描述子地址
        if (OsIsPte2SmallPage(l2Entry) || OsIsPte2SmallPageXN(l2Entry)) {
            if (paddr != NULL) {//物理地址 = 小页基地址(L2页表项的高20位) + 虚拟地址低12位
                *paddr = MMU_DESCRIPTOR_L2_SMALL_PAGE_ADDR(l2Entry) + (vaddr & (MMU_DESCRIPTOR_L2_SMALL_SIZE - 1));
            }

            if (flags != NULL) {
                OsCvtPte2AttsToFlags(l1Entry, l2Entry, flags);//获取虚拟内存的flag信息
            }
        } else if (OsIsPte2LargePage(l2Entry)) {//鸿蒙目前暂不支持64K大页,未来手机版应该会支持。
            LOS_Panic("%s %d, large page unimplemented
", __FUNCTION__, __LINE__);
        } else {
            return LOS_ERRNO_VM_NOT_FOUND;
        }
    }

    return LOS_OK;
}

这是鸿蒙内核对地址使用最频繁的功能,通过虚拟地址得到物理地址和flag信息,看下哪些地方会调用到它。

二级页表L2

L1页表项表示1M的地址范围,L2把1M分成更多的小页,鸿蒙内核 一页按4K算,所以被分成 256个小页。

L2页表中包含256个页表项,每个32位(4个字节),L2页表需要 256*4 = 1K的空间,必须按1K对齐,每个L2页表项将4K的虚拟内存地址转换为物理地址,每个L2页面项都给出了一个4K的页基地址。

L2页表项有三种格式:

/* L2 descriptor type */
#define MMU_DESCRIPTOR_L2_TYPE_INVALID                          (0x0 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_LARGE_PAGE                       (0x1 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE                       (0x2 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_SMALL_PAGE_XN                    (0x3 << 0)
#define MMU_DESCRIPTOR_L2_TYPE_MASK                             (0x3 << 0)

第一种:Fault(INVALID)页表项,表示对应虚拟地址未被映射,访问将产生一个数据中止异常。

第二种:大页表项,包含一个指向64K页的指针,但鸿蒙内核并没有实现大页表的支持,给出了未实现的提示

if (OsIsPte2LargePage(l2Entry)) {
            LOS_Panic("%s %d, large page unimplemented
", __FUNCTION__, __LINE__);
        } 

第三种:小页表项,包含一个指向4K页的指针。

映射初始化的过程

先看调用和被调用的关系

//启动映射初始化
VOID OsInitMappingStartUp(VOID)
{
    OsArmInvalidateTlbBarrier();//使TLB失效

    OsSwitchTmpTTB();//切换到临时TTB

    OsSetKSectionAttr();//设置内核段(text,rodata,bss)映射

    OsArchMmuInitPerCPU();//初始化CPU与mmu相关信息
}

干脆利落,调用了四个函数,其中三个在鸿蒙内核源码分析(内存汇编篇)有涉及,不展开讲,这里说OsSetKSectionAttr

它实现了内核空间各个区的映射,内核本身也是程序,鸿蒙把内核空间在物理内存上就独立开来了,也就是说在物理内存上有一段区域是只给内核空间享用的,从根上就把内核和APP 空间隔离了,里面放的是内核的重要数据(包括代码,常量和全局变量),具体看代码,代码很长,整个函数全贴出来了,都加上了注释。

OsSetKSectionAttr内核空间的设置和映射

typedef struct ArchMmuInitMapping {
    PADDR_T phys;//物理地址
    VADDR_T virt;//虚拟地址
    size_t  size;//大小
    unsigned int flags;//标识 读/写/.. VM_MAP_REGION_FLAG_PERM_*
    const char *name;//名称
} LosArchMmuInitMapping;

VADDR_T *OsGFirstTableGet()
{
    return (VADDR_T *)g_firstPageTable;//UINT8 g_firstPageTable[MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS]
}
//设置内核空间段属性,可看出内核空间是固定映射到物理地址
STATIC VOID OsSetKSectionAttr(VOID)
{
    /* every section should be page aligned */
    UINTPTR textStart = (UINTPTR)&__text_start;//代码段开始位置
    UINTPTR textEnd = (UINTPTR)&__text_end;//代码段结束位置
    UINTPTR rodataStart = (UINTPTR)&__rodata_start;//常量只读段开始位置
    UINTPTR rodataEnd = (UINTPTR)&__rodata_end;//常量只读段结束位置
    UINTPTR ramDataStart = (UINTPTR)&__ram_data_start;//全局变量段开始位置
    UINTPTR bssEnd = (UINTPTR)&__bss_end;//bss结束位置
    UINT32 bssEndBoundary = ROUNDUP(bssEnd, MB);
    LosArchMmuInitMapping mmuKernelMappings[] = {
        {
            .phys = SYS_MEM_BASE + textStart - KERNEL_VMM_BASE,//映射物理内存位置
            .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 - KERNEL_VMM_BASE,//映射物理内存位置
            .virt = rodataStart,//内核常量区
            .size = ROUNDUP(rodataEnd - rodataStart, MMU_DESCRIPTOR_L2_SMALL_SIZE),//4K对齐
            .flags = VM_MAP_REGION_FLAG_PERM_READ,//常量段只读
            .name = "kernel_rodata"
        },
        {
            .phys = SYS_MEM_BASE + ramDataStart - KERNEL_VMM_BASE,//映射物理内存位置
            .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;
    paddr_t oldTtPhyBase;
    int i;
    LosArchMmuInitMapping *kernelMap = NULL;//内核映射
    UINT32 kmallocLength;

    /* use second-level mapping of default READ and WRITE */
    kSpace->archMmu.virtTtb = (PTE_T *)g_firstPageTable;//__attribute__((section(".bss.prebss.translation_table"))) UINT8 g_firstPageTable[MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS];
    kSpace->archMmu.physTtb = LOS_PaddrQuery(kSpace->archMmu.virtTtb);//通过TTB虚拟地址查询TTB物理地址
    status = LOS_ArchMmuUnmap(&kSpace->archMmu, KERNEL_VMM_BASE,
                               (bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT);//解绑 bssEndBoundary - KERNEL_VMM_BASE 映射
    if (status != ((bssEndBoundary - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {//解绑失败
        VM_ERR("unmap failed, status: %d", status);
        return;
    }
	//映射 textStart - KERNEL_VMM_BASE 区
    status = LOS_ArchMmuMap(&kSpace->archMmu, KERNEL_VMM_BASE, SYS_MEM_BASE,
                             (textStart - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
                             VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE |
                             VM_MAP_REGION_FLAG_PERM_EXECUTE);
    if (status != ((textStart - KERNEL_VMM_BASE) >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
        VM_ERR("mmap failed, status: %d", status);
        return;
    }

    length = sizeof(mmuKernelMappings) / sizeof(LosArchMmuInitMapping);
    for (i = 0; i < length; i++) {//对mmuKernelMappings一一映射好
        kernelMap = &mmuKernelMappings[i];
        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 = KERNEL_VMM_BASE + SYS_MEM_SIZE_DEFAULT - bssEndBoundary;
    status = LOS_ArchMmuMap(&kSpace->archMmu, bssEndBoundary,
                             SYS_MEM_BASE + bssEndBoundary - KERNEL_VMM_BASE,
                             kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT,
                             VM_MAP_REGION_FLAG_PERM_READ | VM_MAP_REGION_FLAG_PERM_WRITE);
    if (status != (kmallocLength >> MMU_DESCRIPTOR_L2_SMALL_SHIFT)) {
        VM_ERR("unmap failed, status: %d", status);
        return;
    }
    LOS_VmSpaceReserve(kSpace, kmallocLength, bssEndBoundary);

    /* we need free tmp ttbase */
    oldTtPhyBase = OsArmReadTtbr0();//读取TTB值
    oldTtPhyBase = oldTtPhyBase & MMU_DESCRIPTOR_L2_SMALL_FRAME;
    OsArmWriteTtbr0(kSpace->archMmu.physTtb | MMU_TTBRx_FLAGS);//内核页表基地址写入CP15 c2(TTB寄存器)
    ISB;

    /* we changed page table entry, so we need to clean TLB here */
    OsCleanTLB();//清空TLB缓冲区

    (VOID)LOS_MemFree(m_aucSysMem0, (VOID *)(UINTPTR)(oldTtPhyBase - SYS_MEM_BASE + KERNEL_VMM_BASE));//释放内存池
}

LOS_ArchMmuMap生成L1,L2页表项,实现映射的过程

mmu的map 就是生成L1,L2页表项的过程,以供虚实地址的转换使用,还是直接看代码吧,代码说明一切!

//所谓的 map 就是 生成L1,L2页表项的过程
status_t LOS_ArchMmuMap(LosArchMmu *archMmu, VADDR_T vaddr, PADDR_T paddr, size_t count, UINT32 flags)
{
    PTE_T l1Entry;
    UINT32 saveCounts = 0;
    INT32 mapped = 0;
    INT32 checkRst;

    checkRst = OsMapParamCheck(flags, vaddr, paddr);//检查参数
    if (checkRst < 0) {
        return checkRst;
    }

    /* see what kind of mapping we can use */
    while (count > 0) {
        if (MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(vaddr) && //虚拟地址和物理地址对齐 0x100000(1M)时采用
            MMU_DESCRIPTOR_IS_L1_SIZE_ALIGNED(paddr) && //section页表项格式
            count >= MMU_DESCRIPTOR_L2_NUMBERS_PER_L1) { //MMU_DESCRIPTOR_L2_NUMBERS_PER_L1 = 0x100 
            /* compute the arch flags for L1 sections cache, r ,w ,x, domain and type */
            saveCounts = OsMapSection(archMmu, flags, &vaddr, &paddr, &count);//生成L1 section类型页表项并保存
        } else {
            /* have to use a L2 mapping, we only allocate 4KB for L1, support 0 ~ 1GB */
            l1Entry = OsGetPte1(archMmu->virtTtb, vaddr);//获取L1页面项
            if (OsIsPte1Invalid(l1Entry)) {//L1 fault页面项类型
                OsMapL1PTE(archMmu, &l1Entry, vaddr, flags);//生成L1 page table类型页表项并保存
                saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);//生成L2 页表项目并保存
            } else if (OsIsPte1PageTable(l1Entry)) {//L1 page table页面项类型
                saveCounts = OsMapL2PageContinous(l1Entry, flags, &vaddr, &paddr, &count);//生成L2 页表项目并保存
            } else {
                LOS_Panic("%s %d, unimplemented tt_entry %x
", __FUNCTION__, __LINE__, l1Entry);
            }
        }
        mapped += saveCounts;
    }

    return mapped;
}

STATIC UINT32 OsMapL2PageContinous(PTE_T pte1, UINT32 flags, VADDR_T *vaddr, PADDR_T *paddr, UINT32 *count)
{
    PTE_T *pte2BasePtr = NULL;
    UINT32 archFlags;
    UINT32 saveCounts;

    pte2BasePtr = OsGetPte2BasePtr(pte1);
    if (pte2BasePtr == NULL) {
        LOS_Panic("%s %d, pte1 %#x error
", __FUNCTION__, __LINE__, pte1);
    }

    /* compute the arch flags for L2 4K pages */
    archFlags = OsCvtPte2FlagsToAttrs(flags);
    saveCounts = OsSavePte2Continuous(pte2BasePtr, OsGetPte2Index(*vaddr), *paddr | archFlags, *count);
    *paddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
    *vaddr += (saveCounts << MMU_DESCRIPTOR_L2_SMALL_SHIFT);
    *count -= saveCounts;
    return saveCounts;
}

OsMapL2PageContinous没有加注释,希望你别太懒,赶紧动起来,到这里应该都能看懂了!最好能结合 鸿蒙内核源码分析(内存汇编篇)一起看理解会更深透。

编辑:hfy

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • MMU
    MMU
    +关注

    关注

    0

    文章

    91

    浏览量

    18244
  • 鸿蒙系统
    +关注

    关注

    183

    文章

    2633

    浏览量

    66119
收藏 人收藏

    评论

    相关推荐

    如何获取MAC地址?MAC地址的三个申请条件

    在产品设备都规范化管理的当今社会,如果您的产品设备想要投入市场,设备物理地址码MAC地址则是唯一能识别设备的识别码。那么MAC地址的申请条件有哪些?接下来请看英利检测的分享。提交必要信息申请人信息
    的头像 发表于 09-06 17:14 294次阅读
    如何获取MAC<b class='flag-5'>地址</b>?MAC<b class='flag-5'>地址</b>的三个申请条件

    欢创播报 华为宣布鸿蒙内核已超越Linux内核

    1 华为宣布鸿蒙内核已超越Linux内核   6月21日,在华为开发者大会上, HarmonyOS NEXT(鸿蒙NEXT)——真正独立于安卓和iOS的
    的头像 发表于 06-27 11:30 745次阅读

    AOSP源码定制-内核驱动编写

    有时候为了分析一些壳的检测,需要在内核层面对读写相关的操作进行监控,每次去修改对应的内核源码编译重刷过于耗时耗力,这里就来尝试编写一个内核
    的头像 发表于 04-23 11:15 1002次阅读
    AOSP<b class='flag-5'>源码</b>定制-<b class='flag-5'>内核</b>驱动编写

    Linux内核内存管理之内核非连续物理内存分配

    我们已经知道,最好将虚拟地址映射到连续页帧,从而更好地利用缓存并实现更低的平均内存访问时间。然而,如果对内存区域的请求并不频繁,那么考虑基于通过连续线性地址访问非连续页帧的分配方案是有意义的。该模式
    的头像 发表于 02-23 09:44 829次阅读
    Linux<b class='flag-5'>内核</b>内存管理之<b class='flag-5'>内核</b>非连续<b class='flag-5'>物理</b>内存分配

    鸿蒙内核源码分析:Newlib C

    使用 Musl C 库的时候,内核提供了基于 LOS_XXX 适配实现 pthread、mqeue、fs、semaphore、time 等模块的 posix 接口(//kernel/liteos_m
    的头像 发表于 02-18 15:41 599次阅读
    <b class='flag-5'>鸿蒙</b>轻<b class='flag-5'>内核</b><b class='flag-5'>源码</b><b class='flag-5'>分析</b>:Newlib C

    鸿蒙开发者预览版如何?

    Linux内核以及AOSP代码,采用的鸿蒙内核以及代码,HarmonyOS NEXT系统仅支持鸿蒙内核
    发表于 02-17 21:54

    获取Linux内核源码的方法

    关键功能,今天小编就给各位小伙伴介绍一下如何获取Linux内核源码。获取Linux内核源码的渠道Linux有一个庞大的开源社区,每个人都可以向开源社区提交代码。由
    的头像 发表于 12-13 09:49 601次阅读
    获取Linux<b class='flag-5'>内核</b><b class='flag-5'>源码</b>的方法

    如何绑定ip地址与mac物理地址呢?

    如何绑定ip地址与mac物理地址呢? 绑定IP地址与MAC物理地址是一种网络管理和安全措施,可以帮助确保网络设备的身份验证和安全性。在这篇文章中,我们将详细介绍如何进行IP
    的头像 发表于 12-07 09:33 6339次阅读

    MMU多级页表映射过程

    空间,也有相应的页表负责虚拟地址物理地址之间的转换。MMU查询的过程中,用户进程的一级页表的基址存放在TTBR0。操作系统的内核空间公用一块地址空间,MMU查询的过程中,
    的头像 发表于 11-26 16:28 900次阅读
    MMU多级页表<b class='flag-5'>映射</b>过程

    为什么要有TLB

    TLB 是 translation lookaside buffer 的简称。首先,我们知道 MMU 的作用是把虚拟地址转换成物理地址。 虚拟地址物理地址
    的头像 发表于 11-26 15:54 646次阅读
    为什么要有TLB

    四种获取内核函数地址的方法

    内核调试中,经常需要知道某个函数的地址,或者根据函数地址找到对应的函数,从而进行更深一步的debug。
    的头像 发表于 11-17 16:58 1196次阅读
    四种获取<b class='flag-5'>内核</b>函数<b class='flag-5'>地址</b>的方法

    linux系统查看物理地址

    在Linux系统中,访问和查看物理地址是一个非常重要的任务,因为它提供了对硬件设备的直接访问。本文将详细介绍如何在Linux系统中查看物理地址,包括不同的方法和工具,以及如何解释和使用这些地址。我们
    的头像 发表于 11-16 16:47 3237次阅读

    MAC地址注册的原理和应用

    MAC地址注册是指在网络设备中,将设备的物理地址(即MAC地址)与设备的IP地址进行关联和注册的过程。MAC地址是以太网卡硬件上的独特标识符
    的头像 发表于 11-13 16:07 931次阅读
    MAC<b class='flag-5'>地址</b>注册的原理和应用

    epoll源码分析

    个函数进行源码分析源码来源 由于epoll的实现内嵌在内核中,直接查看内核源码的话会有一些无
    的头像 发表于 11-13 11:49 949次阅读
    epoll<b class='flag-5'>源码</b><b class='flag-5'>分析</b>

    linux内存性能优化介绍

    不同;图示为 32 位和 64 位系统的虚拟地址空间; 内存映射是将虚拟内存地址映射物理内存地址
    的头像 发表于 11-10 15:23 652次阅读
    linux内存性能优化介绍