diff --git a/study/kernel/02-memory/03-initialize/00-initialize/README.md b/study/kernel/02-memory/03-initialize/00-initialize/README.md index e03a0c9..7216bef 100644 --- a/study/kernel/02-memory/03-initialize/00-initialize/README.md +++ b/study/kernel/02-memory/03-initialize/00-initialize/README.md @@ -1,677 +1,674 @@ -启动过程期间的内存管理--bootmem分配器 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | 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) | - - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. - - -而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. - - - - -#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 内存结点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)。 - - -* 对于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.6 今日内容(启动过程中的内存初始化) -------- - - - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -因此我们可以把linux内核的内存管理分三个阶段。 - -| 阶段 | 起点 | 终点 | 描述 | -|:-----:|:-----:|:-----:| -| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | -| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | -| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | - - - -**系统启动过程中的内存管理** - - -首先我们来看看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) -{ - - /* 设置特定架构的信息 - * 同时初始化memblock */ - 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变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非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) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用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的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在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?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L28), 其定义如下所示 - - -关于引导内存分配器的具体内容, 请参见另外一篇博文 - -| 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成为初始化阶段的内存管理器 - -关于引导内存分配器的具体内容, 请参见另外一篇博文 - - -| 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) | - - - - -##2.3 两者的区别与联系 - -bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved - -memory链表维护系统的内存信息(在初始化阶段通过bios获取的), 对于任何内存分配, 先去查找memory链表, 然后在reserve链表上记录(新增一个节点,或者合并) - - -1. 两者都可以分配小于一页的内存; - -2. 两者都是就近查找可用的内存, bootmem是从低到高找, memblock是从高往低找; - - -在boot传递给kernel memory bank相关信息后,kernel这边会以memblcok的方式保存这些信息,当buddy system 没有起来之前,在kernel中也是要有一套机制来管理memory的申请和释放. - - -Kernel可以选择nobootmem 或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -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) | - -##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内存管理) -------- - - -在arm64架构下, 内核在start_kernel()->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) | 初始化内存管理 | - - -其中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)开始启动 - - -* 然后进入体系结构相关的设置部分[setup_arch](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 开始获取并设置指定体系结构的一些物理信息, 而arm64架构下则对应着[rch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) - - -* 在setup_arch函数内, 通过paging_init函数初始化了分页机制和页表的信息 - - -* 接着paging_init函数通过[bootmem_init](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c#L1642)开始进行初始化工作 - - -arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别 - - -* 由于arm是在后期才开始加入了MMU内存管理单元的, 因此内核必须实现mmu和nonmmu两套不同的代码, 这主要是提现在分页机制的不同上, 因而paging_init分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 但是它们均调用了bootmem_init来完成初始化 - - -* 也是因为上面的原因, 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.2 paging_init初始化分页机制 -------- - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 - -因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。 - -在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB -用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间. - -这些划分主要的动机如下所示 - -* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用. - -* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作. - -如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现. - -虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的 - - -##3.3 虚拟地址空间(以x86_32位系统为例) -------- - - - -出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间 - - - -![虚拟地址空间](../images/vmarea_space.jpg) - -按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段 - -![内核空间](../images/kernel_space.jpg) - -地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量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), 也被划分为几个段, 包括从高地址到低地址分别为 : - - - -![进程的虚拟地址空间](../images/user_space.jpg) - -
- -| 区域 | 存储内容 | -|:---:|:------:| -| 栈 | 局部变量, 函数参数, 返回地址等 | -| 堆 | 动态分配的内存 | -| BSS段 | 未初始化或初值为0的全局变量和静态局部变量| -| 数据段 | 一初始化且初值非0的全局变量和静态局部变量| -| 代码段 | 可执行代码, 字符串面值, 只读变量 | - - -##3.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](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282) | [arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | - - - - -##3.5 build_all_zonelists初始化每个内存节点的zonelists -------- - -内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. - - -内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 - -```cpp -// 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个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 - - - -#4 总结 -------- - - - -##4.1 start_kernel启动流程 -------- - - -```cpp -start_kernel() - |---->page_address_init() - | 考虑支持高端内存 - | 业务:初始化page_address_pool链表; - | 将page_address_maps数组元素按索引降序插入 - | page_address_pool链表; - | 初始化page_address_htable数组. - | - |---->setup_arch(&command_line); - | 初始化特定体系结构的内容 - |---->arm64_memblock_init( ); [参见memblock和bootmem] - | 初始化引导阶段的内存分配器memblock - | - |---->paging_init(); [参见分页机制初始化paging_init] - | 分页机制初始化 - | - |---->bootmem_init(); [与build_all_zonelist共同完成内存数据结构的初始化] - | 初始化内存数据结构包括内存节点和内存域 - | - |---->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 - | - |---->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; - | -``` - - -##4.2 体系结构相关的初始化工作setup_arch -------- - - -```cpp -setup_arch(char **cmdline_p) - |---->arm64_memblock_init( ); - | 初始化引导阶段的内存分配器memblock - | - | - |---->paging_init(); - | 分页机制初始化 - | - | - |---->bootmem_init(); - | 初始化内存数据结构包括内存节点和内存域 -} -``` - -##4.3 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) -------- - -```cpp -bootmem_init(void) - |---->arm64_memblock_init( ); - | 初始化引导阶段的内存分配器memblock - | - |---->min = PFN_UP(memblock_start_of_DRAM()); - |---->max = PFN_DOWN(memblock_end_of_DRAM()); - | - | - |---->arm64_numa_init(); - | 支持numa架构 - |---->arm64_numa_init(); - 支持numa架构 - | - | - |---->zone_sizes_init(min, max); - 来初始化节点和管理区的一些数据项 - | - |---->free_area_init_node - | 初始化内存节点 - | - | - |---->free_area_init_core初始化zone - | - | - |---->memmap_init初始化page页面 - | - | - | - |---->memblock_dump_all(); - | 初始化完成, 显示memblock的保留的所有内存信息 -``` - - -##4.4 build_all_zonelists初始化每个内存节点的zonelists -------- - - -```cpp -void build_all_zonelists(void) - |---->set_zonelist_order() - |---->current_zonelist_order = ZONELIST_ORDER_ZONE; - | - |---->__build_all_zonelists(NULL); - | Memory不支持热插拔, 为每个zone建立后备的zone, - | 每个zone及自己后备的zone,形成zonelist - | - |---->pg_data_t *pgdat = NULL; - | pgdat = &contig_page_data;(单node) - | - |---->build_zonelists(pgdat); - | 为每个zone建立后备zone的列表 - | - |---->struct zonelist *zonelist = NULL; - | enum zone_type j; - | zonelist = &pgdat->node_zonelists[0]; - | - |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); - | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone - | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone - | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): - | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; - | node_zonelist[0]._zonerefs[0].zone_idx = 2; - | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; - | node_zonelist[0]._zonerefs[1].zone_idx = 1; - | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; - | node_zonelist[0]._zonerefs[2].zone_idx = 0; - | - | zonelist->_zonerefs[3].zone = NULL; - | zonelist->_zonerefs[3].zone_idx = 0; - | - |---->build_zonelist_cache(pgdat); - |---->pdat->node_zonelists[0].zlcache_ptr = NULL; - | UMA体系结构 - | - |---->for_each_possible_cpu(cpu) - | setup_pageset(&per_cpu(boot_pageset, cpu), 0); - |详见下文 - |---->vm_total_pages = nr_free_pagecache_pages(); - | 业务:获得所有zone中的present_pages总和. - | - |---->page_group_by_mobility_disabled = 0; - | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) -``` - - -版权声明知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 - +启动过程期间的内存管理--bootmem分配器 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | 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) | + + + +在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. + + +而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. + + + + +#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 内存结点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)。 + + +* 对于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.6 今日内容(启动过程中的内存初始化) +------- + + + + +在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. + + +因此我们可以把linux内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | +| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | +| 第三阶段 | buddy初始化完成 | 系统停止运行 | 可以用cache和buddy分配内存 | + + + +**系统启动过程中的内存管理** + + +首先我们来看看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) +{ + + /* 设置特定架构的信息 + * 同时初始化memblock */ + 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变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非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) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用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的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在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?v=4.7, line 28](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v=4.7#L28), 其定义如下所示 + + +关于引导内存分配器的具体内容, 请参见另外一篇博文 + +| 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成为初始化阶段的内存管理器 + +关于引导内存分配器的具体内容, 请参见另外一篇博文 + + +| 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) | + + + + +##2.3 两者的区别与联系 + +bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved + +memory链表维护系统的内存信息(在初始化阶段通过bios获取的), 对于任何内存分配, 先去查找memory链表, 然后在reserve链表上记录(新增一个节点,或者合并) + + +1. 两者都可以分配小于一页的内存; + +2. 两者都是就近查找可用的内存, bootmem是从低到高找, memblock是从高往低找; + + +在boot传递给kernel memory bank相关信息后,kernel这边会以memblcok的方式保存这些信息,当buddy system 没有起来之前,在kernel中也是要有一套机制来管理memory的申请和释放. + + +Kernel可以选择nobootmem 或者bootmem 来在buddy system起来之前管理memory. +这两种机制对提供的API是一致的,因此对用户是透明的 + +参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) + + +```cpp +ifdef CONFIG_NO_BOOTMEM + obj-y += nobootmem.o +else + obj-y += bootmem.o +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) | + +##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内存管理) +------- + + +在arm64架构下, 内核在start_kernel()->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) | 初始化内存管理 | + + +其中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)开始启动 + + +* 然后进入体系结构相关的设置部分[setup_arch](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 开始获取并设置指定体系结构的一些物理信息, 而arm64架构下则对应着[rch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) + + +* 在setup_arch函数内, 通过paging_init函数初始化了分页机制和页表的信息 + + +* 接着paging_init函数通过[bootmem_init](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c#L1642)开始进行初始化工作 + + +arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别 + + +* 由于arm是在后期才开始加入了MMU内存管理单元的, 因此内核必须实现mmu和nonmmu两套不同的代码, 这主要是提现在分页机制的不同上, 因而paging_init分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 但是它们均调用了bootmem_init来完成初始化 + + +* 也是因为上面的原因, 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.2 paging_init初始化分页机制 +------- + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + +因此在仔细考察其实现之前,很重要的一点是解释该函数的目的。 + +在x86_32系统上内核通常将总的4GB可用虚拟地址空间按3:1的比例划分给用户空间和内核空间, 虚拟地址空间的低端3GB +用于用户状态应用程序, 而高端的1GB则专用于内核. 尽管在分配内核的虚拟地址空间时, 当前系统上下文是不相干的, 但每个进程都有自身特定的地址空间. + +这些划分主要的动机如下所示 + +* 在用户应用程序的执行切换到核心态时(这总是会发生,例如在使用系统调用或发生周期性的时钟中断时),内核必须装载在一个可靠的环境中。因此有必要将地址空间的一部分分配给内核专用. + +* 物理内存页则映射到内核地址空间的起始处,以便内核直接访问,而无需复杂的页表操作. + +如果所有物理内存页都映射到用户空间进程能访问的地址空间中, 如果在系统上有几个应用程序在运行, 将导致严重的安全问题. 每个应用程序都能够读取和修改其他进程在物理内存中的内存区. 显然必须不惜任何代价防止这种情况出现. + +虽然用于用户层进程的虚拟地址部分随进程切换而改变,但是内核部分总是相同的 + + +##3.3 虚拟地址空间(以x86_32位系统为例) +------- + + + +出于内存保护等一系列的考虑, 内核将整个进程的虚拟运行空间划分为内核虚拟运行空间和内核虚拟运行空间 + + + +![虚拟地址空间](../images/vmarea_space.jpg) + +按3:1的比例划分地址空间, 只是约略反映了内核中的情况,内核地址空间作为内核的常驻虚拟地址空间, 自身又分为各个段 + +![内核空间](../images/kernel_space.jpg) + +地址空间的第一段用于将系统的所有物理内存页映射到内核的虚拟地址空间中。由于内核地址空间从偏移量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), 也被划分为几个段, 包括从高地址到低地址分别为 : + + + +![进程的虚拟地址空间](../images/user_space.jpg) + +
+ +| 区域 | 存储内容 | +|:---:|:------:| +| 栈 | 局部变量, 函数参数, 返回地址等 | +| 堆 | 动态分配的内存 | +| BSS段 | 未初始化或初值为0的全局变量和静态局部变量| +| 数据段 | 一初始化且初值非0的全局变量和静态局部变量| +| 代码段 | 可执行代码, 字符串面值, 只读变量 | + + +##3.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](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282) | [arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | + + + + +##3.5 build_all_zonelists初始化每个内存节点的zonelists +------- + +内核setup_arch的最后通过bootmem_init中完成了内存数据结构的初始化(包括内存结点pg_data_t, 内存管理域zone和页面信息page), 数据结构已经基本准备好了, 在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行. + + +内存节点pg_data_t中将内存节点中的内存区域zone按照某种组织层次存储在一个zonelist中, 即pglist_data->node_zonelists成员信息 + +```cpp +// 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个本地内存域都无法找到空闲内存,则查看其他结点。这种情况下,备选结点应该尽可能靠近主结点,以最小化访问非本地内存引起的性能损失。 + + + +#4 总结 +------- + + + +##4.1 start_kernel启动流程 +------- + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + |---->arm64_memblock_init( ); [参见memblock和bootmem] + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); [参见分页机制初始化paging_init] + | 分页机制初始化 + | + |---->bootmem_init(); [与build_all_zonelist共同完成内存数据结构的初始化] + | 初始化内存数据结构包括内存节点和内存域 + | + |---->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 + | + |---->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; + | +``` + + +##4.2 体系结构相关的初始化工作setup_arch +------- + + +```cpp +setup_arch(char **cmdline_p) + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + | + |---->paging_init(); + | 分页机制初始化 + | + | + |---->bootmem_init(); + | 初始化内存数据结构包括内存节点和内存域 +} +``` + +##4.3 bootmem_init初始化内存的基础数据结构(结点pg_data, 内存域zone, 页面page) +------- + +```cpp +bootmem_init(void) + |---->min = PFN_UP(memblock_start_of_DRAM()); + |---->max = PFN_DOWN(memblock_end_of_DRAM()); + | + | + |---->arm64_numa_init(); + | 支持numa架构 + |---->arm64_numa_init(); + | 支持numa架构 + | + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + | + |---->free_area_init_core初始化zone + | + | + |---->memmap_init初始化page页面 + | + | + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 +``` + + +##4.4 build_all_zonelists初始化每个内存节点的zonelists +------- + + +```cpp +void build_all_zonelists(void) + |---->set_zonelist_order() + |---->current_zonelist_order = ZONELIST_ORDER_ZONE; + | + |---->__build_all_zonelists(NULL); + | Memory不支持热插拔, 为每个zone建立后备的zone, + | 每个zone及自己后备的zone,形成zonelist + | + |---->pg_data_t *pgdat = NULL; + | pgdat = &contig_page_data;(单node) + | + |---->build_zonelists(pgdat); + | 为每个zone建立后备zone的列表 + | + |---->struct zonelist *zonelist = NULL; + | enum zone_type j; + | zonelist = &pgdat->node_zonelists[0]; + | + |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); + | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone + | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone + | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): + | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; + | node_zonelist[0]._zonerefs[0].zone_idx = 2; + | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; + | node_zonelist[0]._zonerefs[1].zone_idx = 1; + | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; + | node_zonelist[0]._zonerefs[2].zone_idx = 0; + | + | zonelist->_zonerefs[3].zone = NULL; + | zonelist->_zonerefs[3].zone_idx = 0; + | + |---->build_zonelist_cache(pgdat); + |---->pdat->node_zonelists[0].zlcache_ptr = NULL; + | UMA体系结构 + | + |---->for_each_possible_cpu(cpu) + | setup_pageset(&per_cpu(boot_pageset, cpu), 0); + |详见下文 + |---->vm_total_pages = nr_free_pagecache_pages(); + | 业务:获得所有zone中的present_pages总和. + | + |---->page_group_by_mobility_disabled = 0; + | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) +``` + + +版权声明知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 + diff --git a/study/kernel/02-memory/03-initialize/01-bootinit/README.md b/study/kernel/02-memory/03-initialize/01-bootinit/README.md deleted file mode 100644 index 649a76b..0000000 --- a/study/kernel/02-memory/03-initialize/01-bootinit/README.md +++ /dev/null @@ -1,979 +0,0 @@ -启动过程期间的内存管理--bootmem分配器 -======= - - - -| 日期 | 内核版本 | 架构| 作者 | 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) | - - - -在内存管理的上下文中, 初始化(initialization)可以有多种含义. 在许多CPU上, 必须显式设置适用于Linux内核的内存模型. 例如在x86_32上需要切换到保护模式, 然后内核才能检测到可用内存和寄存器. - - -而我们今天要讲的boot阶段就是系统初始化阶段使用的内存分配器. - - - - -#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 内存结点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)。 - - -* 对于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.6 今日内容(启动过程中的内存初始化) -------- - - - - -在初始化过程中, 还必须建立内存管理的数据结构, 以及很多事务. 因为内核在内存管理完全初始化之前就需要使用内存. 在系统启动过程期间, 使用了额外的简化悉尼股市的内存管理模块, 然后在初始化完成后, 将旧的模块丢弃掉. - - -因此我们可以把linux内核的内存管理分三个阶段。 - -| 阶段 | 起点 | 终点 | 描述 | -|:-----:|:-----:|:-----:| -| 第一阶段 | 系统启动 | bootmem或者memblock初始化完成 | 此阶段只能使用memblock_reserve函数分配内存, 早期内核中使用init_bootmem_done = 1标识此阶段结束 | -| 第二阶段 | bootmem或者memblock初始化完 | buddy完成前 | 引导内存分配器bootmem或者memblock接受内存的管理工作, 早期内核中使用mem_init_done = 1标记此阶段的结束 | -| 第三阶段 | buddy开始初始化 | 全部内存以及buddy初始化完毕 | 可以用cache和buddy分配内存 | - - -#2 第一阶段 -------- - - -#3 第二阶段 -------- - - - - -#4 第四阶段 -------- - - - - -##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) - -其代码很复杂, 我们只截取出其中与内存管理初始化相关的部分, 如下所示 - - -```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变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非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) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用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的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在CPU中设置一个最近存取页面的高速缓存硬件机制,当进行存储器访问时,先检查要访问的页面是否在高速缓存中. | - - - - - - -##2.2 引导过程中的内存初始化(bootmem和nobootmem) -------- - - - -由于硬件配置多种多样, 所以在编译时就静态初始化所有的内核存储结构是不现实的. 在boot传递给kernel memory bank相关信息后,kernel这边会以memblcok的方式保存这些信息,当buddy system 没有起来之前,在kernel中也是要有一套机制来管理memory的申请和释放. - - -Kernel可以选择nobootmem 或者bootmem 来在buddy system起来之前管理memory. -这两种机制对提供的API是一致的,因此对用户是透明的 - -参见[mm/Makefile](http://lxr.free-electrons.com/source/mm/Makefile#L44) - - -```cpp -ifdef CONFIG_NO_BOOTMEM - obj-y += nobootmem.o -else - obj-y += bootmem.o -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) | - - - - - - - -##2.2.1 初始化阶段的引导内存分配器bootmem -------- - - -不管是否使用bootmem分配器是系统启动初期的内存分配方式,在耳熟能详的伙伴系统建立前内存都是利用bootmem分配器来分配的,伙伴系统框架建立起来后,bootmem会过度到伙伴系统. - - -在启动过程期间, 尽管内存管理尚未初始化, 但是内核仍然需要分配内存以创建各种数据结构. 因此在系统启动过程期间, 内核使用了一个额外的简化形式的内存管理模块**引导内存分配器(boot memory allocator--bootmem分配器)**, 用于在启动阶段早期分配内存, 而在系统初始化完成后, 该分配器被内核抛弃, 然后初始化了一套新的更加完善的内存分配器. - -显然, 对该内存分配器的需求集中于简单性方面, 而不是性能和通用性, 它仅用于初始化阶段. 因此内核开发者决定实现一个最先适配(first-first)分配器用于在启动阶段管理内存. 这是可能想到的最简单的方式. - - -**引导内存分配器(boot memory allocator--bootmem分配器)**基于最先适配(first-first)分配器的原理(这儿是很多系统的内存分配所使用的原理), 使用一个位图来管理页, 以位图代替原来的空闲链表结构来表示存储空间, 位图的比特位的数目与系统中物理内存页面数目相同. 若位图中某一位是1, 则标识该页面已经被分配(已用页), 否则表示未被占有(未用页). - -在需要分配内存时, 分配器逐位的扫描位图, 直至找到一个能提供足够连续页的位置, 即所谓的最先最佳(first-best)或最先适配位置. - -该分配机制通过记录上一次分配的页面帧号(PFN)结束时的偏移量来实现分配大小小于一页的空间, 连续的小的空闲空间将被合并存储在一页上. - - - -**为什么在系统运行时抛弃bootmem** - -当系统运行时, 为何不继续使用bootmem分配机制呢? - -* 其中一个关键原因在于 : 但它每次分配都必须从头扫描位图, 每次通过对内存域进行线性搜索来实现分配. - -* 其次首先适应算法容易在内存的起始断留下许多小的空闲碎片, 在需要分配较大的空间页时, 检查位图的成本将是非常高的. - - - -引导内存分配器bootmem分配器简单却非常低效, 因此在内核完全初始化之后, 不能将该分配器继续欧诺个与内存管理, 而伙伴系统(连同slab, slub或者slob分配器)是一个好很多的备选方案. - - - -如果使用bootmem机制, 那么在初始化的初期内核首先初始化了bootmem自身, 然后接着又用bootmem分配和初始化了内存结点和管理域, 因此初始化bootmem的工作主要分成两步 - -* 初始化bootmem自身的数据结构 - -* 用bootmem初始化内存结点管理域 - - -##2.2.2 初始化阶段的nonbootmem(memblock) -------- - -但是bootmem也有很多问题. 最明显的就是外碎片的问题, 因此内核同时维护了memblock内存管理器, 同时用memblock实现了一份bootmem相同的兼容API, 即nobootmem - -bootmem是通过位图来管理,位图存在地地址段, 而memblock是在高地址管理内存, 维护两个链表, 即memory和reserved - -memory链表维护系统的内存信息(在初始化阶段通过bios获取的), 对于任何内存分配, 先去查找memory链表, 然后在reserve链表上记录(新增一个节点,或者合并) - - -1. 两者都可以分配小于一页的内存; - -2. 两者都是就近查找可用的内存, bootmem是从低到高找, memblock是从高往低找; - - - -##2.3 bootmem的初始化过程 -------- - - -目前大多数架构下, 都已经不再使用bootmem来进行内存的初始化了, 但是由于历史的影子, 即使是nobootmem的系统中我们仍然能够看到bootmem的一些影子, 因此我们首先来讲解一下bootmem下的系统初始化 - - - - - -##2.3.1 bootmem_data描述内存引导区 -------- - -内核用bootmem_data表示引导内存区域 - -即使是初始化用的最先适配分配器也必须使用一些数据结构存, 内核为系统中每一个结点都提供了一个struct bootmem_data结构的实例, 用于bootmem的内存管理. 它含有引导内存分配器给结点分配内存时所需的信息. 当然, 这时候内存管理还没有初始化, 因而该结构所需的内存是无法动态分配的, 必须在编译时分配给内核. - -在UMA系统上该分配的实现与CPU无关, 而NUMA系统内存结点与CPU相关联, 因此采用了特定体系结构的解决方法. - -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), 其定义如下所示 - -```cpp -#ifndef CONFIG_NO_BOOTMEM -/* -* node_bootmem_map is a map pointer - the bits represent all physical -* memory pages (including holes) on the node. -*/ -typedef struct bootmem_data { - unsigned long node_min_pfn; - unsigned long node_low_pfn; - void *node_bootmem_map; - unsigned long last_end_off; - unsigned long hint_idx; - struct list_head list; -} bootmem_data_t; - -extern bootmem_data_t bootmem_node_data[]; - -#endif -``` - -##2.3.2 初始化bootmem -------- - - -bootmem分配器的初始化是一个特定于体系结构的过程, 此外还取决于系统的内存布局 - -系统是从start_kernel开始启动的, 在启动过程中通过调用体系结构相关的setup_arch函数, 来获取初始化引导内存分配器所需的参数信息, 各种体系结构都有对应的函数来获取这些信息. - - -下面我们查看一下子早期内核中i386架构下的bootmem初始化流程 - -![i386上的初始化过程](../images/i386-setup_memory.jpg) - - -##2.4 memblcok(nonbootmem)的初始化流程 -------- - - - -目前大多数架构下都已经开始使用memblock来管理引导阶段的内存, 只有少数架构使用着早期的引导内存分配器bootmem - - - - - -#3 初始化buddy内存管理 -------- - - - - -系统是从start_kernel开始启动的, 在启动过程中通过调用体系结构相关的setup_arch函数, 来获取初始化引导内存分配器所需的参数信息, 各种体系结构都有对应的函数来获取这些信息, 在获取信息完成后 - - -| 调用层次 | 描述 | x86(已经不使用bootmem初始化) | arm | arm64 | -|:-------:|:---:|:---:|:---:|:-----:| -| setup_arch | 设置特定体系的信息 | [arch/x86/kernel/setup.c](http://lxr.free-electrons.com/source/arch/x86/kernel/setup.c?v=4.7#L857), 但是不再调用paging_init | [arch/arm/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 调用了[paging_init](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073) | [arch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L266), 调用了[paging_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L266)和[bootmem_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L271) | -| paging_init | 初始化分页机制 | 定义了[arch/x86/mm/init_32.c](http://lxr.free-electrons.com/source/arch/x86/mm/init_32.c?v=4.7#L695)和[arch/x86/mm/init_64.c](http://lxr.free-electrons.com/source/arch/x86/mm/init_64.c?v=4.7#L579)两个版本 | 分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 均调用了bootmem_init | [arch/arm64/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm64/mm/mmu.c?v=4.7#L538) | -| bootmem_init | 初始化bootmem分配器 | 无定义 | [arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282), 调用了zone_sizes_init | [arch/arm64/mm/init.c](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306),调用了zone_sizes_init | -| zone_sizes_init | 初始化节点和管理区
一般来说NUMA结构下会调用free_area_init_nodes完成所有内存结点的初始化, 而UMA结构下则会调用free_area_init_node完成唯一一个结点的初始化 | [arch/x86/mm/init.c](http://lxr.free-electrons.com/source/arch/x86/mm/init.c?v=4.7#L718), zone_sizes_init依据系统是NUMA还是UMA会有不同的定义 | [arch/arm/mm/init.c](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L137), 注意arm是非numa结构, 因此直接调用free_area_init_node完成初始化 | [arch/arm64/mm/init.c](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L92) | -| [free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) | 初始化结点中所有内存区 | [mm/page_alloc.c](http://lxr.free-electrons.com/ident?i=free_area_init_nodes), 体系结构无关 | [mm/page_alloc.c](http://lxr.free-electrons.com/ident?i=free_area_init_nodes), 体系结构无关 | [mm/page_alloc.c](http://lxr.free-electrons.com/ident?i=free_area_init_nodes), 体系结构无关 | -| free_area_init_node | 初始化单个节点域 | | | | - - - - - - -下面我们就以标准的arm架构来分析bootmem初始化内存结点和内存域的过程, 在讲解的过程中我们会兼顾的考虑arm64架构下的异同 - -* 首先内核从[start_kernel](http://lxr.free-electrons.com/source/init/main.c?v=4.7#L505)开始启动 - - -* 然后进入体系结构相关的设置部分[setup_arch](http://lxr.free-electrons.com/source/arch/arm/kernel/setup.c?v=4.7#L1073), 开始获取并设置指定体系结构的一些物理信息, 而arm64架构下则对应着[rch/arm64/kernel/setup.c](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229) - - -* 在setup_arch函数内, 通过paging_init函数初始化了分页机制和页表的信息 - - -* 接着paging_init函数通过[bootmem_init](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c#L1642)开始进行初始化工作 - - -arm64在整个初始化的流程上并没有什么不同, 但是有细微的差别 - - -* 由于arm是在后期才开始加入了MMU内存管理单元的, 因此内核必须实现mmu和nonmmu两套不同的代码, 这主要是提现在分页机制的不同上, 因而paging_init分别定义了[arch/arm/mm/nommu.c](http://lxr.free-electrons.com/source/arch/arm/mm/nommu.c?v=4.7#L311)和[arch/arm/mm/mmu.c](http://lxr.free-electrons.com/source/arch/arm/mm/mmu.c?v=4.7#L1623)两个版本, 但是它们均调用了bootmem_init来完成初始化 - - -* 也是因为上面的原因, 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的初始化 - - - -##4.2 bootmem_init -------- - -在paging_init之后, 系统的页帧已经建立起来, 然后通过bootmem_init中, 系统开始完成bootmem的初始化工作. - - -不同的体系结构bootmem_init的实现, 没有很大的区别, 但是在初始化的过程中, 其中的很多函数, 依据系统是NUMA还是UMA结构则有不同的定义 - - - -###4.2.1 bootmem_init函数 - -| 函数实现 | arm | arm64 | -|:---:|:---:|:-----:| -| bootmem_init | [arch/arm/mm/init.c, line 282](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L282) | [arch/arm64/mm/init.c, line 306](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L306) | - - -```cpp -// http://lxr.free-electrons.com/source/arch/arm/mm/init.c#L282 -void __init bootmem_init(void) -{ - unsigned long min, max_low, max_high; - - memblock_allow_resize(); - max_low = max_high = 0; - - /* 找到内存区域大小, - * max_low低端内存上界限 - * max_high 总内存上界 - */ - find_limits(&min, &max_low, &max_high); - - early_memtest((phys_addr_t)min << PAGE_SHIFT, - (phys_addr_t)max_low << PAGE_SHIFT); - - /* - * Sparsemem tries to allocate bootmem in memory_present(), - * so must be done after the fixed reservations - */ - arm_memory_present(); - - /* - * sparse_init() needs the bootmem allocator up and running. - */ - sparse_init(); - - /* - * Now free the memory - free_area_init_node needs - * the sparse mem_map arrays initialized by sparse_init() - * for memmap_init_zone(), otherwise all PFNs are invalid. - */ - zone_sizes_init(min, max_low, max_high); - - /* - * This doesn't seem to be used by the Linux memory manager any - * more, but is used by ll_rw_block. If we can get rid of it, we - * also get rid of some of the stuff above as well. - */ - min_low_pfn = min; - max_low_pfn = max_low; - max_pfn = max_high; -} - -// http://lxr.free-electrons.com/source/arch/arm64/mm/init.c#L306 -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(); -``` - - - -###4.2.2 函数设置内存区域大小 -------- - - - -要想初始化内存区域, 首先要先获取内存域的大小(起始和结束), 内核通过min_low_pfn, max_low_pfn, max_pfn三个全局变量标识了内存页面的开始和结束. 这几个变量我们在之前的[struct zone详解中](https://github.com/gatieme/LDD-LinuxDeviceDrivers/tree/master/study/kernel/02-memory/01-description/03-zone)中提到过, 内核也通过这些全局变量标记了物理内存所在页面的偏移, 这些变量定义在[mm/nobootmem.c?v4.7, line 31](http://lxr.free-electrons.com/source/mm/nobootmem.c?v4.7#L31) - -| 变量 | 描述 | -|:----:|:---:| -| max_low_pfn | max_low_pfn变量是由find_max_low_pfn函数计算并且初始化的,它被初始化成ZONE_NORMAL的最后一个page的位置。这个位置是kernel直接访问的物理内存, 也是关系到kernel/userspace通过“PAGE_OFFSET宏”把线性地址内存空间分开的内存地址位置 | -| min_low_pfn | 系统可用的第一个pfn是[min_low_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L16), 开始与_end标号的后面, 也就是kernel结束的地方.在文件mm/bootmem.c中对这个变量作初始化 -| max_pfn | 系统可用的最后一个PFN是[max_pfn变量](http://lxr.free-electrons.com/source/include/linux/bootmem.h?v4.7#L21), 这个变量的初始化完全依赖与硬件的体系结构. | - -**arm架构**下通过find_limits函数用来查找系统中可用内存区域的大小, 该函数定义在[arch/arm/mm/init.c?v=4.7, line 90](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L90) - -```cpp -static void __init find_limits(unsigned long *min, unsigned long *max_low, - unsigned long *max_high) -{ - *max_low = PFN_DOWN(memblock_get_current_limit()); - *min = PFN_UP(memblock_start_of_DRAM()); - *max_high = PFN_DOWN(memblock_end_of_DRAM()); -} -``` - -而**arm64架构**下, 则直接通过如下代码获取 - -```cpp -void __init bootmem_init(void) -{ - /* ...... */ - 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; - /* ...... */ -``` - - - - -###4.2.3 zone_sizes_init初始化节点和内存域 -------- - - -内核通过zone_sizes_init函数来初始化节点和管理区的一些数据项 - - -| 函数实现 | arm | arm64 | -|:---:|:---:|:-----:| -| zone_sizes_init | [arch/arm/mm/init.c?v=4.7#L137](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L137) | [arch/arm64/mm/init.c?v=4.7, line 90](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L90) | - -**arm架构**下该函数定义在[arch/arm/mm/init.c?v=4.7#L137](http://lxr.free-electrons.com/source/arch/arm/mm/init.c?v=4.7#L137)中. 如下所示 - - -```cpp -static void __init zone_sizes_init(unsigned long min, unsigned long max_low, - unsigned long max_high) -{ - unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES]; - struct memblock_region *reg; - - /* - * initialise the zones. - */ - memset(zone_size, 0, sizeof(zone_size)); - - /* - * The memory size has already been determined. If we need - * to do anything fancy with the allocation of this memory - * to the zones, now is the time to do it. - */ - zone_size[0] = max_low - min; -#ifdef CONFIG_HIGHMEM - zone_size[ZONE_HIGHMEM] = max_high - max_low; -#endif - - /* - * Calculate the size of the holes. - * holes = node_size - sum(bank_sizes) - */ - 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_low) { - unsigned long low_end = min(end, max_low); - zhole_size[0] -= low_end - start; - } -#ifdef CONFIG_HIGHMEM - if (end > max_low) { - unsigned long high_start = max(start, max_low); - zhole_size[ZONE_HIGHMEM] -= end - high_start; - } -#endif - } - -#ifdef CONFIG_ZONE_DMA - /* - * Adjust the sizes according to any special requirements for - * this machine type. - */ - if (arm_dma_zone_size) - arm_adjust_dma_zone(zone_size, zhole_size, - arm_dma_zone_size >> PAGE_SHIFT); -#endif - - free_area_init_node(0, zone_size, min, zhole_size); -} -``` - - -内核在zone_sizes_init函数来中获取了三个管理区的页面数(即大小), 然后通过free_area_init_node函数来设置和初始化内存域 - - -由于arm是非numa结构, 因为只需要通过 -```cpp -free_area_init_node(0, zone_size, min, zhole_size); -``` -设置唯一一个内存节点即可. - - -而**arm64架构**下则需要一句系统是NUMA还是UMA结构分别进行处理 - -* 如果是UMA结构则直接通过free_area_init_node初始化全局唯一的内存结点即可 - -```cpp -free_area_init_node(0, zone_size, min, zhole_size); - -``` - -* 如果是NUMA结构则需要free_area_init_nodes使用初始化所有结点, 而该函数会进一步遍历所有的结点, 依次通过free_area_init_node完成内存结点的初始化 - -```cpp -free_area_init_nodes(max_zone_pfns); -``` - - -arm64架构下zone_sizes_init的定义在[arch/arm64/mm/init.c?v=4.7, line 90](http://lxr.free-electrons.com/source/arch/arm64/mm/init.c?v=4.7#L90) - - -```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 */ -``` - - -###4.2.4 free_area_init_nodes初始化内存节点 -------- - - -内核通过zone_sizes_init函数来初始化节点和管理区的一些数据项 - - -| 函数实现 | 体系结构无关 | -|:---:|:---:|:-----:| -| free_area_init_nodes | [mm/page_alloc.c?v=4.7, line 6460](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) | -| free_area_init_node | [mm/page_alloc.c?v4.7, line 6076](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076) | - - - -```cpp - -/** - * free_area_init_nodes - Initialise all pg_data_t and zone data - * @max_zone_pfn: an array of max PFNs for each zone - * - * This will call free_area_init_node() for each active node in the system. - * Using the page ranges provided by memblock_set_node(), the size of each - * zone in each node and their holes is calculated. If the maximum PFN - * between two adjacent zones match, it is assumed that the zone is empty. - * For example, if arch_max_dma_pfn == arch_max_dma32_pfn, it is assumed - * that arch_max_dma32_pfn has no pages. It is also assumed that a zone - * starts where the previous one ended. For example, ZONE_DMA32 starts - * at arch_max_dma_pfn. - */ -void __init free_area_init_nodes(unsigned long *max_zone_pfn) -{ - unsigned long start_pfn, end_pfn; - int i, nid; - - /* Record where the zone boundaries are */ - memset(arch_zone_lowest_possible_pfn, 0, - sizeof(arch_zone_lowest_possible_pfn)); - memset(arch_zone_highest_possible_pfn, 0, - sizeof(arch_zone_highest_possible_pfn)); - arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); - arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; - for (i = 1; i < MAX_NR_ZONES; i++) { - if (i == ZONE_MOVABLE) - continue; - arch_zone_lowest_possible_pfn[i] = - arch_zone_highest_possible_pfn[i-1]; - arch_zone_highest_possible_pfn[i] = - max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); - } - arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; - arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; - - /* Find the PFNs that ZONE_MOVABLE begins at in each node */ - memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); - find_zone_movable_pfns_for_nodes(); - - /* Print out the zone ranges */ - pr_info("Zone ranges:\n"); - for (i = 0; i < MAX_NR_ZONES; i++) { - if (i == ZONE_MOVABLE) - continue; - pr_info(" %-8s ", zone_names[i]); - if (arch_zone_lowest_possible_pfn[i] == - arch_zone_highest_possible_pfn[i]) - pr_cont("empty\n"); - else - pr_cont("[mem %#018Lx-%#018Lx]\n", - (u64)arch_zone_lowest_possible_pfn[i] - << PAGE_SHIFT, - ((u64)arch_zone_highest_possible_pfn[i] - << PAGE_SHIFT) - 1); - } - - /* Print out the PFNs ZONE_MOVABLE begins at in each node */ - pr_info("Movable zone start for each node\n"); - for (i = 0; i < MAX_NUMNODES; i++) { - if (zone_movable_pfn[i]) - pr_info(" Node %d: %#018Lx\n", i, - (u64)zone_movable_pfn[i] << PAGE_SHIFT); - } - - /* Print out the early node map */ - pr_info("Early memory node ranges\n"); - for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) - pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid, - (u64)start_pfn << PAGE_SHIFT, - ((u64)end_pfn << PAGE_SHIFT) - 1); - - /* Initialise every node */ - mminit_verify_pageflags_layout(); - setup_nr_node_ids(); - for_each_online_node(nid) { - pg_data_t *pgdat = NODE_DATA(nid); - free_area_init_node(nid, NULL, - find_min_pfn_for_node(nid), NULL); - - /* Any memory on that node */ - if (pgdat->node_present_pages) - node_set_state(nid, N_MEMORY); - check_for_memory(pgdat, nid); - } -} -``` - - -###4.2.5 free_area_init_node初始化内存节点 -------- - -| 函数实现 | 体系结构无关 | -|:---:|:---:|:-----:| -| free_area_init_nodes | [mm/page_alloc.c?v=4.7, line 6460](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) | -| free_area_init_node | [mm/page_alloc.c?v4.7, line 6076](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076) | - - -```cpp -void __paginginit free_area_init_node(int nid, unsigned long *zones_size, - unsigned long node_start_pfn, unsigned long *zholes_size) -{ - pg_data_t *pgdat = NODE_DATA(nid); - unsigned long start_pfn = 0; - unsigned long end_pfn = 0; - - /* pg_data_t should be reset to zero when it's allocated */ - WARN_ON(pgdat->nr_zones || pgdat->classzone_idx); - - reset_deferred_meminit(pgdat); - pgdat->node_id = nid; - pgdat->node_start_pfn = node_start_pfn; -#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP - get_pfn_range_for_nid(nid, &start_pfn, &end_pfn); - pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid, - (u64)start_pfn << PAGE_SHIFT, - end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0); -#else - start_pfn = node_start_pfn; -#endif - calculate_node_totalpages(pgdat, start_pfn, end_pfn, - zones_size, zholes_size); - - alloc_node_mem_map(pgdat); -#ifdef CONFIG_FLAT_NODE_MEM_MAP - printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", - nid, (unsigned long)pgdat, - (unsigned long)pgdat->node_mem_map); -#endif - - free_area_init_core(pgdat); -} -``` - - -###4.2.6 free_area_init_core -------- - - -| 函数实现 | 体系结构无关 | -|:---:|:---:|:-----:| -| free_area_init_core | [mm/page_alloc.c?v=4.7#L5932](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932) | - - -```cpp -/* - * Set up the zone data structures: - * - mark all pages reserved - * - mark all memory queues empty - * - clear the memory bitmaps - * - * NOTE: pgdat should get zeroed by caller. - */ -static void __paginginit free_area_init_core(struct pglist_data *pgdat) -{ - enum zone_type j; - int nid = pgdat->node_id; - int ret; - - pgdat_resize_init(pgdat); -#ifdef CONFIG_NUMA_BALANCING - spin_lock_init(&pgdat->numabalancing_migrate_lock); - pgdat->numabalancing_migrate_nr_pages = 0; - pgdat->numabalancing_migrate_next_window = jiffies; -#endif -#ifdef CONFIG_TRANSPARENT_HUGEPAGE - spin_lock_init(&pgdat->split_queue_lock); - INIT_LIST_HEAD(&pgdat->split_queue); - pgdat->split_queue_len = 0; -#endif - init_waitqueue_head(&pgdat->kswapd_wait); - init_waitqueue_head(&pgdat->pfmemalloc_wait); -#ifdef CONFIG_COMPACTION - init_waitqueue_head(&pgdat->kcompactd_wait); -#endif - pgdat_page_ext_init(pgdat); - - for (j = 0; j < MAX_NR_ZONES; j++) { - struct zone *zone = pgdat->node_zones + j; - unsigned long size, realsize, freesize, memmap_pages; - unsigned long zone_start_pfn = zone->zone_start_pfn; - - size = zone->spanned_pages; - realsize = freesize = zone->present_pages; - - /* - * Adjust freesize so that it accounts for how much memory - * is used by this zone for memmap. This affects the watermark - * and per-cpu initialisations - */ - memmap_pages = calc_memmap_size(size, realsize); - if (!is_highmem_idx(j)) { - if (freesize >= memmap_pages) { - freesize -= memmap_pages; - if (memmap_pages) - printk(KERN_DEBUG - " %s zone: %lu pages used for memmap\n", - zone_names[j], memmap_pages); - } else - pr_warn(" %s zone: %lu pages exceeds freesize %lu\n", - zone_names[j], memmap_pages, freesize); - } - - /* Account for reserved pages */ - if (j == 0 && freesize > dma_reserve) { - freesize -= dma_reserve; - printk(KERN_DEBUG " %s zone: %lu pages reserved\n", - zone_names[0], dma_reserve); - } - - if (!is_highmem_idx(j)) - nr_kernel_pages += freesize; - /* Charge for highmem memmap if there are enough kernel pages */ - else if (nr_kernel_pages > memmap_pages * 2) - nr_kernel_pages -= memmap_pages; - nr_all_pages += freesize; - - /* - * Set an approximate value for lowmem here, it will be adjusted - * when the bootmem allocator frees pages into the buddy system. - * And all highmem pages will be managed by the buddy system. - */ - zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; -#ifdef CONFIG_NUMA - zone->node = nid; - zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) - / 100; - zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; -#endif - zone->name = zone_names[j]; - spin_lock_init(&zone->lock); - spin_lock_init(&zone->lru_lock); - zone_seqlock_init(zone); - zone->zone_pgdat = pgdat; - zone_pcp_init(zone); - - /* For bootup, initialized properly in watermark setup */ - mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages); - - lruvec_init(&zone->lruvec); - if (!size) - continue; - - set_pageblock_order(); - setup_usemap(pgdat, zone, zone_start_pfn, size); - ret = init_currently_empty_zone(zone, zone_start_pfn, size); - BUG_ON(ret); - memmap_init(size, nid, j, zone_start_pfn); - } -} -``` - - -#总结 -------- - -版权声明知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。 - diff --git a/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md b/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md new file mode 100644 index 0000000..1c66c60 --- /dev/null +++ b/study/kernel/02-memory/03-initialize/04-bootmem_init/README.md @@ -0,0 +1,1245 @@ +初始化内存管理 +======= + + + +| 日期 | 内核版本 | 架构| 作者 | 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) | + + +在内存管理的上下文中, 初始化(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内核的内存管理分三个阶段。 + +| 阶段 | 起点 | 终点 | 描述 | +|:-----:|:-----:|:-----:| +| 第一阶段 | 系统启动 | 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_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变量申请空间.
在SMP系统中, setup_per_cpu_areas初始化源代码中(使用[per_cpu宏](http://lxr.free-electrons.com/source/include/linux/percpu-defs.h#L256))定义的静态per-cpu变量, 这种变量对系统中每个CPU都有一个独立的副本.
此类变量保存在内核二进制影像的一个独立的段中, setup_per_cpu_areas的目的就是为系统中各个CPU分别创建一份这些数据的副本
在非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) | 建立了内核的内存分配器,
其中通过[mem_init](http://lxr.free-electrons.com/ident?v=4.7&i=mem_init)停用bootmem分配器并迁移到实际的内存管理器(比如伙伴系统)
然后调用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的第一个数组元素分配内存, 换句话说, 其实就是第一个系统处理器分配
由于在分页情况下,每次存储器访问都要存取多级页表,这就大大降低了访问速度。所以,为了提高速度,在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) | 初始化内存管理 | + + +该函数主要执行了如下操作 + + +1. 使用arm64_memblock_init来完成memblock机制的初始化工作, 至此memblock分配器接受系统中系统中内存的分配工作 + +2. 调用paging_init来完成系统分页机制的初始化工作, 建立页表, 从而内核可以完成虚拟内存的映射和转换工作 + +3. 最后调用bootmem_init来完成实现buddy内存管理所需要的工作 + + + +##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)函数来实现的 + +```cpp +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + | + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); + | 分页机制初始化 + | + |---->bootmem_init(); [当前位置] + | 始化内存数据结构包括内存节点, 内存域和页帧page + | + |---->arm64_numa_init(); + | 支持numa架构 + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 + | + |---->build_all_zonelist() + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | +``` + + +##1.6 今日内容(第二阶段(一)--初始化内存管理数据结构) +------- + + +我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 + + + +**特定于体系结构的设置** + + +在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 + +* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. + +在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + + +* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 + +```cpp +bootmem_init() +始化内存数据结构包括内存节点, 内存域和页帧page +| +|---->arm64_numa_init(); +| 支持numa架构 +| +|---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 +| +|---->memblock_dump_all(); +| 初始化完成, 显示memblock的保留的所有内存信息 +``` + + + + +**建立内存管理的数据结构** + + +对相关数据结构的初始化是从全局启动函数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()函数开始执行 + +体系结构相关的代码需要在启动期间建立如下信息 + +* 系统中各个内存域的页帧边界,保存在max_zone_pfn数组 + +早期的内核还需记录各结点页帧的分配情况,保存在全局变量early_node_map中 + +![zone_sizes_init函数](../images/arch_do_somethig.png) + +内核提供了一个通用的框架, 用于将上述信息转换为伙伴系统预期的节点和内存域数据结构, 但是在此之前各个体系结构必须自行建立相关结构. + + +##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()来初始化节点和管理区的一些数据项, 其中关键的是初始化了系统中各个内存域的页帧边界,保存在max_zone_pfn数组. + + + +[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 */ +``` + +在获取了三个管理区的页面数后, NUMA架构下通过free_area_init_nodes()来完成后续工作, 其中核心函数为free_area_init_node(),用来针对特定的节点进行初始化, 由于UMA架构下只有一个内存结点, 因此直接通过free_area_init_node来完成内存结点的初始化 + + +截至到目前为止, 体系结构相关的部分已经结束了, 各个体系结构已经自行建立了自己所需的一些底层数据结构, 这些结构建立好以后, 内核将繁重的内存数据结构创建和初始化的工作交给free_area_init_node(s)函数来完成, + +#3 free_area_init_nodes初始化NUMA管理数据结构 +------- + +>注意 +> +>此部分内容参照 +> +>[Linux内存管理伙伴算法](http://www.linuxidc.com/Linux/2012-09/70711p3.htm) +> +>[linux 内存管理 - paging_init 函数](http://blog.csdn.net/decload/article/details/8080126) + + +[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460)初始化了NUMA系统中所有结点的pg_data_t和zone、page的数据, 并打印了管理区信息, 该函数定义在[mm/page_alloc.c?v=4.7, line 6460](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6460) + +##3.1 代码注释 +------- + +```cpp +// 初始化各个节点的所有pg_data_t和zone、page的数据 +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + unsigned long start_pfn, end_pfn; + int i, nid; + + /* Record where the zone boundaries are + * 全局数组arch_zone_lowest_possible_pfn + * 用来存储各个内存域可使用的最低内存页帧编号 */ + memset(arch_zone_lowest_possible_pfn, 0, + sizeof(arch_zone_lowest_possible_pfn)); + + /* 全局数组arch_zone_highest_possible_pfn + * 用来存储各个内存域可使用的最高内存页帧编号 */ + memset(arch_zone_highest_possible_pfn, 0, + sizeof(arch_zone_highest_possible_pfn)); + + /* 辅助函数find_min_pfn_with_active_regions + * 用于找到注册的最低内存域中可用的编号最小的页帧 */ + arch_zone_lowest_possible_pfn[0] = find_min_pfn_with_active_regions(); + + /* max_zone_pfn记录了各个内存域包含的最大页帧号 */ + arch_zone_highest_possible_pfn[0] = max_zone_pfn[0]; + + /* 依次遍历,确定各个内存域的边界 */ + for (i = 1; i < MAX_NR_ZONES; i++) { + /* 由于ZONE_MOVABLE是一个虚拟内存域 + * 不与真正的硬件内存域关联 + * 该内存域的边界总是设置为0 */ + if (i == ZONE_MOVABLE) + continue; + /* 第n个内存域的最小页帧 + * 即前一个(第n-1个)内存域的最大页帧 */ + arch_zone_lowest_possible_pfn[i] = + arch_zone_highest_possible_pfn[i-1]; + /* 不出意外,当前内存域的最大页帧 + * 由max_zone_pfn给出 */ + arch_zone_highest_possible_pfn[i] = + max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); + } + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + + /* Print out the zone ranges + * 将各个内存域的最大、最小页帧号显示出来 */ + pr_info("Zone ranges:\n"); + for (i = 0; i < MAX_NR_ZONES; i++) { + if (i == ZONE_MOVABLE) + continue; + pr_info(" %-8s ", zone_names[i]); + if (arch_zone_lowest_possible_pfn[i] == + arch_zone_highest_possible_pfn[i]) + pr_cont("empty\n"); + else + pr_cont("[mem %#018Lx-%#018Lx]\n", + (u64)arch_zone_lowest_possible_pfn[i] + << PAGE_SHIFT, + ((u64)arch_zone_highest_possible_pfn[i] + << PAGE_SHIFT) - 1); + } + + /* Print out the PFNs ZONE_MOVABLE begins at in each node */ + pr_info("Movable zone start for each node\n"); + for (i = 0; i < MAX_NUMNODES; i++) { + /* 对每个结点来说,zone_movable_pfn[node_id] + * 表示ZONE_MOVABLE在movable_zone内存域中所取得内存的起始地址 + * 内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配 */ + if (zone_movable_pfn[i]) + { + /* 显示各个内存域的分配情况 */ + pr_info(" Node %d: %#018Lx\n", i, + (u64)zone_movable_pfn[i] << PAGE_SHIFT); + } + } + + /* Print out the early node map */ + pr_info("Early memory node ranges\n"); + for_each_mem_pfn_range(i, MAX_NUMNODES, &start_pfn, &end_pfn, &nid) + pr_info(" node %3d: [mem %#018Lx-%#018Lx]\n", nid, + (u64)start_pfn << PAGE_SHIFT, + ((u64)end_pfn << PAGE_SHIFT) - 1); + + /* Initialise every node */ + mminit_verify_pageflags_layout(); + setup_nr_node_ids(); + + /* 代码遍历所有的活动结点, + * 并分别对各个结点调用free_area_init_node建立数据结构, + * 该函数需要结点第一个可用的页帧作为一个参数, + * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ + for_each_online_node(nid) { + pg_data_t *pgdat = NODE_DATA(nid); + free_area_init_node(nid, NULL, + find_min_pfn_for_node(nid), NULL); + + /* Any memory on that node + * 根据node_present_pages字段判断结点具有内存 + * 则在结点位图中设置N_HIGH_MEMORY标志 + * 该标志只表示结点上存在普通或高端内存 + * 因此check_for_regular_memory + * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 + * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ + if (pgdat->node_present_pages) + node_set_state(nid, N_MEMORY); + check_for_memory(pgdat, nid); + } +} +``` + +free_area_init_nodes函数中通过循环遍历各个节点,循环中调用了free_area_init_node函数初始化该节点对应的pg_data_t和zone、page的数据. + + +##3.2 设置可使用的页帧编号 +------- + + +free_area_init_nodes首先必须分析并改写特定于体系结构的代码提供的信息。其中,需要对照在zone_max_pfn和zone_min_pfn中指定的内存域的边界,计算各个内存域可使用的最低和最高的页帧编号。使用了两个全局数组来存储这些信息: + +参见[mm/page_alloc.c?v=4.7, line 259)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L259) + +```cpp +static unsigned long __meminitdata arch_zone_lowest_possible_pfn[MAX_NR_ZONES]; + +static unsigned long __meminitdata arch_zone_highest_possible_pfn[MAX_NR_ZONES]; +``` + +通过max_zone_pfn传递给free_area_init_nodes的信息记录了各个内存域包含的最大页帧号。 +free_area_init_nodes将该信息转换为一种更方便的表示形式,即以[low, high]形式描述各个内 +存域的页帧区间,存储在前述的全局变量中(我省去了对这些变量填充字节0的初始化过程): + + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* ...... */ + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + /* 依次遍历,确定各个内存域的边界 */ + for (i = 1; i < MAX_NR_ZONES; i++) { + /* 由于ZONE_MOVABLE是一个虚拟内存域 + * 不与真正的硬件内存域关联 + * 该内存域的边界总是设置为0 */ + if (i == ZONE_MOVABLE) + continue; + /* 第n个内存域的最小页帧 + * 即前一个(第n-1个)内存域的最大页帧 */ + arch_zone_lowest_possible_pfn[i] = + arch_zone_highest_possible_pfn[i-1]; + /* 不出意外,当前内存域的最大页帧 + * 由max_zone_pfn给出 */ + arch_zone_highest_possible_pfn[i] = + max(max_zone_pfn[i], arch_zone_lowest_possible_pfn[i]); + } + + /* ...... */ +} +``` +辅助函数find_min_pfn_with_active_regions用于找到注册的最低内存域中可用的编号最小的页帧。该内存域不必一定是ZONE_DMA,例如,在计算机不需要DMA内存的情况下也可以是ZONE_NORMAL。最低内存域的最大页帧号可以从max_zone_pfn提供的信息直接获得。 + +##3.3 构建其他内存域的页帧区间 +------- + +接下来构建其他内存域的页帧区间,方法很直接:第n个内存域的最小页帧,即前一个(第n-1个)内存域的最大页帧。当前内存域的最大页帧由max_zone_pfn给出 + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* ...... */ + + arch_zone_lowest_possible_pfn[ZONE_MOVABLE] = 0; + arch_zone_highest_possible_pfn[ZONE_MOVABLE] = 0; + + /* Find the PFNs that ZONE_MOVABLE begins at in each node */ + memset(zone_movable_pfn, 0, sizeof(zone_movable_pfn)); + /* 用于计算进入ZONE_MOVABLE的内存数量 */ + find_zone_movable_pfns_for_nodes(); + + /* ...... */ +} +``` +由于ZONE_MOVABLE是一个虚拟内存域,不与真正的硬件内存域关联,该内存域的边界总是设置为0。回忆前文,可知只有在指定了内核命令行参数kernelcore或movablecore之一时,该内存域才会存在. +该内存域一般开始于各个结点的某个特定内存域的某一页帧号。相应的编号在find_zone_movable_pfns_for_nodes里计算。 + +现在可以向用户提供一些有关已确定的页帧区间的信息。举例来说,其中可能包括下列内容(输出取自AMD64系统,有4 GiB物理内存): + + +```cpp +> dmesg + +Zone PFN ranges: +DMA 0 0 -> 4096 +DMA32 4096 -> 1048576 +Normal 1048576 -> 1245184 +``` + + +##3.4 建立结点数据结构 +------- + + +free_area_init_nodes剩余的部分遍历所有结点,分别建立其数据结构 + +```cpp +void __init free_area_init_nodes(unsigned long *max_zone_pfn) +{ + /* 输出有关内存域的信息 */ + /* ...... */ + + /* 代码遍历所有的活动结点, + * 并分别对各个结点调用free_area_init_node建立数据结构, + * 该函数需要结点第一个可用的页帧作为一个参数, + * 而find_min_pfn_for_node则从early_node_map数组提取该信息 */ + for_each_online_node(nid) { + pg_data_t *pgdat = NODE_DATA(nid); + free_area_init_node(nid, NULL, + find_min_pfn_for_node(nid), NULL); + + /* Any memory on that node + * 根据node_present_pages字段判断结点具有内存 + * 则在结点位图中设置N_HIGH_MEMORY标志 + * 该标志只表示结点上存在普通或高端内存 + * 因此check_for_regular_memory + * 进一步检查低于ZONE_HIGHMEM的内存域中是否有内存 + * 并据此在结点位图中相应地设置N_NORMAL_MEMORY */ + if (pgdat->node_present_pages) + node_set_state(nid, N_MEMORY); + check_for_memory(pgdat, nid); + } + + /* ...... */ +} +``` + + +代码遍历所有活动结点,并分别对各个结点调用free_area_init_node建立数据结构。该函数需要结点第一个可用的页帧作为一个参数,而find_min_pfn_for_node则从early_node_map数组提取该信息。 + +如果根据node_present_pages字段判断结点具有内存,则在结点位图中设置N_HIGH_MEMORY标志。我们知道该标志只表示结点上存在普通或高端内存,因此check_for_regular_memory进一步检查低于ZONE_HIGHMEM的内存域中是否有内存,并据此在结点位图中相应地设置N_NORMAL_MEMORY标志 + + +#4 free_area_init_node初始化UMA内存结点 +------- + +[free_area_init_nodes](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076)函数初始化所有结点的pg_data_t和zone、page的数据,并打印了管理区信息. + + +##4.1 free_area_init_node函数注释 +------- + +该函数定义在[mm/page_alloc.c?v=4.7, line 6076](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6076) + +```cpp +void __paginginit free_area_init_node(int nid, unsigned long *zones_size, + unsigned long node_start_pfn, unsigned long *zholes_size) +{ + pg_data_t *pgdat = NODE_DATA(nid); + unsigned long start_pfn = 0; + unsigned long end_pfn = 0; + + /* pg_data_t should be reset to zero when it's allocated */ + WARN_ON(pgdat->nr_zones || pgdat->classzone_idx); + + reset_deferred_meminit(pgdat); + pgdat->node_id = nid; + pgdat->node_start_pfn = node_start_pfn; +#ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP + get_pfn_range_for_nid(nid, &start_pfn, &end_pfn); + pr_info("Initmem setup node %d [mem %#018Lx-%#018Lx]\n", nid, + (u64)start_pfn << PAGE_SHIFT, + end_pfn ? ((u64)end_pfn << PAGE_SHIFT) - 1 : 0); +#else + start_pfn = node_start_pfn; +#endif + /* 首先累计各个内存域的页数 + * 计算结点中页的总数 + * 对连续内存模型而言 + * 这可以通过zone_sizes_init完成 + * 但calculate_node_totalpages还考虑了内存空洞 */ + calculate_node_totalpages(pgdat, start_pfn, end_pfn, + zones_size, zholes_size); + /* 分配了该节点的页面描述符数组 + * [pgdat->node_mem_map数组的内存分配 */ + alloc_node_mem_map(pgdat); +#ifdef CONFIG_FLAT_NODE_MEM_MAP + printk(KERN_DEBUG "free_area_init_node: node %d, pgdat %08lx, node_mem_map %08lx\n", + nid, (unsigned long)pgdat, + (unsigned long)pgdat->node_mem_map); +#endif + + /* 对该节点的每个区[DMA,NORMAL,HIGH]的的结构进行初始化 */ + free_area_init_core(pgdat); +} +``` +##4.2 流程分析 +------- + +* [calculate_node_totalpages](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789)函数累计各个内存域的页数,计算结点中页的总数。对连续内存模型而言,这可以通过zone_sizes_init完成,但calculate_node_totalpages还考虑了内存空洞,该函数定义在[mm/page_alloc.c, line 5789](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5789) + + 以下例子取自一个UMA系统, 具有512 MiB物理内存。 +```cpp +> dmesg +... +On node 0 totalpages: 131056 +``` + + +* [alloc_node_mem_map(pgdat)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L6030)函数分配了该节点的页面描述符数组[pgdat->node_mem_map数组的内存分配. + + +* 继续调用[free_area_init_core](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932)函数,继续初始化该节点的pg_data_t结构,初始化zone以及page结构 ,##2.6 free_area_init_core函数是初始化zone的核心 + + + +##4.3 alloc_node_mem_map函数 +------- + + +alloc_node_mem_map负责初始化一个简单但非常重要的数据结构。如上所述,系统中的各个物理内存页,都对应着一个struct page实例。该结构的初始化由alloc_node_mem_map执行 + + +```cpp +static void __init_refok alloc_node_mem_map(struct pglist_data *pgdat) +{ + unsigned long __maybe_unused start = 0; + unsigned long __maybe_unused offset = 0; + + /* Skip empty nodes */ + if (!pgdat->node_spanned_pages) + return; + +#ifdef CONFIG_FLAT_NODE_MEM_MAP + start = pgdat->node_start_pfn & ~(MAX_ORDER_NR_PAGES - 1); + offset = pgdat->node_start_pfn - start; + /* ia64 gets its own node_mem_map, before this, without bootmem */ + if (!pgdat->node_mem_map) { + unsigned long size, end; + struct page *map; + + /* + * The zone's endpoints aren't required to be MAX_ORDER + * aligned but the node_mem_map endpoints must be in order + * for the buddy allocator to function correctly. + */ + end = pgdat_end_pfn(pgdat); + end = ALIGN(end, MAX_ORDER_NR_PAGES); + size = (end - start) * sizeof(struct page); + map = alloc_remap(pgdat->node_id, size); + if (!map) + map = memblock_virt_alloc_node_nopanic(size, + pgdat->node_id); + pgdat->node_mem_map = map + offset; + } +#ifndef CONFIG_NEED_MULTIPLE_NODES + /* + * With no DISCONTIG, the global mem_map is just set as node 0's + */ + if (pgdat == NODE_DATA(0)) { + mem_map = NODE_DATA(0)->node_mem_map; +#if defined(CONFIG_HAVE_MEMBLOCK_NODE_MAP) || defined(CONFIG_FLATMEM) + if (page_to_pfn(mem_map) != pgdat->node_start_pfn) + mem_map -= offset; +#endif /* CONFIG_HAVE_MEMBLOCK_NODE_MAP */ + } +#endif +#endif /* CONFIG_FLAT_NODE_MEM_MAP */ +} +``` +没有页的空结点显然可以跳过。如果特定于体系结构的代码尚未建立内存映射(这是可能的,例如,在IA-64系统上),则必须分配与该结点关联的所有struct page实例所需的内存。各个体系结构可以为此提供一个特定的函数。但目前只有在IA-32系统上使用不连续内存配置时是这样。在所有其他的配置上,则使用普通的自举内存分配器进行分配。请注意,代码将内存映射对齐到伙伴系统的最大分配阶,因为要使所有的计算都工作正常,这是必需的。 + + +指向该空间的指针不仅保存在pglist_data实例中,还保存在全局变量mem_map中,前提是当前考察的结点是系统的第0个结点(如果系统只有一个内存结点,则总是这样)。mem_map是一个全局数组,在讲解内存管理时,我们会经常遇到, 定义在[mm/memory.c?v=4.7, line 85](http://lxr.free-electrons.com/source/mm/memory.c?v=4.7#L85) + +```cpp +struct page *mem_map; +``` + + + +然后在free_area_init_node函数的最后, 通过free_area_init_core来完成内存域zone的初始化 + + + +#5 free_area_init_core初始化内存域zone +------- + +初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域, 该函数定义在[mm/page_alloc.c?v=4.7, line 5932](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5932) + + +##5.1 free_area_init_core函数代码注释 +------- + +```cpp +/* + * Set up the zone data structures: + * - mark all pages reserved + * - mark all memory queues empty + * - clear the memory bitmaps + * + * NOTE: pgdat should get zeroed by caller. + */ +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + enum zone_type j; + int nid = pgdat->node_id; + int ret; + + /* 初始化pgdat->node_size_lock自旋锁 */ + pgdat_resize_init(pgdat); +#ifdef CONFIG_NUMA_BALANCING + spin_lock_init(&pgdat->numabalancing_migrate_lock); + pgdat->numabalancing_migrate_nr_pages = 0; + pgdat->numabalancing_migrate_next_window = jiffies; +#endif +#ifdef CONFIG_TRANSPARENT_HUGEPAGE + spin_lock_init(&pgdat->split_queue_lock); + INIT_LIST_HEAD(&pgdat->split_queue); + pgdat->split_queue_len = 0; +#endif + + /* 初始化pgdat->kswapd_wait等待队列 */ + init_waitqueue_head(&pgdat->kswapd_wait); + /* 初始化页换出守护进程创建空闲块的大小 + * 为2^kswapd_max_order */ + init_waitqueue_head(&pgdat->pfmemalloc_wait); +#ifdef CONFIG_COMPACTION + init_waitqueue_head(&pgdat->kcompactd_wait); +#endif + pgdat_page_ext_init(pgdat); + + /* 遍历每个管理区 */ + for (j = 0; j < MAX_NR_ZONES; j++) { + struct zone *zone = pgdat->node_zones + j; + unsigned long size, realsize, freesize, memmap_pages; + unsigned long zone_start_pfn = zone->zone_start_pfn; + + /* size为该管理区中的页框数,包括洞 */ + size = zone->spanned_pages; + /* realsize为管理区中的页框数,不包括洞 / + realsize = freesize = zone->present_pages; + + /* + * Adjust freesize so that it accounts for how much memory + * is used by this zone for memmap. This affects the watermark + * and per-cpu initialisations + * 调整realsize的大小,即减去page结构体占用的内存大小 */ + /* memmap_pags为包括洞的所有页框的page结构体所占的大小 */ + memmap_pages = calc_memmap_size(size, realsize); + if (!is_highmem_idx(j)) { + if (freesize >= memmap_pages) { + freesize -= memmap_pages; + if (memmap_pages) + printk(KERN_DEBUG + " %s zone: %lu pages used for memmap\n", + zone_names[j], memmap_pages); + } else /* 内存不够存放page结构体 */ + pr_warn(" %s zone: %lu pages exceeds freesize %lu\n", + zone_names[j], memmap_pages, freesize); + } + + /* Account for reserved pages + * 调整realsize的大小,即减去DMA保留页的大小 */ + if (j == 0 && freesize > dma_reserve) { + freesize -= dma_reserve; + printk(KERN_DEBUG " %s zone: %lu pages reserved\n", + zone_names[0], dma_reserve); + } + + if (!is_highmem_idx(j)) + nr_kernel_pages += freesize; + /* Charge for highmem memmap if there are enough kernel pages */ + else if (nr_kernel_pages > memmap_pages * 2) + nr_kernel_pages -= memmap_pages; + nr_all_pages += freesize; + + /* + * Set an approximate value for lowmem here, it will be adjusted + * when the bootmem allocator frees pages into the buddy system. + * And all highmem pages will be managed by the buddy system. + */ + /* 设置zone->spanned_pages为包括洞的页框数 */ + zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; +#ifdef CONFIG_NUMA + /* 设置zone中的节点标识符 */ + zone->node = nid; + /* 设置可回收页面比率 */ + zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) + / 100; + /* 设置slab回收缓存页的比率 */ + zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; +#endif + /* 设置zone的名称 */ + zone->name = zone_names[j]; + + /* 初始化各种锁 */ + spin_lock_init(&zone->lock); + spin_lock_init(&zone->lru_lock); + zone_seqlock_init(zone); + /* 设置管理区属于的节点对应的pg_data_t结构 */ + zone->zone_pgdat = pgdat; + /* 初始化cpu的页面缓存 */ + zone_pcp_init(zone); + + /* For bootup, initialized properly in watermark setup */ + mod_zone_page_state(zone, NR_ALLOC_BATCH, zone->managed_pages); + + /* 初始化lru相关成员 */ + lruvec_init(&zone->lruvec); + if (!size) + continue; + + set_pageblock_order(); + /* 定义了CONFIG_SPARSEMEM该函数为空 */ + setup_usemap(pgdat, zone, zone_start_pfn, size); + /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 + * 初始化zone->free_area成员 + * 初始化zone->wait_table相关成员 + */ + ret = init_currently_empty_zone(zone, zone_start_pfn, size); + BUG_ON(ret); + /* 初始化该zone对应的page结构 */ + memmap_init(size, nid, j, zone_start_pfn); + } + /* ...... */ +} +``` + + +##5.2 流程讲解 +------- + + +初始化内存域数据结构涉及的繁重工作由free_area_init_core执行,它会依次遍历结点的所有内存域 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + enum zone_type j; + int nid = pgdat->node_id; + int ret; + + /* ...... */ + /* 遍历每个管理区 */ + for (j = 0; j < MAX_NR_ZONES; j++) { + struct zone *zone = pgdat->node_zones + j; + unsigned long size, realsize, freesize, memmap_pages; + unsigned long zone_start_pfn = zone->zone_start_pfn; + + /* size为该管理区中的页框数,包括洞 */ + size = zone->spanned_pages; + /* realsize为管理区中的页框数,不包括洞 / + realsize = freesize = zone->present_pages; + + /* ...... */ +} +``` + + +内存域的真实长度,可通过跨越的页数减去空洞覆盖的页数而得到。这两个值是通过两个辅助函数计算的,我不会更详细地讨论了。其复杂性实质上取决于内存模型和所选定的配置选项,但所有变体最终都没有什么意外之处 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + /* ...... */ + if (!is_highmem_idx(j)) + nr_kernel_pages += freesize; + /* Charge for highmem memmap if there are enough kernel pages */ + else if (nr_kernel_pages > memmap_pages * 2) + nr_kernel_pages -= memmap_pages; + nr_all_pages += freesize; + + /* + * Set an approximate value for lowmem here, it will be adjusted + * when the bootmem allocator frees pages into the buddy system. + * And all highmem pages will be managed by the buddy system. + */ + /* 设置zone->spanned_pages为包括洞的页框数 */ + zone->managed_pages = is_highmem_idx(j) ? realsize : freesize; +#ifdef CONFIG_NUMA + /* 设置zone中的节点标识符 */ + zone->node = nid; + /* 设置可回收页面比率 */ + zone->min_unmapped_pages = (freesize*sysctl_min_unmapped_ratio) + / 100; + /* 设置slab回收缓存页的比率 */ + zone->min_slab_pages = (freesize * sysctl_min_slab_ratio) / 100; +#endif + /* 设置zone的名称 */ + zone->name = zone_names[j]; + + /* 初始化各种锁 */ + spin_lock_init(&zone->lock); + spin_lock_init(&zone->lru_lock); + zone_seqlock_init(zone); + /* 设置管理区属于的节点对应的pg_data_t结构 */ + zone->zone_pgdat = pgdat; + /* ...... */ +} +``` + + +内核使用两个全局变量跟踪系统中的页数。nr_kernel_pages统计所有一致映射的页,而nr_all_pages还包括高端内存页在内free_area_init_core始化为0 + +我们比较感兴趣的是调用的两个辅助函数 + +* zone_pcp_init尝试初始化该内存域的per-CPU缓存, 定义在[mm/page_alloc.c?v=4.7, line 5443](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5443) + +* init_currently_empty_zone初始化free_area列表,并将属于该内存域的所有page实例都设置为初始默认值。正如前文的讨论,调用了memmap_init_zone来初始化内存域的页, 定义在[mm/page_alloc.c?v=4.7, line 5458](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5458) + +我们还可以回想前文提到的,所有页属性起初都设置MIGRATE_MOVABLE。 +此外,空闲列表是在zone_init_free_lists中初始化的 + + +```cpp +static void __paginginit free_area_init_core(struct pglist_data *pgdat) +{ + /* ...... */ + { + /* 初始化cpu的页面缓存 */ + zone_pcp_init(zone); + + /* 设置pgdat->nr_zones和zone->zone_start_pfn成员 + * 初始化zone->free_area成员 + * 初始化zone->wait_table相关成员 + */ + ret = init_currently_empty_zone(zone, zone_start_pfn, size); + BUG_ON(ret); + /* 初始化该zone对应的page结构 */ + memmap_init(size, nid, j, zone_start_pfn); + } + /* ...... */ +} +``` + + + +#6 memmap_init初始化page页面 +------- + +在free_area_init_core初始化内存管理区zone的过程中, 通过memmap_init函数对每个内存管理区zone的page内存进行了初始化 + + +memmap_init函数定义在[mm/page_alloc.c?v=4.7, line ](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5241) + +```cpp +#ifndef __HAVE_ARCH_MEMMAP_INIT +#define memmap_init(size, nid, zone, start_pfn) \ + memmap_init_zone((size), (nid), (zone), (start_pfn), MEMMAP_EARLY) +#endif +``` +memmap_init_zone函数完成了page的初始化工作, 该函数定义在[mm/page_alloc.c?v=4.7, line 5139](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L5139) + + +至此,节点和管理区的关键数据已完成初始化,内核在后面为内存管理做得一个准备工作就是将所有节点的管理区都链入到zonelist中,便于后面内存分配工作的进行 + +内核在start_kernel()-->build_all_zonelist()中完成zonelist的初始化 + + + + +#7 总结 +------- + + + +##7.1 start_kernel启动流程 +------- + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | + |---->setup_per_cpu_areas(); + | 为per-CPU变量分配空间 + | + |---->build_all_zonelist() + | 为系统中的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 + | + |---->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; + | +``` + +##7.2 pidhash_init配置高端内存 +------- + + +```cpp +void pidhash_init(void) + |---->pid_hash = alloc_large_system_hash("PID", sizeof(*pid_hash), + | 0, 18, HASH_EARLY|HASH_SMALL, &pidhash_shift, NULL, 4096); + | 根据nr_kernel_pages(低端内存的页数),分配哈希数组,以及各个哈希 + | 数组元素下的哈希链表的空间,原理如下: + | number = nr_kernel_pages; + | number >= (18 - PAGE_SHIFT) 根据散列度获得数组元素个数 + | number = roundup_pow_of_two(number); + | pidhash_shift = max{x | 2**x <= number} + | size = number * sizeof(*pid_hash); + | 使用位图分配器分配size空间,将返回值付给pid_hash; + | + |---->pidhash_size = 1 << pidhash_shift; + | + |---->for(i = 0; i < pidhash_size; i++) + | INIT_HLIST_HEAD(&pid_hash[i]); +``` + +##7.3 build_all_zonelists初始化每个内存节点的zonelists +------- + + + +```cpp +void build_all_zonelists(void) + |---->set_zonelist_order() + |---->current_zonelist_order = ZONELIST_ORDER_ZONE; + | + |---->__build_all_zonelists(NULL); + | Memory不支持热插拔, 为每个zone建立后备的zone, + | 每个zone及自己后备的zone,形成zonelist + | + |---->pg_data_t *pgdat = NULL; + | pgdat = &contig_page_data;(单node) + | + |---->build_zonelists(pgdat); + | 为每个zone建立后备zone的列表 + | + |---->struct zonelist *zonelist = NULL; + | enum zone_type j; + | zonelist = &pgdat->node_zonelists[0]; + | + |---->j = build_zonelists_node(pddat, zonelist, 0, MAX_NR_ZONES - 1); + | 为pgdat->node_zones[0]建立后备的zone,node_zones[0]后备的zone + | 存储在node_zonelist[0]内,对于node_zone[0]的后备zone,其后备的zone + | 链表如下(只考虑UMA体系,而且不考虑ZONE_DMA): + | node_zonelist[0]._zonerefs[0].zone = &node_zones[2]; + | node_zonelist[0]._zonerefs[0].zone_idx = 2; + | node_zonelist[0]._zonerefs[1].zone = &node_zones[1]; + | node_zonelist[0]._zonerefs[1].zone_idx = 1; + | node_zonelist[0]._zonerefs[2].zone = &node_zones[0]; + | node_zonelist[0]._zonerefs[2].zone_idx = 0; + | + | zonelist->_zonerefs[3].zone = NULL; + | zonelist->_zonerefs[3].zone_idx = 0; + | + |---->build_zonelist_cache(pgdat); + |---->pdat->node_zonelists[0].zlcache_ptr = NULL; + | UMA体系结构 + | + |---->for_each_possible_cpu(cpu) + | setup_pageset(&per_cpu(boot_pageset, cpu), 0); + |详见下文 + |---->vm_total_pages = nr_free_pagecache_pages(); + | 业务:获得所有zone中的present_pages总和. + | + |---->page_group_by_mobility_disabled = 0; + | 对于代码中的判断条件一般不会成立,因为页数会最够多(内存较大) +``` \ No newline at end of file diff --git a/study/kernel/02-memory/03-initialize/04-init_buddy/README.md b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md similarity index 92% rename from study/kernel/02-memory/03-initialize/04-init_buddy/README.md rename to study/kernel/02-memory/03-initialize/05-build_zonelists/README.md index 71ba230..842eee6 100644 --- a/study/kernel/02-memory/03-initialize/04-init_buddy/README.md +++ b/study/kernel/02-memory/03-initialize/05-build_zonelists/README.md @@ -154,23 +154,61 @@ void __init setup_arch(char **cmdline_p) 这个阶段的内存分配其实很简单, 因此我们往往称之为内存分配器(而不是内存管理器), 早期的内核中内存分配器使用的**bootmem引导分配器**, 它基于一个内存位图bitmap, 使用最优适配算法来查找内存, 但是这个分配器有很大的缺陷, 最严重的就是内存碎片的问题, 因此在后来的内核中将其舍弃《而使用了**新的memblock机制**. memblock机制的初始化在arm64上是通过[arm64_memblock_init](http://lxr.free-electrons.com/source/arch/arm64/kernel/setup.c?v=4.7#L229)函数来实现的 +```cpp +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + | + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); + | 分页机制初始化 + | + |---->bootmem_init(); + | 始化内存数据结构包括内存节点, 内存域和页帧page + | + |---->arm64_numa_init(); + | 支持numa架构 + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 + | + |---->build_all_zonelist() [当前位置] + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | +``` - -##1.6 今日内容(第二阶段--初始化buddy内存管理) +##1.6 今日内容(第二阶段(二)--初始化备用内存域列表zonelists) ------- -**初始化内存分页机制** - - -在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. - - -在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 - - -paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 +我们之前讲了在memblock完成之后, 内存初始化开始进入第二阶段, 第二阶段是一个漫长的过程, 它执行了一系列复杂的操作, 从体系结构相关信息的初始化慢慢向上层展开, 其主要执行了如下操作 @@ -179,6 +217,41 @@ paging_init负责建立只能用于内核的页表, 用户空间是无法访问 在完成了基础的内存结点和内存域的初始化工作以后, 我们必须克服一些硬件的特殊设置 +* 在初始化内存的结点和内存区域之前, 内核先通过pagging_init初始化了内核的分页机制, 这样我们的虚拟运行空间就初步建立, 并可以完成物理地址到虚拟地址空间的映射工作. + +在arm64架构下, 内核在start_kernel()->setup_arch()中通过arm64_memblock_init( )完成了memblock的初始化之后, 接着通过setup_arch()->paging_init()开始初始化分页机制 + + +paging_init负责建立只能用于内核的页表, 用户空间是无法访问的. 这对管理普通应用程序和内核访问内存的方式,有深远的影响 + + +* 在分页机制完成后, 内核通过setup_arch()->bootmem_init开始进行内存基本数据结构(内存结点pg_data_t, 内存域zone和页帧)的初始化工作, 就是在这个函数中, 内核开始从体系结构相关的部分逐渐展开到体系结构无关的部分, 在zone_sizes_init->free_area_init_node中开始, 内核开始进行内存基本数据结构的初始化, 也不再依赖于特定体系结构无关的层次 + +```cpp +bootmem_init +始化内存数据结构包括内存节点, 内存域和页帧page +| +|---->arm64_numa_init(); +| 支持numa架构 +| +|---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 +| +|---->memblock_dump_all(); +| 初始化完成, 显示memblock的保留的所有内存信息 +``` + + + **建立内存管理的数据结构** diff --git a/study/kernel/02-memory/03-initialize/images/arch_do_somethig.png b/study/kernel/02-memory/03-initialize/images/arch_do_somethig.png new file mode 100644 index 0000000..6a9280c Binary files /dev/null and b/study/kernel/02-memory/03-initialize/images/arch_do_somethig.png differ diff --git a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md index 06ed87e..21ff471 100644 --- a/study/kernel/02-memory/04-buddy/01-buddy_system/README.md +++ b/study/kernel/02-memory/04-buddy/01-buddy_system/README.md @@ -10,6 +10,16 @@ 在内核初始化完成之后, 内存管理的责任就由伙伴系统来承担. 伙伴系统基于一种相对简单然而令人吃惊的强大算法. +Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该算法由Knowlton设计, 后来Knuth又进行了更深刻的描述. + +伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者. + +* 内核如何记住哪些内存块是空闲的 +* 分配空闲页面的方法 +* 影响分配器行为的众多标识位 +* 内存碎片的问题和分配器如何处理碎片 + + #2 伙伴系统的结构 @@ -28,7 +38,7 @@ struct zone }; ``` -struct free_area是一个辅助数据结构, 它定义在[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) +struct free_area是一个伙伴系统的辅助数据结构, 它定义在[include/linux/mmzone.h?v=4.7, line 88](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L88) ```cpp struct free_area { @@ -43,7 +53,30 @@ unsigned long nr_free; | nr_free | 指定了当前内存区中空闲页块的数目(对0阶内存区逐页计算,对1阶内存区计算页对的数目,对2阶内存区计算4页集合的数目,依次类推 | -阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是2order,其中order的范围从0到MAX_ORDER, 参见[include/linux/mmzone.h?v=4.7, line 22](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L22) + +伙伴系统的分配器维护空闲页面所组成的块, 这里每一块都是2的方幂个页面, 方幂的指数称为**阶**. + + +阶是伙伴系统中一个非常重要的术语. 它描述了内存分配的数量单位. 内存块的长度是$2^order$, 其中order的范围从0到MAX_ORDER + + +zone->free_area[MAX_ORDER]数组中阶作为各个元素的索引, 用于指定对应链表中的连续内存区包含多少个页帧. + +* 数组中第0个元素的阶为0, 它的free_list链表域指向具有包含区为单页($2^0=1$)的内存页面链表 + +* 数组中第1个元素的free_list域管理的内存区为两页($2^1=2$) + +* 第3个管理的内存区为4页, 依次类推. + +* 直到$2^{MAX_ORDER-1}$个页面大小的块 + +![空闲页快](../images/order_free_list.png) + + +##MAX_ORDER与FORCE_MAX_ZONEORDER配置选项 +------- + +一般来说`MAX_ORDER`默认定义为11, 这意味着一次分配可以请求的页数最大是2^11=2048, 参见[include/linux/mmzone.h?v=4.7, line 22](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L22) ```cpp /* Free memory management - zoned buddy allocator. */ @@ -55,7 +88,7 @@ unsigned long nr_free; #define MAX_ORDER_NR_PAGES (1 << (MAX_ORDER - 1)) ``` -该常数通常设置为11,这意味着一次分配可以请求的页数最大是2^11=2048 + 但如果特定于体系结构的代码设置了`FORCE_MAX_ZONEORDER`配置选项, 该值也可以手工改变 @@ -82,16 +115,10 @@ default "11"` -free_area[]数组中各个元素的索引也解释为阶, 用于指定对应链表中的连续内存区包含多少个页帧. - -* 第0个链表包含的内存区为单页($2^0=1$) - -* 第1个链表管理的内存区为两页($2^1=2$) - -* 第3个管理的内存区为4页, 依次类推. -内存区是如何连接的? +##内存区是如何连接的 +------- 内存区中第1页内的链表元素, 可用于将内存区维持在链表中。因此,也不必引入新的数据结构来管理物理上连续的页,否则这些页不可能在同一内存区中. 如下图所示 @@ -112,7 +139,8 @@ free_area[]数组中各个元素的索引也解释为阶, 用于指定对应链 最后要注意, 有关伙伴系统和当前状态的信息可以在/proc/buddyinfo中获取 -![伙伴系统和当前状态的信息](../imaes) +![伙伴系统和当前状态的信息](../images/buddy_info.png) + 上述输出给出了各个内存域中每个分配阶中空闲项的数目, 从左至右, 阶依次升高. 上面给出的信息取自4 GiB物理内存的AMD64系统. diff --git a/study/kernel/02-memory/04-buddy/02-initialization/README.md b/study/kernel/02-memory/04-buddy/02-initialization/README.md new file mode 100644 index 0000000..b08e4f0 --- /dev/null +++ b/study/kernel/02-memory/04-buddy/02-initialization/README.md @@ -0,0 +1,113 @@ +服务器体系与共享存储器架构 +======= + +| 日期 | 内核版本 | 架构| 作者 | 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) | + + + +#1 前景回顾 +------ + + +##1.1 启动阶段的内存初始化 +------- + +之前我们讲解了系统内存管理初始化的第二阶段(buddy的初始化), 但是我们讲解的很粗糙, 我们仅仅讲解了内存管理的主要流程创建 + + +```cpp +start_kernel() + |---->page_address_init() + | 考虑支持高端内存 + | 业务:初始化page_address_pool链表; + | 将page_address_maps数组元素按索引降序插入 + | page_address_pool链表; + | 初始化page_address_htable数组. + | + |---->setup_arch(&command_line); + | 初始化特定体系结构的内容 + | + |---->arm64_memblock_init( ); + | 初始化引导阶段的内存分配器memblock + | + |---->paging_init(); + | 分页机制初始化 + | + |---->bootmem_init(); + | 始化内存数据结构包括内存节点, 内存域和页帧page + | + |---->arm64_numa_init(); + | 支持numa架构 + | + |---->zone_sizes_init(min, max); + 来初始化节点和管理区的一些数据项 + | + |---->free_area_init_node + | 初始化内存节点 + | + |---->free_area_init_core + | 初始化zone + | + |---->memmap_init + | 初始化page页面 + | + |---->memblock_dump_all(); + | 初始化完成, 显示memblock的保留的所有内存信息 + | + |---->build_all_zonelist() + | 为系统中的zone建立后备zone的列表. + | 所有zone的后备列表都在 + | pglist_data->node_zonelists[0]中; + | + | 期间也对per-CPU变量boot_pageset做了初始化. + | +``` + + +```cpp + |---->page_alloc_init() + |---->hotcpu_notifier(page_alloc_cpu_notifier, 0); + | 不考虑热插拔CPU + | + |---->pidhash_init() + | 详见下文. + | 根据低端内存页数和散列度,分配hash空间,并赋予pid_hash + | + |---->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; + |解buddy的内部机理, + +``` + + + +我们只在特定于体系结构的代码中看到了内核如何检测系统中的可用内存。与高层数据结构(如内存域和结点)的关联, 则需要根据该信息构建。我们知道,体系结构相关代码需要在启动期间建立以下信息: + +* 系统中各个内存域的页帧边界,保存在max_zone_pfn数组 + +* 各结点页帧的分配情况,保存在全局变量early_node_map中 + + +##1.2 内存节点的初始化 +------- + + + +##1.3 今日内容(buddy的初始化) +------- + + diff --git a/study/kernel/02-memory/04-buddy/02-fragmentation/README.md b/study/kernel/02-memory/04-buddy/03-fragmentation/README.md similarity index 100% rename from study/kernel/02-memory/04-buddy/02-fragmentation/README.md rename to study/kernel/02-memory/04-buddy/03-fragmentation/README.md diff --git a/study/kernel/02-memory/04-buddy/03-initialization/README.md b/study/kernel/02-memory/04-buddy/03-initialization/README.md deleted file mode 100644 index 1a3d0ec..0000000 --- a/study/kernel/02-memory/04-buddy/03-initialization/README.md +++ /dev/null @@ -1,46 +0,0 @@ -服务器体系与共享存储器架构 -======= - -| 日期 | 内核版本 | 架构| 作者 | 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) | - - -#1 目录 -------- - -| CSDN | GitHub | -|:-------:|:-------:| -| 描述物理内存 | -| 页表管理 | -| 初始化内存挂历 | -| 物理内存的管理 | -| slab分配器 | - -| 非连续内存分配 | -| 高端内存管理 | -| 页面帧回收 | -| 交换管理 | -| 进程虚拟地址空间 | -| 共享内存虚拟文件系统 | -| 内存溢出管理 | - - -#2 参考内容 -------- - -| 链接 | -|:-------:| -| [内存管理(一)内存模型之Node](http://biancheng.dnbcw.info/linux/387391.html) | -| [Linux 内存管理 重要结构体](http://blog.chinaunix.net/uid-26009500-id-3078986.html) | -| [Bootmem机制](http://blog.csdn.net/samssm/article/details/25064897) | -| [Linux-2.6.32 NUMA架构之内存和调度](http://www.cnblogs.com/zhenjing/archive/2012/03/21/linux_numa.html) | -| [Linux 用户空间与内核空间——高端内存详解](http://blog.csdn.net/tommy_wxie/article/details/17122923) | -| [探索 Linux 内存模型](http://www.ibm.com/developerworks/cn/linux/l-memmod/) | -| [Linux内存管理](http://blog.chinaunix.net/uid/21718047/cid-151509-list-2.html) | -| [内存管理-之内核内存管理-基于linux3.10](http://blog.csdn.net/shichaog/article/details/45509917) | -| [内存管理(一)](http://www.cnblogs.com/openix/p/3334026.html) | -| [Linux内存管理原理](http://www.cnblogs.com/zhaoyl/p/3695517.html) | -| [第 15 章 内存映射和 DMA](http://www.embeddedlinux.org.cn/ldd3/ch15.html) | -| [ 内存管理(二)struct page ](http://blog.chinaunix.net/uid-30282771-id-5176971.html) | - diff --git a/study/kernel/02-memory/04-buddy/images/order_free_list.png b/study/kernel/02-memory/04-buddy/images/order_free_list.png new file mode 100644 index 0000000..84e186a Binary files /dev/null and b/study/kernel/02-memory/04-buddy/images/order_free_list.png differ