Linux内存管理 | 青训营笔记

255 阅读21分钟
  • 这是我参与「第三届青训营 -后端场」笔记创作活动的的第五篇笔记

一,页

内核把物理页作为内存管理的基本单位。

内核中用struct page结构表示系统中的每个物理页,该结构位于<linux/mm_types.h>中

struct page{
	unsigned long	flags;
    atomic_t	_count;
    atomic_t	_mapcount;
    unsigned long	private;
    struct address_space	*mapping;
    pgoff_t		index;
    struct list_head	lru;
    void	*virtual;
}
  • flag:每位表示一种状态,包括页是不是脏的,是不是被锁定在内存中等。
  • _count:存放页的引用计数,当计数值变为-1时,就说明当前内核并没有引用这一页,内核代码不应该直接检查该域,而是应该调用page_count()函数进行检查,该函数唯一的参数就是page参数,当页空闲的时候返回0表示空闲,返回一个正整数表示正在使用
  • virtual:页的虚拟地址,通常情况下,它就是页在虚拟内存中地址,有些内存(高端内存)并不永久地映射到内核地址空间上,这种情况下,该值为null,需要的时候再动态映射这些页

page结构是与物理页相关的,并非与虚拟页相关,因此,描述是短暂的,即使页中所包含的数据继续存在,由于交换等原因,它们也可能并不再和同一个page相关联。

内核只是用该数据结构描述当前时刻在相关物理页中存放的东西。这种数据结构目的在于描述物理内存本身,而不是描述包含其中的数据

在struct page的"flags"里,它用flags的高8位存储了它所属的zone和node。这是通过page flags找到page所属的node的方法:

static inline int page_to_nid(const struct page *page)
{
	struct page *p = (struct page *)page;
	return (PF_POISONED_CHECK(p)->flags >> NODES_PGSHIFT) & NODES_MASK;
}

这是通过page flags找到page所属的zone的:

static inline enum zone_type page_zonenum(const struct page *page)
{
	return (page->flags >> ZONES_PGSHIFT) & ZONES_MASK;
}

static inline struct zone *page_zone(const struct page *page)
{
	return &NODE_DATA(page_to_nid(page))->node_zones[page_zonenum(page)];
}

二,区

由于硬件限制,内核不能对所有页一视同仁,有些页只能位于内存中特定的物理地址上。由于存在这种限制,所以内核把页划分为不同的区(zone)。

  • 些硬件只能用某些特定的内存地址来执行DMA(直接内存访问)。
  • 一些体系结构的内存的物理寻址范围比虚拟寻址范围大得多。这样,就有一些内存不能永久地映射到内核空间上

因为存在这些制约条件, Linux主要使用了四种区:

  • ZONE DMA这个区包含的页能用来执行DMA操作。
  • ZONE DMA32—和 ZOME DMA类似,该区包含的页面可用来执行DMA操作;而和ZONE DMA不同之处在于,这些页面只能被32位设备访问。在某些体系结构中,该区将比 ZONE DMA更大。
  • ZONE NORMAL—这个区包含的都是能正常映射的页。
  • ZONE HIGHIEM这个区包含“高端内存”,其中的页并不能永久地映射到内核地址空间。

区的实际使用是和体系结构相关的,并不是所有体系结构都需要


有可能都用ZONE NORMAL区,如果硬件允许的话

Linux把系统的页划分为区,形成不同的内存池,分配内存就可以在对应的内存池中分配。注意,区的划分没有任何物理意义,这只不过是内核为了管理页而采取的一种逻辑上的分组。


struct zone {
     spinlock_t         lock;

     unsigned long      spanned_pages;
     unsigned long      present_pages; 
     unsigned long      nr_reserved_highatomic;    
     atomic_long_t      managed_pages;

     struct free_area   free_area[MAX_ORDER];
     unsigned long      _watermark[NR_WMARK];
     long               lowmem_reserve[MAX_NR_ZONES];
     atomic_long_t      vm_stat[NR_VM_ZONE_STAT_ITEMS];

