【原创】(二)Linux物理内存初始化
背景
Read the fucking source code!--By 鲁迅A picture is worth a thousand words.--By 高尔基
说明:
- Kernel版本:4.14
- ARM64处理器,Contex-A53,双核
- 使用工具:Source Insight 3.5, Visio
1. 介绍
让我们思考几个朴素的问题?
- 系统是怎么知道物理内存的?
- 在内存管理真正初始化之前,内核的代码执行需要分配内存该怎么处理?
我们先来尝试回答第一个问题,看过dts文件的同学应该见过memory的节点,以arch/arm64/boot/dts/freescale/fsl-ls208xa.dtsi为例:
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0 0x80000000>;
/* DRAM space - 1, size : 2 GB DRAM */
};
这个节点描述了内存的起始地址及大小,事实上内核在解析dtb文件时会去读取该memory节点的内容,从而将检测到的内存注册进系统。
那么新的问题又来了?Uboot会将kernel image和dtb拷贝到内存中,并且将dtb物理地址告知kernel,kernel需要从该物理地址上读取到dtb文件并解析,才能得到最终的内存信息,dtb的物理地址需要映射到虚拟地址上才能访问,但是这个时候paging_init还没有调用,也就是说物理地址的映射还没有完成,那该怎么办呢?没错,Fixed map机制出现了。
第二个问题答案:当所有物理内存添加进系统后,在mm_init之前,系统会使用memblock模块来对内存进行管理。
开启探索之旅吧!
2. early_fixmap_init
简单来说,Fixed map指的是虚拟地址中的一段区域,在该区域中所有的线性地址是在编译阶段就确定好的,这些虚拟地址需要在boot阶段去映射到物理地址上。
来张图片看看虚拟地址空间:

图中 fixed: 0xffffffbefe7fd000 - 0xffffffbefec00000,描述的就是Fixed map的区域。
那么这段区域中的详细一点的布局是怎样呢?看看arch/arm64/include/asm/fixmap.h中的enum fixed_address结构就清晰了,图来了:

从图中可以看出,如果要访问DTB所在的物理地址,那么需要将该物理地址映射到Fixed map中的区域,然后访问该区域中的虚拟地址即可。访问IO空间也是一样的道理,下文也会讲述到。
那么来看看early_fixmap_init函数的关键代码吧:
void __init early_fixmap_init(void)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
unsigned long addr = FIXADDR_START; /* (1) */
pgd = pgd_offset_k(addr); /* (2) */
if (CONFIG_PGTABLE_LEVELS > 3 &&
!(pgd_none(*pgd) || pgd_page_paddr(*pgd) == __pa_symbol(bm_pud))) {
/*
* We only end up here if the kernel mapping and the fixmap
* share the top level pgd entry, which should only happen on
* 16k/4 levels configurations.
*/
BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
pud = pud_offset_kimg(pgd, addr);
} else {
if (pgd_none(*pgd))
__pgd_populate(pgd, __pa_symbol(bm_pud), PUD_TYPE_TABLE); /* (3) */
pud = fixmap_pud(addr);
}
if (pud_none(*pud))
__pud_populate(pud, __pa_symbol(bm_pmd), PMD_TYPE_TABLE); /* (4) */
pmd = fixmap_pmd(addr);
__pmd_populate(pmd, __pa_symbol(bm_pte), PMD_TYPE_TABLE); /* (5) */
......
}
关键点:
FIXADDR_START,定义了Fixed map区域的起始地址,位于arch/arm64/include/asm/fixmap.h中;pgd_offset_k(addr),获取addr地址对应pgd全局页表中的entry,而这个pgd全局页表正是swapper_pg_dir全局页表;- 将
bm_pud的物理地址写到pgd全局页目录表中; - 将
bm_pmd的物理地址写到pud页目录表中; - 将
bm_pte的物理地址写到pmd页表目录表中;
bm_pud/bm_pmd/bm_pte是三个全局数组,相当于是中间的页表,存放各级页表的entry,定义如下:
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;
事实上,early_fixmap_init只是建立了一个映射的框架,具体的物理地址和虚拟地址的映射没有去填充,这个是由使用者具体在使用时再去填充对应的pte entry。比如像fixmap_remap_fdt()函数,就是典型的填充pte entry的过程,完成最后的一步映射,然后才能读取dtb文件。
来一张图片就懂了,是透彻的懂了:

