一个关于线性映射区的疑问

学习内存管理时一直有个疑问:内核虚拟地址空间中有一部分是线性映射用的,在内核初始化时会把物理内存映射到这个线性区域,既然映射已经建立完成了,那是不是就把物理内存全都占用了,用户空间是否就不能申请这些内存了?
后来宋宝华老师给出了解释:被映射和被占用完全是两码事。
下面就结合代码,谈谈我最这个解释的理解,以及自己碰到的疑问。
我最大的疑问是,在实际项目中,我看到的线性映射区并没有对所有的物理内存进行映射,而只有很小一部分进行了映射。这个结论是从ramdump中获取到的。
先列下项目中的虚拟地址空间布局:

[    0.000000] Virtual kernel memory layout:
[    0.000000]     modules : 0xffffff8000000000 - 0xffffff8008000000   (   128 MB)
[    0.000000]     vmalloc : 0xffffff8008000000 - 0xffffffbebfff0000   (   250 GB)
[    0.000000]       .text : 0xffffff8008080000 - 0xffffff8009010000   ( 15936 KB)
[    0.000000]     .rodata : 0xffffff8009100000 - 0xffffff8009900000   (  8192 KB)
[    0.000000]       .init : 0xffffff8009900000 - 0xffffff8009ac0000   (  1792 KB)
[    0.000000]       .data : 0xffffff8009ac0000 - 0xffffff8009cfb200   (  2285 KB)
[    0.000000]        .bss : 0xffffff8009cfb200 - 0xffffff800aa823a8   ( 13853 KB)
[    0.000000]     fixed   : 0xffffffbefe7fb000 - 0xffffffbefec00000   (  4116 KB)
[    0.000000]     PCI I/O : 0xffffffbefee00000 - 0xffffffbeffe00000   (    16 MB)
[    0.000000]     vmemmap : 0xffffffbf00000000 - 0xffffffc000000000   (     4 GB maximum)
[    0.000000]               0xffffffbf00400000 - 0xffffffbf04000000   (    60 MB actual)
[    0.000000]     memory  : 0xffffffc010000000 - 0xffffffc100000000   (  3840 MB)

我这是kernel 4.9版本,arm64的CPU,4GB RAM的配置:
DDRCS0.BIN 10000000–7fffffff
DDRCS1.BIN 80000000–ffffffff
所以,我的线性映射区虚拟地址范围是0xffffffc010000000 - 0xffffffc100000000
对应物理地址范围是0x10000000 - 0x100000000
xx从图中可以看出,在线性映射区中只有小部分是有效映射,这是为什么?这就和之前的说法有矛盾:之前说已经全部做好映射了,可是这里却只有一小部分。

疑问抛出来,下面是我的一些分析。
通过查询页表的信息,来看看到底有没有做映射,没准是trace32的问题呢。
我选了两个地址,一个有映射,一个没有:
0xFFFFFFC011900000 --> 0x11900000
0xFFFFFFC012C00000 --> null
下面是简单的映射表的查找:
ffffff800aa86000 A swapper_pg_dir
1000000008=0x800
010001100
8=0x460
100000000*8=0x800
d.dump 0xffffff800aa86000+0x800 -> 0xF23E7003 最后12bit清零,同时减去0x80000000得到0x723E7000
hexdump -C -s 0x723E7000+0x460 -n 8 DDRCS1.BIN -->0xf23e5003 最后12bit清零,同时减去0x80000000得到0x723e5000
hexdump -C -s 0x723e5000+0x800 -n 8 DDRCS1.BIN -->e80000 11900f13 ->0x11900000
这与trace32显示的结果是匹配上的,确实映射到了物理地址0x11900000

