《Linux操作系统-系统移植》第4章 内核移植 - 内核解析(Linux3.14)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013162035/article/details/79200907

4.1内核配置过程分析

4.1.1前言

1、linux内核五大功能:
进程管理(cpu)
内存管理(内存)
设备管理(驱动)
网络管理(网络协议tcp/ip)
文件系统(vfs)

2、linux版本号:
linux 2.6 2—>主版本号 6---->次版本号:奇数代表测试版本 偶数代表稳定版本
本文使用内核版本为linux3.14

3、linux目录结构:
arch:体系结构相关代码
ipc:进程调度相关代码
mm:内存管理
Documentation:帮助文档
net:网络协议
lib:库
scripts:编译相关脚本工具
tools:编译相关工具
drivers:设备驱动
fs:文件系统实现

4、编译相关文件:
顶层Makefile 与 子目录下的Makefile
各级目录Kconfig

5、内核的使用流程
a、清除命令
一般在第一次编译时使用
make mrproper
b、导入默认配置信息(在内核根目录中)
方法1:make exynos_deconfig
方法2:cp arch/arm/configs/exynos_deconfig .config
c、详细配置
make menuconfig
d、编译
make uImage —生成内核镜像 /arch/arm/boot/uImag
make dtbs —生成设备树文件 /arch/arm/boot/dtb/exynos4412-fs4412.dtb
make modules —把配置值选成M的代码编译生成模块文件。(.ko) 放在对应的源码目录下。
可以看出,内核编译主要包括两部分:一部分是内核配置;另一部分是内核编译;我们先从内核配置开始。

4.1.2内核的Kconfig分析

a – 我们解压内核后需要先修改内核顶层目录下的Makefile,配置好交叉编译工具
这里写图片描述

b – 然后导入默认配置(使用make exynis_defconfig 或者
cp arch/arm/configs/exynos_deconfig .config)
可以看到该命令会将 配置信息写入 .config 中,.config 是内核根目录下的隐藏文件,makefile 会根据它里面的内容进行编译。

c – 配置内核
使用 make menuconfig 命令进行内核配置(窗口太小打不开,窗口要19行 80列),可能会出现下面的报错:
使用 sudo apt-get install libncurses5-dev命令安装一下接可以了,安装以后再使用 make menuconfig,便可以看到下面的界面了。

这里写图片描述

图1

如上图所示,这就是内核的配置界面。通过配置界面,可以选择芯片类型、选择需要支持的文件系统。去除不需要的选项等,这就称为“配置内核”。注意,也有其他形式的配置界面,比如“make config”命令启动字符配置界面,对于每个选项都会依次出现一行提示信息,组个回答;“make xconfig”命令启动 X-Windows 图形配置界面。

所有配置工具都是读取arch/$(ARCH)/Kconnfig 文件来生成配置界面,这个文件是所有配置文件的总入口,它会包含其他目录的Kconfig文件。配置界面如上图所示。

内核源码每个子目录中,都有一个Makefile文件和Kconfig文件。Makefile的作用下篇文章会讲述,Kconfig用于配置内核,它就是各种配置界面的源文件。内核的配置工具读取各个Kconfig文件,生成配置界面供开发人员配置内核,最后生成配置文件.config。

内核的配置界面以树状的菜单形式组织,主菜单下有若干子菜单,子菜单下又有子菜单或配置选项。每个子菜单或选项都有依赖关系,这些依赖关系用来确定它们是否显示。只有被依赖的父项已经被选中,子项才会显示。

下面学习Kconfig文件的简单语法:
1、Kconfig 文件的基本要素:config 条目(entry)
config 条目常被其他条目包含,用来生成菜单,进行多项选择等。
config 条目用来配置一个选项,或者这么说,它用于生成一个变量,这个变量会连同它的值一起被写入配置文件 .config 中。比如有一个config 条目用来配置CONFIG_LEDS_FS4412,根据用户的选择,.config文件中可能出现下面3种配置结果中的一个。

CONFIG_LEDS_FS4412=y   # 对应的文件被编进内核
CONFIG_LEDS_FS4412=m   # 对应的文件被编成模块
#CONFIG_LEDS_FS4412   # 对应的文件没有被使用

以一个例子说明config条目格式,下面代码选自 drviers/char/Kconfig 文件,它用于配置CONFIG_TTY_PRINTK选项
这里写图片描述
代码中包含了大部分元素,下面一一说明。
第42行,config是关键字,便是一个配置选项的开始;紧跟着的TTY_PRINTK是配置选项的名称,省略了前缀“CONFIG_”。
第43行,boot表示变量类型,即CONFIG_TTY_PRINTK的类型,有五种类型:
Bool 布尔类型,结果是Y[] , N[]
Tristate 三项选择,结果是Y[
], N[], M[m]
String 字符串, 结果(arm-linux-)
Hex 十六进制,(16)
Int 十进制,(10)
“bool” 之后的字符串提示信息,在配置界面中上下移动光标选中它时,可以通过按空格或回车键来设置CONFIG_TTY_PRINTK的值。
第44行,表示依赖关系,格式如下。只有EXPERT && TTY 配置选项被选中时,当前配置选项的提示信息才会出现,才能设置当前配置选项。注意,如果依赖条件不满足,则它取默认值。
第45行表示默认值为n;
第46行表示下面几行是帮助信息;

2、menu 条目
menu条目用于生成菜单, 在menuconfig 中是以------>形式出现 格式如下:
“menu” < prompt>
< menu options>
< menu block>
“endmenu”
它的实际使用并不如它的标准格式那样复杂,下面是一个例子
menu之后字符串是菜单名,“menu”和“endmunu”之间有很多config条目。在配置界面上回出现如下字样的菜单,移动光标选中它后按回车键进入,就会看到这些config条目定义的配置选项。

3、Source条目
xx/kconfig :当前Kconfig可以包含其它目录下的kconfig,用于读入另一个Kconfig文件,格式如下:
“source” < prompt>

4、Choice
choice条目将多个类似的配置选项组合在一起,供用户单选或多选,格式如下:
“choice”
< choice options>
< choice block>
“end choice”
实际使用中,也是在“choice”和“endchoice”之间定义多个config条目,比如arch/arm/Kconfig中有如下代码:
这里写图片描述

4.2内核编译过程分析

4.2.1 Linux Makefile分析

内核中的哪些文件将被编译?它们是怎样被编译的?它们连接时的顺序如何确定?那个文件在最前面?哪些文件或函数先执行?这些都是通过Makefile 来管理的。从最简单的角度来总结Makefile的作用,有以下3点:
1)-- 决定编译哪些文件;
2)-- 怎样编译这些文件?
3)-- 怎样连接这些文件,最重要的是它们的顺序如何?

Linux 内核源码中含有很多个Makefile文件,这些Makefile文件又要包含其他一些文件(比如配置信息、通用的规则等)。这些文件构成了 Linux 的Makefile 体系,可以分为下表中的5类:

表1

这里写图片描述

以下根据见面总结的Makefile 的3大作用分析这5类文件。

1、决定编译哪些文件
Linux内核的编译过程从顶层Makefile开始,然后递归地进入各级子目录调用他们的makefile,分为3个步骤:
【1】顶层Makefile 决定内核根目录下哪些子目录将被编进内核;
【2】arch/(ARCH)/Makefilearch/(ARCH)/Makefile 决定arch/(ARCH)目录下哪些文件、哪些目录将被编进内核;
【3】各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被编程模块(即驱动程序),进入哪些子目录继续调用它们的Makefile。
a – 先看步骤a, 顶层Makefile的编译
在顶层Makefile 中可以看到如下内容:
这里写图片描述

可见,顶层Makefile将这14个子目录分为5类:init-y、divers-y、net-y、libs-y和core-y。我们可以看到,最重要的arch目录没有出现在内核中。它在arch/$(ARCH)/Makefile中被包含进内核,在顶层Makefile中直接包含了这个Makefile,如下所示:

这里写图片描述

对于ARCH变量,可以在执行make时传入,比如“make ARCH=arm …”。另外,对于非X86平台,还需要指定交叉编译工具,这也可以在执行make 命令时传入,比如“make CROSS_COPILE=arm-linux- …”。为了方便,常在顶层Makefile中进行如下修改。这样执行make时就会将ARCH变量传入。
这里写图片描述

b – 看步骤 b,arch/(ARCH)/Makefilebarch/(ARCH)/Makefile的编译 对于步骤 b 的 arch/(ARCH)/Makefile,以ARM为例,在arch/arm/Makefile 中可以看到如下内容:
这里写图片描述

从129行可知,除了刚面的5类子目录外,又出现了一类:head-y,不过它直接以文件名出现;
arch/arm/Makefile 中类似第268-273行的代码进一步扩展了core-y的内容,第276行扩展了libs-y的内容,这些都是体系相关的目录;第261-265行中的CONFIG_在配置内核时定义,它的值有3种:y、m或空。y表示编进内核,m表示编为模块,空表示不使用;

编译内核时,将依次进入init-y、core-y、libs-y、drivers-y和net-y 所列出的目录中执行它们的Makefile,每个子目录都会生成一个 built-in.o(libs-y所列目录下,有可能生成lib.a文件)。最后,head-y所表示的文件将和在这些built-in.o、lib.a 一起被连接成内核映像文件 vmlinux。

c – 步骤 c 是如何进行的,各级子目录下的Makefile的编译
在配置内核时,生成配置文件.config。内核顶层Makefile使用如下语句间接包含.config 文件,以后就根据.config中定义的各个变量决定编译哪些文件。值所以说是“间接”包含,试音为包含的是include/config/auto.conf 文件,而它只是将.config文件中的注释去掉,并根据顶层Makefile中定义的变量增加一些变量而已。

2、怎样编译这些文件
即编译选项、连接选项是什么。这些选项分3类:全局的,适用于整个内核代码树;局部的,仅适用于某个Makefile中的所有文件;个体的,仅适用于某个文件。

全局选项在顶层Makefile和arch/$(ARCH)/Makefile 中定义,这些选项的名称为:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS,他们分别是编译C文件的选项、编译汇编问价你的选项、连接文件的选项、制作库文件的选项。