3. early_ioremap_init
如果在boot早期需要操作IO设备的话,那么ioremap就用上场了,由于跟实际的内存管理关系不太大,不再太深入的分析。

简单来说,ioremap的空间为7 * 256K的区域,保存在slot_vir[]数组中,当需要进行IO操作的时候,最终会调用到__early_ioremap函数,在该函数中去填充对应的pte entry,从而完成最终的虚拟地址和物理地址的映射。
4. memblock
上文讲的内容都只是铺垫,为了能正确访问DTB文件并且解析得到物理地址信息。从入口到最终添加的调用过程如下图:

所以,这个章节的重点就是memblock模块,这个是早期的内存分配管理器,我不禁想起了之前在Nuttx中的内存池实现了,细节已然不太清晰了,但是框架性的思维都大同小异。
4.1 结构体

总共由三个数据结构来描述:
struct memblock定义了一个全局变量,用来维护所有的物理内存;struct memblock_type代表系统中的内存类型,包括实际使用的内存和保留的内存;struct memblock_region用来描述具体的内存区域,包含在struct memblock_type中的regions数组中,最多可以存放128个。
直接上个代码吧:
static struct memblock_region memblock_memory_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
static struct memblock_region memblock_reserved_init_regions[INIT_MEMBLOCK_REGIONS] __initdata_memblock;
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
static struct memblock_region memblock_physmem_init_regions[INIT_PHYSMEM_REGIONS] __initdata_memblock;
#endif
struct memblock memblock __initdata_memblock = {
.memory.regions = memblock_memory_init_regions,
.memory.cnt = 1, /* empty dummy entry */
.memory.max = INIT_MEMBLOCK_REGIONS,
.memory.name = "memory",
.reserved.regions = memblock_reserved_init_regions,
.reserved.cnt = 1, /* empty dummy entry */
.reserved.max = INIT_MEMBLOCK_REGIONS,
.reserved.name = "reserved",
#ifdef CONFIG_HAVE_MEMBLOCK_PHYS_MAP
.physmem.regions = memblock_physmem_init_regions,
.physmem.cnt = 1, /* empty dummy entry */
.physmem.max = INIT_PHYSMEM_REGIONS,
.physmem.name = "physmem",
#endif
.bottom_up = false,
.current_limit = MEMBLOCK_ALLOC_ANYWHERE,
};
定义的memblock为全局变量,在定义的时候就进行了初始化。初始化的时候,regions指向的也是静态全局的数组,其中数组的大小为INIT_MEMBLOCK_REGIONS,也就是128个,限制了这些内存块的个数了,实际在代码中可以看到,当超过这个数值时,数组会以2倍的速度动态扩大。
初始化完了后,大体是这个样子的:

4.2 memblock_add/memblock_remove
memblock子模块,基本的逻辑都是围绕内存的添加和移除操作来展开,最终是通过调用memblock_add_range/memblock_remove_range来实现的。
memblock_add_range:

图中的左侧是函数的执行流程图,执行效果是右侧部分。右侧部分画的是一个典型的情况,实际的情况可能有多种,但是核心的逻辑都是对插入的region进行判断,如果出现了物理地址范围重叠的部分,那就进行split操作,最终对具有相同flag的region进行merge操作。
memblock_remove_range
该函数执行的一个典型case效果如下图所示:

假如现在需要移除掉一片区域,而该区域跨越了多个region,则会先调用memblock_isolate_range来对这片区域进行切分,最后再调用memblock_isolate_range对区域范围内的region进行移除操作。
当调用memblock_alloc函数进行地址分配时,最后也是调用memblock_add_range来实现的,申请的这部分内存最终会添加到reserved类型中,毕竟已经分配出去了,其他人也不应该使用了。
5. arm64_memblock_init
当物理内存都添加进系统之后,arm64_memblock_init会对整个物理内存进行整理,主要的工作就是将一些特殊的区域添加进reserved内存中。函数执行完后,如下图所示:

- 其中浅绿色的框表示的都是保留的内存区域, 剩下的部分就是可以实际去使用的内存了。
物理内存大体面貌就有了,后续就需要进行内存的页表映射,完成实际的物理地址到虚拟地址的映射了。
那就待续吧。

