内存管理

内存初始化

内存布局探测:E820图

  • E820介绍

    • 在x86的机器上,由bios提供的中断,中断号是0x15,在调用的时候AX寄存器必须为0xE820,每次返回一段内存的空间的起始地址和大小以及它的属性(可用的RAM or 被BIOS保留的)

注:以下代码 有的是linux 5.10,有的是linux 2.6.30.4

代码1 arch/x86/boot/memory.c

static void detect_memory_e820(void)
{
    int count = 0;
    struct biosregs ireg, oreg;
    struct boot_e820_entry *desc = boot_params.e820_table;
    static struct boot_e820_entry buf; /* static so it is zeroed */

    initregs(&ireg);        // 初始化寄存器
    ireg.ax  = 0xe820;      // 规范
    ireg.cx  = sizeof(buf); // 缓冲区大小
    ireg.edx = SMAP;        
    ireg.di  = (size_t)&buf; 

    /*
     * Note: at least one BIOS is known which assumes that the
     * buffer pointed to by one e820 call is the same one as
     * the previous call, and only changes modified fields.  Therefore,
     * we use a temporary buffer and copy the results entry by entry.
     *
     * This routine deliberately does not try to account for
     * ACPI 3+ extended attributes.  This is because there are
     * BIOSes in the field which report zero for the valid bit for
     * all ranges, and we don't currently make any use of the
     * other attribute bits.  Revisit this if we see the extended
     * attribute bits deployed in a meaningful way in the future.
     */

    do {  // 一条一条地取出所有entry
        intcall(0x15, &ireg, &oreg); // int指令产生(模拟)0x15中断
        ireg.ebx = oreg.ebx; /* for next iteration... */ // 表示下一次要读取的序号

        /* BIOSes which terminate the chain with CF = 1 as opposed
           to %ebx = 0 don't always report the SMAP signature on
           the final, failing, probe. */
        if (oreg.eflags & X86_EFLAGS_CF) // 表示调用产生了error,中止
            break;

        /* Some BIOSes stop returning SMAP in the middle of
           the search loop.  We don't know exactly how the BIOS
           screwed up the map at that point, we might have a
           partial map, the full map, or complete garbage, so
           just return failure. */
        if (oreg.eax != SMAP) { // 检查一下签名是不是SMAP,不是就是出问题了
            count = 0;
            break;
        }

        *desc++ = buf;
        count++;
    } while (ireg.ebx && count < ARRAY_SIZE(boot_params.e820_table)); // 直到取出的ebx为0

    boot_params.e820_entries = count; 
}

代码2 e820 entry定义 /usr/include/x86\_64-linux-gnu/asm/bootparam.h


struct boot_e820_entry {
    __u64 addr;
    __u64 size;
    __u32 type;
} __attribute__((packed));

代码3 打印E820图 arch/x86/kernel/e820.c

void __init e820_print_map(char *who)
{
    int i;

    for (i = 0; i < e820.nr_map; i++) {
        printk(KERN_INFO " %s: %016Lx - %016Lx ", who,
               (unsigned long long) e820.map[i].addr, // 起始地址
               (unsigned long long) // 终止地址
               (e820.map[i].addr + e820.map[i].size));
        e820_print_type(e820.map[i].type); // 类型
        printk(KERN_CONT "\n");
    }
}

memblock

用于启动阶段的一个简单的分配器,它负责page alloc初始化之前的内存分配管理以及在系统boot阶段满足最大内存的请求(请求大小超过page alloc的最大限制)

实现:所有状态都保存在一个全局变量中

代码4 include/linux/memblock.h

/**
 * struct memblock_region - represents a memory region
 * @base: base address of the region
 * @size: size of the region
 * @flags: memory region attributes
 * @nid: NUMA node id
 */
struct memblock_region {
    phys_addr_t base;
    phys_addr_t size;
    enum memblock_flags flags;
#ifdef CONFIG_NEED_MULTIPLE_NODES
    int nid;  // NUMA用于非对称内存访问
#endif
};

/**
 * struct memblock_type - collection of memory regions of certain type
 * @cnt: number of regions
 * @max: size of the allocated array
 * @total_size: size of all regions
 * @regions: array of regions
 * @name: the memory type symbolic name
 */
struct memblock_type {  // 内存区间
    unsigned long cnt;  // 区间的个数
    unsigned long max;
    phys_addr_t total_size;
    struct memblock_region *regions;  // 这个东西是数组
    char *name;
};

/**
 * struct memblock - memblock allocator metadata
 * @bottom_up: is bottom up direction?
 * @current_limit: physical address of the current allocation limit
 * @memory: usable memory regions
 * @reserved: reserved memory regions
 */
struct memblock {
    bool bottom_up;  /* is bottom up direction? */
    phys_addr_t current_limit;
    struct memblock_type memory;    // 可用内存区域
    struct memblock_type reserved;  // 保留内存区域
};

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DzwrtMg0-1608991788791)(D:\微云同步助手\同步的文件\学习\Linux内核学习.assets\image-20201226152333853.png)]

代码5 添加内存区域 mm/memblock.c

/**
 * memblock_add_range - add new memblock region
 * @type: memblock type to add new region into
 * @base: base address of the new region
 * @size: size of the new region
 * @nid: nid of the new region
 * @flags: flags of the new region
 *
 * Add new memblock region [@base, @base + @size) into @type.  The new region
 * is allowed to overlap with existing ones - overlaps don't affect already
 * existing regions.  @type is guaranteed to be minimal (all neighbouring
 * compatible regions are merged) after the addition.
 *
 * Return:
 * 0 on success, -errno on failure.
 */
static int __init_memblock memblock_add_range(struct memblock_type *type,
                phys_addr_t base, phys_addr_t size,
                int nid, enum memblock_flags flags)
{
    bool insert = false;
    phys_addr_t obase = base;
    phys_addr_t end = base + memblock_cap_size(base, &size);
    int idx, nr_new;
    struct memblock_region *rgn;

    if (!size)
        return 0;

    /* special case for empty array */
    if (type->regions[0].size == 0) {  // 如果一项也没有,直接插进去
        WARN_ON(type->cnt != 1 || type->total_size);
        type->regions[0].base = base;
        type->regions[0].size = size;
        type->regions[0].flags = flags;
        memblock_set_region_node(&type->regions[0], nid);
        type->total_size = size;
        return 0;
    }
repeat:
    /*
     * The following is executed twice.  Once with %false @insert and
     * then with %true.  The first counts the number of regions needed
     * to accommodate the new area.  The second actually inserts them.
     */
    base = obase;
    nr_new = 0;

    for_each_memblock_type(idx, type, rgn) {  // 遍历原有的区域,检查有没有重合
        phys_addr_t rbase = rgn->base;
        phys_addr_t rend = rbase + rgn->size;

        if (rbase >= end)  // 表示遍历结束
            break;
        if (rend <= base)
            continue;  // 表示没有重合
        /*
         * @rgn overlaps.  If it separates the lower part of new
         * area, insert that portion.
         */
        if (rbase > base) {
#ifdef CONFIG_NEED_MULTIPLE_NODES
            WARN_ON(nid != memblock_get_region_node(rgn));
#endif
            WARN_ON(flags != rgn->flags);
            nr_new++;
            if (insert)
                memblock_insert_region(type, idx++, base,
                               rbase - base, nid,
                               flags);
        }
        /* area below @rend is dealt with, forget about it */
        base = min(rend, end);
    }

    /* insert the remaining portion */
    if (base < end) {  // 插入memblock region
        nr_new++;
        if (insert)
            memblock_insert_region(type, idx, base, end - base,
                           nid, flags);
    }

    if (!nr_new)
        return 0;

    /*
     * If this was the first round, resize array and repeat for actual
     * insertions; otherwise, merge and return.
     */
    if (!insert) {
        while (type->cnt + nr_new > type->max)  // 如果当前数组空间不够就进行扩展
            if (memblock_double_array(type, obase, size) < 0)
                return -ENOMEM;
        insert = true;
        goto repeat;  // 再重复一遍循环,进行区域的插入
    } else {
        memblock_merge_regions(type);  // 合并相邻区间
        return 0;
    }
}
  • 将一段区域设为可用 memblock_add_node
  • 将一段区域设为保留 memblock_reserve
  • 从memblock中分配内存:
  • 其基本算法是,找到在memblock.memory但不在memblock.reserved的满足size大小的区域,然后将该段区域加入到memblock.reserved中

代码6 memblock分配内存

static void * __init memblock_alloc_internal(
                phys_addr_t size, phys_addr_t align,
                phys_addr_t min_addr, phys_addr_t max_addr,
                int nid, bool exact_nid)
  • memblock释放内存:memblock_free()
  • Linux Kernel中memblock的使用
  • kernel将自己占用的内存部分设为reserved,例如kernel的image所占内存,initrd所占的内存等
  • 将e820探测的可用内存加入到memblock.memory中
  • 总而言之,系统的空闲内存存在于memory中单不包括reserved的部分