     unsigned long      zone_start_pfn;
     struct pglist_data *zone_pgdat;
     struct page        *zone_mem_map;
     ...    
} 
  • lock:自旋锁,防止该结构被并发访问,注意,这个只保护结构,而不保护这个区中的所有页
  •  free_area:空闲区域数组,里面存放了空闲列表
  • watermark:该数组持有该区的最小值、最低和最高水位值。内核使用水位为每个内存区设置合适的内存消耗基准。该水位随空闲内存的多少而变化

三,NUMA

所谓物理内存,就是安装在机器上的,实打实的内存设备(不包括硬件cache),被CPU通过总线访问。

在多核系统中:

  • 如果物理内存对所有CPU来说没有区别,每个CPU访问内存的方式也一样,则这种体系结构被称为Uniform Memory Access(UMA)。
  • 如果物理内存是分布式的,由多个cell组成(比如每个核有自己的本地内存),那么CPU在访问靠近它的本地内存的时候就比较快,访问其他CPU的内存或者全局内存的时候就比较慢,这种体系结构被称为Non-Uniform Memory Access(NUMA)。

在Linux中,由于UMA可以看作只有一个node的特殊的NUMA,所以两者可以统一的用NUMA模型表示

在NUMA系统中,当Linux内核收到内存分配的请求时,它会优先从发出请求的CPU本地或邻近的内存node中寻找空闲内存,这种方式被称作lo cal allocation,local allocation能让接下来的内存访问相对底层的物理资源是local的。

每个node由一个或多个zone组成(我们可能经常在各种对虚拟内存和物理内存的描述中迷失,但以后你见到zone,就知道指的是物理内存),每个zone又由若干page frames组成(一般page frame都是指物理页面)

Node

在Linux,表示NUMA节点的是pglist_data结构体

typedef struct pglist_data {
     int nr_zones;
     struct zone node_zones[MAX_NR_ZONES];
     struct zonelist node_zonelists[MAX_ZONELIST];

     unsigned long node_size;
     struct page *node_mem_map;

     int node_id;
     unsigned long node_start_paddr;
     struct pglist_data *node_next;

     spinlock_t lru_lock;
     ...
} pg_data_t; 
  • nr_zones表示这个node含有多少个zones,node_zones[]则是一个包含各个zone结构体的数组。
  • node_zonelists[]包含了2个zonelist,一个是由本node的zones组成,另一个是由从本node分配不到内存时可选的备用zones组成,相当于是选择了一个退路,所以叫fallback。
  • node_size是指这个node含有多少个page frames,node_mem_map指向node中所有struct page构成的mem_map数组。
  • node_id是这个node的逻辑ID,也就是在NUMA系统中的编号。现在Linux中的内存分配函数区分不同的node,靠的就是这个node_id,类似于文件描述符fd。
  • node_start_paddr(在2.6内核中被换成了node_start_pfn)是该node的起始物理地址。node_next指向由多个node构成的NUMA单向链表pgdat_list中的下一个节点。如果是UMA系统,只有一个node,则node_start_pfn为0,node_next为NULL。

四,内存的获取

内存的获取都是以页为单位来获取的,核心接口有

struct page * alloc_pages(gfp_t gfp_mask, unsigned int order)

该函数分配 (1<<order)个连续的物理页,并返回一个指针,该指针指向第一个页的page结构体,如果分配出错,就返回NULL

如果你需要逻辑地址,你可以使用下面的函数转换:

void * page_address(struct page *page)

如果你无需page结构体,你还可以调用下面的方法:

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)

这个函数和上面的alloc_pages()作用相同,不过它返回所请求的第一个页的逻辑地址,因为页是连续的,所以其他页也会紧随其后

获取填充为0的页:

unsigned long get_zeroed_page(unsigned int gfp_mask)

和__get_free_pages工作方式相同,只不过分配的页面中都被填充为0了

释放页

void __free_pages(struct page *page, unsigned int order);
void free_pages(unsigned long addr, unsigned int order);
void free_page(unsigned long addr);

上面的函数都可以释放页,释放页的时候你只能释放属于自己的页,如果传递了错误的struct page或地址,用了错误的order值,可能会导致系统崩溃

kmalloc

当你要以页为单位分配内存时,上面的函数很有用,但是你要以字节为单位来分配内存的话,使用kmalloc()是很有用的。它声明在<linux/slab.h>中

void * kmalloc(size_t size, gfp_t flag)

该函数返回一个指向内存块的指针,其内存块至少要有size大小。所分配的内存区在物理上是连续的,在出错的时候返回NULL。

回收内存:

kmalloc对应的回收内存的函数是下面的函数:

void kfree(const void *ptr)

kfree()函数释放由kmalloc()分配出来的内存块,如果想要释放的不是由kmalloc()分配的,或者想要释放的内存早就被释放了,调用这个函数会导致严重的后果

vmallloc

vmalloc()函数的工作方式类似于kmalloc(),只不过前者分配的内存虚拟地址是连续的,而物理地址无需连续。

vmalloc通过分配非连续的物理内存块,再“修正”页表,把内存映射到逻辑地址的连续区域中

vmalloc()函数声明在<linux/vmalloc.h>中,定义在<mm/vmalloc.c>中

void * vmalloc(unsigned long size)

该函数返回一个指针,指向逻辑上连续的一块内存区,大小至少是size,发生错误的时候返回NULL,该函数可能休眠,因此不可在中断上下文中进行调用

释放内存:

void vfree(const vouid* addr)

五,slab

Linux内核中的小内存分配器主要有三种,SLAB/SLUB/SLOB,slub分配器是slab分配器的进化版,而slob是一种精简的小内存分配算法,主要用于嵌入式系统。

有时候用slab来统称slab, slub和slob。slab, slub和slob仅仅是分配内存策略不同,所以下面可能会使用slub和slab混用表示同一个意思

在Linux中,伙伴系统(buddy system)是以页为单位管理和分配内存。但是现实的需求却以字节为单位,假如我们需要申请20Bytes,总不能分配一页吧!那岂不是严重浪费内存。那么该如何分配呢?slab分配器就应运而生了,专为小内存分配而生。slab分配器分配内存以Byte为单位。但是slab分配器并没有脱离伙伴系统,而是基于伙伴系统分配的大内存进一步细分成小内存分配。

kmem_cache

如果从伙伴系统分配一个页用于slub分配器管理,对于slub分配器来说,需要将这段连续内存平均分成若干大小相等的object(对象)进行管理,因此需要一个数据结构来管理相关信息(如,每个object的size,和管理的内存页数),这个结构就是kmem_cache

    struct kmem_cache {
        struct kmem_cache_cpu __percpu *cpu_slab;
        /* Used for retriving partial slabs etc */
        slab_flags_t flags;
        unsigned long min_partial;
        int size;             /* The size of an object including meta data */
        int object_size;     /* The size of an object without meta data */
        int offset;           /* Free pointer offset. */
    #ifdef CONFIG_SLUB_CPU_PARTIAL
        int cpu_partial;      /* Number of per cpu partial objects to keep around */
    #endif
        struct kmem_cache_order_objects oo;
        /* Allocation and freeing of slabs */
        struct kmem_cache_order_objects max;
        struct kmem_cache_order_objects min;
        gfp_t allocflags;    /* gfp flags to use on each alloc */
        int refcount;         /* Refcount for slab cache destroy */
        void (*ctor)(void *);
        int inuse;            /* Offset to metadata */
        int align;            /* Alignment */
        int reserved;         /* Reserved bytes at the end of slabs */
        const char *name;    /* Name (only for display!) */
        struct list_head list;  /* List of slab caches */
        struct kmem_cache_node *node[MAX_NUMNODES];
    };
  • cpu_slab :一个 per cpu 变量,对于每个 cpu 来说,相当于一个本地内存缓存池。当分配内存的时候优先从本地 cpu 分配内存以保证 cache 的命中率。
  • flags:object分配掩码,例如经常使用的SLAB_HWCACHE_ALIGN标志位,代表创建的kmem_cache管理的object按照硬件cache 对齐,一切都是为了速度。
  • min_partial:限制struct kmem_cache_node中的partial链表slab的数量。虽说是mini_partial,但是代码的本意告诉我这个变量是kmem_cache_node中partial链表最大slab数量,如果大于这个mini_partial的值,那么多余的slab就会被释放。
  • size :分配的 object size
  • object_size :实际的 object size ,就是创建 kmem_cache 时候传递进来的参数。和 size 的关系就是, size 是各种地址对齐之后的大小。因此, size 要大于等于 object_size
  • offset:slub分配在管理object的时候采用的方法是:既然每个object在没有分配之前不在乎每个object中存储的内容,那么完全可以在每个object中存储下一个object内存首地址,就形成了一个单链表。很巧妙的设计。那么这个地址数据存储在object什么位置呢?offset就是存储下个object地址数据相对于这个object首地址的偏移。
  •   ****cpu_partial per cpu partial 中所有 slab free object 的数量的最大值,超过这个值就会将所有的 slab 转移到 kmem_cache_node partial 链表。
  • oo :低 16 位代表一个 slab 中所有 object 的数量( oo & ((1 << 16) - 1) ),高 16 位代表一个 slab 管理的 page 数量( (2^(oo  16)) pages )。
  • max:看了代码好像就是等于oo。
  • min:当按照oo大小分配内存的时候出现内存不足就会考虑min大小方式分配。min只需要可以容纳一个object即可。
  • allocflags:从伙伴系统分配内存掩码。
  • inuse:object_size按照word对齐之后的大小。
  • align:字节对齐大小。
  • name:sysfs文件系统显示使用。
  • list:系统有一个slab_caches链表,所有的slab都会挂入此链表。
  • node slab 节点。在 NUMA 系统中,每个 node 都有一个 struct kmem_cache_node 数据结构。

kmem_cache_cpu

struct kmem_cache_cpu是对本地内存缓存池的描述,每一个cpu对应一个结构体。其数据结构如下: 

    struct kmem_cache_cpu { 
        void **freelist;    /* Pointer to next available object */
        unsigned long tid;  /* Globally unique transaction id */
        struct page *page;  /* The slab from which we are allocating */
    #ifdef CONFIG_SLUB_CPU_PARTIAL
        struct page *partial;   /* Partially allocated frozen slabs */
    #endif
    }; 
  • freelist:指向下一个可用的object。
  • tid:一个神奇的数字,主要用来同步作用的。
  • page:slab内存的page指针。
  • partial:本地slab partial链表。主要是一些部分使用object的slab。

kmem_cache_node

kmem_cache_node结构体是一个全局共享的缓存池,下面是slub分配器的结构,相对于slab分配器来说更为简洁

    struct kmem_cache_node {
        spinlock_t list_lock;
        unsigned long nr_partial;
        struct list_head partial;
    };
  • list_lock:自旋锁,保护数据。
  • nr_partial:slab节点中slab的数量。
  • partial:slab节点的slab partial链表,和struct kmem_cache_cpu的partial链表功能类似。

slab的该结构与slub的区别在于:lab分配器中每个node结点有三个链表,分别是空闲slab链表,部分空slab链表,已满slab链表,这三个链表中维护着对应的slab缓冲区

而slub分配器把node结点的这三个链表精简为了一个链表,只保留了部分空slab链表


slab

下面是slab的描述符,使用一个slab结构体来表示

struct slab{
	struct list_haad	list;	//空、部分满或空链表
    unsigned long	colouroff;	//slab着色的偏移量
    void	*s_mem;	//在slab中的第一个对象
    unsigned int	inuse;	//slab中已分配的对象数
    kmem_bufctl_t	free;	//第一个空闲对象(如果有的话)
}

slab描述符要么在sab之外另行分配,要么就放在slab自身开始的地方。如果slab很小,或者slab内部有足够的空间容纳 slab描述符,那么描述符就存放在slab里面。

