This commit is contained in:
gatieme
2016-08-15 23:52:21 +08:00
parent 1f4e4650f2
commit 6a7fb3986f
4 changed files with 156 additions and 35 deletions
@@ -15,8 +15,11 @@ Linux内核使用二进制伙伴算法来管理和分配物理内存页面, 该
伙伴系统是一个结合了2的方幂个分配器和空闲缓冲区合并计技术的内存分配方案, 其基本思想很简单. 内存被分成含有很多页面的大块, 每一块都是2个页面大小的方幂. 如果找不到想要的块, 一个大块会被分成两部分, 这两部分彼此就成为伙伴. 其中一半被用来分配, 而另一半则空闲. 这些块在以后分配的过程中会继续被二分直至产生一个所需大小的块. 当一个块被最终释放时, 其伙伴将被检测出来, 如果伙伴也空闲则合并两者.
* 内核如何记住哪些内存块是空闲的
* 分配空闲页面的方法
* 影响分配器行为的众多标识位
* 内存碎片的问题和分配器如何处理碎片
@@ -592,11 +595,11 @@ not_early:
总而言之, 这种做法避免了启动期间内核分配的内存(经常在系统的整个运行时间都不释放)散布到物理内存各处, 从而使其他类型的内存分配免受碎片的干扰,这也是页可移动性分组框架的最重要的目标之一.
# 分配器API
#4 分配器API
-------
## 分配内存的接口
##4.1 分配内存的接口
-------
就伙伴系统的接口而言, NUMA或UMA体系结构是没有差别的, 二者的调用语法都是相同的.
@@ -636,7 +639,9 @@ not_early:
| [free_page(struct page *)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L520)<br>[free_pages(struct page *, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3918) | 用于将一个或2order页返回给内存管理子系统。内存区的起始地址由指向该内存区的第一个page实例的指针表示 |
| [__free_page(addr)](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L519)<br>[__free_pages(addr, order)](http://lxr.free-electrons.com/source/mm/page_alloc.c?v=4.7#L3906) | 类似于前两个函数,但在表示需要释放的内存区时,使用了虚拟内存地址而不是page实例 |
##分配掩码
##4.2 分配掩码
-------
@@ -663,13 +668,19 @@ not_early:
#define __GFP_HIGHMEM ((__force gfp_t)___GFP_HIGHMEM)
#define __GFP_DMA32 ((__force gfp_t)___GFP_DMA32)
#define __GFP_MOVABLE ((__force gfp_t)___GFP_MOVABLE) /* ZONE_MOVABLE allowed */
#define GFP_ZONEMASK (__GFP_DMA|__GFP_HIGHMEM|__GFP_DMA32|__GFP_MOVABLE)
// line 194 ~ line 260
#define GFP_DMA __GFP_DMA
#define GFP_DMA32 __GFP_DMA32
```
其中GFP缩写的意思为获取空闲页(get free page), __GFP_MOVABLE不表示物理内存域, 但通知内核应在特殊的虚拟内存域ZONE_MOVABLE进行相应的分配.
我们从注释中找到这样的信息
我们从注释中找到这样的信息, 可以作为参考
```cpp
bit result
=================
@@ -693,9 +704,11 @@ bit result
GFP_ZONES_SHIFT must be <= 2 on 32 bit platforms.
```
很有趣的一点是,没有\__GFP_NORMAL常数,而内存分配的主要负担却落到ZONE_NORMAL内存域
内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394)
内核考虑到这一点, 提供了一个函数gfp_zone来计算与给定分配标志兼容的最高内存域. 那么内存分配可以从该内存域或更低的内存域进行, 该函数定义在[include/linux/gfp.h?v=4.7, line 394](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L394)
```cpp
static inline enum zone_type gfp_zone(gfp_t flags)
@@ -710,5 +723,75 @@ static inline enum zone_type gfp_zone(gfp_t flags)
}
```
其中GFP_ZONES_SHIFT的定义如下, 在[include/linux/gfp.h?v=4.7, line 337](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L337)
```cpp
#if defined(CONFIG_ZONE_DEVICE) && (MAX_NR_ZONES-1) <= 4
/* ZONE_DEVICE is not a valid GFP zone specifier */
#define GFP_ZONES_SHIFT 2
#else
#define GFP_ZONES_SHIFT ZONES_SHIFT
#endif
#if 16 * GFP_ZONES_SHIFT > BITS_PER_LONG
#error GFP_ZONES_SHIFT too large to create GFP_ZONE_TABLE integer
#endif
```
由于内存域修饰符的解释方式不是那么直观, 表3-7给出了该函数结果的一个例子, 其中DMA和DMA32内存域相同. 假定在下文中没有设置\__GFP_MOVABLE修饰符.
| 修饰符 | 扫描的内存域 |
|:-------:|:--------------:|
| 无 | ZONE_NORMAL、ZONE_DMA |
| \__GFP_DMA | ZONE_DMA |
| \__GFP_DMA & \__GFP_HIGHMEM | ZONE_DMA |
| \__GFP_HIGHMEM | ZONE_HIGHMEM、ZONE_NORMAL、ZONE_DMA |
* 如果\__GFP_DMA和\__GFP_HIGHMEM都没有设置, 则首先扫描ZONE_NORMAL, 后面是ZONE_DMA
* 如果设置了\__GFP_HIGHMEM没有设置__GFP_DMA,则结果是从ZONE_HIGHMEM开始扫描所有3个内存域。=
* 如果设置了__GFP_DMA,那么\__GFP_HIGHMEM设置与否没有关系. 只有ZONE_DMA用于3种情形. 这是合理的, 因为同时使用\__GFP_HIGHMEM和__GFP_DMA没有意义. 高端内存从来都不适用于DMA
设置\__GFP_MOVABLE不会影响内核的决策,除非它与\__GFP_HIGHMEM同时指定. 在这种情况下, 会使用特殊的虚拟内存域ZONE_MOVABLE满足内存分配请求. 对前文描述的内核的反碎片策略而言, 这种行为是必要的.
除了内存域修饰符之外, 掩码中还可以设置一些标志.
下图中给出了掩码的布局,以及与各个比特位置关联的常数. \__GFP_DMA32出现了几次,因为它可能位于不同的地方.
![GFP掩码的布局](../images/gfp_flag_mask.png)
与内存域修饰符相反, 这些额外的标志并不限制从哪个物理内存段分配内存, 但确实可以改变分配器的行为. 例如, 它们可以修改查找空闲内存时的积极程度. 内核源代码中定义的下列标志, 定义在[include/linux/gfp.h?v=4.7, line 23](http://lxr.free-electrons.com/source/include/linux/gfp.h?v=4.7#L23)
```cpp
#define ___GFP_HIGH 0x20u /* 应该访问紧急分配池? */
#define ___GFP_IO 0x40u /* 可以启动物理IO */
#define ___GFP_FS 0x80u /* 可以调用底层文件系统? */
#define ___GFP_COLD 0x100u /* 需要非缓存的冷页 */
#define ___GFP_NOWARN 0x200u /* 禁止分配失败警告 */
#define ___GFP_REPEAT 0x400u /* 重试分配,可能失败 */
#define ___GFP_NOFAIL 0x800u /* 一直重试,不会失败 */
#define ___GFP_NORETRY 0x1000u /* 不重试,可能失败 */
#define ___GFP_MEMALLOC 0x2000u /* 不使用紧急分配链表 */
#define ___GFP_COMP 0x4000u
#define ___GFP_ZERO 0x8000u
#define ___GFP_NOMEMALLOC 0x10000u
#define ___GFP_HARDWALL 0x20000u
#define ___GFP_THISNODE 0x40000u /* 没有备用结点,没有策略 */
#define ___GFP_ATOMIC 0x80000u
#define ___GFP_ACCOUNT 0x100000u
#define ___GFP_NOTRACK 0x200000u
#define ___GFP_DIRECT_RECLAIM 0x400000u
#define ___GFP_OTHER_NODE 0x800000u
#define ___GFP_WRITE 0x1000000u
#define ___GFP_KSWAPD_RECLAIM 0x2000000u
```
@@ -89,7 +89,7 @@ start_kernel()
| :
| 14 - PAGE_SHIFT;
| alloc_large_system_hash的最后参数值为0;
|buddy的内部机理,
|buddy的内部机理,
```
@@ -102,11 +102,13 @@ start_kernel()
* 各结点页帧的分配情况,保存在全局变量early_node_map中
##1.2 内存节点的初始化
-------
##1.3 今日内容(buddy的初始化)
-------
@@ -8,6 +8,7 @@
| 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 前景提要
-------
@@ -16,14 +17,11 @@
-------
##1.2 buddy伙伴系统
-------
##1.2 碎片化问题
##1.3 今日内容(buddy伙伴系统如何避免碎片)
-------
**分页与分段**
页是信息的物理单位, 分页是为了实现非连续分配, 以便解决内存碎片问题, 或者说分页是由于系统管理的需要. 段是信息的逻辑单位,它含有一组意义相对完整的信息, 分段的目的是为了更好地实现共享, 满足用户的需要.
@@ -32,24 +30,57 @@
分页的作业地址空间是一维的. 分段的地址空间是二维的.
**内部碎片与外部碎片**
随着存储区的分配和释放过程的进行,在各个被分配出去的分区之间会存在很多的小空闲区,暂时不能被利用,这就是"外部碎片"
    在固定分区管理算法中,分给程序的内存空间往往大于程序所需的空间,这剩余部分的空间不能被其他程序所用,这就是内部碎片。Linux分配内存的大小要求2的幂指数页,这也会产生严重的内部碎片。
内存管理中,“内零头”和“外零头”个指的是什么?在固定式分区分配、可变式分区分配、页式虚拟存储系统、段式虚拟存储系统中,各会存在何种零头?为什么?
解答:
页式虚拟存储系统中, 用户作业的地址空间被划分成若干大小相等的页面, 存储空间也分成也页大小相等的物理块, 但一般情况下, 作业的大小不可能都是物理块大小的整数倍, 因此作业的最后一页中仍有部分空间被浪费掉了. 由此可知, 页式虚拟存储系统中存在**内碎片**.
在段式虚拟存储系统中, 作业的地址空间由若干个逻辑分段组成, 每段分配一个连续的内存区, 但各段之间不要求连续, 其内存的分配方式类似于动态分区分配.由此可知, 段式虚拟存储系统中存在**外碎片**.
<font color = 0x00ffff>
在内存管理中, "内零头"和"外零头"个指的是什么?
在固定式分区分配, 可变式分区分配, 页式虚拟存储系统, 段式虚拟存储系统中, 各会存在何种碎片? 为什么?
</font>
解答:
在存储管理中
内零头是指分配给作业的存储空间中未被利用的部分,
外零头是指系统中无法利用的小存储块。
在固定式分区分配中,为将一个用户作业装入内存,内存分配程序从系统分区表中找出一个能满足作业要求的空闲分区分配给作业,由于一个作业的大小并不一定与分区大小相等,因此,分区中有一部分存储空间浪费掉了。由此可知,固定式分区分配中存在内零头。
在可变式分区分配中,为把一个作业装入内存,应按照一定的分配算法从系统中找出一个能满足作业需求的空闲分区分配给作业,如果这个空闲分区的容量比作业申请的空间容量要大,则将该分区一分为二,一部分分配给作业,剩下的部分仍然留作系统的空闲分区。由此可知,可变式分区分配中存在外零头。
在页式虚拟存储系统中,用户作业的地址空间被划分成若干大小相等的页面,存储空间也分成也页大小相等的物理块,但一般情况下,作业的大小不可能都是物理块大小的整数倍,因此作业的最后一页中仍有部分空间被浪费掉了。由此可知,页式虚拟存储系统中存在内零头。在段式虚拟存储系统中,作业的地址空间由若干个逻辑分段组成,每段分配一个连续的内存区,但各段之间不要求连续,其内存的分配方式类似于动态分区分配。由此可知,段式虚拟存储系统中存在外零头。
* **内碎片**是指分配给作业的存储空间中未被利用的部分
#2 伙伴系统的问题
在固定式分区分配中, 为将一个用户作业装入内存, 内存分配程序从系统分区表中找出一个能满足作业要求的空闲分区分配给作业, 由于一个作业的大小并不一定与分区大小相等, 因此, 分区中有一部分存储空间浪费掉了. 由此可知, 固定式分区分配中存在**内碎片**.
* **外碎片**是指系统中无法利用的小存储块.
在可变式分区分配中, 为把一个作业装入内存, 应按照一定的分配算法从系统中找出一个能满足作业需求的空闲分区分配给作业, 如果这个空闲分区的容量比作业申请的空间容量要大, 则将该分区一分为二, 一部分分配给作业, 剩下的部分仍然留作系统的空闲分区。由此可知,可变式分区分配中存在**外碎片**.
简言之
随着存储区的分配和释放过程的进行, 在各个被分配出去的分区之间会存在很多的小空闲区, 暂时不能被利用, 这就是"外部碎片".
在固定分区管理算法中, 分给程序的内存空间往往大于程序所需的空间, 这剩余部分的空间不能被其他程序所用, 这就是"内部碎片"
##1.3 buddy伙伴系统
-------
##1.4 今日内容(buddy伙伴系统如何避免碎片)
-------
Linux伙伴系统分配内存的大小要求2的幂指数页, 这也会产生严重的**内部碎片**.
伙伴系统的基本原理已经在前面已经讨论过, 一个双链表即可满足伙伴系统的所有需求, 其方案在最近几年间确实工作得非常好。但在Linux内存管理方面,有一个长期存在的问题 : 在系统启动并长期运行后,物理内存会产生很多碎片。该情形如下图所示
![物理内存的碎片](../images/physical_memory_fragmentation.png)
@@ -72,13 +103,18 @@
很长时间以来,物理内存的碎片确实是Linux的弱点之一。尽管已经提出了许多方法,但没有哪个方法能够既满足Linux需要处理的各种类型工作负荷提出的苛刻需求,同时又对其他事务影响不大。
目前Linux内核为解决内存碎片的方案提供了两类解决方案
#3 依据可移动性组织页避免内存碎片
* 依据可移动性组织页避免内存碎片
* 虚拟可移动内存域避免内存碎片
#2 依据可移动性组织页避免内存碎片
-------
依据可移动性组织页是方式物理内存碎片的一种可能方法.
##3.1 依据可移动性组织页
##2.1 依据可移动性组织页
-------
@@ -122,7 +158,7 @@
##3.2 迁移类型
##2.2 迁移类型
-------
@@ -183,7 +219,7 @@ extern char * const migratetype_names[MIGRATE_TYPES];
#endif
```
##3.3 free_area的改进
##2.3 free_area的改进
-------
@@ -215,7 +251,7 @@ unsigned long nr_free;
```
##3.4 迁移备用列表fallbacks
##2.4 迁移备用列表fallbacks
-------
@@ -257,7 +293,7 @@ static int fallbacks[MIGRATE_TYPES][4] = {
>每一行对应一个类型的备用搜索域的顺序, 在内核想要分配不可移动页`MIGRATE_UNMOVABLE`时, 如果对应链表为空, 则遍历fallbacks[MIGRATE_UNMOVABLE], 首先后退到可回收页链表`MIGRATE_RECLAIMABLE`, 接下来到可移动页链表`MIGRATE_MOVABLE`, 最后到紧急分配链表`MIGRATE_TYPES`.
##3.5 全局pageblock_order变量
##2.5 全局pageblock_order变量
-------
@@ -308,7 +344,7 @@ static int fallbacks[MIGRATE_TYPES][4] = {
##3.6 gfpflags_to_migratetype转换分配标识到迁移类型
##2.6 gfpflags_to_migratetype转换分配标识到迁移类型
-------
@@ -350,7 +386,7 @@ static inline int allocflags_to_migratetype(gfp_t gfp_flags)
, . . free_area.free_list的数组索引.
##3.7 pageblock_flags变量与其函数接口
##2.7 pageblock_flags变量与其函数接口
, , pageblock_nr_pages个页的内存区的属性. zone->pageblock_flags字段, 使, [include/linux/mmzone.h?v=4.7, line 367](http://lxr.free-electrons.com/source/include/linux/mmzone.h?v=4.7#L367)
@@ -413,7 +449,7 @@ void set_pageblock_migratetype(struct page *page, int migratetype)
PB_migrate_end, MIGRATETYPE_MASK)
```
##3.8 /proc/pagetypeinfo获取页面分配状态
##2.8 /proc/pagetypeinfo获取页面分配状态
-------
最后请注意, 在各个迁移链表之间, 当前的页面分配状态可以从`/proc/pagetypeinfo`获得.
@@ -424,7 +460,7 @@ void set_pageblock_migratetype(struct page *page, int migratetype)
##3.9 可移动性的分组的初始化
##2.9 可移动性的分组的初始化
-------
@@ -468,10 +504,10 @@ not_early:
#4 虚拟可移动内存域避免内存碎片
#3 虚拟可移动内存域避免内存碎片
-------
##4.1 虚拟可移动内存域
##3.1 虚拟可移动内存域
依据可移动性组织页是防止物理内存碎片的一种可能方法,内核还提供了另一种阻止该问题的手段 : 虚拟内存域`ZONE_MOVABLE`.
@@ -484,7 +520,7 @@ not_early:
这显然对内核要求太高,因此系统管理员必须作出决定。毕竟,人可以更好地预测计算机需要处理的场景,以及各种类型内存分配的预期分布.
##4.2 数据结构
##3.2 数据结构
-------
@@ -550,7 +586,7 @@ static bool mirrored_kernelcore;
内核确保这些页将用于满足符合ZONE_MOVABLE职责的内存分配。
##4.3 实现
##3.3 实现
-------
Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB