Linux-3.14.12内存管理笔记【构建内存管理框架(1)】

摘要:
在传统的计算机架构中,整个物理内存都是在线的,CPU需要同时访问整个内存空间。这种情况下的内存结构称为NUMA。由于NUMA存储结构的引入,它需要相应的管理机制来支持它。Linux 2.4已经开始支持它。因此,在当前分析的3.14.12版本中,Linux的物理内存管理机制将物理内存分为三个级别进行管理,即节点、区域(管理区域)和页面(页面)。Linux内核的设计是释放32页全局目录条目,占256页的1/8。

传统的计算机结构中,整个物理内存都是一条线上的,CPU访问整个内存空间所需要的时间都是相同的。这种内存结构被称之为UMA(Uniform Memory Architecture,一致存储结构)。但是随着计算机的发展,一些新型的服务器结构中,尤其是多CPU的情况下,物理内存空间的访问就难以控制所需的时间相同了。在多CPU的环境下,系统只有一条总线,有多个CPU都链接到上面,而且每个CPU都有自己本地的物理内存空间,但是也可以通过总线去访问别的CPU物理内存空间,同时也存在着一些多CPU都可以共同访问的公共物理内存空间。于是乎这就出现了一个新的情况,由于各种物理内存空间所处的位置不同,于是访问它们的时间长短也就各异,没法保证一致。对于这种情况的内存结构,被称之为NUMA(Non-Uniform Memory Architecture,非一致存储结构)。事实上也没有完全的UMA,比如常见的单CPU电脑,RAM、ROM等物理存储空间的访问时间并非一致的,只是纯粹对RAM而言,是UMA的。此外还有一种称之为MPP的结构(Massive Parallel Processing,大规模并行处理系统),是由多个SMP服务器通过一定的节点互联网络进行连接,协同工作,完成相同的任务。从外界使用者看来,它是一个服务器系统。

回归正题,着重看一下NUMA。由于NUMA存储结构的引入,这就需要相应的管理机制来支持, linux 2.4版本就已经开始对其支持了。随着新增管理机制的支持,也随之引入了Node的概念(存储节点),把访问时间相同的存储空间归结为一个存储节点。于是当前分析的3.14.12版本,linux的物理内存管理机制将物理内存划分为三个层次来管理,依次是:Node(存储节点)、Zone(管理区)和Page(页面)。

image

存储节点的数据结构为pg_data_t,每个NUMA节点都有一个pg_data_t负责记载该节点的内存布局信息。其中pg_data_t结构体中存储管理区信息的为node_zones成员,其数据结构为zone,每个pg_data_t都有多个node_zones,通常是三个:ZONE_DMA、ZONE_NORMAL、ZONE_HIGHMEM。

ZONE_DMA区通常是由于计算机中部分设备无法直接访问全部内存空间而特地划分出来给该部分设备使用的区,x86环境中,该区通常小于16M。

ZONE_NORMAL区位于ZONE_DMA后面,这个区域被内核直接映射到线性地址的高端部分,x86环境中,该区通常为16M-896M。

ZONE_HIGHMEM区则是系统除了ZONE_DMA和ZONE_NORMAL区后剩下的物理内存,这个区不能直接被内核映射,x86环境中,该区通常为896M以后的内存。

为什么要有高端内存的存在?通常都知道内核空间的大小为1G(线性空间为:3-4G)。那么映射这1G内存需要多少页全局目录项?很容易可以算出来是256项,内核有这么多线程在其中,1G够吗?很明显不够,如果要使用超出1G的内存空间怎么办?如果要使用内存,很明显必须要做映射,那么腾出几个页全局目录项出来做映射?Bingo,就是这样,那么腾出多少来呢?linux内核的设计就是腾出32个页全局目录项,256的1/8。那么32个页全局目录项对应多大的内存空间?算一下可以知道是128M,也就是说直接映射的内存空间是896M。使用超过896M的内存空间视为高端内存,一旦使用的时候,就需要做映射转换,这是一件很耗资源的事情。所以不要常使用高端内存,就是这么一个由来。

接着看一下内存管理框架的初始化实现,initmem_init():

【file:/arch/x86/mm/init_32.c】
#ifndef CONFIG_NEED_MULTIPLE_NODES
void __init initmem_init(void)
{
#ifdef CONFIG_HIGHMEM
    highstart_pfn = highend_pfn = max_pfn;
    if (max_pfn > max_low_pfn)
        highstart_pfn = max_low_pfn;
    printk(KERN_NOTICE "%ldMB HIGHMEM available.
",
        pages_to_mb(highend_pfn - highstart_pfn));
    high_memory = (void *) __va(highstart_pfn * PAGE_SIZE - 1) + 1;
#else
    high_memory = (void *) __va(max_low_pfn * PAGE_SIZE - 1) + 1;
#endif
 
    memblock_set_node(0, (phys_addr_t)ULLONG_MAX, &memblock.memory, 0);
    sparse_memory_present_with_active_regions(0);
 
#ifdef CONFIG_FLATMEM
    max_mapnr = IS_ENABLED(CONFIG_HIGHMEM) ? highend_pfn : max_low_pfn;
#endif
    __vmalloc_start_set = true;
 
    printk(KERN_NOTICE "%ldMB LOWMEM available.
",
            pages_to_mb(max_low_pfn));
 
    setup_bootmem_allocator();
}
#endif /* !CONFIG_NEED_MULTIPLE_NODES */

将high_memory初始化为低端内存页框max_low_pfn对应的地址大小,接着调用memblock_set_node,根据函数命名,可以推断出该函数用于给早前建立的memblock算法设置node节点信息。

memblock_set_node的实现:

【file:/mm/memblock.c】
/**
 * memblock_set_node - set node ID on memblock regions
 * @base: base of area to set node ID for
 * @size: size of area to set node ID for
 * @type: memblock type to set node ID for
 * @nid: node ID to set
 *
 * Set the nid of memblock @type regions in [@base,@base+@size) to @nid.
 * Regions which cross the area boundaries are split as necessary.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
int __init_memblock memblock_set_node(phys_addr_t base, phys_addr_t size,
                      struct memblock_type *type, int nid)
{
    int start_rgn, end_rgn;
    int i, ret;
 
    ret = memblock_isolate_range(type, base, size, &start_rgn, &end_rgn);
    if (ret)
        return ret;
 
    for (i = start_rgn; i < end_rgn; i++)
        memblock_set_region_node(&type->regions[i], nid);
 
    memblock_merge_regions(type);
    return 0;
}

memblock_set_node主要调用了三个函数做相关操作:memblock_isolate_range、memblock_set_region_node和memblock_merge_regions。

其中memblock_isolate_range:

【file:/mm/memblock.c】
/**
 * memblock_isolate_range - isolate given range into disjoint memblocks
 * @type: memblock type to isolate range for
 * @base: base of range to isolate
 * @size: size of range to isolate
 * @start_rgn: out parameter for the start of isolated region
 * @end_rgn: out parameter for the end of isolated region
 *
 * Walk @type and ensure that regions don't cross the boundaries defined by
 * [@base,@base+@size). Crossing regions are split at the boundaries,
 * which may create at most two more regions. The index of the first
 * region inside the range is returned in *@start_rgn and end in *@end_rgn.
 *
 * RETURNS:
 * 0 on success, -errno on failure.
 */
