linux-内存管理

内存管理架构

内存管理子系统架构可以分为:用户空间、内核空间及硬件部分3个层面,具体结构如 下图所示:

1、用户空间:应用程序使用malloc()申请内存资源/free()释放内存资源。

2、内核空间:内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留, 不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

3、硬件:处理器包含一个内存管理单元(Memory Management Uint,MMU)的部 件,负责把虚拟地址转换为物理地址。

malloc实现中与sbrk函数的关系 - 知乎

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
constexpr int MAX = 1024;

int main(int argc, char const *argv[])
{
    /*
    sbrk函数在内核的管理下将虚拟地址空间映射到内存,供malloc函数使用。
    */
    void *p = sbrk(0);
    void *old = p;

    p = (int *)sbrk(MAX * MAX);
    if (p == (void *)(-1))
    {
        std::cout << "sbrk error\n";
        exit(0);
    }
    printf("old:%p\tp=%p\n", p, old);

    void *new_ = sbrk(0);
    printf("new:%p\n", new_);

    printf("pid= %d\n", getpid());

    while (true)
    {
    }

    sbrk(-MAX * MAX);

    return 0;
}

linux内存管理(十九)brk和sbrk介绍(番外篇) - 墨天轮

其中:sbrk()增加程序的heap increment字节,返回增加前的heap的program break

在上面的代码中,申请了2^20次方字节,换算成16进制 :0x100000,

因此之前是:p=0x5654af388000  

之后是:    new:0x5654af488000

 堆顶变成了0x5654af488000

虚拟地址空间布局架构

        因为目前应用程序没有那么大的内存需求,所以ARM64处理器不支持完全的64位虚拟地址。

        在ARM64架构的Linux内核中,内核虚拟地址和用户虚拟地址的宽度相同。

        目前Linux64位操作系统的虚拟地址空间采用48位虚拟地址,内核虚拟地址和用户虚拟地址的宽度相同,因此用户地址空间是0x0->0x 0000 ffff ffff ffff,一共12个f,换算成二进制即48位;同理有内核地址空间

    所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程
组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。

用户虚拟地址空间划分 

        进程的用户虚拟空间的起始地址是0,长度是TASK_SIZE,由每种处理器架构定义自己 的宏TASK_SIZE。ARM64架构定义的宏TASK_SIZE如下:

        32位用户空间程序:TASK_SIZE的值是TASK_SIZE_32,即0x100000000,等4GB。 64位用户空间程序:TASK_SIZE的值是TASK_SIZE_64,即2的VA_BITS次方字节。

        VA_BITS即虚拟地址空间的位数,一般是48位:cat /proc/cpuinfo可以查看

  

 C++内存布局_~怎么回事啊~的博客-CSDN博客

 Linux内核使用内存描述符mm_struct,描述进程的用户虚拟地址空间,内核源码分析如下图所示:

 

 

 

 内核地址空间布局

        ARM64处理器架构内核地址空 间布局如图所示:

内存映射

内存映射原理

        内存映射即在进程的虚拟地址空间中创建一个映射,分为两种:

        (1)文件映射:文件支持的内存映射,把文件的一个区间映射到进程的虚拟地址空间, 数据源是存储设备上的文件。

        (2)匿名映射:没有文件支持的内存映射,把物理内存映射到进程的虚拟地址空间, 没有数据源。

        

【内存映射的原理】

        创建内存映射时,在进程的用户虚拟地址空间中分配一个虚拟内存区域。内核采用延
迟分配物理内存的策略,在进程第一次访问虚拟页的时候,产生缺页异常。如果是文件映射,
那么分配物理页,把文件指定区间的数据读到物理页中,然后在页表中把虚拟页映射到物理页。
如果是匿名映射,就分配物理页,然后在页表中把虚拟页映射到物理页。

数据结构

        虚拟内存区域分配给进程的一个虚拟地址范围,内核使用结构体vm_area_struct描述虚拟内存区域,主要核心成员如下:

系统调用 

内存管理子系统提供如下常用的系统调用函数:

1、mmap()----创建内存映射

#include <sys/mman.h>
void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);

        系统调用mmap():进程创建匿名的内存映射,把内存的物理页映射到进程的虚拟地址空间。进程把文件映射到进程的虚拟地址空间,可以像访问内存一样访问文件,不需要调用系 统调用read()/write()访问文件,从而避免用户模式和内核模式之间的切换,提高读写文件速度。 两个进程针对同一个文件创建共享的内存映射,实现共享内存。

linux库函数mmap()原理_skybabybzh的博客-CSDN博客_linux mmap

2、munmap()----删除内存映射

#include <sys/mman.h>
int munmap(void *addr, size_t len);

3、mprotect()----设置虚拟内存区域的访问权限

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);

测试代码:

#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

typedef struct 
{
    /* data */
    char name[4];
    int age;
}people;


void main(int argc,char**argv)
{
    int fd,i;
    people *p_map;
    char temp;
    fd=open(argv[1],O_CREAT|O_RDWR|O_TRUNC,00777);

    lseek(fd,sizeof(people)*5-1,SEEK_SET);
    write(fd,"",1);

    p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p_map==(void*)-1)
    {
        fprintf(stderr,"mmap : %s \n",strerror(errno));
        return ;
    }
    close(fd);

    temp='A';
    for(i=0;i<10;i++)
    {
        temp=temp+1;
        (*(p_map+i)).name[1]='\0';
        memcpy((*(p_map+i)).name,&temp,1);
        (*(p_map+i)).age=30+i;
    }

    printf("Initialize.\n");

    sleep(15);

    munmap(p_map,sizeof(people)*10);

    printf("UMA OK.\n");

}

















#include <sys/mman.h>
#include <sys/types.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>

typedef struct 
{
    /* data */
    char name[4];
    int age;
}people;

void main(int argc,char**argv)
{
    int fd,i;
    people *p_map;

    fd=open(argv[1],O_CREAT|O_RDWR,00777);
    p_map=(people*)mmap(NULL,sizeof(people)*10,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(p_map==(void*)-1)
    {
        fprintf(stderr,"mmap : %s \n",strerror(errno));
        return ;
    }

    for(i=0;i<10;i++)
    {
        printf("name:%s age:%d\n",(*(p_map+i)).name,(*(p_map+i)).age);
    }

    munmap(p_map,sizeof(people)*10);   

}


系统调用mmap

        系统调用mmap用来创建内存映射,把创建内存映射主要工作委托给do_mmap函数, 内核源码文件处理:mm/mmap.c

系统调用munmap 

        系统调用munmap用来删除 内存映射,它有两个参数:起始地址 和长度即可。它的主要工作委托给内 核源码文件处理“mm/mmap.c”当 中的函数do_munmap。

物理内存组织结构 

体系结构

    目前多处理器系统有两种体系结构:

1)非一致内存访问(Non-Unit Memory Access,NUMA):指内存被划分成多个内存节点的多处理器系统。访问一个内存节点花费的时间取决于处理器和内存节点的距离。

2)对称多处理器(Symmetric Multi-Processor,SMP):即一致内存访问 (Uniform Memory Access,UMA),所有处理器访问内存花费的时间是相同。

内存模型

内存模型是从处理器角度看到的物理内存分布,内核管理不同内存模型的方式存差异。 内存管理子系统支持3种内存模型:

1)平坦内存(Flat Memory):内存的物理地址空间是连续的,没有空洞。

2)不连续内存(Discontiguous Memory):内存的物理地址空间存在空洞,这种模型可以高效地处理空洞。

3)稀疏内存(Space Memory):内存的物理地址空间存在空洞,如果要支持内存热插拔,只能选择稀疏内存模型。

三级结构

内存管理子系统使用节点(node),区域(zone)、页(page)三级结构描述物理内存。

a、内存节点------>分为两种情况:

(1)NUMA体系的内存节点,根据处理器和内存的距离划分;

(2)在具有不连续内存的NUMA系统中,表示比区域的级别更高的内存区域,根据物 理地址是否连续划分,每块物理地址连续的内存是一个内存节点。

pglist_data结构体内核源码:include/linux/mmzone.h

b、内存区域

内存节点被划分为内存区域。Linux内核源码分析:include/linux/mmzone.h

c、物理页

每个物理页对应一个page结构体,称为页描述符,内存节点的pglist_data实例的成员 node_mem_map指向该内存节点包含的所有物理页的页描述符组成的数组。Linux内核源码分 析:include/linux/mm_types.h

 

引导内存分配原理

bootmem分配器 

 在内核初始化的过程中需要分配内存,内核提供临时的引导内存分配器,在页分配器
和块分配器初始化完成之后,把空闲的物理页交给页分配器管理,丢弃引导内存分配器。

bootmem分配器定义的数据结构,内核源码如下:

memblock分配器

        Linux内核使用伙伴系统管理内存,那么在伙伴系统之前,内核使通过memblock来管理。在系统启动阶段,使用memblock记录物理内存的使用情况,首先我们知道在内核启动后,对于内存,分成好几块内存中的某些部分使永久分配给内核的,例如代码段和数据段,ramdisk和dtb占用的空间等,是系统内存的一部分,不能被侵占,也不参与内存的分配,称之为静态内存GPU/camera/多核共享的内存都需要预留大量连续内存,这部分内存平时不使用,但是必须为各个应用场景预留,这样的内存称之为预留内存;内存其余的部分,是需要内核管理的内存,称之为动态内存
        那么memblock就是将以上内存按功能划分为若干内存区,使用不同的类型存放在memory和reserved的两个集合中,memory即为动态内存,而resvered包括静态内存等。

        memblock介绍
        memblock的算法实现是,它将所有的状态都保持在一个全局变量__initdata_memblock中,算法的初始化以及内存的申请释放都是在将内存块的状态做变更。那么从数据结构入手,__initdata_memblock是一个memblock结构体,其定义如下:

 

伙伴分配器

         当系统内核初始化完毕后,使用页分配器管理物理页,使用的页分配器是伙伴分配器,伙伴分配器的特点是算法简单且高效。

        连续的物理页称为页块(page block)。阶(order)是伙伴分配器的一个专业术语, 是页的数量单位,2^n个连续页称为n阶页块。

        满足以下条件 的两个n阶页块称为伙伴(buddy -->英 [ˈbʌdi]):

1、两个页块是相邻的,即物理地址是连续的;

2、页块的第一页的物理页号必须是2^n的整数倍;

3、如果合并成(n+1)阶页块,第一页的物理页号必须是2^(n+1)的整数倍。

        伙伴分配器分配和释放物理页的数量单位为阶。分配n阶页块的过程如下:

1、查看是否有空闲的n阶页块,如果有直接分配;否则,继续执行下一步;

2、查看是否存在空闲的(n+1)阶页块,如果有,把(n+1)阶页块分裂为两个n阶 页块,一个插入空闲n阶页块链表,另一个分配出去;否则继续执行下一步。

3、查看是否存在空闲的(n+2)阶页块,如果有把(n+2)阶页块分裂为两个(n+1) 阶页块,一个插入空闲(n+1)阶页块链表,另一个分裂为两个n阶页块,一个插入空间(n阶页 块链表,另一个分配出去;如果没有,继续查看更高阶是否存在空闲页块。

Linux 伙伴算法简介 - 浩天之家 - 博客园

分区的伙伴分配器

        1、数据结构

        分区的伙伴分配器专注于某个内存节点的某个区域。内存区域的结构体成员free_area 用来维护空闲页块,数组下标对应页块的阶数。 内核源码结构:

2、根据分配标志获取首选区域类型

申请页时,最低的4个标志位用来指定首选的内存区域类型,内核源码如下:

3、备用区域列表

    如果首选的内存节点或区域不能满足分配请求,可以从备用的内存区域借用物理页。
借用必须遵守相应的规则。

内存节点的pg_data_t实例已定义备用区域列表,内核源码如下:

4、区域水线

首选的内存区域什么情况下从备用区域借用物理页呢?每个内存区域有3个水线 a.高水线(high):如果内存区域的空闲页数大于高水线,说明内存区域的内存充足;

b.低水线(low):如果内存区域的空闲页数小于低水线,说明内存区域的内存轻微不足; c.最低水线(min):如果内存区域的空闲页数小于最低水线,说明内存区域的内存严重不足。

 分配页

在Linux内核中,所有分配页的函数最终都会调用到__alloc_pages_nodemask,此函 数被称为分区的伙伴分配器的心脏。函数原型如下:

算法流程:

1、根据分配标志位得到首选区域类型和迁移类型; 2、执行快速路径,使用低水线尝试第一次分配; 3、如果快速路径分配失败,才执行慢速路径。

快速路径调用函数如下:

慢速路径调用函数如下:

释放页 

页分配器提供释放页的接口:

void __free_pages(struct page *page, unsigned int order),第一个参数是第一个 物理页的page实例的地址,第二个参数是阶数。

SLAB分配器

todo



Linux内存管理之SLAB分配器_ibless的博客-CSDN博客_slab 分配器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值