3、怎样连接这些文件,它们顺序如何
前面分析有哪些文件要编进内核时,顶层Makefile和arch/$(ARCH)/Makefile定义了6类目录(或文件):head-y、init-y、drivers、libs-y 和 core-y。它们的初始值如下(以ARM体系为例)

arch/arm/Makefile 中:
这里写图片描述

顶层makefile 中:
这里写图片描述

可见,除head-y 外,其余的init-y、drivers-y等都是目录名。在顶层Makefile 中,这IE目录名的后面直接加上built-in.o 或 lib.a,表示要连接进内核的文件。
这里写图片描述

上面的patubst是个字符串处理函数,它的用法如下:
(patsubst pattern, replacement,text)
表示寻找“text”中符合格式“pattern”的字,用”replacement"替换它们。比如上面的init-y初值为“init/”,经过793行的交互后,“init-y” 变为“init/built - in.o”。
顶层Makefile中,再往下看:

这里写图片描述

对于ARM体系,连接脚本就是arch/arm/kernel/vmlinux.lds,它由 arch/arm/kernel/vmlinux/lds.S文件生成,先将生成的arch/arm/kernel/vmlinux.lds摘录如下:
总结:

a – 配置文件.config 中定义了一系列的变量,Makefile将结合它们来决定哪些文件被编进内核、哪些文件被编进模块、涉及哪些子目录;

b – 顶层Makefile和arch/(ARCH)/Makefilearch/(ARCH)/Makefile决定根目录下哪些子目录、arch/(ARCH) 目录下哪些文件和目录将被编进内核;

c – 最后,各级子目录下的Makefile决定所在目录下哪些文件将被编进内核,哪些文件将被变成模块(即驱动程序),进入哪些子目录继续调用它们的Makefile;

d – 顶层Nakedfile和arch/$(ARCH)/Makefile设置了可以影响所有文件的编译、连接选项:CFLAGS、AFLAGS、LDFLAGS、ARFLAGS;

e – 顶层Makefile按照一定的顺序组织文件,根据连接脚本 arch/$(ARCH)/kernel/vmlinux.lds生成内核映像文件vmlinux。

4.3内核启动过程分析

内核启动所用函数如下:

这里写图片描述

图2

与移植U-Boot 的过程相似,在移植Linux 之前,先了解它的启动过程。Linux 的过程可以分为两部分:架构/开发板相关的引导过程、后续的通用启动过程。对于uImage、zImage ,它们首先进行自解压得到vmlinux ,然后执行 vmlinux 开始“正常的”启动流程。

引导阶段通常使用汇编语言编写,它首先检查内核是否支持当前架构的处理器,然后检查是否支持当前开发板。通过检查后,就为调用下一阶段的start_kernel函数作准备了。这主要分如下两个步骤:
1)-- 连接内核时使用的虚拟地址,所以要设置页表、使能MMU;
2)调用C 函数 start_kernel 之前的常规工作,包括复制数据段、清除BSS段、调用start_kernel 函数。
第二阶段的关键代码主要使用C语言编写。它进行内核初始化的全部工作,最后调用 rest_init 函数启动init 过程,创建系统第一个进程:init 进程。在第二阶段,仍有部分架构/开发板相关的代码,比如重新设置页表、设置系统时钟、初始化串口等。
与Uboot 一样,我们在连接文件中查看函数入口点,内核编译完成后会在arch/arm/kernel/下生成 vmlinux.lds 文件,打开:
这里写图片描述
这里写图片描述
这里写图片描述

stext 在 linux/arch/arm/kernel/head.S 中被定义,做为函数入口点,/arch/arm/kernel/head.S是linux内核映像解压后执行的第一个文件。
这里写图片描述
代码只是部分,但可以看到这一阶段究竟做了些什么:
a – 设定为SVC模式,关闭IRQ、FIQ;
b – 确定CPU的ID号,判定其是否有效;
c – 确定machine的ID号,检查合法性;
d – 检查bootloader传入的参数列表atags的合法性
e – 创建初始页表

下面对上面遇到的程序段展开分析:
a – 确保处于SVC模式
这里写图片描述
这没什么好讲的,就是设置CPSR 模式位,并屏蔽中断;
b – 检查CPU ID 是否匹配
这里写图片描述
获取ID并放到 r9 寄存器中,调用_lookup_processor_type 函数, 函数主要用来判定内核是否和当前的CPU匹配,如果不匹配,r5寄存器的值应为0,此时会调用 _error_p函数,它用来打印错误信息,即内核和当前的CPU不匹配,此时内核时不能启动的;如果两者匹配,会返回一个描述处理器结构的地址(在r5寄存器中),然后调用下面的函数。

下面看_lookup_processor_type 函数,在arch/arm/kernel/head-common.S 中定义:
这里写图片描述
这里写图片描述

上面的代码其实就是一个地址转换过程,因为在判定CPU架构时未开启系统的MMU功能,所以均使用物理地址,而内核代码在连接时是以虚拟地址来实现的,因此要想用proc_info_list 结构体,就要先找到proc_info_list 结构的物理地址,这样必须使用上面的转换代码。
proc_info_list 结构体很重要。在Linux 内核映像中定义了很多个proc_info_list 结构,该结构表示的是内核所支持的CPU架构,这部分下面会讲到,先分析上面的代码:
153 行:r3 存储的是_lookup_processor_type_data 的物理地址 ;
155 行:得到虚拟地址和物理地址之间的offset;
156 - 157 行:利用offset,将 r5 和 r6 中保存的虚拟地址转变为物理地址,主要是获得_proc_info_begin 及_proc_info_end 的物理地址,分别放到r5 和 r6 中;
159 行:r9 中存放的是先前读出的 processor ID,此处屏蔽不需要的位;
160 行:查看代码和CPU硬件是否匹配,如果匹配就返回,此时 r5 存放的是该CPU类型对应的结构体_proc_info_list 的基地址 ;不成功,则查看下一个 proc_info_list 结构体;
163行:如果直到 _proc_info_end ,都没有匹配,则定为未知CPU,向 r5 赋 0,然后返回 ;

下面来看看 proc_info_list 结构体 ,这个结构体在 arch/arm/include/asm/procinfo.h 中定义:
这里写图片描述
对于 Cortex-A9 来说,其结构体在文件 arch/arm/mm/proc-v7.S 中初始化:
这里写图片描述
.section ".proc.info.init"表明了该结构在编译后存放的位置。在链接文件arch/arm/kernel/vmlinux.lds中:

__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;

上面两个变量 _proc_info_begin 与 _proc_info_end 用于计算 proc_info_list 结构的物理地址。
如果CPU ID匹配,在编译内核文件时,会编译 proc-v7.S 这个文件,可以在arch/arm/mm/Makefile 中看到这个文件。
这里写图片描述

c – 检测 机器ID是否匹配
主要用到_lookup_machine_type 函数,其与_lookup_processor_type 函数实现代码很相似。

d – 检查bootloader传入的参数列表atags的合法性
这里写图片描述

_vet_atags 函数用于检测参数列表atags的合法性
内核参数链表的格式和说明可以从内核源代码目录树中的 中找到,参数链表必须以ATAG_CORE 开始,以ATAG_NONE结束。这里的 ATAG_CORE,ATAG_NONE是各个参数的标记,本身是一个32位值,例如:ATAG_CORE=0x54410001。其它的参数标记还包括: ATAG_MEM32 , ATAG_INITRD , ATAG_RAMDISK ,ATAG_COMDLINE 等。每个参数标记就代表一个参数结构体,由各个参数结构体构成了参数链表。参数结构体的定义如下:

struct tag {  
      struct  tag_header  hdr;  
      union {  
struct tag_core  core;  
struct tag_mem32   mem;  
struct tag_videotext videotext;  
struct tag_ramdisk  ramdisk;  
struct tag_initrd     initrd;  
struct tag_serialnr     serialnr;  
struct tag_revision  revision;  
struct tag_videolfb  videolfb;  
struct tag_cmdline  cmdline;  
struct tag_acorn       acorn;  
struct tag_memclk    memclk;  
        } u;  
};  

参数结构体包括两个部分,一个是 tag_header结构体,一个是u联合体。
tag_header结构体的定义如下:

   struct tag_header { 
                 u32 size;   
                 u32 tag; 
}; 

其中 size:表示整个 tag 结构体的大小(用字的个数来表示,而不是字节的个数),等于tag_header的大小加上 u联合体的大小,例如,参数结构体 ATAG_CORE 的 size=(sizeof(tag->tag_header)+sizeof(tag->u.core))>>2,一般通过函数 tag_size(struct * tag_xxx)来获得每个参数结构体的 size。其中 tag:表示整个 tag 结构体的标记,如:ATAG_CORE等。