slub相关接口

    struct kmem_cache *kmem_cache_create(const char *name,
            size_t size,
            size_t align,
            unsigned long flags,
            void (*ctor)(void *));
    void kmem_cache_destroy(struct kmem_cache *);
    void *kmem_cache_alloc(struct kmem_cache *cachep, int flags);
    void kmem_cache_free(struct kmem_cache *cachep, void *objp);
  • kmem_cache_create是创建kmem_cache数据结构,参数描述如下:
    • name:kmem_cache的名称
    • size :slab管理对象的大小
    • align:slab分配器分配内存的对齐字节数(以align字节对齐)
    • flags:分配内存掩码
    • ctor :分配对象的构造回调函数
  • kmem_cache_destroy作用和kmem_cache_create相反,就是销毁创建的kmem_cache。
  • kmem_cache_alloc是从cachep参数指定的kmem_cache管理的内存缓存池中分配一个对象,其中flags是分配掩码,GFP_KERNEL是不是很熟悉的掩码?
  • kmem_cache_free是kmem_cache_alloc的反操作

slub的整体结构

申请

  • 初始化完成后,slub中并没有一个slab缓冲区,只有在第一次申请时,才会从伙伴系统中获取一段连续页框作为一个slab缓冲区
  • 当cpu当前使用的slub已经使用完了后就会向cpu的部分空的slub申请,如果cpu部分空的也使用完了,那么就向node节点申请并将它们放入CPU的部分空slab链表中。

释放

  • 如果释放是obj是属于正在使用的cpu上的slab,那么直接释放即可
  • 如果不是,要判断是否是full状态
    • 是full状态:释放后成为部分空状态(partial empty),会从游离状态转到cpu的partial链表上,如果cpu的partial链表管理的所有slab的free object 数量超过kmem_cache的cpu_partial 成员的话,就需要将per cpu partial链表管理的所有slab移动到per node partial 链表管理
    • 不是full状态,需要判断释放后是不是empty状态
      • 如果是:在满足kmem_cache_node的nr_partial大于kmem_cache的min_partial的情况下,则会释放该slab的内存
      • 其他情况:直接释放

下面是释放之后为空,且满足kmem_cache_node的nr_partial大于kmem_cache的min_partial的情况:

如果不满足kmem_cache_node的nr_partial大于kmem_cache的min_partial的情况,node列表会继续保留空的slab

下面是如果是full状态的slab被释放了一个object且,满足cpu的partial链表管理的所有slab的free object 数量超过kmem_cache的cpu_partial 成员的情况,需要将per cpu partial链表管理的所有slab移动到per node partial 链表管理


如果不满足cpu的partial链表管理的所有slab的free object 数量超过kmem_cache的cpu_partial 成员的情况,那将由cpu的partial链表管理

六,高端内存

在linux中,内核是使用3G-4G的线性地址空间(可以参考进程的内存结构,所有进程共享这段空间),也就是说只有1G的地址空间可以来映射物理地址空间

但是如果内存大于1G的情况呢?

内核引入一个高端内存的概念,把1G的线性空间划分成两部分:

  • 小于896M的物理地址称之为低端内存,这一部分物理内存是和3G开始的地址空间一一对应映射的
  • 剩下的128M的线性空间用来映射剩下的大于896M的物理地址空间,这也就是我们通常说的高端内存区,所谓的建立高端内存的映射就是能用一个线性地址来访问高端内存的页

对于低端内存,我们可以使用虚拟地址直接进行访问,因为这些映射在系统初始化后就已经存在这样的映射

而高端内存还不存在这样一个映射(页目录项,页表都是空的),所以我们必须要在系统初始化完后,提供一系列的函数来实现这个功能,这就是所谓的高端内存的映射

为什么需要高端内存?

对于早期的计算机,只有32位总线,所以最大寻找范围只能是4G,同时由于计划内核调用很频繁,所以采用了内核1G,应用程序使用3G的虚拟地址的设计,从而带来了问题,如果内核空间大于1G,那么要如何访问呢?所以需要高端内存来动态的建立映射来访问超过的内存空间

高端内存只是内核的问题,不涉及普通应用程序

对于64位的内核,一般不需要高端内存,因为64位总线足够使用了


七,虚拟内存

虚拟内存是计算机中重要的概念,在物理内存和进程之间添加一层虚拟内存,大大方便了内存的管理

虚拟内存的作用:

  • 虚拟内存可以利用内存起到缓存的作用,提高进程访问磁盘的速度;
  • 虚拟内存可以为进程提供独立的内存空间,简化程序的链接、加载过程并通过动态库共享内存;
  • 虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统的安全性;

概念:

  • 虚拟地址(Virtual Address,VA):虚拟内存中的地址
  • 物理地址(Physical Address,PA):物理内存中的地址
  • 页表(page table,PTE):记录虚拟地址到物理地址映射信息和其他信息(如是不是脏页,是不是在内存中的页)的记录项
  • MMU:CPU芯片上叫内存管理单元(Memory Management Unit),负责地址翻译的专用硬件

每次程序通过虚拟地址想要访问物理内存中的信息的时候,都会通过MMU进行地址翻译,翻译成物理地址再进行访问

虚拟内存给每个进程都提供了相同的一样大的虚拟内存空间,但是物理内存本身可能没有那么大,那么就需要使用磁盘来存储更多的数据了

所以想要再页表上记录该页的地址是不是在物理内存上的,如果不是,就想需要进行缺页中断,从磁盘中加载数据到物理内存中来

局部性原理

  • 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
    • 程序循环、堆栈等是产生时间局部性的原因。
  • 空间局部性(Spatial Locality):在最近的将来将用到的信息很可能与正在使用的信息在空间地址上是临近的。、
  • 顺序局部性(Order Locality):在典型程序中,除转移类指令外,大部分指令是顺序进行的。顺序执行和非顺序执行的比例大致是5:1。此外,对大型数组访问也是顺序的。
    • 指令的顺序执行、数组的连续存放等是产生顺序局部性的原因。

页面置换算法

如果程序访问到不在物理内存中的数据时,需要产生缺页中断从磁盘中加载到内存,内存中也需要选择一个牺牲页来置换出去,如何选择这个牺牲页对程序的性能影响很大,所以页面置换算法可以说是选择牺牲页的算法

常见的页面置换算法有:

最佳置换算法(OPT)

  • 实现原理:每次选择未来长时间不被访问的或者以后永不使用的页面进行淘汰。
  • 缺点:最佳置换算法是一种理想化算法,具有较好的性能,但是实际上无法实现(无法预知一个进程中的若干页面哪一个最长时间不被访问);
  • 优点:最佳置换算法可以保证获得最低的缺页率

先进先出置换算法(FIFO)

  • 实现原理:淘汰最先进入内存的页面,即选择在页面待的时间最长的页面淘汰。
  • 优点:先进先出算法实现简单,是最直观的一个算法
  • 缺点:先进先出的性能最差,因为与通常页面的使用规则不符合,所以实际应用少

最近最少使用置换算法(LRU)

  • 实现原理:选择最近且最久未被使用的页面进行淘汰
  • 优点:由于考虑程序访问的时间局部性,一般能有较好的性能;实际应用多
  • 缺点:实现需要较多的硬件支持,会增加硬件成本

Linux使用的是改进过后的LRU算法,被称为双链策略

Linux维护了两个LRU链表:活跃链表和非活跃链表。处于活跃链表的页面被认为是不会被换出的,而在非活跃的链表上的页面是可以被换出的,在活跃链表中的页面必须在其被访问时就处于非活跃链表中。

两个链表如同队列般,只能从头部移除,从尾部加入,并且如果活跃链表变得过多而超过非活跃链表,那么会移除活跃链表中的头页面加入到非活页链表

快表(TLB)

