内存管理(读奔跑吧linux内核的总结)

内存操作简介:所有执行的进程都有一定的数量的内存,用来存放从磁盘载入的程序的代码,或者存放自身用户输入的数据,,因为内存的管理方式不同,内存的用途也不同,有些内存静态回收,有些动态分配,回收。主要包括代码段,数据段,BSS段,堆,栈等操作。BBS和堆通常是连续存储的:内存位置上是连续的,代码段和栈是被独立存放的

一:物理内存初始化:

1.1:物理内存的大小

32位(4GB虚拟地址空间(用户空间内核空间))在arch/arm/boot/dts/vexpress-v2p-ca9.dts文件中配置

定义了内存的起始地址0x60000000 大小 x040000000(1GB的内存大小)

解析dts的过程:start_kernel-->setup_arch-->setup_machine_fdt-->early_init_dt_scan_nodes-->of_scan_flat_dt(遍历Nodes)-->early_init_dt_scan_memory(初始化单个内存Node)。

然后根据解析出的base/size,调用early_init_dt_add_memory_arch-->memblock_add-->memblock_add_range将解析出的物理内存加入到memblock子系统。

linux内核使用伙伴管理系统管理内存之前,采用的是memblock子系统进行简单的内存管理,记录内存的使用情况。

内存中的代码段和数据段,ramdisk和fdt是永久分配给内核的,GPU,Camera等预留大量的连续内存,提前预留好

memblock子系统把物理内存划分为若干内存区,按照使用类型放在动态内存集合或者静态内存集合。

具体解析看:https://blog.csdn.net/modianwutong/article/details/53162142(详细介绍memblock子系统)

1.2:物理内存映射

内核使用内存之前,需要初始化内核页表,linux一般采用两层映射。PGD->PUD->PMD->PTE中间的PUD/PMD被省略,真正创建页表是在map_lowmem函数中实现,

映射区域一:0x60000000~0x60800000物理地址(0xc0000000~0xc0800000虚拟地址)一般存放内核代码数据段,(读写执行权限)包括swapper_pg_dir。

区域二:0x60800000~0x8f800000物理地址(0xc0800000~0xef800000虚拟地址)具有读写,不具有执行,是Normal memory部分。

pmd_clear()清空了三段地址页表项,0~MODULES_VADDR、MODULES_VADDR~PAGE_OFFSET、0xef800000~VMALLOC_START。

1.3:zone初始化

内存将一个node分成若干个zone进行管理,结构体include/linux/mmzone.h (struct zone)zone分类:NORMAL和HIGHMEM两种(ENUM zone_type中)。

zone的初始化在bootmem_init中进行。通过find_limits找出物理内存开始帧号min_low_pfn、结束帧号max_pfn、NORMAL区域的结束帧号max_low_pfn。

zone_sizes_init中计算出每个zone大小以及zone之间的hole,然后调用free_area_init_node创建内存节点的zone

zone_sizes_init->zone_sizes_init_node->zone_sizes_init_core用来初始化zone

ZONE_NORMAL对应的物理地址是0x60000000 - 0x8f800000,ZONE_HIGHMEM对应的物理地址是0x8f800000 - 0xa0000000。

每个zone在系统初始化的时候会计算水位值:WMARK_MIN、WMARK_LOW、WMARK_HIGH。这些参数在kswapd回收页面内存的时候会用到。计算水位的一个重要参数min_free_kbytes是在init_per_zone_wmark_min中进行的:

1.4:物理内存初始化

在内核知道物理内存ddr的大小以及计算出高端内存的起始地址和内核的空间布局后,物理页面如何加入到伙伴系统中去

伙伴系统:操作系统中常见的一种动态存储管理方法。用户提出申请时,分配一块大小合适的内存块给用户,释放时回收内存块。页的大小,和2的order次冥的页。

内核中分成几个zone来管理,zone结构体中有个free_area,每个free_area有几个相应的链表

MIGRATE_TYPES

enum {  MIGRATE_UNMOVABLE,--------------------页框内容不可移动,在内存中位置必须固定,无法移动到其它地方,核心内核分配的大部分页面都属于这一类。
    MIGRATE_RECLAIMABLE,------------------页框内容可回收,不能直接移动。因为还可以从某些源重建页面,比如映射文件的数据属于这种类别,kswapd会按照一定的规则,周期性回收这类页面。
    MIGRATE_MOVABLE,----------------------页框内容可移动,属于用户空间应用程序的页属于此类页面,他们是通过页表映射的,因此只需要更新页表项,并把数据复制到新位置就可以了。当然要注意,一个页面可能被多个进程共享,对应着多个页表项}

大部分的物理初始化页面存放在MIGRATE_MOVABLE链表中,内存页面初始化存放在2的10次幂的链表中

pageblock

通常是2的(MAX_ORDER-1)次幂的页面。

物理页面是通过2的n次幂加入到伙伴系统中去。,通过for_each_free_mem_range()函数来遍历所有的memblock内存块。找出内存块的起始地址和结束地址。

二:页表映射过程(将虚拟地址转换为对应的物理地址)

2.1:arm32页表映射

arm32和linux内核维护的页表有所不同,有两套PTE.

PGD存放在swapper_pd_dir中,一个PGD目录包含了两份ARM32 PGD,再分配PTE时,共分配了1024个PTE,512个给linux维护使用,512个给ARM32的MMU使用,对应两个PGD的页表数目。

32bit的三级映射:PGD->PMD->PTE

2.1.1:页面映射过程

如果采用页表映射的方式,一级映射表(PGD),提供的不是物理地址,而是二级页表(PTE)的基地址。

32位虚拟地址的高12位(bit[31:20])作为访问一级页表的索引值,找到相应的表项,每个表项指向一个二级页表。

以虚拟地址的次8位(bit[19:12])作为访问二级页表的索引值,得到相应的页表项,从这个页表项中找到20位的物理页面地址。

这个过程在ARM32架构中由MMU硬件完成,软件不需要接入

2.2:页表的相关数据结构

在map_lowmem()使用create_mapping()创建页表映射,这个函数的参数结构是struct map_desc。

arch\arm\include\asm\mach\map.h: 
struct map_desc {
    unsigned long virtual;------虚拟地址起始地址
    unsigned long pfn;----------物理地址开始页帧号
    unsigned long length;-------内存空间大小
    unsigned int type;----------mem_types中的序号
}; map_desc中的type指向类型为struct mem_type的mem_types数组:
arch\arm\mm\mm.h:
struct mem_type {
    pteval_t prot_pte;------------PTE属性
    pteval_t prot_pte_s2;---------定义CONFIG_ARM_LPAE才有效
    pmdval_t prot_l1;-------------PMD属性
    pmdval_t prot_sect;-----------Section类型映射
    unsigned int domain;----------定义ARM中不同的域(eg:系统空间,用户空间等)
};
arch\arm\mm\mmu.c:
static struct mem_type mem_types[] = {
...
    [MT_MEMORY_RWX] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY,----------------------注意这里都是L_PTE_*类型,需要在写入MMU对应PTE时进行转换。
        .prot_l1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain    = DOMAIN_KERNEL,
    },
    [MT_MEMORY_RW] = {
        .prot_pte  = L_PTE_PRESENT | L_PTE_YOUNG | L_PTE_DIRTY |
                 L_PTE_XN,
        .prot_l1   = PMD_TYPE_TABLE,
        .prot_sect = PMD_TYPE_SECT | PMD_SECT_AP_WRITE,
        .domain    = DOMAIN_KERNEL,
    },
...
}

create_mapping的参数是struct map_desc类型,用于描述一个虚拟地址区域线性映射到物理区域。基于这块区域创建PGD/PTE。

 Linux的PGD页表目录和ARM32不同,总数和ARM32是一样的。在arm_mm_memblock_reserve中,通过swapper_pg_dir(内核页表存地方)可以知道其大小为16KB。pgd_offset_k可以通过init_mm结构体所指定的页面目录中来获取地址addr所指的页面目录指针pgd。然后通过addr右移PGDIR_SHIFT(21)得到pgd的索引值。最后再一级页表中找到pgd的指针。内核页表的基地址定义在0xc0004000。

一个pgd页表对应2MB的大小空间,即[addr,addr+2mb],(一个pgd对应的虚拟内存空间的大小)。

pgd的数目为2的11次方(2048)个,整个pgd页表所占空间2048*4=8kb。(pgd所占的空间大小,2048个pgd,一个pgd4字节的大小)。

这和ARM硬件的4096 PGD不一致。这里涉及到Linux实现技巧,在创建PTE中进行分析。

由于ARM-Linux采用两级页表映射,跳过PUD/PMD,直接到alloc_init_pte创建PTE。

就来看看SWAPPER_PG_DIR_SIZE,一共2048个PGD,但是每个PGD包含了两个相邻的PGD页面目录项。

所以存放PGD需要的空间通过memblock进行申请,PTE_HWTABLE_OFF和PTE_HWTABLE_SIZE都为512,所以一个1024个PTE。

early_pte_alloc分配的空间:前面512个表项是给Linux OS使用的,后512个表项是给ARM硬件MMU用的。

三:内核分配区域

https://blog.csdn.net/abc3240660/article/details/81484984

内存管理(读奔跑吧linux内核的总结)_第1张图片

896MB细分为ZONE_DMA和ZONE_NORMAL区域,