page allocator

  • Linux内存中在运行阶段可用的大内存分配器,是以页为单位
  • 分配的大小以2的倍数为单位,范围从20到2MAX\_ORDER,MAX_ORDER可以编译选项CONFIG_FORCE_MAX_ZONEORDER配置,默认是11。即最大可以请求210个页面,一个页面是4K
  • 涉及的基本概念
  • Node: NUMA的概念,即系统中的内存节点,每个node都在struct pglist_data *node_data[]中有对应的一项,以node的ID为序号。NUMA的内存布局探测是在ACPI中完成的,和E820不一样。在代码中对应NODE_DATA()
  • Zone: 可以理解为每个页面的类型
  
  

- 每个node中都有对应的zone, 存放在`node_data[node.id]->node_zones[MAX_NR_ZONES];`
- Zone的类型有:`ZONE_DMA`;`ZONE_DMA32`;`ZONE_NORMAL`
  • Zone order:即zone的查找次序,它决定如果当前请求的类型不满足后应该随后要到哪个zone中去分配。比如,用户可能想请求ZONE_HIGH,但ZONE_HIGH中的内存已经分配完了,这时候可以让它到其它的ZONE
  • Kernel中有两种类型的order
  
  

- A: NODE序,即所有请求都优先在本地节点完成
- B: ZONE序,即在各个节点中优先分配相同类型的内存
  • Kernel中每一个页面都有一个表示结构,即struct page,存放在struct page mem\_map[]中,它以物理页面的序号作为索引,每个页面只能属于zone,函数page\_zone(page)可以找到page的zone
  
  
 ![image-20201226163804706]()
  • 算法:伙伴系统
  • 需要高效,且尽量避免碎片
  • 按照2的幂大小来组织内存,幂为0~MAX\_ORDER,对应有MAX\_ORDER-1条链表来组织空间
  • 每个ZONE都有MAX\_ORDER-1条链表,存放在zone->free_area[MAX_ORDER]
  • 分配时,根据请求的大小匹配到最佳空闲区,然后进行分配
  • 如果最佳空闲区没有空闲页面了,则一直往上请求然后将上层进行拆分,如再请求2(n+1)个内存
  • ![image-20201226164527612]()
  • 释放页面时,看它是否和相近的合并,如这种情况:
  • ![image-20201226164655858]()
  • 释放页面时,如果可以合并则拼成一个大的空闲区并将之移动到上层,一直这样合并下去:
  • ![image-20201226164721833]()
  • Linux Kernel中的页面组织方式
  
  

- 为了让`struct page`尽可能的小,page allocator和其它子系统复用很多的成员
- `page->lru`链表用来链接对应order的空闲块
- `page->_mapcount`为`PAGE_BUDDY_MAPCOUNT_VALUE`时表示以该page为起始页面的内存块是空闲的(中间块的计数为-1),如果分配出去则置位-1
- `page->private`表示该page为起始页面的内存块所有的order,即该内存块的大小是2page->private
- `page`所有的node和zone的信息都被编码在`page->flags`中,通过`page_to_nid(page)`可得到该page所在的node,通过`page_zonenum(page)`可得到该page所有的zone类型,`page_zone(page)`直接返回page所在的zone
- 找到自己的小伙伴的过程 
  - 先将自己的页号 对 (1 << MAX\_ORDER) 取余,即:  
     `page_index = page_to_pfn(page) & (1 << MAX_ORDER - 1)`
  - 然后找到小伙伴的index也就是 `__find_buddy_index()`  
     `buddy_idx = page_idx ^ (1 << order)`
  - 最后找到小伙伴的页面  
     `buddy = page + (buddy_idx - page_idx)`
  • 基于 page migration type 的页面分组:
  
  

- 将页面按照migration进行分组,为了避免外碎片而导入的机制,对应migration type的请求都会到相应的区域中寻找
- > 内部碎片的产生:因为所有的内存分配必须起始于可被 4、8 或 16 整除(视处理器体系结构而定)的地址或者因为MMU的分页机制的限制,决定内存分配算法仅能把预定大小的内存块分配给客户。假设当某个客户请求一个 43 字节的内存块时,因为没有适合大小的内存,所以它可能会获得 44字节、48字节等稍大一点的字节,因此由所需大小四舍五入而产生的多余空间就叫内部碎片。  
  >  外部碎片的产生: 频繁的分配与回收物理页面会导致大量的、连续且小的页面块夹杂在已分配的页面中间,就会产生外部碎片。
- 如果想请求的type不能满足,会fallback到其它类型中
- 每个zone都有自己单独的分组
- `enum {MIGRATE_UNMOVABLE, MIGRATE_RECLAIMABLE, MIGRATE_MOVABLE, MIGRATE_PCPTYPES, MIGRATE_RESERVE}`
  • Linux Kernel为了加速单个页面分配释放同时又提高cache利用率而导入的缓存
  
  

- 每个zone都有一个称为pcp(struct per\_cpu\_pages)的percpu缓存,例如  
   `pcp = &this_cpu_ptr(zone->pageset)->pcp`
