《Linux内核深度解析》之内存地址空间

阅码场用户程磊对《linux内核深度解析》推荐如下:
1.语言浅显易懂,内容深入浅出。
2.逻辑清晰,条理分明,逐步深入,层层递进。
3.基于较新的4.12内核版本,很多经典内核书籍虽然写的都非常好,但是都是基于2.6内核,很多在2.6之后引入的新技术并没有讲到,而本书对这些新技术都有非常详细的讲解。
3.1 内存管理概述
内存管理子系统的架构如图 3.1 所示,分为用户空间、内核空间和硬件 3 个层面。
图3.1 内存管理架构
3.1.1.用户空间
应用程序使用 malloc()申请内存,使用 free()释放内存。
malloc()和 free()是 glibc 库的内存分配器 ptmalloc 提供的接口,ptmalloc 使用系统调用brk 或 mmap 向内核以页为单位申请内存,然后划分成小内存块分配给应用程序。
用户空间的内存分配器,除了 glibc 库的 ptmalloc,还有谷歌公司的 tcmalloc 和 freebsd的 jemalloc。
3.1.2.内核空间
(1)内核空间的基本功能。
虚拟内存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk 用来扩大或收缩堆,sys_mmap 用来在内存映射区域分配虚拟页,sys_munmap 用来释放虚拟页。
内核使用延迟分配物理内存的策略,进程第一次访问虚拟页的时候,触发页错误异常,页错误异常处理程序从页分配器申请物理页,在进程的页表中把虚拟页映射到物理页。
页分配器负责分配物理页,当前使用的页分配器是伙伴分配器。
内核空间提供了把页划分成小内存块分配的块分配器,提供分配内存的接口 kmalloc()和释放内存的接口 kfree(),支持 3 种块分配器:slab 分配器、slub 分配器和 slob分配器。
在内核初始化的过程中,页分配器还没准备好,需要使用临时的引导内存分配器分配内存。
(2)内核空间的扩展功能。
不连续页分配器提供了分配内存的接口 vmalloc 和释放内存的接口 vfree,在内存碎片化的时候,申请连续物理页的成功率很低,可以申请不连续的物理页,映射到连续的虚拟页,即虚拟地址连续而物理地址不连续。
每处理器内存分配器用来为每处理器变量分配内存。
连续内存分配器(contiguous memory allocator,cma)用来给驱动程序预留一段连续的内存,当驱动程序不用的时候,可以给进程使用;当驱动程序需要使用的时候,把进程占用的内存通过回收或迁移的方式让出来,给驱动程序使用。
内存控制组用来控制进程占用的内存资源。
当内存碎片化的时候,找不到连续的物理页,内存碎片整理(“memory compaction”的意译,直译为“内存紧缩”)通过迁移的方式得到连续的物理页。
在内存不足的时候,页回收负责回收物理页,对于没有后备存储设备支持的匿名页,把数据换出到交换区,然后释放物理页;对于有后备存储设备支持的文件页,把数据写回存储设备,然后释放物理页。如果页回收失败,使用最后一招:内存耗尽杀手(oom killer,out-of-memory killer),选择进程杀掉。
3.1.3.硬件层面
处理器包含一个称为内存管理单元(memory management unit,mmu)的部件,负责把虚拟地址转换成物理地址。
内存管理单元包含一个称为页表缓存(translation lookaside buffer,tlb)的部件,保存最近使用过的页表映射,避免每次把虚拟地址转换成物理地址都需要查询内存中的页表。
为了解决处理器的执行速度和内存的访问速度不匹配的问题,在处理器和内存之间增加了缓存。缓存通常分为一级缓存和二级缓存,为了支持并行地取指令和取数据,一级缓存分为数据缓存和指令缓存。
3.2 虚拟地址空间布局
3.2.1 虚拟地址空间划分
因为目前应用程序没有那么大的内存需求,所以 arm64 处理器不支持完全的 64 位虚拟地址,实际支持情况如下。
图3.2 arm64内核/用户虚拟地址空间划分
(1)虚拟地址的最大宽度是 48 位,如图 3.2 所示。内核虚拟地址在 64 位地址空间的顶部,高 16 位是全 1,范围是[0xffff 0000 00000000,0xffff ffff ffff ffff];用户虚拟地址在 64 位地址空间的底部,高 16 位是全 0,范围是[0x0000 0000 0000 0000,0x0000 ffff ffff ffff];高 16 位是全 1 或全 0 的地址称为规范的地址,两者之间是不规范的地址,不允许使用。
(2)如果处理器实现了 armv8.2 标准的大虚拟地址(large virtual address,lva)支持,并且页长度是 64kb,那么虚拟地址的最大宽度是 52 位。
(3)可以为虚拟地址配置比最大宽度小的宽度,并且可以为内核虚拟地址和用户虚拟地址配置不同的宽度。转换控制寄存器(translation control register)tcr_el1 的字段 t0sz 定义了必须是全 0 的最高位的数量,字段 t1sz 定义了必须是全 1 的最高位的数量,用户虚拟地址的宽度是(64-tcr_el1.t0sz),内核虚拟地址的宽度是(64-tcr_el1.t1sz)。
在编译 arm64 架构的 linux 内核时,可以选择虚拟地址宽度。
(1)如果选择页长度 4kb,默认的虚拟地址宽度是 39 位。
(2)如果选择页长度 16kb,默认的虚拟地址宽度是 47 位。
(3)如果选择页长度 64kb,默认的虚拟地址宽度是 42 位。
(4)可以选择 48 位虚拟地址。
在 arm64 架构的 linux 内核中,内核虚拟地址和用户虚拟地址的宽度相同。
所有进程共享内核虚拟地址空间,每个进程有独立的用户虚拟地址空间,同一个线程组的用户线程共享用户虚拟地址空间,内核线程没有用户虚拟地址空间。
3.2.2 用户虚拟地址空间布局
进程的用户虚拟地址空间的起始地址是 0,长度是 task_size,由每种处理器架构定义自己的宏 task_size。arm64 架构定义的宏 task_size 如下所示。
(1)32 位用户空间程序:task_size 的值是task_size_32,即0x100000000,等于4gb。
(2)64 位用户空间程序:task_size 的值是 task_size_64,即 2va_bits字节,va_bits是编译内核时选择的虚拟地址位数。
arch/arm64/include/asm/memory.h #define va_bits (config_arm64_va_bits) #define task_size_64 (ul(1) mmap_base = task_unmapped_base + random_factor; 10 mm->get_unmapped_area = arch_get_unmapped_area; 11 } else { 12 mm->mmap_base = mmap_base(random_factor); 13 mm->get_unmapped_area = arch_get_unmapped_area_topdown; 14 } 15 } 16 17 static int mmap_is_legacy(void) 18 { 19 if (current->personality & addr_compat_layout) 20 return 1; 21 22 if (rlimit(rlimit_stack) == rlim_infinity) 23 return 1; 24 25 return sysctl_legacy_va_layout; 26 }  
第 8~10 行代码,如果给进程描述符的成员 personality 设置标志位 addr_compat_layout 表示使用传统的虚拟地址空间布局,或者用户栈可以无限增长,或者通过文件“/proc/sys/vm/legacy_va_layout”指定,那么使用传统的自底向上增长的布局,内存映射区域的起始地址是 task_unmapped_base 加上随机值,分配未映射区域的函数是arch_get_unmapped_area。
第 11~13 行代码,如果使用自顶向下增长的布局,那么分配未映射区域的函数是 arch_get_unmapped_area_topdown,内存映射区域的起始地址的计算方法如下:
arch/arm64/include/asm/elf.h #ifdef config_compat #define stack_rnd_mask (test_thread_flag(tif_32bit) ? 0x7ff >> (page_shift - 12) : 0x3ffff >> (page_shift - 12)) #else #define stack_rnd_mask (0x3ffff >> (page_shift - 12)) #endif arch/arm64/mm/mmap.c #define min_gap (sz_128m + ((stack_rnd_mask << page_shift) + 1)) #define max_gap (stack_top/6*5) static unsigned long mmap_base(unsigned long rnd) { unsigned long gap = rlimit(rlimit_stack); if (gap max_gap) gap = max_gap; return page_align(stack_top - gap - rnd); }
先计算内存映射区域的起始地址和栈顶的间隙:初始值取用户栈的最大长度,限定不能小于“128mb + 栈的最大随机偏移值 + 1”,确保用户栈最大可以达到 128mb;限定不能超过 stack_top 的 5/6。内存映射区域的起始地址等于“stack_top−间隙−随机值”,然后向下对齐到页长度。
回到函数load_elf_binary:函数 setup_arg_pages 把栈顶设置为 stack_top 减去随机120 3.2 虚拟地址空间布局值,然后把环境变量和参数从临时栈移到最终的用户栈;函数 set_brk 设置堆的起始地址,如果启用堆随机化,把堆的起始地址加上随机值。
fs/binfmt_elf.c static int load_elf_binary(struct linux_binprm *bprm) { … retval = setup_arg_pages(bprm, randomize_stack_top(stack_top), executable_stack); … retval = set_brk(elf_bss, elf_brk, bss_prot); … if ((current->flags & pf_randomize) && (randomize_va_space > 1)) { current->mm->brk = current->mm->start_brk = arch_randomize_brk(current->mm); } … }  
3.2.3 内核地址空间布局
arm64 处理器架构的内核地址空间布局如图 3.8 所示。
(1)线性映射区域的范围是[page_offset,],起始位置是 page_offset =(0xffff ffff ffff ffff << (va_bits-1)),长度是内核虚拟地址空间的一半。称为线性映射区域的原因是虚拟地址和物理地址是线性关系:
虚拟地址 =((物理地址 − phys_offset)+ page_offset),其中 phys_offset是内存的起始物理地址。
(2)vmemmap 区域的范围是 [vmemmap_start, page_offset),长度是
vmemmap_size =(线性映射区域的长度 / 页长度 * page 结构体的长度上限)。
内核使用 page 结构体描述一个物理页,内存的所有物理页对应一个 page 结构体数组。如果内存的物理地址空间不连续,存在很多空洞,称为稀疏内存。vmemmap 区域是稀疏内存的 page 结构体数组的虚拟地址空间。
(3)pci i/o 区域的范围是[pci_io_start, pci_io_end),长度是 16mb,结束地址是pci_io_end = (vmemmap_start − 2mb)。
外围组件互联(peripheral component interconnect,pci)是一种总线标准,pci i/o 区域是 pci 设备的 i/o 地址空间。
(4)固定映射区域的范围是[fixaddr_start, fixaddr_top),长度是fixaddr_size,结束地址是 fixaddr_top = (pci_io_start − 2mb)。
固定地址是编译时的特殊虚拟地址,编译的时候是一个常量,在内核初始化的时候映射到物理地址。
(5)vmalloc 区域的范围是[vmalloc_start, vmalloc_end),起始地址是vmalloc_start,等于内核模块区域的结束地址,结束地址是 vmalloc_end = (page_offset −pud_size − vmemmap_size − 64kb),其中 pud_size 是页上级目录表项映射的地址空间的长度。
vmalloc 区域是函数 vmalloc 使用的虚拟地址空间,内核使用 vmalloc 分配虚拟地址连续但物理地址不连续的内存。内核镜像在 vmalloc 区域,起始虚拟地址是(kimage_vaddr + text_offset) ,其中 kimage_vaddr 是内核镜像的虚拟地址的基准值,等于内核模块区域的结束地址modules_end;text_offset 是内存中的内核镜像相对内存起始位置的偏移。
(6)内核模块区域的范围是[modules_vaddr, modules_end),长度是 128mb,起始地址是 modules_vaddr =(内核虚拟地址空间的起始地址 + kasan 影子区域的长度)。
内核模块区域是内核模块使用的虚拟地址空间。
(7)kasan 影子区域的起始地址是内核虚拟地址空间的起始地址,长度是内核虚拟地址空间长度的 1/8。
内核地址消毒剂(kernel address sanitizer,kasan)是一个动态的内存错误检查工具。它为发现释放后使用和越界访问这两类缺陷提供了快速和综合的解决方案。
3.3 物理地址空间
物理地址是处理器在系统总线上看到的地址。使用精简指令集(reduced instruction setcomputer,risc)的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间。有些处理器架构把分配给外围设备的物理地址区域称为设备内存。
处理器通过外围设备控制器的寄存器访问外围设备,寄存器分为控制寄存器、状态寄存器和数据寄存器三大类,外围设备的寄存器通常被连续地编址。处理器对外围设备寄存器的编址方式有两种。
(1)i/o 映射方式(i/o-mapped):英特尔的 x86 处理器为外围设备专门实现了一个单独的地址空间,称为“i/o 地址空间”或“i/o 端口空间”,处理器通过专门的 i/o 指令(如x86 的 in 和 out 指令)来访问这一空间中的地址单元。
(2)内存映射方式(memory-mapped):使用精简指令集的处理器通常只实现一个物理地址空间,外围设备和物理内存使用统一的物理地址空间,处理器可以像访问一个内存单元那样访问外围设备,不需要提供专门的 i/o 指令。
程序只能通过虚拟地址访问外设寄存器,内核提供了以下函数来把外设寄存器的物理地址映射到虚拟地址空间。
(1)函数 ioremap()把外设寄存器的物理地址映射到内核虚拟地址空间。
  void * ioremap(unsigned long phys_addr, unsigned long size, unsigned long flags);  
(2)函数 io_remap_pfn_range()把外设寄存器的物理地址映射到进程的用户虚拟地址空间。
    int io_remap_pfn_range(struct vm_area_struct *vma, unsigned long addr,unsigned long pfn, unsigned long size, pgprot_t prot);  
除了 sparc 处理器以外,在其他处理器架构中函数 io_remap_pfn_range()和函数 remap_pfn_range()等价。函数 remap_pfn_range()用于把内存的物理页映射到进程的用户虚拟地址空间。
内核提供了函数 iounmap(),它用来删除函数 ioremap()创建的映射。
  void iounmap(void *addr);  
arm64 架构的实现
arm64 架构定义了两种内存类型。
(1)正常内存(normal memory):包括物理内存和只读存储器(rom)。
(2)设备内存(device memory):指分配给外围设备寄存器的物理地址区域。
对于正常内存,可以设置共享属性和缓存属性。共享属性用来定义一个位置是否可以被多个核共享,分为不可共享、内部共享和外部共享。不可共享是指只被处理器的一个核使用,内部共享是指一个处理器的所有核共享或者多个处理器共享,外部共享是指处理器和其他观察者(比如图形处理单元或 dma 控制器)共享。缓存属性用来定义访问时是否通过处理器的缓存。
设备内存的共享属性总是外部共享,缓存属性总是不可缓存(即必须绕过处理器的缓存)。
arm64 架构根据 3 种属性把设备内存分为 4 种类型。
(1)device-ngnrne,这种类型限制最严格。
(2)device-ngnre。
(3)device-ngre。
(4)device-gre,这种类型限制最少。
3 种属性分别如下。
(1)聚集属性:g 表示聚集(gathering),ng 表示不聚集(non gathering)。聚集属性决定对内存区域的多个访问是否可以被合并为一个总线事务。如果地址被标记为“不聚集”,那么必须按照程序里面的地址和长度访问。如果地址被标记为“聚集”,处理器可以把两个“写一个字节”的访问合并成一个“写两个字节”的访问,可以把对相同内存位置的多个访问合并,例如读相同位置两次,处理器只需要读一次,为两条指令返回相同的结果。
(2)重排序属性:r 表示重排序(re-ordering),nr 表示不重排序(non re-ordering)。这个属性决定对相同设备的多个访问是否可以重新排序。如果地址被标记为“不重排序”,那么对同一个块的访问总是按照程序顺序执行。
(3)早期写确认属性:e 表示早期写确认(early write acknowledgement),ne 表示不执行早期写确认(non early write acknowledgement)。
这个属性决定是否允许处理器和从属设备之间的中间写缓冲区发送“写完成”确认。如果地址被标记为“不执行早期写确认”,那么必须由外围设备发送“写完成”确认。如果地址被标记为“早期写确认”,那么允许写缓冲区在外围设备收到数据之前发送“写完成”确认。
物理地址宽度
目前 arm64 处理器支持的最大物理地址宽度是 48 位,如果实现了 armv8.2 标准的大物理地址(large physical address,lpa)支持,并且页长度是 64kb,那么物理地址的最大宽度是 52 位。
可以使用寄存器 tcr_el1(translation control register for exception level 1,异常级别 1 的转换控制寄存器)的字段 ips(intermediate physical address size,中间物理地址长度)控制物理地址的宽度,ips 字段的长度是 3 位,ips 字段的值和物理地址宽度的对应关系如表 3.3 所示。


恩智浦宣布推出用于安全汽车高性能计算的BlueBox 3.0开发平台
DIY 2.4G WIFI圆筒天线,WIFI antenna
中国电子视像行业协会Mini/Micro LED显示产业分会隆重举行
中央广播电视总台8K超高清电视实验频道试播成功 采用海思TV解码芯片
如何使用模拟数据修复过程并改进产品开发?
《Linux内核深度解析》之内存地址空间
电源中AC与DC理解
Oculus将开始“逐步”推出其新的Oculus Move功能
FP6298支持9V输出异步升压芯片,3-5V升压到5V/9V
给工作、游戏带来强大性能的多平台三星970系列固态硬盘
Flownex在智能化数据中心的应用有哪些
3C锂电池测试方案中弹片微针模组的作用是什么
基于氮化硼纳米管的天然纳腔构筑双曲回音壁极化激元
电机调速器控制芯片的种类
移位寄存器的原理
三星Note8下周就来,三星Note8配置、价格、双摄、双版本曝光,这才是三星的真正水平
三星QLED电视Q60让色彩宛如新生 为消费者打造视觉盛宴
开步电子签约智能电能表关键元器件协同攻关联合体
小米战略入股TCL 积极布局进军智能家居行业
周立功专访:周立功和他的团队已经找到了属于自己的道路