在每次CPU产生一个虚拟地址的时候,MMU都要查询一个PTE,以便将虚拟地址翻译为物理地址,而PTE存储在主存中,速度还是比较慢的,所有我们可以在L1高速缓存中存放一个TLB(Translation Lookaside Buffer)翻译后备缓冲器(可以理解为一小部分的页表项)

  • 第1步:CPU产生一个虚拟地址
  • 第2步和第3步:MMU从TLB中取出相应的PTE。
  • 第4步:MMU将这个虚拟地址翻译成一个物理地址,并且将它发送到高速缓存/主存。
  • 第5步:高速缓存/主存将所请求的数据字返回给CPU。

当TLB不命中时,MMU必须从L1缓存中取出相应的PTE。新取出的PTE存放在TLB中,可能会覆盖一个已经存在的条目。

多级页表

对于32位的计算机来说,如果虚拟内存空间为4GB,每一页的大小为4KB,那么就会有1M的页表项,就算每个页表项占用内存在少,这对内存来说也是比较大的负担(页表项需要在内存中),所以我们需要使用多级页表的形式,按照需要加载对于的页表就可以了。

对于64位系统,Linux系统使用了三级页表结构:页目录、中间页目录和页表。PGD为顶级页表,是一个pgd_t数据类型(定义在文件linux/include/page.h中)的数组,每个数组元素指向一个中间页目录;PMD为二级页表,是一个pmd_t数据结构的数组,每个数组元素指向一个页表;PTE则是页表,是一个pte_t数据类型的数组,每个元素中含有物理地址。

也有4级页表的设计,不过不常见(目前不需要那么大的内存空间)

伙伴算法

外部碎片

当需要分配大块内存的时候,要用好几页组合起来才够,而系统分配物理内存页的时候会尽量分配连续的内存页面,频繁的分配与回收物理页导致大量的小块内存夹杂在已分配页面中间,形成外部碎片,举个例子:

内部碎片

物理内存是按页来分配的,这样当实际只需要很小内存的时候,也会分配至少是 4K 大小的页面,而内核中有很多需要以字节为单位分配内存的场景,这样本来只想要几个字节而已却不得不分配一页内存,除去用掉的字节剩下的就形成了内部碎片。

伙伴算法

Linux 内核引入了伙伴系统算法(Buddy system),什么意思呢?就是把相同大小的页框块用链表串起来,页框块就像手拉手的好伙伴,也是这个算法名字的由来。

伙伴算法会给程序分配一个较大的内存空间,即保证所有大块内存都能得到满足。很明显分配比需求还大的内存空间,会产生内部碎片。所以伙伴算法虽然能够完全避免外部碎片的产生,但这恰恰是以产生内部碎片为代价的。

伙伴算法将空闲的页分组为11块,每一块链表分别包含大小为1,2,4,8,16,32,64,128,256,512 和 1024 个连续的页框。

内存分配

就是在分配内存时,首先从空闲的内存中搜索比申请的内存大的最小的内存块。如果这样的内存块存在,则将这块内存标记为“已用”,同时将该内存分配给应用程序。

如果这样的内存不存在,则操作系统将寻找更大块的空闲内存,然后将这块内存平分成两部分(因为所有块大小都是2的倍数),一部分返回给程序使用,另一部分作为空闲的内存块等待下一次被分配。

内存释放

在释放内存后,内核会试图找到大小一样大的另一个空闲块合并成更大的一块

如:

释放了大小为 b 的一对空闲伙伴块合并为一个大小为 2b 的单独块。满足以下条件的两个块称为伙伴:

  • 两个快具有相同的大小,记作 b
  • 它们的物理地址是连续的
  • 第一块的第一个页框的物理地址是的倍数

该算法是迭代的,如果它成功合并所释放的块,它会试图合并 2b 的块,以再次试图形成更大的块。

另外slab技术是为了解决内部碎片的问题

参考资料:

知乎:Linux中的物理内存管理 [一]

知乎:Linux中的物理内存管理 [二]

图解slub

关于高端内存的权威解释

Linux的虚拟内存详解(MMU、页表结构)

10 张图解再谈 Linux 物理内存和虚拟内存

【Linux 内核】内存管理(二)伙伴算法