static int __init_memblock memblock_isolate_range(struct memblock_type *type,
                    phys_addr_t base, phys_addr_t size,
                    int *start_rgn, int *end_rgn)
{
    phys_addr_t end = base + memblock_cap_size(base, &size);
    int i;
 
    *start_rgn = *end_rgn = 0;
 
    if (!size)
        return 0;
 
    /* we'll create at most two more regions */
    while (type->cnt + 2 > type->max)
        if (memblock_double_array(type, base, size) < 0)
            return -ENOMEM;
 
    for (i = 0; i < type->cnt; i++) {
        struct memblock_region *rgn = &type->regions[i];
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;
 
        if (rbase >= end)
            break;
        if (rend <= base)
            continue;
 
        if (rbase < base) {
            /*
             * @rgn intersects from below. Split and continue
             * to process the next region - the new top half.
             */
            rgn->base = base;
            rgn->size -= base - rbase;
            type->total_size -= base - rbase;
            memblock_insert_region(type, i, rbase, base - rbase,
                           memblock_get_region_node(rgn),
                           rgn->flags);
        } else if (rend > end) {
            /*
             * @rgn intersects from above. Split and redo the
             * current region - the new bottom half.
             */
            rgn->base = end;
            rgn->size -= end - rbase;
            type->total_size -= end - rbase;
            memblock_insert_region(type, i--, rbase, end - rbase,
                           memblock_get_region_node(rgn),
                           rgn->flags);
        } else {
            /* @rgn is fully contained, record it */
            if (!*end_rgn)
                *start_rgn = i;
            *end_rgn = i + 1;
        }
    }
 
    return 0;
}

该函数主要做分割操作,在memblock算法建立时,只是判断了flags是否相同,然后将连续内存做合并操作,而此时建立node节点,则根据入参base和size标记节点内存范围将内存划分开来。如果memblock中的region恰好以该节点内存范围末尾划分开来的话,那么则将region的索引记录至start_rgn,索引加1记录至end_rgn返回回去;如果memblock中的region跨越了该节点内存末尾分界,那么将会把当前的region边界调整为node节点内存范围边界,另一部分通过memblock_insert_region()函数插入到memblock管理regions当中,以完成拆分。

顺便看一下memblock_insert_region()函数:

【file:/mm/memblock.c】
/**
 * memblock_insert_region - insert new memblock region
 * @type: memblock type to insert into
 * @idx: index for the insertion point
 * @base: base address of the new region
 * @size: size of the new region
 * @nid: node id of the new region
 * @flags: flags of the new region
 *
 * Insert new memblock region [@base,@base+@size) into @type at @idx.
 * @type must already have extra room to accomodate the new region.
 */
static void __init_memblock memblock_insert_region(struct memblock_type *type,
                           int idx, phys_addr_t base,
                           phys_addr_t size,
                           int nid, unsigned long flags)
{
    struct memblock_region *rgn = &type->regions[idx];
 
    BUG_ON(type->cnt >= type->max);
    memmove(rgn + 1, rgn, (type->cnt - idx) * sizeof(*rgn));
    rgn->base = base;
    rgn->size = size;
    rgn->flags = flags;
    memblock_set_region_node(rgn, nid);
    type->cnt++;
    type->total_size += size;
}

这里一个memmove()将后面的region信息往后移,另外调用memblock_set_region_node()将原region的node节点号保留在被拆分出来的region当中。

而memblock_set_region_node()函数实现仅是赋值而已:

【file:/mm/memblock.h】
static inline void memblock_set_region_node(struct memblock_region *r, int nid)
{
    r->nid = nid;
}

至此,回到memblock_set_node()函数,里面接着memblock_isolate_range()被调用的memblock_set_region_node()已知是获取node节点号,而memblock_merge_regions()则前面已经分析过了,是用于将region合并的。

最后回到initmem_init()函数中,memblock_set_node()返回后,接着调用的函数为sparse_memory_present_with_active_regions()。

这里sparse memory涉及到linux的一个内存模型概念。linux内核有三种内存模型:Flat memory、Discontiguous memory和Sparse memory。其分别表示:

  • Flat memory:顾名思义,物理内存是平坦连续的,整个系统只有一个node节点。
  • Discontiguous memory:物理内存不连续,内存中存在空洞,也因而系统将物理内存分为多个节点,但是每个节点的内部内存是平坦连续的。值得注意的是,该模型不仅是对于NUMA环境而言,UMA环境上同样可能存在多个节点的情况。
  • Sparse memory:物理内存是不连续的,节点的内部内存也可能是不连续的,系统也因而可能会有一个或多个节点。此外,该模型是内存热插拔的基础。

看一下sparse_memory_present_with_active_regions()的实现:

【file:/mm/page_alloc.c】
/**
 * sparse_memory_present_with_active_regions - Call memory_present for each active range
 * @nid: The node to call memory_present for. If MAX_NUMNODES, all nodes will be used.
 *
 * If an architecture guarantees that all ranges registered with
 * add_active_ranges() contain no holes and may be freed, this
 * function may be used instead of calling memory_present() manually.
 */
void __init sparse_memory_present_with_active_regions(int nid)
{
    unsigned long start_pfn, end_pfn;
    int i, this_nid;
 
    for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, &this_nid)
        memory_present(this_nid, start_pfn, end_pfn);
}

里面的for_each_mem_pfn_range()是一个旨在循环的宏定义,而memory_present()由于实验环境中没有定义CONFIG_HAVE_MEMORY_PRESENT,所以是个空函数。暂且搁置不做深入研究。

最后看一下initmem_init()退出前调用的函数setup_bootmem_allocator():

【file:/arch/x86/mm/init_32.c】
void __init setup_bootmem_allocator(void)
{
    printk(KERN_INFO " mapped low ram: 0 - %08lx
",
         max_pfn_mapped<<PAGE_SHIFT);
    printk(KERN_INFO " low ram: 0 - %08lx
", max_low_pfn<<PAGE_SHIFT);
}

原来该函数是用来初始化bootmem管理算法的,但现在x86的环境已经使用了memblock管理算法,这里仅作保留打印部分信息。

免责声明:文章转载自《Linux-3.14.12内存管理笔记【构建内存管理框架(1)】》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇vueRouter中scrollBehavior实现滚动固定位置桌面小部件开发下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

浅谈虚拟机、Docker和Hyper技术

操作系统 我们知道: 完整的操作系统=内核+apps 内核负责管理底层硬件资源,包括CPU、内存、磁盘等等,并向上为apps提供系统调用接口,上层apps应用必须通过系统调用方式使用硬件资源,通常并不能直接访问资源。apps就是用户直接接触的应用,比如命令行工具、图形界面工具等(linux的图形界面也是作为可选应用之一,而不像windows是集成到内核中...

XP 任务管理器——性能 中 各项的意思

XP  任务管理器——性能 中 各项的意思     1、物理内存——系统缓存 是指什么?是硬盘上的 虚拟内存 么?2、内存使用——总数、限制、峰值 都是指什么?3、核心内存 是指什么呢?   一个高手给我的回复:   物理内存:计算机上安装的总物理内存,也称RAM,“可用数”物理内存中可被程序使用的空余量。但实际的空余量要比这个数值略大一点,因为物理内存...

用 16G 内存存放 30亿数据(Java Map)转载

在讨论怎么去重,提出用 direct buffer 建 btree,想到应该有现成方案,于是找到一个好东西: MapDB - MapDB : http://www.mapdb.org/ 以下来自:kotek.net : http://kotek.net/blog/3G_map 3 billion items in Java Map with 16 GB R...

Linux内存描述之内存节点node--Linux内存管理(二)

1 内存节点node 1.1 为什么要用node来描述内存 这点前面是说的很明白了, NUMA结构下, 每个处理器CPU与一个本地内存直接相连, 而不同处理器之前则通过总线进行进一步的连接, 因此相对于任何一个CPU访问本地内存的速度比访问远程内存的速度要快 Linux适用于各种不同的体系结构, 而不同体系结构在内存管理方面的差别很大. 因此linux内核...

Windows内存小结(有好多图,比较清楚)

以前写过一篇理解程序内存, 当时主要是针对用户态,下面再稍微深入一点: 我们以32位程序为例(不启用AWE), 总共4G虚拟空间,其中低2G属于用户态, 高2G属于操作系统内核, 每个程序都有自己的低2G用户空间, 高2G内核空间是所有程序共享的。高2G内核空间中, 属于同一Session的程序又共享相同的session空间: x86系统所有的内存以...

GDB dump mem example和命令

使用方法: You can use the commandsdump,append, andrestoreto copy data between target memory and a file. Thedumpandappendcommands write data to a file, and therestorecommand reads data...