__vet_atags:  
tst r2, #0x3 //r2指向该参数链表的起始位置,此处判断它是否字对齐  
bne 1f  
ldr r5, [r2, #0] //获取第一个tag结构的size  
//#define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2) 判断该tag的长度是否合法  
subs r5, r5, #ATAG_CORE_SIZE  
bne 1f  
ldr r5, [r2, #4]  //获取第一个tag结构体的标记,  
ldr r6, =ATAG_CORE   
cmp r5, r6 //判断第一个tag结构体的标记是不是ATAG_CORE  
bne 1f    
mov pc, lr //正常退出  
1: mov r2, #0  
mov pc, lr  //参数连表不正确  
ENDPROC(__vet_atags)  

e – 创建初始页表
这里写图片描述

其在下面被执行:
这里写图片描述

下面是详细分析:

/* 
 * Setup the initial page tables.  We only setup the barest 
 * amount which are required to get the kernel running, which 
 * generally means mapping in the kernel code. 
 * 
 * r8 = phys_offset, r9 = cpuid, r10 = procinfo 
 * 
 * Returns: 
 *  r0, r3, r5-r7 corrupted 
 *  r4 = page table (see ARCH_PGD_SHIFT in asm/memory.h) 
 */  
__create_page_tables:  
    pgtbl   r4, r8              @ page table address  
  
    /* 
     * Clear the swapper page table 
     */  
    mov r0, r4  
    mov r3, #0  
    add r6, r0, #PG_DIR_SIZE  
1:  str r3, [r0], #4  
    str r3, [r0], #4  
    str r3, [r0], #4  
    str r3, [r0], #4  
    teq r0, r6  
    bne 1b  
  
#ifdef CONFIG_ARM_LPAE  
    /* 
     * Build the PGD table (first level) to point to the PMD table. A PGD 
     * entry is 64-bit wide. 
     */  
    mov r0, r4  
    add r3, r4, #0x1000         @ first PMD table address  
    orr r3, r3, #3          @ PGD block type  
    mov r6, #4              @ PTRS_PER_PGD  
    mov r7, #1 << (55 - 32)       @ L_PGD_SWAPPER  
1:  
#ifdef CONFIG_CPU_ENDIAN_BE8  
    str r7, [r0], #4            @ set top PGD entry bits  
    str r3, [r0], #4            @ set bottom PGD entry bits  
#else  
    str r3, [r0], #4            @ set bottom PGD entry bits  
    str r7, [r0], #4            @ set top PGD entry bits  
#endif  
    add r3, r3, #0x1000         @ next PMD table  
    subs    r6, r6, #1  
    bne 1b  
  
    add r4, r4, #0x1000         @ point to the PMD tables  
#ifdef CONFIG_CPU_ENDIAN_BE8  
    add r4, r4, #4          @ we only write the bottom word  
#endif  
#endif  
  
    ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags  
  
    /* 
     * Create identity mapping to cater for __enable_mmu. 
     * This identity mapping will be removed by paging_init(). 
     */  
    adr r0, __turn_mmu_on_loc  
    ldmia   r0, {r3, r5, r6}  
    sub r0, r0, r3          @ virt->phys offset  
    add r5, r5, r0          @ phys __turn_mmu_on  
    add r6, r6, r0          @ phys __turn_mmu_on_end  
    mov r5, r5, lsr #SECTION_SHIFT  
    mov r6, r6, lsr #SECTION_SHIFT  
  
1:  orr r3, r7, r5, lsl #SECTION_SHIFT  @ flags + kernel base  
    str r3, [r4, r5, lsl #PMD_ORDER]    @ identity mapping  
    cmp r5, r6  
    addlo   r5, r5, #1          @ next section  
    blo 1b  
  
    /* 
     * Map our RAM from the start to the end of the kernel .bss section. 
     */  
    add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)  
    ldr r6, =(_end - 1)  
    orr r3, r8, r7  
    add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)  
1:  str r3, [r0], #1 << PMD_ORDER  
    add r3, r3, #1 << SECTION_SHIFT  
    cmp r0, r6  
    bls 1b  
  
#ifdef CONFIG_XIP_KERNEL  
    /* 
     * Map the kernel image separately as it is not located in RAM. 
     */  
#define XIP_START XIP_VIRT_ADDR(CONFIG_XIP_PHYS_ADDR)  
    mov r3, pc  
    mov r3, r3, lsr #SECTION_SHIFT  
    orr r3, r7, r3, lsl #SECTION_SHIFT  
    add r0, r4,  #(XIP_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)  
    str r3, [r0, #((XIP_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!  
    ldr r6, =(_edata_loc - 1)  
    add r0, r0, #1 << PMD_ORDER  
    add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)  
1:  cmp r0, r6  
    add r3, r3, #1 << SECTION_SHIFT  
    strls   r3, [r0], #1 << PMD_ORDER  
    bls 1b  
#endif  
  
    /* 
     * Then map boot params address in r2 if specified. 
     * We map 2 sections in case the ATAGs/DTB crosses a section boundary. 
     */  
    mov r0, r2, lsr #SECTION_SHIFT  
    movs    r0, r0, lsl #SECTION_SHIFT  
    subne   r3, r0, r8  
    addne   r3, r3, #PAGE_OFFSET  
    addne   r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)  
    orrne   r6, r7, r0  
    strne   r6, [r3], #1 << PMD_ORDER  
    addne   r6, r6, #1 << SECTION_SHIFT  
    strne   r6, [r3]  
  
#if defined(CONFIG_ARM_LPAE) && defined(CONFIG_CPU_ENDIAN_BE8)  
    sub r4, r4, #4          @ Fixup page table pointer  
                        @ for 64-bit descriptors  
#endif  
  
#ifdef CONFIG_DEBUG_LL  
#if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING)  
    /* 
     * Map in IO space for serial debugging. 
     * This allows debug messages to be output 
     * via a serial console before paging_init. 
     */  
    addruart r7, r3, r0  
  
    mov r3, r3, lsr #SECTION_SHIFT  
    mov r3, r3, lsl #PMD_ORDER  
  
    add r0, r4, r3  
    mov r3, r7, lsr #SECTION_SHIFT  
    ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags  
    orr r3, r7, r3, lsl #SECTION_SHIFT  
#ifdef CONFIG_ARM_LPAE  
    mov r7, #1 << (54 - 32)       @ XN  
#ifdef CONFIG_CPU_ENDIAN_BE8  
    str r7, [r0], #4  
    str r3, [r0], #4  
#else  
    str r3, [r0], #4  
    str r7, [r0], #4  
#endif  
#else  
    orr r3, r3, #PMD_SECT_XN  
    str r3, [r0], #4  
#endif  
  
#else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */  
    /* we don't need any serial debugging mappings */  
    ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags  
#endif  
  
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)  
    /* 
     * If we're using the NetWinder or CATS, we also need to map 
     * in the 16550-type serial port for the debug messages 
     */  
    add r0, r4, #0xff000000 >> (SECTION_SHIFT - PMD_ORDER)  
    orr r3, r7, #0x7c000000  
    str r3, [r0]  
#endif  
#ifdef CONFIG_ARCH_RPC  
    /* 
     * Map in screen at 0x02000000 & SCREEN2_BASE 
     * Similar reasons here - for debug.  This is 
     * only for Acorn RiscPC architectures. 
     */  
    add r0, r4, #0x02000000 >> (SECTION_SHIFT - PMD_ORDER)  
    orr r3, r7, #0x02000000  
    str r3, [r0]  
    add r0, r4, #0xd8000000 >> (SECTION_SHIFT - PMD_ORDER)  
    str r3, [r0]  
#endif  
#endif  
#ifdef CONFIG_ARM_LPAE  
    sub r4, r4, #0x1000     @ point to the PGD table  
    mov r4, r4, lsr #ARCH_PGD_SHIFT  
#endif  
    mov pc, lr  
ENDPROC(__create_page_tables)  

f – 使能MMU,跳转到start_kernel
这里写图片描述

文件linux/arch/arm/kernel/head.S中
这里写图片描述
这里写图片描述

在前面有过这样的指令操作ldr r13, __switch_data ,
mov pc, r13 就是将跳转到__switch_data处。
在文件linux/arch/arm/kernel/head-common.S中:

.type __switch_data, %object  //定义一个对象  
__switch_data:  
.long __mmap_switched  //由此可知上面程序将跳转到该程序段处。  
.long __data_loc @ r4  
.long _data @ r5  
.long __bss_start @ r6  
.long _end @ r7  
.long processor_id @ r4  
.long __machine_arch_type @ r5  
.long __atags_pointer @ r6  
.long cr_alignment @ r7  
.long init_thread_union + THREAD_START_SP @ sp  
   
   
 . = PAGE_OFFSET + TEXT_OFFSET;  
 #else  
 . = ALIGN(THREAD_SIZE);  
 __data_loc = .;  
 #endif  
   
 .data : AT(__data_loc) {  //此处数据存储在上面__data_loc处。  
 _data = .;  
   
 *(.data.init_task)  
…………………………  
   
.bss : {  
__bss_start = .;  
*(.bss)  
*(COMMON)  
_end = .;  
}  
………………………………  
}  

init_thread_union 是 init进程的基地址. 在 arch/arm/kernel/init_task.c 中:

00033: union thread_union init_thread_union  
00034:         __attribute__((__section__(".init.task"))) =  
00035:                 { INIT_THREAD_INFO(init_task) };          

对照 vmlnux.lds.S 中,我们可以知道init task是存放在 .data 段的开始8k, 并且是THREAD_SIZE(8k)对齐的

__mmap_switched:  
adr r3, __switch_data + 4  
   
ldmia r3!, {r4, r5, r6, r7}    
cmp r4, r5 @ Copy data segment if needed  
1: cmpne r5, r6  //将 __data_loc处数据搬移到_data处  
ldrne fp, [r4], #4  
strne fp, [r5], #4  
bne 1b  

mov fp, #0 //清除BSS段内容  
1: cmp r6, r7      
strcc fp, [r6],#4  
bcc 1b  
   
ldmia r3, {r4, r5, r6, r7, sp}  
str r9, [r4] @ Save processor ID  
str r1, [r5] @ Save machine type  
str r2, [r6] @ Save atags pointer  
bic r4, r0, #CR_A @ Clear 'A' bit  
stmia r7, {r0, r4} @ Save control register values  
b start_kernel  //程序跳转到函数start_kernel进入C语言部分。  
ENDPROC(__mmap_switched)  

4.4 MMU 相关知识解析

4.4.1 MMU的产生

许多年以前,当人们还在使用DOS或是更古老的操作系统的时候,计算机的内存还非常小,一般都是以K为单位进行计算,相应的,当时的程序规模也不大,所以内存容量虽然小,但还是可以容纳当时的程序。但随着图形界面的兴起还用用户需求的不断增大,应用程序的规模也随之膨胀起来,终于一个难题出现在程序员的面前,那就是应用程序太大以至于内存容纳不下该程序,通常解决的办法是把程序分割成许多称为覆盖块(overlay)的片段。覆盖块0首先运行,结束时他将调用另一个覆盖块。虽然覆盖块的交换是由OS完成的,但是必须先由程序员把程序先进行分割,这是一个费时费力的工作,而且相当枯燥。人们必须找到更好的办法从根本上解决这个问题。不久人们找到了一个办法,这就是虚拟存储器(virtual memory).虚拟存储器的基本思想是程序,数据,堆栈的总的大小可以超过物理存储器的大小,操作系统把当前使用的部分保留在内存中,而把其他未被使用的部分保存在磁盘上比如对一个16MB的程序和一个内存只有4MB的机器,OS通过选择,可以决定各个时刻将哪4M的内容保留在内存中,并在需要时在内存和磁盘间交换程序片段,这样就可以把这个16M的程序运行在一个只具有4M内存机器上了。而这个16M的程序在运行前不必由程序员进行分割。