低端内存(ZONE_DMA):3G-3G+16M 用于DMA __pa线性映射
普通内存(ZONE_NORMAL):3G+16M-3G+896M __pa线性映射 (若物理内存<896M,则分界点就在3G+实际内存)
高端内存(ZONE_HIGHMEM):3G+896-4G 采用动态的分配方式
ZONE_DMA+ZONE_NORMAL属于直接隐射区域 :虚拟地址=3G+物理地址,从该区域分配内存不会触发页表简历隐射关系。

ZONE_HIGHMEM属于动态分配区域:128M虚拟地址空间可以动态映射的物理内存,从该区域分配的内存需要更新页表来建立映射关系,vmalloc就是从该区域分配,直接映射的作用是为了保证能够申请到物理上的连续内存区域。

内存管理(读奔跑吧linux内核的总结)_第2张图片

页大小4K(4096),每页都有一个struct pages表示。vmalloc分配的是虚拟地址的连续地址

kmalloc只能从低端内存中分配,最大32个page,共128k,kzalloc都是其变种(可以得到8m以及更大)

vmalloc只能在高管内存区域分配,alloc_page可以在高端内存区域分配,也可以在低端内存区域分配(最大4M),_get_free_page:只能在低端内存中分配,ioremap是将已知的一段物理内存映射到虚拟地址空间

三:内存内核布局

https://www.cnblogs.com/arnoldlu/p/8068286.html。

内存布局有关于理解内存的管理:理解内存管理的初始化,虚拟内存,内存分配的作用,网上流传的重要的内核布局图:

内存管理(读奔跑吧linux内核的总结)_第3张图片

内存管理(读奔跑吧linux内核的总结)_第4张图片

0x00000000~0xc000000(用户空间) ~0xc0004000()~0xc0008000(swapper_pg_dir)~0xc060b000(.text)~0xc075c000(.init)~0xc075e000(init_thread_union8kb)~0xc07833c0(.data)~0xc07acbf0(.bss)~0xef800000(NOrmal Memory) ~0xf0000000(8Mvmalloc_offset)~0xff000000(vmalloc)~0xfffffff

内核启动时会打印内存空间布局图:打印是在mem_init()函数中实现。

Memory: 1031428K/1048576K available (4787K kernel code, 156K rwdata, 1364K rodata, 1348K init, 166K bss, 17148K reserved, 0K cma-reserved, 270336K highmem)
Virtual kernel memory layout:
    vector  : 0xffff0000 - 0xffff1000   (   4 kB)
    fixmap  : 0xffc00000 - 0xfff00000   (3072 kB)
    vmalloc : 0xf0000000 - 0xff000000   ( 240 MB)
    lowmem  : 0xc0000000 - 0xef800000   ( 760 MB)
    pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
    modules : 0xbf000000 - 0xbfe00000   (  14 MB)
      .text : 0xc0008000 - 0xc060a09c   (6153 kB)
      .init : 0xc060b000 - 0xc075c000   (1348 kB)
      .data : 0xc075c000 - 0xc07833c0   ( 157 kB)
       .bss : 0xc07833c0 - 0xc07acbf0   ( 167 kB)

用来具体分析内核布局;线性映射区的意思是0xc0000000 - 0xef800000这段虚拟地址,和0x60000000 - 0x8f800000这段物理地址是一一对应的。

.text、.init、.data、.bss都属于lowmem区域,也即ZONE_NORMAL;vector、fixmap、vmalloc属于ZONE_HIGHMEM区域。

pkmap、modules属于用户空间。

在系统管理中的每个进程都拥有一个struct mm_struct结构,其中包含了虚拟内存区域链表,页表以及其他大量的呢欧村管理信息,它还包含其他大量的内存管理信息,驱动程序访问它,是调用currect->mm,多个内存可以共享内存管理结构,

内核编译完之后,会生成一个system.map的文件,可以找到这些数据的具体数值。

1gb的内核空间,其中一部分用于直接映射物理内存,这个区域称为线性映射区域(0~760M)这一部分内存区域被线性映射到(3gb~3gb+760mb)的虚拟地址上。,线性映射区域的虚拟地址和物理地址相差PAGE_OFFSET(3gb),可以通过-pa()虚拟转物理。高端地址的内存起始地址(760M)如何确定(arch/arm/mm/mmu.c)中的vmalloc_min中配置。

剩下的264mb空间保留给vmalloc,fixmap和高端向量表使用,

低于760mb的称为线性映射内存,高于760的称为高端内存,,我们可以从保存了240mb的虚拟地址空间中划出一部分用于动态映射高端内存,这样内核就可以访问到全部的4gb内存了。

一些函数:virt_to_page:负责将内核地址转换为相应的page结构指针,需要的是一个逻辑地址,不能操作vmalloc生成的地址。

pfn_to_page:根据页帧号返回页信息。page_address:根据页信息,返回内核的虚拟地址。

 

你可能感兴趣的:(个人随笔)