一、内存寻址
1.1 逻辑地址、线性地址、物理地址的概念
1.2 逻辑地址转换线性地址步骤
1.3 线性地址到物理地址的转换
二、内存管理
2.1 引导内存分配器阶段
2.2 内存管理子系统
2.3 32位架构的地址空间划分
2.4 64位架构的地址空间划分
2.5 内核态的内存管理
2.6 用户态内存管理
2.7 一些内存的特殊用法
一、 内存寻址
本节介绍linux内核如何进行芯片级的内存寻址;
当处理器运行在实模式时,处理器执行指令直接使用物理地址;
本节介绍在保护模式下,处理器运行linux内核时,是如何对内存进行寻址的;在保护模式下,处理器执行指令时首先看到的是逻辑地址,逻辑地址需要先转换成线性地址,线性地址再转换成物理地址,才能对内存进行访问;这个过程除了需要操作系统的处理,还需要借助硬件单元,比如逻辑地址到线性地址的转换,需要linux内核创建一个段描述符表,分段单元借助这个段描述符表把逻辑地址转换成线性地址;对于线性地址到物理地址的转换,软件上需要创建页表,硬件上需要MMU硬件单元,然后在访问内存的过程中,为了提高程序的访存效率,硬件上还引入了高速缓存和TLB缓存,本节介绍在这样的硬件结构下,运行linux内核时,处理器是如何进行内存访问的;
1.1 逻辑地址、线性地址、物理地址的概念
逻辑地址,机器语言指令中使用的地址,也就是可执行文件中一条指令或者一个操作数的地址,cpu在取指令、取操作数时首先看到的就是逻辑地址;可执行文件被分为一个一个段,比如代码段、数据段、栈段、任务状态段、局部线程存储段等,因此一个逻辑地址就由一个16位的段标识符和32位偏移量组成;
线性地址,操作系统使用的地址,在代码层面看到的指令地址和内存地址,就是线性地址;
物理地址,最终访问存储单元,从地址总线发出去的就是物理地址;
1.2 逻辑地址转换线性地址步骤
逻辑地址由一个16位的段标识符和32位偏移量组成,如下图所示,首先硬件把16位段选择符装入段寄存器(处理器为每个段都提供了段寄存器,cs、ss、ds、es、fs、gs),硬件通过段选择符找到对应的段描述符,这个段描述符保存在全局描述符表GDT或者局部描述符表LDT里面(描述符表是内核创建的,保存在内存的cpu_gdt_table数组里面,每个cpu有一个GDT,GDT的物理首地址放在gdtr控制寄存器中,当前正被使用的LDT地址和大小放在ldtr控制寄存器中),段描述符被硬件加载到cpu的非编程寄存器;段描述符里面包含段的首字节对应的线性地址,首字节的线性地址加上32位偏移量就得到这个逻辑地址对应的线性地址;需要说明的是在linux中,各段首对应的线性地址都是0,因此,在linux下逻辑地址总是和线性地址一致;
1.3 线性地址到物理地址的转换
线性地址转换成物理地址的步骤,这个步骤通过查询页表来完成的,操作系统把线性地址划分成一个一个页,然后把物理内存也分成页大小一样的一个一个页框,页的内容可以保存在任意页框里面,页保存在哪个页框就记录在页表里面;页表由操作系统创建,每个进程都有一个页表,这个页表保存在内存里面,页表的首地址会保存在处理器的寄存器中,x86是保存在cr3寄存器,arm架构是保存在cp15协处理器的寄存器里面;内核提供了一组宏用于操作页表项,如pdg_index(addr)、pgd_offset(mm, addr)、pgd_page(pgd)等;
由于线性地址庞大,为了减小页表的大小,操作系统一般会把页表组织成页目录的结构,比如linux的4级目录结构包括页全局目录、页上级目录、页中间目录、页表,对应的线性地址也会被分为4段,页全局目录索引、页上级目录索引、页中间目录索引、页表索引,查找过程大致是,首先从cpu的寄存器取出页全局目录的首地址,然后通过页全局目录的索引找到对应的页全局目录表项,这个表项里面里面保存了页上级目录的地址,找到页上级目录后,通过页上级目录索引,找到页上级目录中对应的表项,这个表项保存了页中间目录的地址,找到页中间目录后,通过页中间目录索引,找到页中间目录中对应的表项,这个表项保存了页表的地址,找到页表地址后,通过页表索引找到对应的页表项,这个页表项里面保存了对应页框的物理首地址,这个首地址加上线性地址中的偏移字段,就是线性地址对应的物理地址;需要指出的是,这些表项除了保存下一级的首地址,还保存了这个页对应的属性,包括present标志(页是否在主存中)、read/write标志(页或页表的存取权限)、user/supervisor标志(访问该页或页表需要的特权级)、PCD和PWT标志(控制硬件高速缓存处理页或页表的方式);
由于页表保存在内存,每次查表都访问内存效率低,因此硬件上引入了页表的高速缓存TLB,类似于内存的高速缓存,把最近使用的页表缓存到TLB,处理器访问TLB的开销比访问内存小;
二、 内存管理
2.1 引导内存分配器阶段
在内核的内存管理子系统还没初始化之前,分配内存的工作由引导内存分配器完成,早期使用的引导分配器是bootmem分配器,目前大部分使用的是memblock分配器;
memblock分配器维护了一个数据结构,这个数据结构描述了哪些物理内存已经被使用,哪些物理内存可以被内核使用,数据结构如下,其中memory成员描述了内核可以访问的物理内存,reserved成员描述的是已经被使用的物理内存;
#include /linux/memblock.h struct memblock {bool bottom_up; /* is bottom up direction? */phys_addr_t current_limit;struct memblock_type memory;struct memblock_type reserved; #ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAPstruct memblock_type physmem; #endif };
memblock分配器的初始化函数是arm64_memblock_init,这个函数会解析设备树的内存信息节点/memory,这个节点描述了哪些物理内存可以被内核使用,解析这个节点的信息填充到memory成员中;从设备树读取保留内存的信息保存到reserved成员中,保留内存对应的节点是/memreserve和/reserved-memory;把内存镜像占用的物理内存范围添加到reserved成员中,这段内存可以被内核访问,但是已经被占用,所以memory和reserved两个成员都要添加;
memblock分配器对外提供的接口有memblock_add、memblock_remove、memblock_alloc、memblock_free;
2.2 内存管理子系统
内存管理子系统使用节点、区域和页框三级结构描述物理内存;
节点:在存储系统中,cpu访问不同区域的物理内存开销是不一样的,这种存储系统叫做非一致内存访问NUMA,内存管理子系统将物理内存分为不同的节点,同一个节点cpu访问内存的开销是相同的,节点使用pglist_data结构体描述该节点的内存布局,里面描述了这个节点包含的区域信息,以及这个节点包含的物理页框信息;
区域:计算机体系结构有硬件的制约,有的外设使用DMA只能对RAM的前16M寻址,32位系统里面,内核空间的虚拟地址只有1G大小,如果RAM大于1G,那么内核的1G线性地址空间就无法覆盖所有RAM区域,基于这个考虑,内存管理系统将节点里面的内存分为DMA区域、NORMAL区域、高端内存区域,DMA区域:包含低于16MB的物理页框,NORMAL区域:直接映射到内核虚拟地址空间的区域,虚拟地址和物理地址是线性映射的关系,只差一个偏移,是否使用页表,不同处理器实现不同,MIPS处理器不需要页表,ARM处理器需要使用页表;高端内存区域:32位处理器才有这个区域,这个区域是由于RAM大于1G,导致内核1G的虚拟地址无法覆盖到,64位处理器就没有这个区域,因为内核的虚拟地址空间足够大;描述区域的数据结构是;
页框:内存管理子系统以页框为单位对物理内存进行管理,描述页框的数据结构是page,记录页框的当前状态,比如该页框属于哪个进程的页、是否空闲、属于哪个区域、是否包含内核代码还是内核数据:
分区页框分配器:
内核管理子系统使用页分配器管理物理页,当前使用的页分配器是伙伴分配器;伙伴分配器就是把物理页框分成11个块链表组成,每个链表块大小不一样,链表中块大小是一样的,各链表块大小都是2的幂次方大小1、2、4、8、16,每个块中物理页框都是物理地址连续的;请求和释放页框以操作链表为单位,请求块过程中如果操作的块比请求的页框大,那么会将块进行拆分,拆分后剩余的页框会插入另一个小块链表中,如果拆分的块可以和其他块构成伙伴关系,那么还会进行块的合并操作,释放块时也会进行伙伴块合并已经块迁移链表的动作;请求和释放页框常用的函数有alloc_pages(gfp_mask, order)、alloc_page(gfp_mask)、__get_free_pages(gfp_mask, 0)、__get_free_page(gfp_mask)、__get_zeroed_page(gfp_mask)、__get_dma_pages(gfp_mask, order);
2.3 32位架构的地址空间划分
32位架构可使用的虚拟地址空间是4GB,前0~3GB是用户空间使用,3~4GB是内核空间使用;3GB+896MB这段地址是线性映射区,也就是低端内存区(包括DMA和NORMAL区),其中内核页表swapper_pg_dir和内核可执行文件存在这个区域开始的地方0xc0000000;接着是高端内存区,高端内存区里面有非连续内存映射区vmalloc、高端内存永久映射区、高端内存固定映射区;
2.4 64位架构的地址空间划分
目前应用程序不需要64位那么大的内存需求,arm64处理器不支持完全的64位虚拟地址;实际虚拟地址的最大宽度是48位,用户空间访问是0x0000 0000 0000 0000~0x0000 FFFF FFFF FFFF,内核地址范围是0xFFFF 0000 0000 0000~0xFFFF FFFF FFFF FFFF;
ARM64架构的内核地址空间布局如下图所示,最上面是线性映射区,长度是内核虚拟地址空间的一半,这部分区域虚拟地址和物理地址是线性关系;vmemmap区域是稀疏内存的page结构体数组的虚拟地址空间;PCI I/O区域是PCI设备的I/O地址空间;固定映射区域是编译时的特殊虚拟地址,编译的时候是一个常量,在内核初始化的时候映射到物理地址;vmalloc区域是非线性映射区,内核使用vmalloc分配该区域的内存,内核镜像也在这个区域;内核模块区域是内核模块使用的虚拟地址空间;最后是KASAN内存检测工具使用的区域;
2.5 内核态的内存管理
Linux操作系统的内存管理子系统是运行在内核态的,用户态申请内存最终分配物理页框时也是在内核态完成的;
从请求内存的大小来分,内存管理子系统提供两种内存分配方式,一种是以页框大小倍数为单位申请和释放内存,这种方式基于分区页框分配器(当前用的是伙伴管理系统)实现;另一种是任意字节大小内存的分配,这种方式基于slab分配器实现,slab分配器使用伙伴分配器获得一块连续的物理页框,然后把这个页框进行切割,分成一个个固定大小的内存对象,请求内存时,将大小适合的内存对象分配给请求者;
从请求的物理内存是否连续来区分,直接使用伙伴系统请求的页框是物理地址连续的,使用slab分配器申请的内存也是物理地址连续的;使用vmalloc申请的内存是不保证物理地址连续的;
需要说明的是,内存管理子系统将物理内存分为三个区域:DMA区、NORMAL区以及高端内存区;使用伙伴系统申请连续页框时,可以使用标志指定从哪个区域请求页框;使用slab分配器请求页框也是一样的,可以使用标志指定从哪个区域请求内存;__GFP_DMA、__GFP_HIGHMEM;
slab分配器:核心思想是把申请和释放的内存当作对象来处理,如下图所示slab分配器把整块内存分为一个个小的内存对象,并且采用面向对象的思想,为这些对应提供对应的构造和析构函数;对象类型比如内核专用的对象有进程描述符、内存描述符、文件描述符等,这些都是常用对象,slab分配器预先分配好这些对象,当内存请求这些对象时,就可以直接提供,提高了内存申请效率;还有通用的内存对象,这些对象以2的幂次方大小提供,从普通区域分配页的内存对象名称是kmalloc-<size>,从DMA区域分配页的对象名称是dma-kmalloc-<size>,通过命令cat /proc/slabinfo可以查询这些通用对象;
基于slab分配器衍生出来的还有slub分配器和slob分配器,slab分配器由于数据结构复杂,本身的内存开销大,在物理内存使用量大的时候内存开销大,因此设计了slub分配器;由于slab分配器代码多、实现复杂,因此针对小内存的嵌入式系统设计了精简的slob分配器;
非连续内存区管理:内核态的线性地址空间有一段vmalloc区间,这段区间用于映射非连续物理内存,调用vmalloc接口申请内存时,在这个区间找到一块大小合适的线性地址区间,然后内存管理子系统找到合适的物理页框,创建页表把线性地址和物理页框映射起来;每次调用vmalloc都调用get_vm_area()函数创建一个非连续内存区描述符,然后多次调用alloc_page接口从分区页框分配器申请物理页框,接着调用map_vm_area函数把线性地址和物理页框对应的物理地址映射起来;需要注意的是vmalloc接口优先从高端内存区请求页框,每次请求物理内存都是以页框为单位,也就是不管申请的内存大小是多少,至少都是4096字节大小,因此建议需要申请的内存大于一个页时才使用vmalloc;
2.6 用户态内存管理
进程的用户虚拟地址空间包含以下区域:
1、 代码段、数据段和未初始化数据段;
2、 动态库的代码段、数据段和未初始化数据段;
3、 动态内存:堆;
4、 栈;
5、 存放在栈底部的环境变量和参数字符串;
6、把文件映射到虚拟地址空间的内存映射区;
Linux通过线性区的方式来管理进程的用户态虚拟地址空间;比如堆是一个线性区、栈是一个线性区、代码段是一个线性区、数据段是一个线性区、对一个文件执行内存映射创建线性区、共享内存线性区等;
线性区使用数据结构vm_area_struct表示,线性区数据结构包含在进程的内存描述符里面mm_struct;
线性区映射的物理内存采用延迟分配的原则,在使用时才触发缺页异常处理来分配实际的物理页,比如代码段和数据段的访问,代码段和数据段一般是保存在硬盘这样的存储介质里面,进程通过文件映射的方式把可执行文件的代码段和数据段映射到进程的线性区,当cpu实际访问代码段和数据段的时候,内核才给代码段和数据段映射物理页框,然后把代码段和数据段从硬盘读到物理页框;
2.7 一些内存的特殊用法
待总结~
参考博客:
https://blog.csdn.net/u012489236/article/details/106109251?spm=1001.2014.3001.5501
《ARM体系结构与编程》
《linux内核深度解析》
《深入理解linux内核》
本文仅学习总结以便更好地理解linux的内存管理,还有部分博客没有一一列上,如有侵权请联系删除