下面是0xFFFFFFC012C00000的映射过程
1000000008=0x800
010010110
8=0x4B0
000000000*8=0x0
d.dump 0xffffff800aa86000+0x800 -> 0xF23E7003 最后12bit清零,同时减去0x80000000得到0x723E7000
hexdump -C -s 0x723E7000+0x4b0 -n 8 DDRCS1.BIN ->f23db003 最后12bit清零,同时减去0x80000000得到723db000
hexdump -C -s 0x723db000+0x0 -n 8 DDRCS1.BIN ->e80000 12c00f12 ->0x12c00000
貌似也映射到了预期的0x12c00000地址。

查了下armv8文档中关于MMU的介绍,发现这个最后的pte其实是无效的,因为它的最后一个bit是0
在这里插入图片描述从这里可以看到,若最后一个bit为0,则代表为Invalid。
查了下内核代码,确实有相关定义:
#define PTE_VALID (_AT(pteval_t, 1) << 0)
因此,我的结论是:映射已经执行完成了,因为相关的页表都是有效的,但是映射完成后做了特殊处理,使之无效了。

根据PTE_VALID这个线索查下去,发现在free page时会将其置为无效,在alloc page时会置为有效:

__free_pages
	__free_pages_ok
		free_pages_prepare
			kernel_map_pages(page, 1 << order, 0);//此处的0代表无效
get_page_from_freelist
	prep_new_page
		post_alloc_hook
			kernel_map_pages(page, 1 << order, 1);//此处的1代表有效

所以,我推测是在内核初始化阶段映射的工作确实做了,但是初始化完成,由memblock转到buddy system去管理内存时,会把memory中非reserved的内存释放,转给buddy,那里会调用free page,因此就会把没有分配的内存的pte置为无效。在后续申请内存时,只需要再置为有效就可以,不需要再做映射操作了。这就是我对自己疑问的解释。

在这个过程中,我注意到了一个配置:CONFIG_DEBUG_PAGEALLOC=y
我的项目中,配置为y。
看它的描述,和前面的分析基本一致:会把线性映射区未使用的空间unmap掉,在申请page时再完成map。

config DEBUG_PAGEALLOC
	bool "Debug page memory allocations"
	---help---
	  Unmap pages from the kernel linear mapping after free_pages().
	  Depending on runtime enablement, this results in a small or large
	  slowdown, but helps to find certain types of memory corruption.

	  For architectures which don't enable ARCH_SUPPORTS_DEBUG_PAGEALLOC,
	  fill the pages with poison patterns after free_pages() and verify
	  the patterns before alloc_pages().  Additionally,
	  this option cannot be enabled in combination with hibernation as
	  that would result in incorrect warnings of memory corruption after
	  a resume because free pages are not saved to the suspend image.

	  By default this option will have a small overhead, e.g. by not
	  allowing the kernel mapping to be backed by large pages on some
	  architectures. Even bigger overhead comes when the debugging is
	  enabled by DEBUG_PAGEALLOC_ENABLE_DEFAULT or the debug_pagealloc
	  command line parameter.

既然如此,把它关闭会怎样?
看DEBUG_PAGEALLOC_ENABLE_DEFAULT相关注释可知,这个功能可以动态关闭。不过我采用的还是直接关闭CONFIG_DEBUG_PAGEALLOC。之后获取到的ramdump是这样的。
在这里插入图片描述恩,这次好多了,但是还是有6块区域并没有映射。
针对这个疑问,我只能解释一部分:其中有3块区域我可以解释清楚,另外3块就不知道是为什么了。

android:/sys/kernel/debug/memblock # cat memory                                                                        
   0: 0x0000000010000000..0x00000000849fffff
   1: 0x0000000086800000..0x0000000086bfffff
   2: 0x000000008ee00000..0x00000000929fffff
   3: 0x000000009a200000..0x00000000ffffffff

从/sys/kernel/debug/memblock/memory文件可知,这个项目申请了3块预留区域,这些预留预期在map时是不会映射。
剩下那3块,我不知该如何解释。暂时遗留吧。

最后看看相关代码

