Linux内核中引入了一种新的向内核传递参数的方法tag标记。内核参数通过一个静态的tag链表在启动的时候传递到内核。每个tag的结构为如下类型。这个结构在u-boot结束是初始化完成,放在内存固定位置,2440单板在0x3000_0100出。
+-----------+
tag_header
+-----------+
tag_xxx
+-----------+
初始化过程中获取内存或者是获取其他数据都需要从tag中来拿到,然后解析tag赋值给初始化的全局或者是局部变量中。本篇文章分析如下一段代码,代码主要是完成三项任务。一,解析tag内容获取内存;二,根据内存类型将内存复制给meminfo结构体;三,获取程序的整体位置,复制给全局变量。
if (mdesc->boot_params)
tags = phys_to_virt(mdesc->boot_params); ------(1.1)
/*
* If we have the old style parameters, convert them to
* a tag list.
*/
if (tags->hdr.tag != ATAG_CORE)
convert_to_tag_list(tags); ------(1.2)
if (tags->hdr.tag != ATAG_CORE)
tags = (struct tag *)&init_tags; ------(1.3)
if (mdesc->fixup)
mdesc->fixup(mdesc, tags, &from, &meminfo); ------(2.1)
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags); (*)
parse_tags(tags); ------(2.2)
}
/* 获取内核代码的其实位置和结束位置,arch/arm/kernel/vmlinux.lds文件中定义对应的数值
* KERN_NOTICE: _text =c002b000 _etext=c035b000 _edata=c039b688 _end=c03cebf4
*/
init_mm.start_code = (unsigned long) &_text;
init_mm.end_code = (unsigned long) &_etext;
init_mm.end_data = (unsigned long) &_edata;
init_mm.brk = (unsigned long) &_end;
1. tag
其中tag_header为tag头,表明tag_xxx的类型和大小,之所以要标识tag_xxx的类型是因为不同的tag需要不同的处理函数(下文讲tagtable的时候会分析到)。tag_header的结构为
struct tag_header
{
int size;
int tag;
}
size表示tag的结构大小,tag为表示tag类型的常量。这个静态的链表必须以tag_header.tag = ATAG_CORE开始,并以tag_header.tag = ATAG_NONE结束。由于不同的tag所使用的格式可能不尽相同,所以内核又定义了一个结构tagtable来把tag和相应的操作函数关联起来
struct tagtable
{
u32 tag;
int (*parse)(const struct tag*);
}
其中tag为标识入ATAG_NONE,ATAG_CORE等。parse为处理函数。Linux内核将tagtable也组成了一个静态的链表放入.taglist.init节中,这是通过__tagtable宏来实现的
#define __tag __attribute_used__ __attribute__((__section__ (“.taglist.init”)))
#define __tagble(tag,fn) static struct tagtable __tagtable_##fn __tag = {tag, fn}
内存和command line的解析tag实现逻辑如下
static int __init parse_tag_mem32(const struct tag *tag)
{
if (meminfo.nr_banks >= NR_BANKS) {
printk(KERN_WARNING
"Ignoring memory bank 0x%08x size %dKB\n",
tag->u.mem.start, tag->u.mem.size / 1024);
return -EINVAL;
}
arm_add_memory(tag->u.mem.start, tag->u.mem.size);
return 0;
}
__tagtable(ATAG_MEM, parse_tag_mem32);
命令行:
static int __init parse_tag_cmdline(const struct tag* tag)
{
strlcpy(default_command_line, tag->u.cmdline.cmdline, COMMAND_LINE_SIZE);
}
__tagtable(ATAG_CMDLINE, parse_tag_cmdline)
可以看到parse_tag_cmdline将命令行参数拷贝到default_command_line里,__tagtable将ATAG_CMDLINE和parse_tag_cmdline挂钩。
根据如上的铺垫我们可以分析一下tag的基本解析过程:
- (1.1)命令将uboot传入的tag位置物理地址转换为虚拟地址。因为当前已经启动了MMU,使用虚拟地址进行访问。
- (1.2)如果tag的格式如果传递参数非tag list格式,那么就是param_struct格式,这种情况下面我们需要调用转换函数包param结构体转换成tag格式。
- (1.3)如果这个时候还是不对,也就是说明(1.2)中param给定的信息也是不对的。我们就使用默认的init_tags结构体中的数据。具体定义见下面。
static struct init_tags
{
struct tag_header hdr1;
struct tag_core core;
struct tag_header hdr2;
struct tag_mem32 mem;
struct tag_header hdr3;
}init_tags __initdata = {
{ tag_size(tag_core), ATAG_CORE },
{ 1, PAGE_SIZE, 0xff },
{ tag_size(tag_mem32), ATAG_MEM },
{ MEM_SIZE, PHYS_OFFSET },
{ 0, ATAG_NONE }
}
2. 内存解析
- (2.1)对于2440单板文件mach-smdk2440.c文件中没有fixup函数实现,空指针跳过,通过tag进行数据解析
- (2.2)parse_tags->parse_tag->(tagtable->parse)解析函数会一次调用到对应的tag内部的解析函数对数据进行解析。这里解析了所有的tag,包括memory。memory解析最终的数据给到了meminfo结构体中
其中(*)步骤需要注意一下,其是 在meminfo中有配置内存tag则跳过对内存tag的处理的一个跳转函数,防止从复处理。
背景
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
,从而完成最终的虚拟地址和物理地址的映射。
fix mapped基本功能
在系统启动的汇编阶段,我们建立了临时页表,开启了MMU,进入了虚拟空间的世界,进入到start_kernel,内核要访问内存,要访问IO地址空间,那么就必须要位物理地址建立页表,以实现物理地址和虚拟地址之间的映射。
在内核初始化前期,内存管理系统还未初始化,现在除了临时页表外,主要的还是kernel image空间,其余的物理内存都没有建立页表,那么对于内存管理相关的API接口都无法使用,内核提出了fix mapped address的概念用来解决这些问题,本文主要针对ARM(IMX6ull)体系结构进行分析。
1. fixmap概念
当我们通过创建页表后,开启MMU,进入到start_kernel的世界中,那么当内存管理子系统没有完全初始化成功时候,我们所面对的困难为:
我们无法访问所有的内存,只能访问到临时页表创建的kernel image附件的地址空间
我们无法访问任何的硬件,这些硬件对应的地址空间还没有完成映射关系
所以内核进入start_kernel就马上完成fixmap的建立,对于fixmap从字面的意思来说,fixed map指的是虚拟地址中的一段区域,在该区域中所有的线性地址是在编译阶段就确定好的,这个虚拟地址需要在boot阶段去映射到物理地址上,对于NXP芯片,其虚拟地址为
[ 0.000000] Virtual kernel memory layout:
[ 0.000000] vector : 0xffff0000 - 0xffff1000 ( 4 kB)
[ 0.000000] fixmap : 0xffc00000 - 0xfff00000 (3072 kB)
[ 0.000000] vmalloc : 0xe0800000 - 0xff800000 ( 496 MB)
[ 0.000000] lowmem : 0xc0000000 - 0xe0000000 ( 512 MB)
[ 0.000000] pkmap : 0xbfe00000 - 0xc0000000 ( 2 MB)
[ 0.000000] modules : 0xbf000000 - 0xbfe00000 ( 14 MB)
[ 0.000000] .text : 0xc0008000 - 0xc0a00000 (10208 kB)
[ 0.000000] .init : 0xc0e00000 - 0xc1000000 (2048 kB)
[ 0.000000] .data : 0xc1000000 - 0xc1074c00 ( 467 kB)
[ 0.000000] .bss : 0xc1076000 - 0xc10e8eec ( 460 kB)
1
2
3
4
5
6
7
8
9
10
11
当内核完全启动后,内存管理提供了各种各样的API来使各个模块完成物理地址到虚拟地址的映射功能,但是在内核启动的初期,有些模块就需要使用虚拟地址并mapping到指定的物理地址上,同时,这些模块也没有办法等到内核的内存管理模块完全初始化之后再进行映射功能,因此,内核就分配了fixmap这段地址空间,对于ARM32的为0xffc00000 - 0xfff00000这段虚拟地址空间,这段地址空间就用来实现前期某些特定的模块实现物理内存映射。
fixmap虚拟地址空间映射方式分为以下两部分
永久映射,即建立的映射关系再kernel阶段不会改变,仅供特定模块使用
临时映射,即模块使用前创建映射,使用后解除映射关系
其fixmap地址又被按功能划分成几个更小的部分,每一部分都有特定的功能,其定义如下:
enum fixed_addresses {
FIX_EARLYCON_MEM_BASE,
__end_of_permanent_fixed_addresses,
FIX_KMAP_BEGIN = __end_of_permanent_fixed_addresses,
FIX_KMAP_END = FIX_KMAP_BEGIN + (KM_TYPE_NR * NR_CPUS) - 1,
/* Support writing RO kernel text via kprobes, jump labels, etc. */
FIX_TEXT_POKE0,
FIX_TEXT_POKE1,
__end_of_fixmap_region,
/*
* Share the kmap() region with early_ioremap(): this is guaranteed
* not to clash since early_ioremap() is only available before
* paging_init(), and kmap() only after.
*/
#define NR_FIX_BTMAPS 32
#define FIX_BTMAPS_SLOTS 7
#define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS)
FIX_BTMAP_END = __end_of_permanent_fixed_addresses,
FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1,
__end_of_early_ioremap_region
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
FIX_EARLYCON_MEM_BASE:earlycon虚拟地址空间,对于实际的应用的场景就是串口相关的IO寄存器即可,占用的空间是很小的
FIX_KMAP_BEGIN:永久内存映射kmap
FIX_TEXT_POKE0/FIX_TEXT_POKE1:特定操作的映射区,暂时不清楚用途
FIX_BTMAP_BEGIN:BITMAP空间:从定义上看,应该也属于永久映射区,后面代码中会用到。
2. fixmap的初始化
在执行setup_arch中,会最先进行early_fixmap_init()来填充fixmap,其代码实现如下
void __init early_fixmap_init(void)
{
pmd_t *pmd;
/*
* The early fixmap range spans multiple pmds, for which
* we are not prepared:
*/
BUILD_BUG_ON((__fix_to_virt(__end_of_early_ioremap_region) >> PMD_SHIFT) -----------(1)
!= FIXADDR_TOP >> PMD_SHIFT);
pmd = fixmap_pmd(FIXADDR_TOP); -----------(2)
pmd_populate_kernel(&init_mm, pmd, bm_pte); -----------(3)
pte_offset_fixmap = pte_offset_early_fixmap; -----------(4)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
检查__end_of_early_ioremap_region的范围
FIXADDR_TOP的地址为0xfff00000UL-4K=ffeff000,通过pgd_offset_k(addr),获得FIXADDR_TOP地址对应pgd全局页表项中的entry,而后通过pud_offset找到对应的pud的页目录中的entry,最后通过pmd_offset找到对应的pmd的页表项
将pmd的物理地址写到pte页表中,而 bm_pte[512]存放pte的页表的entry,对于pmd_populate_kernel有三个重要的参数
init_mm : init进程的内存描述符
pmd: ioremap固定映射开始处的页中间目录
bm_pte:初期ioremap页表入口数组定义bm_pte[512]
early_fixmap_init只是建立了一个映射的框架,具体的物理地址和虚拟地址的映射没有直接给出,这个是由使用者具体使用的时候再去填充的对应的pte_entry。
3. ioremap初始化
如果你希望kernel启动早期使用ioremap操作,其实是不行的。我们必须借助early ioremap接口。early ioremap是基于fixmap实现。初始化在early_ioremap_init完成,其代码如下
void __init early_ioremap_init(void)
{
early_ioremap_setup();
}
void __init early_ioremap_setup(void)
{
int i;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
if (WARN_ON(prev_map[i]))
break;
for (i = 0; i < FIX_BTMAPS_SLOTS; i++)
slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
slot_virt` 和其他数组定义在同一个源文件中:
static void __iomem *prev_map[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long prev_size[FIX_BTMAPS_SLOTS] __initdata;
static unsigned long slot_virt[FIX_BTMAPS_SLOTS] __initdata;
1
2
3
slot_virt 包含了固定映射区域的虚拟地址,prev_map 数组包含了初期 ioremap 区域的地址,实际上初期 ioremap 会使用 512 个临时引导时映射,同时你可以看到所有的数组都使用 __initdata 定义,这意味着这些内存都会在内核初始化结束后释放掉。
初期 ioremap 初始化完成后,我们就能使用它了。它提供了两个函数:
early_ioremap
early_iounmap
用于从IO物理地址映射/解除映射到虚拟地址,这两个函数都依赖于CONFIG_MMU,如果该宏没有定义,就直接返回物理地址,什么都不做;如果定义为y,early_ioremap 就会调用 __early_ioremap,它有三个参数:
phys_addr - 要映射到虚拟地址上的 I/O 内存区域的基物理地址
size - I/O 内存区域的尺寸
prot - 页表入口位
在 __early_ioremap 中我们首先遍历了所有初期 ioremap 固定映射槽并检查 prev_map 数组中第一个空闲元素,然后将这个值存在了 slot 变量中,计算需要映射的物理地址结尾 ,同时将映射的size填入到相应slot的prev_size中
for (i = 0; i < FIX_BTMAPS_SLOTS; i++) {
if (!prev_map[i]) {
slot = i;
break;
}
}
if (WARN(slot < 0, "%s(%08llx, %08lx) not found slot\n",
__func__, (u64)phys_addr, size))
return NULL;
/* Don't allow wraparound or zero size */
last_addr = phys_addr + size - 1;
if (WARN_ON(!size || last_addr < phys_addr))
return NULL;
prev_size[slot] = size;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接下来,我们会看到,根据起始物理地址的值计算其偏移量,将phys_addr的最后12位清0,也相当于页对齐操作,然后计算对齐后的size的值。
offset = offset_in_page(phys_addr);
phys_addr &= PAGE_MASK;
size = PAGE_ALIGN(last_addr + 1) - phys_addr;
1
2
3
接下来就需要获取新的ioremap区域所占用的页的数量然后计算固定映射的下标
nrpages = size >> PAGE_SHIFT;
if (WARN_ON(nrpages > NR_FIX_BTMAPS))
return NULL;
1
2
3
现在我们用给定的物理地址填充固定映射区域,逐页操作,在bm_pte页表中设置该地址对应entry的值
idx = FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*slot;
while (nrpages > 0) {
if (after_paging_init)
__late_set_fixmap(idx, phys_addr, prot);
else
__early_set_fixmap(idx, phys_addr, prot);
phys_addr += PAGE_SIZE;
--idx;
--nrpages;
}
prev_map[slot] = (void __iomem *)(offset + slot_virt[slot]);
return prev_map[slot];
1
2
3
4
5
6
7
8
9
10
11
12
将其其实地址的虚拟地址保持到prev_map相对应的slot中,并将该虚拟地址返回,那么就可以通过这个函数实现物理地址到虚拟地址的映射。
对于 early_iounmap ,它会解除对一个 I/O 内存区域的映射。这个函数有两个参数:基地址和 I/O 区域的大小,这看起来与 early_ioremap 很像。它同样遍历了固定映射槽并寻找给定地址的槽。这样它就获得了这个固定映射槽的下标,然后通过判断 after_paging_init 的值决定是调用 __late_clear_fixmap 还是 __early_set_fixmap 。当这个值是 0 时会调用 __early_set_fixmap。最终它会将 I/O 内存区域设为 NULL:
4. 固定映射
内核使用set_fixmap(idx,phys)和set_fixmap_nocache(idx, phys)来建立固定线性地址与物理地址的映射。通过clear_fixmap(idx)解除固定线性地址的映射,其代码流程为
void __set_fixmap(enum fixed_addresses idx, phys_addr_t phys, pgprot_t prot)
{
unsigned long vaddr = __fix_to_virt(idx); --------------(1)
pte_t *pte = pte_offset_fixmap(pmd_off_k(vaddr), vaddr);
/* Make sure fixmap region does not exceed available allocation. */
BUILD_BUG_ON(FIXADDR_START + (__end_of_fixed_addresses * PAGE_SIZE) > --------------(2)
FIXADDR_END);
BUG_ON(idx >= __end_of_fixed_addresses);
if (pgprot_val(prot)) --------------(3)
set_pte_at(NULL, vaddr, pte,
pfn_pte(phys >> PAGE_SHIFT, prot));
else
pte_clear(NULL, vaddr, pte);
local_flush_tlb_kernel_range(vaddr, vaddr + PAGE_SIZE);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
对于ARM32,只是对于earlycon才用了这种方式,set_fixmap_io(FIX_EARLYCON_MEM_BASE, paddr & PAGE_MASK);
5. 总结
对于fixmap地址区域分为两部分,一部分是临时映射,各个内核模块可以使用,用完之后就释放了,典型的应用场景是early ioremap模块,linux在fix map区域的虚拟地址空间打开FIX_BTMAPS_SLOTS个的slot,内核模块通过对应的接口来申请或者示范对某个slot虚拟地址的使用,这部分还有KMAP,后面章节在分析。另外一部分是永久映射,这部分主要应用场景early console模块。