Liteos-a内核分析(三)

gewenbin
gewenbin
gewenbin
188
文章
15
评论
2020年12月19日20:27:03 评论 786

系统内存管理

本篇文章主要分析OsSysMemInit这个函数,主要初始化了内核虚拟空间、vmalloc虚拟空间、还有内核堆和系统物理页初始化等等。

1. TCB内存申请

在调用OsSysMemInit函数之前,其实已经有个函数里已经使用了内存申请类接口,这个函数就是OsTaskInit,这个函数中使用了LOS_MemAlloc来申请TCB内存池:

Liteos-a内核分析(三)

从代码中的注释也可以看出,TCB内存池是常驻内存的,也就是只要申请了就不会被free掉,除非整个系统重启。我们来看下LOS_MemAlloc的实现。

因为内存管理还没有初始化,所以m_aucSysMem0这个变量初始值是NULL,在LOS_MemAlloc函数中调用了OsVmBootMemAlloc:

Liteos-a内核分析(三)

OsVmBootMemAlloc函数很简单,其实就是返回g_vmBootMemBase这个变量的值,然后更新其值:

Liteos-a内核分析(三)

那么这个g_vmBootMemBase得初始值是多少呢,初始值就是__bss_end,也就是bss段的末尾,__bss_end这个值是内核在编译阶段确定的:

Liteos-a内核分析(三)

申请完TCB池之后,系统内存布局如下:

Liteos-a内核分析(三)

注意这里画的是虚拟地址的布局,由于这时物理地址和虚拟地址仅仅是一个数值偏移的关系,所以物理内存上的布局也和这个图一样,这里就不再展示。

2. 内核虚拟空间初始化

内核使用OsKernVmSpaceInit函数初始化内核虚拟空间数据结构:

Liteos-a内核分析(三)

在liteos_a中,使用LosVmSpace来描述一个虚拟空间的布局,每个LosVmSpace都有自己的页表。从图中可以看出,内核虚拟空间使用g_kVmSpace这个变量来表示,使用的页表是当前内核所使用的的段映射页表。

我们来看下OsKernVmSpaceInit这个函数,这个函数主要记录了虚拟空间的起始地址和大小:

Liteos-a内核分析(三)

接着调用OsVmSpaceInitCommon将页表基址记录在LosVmSpace数据结构中:

Liteos-a内核分析(三)

最终是调用OsArchMmuInit将页表的虚拟地址和物理地址信息记录在vmSpace->archMmu中:

Liteos-a内核分析(三)

3. vmalloc虚拟空间初始化

调用OsVMallocSpaceInit函数初始化vmalloc虚拟空间数据结构:

Liteos-a内核分析(三)

整体流程和初始化内核虚拟空间类似,仅仅是记录的起始地址和大小不一样:

Liteos-a内核分析(三)

vmalloc空间我们之前在分析系统启动时画过一张图,这里再贴一下:

Liteos-a内核分析(三)

4. 内核堆初始化

调用OsKHeapInit初始化内核堆:

Liteos-a内核分析(三)

内核堆大小用OS_KHEAP_BLOCK_SIZE表示,默认值是512KB:

Liteos-a内核分析(三)

下面来看下OsKHeapInit函数的实现,首先将内核堆的尾地址对齐到MB,然后重新计算内核堆的大小,使用新的大小通过OsVmBootMemAlloc申请内存,最后将申请到的内核堆基址保存在m_aucSysMem0和m_aucSysMem1这两个变量中:

Liteos-a内核分析(三)

然后使用LOS_MemInit初始化内核堆空间,这个会涉及到内存管理算法的知识,这个留到以后再具体分析。在图中的注释中可以看出为什么要将内核堆的结尾做MB对齐,因为后面会将内核堆结尾之前的所有空间使用二级页表进行重新映射,这段空间被称作内核空间,而内核堆之后的空间还是使用段映射的方式。

调用完OsKHeapInit后内存的布局如下:

Liteos-a内核分析(三)

5. 系统物理页初始化

调用OsVmPageStartup初始化内核物理页,内核物理页是内核在使用kmalloc时所使用:

Liteos-a内核分析(三)

系统物理页空间用VmPhysArea来描述:

Liteos-a内核分析(三)

看数据结构也比较简单,就是简单记录了物理内存起始地址和大小,默认起始地址是内存基址,大小为127MB。

OsVmPageStartup首先调用OsVmPhysAreaSizeAdjust调整了系统物理页空间:

Liteos-a内核分析(三)

这里调整后的物理页空间是内核堆尾到127MB这段空间:

Liteos-a内核分析(三)

内核里使用LosVmPage来管理一个物理页的信息,主要包括物理页起始地址、大小、状态等信息。OsVmPageStartup接着计算了当前物理页空间总共需要多少个LosVmPage物理页控制块,并使用OsVmBootMemAlloc来申请物理页控制块池:

Liteos-a内核分析(三)

然后再次调用OsVmPhysAreaSizeAdjust调整物理页空间,调整后的内存布局如下:

Liteos-a内核分析(三)

接着调用OsVmPhysSegAdd和OsVmPhysInit初始化了g_vmPhysSeg这个数据结构,这个是用于管理物理页控制块的控制块,这两个函数做的事情也比较简单,这里不展开分析了。

最后初始化了当前物理页空间所对应的物理页控制块,最重要的就是将每个物理页的起始地址记录在物理页控制块中:

Liteos-a内核分析(三)

OsVmPageStartup调用完后系统的物理内存布局如下:

Liteos-a内核分析(三)

6. 内核空间使用4KB页映射

内核调用OsInitMappingStartUp来将内核空间切换成页映射:

Liteos-a内核分析(三)

6.1 切换临时页表

调用OsSwitchTmpTTB使用临时页表:

Liteos-a内核分析(三)

由于之前内存管理组件已经被初始化了,所以可以使用LOS_MemAllocAlign从内核堆空间申请16KB内存用于临时页表,然后将当前内核页表g_firstPageTable中的内容复制到临时页表中,最后将临时页表的物理地址设置到系统ttbr0寄存器中,这个寄存器就是用于保存页表物理基址的。

可以看出这里是将内核页表中的内容原封不动的复制到另一个临时页表中,然后切换过去,因为g_firstPageTable这个页表在接下来会被修改。

6.2 建立内核新的映射关系

内核调用OsSetKSectionAttr设置内核新的页表,首先将内核空间分为了3个部分,分别是内核代码段,内核只读数据段,内核数据段:

Liteos-a内核分析(三)

从图中可以看出,kernel_data_bss段包含了内核数据段、内核BSS段、TCB控制块池、内核堆这四个区域。

接着将内核虚拟空间控制块切换回g_firstPageTable,并使用LOS_ArchMmuUnmap取消内核堆尾地址之前所有空间的映射,这段空间也就是内核空间,因为这段空间接下来需要使用4KB页映射,所以先要取消原来的段映射:

Liteos-a内核分析(三)

接着映射了中断向量表区域的虚拟空间和物理空间,映射属性是可读、可写、可执行:

Liteos-a内核分析(三)

然后mmuKernelMappings表中的三个内核区域依次建立映射:

Liteos-a内核分析(三)

随后将127MB内剩余物理页区域建立映射,这段空间应该是内核kmalloc时使用到的:

Liteos-a内核分析(三)

所有映射建立完毕之后,最后操作页表寄存器ttbr0将页表切换为新的内核页表,并刷新tlb,最后将之前使用的临时页表内存释放:

Liteos-a内核分析(三)

OsSetKSectionAttr执行完之后,内核空间新的映射关系如下:

Liteos-a内核分析(三)

由于建立映射使用的是LOS_ArchMmuMap这个函数,这个函数对于大于1MB的空间使用的是段映射,小于1MB的空间使用的是页映射,为啥不全都使用段映射呢,或者不全都使用页映射呢?因为不是每个内核段都大于1MB,所以对于这些段需要使用页映射;至于为啥不全用页映射,理论上是完全可以全都使用页映射的,但是对于一个大于1MB的区域使用页映射就有点浪费时间了,因为现在要映射的虚拟地址空间和物理地址空间都是确定的,使用段映射会缩短映射建立的时间。

6.3 LOS_ArchMmuMap

这个函数用于建立虚拟空间和物理空间的映射关系,首先会检查地址和映射大小是不是页对齐的,然后根据要映射的大小来决定使用段映射还是页映射:

Liteos-a内核分析(三)

如果是段映射,则调用OsMapSection建立映射关系。这个函数首先将系统的映射标志转换为体系架构相关的属性,然后和物理地址组合成页表项值填入到对应的页表项,最后调整虚拟地址、物理地址和剩余映射大小,参与下一次映射:

Liteos-a内核分析(三)

如果是使用的页映射,则首先获得页目录项的地址:

Liteos-a内核分析(三)

然后检查页目录中是否是有效项,如果不是则调用OsMapL1PTE申请二级页表空间并初始化页目录项:

Liteos-a内核分析(三)

我们来看看OsMapL1PTE这个函数,这个函数首先调用OsGetL2Table申请二级页表内存:

Liteos-a内核分析(三)

OsGetL2Table首先检查当前页目录的二级页表是不是已经预先申请了,如果预先申请了,就直接使用预先申请的页表内存空间,如果没有,则申请一个新的:

Liteos-a内核分析(三)

那么为什么会有一个预先申请的说法呢,我们看上图中的代码,如果页表对应的二级页表需要新申请,则调用LOS_PhysPageAlloc申请一个物理页用作二级页表:

Liteos-a内核分析(三)

一个物理页的大小是4KB,但是一个二级页表大小只有1KB大小,这样一个物理页剩余的3KB难道就浪费不用么?并不是的,这3KB就是下3个页目录项的二级页表的内存,这里就是预先申请的概念:

Liteos-a内核分析(三)

然后将新申请的物理页清零,最后返回物理地址:

Liteos-a内核分析(三)

回到OsMapL1PTE函数中来,获得了二级页表基址后,和相关标志组合后生成对应的目录项的值,然后通过OsSavePte1写入到对应的目录项:

Liteos-a内核分析(三)

回到LOS_ArchMmuMap函数中来,设置完页目录项后,然后调用OsMapL2PageContinous设置二级页表项:

Liteos-a内核分析(三)

OsMapL2PageContinous函数做的事情比较简单,就是根据虚拟地址、物理地址、大小、映射属性一一生成二级页目录项,并写入对应的页目录项:

Liteos-a内核分析(三)

6.4 页表基址寄存器切换

经过前面的步骤已经将内核新的页表初始化好了,并且设置到了ttbr0寄存器中了,这里调用OsArchMmuInitPerCPU函数使用ttbr1作为页表基址寄存器:

Liteos-a内核分析(三)

那么TTBCR、TTBR0、TTBR1这三个寄存器有什么关系呢?首先来看下这三个寄存器的格式,如下面几张图所示。

TTBCR

Liteos-a内核分析(三)

TTBR0

Liteos-a内核分析(三)

TTBR1

Liteos-a内核分析(三)

至于TTBR0和TTBR1是这么使用的,如果TTBCR的bit[2:0]是0,则一直使用TTBR0,如果不是0,则按照下面规则使用:

Liteos-a内核分析(三)

从图中可以看出,如果TTBCR.N不为0,那么从图中定义的起始虚拟地址之上的所有地址都是用的TTBR1中的页表进行翻译,在这个虚拟地址之下的所有地址都使用TTBR1中的页表来翻译。

理论知识看完了,我们回到OsArchMmuInitPerCPU函数中来看看是怎么设置这几个寄存器的。因为内核的起始虚拟地址用KERNEL_ASPACE_BASE定义,默认值是0x40000000,在这个地址之下的虚拟地址对于内核而言都是无用的,这部分地址空间可以用TTBR0,在内核虚拟地址之上的空间使用TTBR1:

Liteos-a内核分析(三)

首先根据内核起始虚拟地址计算TTBCR寄存器中的值进行设置:

Liteos-a内核分析(三)

然后将现在TTBR0中的内核页表基址设置到TTBR1中,更新TTBCR寄存器的值,最后将TTBR0寄存器设置为0,因为只有访问了内核虚拟地址以下的空间才会使用到TTBR0,这里只是简单设置为0表示不使用这段空间:

Liteos-a内核分析(三)

gewenbin
  • 本文由 发表于 2020年12月19日20:27:03
  • 转载请务必保留本文链接:http://www.databusworld.cn/9152.html
匿名

发表评论

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: