赞
踩
在内存管理的上下文,初始化可以有多重含义。在许多CPU上,必须显式设置适用于Linux内核的内存模型。文章给出了linux系统设计的初始化阶段设计的内存接口与函数。
1.1. linux内存管理的层次结构
linux把物理内存划分为三个层次来管理,分别是存储节点,管理区和页面。
层次 | 描述 |
存储节点(Node) | CPU被划分为多个节点(node),内存则被分簇,每个CPU对一个本地物理内存,即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 |
管理区(zone) | 每个物理节点node被划分为多个内存管理区域(zone),用于表示不同范围的内存,内核可以使用不同的映射方式映射不物理内存。 |
页面(page) | 内存被划分为多个页面帧,页面是最基本的内存分配单元 |
为了支持NUMA模型,也即CPU对不同的内存单元访问时间可能不同,此时系统的物理内存被划分为几个节点(node),一个node对应一个内存簇bank,即每个内存簇都被认为是一个节点。
1.2. 内存节点pg_data_t
linux内核中引入一个数据结构struct pglist_data来描述一个node,定义在include/linux/mmzone.h文件中。这个结构体又被typedef pg_data_t。
可以使用NODE_DATA(node_id)来查找系统中编号为node_id的节点,而UMA结构下由于只有一个节点,返回红总是返回全局的contig_page_data,而与参数node_id无关。
- extern struct pglist_data *node_data[];
- #define NODE_DATA(nid) (node_data[(nid)])
1.3. 物理内存区域
实际的计算机体系结构有硬件的诸多限制,这限制了页框的使用方式,尤其是在linux内核必须处理80x86体系结构的两种硬件约束。
因此,linux内核对不同区域的内存需要采用不同的管理方式和映射方式,内核将物理地址或者用zone_t表示的不同地址区域。对于x86_32的机器,管理区(内存区域)类型分布如下:
类型 | 区域 |
ZONE_DMA | 0~15MB |
ZONE_NORMAL | 16MB~895MB |
ZONE_HIGHMEM | 896MB~物理内存结束 |
1.4. 物理页帧
内核把物理也作为内存管理的基本单位,尽管处理器最小可寻址单位通常是字。但是,内存管理单元MMU通常以页为单位进行处理。因此,从虚拟内存上来看,也就是最小单位。页帧掉膘了系统内存的最小单位,对内存中行的每个页都会穿件struct page的一个实例,内核必须要保证page结构体足够小,否则仅struct page就要占用大量的内存,结构体内部大量使用联合体union节省空间。mem_map是struct page的数组,管理者系统中所有的物理内存页面。在系统启动过程中,穿件和分配mem_map的内存区域。mem_map定义在mm/page_acllo.c。UMA体系结构中,free_area_init函数在系统唯一的struct node对象conting_page_data中node_mem_map成员赋值给全局mem_map变量。
1.5. 启动过程中的内存初始化
在初始化过程中,还必须建立内存管理的数据结构,以及和很多事物。因为内核在内存管理完全初始化之前就需要使用内存。在系统启动过程期间,使用了额外的简化内存管理模块,然后在初始化完成后将旧模块丢弃掉。linux内核内存管理划分为三个阶段:
阶段 | 起点 | 终点 | 描述 |
第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存,早期内核中使用init_bootmem_done=1标识此阶段结束 |
第二阶段 | bootmem或者memblock初始化完成 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存管理工作,早期内核使用mem_init_done=1标记此阶段结束 |
第三阶段 | buddy初始化完成 | 系统停止运行 | 可以使用cache和buddy分配内存 |
start_kenel完成内存所有的初始化工作,具体将其中与内存相关的街区出来:
- start_kernel()
- |---->page_address_init()
- | 考虑支持高端内存,ARM本函数为空
- | 业务:初始化page_address_pool链表;
- | 将page_address_maps数组元素按索引降序插入
- | page_address_pool链表;
- | 初始化page_address_htable数组.
- |
- |---->setup_arch(&command_line);
- | 初始化特定体系结构的内容
- |---->phys_to_virt(mdesc->boot_params);
- | 获取物理启动tag位置
- |
- |---->mdesc->fixup(mdesc, tags, &from, &meminfo)或者parse_tags(tags)
- | 通过板提供函数或者是uboot传入的参数进行物理内存参数解析
- | 对于高版本内核还可以从设备树中解析出物理SDRAM参数信息
- |
- |---->paging_init(); [参见分页机制初始化paging_init]
- | 分页机制初始化
- |---->build_mem_type_table()
- |---->prepare_page_table()
- |---->bootmem_init() [与build_all_zonelist完成内存数据结构的初始化]
- | 初始化内存数据结构包括内存节点和内存域
- |---->devicemaps_init(mdesc)
- |---->mm_init_cpumask(&init_mm)
- |
- |
- |---->setup_per_cpu_areas();
- | 为per-CPU变量分配空间
- |
- |---->build_all_zonelist() [bootmem_init初始化数据结构, 该函数初始化zonelists]
- | 为系统中的zone建立后备zone的列表.
- | 所有zone的后备列表都在
- | pglist_data->node_zonelists[0]中;
- |
- | 期间也对per-CPU变量boot_pageset做了初始化.
- |
- |---->page_alloc_init()
- |---->hotcpu_notifier(page_alloc_cpu_notifier, 0);
- | 不考虑热插拔CPU
- |
- |---->pidhash_init()
- | 详见下文.
- | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash
- |
- |---->mm_init()
- | 完成新的分配器的初始化,将老分配器boot_mem替换
- |
- |---->kmem_cache_init_late()
- |
- |---->kmemleak_init()
- |
- |---->setup_per_cpu_pageset()
- |
- |
- |---->vfs_caches_init_early()
- |---->dcache_init_early()
- | dentry_hashtable空间,d_hash_shift, h_hash_mask赋值;
- | 同pidhash_init();
- | 区别:
- | 散列度变化了(13 - PAGE_SHIFT);
- | 传入alloc_large_system_hash的最后参数值为0;
- |
- |---->inode_init_early()
- | inode_hashtable空间,i_hash_shift, i_hash_mask赋值;
- | 同pidhash_init();
- | 区别:
- | 散列度变化了(14 - PAGE_SHIFT);
- | 传入alloc_large_system_hash的最后参数值为0;
- |
- |---->reset_init()
函数 | 功能 |
---|---|
setup_arch | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 |
mm_init_cpumask | 初始化CPU屏蔽字 |
setup_per_cpu_areas | 函数(查看定义)给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间. 在SMP系统中, setup_per_cpu_areas初始化源代码中(使用per_cpu宏)定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. 此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本 在非SMP系统中这是一个空操作 |
build_all_zonelists | 建立并初始化结点和内存域的数据结构 |
mm_init | 建立了内核的内存分配器, 其中通过mem_init停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统) 然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 |
kmem_cache_init_late | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器slab, slob, slub都会定义此函数 |
kmemleak_init | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中, Kmemcheck能够帮助定位大多数内存错误的上下文 |
setup_per_cpu_pageset | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分 由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
2. 第一阶段(启动过程的内存管理)
内存管理是操作系统资源管理的重点,在操作系统初始化的初期,操作系统知识获取了内存的基本信息,但是内存管理的数据结构没有建立,而我们这些数据结构创建过程本身就是一个内存分配的过程,那么就会出现一个问题。我们还没有一个内存管理器去负责分配和回收内存,而我们又不可能将所有的内存信息都静态创建并初始化。那么我们怎么分配内存管理器所需要的内存呢?现在我们进入一个先有鸡还是先有蛋的怪圈,这种问题一般解决方法是:我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器),用它来负责系统初始化早期的内存管理。最重要的用它来初始化我们内存的数据结构,直到我们真正的内存管理器被初始化完成并能投入使用,我们将旧的内存管理器丢掉。
即在系统启动过程期间,内核使用了一个额外的简化形式的内存管理模块早期的引导内存分配器(boot memory allocator-bootmem)或者memblock用于实现启动阶段早期内存分配,而在系统初始化完成以后,该分配器被内核抛弃,然后初始化了一套更加完善的内存分配器。
2.1. 引导内存分配器bootmem
在启动过程期间,尽管内存管理尚未初始化,但是内核需要分配内存以创建各种数据结构,再起的内核中负责初始化阶段的内存分配器称为引导内存分配器(boot memory allocator-bootmem分配器),在耳熟能详的伙伴系统创立之前内存都是利用这个分配器来分配的,伙伴系统创建起来后,bootmem会过度到伙伴系统。显然,对该内存分配器的需求集中于简单性方面,而不是性能和通用性,它仅用于初始化阶段。因此,内核开发者决定实现一个最先适配(first-first)分配器在启动阶段管理内存,这是可能想到的最简单的方式。
引导内存分配器(boot memory allocator-bootmem分配器)基于最先适配(first-first)分配器的原理(这是很多系统的内存分配所使用的原理),使用一个位图来管理页,以位图代替原来的空闲列表结构来表示存储空间,位图的比特位数目与系统中物理内存页数目相同。若为图中某一位是1,则标识该页已经被分配,否则表示未被占用。在需要分配内存时,分配器诸位的扫描位图,直至找到一个能够提供足够连续页的位置,即所谓的最先最佳(first-best)或最先适配位置。该分配机制通过记录上一次分配的页面帧号(PFN)结束时的偏移量来实现分配大小小于一页的空间,连续的小空闲空间将会被合并存储在一页上。
即使是初始化用的最先匹配分配器也必须使用一些数据结构,内核显微系统中每一个节点提供了一个struct bootmem_data结构实例用于bootmem的内存管理。它含有引导内存分配器给节点分配内存时所需要的信息。当然,这个还是后内存管理还没有初始化,因而该结构所需要的内存是无法动态分配的,必须在编译时分配给内核。在UMA系统上该分配的实现与CPU无关,而NUMA系统内存节点与CPU相关联,采用特定体系结构的解决方法。bootmem_data的结构定义在include/linux/bootmem.h
关于引导内存分配器的具体内容, 请参见另外一篇博文
CSDN | GitHub |
---|---|
引导内存分配器bootmem | study/kernel/02-memory/03-initialize/02-bootmem |
2.2. memblock内存分配器
但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核维护了memblock内存分配器, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem, Memblock以前被定义为Logical Memory Block( 逻辑内存块),但根据Yinghai Lu的补丁, 它被重命名为memblock. 并最终替代bootmem成为初始化阶段的内存管理器
关于引导内存分配器的具体内容, 请参见另外一篇博文
CSDN | GitHub |
---|---|
memblock内存分配器 | study/kernel/02-memory/03-initialize/03-memblock |
2.3. 两者的区别与联系
bootmem是通过位图来管理,位图存在低地址段,而memblock是在高地址管理内存,维护两个链表,即memory和reserved。memory链表维护系统的内存信息(在初始化阶段通过BIOS获取的),对于任何内存分配,先去查找memory链表,然后再reserve链表上记录(新增一个节点,或者合并)
在boot传递给kernel memory bank相关信息后,kernel这边会以memblock的方式保存这些信息,当buddy system没有起来之前,在kernel中也是需要有一套机制来管理memory的申请和释放。kernel可以选择nobootmem或bootmem来在buddy system起来之前管理memory。
这两种及其对外提供API是一致的,对用户都是透明的。
- ifdef CONFIG_NO_BOOTMEM
- obj-y += nobootmem.o
- else
- obj-y += bootmem.o
- endif
由于接口是一致的, 那么他们共同使用一份
头文件 | bootmem接口 | nobootmem接口 |
---|---|---|
include/linux/bootmem.h | mm/bootmem.c | mm/nobootmem.c |
4. 第二阶段(初始化buddy内存管理)
在arm64架构下, 内核在start_kernel()
->setup_arch()
函数中依次完成了如下工作
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
流程 | 描述 |
---|---|
arm64_memblock_init | 初始化memblock内存分配器 |
paging_init | 初始化分页机制 |
bootmem_init | 初始化内存管理 |
其中arm64_memblock_init就完成了arm64架构下的memblock的初始化.
而setup_arch则主要完成如下工作
bootmem_init
初始化内存管理4.1. 初始化流程
下面我们就以arm64架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同
arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别
4.2. paging_init初始化分页机制
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响
因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。
在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB
用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间.
这些划分主要的动机如下所示
4.3. 虚拟地址空间(以x86_32为例)
出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间
按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段
地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量0xC0000000开始,即经常提到的3 GiB,每个虚拟地址x都对应于物理地址x—0xC0000000,因此这是一个简单的线性平移。
直接映射区域从0xC0000000到high_memory地址,high_memory准确的数值稍后讨论。第1章提到过,这种方案有一问题。由于内核的虚拟地址空间只有1 GiB,最多只能映射1 GiB物理内存。IA-32系统(没有PAE)最大的内存配置可以达到4 GiB,引出的一个问题是,如何处理剩下的内存?
这里有个坏消息。如果物理内存超过896 MiB,则内核无法直接映射全部物理内存。该值甚至比此前提到的最大限制1 GiB还小,因为内核必须保留地址空间最后的128 MiB用于其他目的,我会稍后解释。将这128 MiB加上直接映射的896 MiB内存,则得到内核虚拟地址空间的总数为1 024 MiB = 1GiB。内核使用两个经常使用的缩写normal和highmem,来区分是否可以直接映射的页帧.
内核地址空间的最后128 MiB用于何种用途呢?如图3-15所示,该部分有3个用途.
同样我们的用户空间, 也被划分为几个段, 包括从高地址到低地址分别为 :
区域 | 存储内容 |
---|---|
栈 | 局部变量, 函数参数, 返回地址等 |
堆 | 动态分配的内存 |
BSS段 | 未初始化或初值为0的全局变量和静态局部变量 |
数据段 | 一初始化且初值非0的全局变量和静态局部变量 |
代码段 | 可执行代码, 字符串面值, 只读变量 |
4.4. bootmem_init初始化内存的基础数据结构(结点pg_data,内存域zone,页面page)
在paging_init之后, 系统的页帧已经建立起来, 然后通过bootmem_init中, 系统开始完成bootmem的初始化工作.
不同的体系结构bootmem_init的实现, 没有很大的区别, 但是在初始化的过程中, 其中的很多函数, 依据系统是NUMA还是UMA结构则有不同的定义
bootmem_init函数的实现如下
函数实现 | arm | arm64 |
---|---|---|
bootmem_init | arch/arm/mm/init.c, line 282 | arch/arm64/mm/init.c, line 306 |
4.5. build_all_zonelists初始化每个内存节点的zonelist
内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行.
内存节点pg_data_t
中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息
- // http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L626
- typedef struct pglist_data
- {
- struct zone node_zones[MAX_NR_ZONES];
- struct zonelist node_zonelists[MAX_ZONELISTS];
- }
内核定义了内存的一个层次结构关系, 首先试图分配廉价的内存,如果失败,则根据访问速度和容量,逐渐尝试分配更昂贵的内存.
高端内存最廉价, 因为内核没有任何部分依赖于从该内存域分配的内存, 如果高端内存用尽, 对内核没有副作用, 所以优先分配高端内存
普通内存域的情况有所不同, 许多内核数据结构必须保存在该内存域, 而不能放置到高端内存域, 因此如果普通内存域用尽, 那么内核会面临内存紧张的情况
DMA内存域最昂贵,因为它用于外设和系统之间的数据传输。
举例来讲,如果内核指定想要分配高端内存域。它首先在当前结点的高端内存域寻找适当的空闲内存段,如果失败,则查看该结点的普通内存域,如果还失败,则试图在该结点的DMA内存域分配。如果在3个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。