///kernel/msm-4.9/arch/arm64/mm/mmu.c
void __init paging_init(void)
{
	phys_addr_t pgd_phys = early_pgtable_alloc();
	pgd_t *pgd = pgd_set_fixmap(pgd_phys);
    // 这里完成kernel的重新映射,区别于之前开启MMU时__create_page_tables创建的section映射,这里更细了
	map_kernel(pgd);
    // 这里完成线性映射
	map_mem(pgd);
	// 以前一直没看懂这里到底想干啥,今天终于知道了。开启MMU时已经为kernel镜像创建过映射,那时使用的是位于内核bss段之后的空间来存储页表,我的配置是CONFIG_ARM64_VA_BITS=39,4K page,所以这里只有2个page空间,使用的是section map。那时也不区分代码段,数据段,反正能建立映射访问就可以了,现在就不一样了,memblock已经OK,dtb也访问完成,可以使用动态申请内存???,于是就要重新映射。映射包括把kernel的代码和数据映射到vmalloc区域,同时完成线性区域的映射。但是,做这些操作时不能动之前__create_page_tables创建的页表,否则代码可能就无法执行了。所以,函数一开始就通过fixmap申请了一个临时page作为内核页表的基地址,然后在这上面完成其它页表的建立,最后通过一个copy动作把内容copy到swapper_pg_dir中:当前的ttbr1保存的是swapper_pg_dir;由于pgd_phys的映射已经完成,所以使用pgd_phys,这样就可以随意操作swapper_pg_dir;然后把pgd_phys的内容copy到swapper_pg_dir中;然后还是使用swapper_pg_dir作为ttbr1的内容。如此一来,以前bss段之后的2个page,只有第一个还有用,后面那个就可以free了。
    // ffffff800aa86000 A swapper_pg_dir
    // ffffff800aa88000 A tramp_pg_dir
	cpu_replace_ttbr1(__va(pgd_phys));
	memcpy(swapper_pg_dir, pgd, PAGE_SIZE);
	cpu_replace_ttbr1(lm_alias(swapper_pg_dir));

	pgd_clear_fixmap();
	memblock_free(pgd_phys, PAGE_SIZE);
	memblock_free(__pa_symbol(swapper_pg_dir) + PAGE_SIZE,
		      SWAPPER_DIR_SIZE - PAGE_SIZE);
}

static void __init map_mem(pgd_t *pgd)
{
	struct memblock_region *reg;
    //从这个for循环看,reserved-memory节点下compatible = "removed-dma-pool",且有no-map的,是不会在memblock.memory中出现,因此也就不会被映射,至于下面的memblock_is_nomap,貌似都是true吧。要注意的是,这里并不关注是否在memblock.reserve中出现不影响,都会做映射,只是后续memblock交给buddy时,reserve中的会被保留,不会调用free page,因此其映射也会保留
	/* map all the memory banks */
	for_each_memblock(memory, reg) {
		phys_addr_t start = reg->base;
		phys_addr_t end = start + reg->size;
		if (start >= end)
			break;
		if (memblock_is_nomap(reg))
			continue;
		__map_memblock(pgd, start, end);
	}
}

在做线性映射时,虚拟地址是怎么获取的?因为我们已知的是物理地址空间。虚拟地址就是通过下面这个宏来计算的。
对于我的4G RAM配置而言,此处的PHYS_OFFSET为0,因此这个物理到虚拟的映射关系非常简单,就是物理地址加上PAGE_OFFSET,也就是0xffffffc000000000。
以前一直觉得奇怪,对于内核而言,在vmalloc区域和线性区域,都有虚拟地址空间指向kernel镜像所在的物理地址空间,那物理地址转换为虚拟地址岂不是应该有两种可能的结果。实际确实是,不过,这里的__phys_to_virt返回的只是线性区域的虚拟地址而已。还有一个宏__phys_to_kimg正是用来返回vmalloc区域的虚拟地址的,这个后面再仔细看看。
#define __phys_to_virt(x)	((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET)
#define PHYS_OFFSET		({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })
#define __phys_to_kimg(x)	((unsigned long)((x) + kimage_voffset))
  • 1
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值