1.语言通俗易懂,内容深入浅出。
2.逻辑清晰,条理分明linux内核地址空间,逐渐深入,层层递进。
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库的ptmalloclinux系统日志,还有微软公司的tcmalloc和FreeBSD的jemalloc。
3.1.2.内核空间
(1)内核空间的基本功能。
虚拟显存管理负责从进程的虚拟地址空间分配虚拟页,sys_brk拿来扩大或收缩堆,sys_mmap拿来在显存映射区域分配虚拟页,sys_munmap拿来释放虚拟页。
内核使用延后分配化学显存的策略,进程第一次访问虚拟页的时侯,触发页错误异常,页错误异常处理程序从页分配器申请数学页,在进程的页表中把虚拟页映射到化学页。
页分配器负责分配化学页,当前使用的页分配器是伙伴分配器。
内核空间提供了把页界定成小显存块分配的块分配器,提供分配显存的插口kmalloc()和释放显存的插口kfree(),支持3种块分配器:SLAB分配器、SLUB分配器和SLOB分配器。
在内核初始化的过程中,页分配器还没打算好,须要使用临时的引导显存分配器分配显存。
(2)内核空间的扩充功能。
不连续页分配器提供了分配显存的插口vmalloc和释放显存的插口vfree,在显存碎片化的时侯,申请连续数学页的成功率很低,可以申请不连续的化学页,映射到连续的虚拟页,即虚拟地址连续而化学地址不连续。
每处理器显存分配器拿来为每处理器变量分配显存。
连续显存分配器(ContiguousMemoryAllocator,CMA)拿来给驱动程序预留一段连续的显存,当驱动程序不用的时侯,可以给进程使用;当驱动程序须要使用的时侯,把进程占用的显存通过回收或迁移的形式让下来,给驱动程序使用。
显存控制组拿来控制进程占用的显存资源。
当显存碎片化的时侯,找不到连续的化学页,显存碎片整理(“memorycompaction”的译音,译音为“内存缩紧”)通过迁移的形式得到连续的化学页。
在显存不足的时侯,页回收负责回收化学页,对于没有后备储存设备支持的匿名页,把数据换出到交换区,之后释放化学页;对于有后备储存设备支持的文件页,把数据写回储存设备,之后释放化学页。假如页回收失败,使用最后一招:显存用尽杀手(OOMkiller,Out-of-Memorykiller),选择进程杀掉。
3.1.3.硬件层面
处理器包含一个称为显存管理单元(MemoryManagementUnit,MMU)的部件,负责把虚拟地址转换成化学地址。
显存管理单元包含一个称为页表缓存(TranslationLookasideBuffer,TLB)的部件,保存近来使用过的页表映射,防止每次把虚拟地址转换成化学地址都须要查询显存中的页表。
为了解决处理器的执行速率和显存的访问速率不匹配的问题,在处理器和显存之间降低了缓存。缓存一般分为一级缓存和二级缓存,为了支持并行地取指令和取数据,一级缓存分为数据缓存和指令缓存。
3.2虚拟地址空间布局
3.2.1虚拟地址空间界定
由于目前应用程序没有这么大的显存需求,所以ARM64处理器不支持完全的64位虚拟地址,实际支持情况如下。
图3.2ARM64内核/用户虚拟地址空间界定
(1)虚拟地址的最大长度是48位,如图3.2所示。内核虚拟地址在64位地址空间的底部,高16位是全1,范围是[0xFFFF0,0xFFFFFFFFFFFFFFFF];用户虚拟地址在64位地址空间的顶部,高16位是全0,范围是[0x00000,0x0000FFFFFFFFFFFF];高16位是全1或全0的地址称为规范的地址,二者之间是不规范的地址,不容许使用。
(2)假如处理器实现了ARMv8.2标准的大虚拟地址(LargeVirtualAddress,LVA)支持,但是页宽度是64KB,这么虚拟地址的最大长度是52位。
(3)可以为虚拟地址配置比最大长度小的长度,而且可以为内核虚拟地址和用户虚拟地址配置不同的间距。转换控制寄存器(TranslationControlRegister)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内核中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) << VA_BITS)
#ifdef CONFIG_COMPAT /* 支持执行32位用户空间程序 */
#define TASK_SIZE_32 UL(0x100000000)
/* test_thread_flag(TIF_32BIT)判断用户空间程序是不是32位 */
#define TASK_SIZE (test_thread_flag(TIF_32BIT) ?
TASK_SIZE_32 : TASK_SIZE_64)
#define TASK_SIZE_OF(tsk) (test_tsk_thread_flag(tsk, TIF_32BIT) ?
TASK_SIZE_32 : TASK_SIZE_64)
#else
#define TASK_SIZE TASK_SIZE_64
#endif /* CONFIG_COMPAT */
进程的用户虚拟地址空间包含以下区域。
(1)代码段、数据段和未初始化数据段。
(2)动态库的代码段、数据段和未初始化数据段。
(3)储存动态生成的数据的堆。
(4)储存局部变量和实现函数调用的栈。
(5)储存在栈顶部的环境变量和参数字符串。
(6)把文件区间映射到虚拟地址空间的显存映射区域。
内核使用显存描述符mm_struct描述进程的用户虚拟地址空间,显存描述符的主要成员如表3.1所示。
进程描述符(task_struct)中和显存描述符相关的成员如表3.2所示。
假如进程不属于线程组,这么进程描述符和显存描述符的关系如图3.3所示,进程描述符的成员mm和active_mm都指向同一个显存描述符,显存描述符的成员mm_users是1、成员mm_count是1。
假如两个进程属于同一个线程组,这么进程描述符和显存描述符的关系如图3.4所示,每位进程的进程描述符的成员mm和active_mm都指向同一个显存描述符,显存描述符的成员mm_users是2、成员mm_count是1。
内核线程的进程描述符和显存描述符的关系如图3.5所示,内核线程没有用户虚拟地址空间,当内核线程没有运行的时侯,进程描述符的成员mm和active_mm都是空表针;当内核线程运行的时侯,借用上一个进程的显存描述符,在被借用进程的用户虚拟地址空间的上方运行linux数据恢复,进程描述符的成员active_mm指向借用的显存描述符,假定被借用的显存描述符所属的进程不属于线程组,这么显存描述符的成员mm_users不变,依然是1,成员mm_count加1弄成2。
为了使缓冲区溢出功击愈加困难,内核支持为显存映射区域、栈和堆选择随机的起始地址。进程是否使用虚拟地址空间随机化的功能,由以下两个诱因共同决定。
(1)进程描述符的成员personality(个性化)是否设置ADDR_NO_RANDOMIZE。
(2)全局变量randomize_va_space:0表示关掉虚拟地址空间随机化,1表示使显存映射区域和栈的起始地址随机化,2表示使显存映射区域、栈和堆的起始地址随机化。可以通过文件“/proc/sys/kernel/randomize_va_space”修改。
mm/memory.c
int randomize_va_space __read_mostly =
#ifdef CONFIG_COMPAT_BRK
1;
#else
2;
#endif
为了使旧的应用程序(基于libc5)正常运行,默认打开配置宏CONFIG_COMPAT_BRK,严禁堆随机化。所以默认配置是使显存映射区域和栈的起始地址随机化。
栈一般自顶向上下降,当前只有惠普公司的PA-RISC处理器的栈是自底向下下降。栈的起始地址是STACK_TOP,默认启用栈随机化,须要把起始地址乘以一个随机值。STACK_TOP是每种处理器构架自定义的宏,ARM64构架定义的STACK_TOP如下所示:假如是64位用户空间程序,STACK_TOP的值是TASK_SIZE_64;若果是32位用户空间程序,STACK_TOP的值是异常向量的基准地址0xFFFFxFFFF0000。
arch/arm64/include/asm/processor.h
#define STACK_TOP_MAX TASK_SIZE_64
#ifdef CONFIG_COMPAT /* 支持执行32位用户空间程序 */
#define AARCH32_VECTORS_BASE 0xffff0000
#define STACK_TOP (test_thread_flag(TIF_32BIT) ?
AARCH32_VECTORS_BASE : STACK_TOP_MAX)
#else
#define STACK_TOP STACK_TOP_MAX
#endif /* CONFIG_COMPAT */
显存映射区域的起始地址是显存描述符的成员mmap_base。如图3.6所示,用户虚拟
地址空间有两种布局,区别是显存映射区域的起始位置和下降方向不同。
(1)传统布局:显存映射区域自底向下下降,起始地址是TASK_UNMAPPED_BASE,每种处理器构架都要定义这个宏,ARM64构架定义为TASK_SIZE/4。默认启用显存映射区域随机化,须要把起始地址加上一个随机值。传统布局的缺点是堆的最大宽度遭到限制,在32位系统中影响比较大,并且在64位系统中这不是问题。
(2)新布局:显存映射区域自顶向上下降,起始地址是(STACK_TOP−栈的最大宽度−间隙)。默认启用显存映射区域随机化,须要把起始地址乘以一个随机值。当进程调用execve以装载ELF文件的时侯,函数load_elf_binary将会创建进程的用户虚拟地址空间。函数load_elf_binary创建用户虚拟地址空间的过程如图3.7所示。
若果没有给进程描述符的成员personality设置标志位ADDR_NO_RANDOMIZE(该标志位表示严禁虚拟地址空间随机化),但是全局变量randomize_va_space是非零位,这么给进程设置标志PF_RANDOMIZE,容许虚拟地址空间随机化。
各类处理器构架自定义的函数arch_pick_mmap_layout负责选择显存映射区域的布局。ARM64构架定义的函数arch_pick_mmap_layout如下:
arch/arm64/mm/mmap.c
1 void arch_pick_mmap_layout(struct mm_struct *mm)
2 {
3 unsigned long random_factor = 0UL;
4
5 if (current->flags & PF_RANDOMIZE)
6 random_factor = arch_mmap_rnd();
7
8 if (mmap_is_legacy()) {
9 mm->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 < MIN_GAP)
gap = MIN_GAP;
else 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除以随机1203.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=(0xFFFFFFFFFFFFFFFF