《深入Linux内核架构》第3章 内存管理(4)

目录

3.4 初始化内存管理

3.4.1 建立数据结构

3.4.2 特定于体系架构的设置

内核在内存中的布局

初始化步骤

分页机制的初始化

3.4.3 启动期间的内存管理

数据结构

初始化

与内核的接口

停用bootmem分配器

释放初始化数据


本节主要讲bootmem分配器。

3.4 初始化内存管理

包括:

        显式设置内存模型。

        确认NUMA中内存总数量。

        初始化内存节点。

3.4.1 建立数据结构

只有1个节点时(如UMA中),nid是伪参数。

        #define NODE_DATA(nid) (&contig_page_data)

setup_arch()中内存相关有:

        内存布局初始化:设置地址空间范围。

        MMU配置:包括设置页表、页目录。

        物理内存探测:解析启动参数或BIOS信息,获取内存总量和分布信息。

        启用bootmem分配器,用于启动过程内存分配。

setup_per_cpu_areas:

        bootmem分配器给每个CPU分配一块存储per-cpu变量的区域。

        per-cpu变量保存在ELF文件单独的段中,如.data..percpu。

        并加载到物理内存.data..percpu段中。

build_all_zonelists:

        为每个CPU构建zonelist,其中zone按优先级排序。

        可根据当前CPU的zonelis构快速找到最优的zone,以完成内存分配。

                优点:优化内存访问,降低内存碎片、提高内存利用率。

mm_init:

        初始化内核的页表。

        停用bootmem分配器,迁移到伙伴系统。

        初始化SLAB 分配器(后续章节讲)

        预留内存页。

        初始化内存统计和信息

setup_per_cpu_pageset:

        给每个CPU设置缓存页的结构。

        优点:避免使用伙伴系统分配,引发zone->lock锁竞争。

zonelist构建流程:

        遍历所有内存节点ABCD,每个节点都建立各自zonelist。

如下图是节点C的zonelist,表示内存不足时尝试分配的优先次序。

C2表示:内存结点C的高端内存域。

D1表示:内存结点D的普通内存域

如想要从结点C分配一个高端内存域的内存,查看上图的2,优先从C2中分配,C2无法满足分配需求,依次尝试C1 C0 D2 ....

3.4.2 特定于体系架构的设置

本节所讲不同体系架构实现有差异。

2.6.24内核开始将IA32和AMD64统一为x86。

        体系代码都迁移到目录arch/x86下。

内核在内存中的布局

先讲两个重要宏:

1. PHYS_OFFSET:内存在系统内的起始物理地址。

        该值与平台相关,如项目中:

                #define PHYS_OFFSET UL(CONFIG_DRAM_BASE)

                        CONFIG_DRAM_BASE=0x80000000

2. PAGE_OFFSET:内核空间的起始虚拟地址,划分了内核与用户空间。

        32位系统为例:

                若值为0xC0000000,即用户空间3GB,内核空间1GB。

                若值为0x80000000,即用户空间和内核空间均为2GB。

cat /proc/iomem中可查看:

        1. 系统RAM的起始物理地址。

        2. 内核代码段和数据段的物理地址。

ZRELADDR:内核解压的运行物理地址。

我的项目中ZRELADDR地址为:

        zreladdr-y := $(CONFIG_DRAM_BASE)+0x8000

        CONFIG_DRAM_BASE是内存的起始物理地址,项目中值为0x80000000。

所以内核解压后运行地址是:内存起始偏移0x8000处,即0x80008000。

# cat /proc/iomem 可验证

        80000000-83ffffff : System RAM

                80008000-804cbb83 : Kernel code 内核的物理地址0x80008000

                804ee000-8063b10f : Kernel data

内核编译链接时,vmlinux.ld.S确定内核的_text段,__edata等段,以及符号函数的虚拟地址,并存储在system.map文件。

一个内核符号的物理地址计算方法:

        Physical Address = (Virtual Address - PAGE_OFFSET) + PHYS_OFFSET

                PAGE_OFFSET:内核虚拟地址空间的的起始位置。

                PHYS_OFFSET:内存的其实物理地址。

查看系统物理地址空间分布,包括所有硬件设备,如PCIE,RAM,SPI,PHY,RTC,flash等。

# cat /proc/iomem

        00000000-00000000 : gdm-pmic

        13200000-1320003f : gdm-spi

        13380000-13381fff : gdm-rgmii

        40000000-401fffff : gdm-sflash

        80000000-83ffffff : System RAM

                80008000-804628a7 : Kernel code

                80484000-805daf4b : Kernel data

初始化步骤

Intel IA-32架构为例:

        setup_arch中关于内存管理的部分如下:

parse_cmdline_early:解析cmdline中内存参数,如

        mem=xxx[KMG]:

                控制内核可用内存大小,例如只使用部分内存。

        reserve=<start>,<size>

                预留一块物理内存不使用。

        cma=<start>,<size>

                启动时预留一块连续的物理内存,例如给DMA设备或者图形硬件使用。

setup_memory

        确定每个节点可用的物理内存页数目

        初始化bootmem

        分配各个内存区

paging_init

        初始化内核页表。

        根据编译选项,是否开启PAE特性。

zone_sizes_init

        初始化所有内存结点的pgdata_t实例。

分页机制的初始化

先看几个地址空间分布图,以32位系统为例。

1. 内核虚拟地址空间分布:

2. 内核空间与物理内存映射:

直接映射区:即线性映射区。

        范围:内核空间3G ~ 3G+896M,映射到物理内存DMA和NORMAL两个ZONE

直接映射区的虚拟地址转换为物理地址:

        #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET)

内存DMA和NORMAL的物理地址转换为虚拟地址:

        #define __va(x) ((void *) ((unsigned long) (x) + PAGE_OFFSET))

注意:这两个函数只适用于直接映射区。

        而高端内存区不是简单的线性映射,不适用。

内核空间最后128M,会映射到物理内存ZONE_HIGHMEM。有三种用途:

1. 固定映射: Fixed Mapping

        编译时已映射到特定物理地址。

        无需动态页表,快速访问的物理内存地址

        用于:

                映射硬件寄存器区和I/O。

        起始处:FIXADDR_START

2. 永久/映射映射: pkmap:Persistent Kernel Mapping

        将高端页帧长期映射到内核地址空间中,直到手动解除映射。

        如kmap, kunmap函数。

3. vmalloc区:即动态内存映射区。

        范围:VMALLOC_START ~ VMALLOC_END

        vmalloc函数作用:

                分配较大的连续虚拟内存,对应物理内存不连续。

paging_init:

        划分内核空间与用户空间。

pagetable_init:

        初始化系统PGD PUD PMD PTE页表。

        将物理内存映射到虚拟地址PAGE_OFFSET处。

        初始化固定映射。

load_cr3

        将PGD页表(内核变量swapper_pg_dir)加载到CR3寄存器。

__flush_all_tlb:

        刷出TLB,即清空页表缓存。

zone_pcp_init:

        初始化PCP冷热页,包括初始化per_cpu_pageset实例。

        计算batch值。

                batch值作用:如果pcp没有空闲页,每次从zone中批量分配batch个页面。

                batch大小:考虑让热缓存页有可能放置到CPU L2缓存中

3.4.3 启动期间的内存管理

内核启动过程中,内存管理子系统还未初始化,此时使用bootmem分配器进行内存分配。

bootmem:使用bitmap表示页空闲、已使用的页

        分配时,遍历位图,找到所需连续空闲页。

        缺点:每次分配都遍历位图,不高效。

数据结构

系统每个节点都有一个bootmem_data实例,在编译时分配。

struct bootmem_data {

        unsigned long node_min_pfn;

                起始物理页框号

        unsigned long node_low_pfn;

                可管理的物理内存最后一页编号,即ZONE_NORMAL最后一页

        void *node_bootmem_map;

                位图指针,表示页是否使用。

        unsigned long last_end_off;

                上次分配页的页内偏移,用于分配小于整页的内存

        unsigned long hint_idx;

                提供一个分配起点或最有位置的提示

        struct list_head list;

                链接所有结点的bootmem_data

} bootmem_data_t;

UMA系统只需一个sbootmem_data_t实例

        contig_bootmem_data //contig,即contigous连续内存模型

初始化

ARM:setup_arch-> paging_init -> bootmem_init

而IA-32:set_memory 中进行bootmem初始化

bootmem:不使用内存的高端内存区域。

        使用内存的normal区域,因为可通过固定的线性映射访问。访问简单。

bootmem初始化包括:

        初始化bootmem_data结构体。

        扫描内存,构建位图,表示对应内存页是否使用。

        为设备驱动、DMA区域等预留内存区域。

与内核的接口

启动期间分配内存:

        alloc_bootmem()/alloc_bootmem_pages()

                从ZONE_NORMAL域内分配内存

       

        alloc_bootmem_low()/alloc_bootmem_low_pages()

                从ZONE_DMA域分配内存

        alloc_bootmem_node()

                NUMA中从指定节点的bootmme分配内存.

上述函数都调用alloc_bootmem_core(unsigned long size,

                                unsigned long align,

                                unsigned long goal,

                                unsigned long limit)

        size:所需内存大小。

        align:对齐方式,如按page对齐。

        goal:目标地址,建议分配的内存地址

        limit:确保不会分配超过这个地址的内存。

释放内存

free_bootmem(unsigned long addr, unsigned long size)

void __init free_bootmem_node(pg_data_t *pgdat, unsigned long physaddr, unsigned long size)

        这两个函数使用较少,大部分分配的内存后续会一直使用,不用释放。

停用bootmem分配器

系统初始化到伙伴系统后,伙伴系统将负责内存分配工作,此时需停用bootmem分配器。

停用bootmem函数:

        free_all_bootmem(void)

                扫描bootmem位图,释放未使用页到伙伴系统。

释放初始化数据

__init和__initdata是GNU C编译器语句,编译后会将对应数据和函数放在内核镜像的特定段。

启动结束,可从内存完全删除对应数据和函数。

其实现为:

#define __init __attribute__(__section(.init.text)) __cold

#define __initdata __attribute__(__section(.init.data))

        __attribute__:GCC关键字

        __section:编译器将数据放入二进制文件.init.text和.inint.data中

        __cold:该函数不会被经常调用

命令readelf可查看内核镜像的各个段,如:

        readelf - sections vmlinux

  • 23
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山下小童

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值