- 如果是为cold的页表,也就是说长时间没有使用的页面可能不在cache中了,在释放的时候加至list末尾,否则加到头部(为了优先请求hot页面)



位于:include/linux/mmzone.h

内存映射

硬件背景

  • 使用从page allocator中得到的物理页面

    • 页面对应的是物理地址
    • CPU仅能使用虚拟地址来访问内存
    • 所以,应将物理地址关联到CPU寻址的虚拟地址
  • x86背景

    • 由于历史原因,x86寻址比较复杂:段映射+页面映射
    • 段映射在x86\_64中被废弃掉了
    • 简言之,就是一个radix-tree like的算法,将线性地址分成几个区域,然后各区域值作为对应页表(paging-structure)的偏移
  • CPU模式不同,寻址方式上有些小差别

    • 32位:2层页表(10+10+12)
    • 32位 PAE: 3层页表(2+9+9+12)
    • 64位:4层页表(9+9+9+9+12)
  • 如果映射有异常,CPU会产生page fault异常
  • TLB介绍

    • TLB用来缓存从虚拟地址到物理地址的映射
    • 在reload页表的时候回自动刷新
    • 如果映射关系有修改需要手动刷新TLB项

Linux Kernel 地址空间

  • 起源是CPU的内存保护机制:特权级和非特权级

    • 特权级可以做一切事情
    • 非特权级不能执行特权指令来修改系统资源
  • 两大空间: 内核空间和用户空间

    • 内核空间位于特权层
    • 用户空间位于非特权层
    • x86中有4个特权级,但是Linux内核只用了两级
    • x86_64有2个特权级
  • 内核空间和用户空间复用一部分地址空间

    • 用来避免统一进程内核态和用户态转换时对TLB的刷新
    • 在x86 CPU是内核空间1G,用户空间3G
    • 在x86\_64中,0xffff880000000000以下为用户空间,以上为内核空间
  • 将映射层次抽象成PGD, PUD, PMD, PTE,如果某层不存在,则其对应位数为0
  • 在内核中,虚拟地址的映射如下:

    • 基础映射,物理地址和虚拟地址在偏移地址(PAGE\_OFFSET)上是以1比1的关系映射的,即在内核中,物理地址对应的虚拟地址为: VG=PAGE\_OFFSET+PA
    • 内存拼接的映射,即vmalloc()映射的内存,用来将不连续的物理内存拼接成连续的虚拟地址供用户使用,用来减少内存碎片

Linux Kernel 内存映射

  • 32位CPU上的映射问题

    • 内核仅能使用1G的地址空间,也就是在同一时刻最大能使用1G的物理地址空间,如果物理内存超过1G,如何访问1G以上的内存?
    • 在这种情况下,kernel将一部分地址用来做动态映射,将不能直接访问的物理内存映射到这个地址中
    • ![image-20201226213145975]()
  • 各种API

    • vmalloc的APIs:

      • void *vmalloc(unsigned long size) / vfree(void *p)
    • kmap的APIs:

      • void *kmap(struct page *page) / kunmap(struct page *page)
    • kmap\_atomic的APIs:

      • void *kmap_atomic(struct page *page) / kunmap_atomic(void *addr)

slab allocator

  • 目的:相对应土工页面大小4K的page allocator来说,slab提供的小内存分配器
  • 背景:
  • 当前linux内核中提供了3种slab分配器,分别是slab,slub,slob,他们提供给外部使用的API都是一样的,在编译内核的时候只能选择其一
  • 默认为slub,slob仅适合用于嵌入式中(占用资源极少)
  • slab vs slub
  
  

- slab管理结构很大,设计很复杂,slub简化了一切
- slub便于调试
- 下面的算法分析以slub为例
  • slab简而言之,就是一个对象的缓存器,当有对象释放的时候,就缓存到slab里面,然后需要分配的时候,就从slab缓存中取出来
  • slab以page allocator作为后端,当缓存对象不够时,就从page allocator中取
  • API
  
  

- 创建一个slab: `struct kmem_cache *kmem_cache_create(name, size, align, flags, ctor)`
- 销毁slab: `kmem_cache_destroy`
- 从slab中分配对象 `kmem_cache_alloc`
- 将内存释放到slab中:`kmem_cache_free`
  • 除此之外,slab还内建了一些slab cache, 用于不需要特殊处理的对象分配,对用户可见的接口为
  
  

- 内存分配 `__always_inline`
- 内存释放 `kfree`
- 对于大块的内存请求,会落入到page allocator中

标签: Linux, 内存, 内核, size, struct, page, type, memblock

相关文章推荐

添加新评论,含*的栏目为必填