提示:本文基于开源鸿蒙内核分析,官方源码【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其实并不属于你,一直是别的进程或线程还在使用,你相信吗?什么是逻辑地址,就是按使用人的逻辑来处理的地址,是应用开发者用到能看到的地址,物理地址你是查不到的。
对内核层我们要说 虚拟地址,物理地址,线性地址,映射,段,页表,缺页中断,红黑树这些概念,而不会去说什么逻辑地址。但逻辑地址和虚拟地址其实是一个意思。物理地址是真实的内存地址,有多大的内存就有多少物理地址,那为什么要整出个虚拟地址来,和物理地址是啥关系?答案是:如果没有一个叫MMU(内存管理单元)的东西出现,其实他们也是一样的,什么映射,段,页,页表,缺页中断这些东东根本就不会出现,但因有了它一切才变复杂了,MMU是晕头转向的源头。用它一定有道理,是什么原因必须要用它。
因为多进程/多线程。多进程是刚需,老百姓的要求啊,五彩斑斓的世界需要我们在电脑上秒开运行,一台电脑开10个程序是正常的事吧,聊QQ,玩游戏,听音乐,看直播,你的网络世界贼丰富的啊,还得不卡顿,加上系统本身也要开销,想过这得要多大的内存吗?而你只有4G内存,内存的增长速度和老百姓对网络世界日益提高的要求已经成为内核界的主要矛盾啦,永远不够用,那怎么办?MMU就是来解决这个问题的。如果没有MMU你的丰富体验可能需要16G内存,是它让你只有4G的内存能体验到16G的感觉。
把你暂时没用到的一些程序数据外调到硬盘上去,需要用到时再调回内存中来,让每个程序都以为自己独享了这4G内存,调来调去的怎么管理?调哪些不调哪些怎么决策?有如何保证不会调错?怎么更安全,效率更高?就必然会引入一些概念出来 映射,段,页,页表就是因它而生的,所以正因为支持MMU才会有虚拟内存的概念。MMU:它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件。它的功能包括虚拟地址到物理地址的转换(即虚拟内存管理)、内存保护、中央处理器高速缓存的控制。通过它的一番操作,把物理空间成倍成倍的放大,他们之间的映射关系存放在页面中。
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_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;
鸿蒙内核对物理地址采用了段页式管理,段是说物理地址才使用的概念。
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 加入空闲链表和设置置换算法
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 页面回收后的排序
}
}
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;
被进程使用的内存叫进程内存描述符,虚拟内存空间有多个虚拟存储区域(region),Linux内核中对这些虚拟存储区域的组织方式有两种,一种是采用双循环链表(regions),还有一种是采用树的结构。Linux内核从2.4.10开始,Linux内核对虚拟区的组织不再采用一般平衡二叉树,而是采用红黑树(regionRbTree),这是出于效率的考虑,就是增删改查更快了。node会加到全局链表中,曾有人私信笔者LOS_DL_LIST里面只有两个指针数据去哪了?答案是:谁用它谁就是数据。 链表把所有进程拉进大循环,还记得鸿蒙内核进程池的大小吗?默认64个,另外就是堆栈空间等信息。这里大概说这么多,后续还会拆开细讲。
还记得笔者上面说的new10M 用了5M的问题吗?什么叫线性区:当用户态进程请求动态内存时,并没有获得请求的页框(指物理地址),而仅仅获得对一个新的线性地址区间的使用权,而这一线性地址区间就成为进程地址空间的一部分。这一区间叫做“线性区(memory region)” 。该结构体描述了 protectFlags(权限),LosVmMapRange(范围),线性区的类型(regionType)。映射类型(unTypeData):按文件映射,匿名映射,特殊设备映射,这是个联合体。
本篇先说这些结构体,下篇进入具体的映射过程,也就差它没讲了。
更多文章去 鸿蒙源码分析(总目录)查看