鸿蒙内核源码分析(内存概念篇)|解读鸿蒙源码

原创
09/25 11:53
阅读数 8.9K

提示:本文基于开源鸿蒙内核分析,官方源码【kernel_liteos_a】官方文档【docs】参考文档【Huawei LiteOS
本文作者:鸿蒙内核发烧友,用生活场景讲故事的方式去解构内核,一窥究竟,让神秘的内核栩栩如生,浮现眼前。博文全部原创,持续更新,敬请关注。内容仅代表个人观点,错误之处,欢迎大家指正完善。本系列全部文章进入鸿蒙源码分析(总目录)查看


本文分析虚拟内存模块源码 详见:../kernel/base/vm

目录

最难讲的章节

为什么会懵逼?

内存的概念也是一样的

为什么要有MMU?

它干了一件什么事呢?

先把vm模块下的文件和功能列出来

物理内存按段页式管理

 映射的最小单位(粒度)LosVmPage

 映射的规则map相关结构体

 LosVmSpace(也叫虚拟内存空间)

VmMapRegion(线性区描述符)


最难讲的章节

        坦白讲内存是整个系列里面最难讲的一章,因为概念太多了,笔者写之前查遍了网上资料,发现很多是没有搞清楚就不负责的乱转乱发,越看越晕,脑子里一团浆糊,本来并没那么复杂的概念被复杂化了。笔者坚持原创,不滥竽充数,为自己写和引用的每一个字负责,发布的每篇帖子后续都会反复的修改,查缺补漏,力求描述精准又通俗易懂。喜欢的请收藏。鸿蒙内核知识点非常多,想尽快成体系的整理出来,一来是能加深自己的理解,二来是希望更多人对鸿蒙感兴趣,少走弯路。原创很辛苦,但兴趣使然,值得。本篇重新梳理下内存的概念后再来讲内存。

为什么会懵逼?

        会懵逼究其原因,笔者认为关键问题出在概念的分层上,每个层面都有自己的概念,层面搞混了整个就容易乱。还记得系列篇中说 task/线程吗,也是这样的问题,再来回顾一下。对于上层应用比如 java开发者, 接触的是进程和线程的概念,他们会说的new一个线程,task是什么不用去关心。而对于内核层说的就是task 而不是 thread, 但广义上他们是一个意思的,就是程序运行的载体,在系列篇中有详细介绍。其实很多上层开发者不知道main()就是个线程,只是不需要大家去new的而已,因为到了内核层,创建进程的时候会自动创建一个task,入口函数就是main()函数,而大家在程序里new出来的thread到了运行期间也会在内核层创建task,入口函数就是大家熟知的run() 。

所以对内核层来说,只有task 的概念,没有什么线程的说法,也没有什么应用程序的main(),run(),只有入口函数。

typedef VOID *(*TSK_ENTRY_FUNC)(UINTPTR param1,UINTPTR param2,UINTPTR param3,UINTPTR param4);//*kfy task入口函数,切换task的回调

到了CPU层就更简单,连task都没有了, 只有CPU各寄存器值的变化,你给程序计数器PC(ARM的说法)或指令指针寄存器IP(intel的说法)赋予什么值它就运行那个地址的指令,取指令->译码->执行,就这三个动作来回整。 其实应用层不知道task也不妨碍成为一个优秀的工程师,因为设计者的本意就是要分层,让你对底层的概念透明,不要混在一起理解。可树欲静而风不止,你在网上总能看到 线程和task混在一起说和问,并拿来比较的文章和答案。这就有点像关公战秦琼,如果你不了解这是两个世界的人就会一直有个心结在那。

内存的概念也是一样的

对应用层我们会说 逻辑地址,区(代码区,数据区,栈区,堆区)的概念,在这一层我们会知道编译好的代码会放在代码区,全局变量放在数据区,局部变量和代码执行在栈中运行,分配的动态内存是由堆区提供的,至于怎么提供怎么放的,我完全可以不清楚,那是底层的事情。如果笔者告诉你,假如你new出来的10M内存,只用了5M,剩下的5M其实并不属于你,一直是其他进程或线程在使用,你相信吗?什么是逻辑地址,就是按使用人的逻辑来处理的地址,是应用开发者用到能看到的地址,真的物理地址你是看不到的,也不需要看到。

对内核层我们要说 虚拟地址,物理地址,线性地址,映射,段,页表,缺页中断,红黑树,LRU算法这些概念,而不会去说什么逻辑地址。但逻辑地址和虚拟地址其实是一个意思。物理地址是真实的内存地址,有多大的内存就有多少物理地址,那为什么要整出个虚拟地址来,和物理地址是啥关系?答案是:如果没有一个叫MMU(内存管理单元)的东西出现,其实他们也是一样的,什么映射,段,页,页表,缺页中断这些东东根本就不会出现,但因有了它一切才变复杂了,MMU是晕头转向的源头。用它一定有道理,是什么原因必须要用它呢

为什么要有MMU?

因为多进程/多线程。多进程是刚需,老百姓的要求啊,五彩斑斓的世界需要我们在电脑上秒开运行,一台电脑开10个程序是正常的事吧,聊QQ,玩游戏,听音乐,看直播,你的网络世界贼丰富的啊,还得不卡顿,加上系统本身也要开销,想过这得要多大的内存吗?而你只有4G内存,内存的增长速度和老百姓对网络世界日益提高的要求已经成为内核界的主要矛盾啦,永远不够用,那怎么办?MMU就是来解决这个问题的。如果没有MMU你的丰富体验可能需要16G内存,是它让你只有4G的内存能体验到16G的感觉。

它干了一件什么事呢?

把一些进程/线程暂时没用到的部分程序数据外调到硬盘上去,需要用时再调回内存中来,让每个程序都以为自己独享了这4G内存,那调来调去的怎么管理?调哪些不调哪些怎么决策?又如何保证不会调错?怎么更安全,效率更高?就必然会引入一些概念出来 映射,段,页,页表就是因它而生的,所以正因为支持MMU才会有虚拟内存的概念。MMU:它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制。通过它的一番操作,把物理空间成倍成倍的放大,他们之间的映射关系存放在页面中。

 

先把vm模块下的文件和功能列出来

MMU是负责把虚拟地址映射为物理地址,但凡"映射"必须要解决两个问题:映射的最小单位(粒度)和映射的规则

los_vm_phys.c 物理地址相关的结构体和函数,段是只属于物理地址的,所以定义出现在 los_vm_phs.h中
los_vm_page.c 页是最关键的,映射的最小单位,相当于虚拟地址和物理地址的中间人,是物理地址<--->虚拟地址映射的数据结构基础。
los_vm_map.c 物理地址<--->虚拟地址的映射的规则
los_vm_filemap.c 物理内存和文件系统的映射实现
los_vm_iomap.c 物理内存和io设备的映射实现
los_vm_fault.c 缺页中断,映射失败的处理
los_vm_syscall.c 给上层应用使用虚拟内存的系列函数,官方对内存提供的资料少的可怜,只有这个文件的。
los_arch_mmu.c 这个文件不在vm模块,因为mmu是块芯片,跟着硬件走,代码在 arch模块中

有了上面的概念梳理和文件的脉络理解,我们才能去说具体的代码实现了。教大家一个看源码的方法,通过结构体看源码,结构体出现在哪个文件肯定就属于哪个概念,然后看他怎么会外部使用的,顺着调用关系就能理解整体的脉络。

物理内存按段页式管理

los_vm_phys.h //*kyf 物理地址的管理是用 段页式

#define VM_PHYS_SEG_MAX    32
typedef struct VmPhysSeg {
    PADDR_T start;            /* The start of physical memory area */
    size_t size;              /* The size of physical memory area */
    LosVmPage *pageBase;      /* The first page address of this area */

    SPIN_LOCK_S freeListLock; /* The buddy list spinlock */
    struct VmFreeList freeList[VM_LIST_ORDER_MAX];  /* The free pages in the buddy list */

    SPIN_LOCK_S lruLock;
    size_t lruSize[VM_NR_LRU_LISTS];
    LOS_DL_LIST lruList[VM_NR_LRU_LISTS];
} LosVmPhysSeg;

struct VmPhysArea {
    PADDR_T start;
    size_t size;
};

extern struct VmPhysSeg g_vmPhysSeg[VM_PHYS_SEG_MAX]; //*kyf 把物理地址分成了32段
extern INT32 g_vmPhysSegNum;

 鸿蒙内核对物理地址采用了段页式管理,段是说物理地址才使用的概念。

 映射的最小单位(粒度)LosVmPage

page.c的代码很少,其实就一个函数对物理地址的page化,和段(seg)绑定,我们都贴出来,一一加了注释,

los_vm_page.h //*kyf 页是物理地址和虚拟地址都说的概念,在物理地址上叫物理页或者页框(page frame)
//在虚拟地址空间叫 虚拟页面或者叫页面(page),鸿蒙内核一个page和page frame都是 4K 
typedef struct VmPage {
    LOS_DL_LIST         node;        /**< vm object dl list */
    UINT32              index;       /**< vm page index to vm object */
    PADDR_T             physAddr;    /**< vm page physical addr */
    Atomic              refCounts;   /**< vm page ref count */
    UINT32              flags;       /**< vm page flags */
    UINT8               order;       /**< vm page in which order list */
    UINT8               segID;       /**< the segment id of vm page */
    UINT16              nPages;      /**< the vm page is used for kernel heap */
} LosVmPage;

extern LosVmPage *g_vmPageArray;
extern size_t g_vmPageArraySize;

LosVmPage *LOS_VmPageGet(PADDR_T paddr);//*kyf 通过物理地址获取page
VOID OsVmPageStartup(VOID);//*kyf page初始化

//**kyf page初始化
VOID OsVmPageStartup(VOID)
{
    struct VmPhysSeg *seg = NULL;
    LosVmPage *page = NULL;
    paddr_t pa;
    UINT32 nPage;
    INT32 segID;

    OsVmPhysAreaSizeAdjust(ROUNDUP((g_vmBootMemBase - KERNEL_ASPACE_BASE), PAGE_SIZE));//*kfy 物理内存全部切成4K的物理页,用g_physArea保存

    nPage = OsVmPhysPageNumGet();
    g_vmPageArraySize = nPage * sizeof(LosVmPage);
    g_vmPageArray = (LosVmPage *)OsVmBootMemAlloc(g_vmPageArraySize);

    OsVmPhysAreaSizeAdjust(ROUNDUP(g_vmPageArraySize, PAGE_SIZE));//*kyf 页面头设置为LosVmPage,这段代码很妙

    OsVmPhysSegAdd();//*kyf 段页绑定
    OsVmPhysInit();//*kyf 加入空闲链表和设置置换算法,,LRU(最近最久未使用)算法

    for (segID = 0; segID < g_vmPhysSegNum; segID++) {
        seg = &g_vmPhysSeg[segID];
        nPage = seg->size >> PAGE_SHIFT;
        for (page = seg->pageBase, pa = seg->start; page <= seg->pageBase + nPage;
             page++, pa += PAGE_SIZE) {
            OsVmPageInit(page, pa, segID);//*kfy page初始化
        }
        OsVmPageOrderListInit(seg->pageBase, nPage);//*kyf 页面回收后的排序
    }
}

 映射的规则map相关结构体

typedef struct VmMapRange {
    VADDR_T             base;           /**< vm region base addr */
    UINT32              size;           /**< vm region size */
} LosVmMapRange;

struct VmMapRegion;
typedef struct VmMapRegion LosVmMapRegion;
struct VmFileOps;
typedef struct VmFileOps LosVmFileOps;
struct VmSpace;
typedef struct VmSpace LosVmSpace;

typedef struct VmFault {
    UINT32          flags;              /* FAULT_FLAG_xxx flags */
    unsigned long   pgoff;              /* Logical page offset based on region */
    VADDR_T         vaddr;              /* Faulting virtual address */
    VADDR_T         *pageKVaddr;        /* KVaddr of pagefault's vm page's paddr */
} LosVmPgFault;

struct VmFileOps {
    void (*open)(struct VmMapRegion *region);
    void (*close)(struct VmMapRegion *region);
    int  (*fault)(struct VmMapRegion *region, LosVmPgFault *pageFault);
    void (*remove)(struct VmMapRegion *region, LosArchMmu *archMmu, VM_OFFSET_T offset);
};