任何时候,计算机上都存在一个程序能够产生的地址集合,我们称之为地址范围。这个范围的大小由CPU的位数决定,例如一个32位的CPU,它的地址范围是0~0xFFFFFFFF (4G)而对于一个64位的CPU,它的地址范围为0~0xFFFFFFFFFFFFFFFF (64T),这个范围就是我们的程序能够产生的地址范围,我们把这个地址范围称为虚拟地址空间,该空间中的某一个地址我们称之为虚拟地址。与虚拟地址空间和虚拟地址相对应的则是物理地址空间和物理地址,大多数时候我们的系统所具备的物理地址空间只是虚拟地址空间的一个子集,这里举一个最简单的例子直观地说明这两者,对于一台内存为256MB的32bit x86主机来说,它的虚拟地址空间范围是00xFFFFFFFF(4G),而物理地址空间范围是0x0000000000x0FFFFFFF(256MB)。

在没有使用虚拟存储器的机器上,虚拟地址被直接送到内存总线上,使具有相同地址的物理存储器被读写。而在使用了虚拟存储器的情况下,虚拟地址不是被直接送到内存地址总线上,而是送到内存管理单元——MMU(主角终于出现了)。他由一个或一组芯片组成,一般存在与协处理器中,其功能是把虚拟地址映射为物理地址。

4.4.2 MMU工作过程

大多数使用虚拟存储器的系统都使用一种称为分页(paging)。虚拟地址空间划分成称为页(page)的单位,而相应的物理地址空间也被进行划分,单位是页框(frame).页和页框的大小必须相同。接下来配合图片我以一个例子说明页与页框之间在MMU的调度下是如何进行映射的:

这里写图片描述

图3

这里提到了一个概念,就是 页表,下面将详细介绍 页表。

4.4.3页表

首先让我们来介绍一个概念-页表(page table)。页表就是存储在内存中的一张表,表中记录了将虚拟地址转换成物理地址的关键信息。MMU正是通过对页表进行查询,实现了地址之间的转换。也就是说,MMU每次工作的时候都要去查这张表,从中找出与虚拟地址相对应的物理地址,然后再进行数据存取。页表的作用如下图所示。

这里写图片描述

图4

页表中的条目被称为页表项(page table entry),一个页表项负责记录一段虚拟地址到物理地址的映射关系,稍后我们会详细介绍。

既然页表是存储在内存中的,那么程序每次完成一次内存读取时都至少会访问内存两次,相比于不使用MMU时的一次内存访问,效率被大大降低了,如果所使用的内存的性能比较差的话,这种效率的降低将会更明显。因此,如何在发挥MMU优势的同时使系统消耗尽量减小,就成为了一个亟待解决的问题。

这里写图片描述

图5

于是,TLB产生了。TLB是什么呢?我们叫它转换旁路缓冲器,它实际上是MMU中临时存放转换数据的一组重定位寄存器。既然TLB本质上是一组寄存器,那么不难理解,相比于访问内存中的页表,访问TLB的速度要快很多。因此如果页表的内容全部存放于TLB中,就可以解决访问效率的问题了。

然而,由于制造成本等诸多限制,所有页表都存储在TLB中几乎是不可能的。这样一来,我们只能通过在有限容量的TLB中存储一部分最常用的页表,从而在一定程度上提高MMU的工作效率。

这一方法能够产生效果的理论依据叫做存储器访问的局部性原理。它的意思是说,程序在执行过程中访问与当前位置临近的代码的概率更高一些。因此,从理论上我们可以说,TLB中存储了当前时间段需要使用的大多数页表项,所以可以在很大程度上提高MMU的运行效率。

让我们接着聊页表。页表是由页表项组成的,每一个页表项都能够将一段虚拟地址空间映射到一段物理地址空间中。这里所谓的这段虚拟地址空间,更专业地讲,应该叫页,一个页对应了页表中的一项,页的大小通常是可选的。在ARM中,一个页可以被配置成1K、4K、64K或1M大小(ARM v6体系以后,不再支持1K大小的页),分别叫做微页、小页、大页和段页。页的大小决定了映射的粒度,是根据实际应用有选择地配置的。以1M为例,按照我们前面的描述,假设系统中将有64M内存需要被映射,那么我们一共需要64M/1M个页表项,而每个页表项需要占据4个字节,也就是说,有256字节的内存要专门负责地址映射,不能用于其他用途。

对于1K、4K和64K大小的页,MMU采用二级查表的方法,即首先由虚拟地址索引出第一张表的某一段内容,然后再根据这段内容搜索第二张表,最后才能确定物理地址。这里的第一张表,我们叫它一级页表,第二张表被称为是二级页表。采用二级查表法的主要目的是减小页表自身占据的内存空间,但缺点是进一步降低了内存的寻址效率。不同大小的页对查表方法的支持程度如表1所示。

表2 不同大小页的查表方法

这里写图片描述