【原创】(二)Linux物理内存初始化的更多相关文章
- Linux内存管理 (1)物理内存初始化
专题:Linux内存管理专题 关键词:用户内核空间划分.Node/Zone/Page.memblock.PGD/PUD/PMD/PTE.lowmem/highmem.ZONE_DMA/ZONE_NOR ...
- linux文件系统 - 初始化(二)
加载initrd(上) 一.目的 本文主要讲述linux3.10文件系统初始化过程的第二阶段:加载initrd. initrd是一个临时文件系统,由bootload负责加载到内存中,里面包含了基本的可 ...
- 【原创】Linux中断子系统(二)-通用框架处理
背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...
- 【原创】Linux信号量机制分析
背景 Read the fucking source code! --By 鲁迅 A picture is worth a thousand words. --By 高尔基 说明: Kernel版本: ...
- linux文件系统 - 初始化(一)
术语表: struct task:进程 struct mnt_namespace:命名空间 struct mount:挂载点 struct vfsmount:挂载项 struct file:文件 st ...
- linux文件系统初始化过程(5)---加载initrd(下)
一.目的 linux把文件分为常规文件.目录文件.软链接文件.硬链接文件.特殊文件(设备文件.管道文件.socket文件等)几种类型,分别对应不同的新建函数sys_open().sys_mkdir() ...
- linux文件系统初始化过程(2)---挂载rootfs文件系统
一.目的 本文主要讲述linux3.10文件系统初始化过程的第一阶段:挂载rootfs文件系统. rootfs是基于内存的文件系统,所有操作都在内存中完成:也没有实际的存储设备,所以不需要设备驱动程序 ...
- linux文件系统初始化过程(1)---概述
术语表: struct task:进程 struct mnt_namespace:命名空间 struct mount:挂载点 struct vfsmount:挂载项 struct file:文件 st ...
- (原创)Linux下MySQL 5.5/5.6的修改字符集编码为UTF8(彻底解决中文乱码问题)
« CloudStack+XenServer详细部署方案(10):高级网络功能应用 (总结)CentOS Linux 5.x在GPT分区不能引导的解决方法 » 2013-1 11 (原创)Linux下 ...
随机推荐
- 解读equals()和hashCode()
前面部分摘自:https://blog.csdn.net/javazejian/article/details/51348320 一:Object中equals方法的实现原理 public boole ...
- json与js对象间的转化
- RabbitMQ(三):RabbitMQ与Spring Boot简单整合
RabbitMQ是目前非常热门的一款消息中间件,不管是互联网大厂还是中小企业都在大量使用.Spring Boot的兴起,极大地简化了Spring的开发,本文将使用Spring Boot与RabbitM ...
- Linu基础之权限管理
二十二.权限管理 22.1)什么是权限 针对某些文件或者进程,对用户进行限制,权限可以理解为用于约束用户能对系统所做的操作. 22.2)权限和用户的关系 [root@centos7 ~]# ll ...
- static import和import的区别
import static静态导入是JDK1.5中的新特性.一般我们导入一个类都用 import com.....ClassName;而静态导入是这样:import static com.....Cl ...
- Jmeter CSV config使用
1.添加线程组,自己给线程组命名 2.添加CSV data set config 如上,filename是文件的名字 新增.txt文件,将变量写在文件中,完成后,更名为.csv:变量之间用逗号隔开(第 ...
- codeforces 327 B. Hungry Sequence
题目链接 题目就是让你输出n个数的序列,要保证该序列是递增的,并且第i个数的前面不能保护它的约数,我直接先对前100000的素数打表,然后输出前n个,so easy. //cf 191 B #incl ...
- 后台post注入爆密码
后台登陆框post注入按照注入的方式属于post,和前台搜索型post注入.文本框注入类似,由于目前主流的注 入工具除了穿山甲等较新工具以外几乎都是get注入,尤其是对于这种后台账户型post注入式无 ...
- Robotframework获取移动端toast问题
背景: 在做移动端自动化测试的时候,经常会遇到一个问题就是获取toast提示问题,如果需要解决这个问题需要重新处理,不能按照正常的逻辑,使用robotframework自带的关键字进行获取,需要重新考 ...
- 并发知识(2)——关于Thread
一些容易混淆的知识点 sleep vs wait sleep是Thread,wait是Object方法 wait和notify只能在同步代码块中调用 wait释放锁资源,sleep不释放锁资源 唤醒条 ...