struct VmMapRegion {
    LosRbNode           rbNode;         /**< region red-black tree node */
    LosVmSpace          *space;
    LOS_DL_LIST         node;           /**< region dl list */
    LosVmMapRange       range;          /**< region address range */
    VM_OFFSET_T         pgOff;          /**< region page offset to file */
    UINT32              regionFlags;   /**< region flags: cow, user_wired */
    UINT32              shmid;          /**< shmid about shared region */
    UINT8               protectFlags;   /**< vm region protect flags: PROT_READ, PROT_WRITE, */
    UINT8               forkFlags;      /**< vm space fork flags: COPY, ZERO, */
    UINT8               regionType;     /**< vm region type: ANON, FILE, DEV */
    union {
        struct VmRegionFile {
            unsigned int fileMagic;
            struct file *file;
            const LosVmFileOps *vmFOps;
        } rf;
        struct VmRegionAnon {
            LOS_DL_LIST  node;          /**< region LosVmPage list */
        } ra;
        struct VmRegionDev {
            LOS_DL_LIST  node;          /**< region LosVmPage list */
            const LosVmFileOps *vmFOps;
        } rd;
    } unTypeData;
};

typedef struct VmSpace {
    LOS_DL_LIST         node;           /**< vm space dl list */
    LOS_DL_LIST         regions;        /**< region dl list */
    LosRbTree           regionRbTree;   /**< region red-black tree root */
    LosMux              regionMux;      /**< region list mutex lock */
    VADDR_T             base;           /**< vm space base addr */
    UINT32              size;           /**< vm space size */
    VADDR_T             heapBase;       /**< vm space heap base address */
    VADDR_T             heapNow;        /**< vm space heap base now */
    LosVmMapRegion      *heap;          /**< heap region */
    VADDR_T             mapBase;        /**< vm space mapping area base */
    UINT32              mapSize;        /**< vm space mapping area size */
    LosArchMmu          archMmu;        /**< vm mapping physical memory */
#ifdef LOSCFG_DRIVERS_TZDRIVER
    VADDR_T             codeStart;      /**< user process code area start */
    VADDR_T             codeEnd;        /**< user process code area end */
#endif
} LosVmSpace;
typedef struct OsMux {
    UINT32 magic;        /**< magic number */
    LosMuxAttr attr;     /**< Mutex attribute */
    LOS_DL_LIST holdList; /**< The task holding the lock change */
    LOS_DL_LIST muxList; /**< Mutex linked list */
    VOID *owner;         /**< The current thread that is locking a mutex */
    UINT16 muxCount;     /**< Times of locking a mutex */
} LosMux;

 LosVmSpace(也叫虚拟内存空间)

被进程使用的内存叫进程内存描述符,虚拟内存空间有多个虚拟存储区域(region),Linux内核中对这些虚拟存储区域的组织方式有两种,一种是采用双循环链表(regions),还有一种是采用树的结构。Linux内核从2.4.10开始,Linux内核对虚拟区的组织不再采用一般平衡二叉树,而是采用红黑树(regionRbTree),这是出于效率的考虑,就是增删改查更快了。node会加到全局链表中,曾有人私信笔者LOS_DL_LIST里面只有两个指针数据去哪了?答案是:谁用它谁就是数据。 链表把所有进程拉进大循环,还记得鸿蒙内核进程池的大小吗?默认64个,另外就是堆栈空间等信息。这里大概说这么多,后续还会拆开细讲。

VmMapRegion(线性区描述符)

还记得笔者上面说的new了10M 用了5M的问题吗?什么叫线性区:当用户态进程请求动态内存时,并没有获得请求的页框(指物理地址),而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。这一区间叫做“线性区(memory region)” 。该结构体描述了 protectFlags(权限),LosVmMapRange(范围),线性区的类型(regionType)。映射类型(unTypeData):按文件映射,匿名映射,特殊设备映射,这是个联合体。

本篇先说这些结构体,下篇进入具体的映射过程。更多文章去 鸿蒙源码分析(总目录)查看,支持原创,喜欢的记得点赞关注。

展开阅读全文
打赏
5
3 收藏
分享
加载中
更多评论
打赏
0 评论
3 收藏
5
分享
返回顶部
顶部