下面,首先来研究一下相对简单的一级查表。
1、一级查表
一级查表只支持大小为1M的页。准确地讲,这里所谓的1M大小的页,应称为段(section)。此时,一级页表也被称为段页表。图6描述了段页表的内存分布情况和段页表项的具体格式。

这里写图片描述

图6 段页表项的结构

段页表中的每一项都类似于上图中的形式,其中:

(1)31~20位段表示物理地址的基地址,一共12位,也就是说,如果我们通过虚拟地址找到某一个段页表项,那么就可以确定这段虚拟地址所对应的物理地址的高12位了。因为段页表项后20位正好可以描述1M的内存,因此基地址每增加一个单位,物理地址就会增加1M空间。所以,我们也可以说该基地址表示了虚拟地址属于哪1M范围的物理地址。由此可知,使用段页表进行地址映射时,每一页能够描述1M的物理地址空间。进一步讲,段页表最多支持1024个页,最多占用系统4K字节的内存来存放页表。

(2)11~10位是AP位,区分了用户模式和特权模式对同一个页的不同访问权限。例如,当AP位为"11"时,表示任何模式下都可以对该空间进行读写,"10"则表示特权模式可读写该页,而用户模式只能读取该页。对AP位详细的描述请参考页权限一节。

(3)8~5位代表该页所属的域。在ARM体系结构中,系统中规定了16个域,因此使用4个位就能表示该页属于哪个域,而每一个域又有各自独立的访问权限,从而实现了初级的存储器保护。

(4)3位和2位分别代表cache和write buffer。相应的位为1则表示被映射的物理地址将使用cache或write buffer。关于cache和write buffer的有关内容,我们稍后会详述。

(5)1~0位。这两位用来区分页表类型,对于段页表,这两位的值总是为"10"。

总的来说,段页表项的内容虽然复杂,但归结起来无非就是两个问题,一是,如何解决某一虚拟地址属于哪段物理地址,二是,如何确定这段地址的访问权限。使用其他形式的页表,本质上也是要解决这两个问题。

现在,假设段页表已经被成功地添加到内存之中了,那么接下来的问题是我们应该怎样通过一个虚拟地址找到与之对应的段页表项呢?找到页表项之后,又是怎样找到对应的物理地址的呢?

很显然,虚拟地址本身就可以解决上述问题。

如下图所示,首先,我们要让MMU知道段页表在内存中的首地址,也就是图中所说的页表基地址,因为段页表是我们通过程序确定的,存储在什么位置程序员自然清楚。然后,在CPU需要寻址的时候,MMU就可以自动地利用页表将虚拟地址映射为物理地址并寻址物理地址,其步骤如下:
(1)MMU取出虚拟地址的前12位作为页表项的偏移,结合页表基地址,找到对应的页表项。具体来说,就是将这12位数取出,然后左移两位和页表基地址相或,就能得到相应页表项的地址了。例如,页表基地址是0x10000000,如果虚拟地址是0x00101000,则前12位数是0x001,那么根据上述步骤将其左移两位,结果为0x004,那么页表项地址就应该是0x10000000|0x004=0x10000004,从该地址中读取的32位的数据即是该虚拟地址所对应的页表项。
(2)找到与虚拟地址对应的页表项之后,就可以从该页表项中读出一个重要信息,如图3-4所示,页表项的前12位定位了该虚拟地址所对应的物理地址在哪个范围内。例如,从0x10000004中读出的页表项的内容为0x30000c12,那么我们就已经知道了,虚拟地址0x00101000对应的物理地址在0x30000000与0x30100000之间。既然地址范围已经清楚了,那么虚拟地址又该定位到该范围的哪个位置呢?这就要靠虚拟地址的后20位了。

这里写图片描述

图7 段页表映射过程

3)MMU将虚拟地址后20位和页表项内容清除掉后20位之后的结果做与的操作,就得到了与虚拟地址对应的实际物理地址了。例如,页表项的内容为0x30000c12,清除掉后20位的结果为0x30000000,虚拟地址的后20位为0x01000,将其和0x30000000相与,结果为0x30001000,这便是最终的物理地址。

当然,MMU在进行地址映射期间,还要进行访问权限的检查,方法是读出页表项的权限位,按照既定规则去检查,如果允许对该地址进行访问则正常访问,如果不允许访问,则抛出异常,通过程序将其捕获并处理。这便是使用段页表时MMU的地址映射过程。

这里写图片描述

图8

示例:viraddr是0xc000 0000 映射到 phyaddr 0x2000 000
这里写图片描述

2、二级页表:
可以让我们访问的内存不是连续的
我们也可以选择使用二级查表的方式去实现地址映射。从原理上讲,一级查表和二级查表其实并没有太大的差别。使用二级查表法,经过一级页表得到的数据不再是记录了物理地址信息的数据了,而是二级页表项的索引信息。而这些信息除了相应位和标志与一级页表项略有不同之外,与一级查表并无不同。

这里写图片描述

图9

这里写图片描述

图10

线性地址高10位---------索引页目录表----------->找到相应页表
线性地址中间10位---------索引页表----------->得到页表中相应的项,其中的高20位就是物理地址的高20位
线性地址低12位-------------------->物理地址的低12位

展开阅读全文

没有更多推荐了,返回首页