mirror of
https://github.com/gatieme/LDD-LinuxDeviceDrivers.git
synced 2026-09-24 05:53:54 +08:00
内存管理之内存初始化内存管理...
This commit is contained in:
@@ -47,6 +47,7 @@ Linux把物理内存划分为三个层次来管理
|
||||
##1.2 内存结点pg_data_t
|
||||
-------
|
||||
|
||||
|
||||
在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。
|
||||
|
||||
|
||||
@@ -133,8 +134,8 @@ UMA体系结构中,free_area_init函数在系统唯一的struct node对象cont
|
||||
|
||||
|
||||
|
||||
##2.1 系统启动过程中的内存管理
|
||||
-------
|
||||
**系统启动过程中的内存管理**
|
||||
|
||||
|
||||
首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479)
|
||||
|
||||
@@ -145,12 +146,14 @@ UMA体系结构中,free_area_init函数在系统唯一的struct node对象cont
|
||||
asmlinkage __visible void __init start_kernel(void)
|
||||
{
|
||||
|
||||
/* 设置特定架构的信息
|
||||
* 同时初始化memblock */
|
||||
setup_arch(&command_line);
|
||||
mm_init_cpumask(&init_mm);
|
||||
|
||||
setup_per_cpu_areas();
|
||||
|
||||
|
||||
/* 初始化内存结点和内段区域 */
|
||||
build_all_zonelists(NULL, NULL);
|
||||
page_alloc_init();
|
||||
|
||||
@@ -183,23 +186,22 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 |
|
||||
| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配<br>由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
|
||||
|
||||
#2 第一阶段
|
||||
-------
|
||||
|
||||
|
||||
#3 第二阶段
|
||||
#2 第一阶段(启动过程中的内存管理)
|
||||
-------
|
||||
|
||||
|
||||
内存管理是操作系统资源管理的重点, 但是在操作系统初始化的初期, 操作系统只是获取到了内存的基本信息, 但是内存管理的数据结构都没有建立, 而我们这些数据结构创建的过程本身就是一个内存分配的过程, 那么就出现一个问题
|
||||
|
||||
|
||||
我们还没有一个内存管理器去负责分配和回收内存, 而我们又不可能将所有的内存信息都静态创建并初始化, 那么我们怎么分配内存管理器所需要的内存呢? 现在我们进入了一个先有鸡还是先有蛋的怪圈, 这种问题的一般解决方法是, 我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器), 用它来负责系统初始化初期的内存管理, 最重要的, 用它来初始化我们内存的数据结构, 直到我们真正的内存管理器被初始化完成并能投入使用, 我们将旧的内存管理器丢掉,
|
||||
我们还没有一个内存管理器去负责分配和回收内存, 而我们又不可能将所有的内存信息都静态创建并初始化, 那么我们怎么分配内存管理器所需要的内存呢? 现在我们进入了一个先有鸡还是先有蛋的怪圈, 这种问题的一般解决方法是, 我们先实现一个满足要求的但是可能效率不高的笨家伙(内存管理器), 用它来负责系统初始化初期的内存管理, 最重要的, 用它来初始化我们内存的数据结构, 直到我们真正的内存管理器被初始化完成并能投入使用, 我们将旧的内存管理器丢掉
|
||||
|
||||
即因此在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块早期的**引导内存分配器(boot memory allocator--bootmem分配器)**或者**memblock**, 用于在启动阶段早期分配内存, 而在系统初始化完成后, 该分配器被内核抛弃, 然后初始化了一套新的更加完善的内存分配器.
|
||||
|
||||
|
||||
##3.1 引导内存分配器bootmem
|
||||
|
||||
##2.1 引导内存分配器bootmem
|
||||
-------
|
||||
|
||||
在启动过程期间, 尽管内存管理尚未初始化, 但是内核仍然需要分配内存以创建各种数据结构, 早期的内核中负责初始化阶段的内存分配器称为**引导内存分配器(boot memory allocator--bootmem分配器)**, 在耳熟能详的伙伴系统建立前内存都是利用分配器来分配的,伙伴系统框架建立起来后,bootmem会过度到伙伴系统. 显然, 对该内存分配器的需求集中于简单性方面, 而不是性能和通用性, 它仅用于初始化阶段. 因此内核开发者决定实现一个最先适配(first-first)分配器用于在启动阶段管理内存. 这是可能想到的最简单的方式.
|
||||
@@ -218,16 +220,30 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
bootmem_data的结构定义在[include/linux/bootmem.h?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L28), 其定义如下所示
|
||||
|
||||
|
||||
关于引导内存分配器的具体内容, 请参见另外一篇博文
|
||||
|
||||
##3.2 memblock内存分配器
|
||||
| CSDN | GitHub |
|
||||
|:-----:|:------:|
|
||||
| [引导内存分配器bootmem](带添加链接) | [study/kernel/02-memory/03-initialize/02-bootmem](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/02-bootmem) |
|
||||
|
||||
|
||||
##2.2 memblock内存分配器
|
||||
-------
|
||||
|
||||
|
||||
但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核维护了**memblock内存管理器**, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem, Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. 并最终替代bootmem成为初始化阶段的内存管理器
|
||||
但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核维护了**memblock内存分配器**, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem, Memblock以前被定义为Logical Memory Block( 逻辑内存块), 但根据[Yinghai Lu的补丁](https://lkml.org/lkml/2010/7/13/68), 它被重命名为memblock. 并最终替代bootmem成为初始化阶段的内存管理器
|
||||
|
||||
关于引导内存分配器的具体内容, 请参见另外一篇博文
|
||||
|
||||
|
||||
| CSDN | GitHub |
|
||||
|:-----:|:------:|
|
||||
| [memblock内存分配器](带添加链接) | [study/kernel/02-memory/03-initialize/03-memblock](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/03-initialize/03-memblock) |
|
||||
|
||||
|
||||
|
||||
##3.3 两者的区别和兼容性
|
||||
|
||||
##2.3 两者的区别与联系
|
||||
|
||||
bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved
|
||||
|
||||
@@ -260,37 +276,88 @@ endif
|
||||
由于接口是一致的, 那么他们共同使用一份
|
||||
|
||||
| 头文件 | bootmem接口 | nobootmem接口 |
|
||||
|:-------:|:----------------:|:-------------------:|
|
||||
|:-----:|:-----------:|:------------:|
|
||||
| [include/linux/bootmem.h](http://lxr.free-electrons.com/source/include/linux/bootmem.h) | [mm/bootmem.c](http://lxr.free-electrons.com/source/mm/bootmem.c) | [mm/nobootmem.c](http://lxr.free-electrons.com/source/mm/nobootmem.c) |
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#4 第三阶段
|
||||
##2.4 memblock的初始化(arm64架构)
|
||||
-------
|
||||
|
||||
|
||||
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
| 流程 | 描述 |
|
||||
|:---:|:----:|
|
||||
| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 |
|
||||
| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 |
|
||||
| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 |
|
||||
|
||||
|
||||
|
||||
|
||||
其中arm64_memblock_init就完成了arm64架构下的memblock的初始化
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#3 初始化buddy内存管理
|
||||
#3 第二阶段(初始化buddy内存管理)
|
||||
-------
|
||||
|
||||
|
||||
在arm64架构下, 内核在start_kernel()->setup_arch()函数中依次完成了如下工作
|
||||
|
||||
下面我们就以标准的arm架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同
|
||||
|
||||
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
| 流程 | 描述 |
|
||||
|:---:|:----:|
|
||||
| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 |
|
||||
| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 |
|
||||
| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 |
|
||||
|
||||
|
||||
其中arm64_memblock_init就完成了arm64架构下的memblock的初始化.
|
||||
|
||||
|
||||
而setup_arch则主要完成如下工作
|
||||
|
||||
|
||||
* 调用arm64_memblock_init来完成了memblock的初始化
|
||||
|
||||
* paging_init初始化内存的分页机制
|
||||
|
||||
* bootmem_init初始化内存管理
|
||||
|
||||
|
||||
##3.1 初始化流程
|
||||
-------
|
||||
|
||||
下面我们就以arm64架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同
|
||||
|
||||
* 首先内核从[start_kernel](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L505)开始启动
|
||||
|
||||
@@ -313,9 +380,11 @@ arm64在整个初始化的流程上并没有什么不同, 但是有细微的差
|
||||
* 也是因为上面的原因, arm上paging_init有两份代码([mmu](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L162)和[nonmmu](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)), 为了降低代码的耦合性, arm通过setup_arch调用paging_init函数, 后者进一步调用了bootmem_init来完成, 而arm64上不存在这样的问题, 则在[setup_arch中顺序的先用paging_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L266)初始化了页表, 然后[setup_arch又调用bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v4.7#L271)来完成了bootmem的初始化
|
||||
|
||||
|
||||
##3.1 pagging_init初始化分页机制
|
||||
|
||||
##3.2 paging_init初始化分页机制
|
||||
-------
|
||||
|
||||
|
||||
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响
|
||||
|
||||
因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。
|
||||
@@ -323,17 +392,27 @@ paging_init负责建立只能用于内核的页表, 用户空间是无法访问
|
||||
在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB
|
||||
用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间.
|
||||
|
||||
这些划分主要的动机如下所示。
|
||||
这些划分主要的动机如下所示
|
||||
|
||||
* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用.
|
||||
|
||||
* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作.
|
||||
|
||||
如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现.
|
||||
如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现.
|
||||
|
||||
虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的
|
||||
|
||||

|
||||
|
||||
##3.3 虚拟地址空间(以x86_32位系统为例)
|
||||
-------
|
||||
|
||||
|
||||
|
||||
出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段
|
||||
|
||||
@@ -357,9 +436,24 @@ paging_init负责建立只能用于内核的页表, 用户空间是无法访问
|
||||
的关联可以自行定义,关联建立后内核总是会注意到的
|
||||
|
||||
|
||||
同样我们的用户空间, 也被划分为几个段
|
||||
同样我们的[用户空间](http://www.360doc.com/content/14/1020/21/19947352_418512226.shtml), 也被划分为几个段, 包括从高地址到低地址分别为 :
|
||||
|
||||
##3.2 bootmem_init
|
||||
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
| 区域 | 存储内容 |
|
||||
|:---:|:------:|
|
||||
| 栈 | 局部变量, 函数参数, 返回地址等 |
|
||||
| 堆 | 动态分配的内存 |
|
||||
| BSS段 | 未初始化或初值为0的全局变量和静态局部变量|
|
||||
| 数据段 | 一初始化且初值非0的全局变量和静态局部变量|
|
||||
| 代码段 | 可执行代码, 字符串面值, 只读变量 |
|
||||
|
||||
|
||||
##3.4 bootmem_init初始化内存
|
||||
-------
|
||||
|
||||
在paging_init之后, 系统的页帧已经建立起来, 然后通过bootmem_init中, 系统开始完成bootmem的初始化工作.
|
||||
@@ -368,8 +462,7 @@ paging_init负责建立只能用于内核的页表, 用户空间是无法访问
|
||||
不同的体系结构bootmem_init的实现, 没有很大的区别, 但是在初始化的过程中, 其中的很多函数, 依据系统是NUMA还是UMA结构则有不同的定义
|
||||
|
||||
|
||||
|
||||
###4.2.1 bootmem_init函数
|
||||
bootmem_init函数的实现如下
|
||||
|
||||
| 函数实现 | arm | arm64 |
|
||||
|:---:|:---:|:-----:|
|
||||
@@ -448,10 +541,43 @@ void __init bootmem_init(void)
|
||||
memblock_dump_all();
|
||||
```
|
||||
|
||||
* 通过zone_sizes_init函数设置内存区域大小
|
||||
|
||||
* memblock_dump_all舍弃memblock
|
||||
|
||||
|
||||
###4.2.2 函数设置内存区域大小
|
||||
-------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
/////////////////////////////////////////
|
||||
|
||||
|
||||
|
||||
@@ -493,7 +619,7 @@ void __init bootmem_init(void)
|
||||
|
||||
|
||||
|
||||
###4.2.3 zone_sizes_init初始化节点和内存域
|
||||
###3.4.3 zone_sizes_init初始化节点和内存域
|
||||
-------
|
||||
|
||||
|
||||
|
||||
+5
-74
@@ -57,84 +57,14 @@ Linux把物理内存划分为三个层次来管理
|
||||
* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号.
|
||||
|
||||
|
||||
##1.2 内存结点pg_data_t
|
||||
|
||||
##1.2 start_kernel启动过程
|
||||
-------
|
||||
|
||||
在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。
|
||||
|
||||
|
||||
* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理
|
||||
|
||||
* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node)
|
||||
|
||||
可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 而UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关.
|
||||
|
||||
**NODE_DATA(node_id)查找编号node_id的结点pg_data_t信息** 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA)
|
||||
|
||||
```cpp
|
||||
extern struct pglist_data *node_data[];
|
||||
#define NODE_DATA(nid) (node_data[(nid)])
|
||||
```
|
||||
|
||||
|
||||
在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858)
|
||||
|
||||
|
||||
```cpp
|
||||
extern struct pglist_data contig_page_data;
|
||||
#define NODE_DATA(nid) (&contig_page_data)
|
||||
|
||||
```
|
||||
|
||||
##1.2 物理内存区域
|
||||
-------
|
||||
|
||||
因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束.
|
||||
|
||||
* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址
|
||||
|
||||
* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分
|
||||
|
||||
|
||||
因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域
|
||||
|
||||
对于x86_32的机器,管理区(内存区域)类型如下分布
|
||||
|
||||
|
||||
| 类型 | 区域 |
|
||||
| :------- | ----: |
|
||||
| ZONE_DMA | 0~15MB |
|
||||
| ZONE_NORMAL | 16MB~895MB |
|
||||
| ZONE_HIGHMEM | 896MB~物理内存结束 |
|
||||
|
||||
|
||||
##1.3 物理页帧
|
||||
-------
|
||||
|
||||
内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位.
|
||||
|
||||
页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存.
|
||||
|
||||
|
||||
内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页.
|
||||
|
||||
出于节省内存的考虑,struct page中使用了大量的联合体union.
|
||||
|
||||
|
||||
`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691)
|
||||
|
||||
|
||||
UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量
|
||||
|
||||
##1.4 今日内容(启动过程中的内存初始化)
|
||||
-------
|
||||
|
||||
|
||||
**启动过程中的内存初始化**
|
||||
|
||||
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
|
||||
|
||||
**系统启动**
|
||||
|
||||
|
||||
首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479)
|
||||
|
||||
@@ -184,7 +114,8 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配<br>由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
|
||||
|
||||
|
||||
|
||||
##1.3 setup_arch设置
|
||||
-------
|
||||
|
||||
#2 引导内存分配器bootmem概述
|
||||
-------
|
||||
+33
@@ -143,6 +143,7 @@ http://www.maxwellxxx.com/linuxmemblock
|
||||
|
||||
https://0xax.gitbooks.io/linux-insides/content/mm/linux-mm-1.html
|
||||
|
||||
|
||||
#1 memblock的数据结构
|
||||
-------
|
||||
|
||||
@@ -199,6 +200,8 @@ struct memblock_type
|
||||
struct memblock_region *regions;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
该结构体存储的是内存类型信息
|
||||
|
||||
|
||||
@@ -1294,7 +1297,37 @@ void __init arm_memblock_init(const struct machine_desc *mdesc)
|
||||
##4.3 arm64下的memblock初始化
|
||||
-------
|
||||
|
||||
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
| 流程 | 描述 |
|
||||
|:---:|:----:|
|
||||
| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 |
|
||||
| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 |
|
||||
| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 |
|
||||
|
||||
|
||||
|
||||
|
||||
其中arm64_memblock_init就完成了arm64架构下的memblock的初始化
|
||||
|
||||
|
||||
与arm架构类似, arm64的memblock初始化没有意外, 只是初始化函数成为[arm64_memblock_init()](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L261), 该函数定义在[arch/arm64/mm/init.c?v=4.7, line 192](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L192)
|
||||
|
||||
|
||||
|
||||
#5 总结
|
||||
-------
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
初始化内存管理
|
||||
=======
|
||||
|
||||
|
||||
|
||||
| 日期 | 内核版本 | 架构| 作者 | GitHub| CSDN |
|
||||
| ------- |:-------:|:-------:|:-------:|:-------:|:-------:|
|
||||
| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) |
|
||||
|
||||
http://blog.csdn.net/vanbreaker/article/details/7554977
|
||||
|
||||
|
||||
http://blog.csdn.net/yuzhihui_no1/article/details/50759567
|
||||
|
||||
http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html
|
||||
|
||||
http://www.linuxidc.com/Linux/2012-05/60230.htm
|
||||
|
||||
|
||||
在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器.
|
||||
|
||||
|
||||
|
||||
#1 前景回顾
|
||||
-------
|
||||
|
||||
|
||||
##1.1 Linux内存管理的层次结构
|
||||
-------
|
||||
|
||||
|
||||
Linux把物理内存划分为三个层次来管理
|
||||
|
||||
| 层次 | 描述 |
|
||||
|:----:|:----:|
|
||||
| 存储节点(Node) | CPU被划分为多个节点(node), 内存则被分簇, 每个CPU对应一个本地物理内存, 即一个CPU-node对应一个内存簇bank,即每个内存簇被认为是一个节点 |
|
||||
| 管理区(Zone) | 每个物理内存节点node被划分为多个内存管理区域, 用于表示不同范围的内存, 内核可以使用不同的映射方式映射物理内存 |
|
||||
| 页面(Page) | 内存被细分为多个页面帧, 页面是最基本的页面分配的单位 |
|
||||
|
||||
为了支持NUMA模型,也即CPU对不同内存单元的访问时间可能不同,此时系统的物理内存被划分为几个节点(node), 一个node对应一个内存簇bank,即每个内存簇被认为是一个节点
|
||||
|
||||
|
||||
* 首先, 内存被划分为**结点**. 每个节点关联到系统中的一个处理器, 内核中表示为`pg_data_t`的实例. 系统中每个节点被链接到一个以NULL结尾的`pgdat_list`链表中<而其中的每个节点利用`pg_data_tnode_next`字段链接到下一节.而对于PC这种UMA结构的机器来说, 只使用了一个成为contig_page_data的静态pg_data_t结构.
|
||||
|
||||
* 接着各个节点又被划分为内存管理区域, 一个**管理区域**通过struct zone_struct描述, 其被定义为zone_t, 用以表示内存的某个范围, 低端范围的16MB被描述为ZONE_DMA, 某些工业标准体系结构中的(ISA)设备需要用到它, 然后是可直接映射到内核的普通内存域ZONE_NORMAL,最后是超出了内核段的物理地址域ZONE_HIGHMEM, 被称为高端内存. 是系统中预留的可用内存空间, 不能被内核直接映射.
|
||||
|
||||
|
||||
* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号.
|
||||
|
||||
|
||||
##1.2 Linux内存管理的3个阶段
|
||||
-------
|
||||
|
||||
|
||||
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
|
||||
|
||||
|
||||
因此我们可以把linux内核的内存管理分三个阶段。
|
||||
|
||||
| 阶段 | 起点 | 终点 | 描述 |
|
||||
|:-----:|:-----:|:-----:|
|
||||
| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 |
|
||||
| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 |
|
||||
| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 |
|
||||
|
||||
|
||||
|
||||
##1.3 系统启动阶段的内存初始化过程
|
||||
-------
|
||||
|
||||
首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479)
|
||||
|
||||
其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示
|
||||
|
||||
|
||||
```cpp
|
||||
asmlinkage __visible void __init start_kernel(void)
|
||||
{
|
||||
|
||||
setup_arch(&command_line);
|
||||
mm_init_cpumask(&init_mm);
|
||||
|
||||
setup_per_cpu_areas();
|
||||
|
||||
|
||||
build_all_zonelists(NULL, NULL);
|
||||
page_alloc_init();
|
||||
|
||||
|
||||
/*
|
||||
* These use large bootmem allocations and must precede
|
||||
* mem_init();
|
||||
* kmem_cache_init();
|
||||
*/
|
||||
mm_init();
|
||||
|
||||
kmem_cache_init_late();
|
||||
|
||||
kmemleak_init();
|
||||
setup_per_cpu_pageset();
|
||||
|
||||
rest_init();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
| 函数 | 功能 |
|
||||
|:----:|:----:|
|
||||
| [setup_arch](http://lxr.free-electrons.com/ident?v=4.7;i=setup_arch) | 是一个特定于体系结构的设置函数, 其中一项任务是负责初始化自举分配器 |
|
||||
| [mm_init_cpumask](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v=4.7#L522) | 初始化CPU屏蔽字 |
|
||||
| [setup_per_cpu_areas](http://lxr.free-electrons.com/ident?v=4.7;i=setup_per_cpu_areas) | 函数[(查看定义)](http://lxr.free-electrons.com/source/mm/percpu.c?v4.7#L2205])给每个CPU分配内存,并拷贝.data.percpu段的数据. 为系统中的每个CPU的per_cpu变量申请空间.<br>在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本. <br>此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本<br>在非SMP系统中这是一个空操作 |
|
||||
| [build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029) | 建立并初始化结点和内存域的数据结构 |
|
||||
| [mm_init](http://lxr.free-electrons.com/source/init/main.c?v4.7#L464) | 建立了内核的内存分配器, <br>其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)<br>然后调用kmem_cache_init函数初始化内核内部用于小块内存区的分配器 |
|
||||
| [kmem_cache_init_late](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378) | 在kmem_cache_init之后, 完善分配器的缓存机制, 当前3个可用的内核内存分配器[slab](http://lxr.free-electrons.com/source/mm/slab.c?v4.7#L1378), [slob](http://lxr.free-electrons.com/source/mm/slob.c?v4.7#L655), [slub](http://lxr.free-electrons.com/source/mm/slub.c?v=4.7#L3960)都会定义此函数 |
|
||||
| [kmemleak_init](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1857) | Kmemleak工作于内核态,Kmemleak 提供了一种可选的内核泄漏检测,其方法类似于跟踪内存收集器。当独立的对象没有被释放时,其报告记录在 [/sys/kernel/debug/kmemleak](http://lxr.free-electrons.com/source/mm/kmemleak.c?v=4.7#L1467)中, Kmemcheck能够帮助定位大多数内存错误的上下文 |
|
||||
| [setup_per_cpu_pageset](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5392) | 初始化CPU高速缓存行, 为pagesets的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配<br>由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. |
|
||||
|
||||
|
||||
|
||||
|
||||
##1.4 setup_arch函数初始化内存流程
|
||||
-------
|
||||
|
||||
|
||||
|
||||
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
| 流程 | 描述 |
|
||||
|:---:|:----:|
|
||||
| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 |
|
||||
| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 |
|
||||
| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 |
|
||||
|
||||
之前的文章中我们已经讲过了memblock初始化的过程, 现在我们来看看内核是如何初始化内存管理器的
|
||||
|
||||
|
||||
##1.5 (第一阶段)启动过程中的内存分配器
|
||||
-------
|
||||
|
||||
|
||||
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
|
||||
|
||||
这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的
|
||||
|
||||
|
||||
而第一阶段中memblock的初始化工作就是通过start_kernel->setup_arch->arm64_memblock_init来完成的
|
||||
|
||||
|
||||
##1.7 今日内容(分页机制初始化)
|
||||
-------
|
||||
|
||||
|
||||
|
||||
|
||||
在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制.
|
||||
|
||||
在分页机制完成后, 才会开始初始化系统的内存数据结构(包括内存节点数据和内存区域), 并在随后初始化buddy伙伴系统来接管内存管理的工作
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#2 分页机制初始化
|
||||
-------
|
||||
|
||||
|
||||
arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制
|
||||
|
||||
|
||||
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响
|
||||
|
||||
|
||||
|
||||
|
||||
##2.1 虚拟地址空间(以x86_32位系统为例)
|
||||
-------
|
||||
|
||||
|
||||
因此在仔细考察其实现之前,很重要的一点是解释该函数的目的
|
||||
|
||||
在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB
|
||||
用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间.
|
||||
|
||||
这些划分主要的动机如下所示
|
||||
|
||||
* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用.
|
||||
|
||||
* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作.
|
||||
|
||||
如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现.
|
||||
|
||||
虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的
|
||||
|
||||
|
||||
|
||||
出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
按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个用途。
|
||||
|
||||
* 虚拟内存中连续、但物理内存中不连续的内存区,可以在vmalloc区域分配。该机制通常用于用户过程,内核自身会试图尽力避免非连续的物理地址。内核通常会成功,因为大部分大的内存块都在启动时分配给内核,那时内存的碎片尚不严重。但在已经运行了很长时间的系统上,在内核需要物理内存时,就可能出现可用空间不连续的情况。此类情况,主要出现在动态加载模块时
|
||||
|
||||
* 持久映射用于将高端内存域中的非持久页映射到内核中
|
||||
|
||||
* 固定映射是与物理地址空间中的固定页关联的虚拟地址空间项,但具体关联的页帧可以自由
|
||||
选择。它与通过固定公式与物理内存关联的直接映射页相反,虚拟固定映射地址与物理内存位置之间
|
||||
的关联可以自行定义,关联建立后内核总是会注意到的
|
||||
|
||||
|
||||
同样我们的[用户空间](http://www.360doc.com/content/14/1020/21/19947352_418512226.shtml), 也被划分为几个段, 包括从高地址到低地址分别为 :
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
<br>
|
||||
|
||||
| 区域 | 存储内容 |
|
||||
|:---:|:------:|
|
||||
| 栈 | 局部变量, 函数参数, 返回地址等 |
|
||||
| 堆 | 动态分配的内存 |
|
||||
| BSS段 | 未初始化或初值为0的全局变量和静态局部变量|
|
||||
| 数据段 | 一初始化且初值非0的全局变量和静态局部变量|
|
||||
| 代码段 | 可执行代码, 字符串面值, 只读变量 |
|
||||
|
||||
|
||||
##2.2 paging_init初始化分页机制
|
||||
-------
|
||||
|
||||
paging_init函数定义在[arch/arm64/mm/mmu.c?v=4.7, line 538](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538)
|
||||
|
||||
```cpp
|
||||
/*
|
||||
* paging_init() sets up the page tables, initialises the zone memory
|
||||
* maps and sets up the zero page.
|
||||
*/
|
||||
void __init paging_init(void)
|
||||
{
|
||||
phys_addr_t pgd_phys = early_pgtable_alloc();
|
||||
pgd_t *pgd = pgd_set_fixmap(pgd_phys);
|
||||
|
||||
map_kernel(pgd);
|
||||
map_mem(pgd);
|
||||
|
||||
/*
|
||||
* We want to reuse the original swapper_pg_dir so we don't have to
|
||||
* communicate the new address to non-coherent secondaries in
|
||||
* secondary_entry, and so cpu_switch_mm can generate the address with
|
||||
* adrp+add rather than a load from some global variable.
|
||||
*
|
||||
* To do this we need to go via a temporary pgd.
|
||||
*/
|
||||
cpu_replace_ttbr1(__va(pgd_phys));
|
||||
memcpy(swapper_pg_dir, pgd, PAGE_SIZE);
|
||||
cpu_replace_ttbr1(swapper_pg_dir);
|
||||
|
||||
pgd_clear_fixmap();
|
||||
memblock_free(pgd_phys, PAGE_SIZE);
|
||||
|
||||
/*
|
||||
* We only reuse the PGD from the swapper_pg_dir, not the pud + pmd
|
||||
* allocated with it.
|
||||
*/
|
||||
memblock_free(__pa(swapper_pg_dir) + PAGE_SIZE,
|
||||
SWAPPER_DIR_SIZE - PAGE_SIZE);
|
||||
}
|
||||
```
|
||||
@@ -7,15 +7,6 @@
|
||||
| ------- |:-------:|:-------:|:-------:|:-------:|:-------:|
|
||||
| 2016-06-14 | [Linux-4.7](http://lxr.free-electrons.com/source/?v=4.7) | X86 & arm | [gatieme](http://blog.csdn.net/gatieme) | [LinuxDeviceDrivers](https://github.com/gatieme/LDD-LinuxDeviceDrivers) | [Linux内存管理](http://blog.csdn.net/gatieme/article/category/6225543) |
|
||||
|
||||
http://blog.csdn.net/vanbreaker/article/details/7554977
|
||||
|
||||
|
||||
http://blog.csdn.net/yuzhihui_no1/article/details/50759567
|
||||
|
||||
http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html
|
||||
|
||||
http://www.linuxidc.com/Linux/2012-05/60230.htm
|
||||
|
||||
|
||||
在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后奇偶内核才能检测到可用内存和寄存器.
|
||||
|
||||
@@ -48,94 +39,25 @@ Linux把物理内存划分为三个层次来管理
|
||||
* 最后**页帧(page frame)**代表了系统内存的最小单位, 堆内存中的每个页都会创建一个struct page的一个实例. 传统上,把内存视为连续的字节,即内存为字节数组,内存单元的编号(地址)可作为字节数组的索引. 分页管理时,将若干字节视为一页,比如4K byte. 此时,内存变成了连续的页,即内存为页数组,每一页物理内存叫页帧,以页为单位对内存进行编号,该编号可作为页数组的索引,又称为页帧号.
|
||||
|
||||
|
||||
##1.2 内存结点pg_data_t
|
||||
##1.2 今日内容(启动过程中的内存初始化)
|
||||
-------
|
||||
|
||||
在LINUX中引入一个数据结构`struct pglist_data` ,来描述一个node,定义在[`include/linux/mmzone.h`](http://lxr.free-electrons.com/source/include/linux/mmzone.h#L630) 文件中。(这个结构被typedef pg_data_t)。
|
||||
|
||||
|
||||
* 对于NUMA系统来讲, 整个系统的内存由一个[node_data](http://lxr.free-electrons.com/source/arch/s390/numa/numa.c?v=4.7#L23)的pg_data_t指针数组来管理
|
||||
|
||||
* 对于PC这样的UMA系统,使用struct pglist_data contig_page_data ,作为系统唯一的node管理所有的内存区域。(UMA系统中中只有一个node)
|
||||
|
||||
可以使用NODE_DATA(node_id)来查找系统中编号为node_id的结点, 而UMA结构下由于只有一个结点, 因此该宏总是返回全局的contig_page_data, 而与参数node_id无关.
|
||||
|
||||
**NODE_DATA(node_id)查找编号node_id的结点pg_data_t信息** 参见[NODE_DATA的定义](http://lxr.free-electrons.com/ident?v=4.7;i=NODE_DATA)
|
||||
|
||||
```cpp
|
||||
extern struct pglist_data *node_data[];
|
||||
#define NODE_DATA(nid) (node_data[(nid)])
|
||||
```
|
||||
|
||||
|
||||
在UMA结构的机器中, 只有一个node结点即contig_page_data, 此时NODE_DATA直接指向了全局的contig_page_data, 而与node的编号nid无关, 参照[include/linux/mmzone.h?v=4.7, line 858](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L858)
|
||||
|
||||
|
||||
```cpp
|
||||
extern struct pglist_data contig_page_data;
|
||||
#define NODE_DATA(nid) (&contig_page_data)
|
||||
|
||||
```
|
||||
|
||||
##1.2 物理内存区域
|
||||
-------
|
||||
|
||||
因为实际的计算机体系结构有硬件的诸多限制, 这限制了页框可以使用的方式. 尤其是, Linux内核必须处理80x86体系结构的两种硬件约束.
|
||||
|
||||
* ISA总线的直接内存存储DMA处理器有一个严格的限制 : 他们只能对RAM的前16MB进行寻址
|
||||
|
||||
* 在具有大容量RAM的现代32位计算机中, CPU不能直接访问所有的物理地址, 因为线性地址空间太小, 内核不可能直接映射所有物理内存到线性地址空间, 我们会在后面典型架构(x86)上内存区域划分详细讲解x86_32上的内存区域划分
|
||||
|
||||
|
||||
因此Linux内核对不同区域的内存需要采用不同的管理方式和映射方式, 因此内核将物理地址或者成用zone_t表示的不同地址区域
|
||||
|
||||
对于x86_32的机器,管理区(内存区域)类型如下分布
|
||||
|
||||
|
||||
| 类型 | 区域 |
|
||||
| :------- | ----: |
|
||||
| ZONE_DMA | 0~15MB |
|
||||
| ZONE_NORMAL | 16MB~895MB |
|
||||
| ZONE_HIGHMEM | 896MB~物理内存结束 |
|
||||
|
||||
|
||||
##1.3 物理页帧
|
||||
-------
|
||||
|
||||
内核把物理页作为内存管理的基本单位. 尽管处理器的最小可寻址单位通常是字, 但是, 内存管理单元MMU通常以页为单位进行处理. 因此,从虚拟内存的上来看,页就是最小单位.
|
||||
|
||||
页帧代表了系统内存的最小单位, 对内存中的每个页都会创建struct page的一个实例. 内核必须要保证page结构体足够的小,否则仅struct page就要占用大量的内存.
|
||||
|
||||
|
||||
内核用[struct page(include/linux/mm_types.h?v=4.7, line 45)](http://lxr.free-electrons.com/source/include/linux/mm_types.h?v4.7#L45)结构表示系统中的每个物理页.
|
||||
|
||||
出于节省内存的考虑,struct page中使用了大量的联合体union.
|
||||
|
||||
|
||||
`mem_map`是一个struct page的数组,管理着系统中所有的物理内存页面。在系统启动的过程中,创建和分配mem_map的内存区域, mem_map定义在[mm/page_alloc.c?v=4.7, line 6691](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6691)
|
||||
|
||||
|
||||
UMA体系结构中,free_area_init函数在系统唯一的struct node对象contig_page_data中node_mem_map成员赋值给全局的mem_map变量
|
||||
|
||||
##1.4 今日内容(启动过程中的内存初始化)
|
||||
-------
|
||||
|
||||
|
||||
**启动过程中的内存初始化**
|
||||
|
||||
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
|
||||
|
||||
|
||||
因此我们可以把linux内核的内存管理分三个阶段。
|
||||
|
||||
**建立内存管理的数据结构**
|
||||
|
||||
对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化.
|
||||
| 阶段 | 起点 | 终点 | 描述 |
|
||||
|:-----:|:-----:|:-----:|
|
||||
| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 |
|
||||
| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 |
|
||||
| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 |
|
||||
|
||||
|
||||
|
||||
##1.3 start_kernel系统启动阶段的内存初始化过程
|
||||
|
||||
##1.5 系统启动阶段的内存初始化过程
|
||||
-------
|
||||
|
||||
首先我们来看看start_kernel是如何初始化系统的, start_kerne定义在[init/main.c?v=4.7, line 479](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L479)
|
||||
|
||||
@@ -187,17 +109,232 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
|
||||
|
||||
|
||||
#2 节点和内存域的初始化
|
||||
##1.4 setup_arch函数初始化内存流程
|
||||
-------
|
||||
|
||||
|
||||
##2.1 zone_sizes_init
|
||||
前面我们的内核从start_kernel开始, 进入setup_arch(), 并完成了早期内存分配器的初始化和设置工作.
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
| 流程 | 描述 |
|
||||
|:---:|:----:|
|
||||
| [arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) | 初始化memblock内存分配器 |
|
||||
| [paging_init](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | 初始化分页机制 |
|
||||
| [bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | 初始化内存管理 |
|
||||
|
||||
|
||||
该函数主要执行了如下操作
|
||||
|
||||
|
||||
1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作
|
||||
|
||||
2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作
|
||||
|
||||
3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作
|
||||
|
||||
|
||||
|
||||
##1.5 (第一阶段)启动过程中的内存分配器
|
||||
-------
|
||||
|
||||
在内核首先通过setup_arch()-->paging_init()-->bootmem_init()-->zone_sizes_init()来初始化节点和管理区的一些数据项
|
||||
|
||||
在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉.
|
||||
|
||||
这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的
|
||||
|
||||
|
||||
|
||||
|
||||
##1.6 今日内容(第二阶段--初始化buddy内存管理)
|
||||
-------
|
||||
|
||||
|
||||
**初始化内存分页机制**
|
||||
|
||||
|
||||
在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作.
|
||||
|
||||
|
||||
在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制
|
||||
|
||||
|
||||
paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响
|
||||
|
||||
|
||||
|
||||
**特定于体系结构的设置**
|
||||
|
||||
在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置
|
||||
|
||||
|
||||
**建立内存管理的数据结构**
|
||||
|
||||
对相关数据结构的初始化是从全局启动函数start_kernel中开始的, 该函数在加载内核并激活各个子系统之后执行. 由于内存管理是内核一个非常重要的部分, 因此在特定体系结构的设置步骤中检测并确定系统中内存的分配情况后, 会立即执行内存管理的初始化.
|
||||
|
||||
|
||||
|
||||
|
||||
**移交早期的分配器到内存管理器**
|
||||
|
||||
最后我们的内存管理器已经初始化并设置完成, 可以投入运行了, 因此内核将内存管理的工作从早期的内存分配器(bootmem或者memblock)移交到我们的buddy伙伴系统.
|
||||
|
||||
|
||||
|
||||
|
||||
#2 初始化前的准备工作
|
||||
-------
|
||||
|
||||
|
||||
##2.1 回到setup_arch函数(当前已经完成的工作)
|
||||
-------
|
||||
|
||||
|
||||
现在我们回到start_kernel()->setup_arch()函数
|
||||
|
||||
|
||||
```cpp
|
||||
void __init setup_arch(char **cmdline_p)
|
||||
{
|
||||
/* 初始化memblock */
|
||||
arm64_memblock_init( );
|
||||
|
||||
/* 分页机制初始化 */
|
||||
paging_init();
|
||||
|
||||
bootmem_init();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
到目前位置我们已经完成了如下工作
|
||||
|
||||
* memblock已经通过arm64_memblock_init完成了初始化, 至此系统中的内存可以通过memblock分配了
|
||||
|
||||
* paging_init完成了分页机制的初始化, 至此内核已经布局了一套完整的虚拟内存空间
|
||||
|
||||
|
||||
至此我们所有的内存都可以通过memblock机制来分配和释放, 尽管它实现的笨拙而简易, 但是已经足够我们初始化阶段使用了, 反正内核页不可能指着它过一辈子, 而我们也通过pagging_init创建了页表, 为内核提供了一套可供内核和进程运行的虚拟运行空间, 我们可以安全的进行内存的分配了
|
||||
|
||||
因此该是时候初始化我们强大的buddy系统了.
|
||||
|
||||
内核接着setup_arch()->bootmem_init()函数开始执行
|
||||
|
||||
|
||||
##2.2 bootmem_init函数
|
||||
-------
|
||||
|
||||
|
||||
arm64架构下, 在setup_arch中通过paging_init函数初始化内核分页机制之后, 内核通过`bootmem_init()`开始完成内存结点和内存区域的初始化工作, 该函数定义在[arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306)
|
||||
|
||||
```cpp
|
||||
void __init bootmem_init(void)
|
||||
{
|
||||
unsigned long min, max;
|
||||
|
||||
min = PFN_UP(memblock_start_of_DRAM());
|
||||
max = PFN_DOWN(memblock_end_of_DRAM());
|
||||
|
||||
early_memtest(min << PAGE_SHIFT, max << PAGE_SHIFT);
|
||||
|
||||
max_pfn = max_low_pfn = max;
|
||||
|
||||
arm64_numa_init();
|
||||
/*
|
||||
* Sparsemem tries to allocate bootmem in memory_present(), so must be
|
||||
* done after the fixed reservations.
|
||||
*/
|
||||
arm64_memory_present();
|
||||
|
||||
sparse_init();
|
||||
zone_sizes_init(min, max);
|
||||
|
||||
high_memory = __va((max << PAGE_SHIFT) - 1) + 1;
|
||||
memblock_dump_all();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##2.3 zone_sizes_init函数
|
||||
-------
|
||||
|
||||
|
||||
在初始化内存结点和内存域之前, 内核首先通过setup_arch()-->bootmem_init()-->zone_sizes_init()来初始化节点和管理区的一些数据项
|
||||
|
||||
|
||||
|
||||
[zone_sizes_init](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92)函数定义在[arch/arm64/mm/init.c?v=4.7, line 92](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92), 由于arm64支持NUMA和UMA两种存储器架构, 因此该函数依照NUMA和UMA, 有两种不同的实现.
|
||||
|
||||
```cpp
|
||||
#ifdef CONFIG_NUMA
|
||||
|
||||
static void __init zone_sizes_init(unsigned long min, unsigned long max)
|
||||
{
|
||||
unsigned long max_zone_pfns[MAX_NR_ZONES] = {0};
|
||||
|
||||
if (IS_ENABLED(CONFIG_ZONE_DMA))
|
||||
max_zone_pfns[ZONE_DMA] = PFN_DOWN(max_zone_dma_phys());
|
||||
max_zone_pfns[ZONE_NORMAL] = max;
|
||||
|
||||
free_area_init_nodes(max_zone_pfns);
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
static void __init zone_sizes_init(unsigned long min, unsigned long max)
|
||||
{
|
||||
struct memblock_region *reg;
|
||||
unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
|
||||
unsigned long max_dma = min;
|
||||
|
||||
memset(zone_size, 0, sizeof(zone_size));
|
||||
|
||||
/* 4GB maximum for 32-bit only capable devices */
|
||||
#ifdef CONFIG_ZONE_DMA
|
||||
max_dma = PFN_DOWN(arm64_dma_phys_limit);
|
||||
zone_size[ZONE_DMA] = max_dma - min;
|
||||
#endif
|
||||
zone_size[ZONE_NORMAL] = max - max_dma;
|
||||
|
||||
memcpy(zhole_size, zone_size, sizeof(zhole_size));
|
||||
|
||||
for_each_memblock(memory, reg) {
|
||||
unsigned long start = memblock_region_memory_base_pfn(reg);
|
||||
unsigned long end = memblock_region_memory_end_pfn(reg);
|
||||
|
||||
if (start >= max)
|
||||
continue;
|
||||
|
||||
#ifdef CONFIG_ZONE_DMA
|
||||
if (start < max_dma) {
|
||||
unsigned long dma_end = min(end, max_dma);
|
||||
zhole_size[ZONE_DMA] -= dma_end - start;
|
||||
}
|
||||
#endif
|
||||
if (end > max_dma) {
|
||||
unsigned long normal_end = min(end, max);
|
||||
unsigned long normal_start = max(start, max_dma);
|
||||
zhole_size[ZONE_NORMAL] -= normal_end - normal_start;
|
||||
}
|
||||
}
|
||||
|
||||
free_area_init_node(0, zone_size, min, zhole_size);
|
||||
}
|
||||
|
||||
#endif /* CONFIG_NUMA */
|
||||
```
|
||||
|
||||
在获取了三个管理区的页面数后,通过free_area_init_nodes()来完成后续工作, 其中核心函数为free_area_init_node(),用来针对特定的节点进行初始化
|
||||
|
||||
至此,节点和管理区的关键数据已完成初始化,内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行
|
||||
@@ -205,12 +342,26 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化
|
||||
|
||||
|
||||
|
||||
|
||||
##build_all_zonelists
|
||||
##2.4 free_area_init_node
|
||||
-------
|
||||
|
||||
|
||||
|
||||
#3 初始化内存结点和内存域
|
||||
-------
|
||||
|
||||
|
||||
|
||||
##3.1 回到start_kernel函数(已经完成的工作)
|
||||
-------
|
||||
|
||||
|
||||
|
||||
##3.2 build_all_zonelists
|
||||
-------
|
||||
|
||||
|
||||
|
||||
内核在start_kernel中通过build_all_zonelists完成了内存结点及其管理内存域的初始化工作, 调用如下
|
||||
|
||||
|
||||
@@ -220,7 +371,8 @@ asmlinkage __visible void __init start_kernel(void)
|
||||
|
||||
[build_all_zonelists](http://lxr.free-electrons.com/source/mm/page_alloc.c?v4.7#L5029)建立内存管理结点及其内存域所需的数据结构.
|
||||
|
||||
##2.1 设置结点初始化顺序
|
||||
|
||||
##2.3 设置结点初始化顺序
|
||||
-------
|
||||
|
||||
在build_all_zonelists开始, 首先内核通过set_zonelist_order函数设置了`zonelist_order`,如下所示, 参见[mm/page_alloc.c?v=4.7, line 5031](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5031)
|
||||
@@ -234,7 +386,7 @@ void __ref build_all_zonelists(pg_data_t *pgdat, struct zone *zone)
|
||||
```
|
||||
|
||||
|
||||
##2.1.1 zone table
|
||||
##2.3.1 zone table
|
||||
-------
|
||||
|
||||
|
||||
@@ -258,7 +410,7 @@ EXPORT_SYMBOL(zone_table);
|
||||
|
||||
|
||||
|
||||
##2.1.2 内存域初始化顺序zonelist_order
|
||||
##2.3.2 内存域初始化顺序zonelist_order
|
||||
-------
|
||||
|
||||
|
||||
@@ -338,7 +490,7 @@ static char zonelist_order_name[3][8] = {"Default", "Node", "Zone"};
|
||||
|
||||
|
||||
|
||||
##2.1.3 set_zonelist_order设置排列方式
|
||||
##2.3.3 set_zonelist_order设置排列方式
|
||||
-------
|
||||
|
||||
内核就通过通过set_zonelist_order函数设置当前系统的内存域排列方式current_zonelist_order, 其定义依据系统的NUMA结构还是UMA结构有很大的不同.
|
||||
@@ -382,7 +534,7 @@ static void set_zonelist_order(void)
|
||||
|
||||
2. 当前的排列方式不是默认方式, 则设置为user_zonelist_order指定的内存域排列方式
|
||||
|
||||
##2.1.4 default_zonelist_order函数选择最优的配置
|
||||
##2.3.4 default_zonelist_order函数选择最优的配置
|
||||
-------
|
||||
|
||||
在UMA结构下, 内存域使用NODE和ZONE两个排列方式会产生相同的效果, 因此系统不用特殊指定, 直接通过set_zonelist_order函数, 将当前系统的内存域排列方式`current_zonelist_order`配置为为ZONE方式(与NODE效果相同)即可
|
||||
@@ -420,7 +572,7 @@ static int default_zonelist_order(void)
|
||||
|
||||
|
||||
|
||||
###2.1.5 user_zonelist_order用户指定排列方式
|
||||
###2.3.5 user_zonelist_order用户指定排列方式
|
||||
-------
|
||||
|
||||
|
||||
@@ -499,11 +651,11 @@ static __init int setup_numa_zonelist_order(char *s)
|
||||
early_param("numa_zonelist_order", setup_numa_zonelist_order);
|
||||
```
|
||||
|
||||
##2.2 build_all_zonelists_init
|
||||
##2.4 build_all_zonelists_init
|
||||
-------
|
||||
|
||||
|
||||
###2.2.1 system_state系统状态标识
|
||||
###2.4.1 system_state系统状态标识
|
||||
|
||||
其中`system_state`变量是一个系统全局定义的用来表示系统当前运行状态的枚举变量, 其定义在[include/linux/kernel.h?v=4.7, line 487](http://lxr.free-electrons.com/source/include/linux/kernel.h?v=4.7#L487)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Reference in New Issue
Block a user