Linux内核深度解析(txt+pdf+epub+mobi电子书下载)


发布时间:2021-04-26 02:19:08

点击下载

作者:余华兵

出版社:人民邮电出版社有限公司

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

Linux内核深度解析

Linux内核深度解析试读:

前言

Linux内核是使用最广泛的开源内核,在服务器和智能手机领域处于统治地位,物联网、大数据、云计算和人工智能等热点技术也离不开Linux内核。对于商业公司而言,采用开源的Linux内核可以享受很多好处,比如节约成本,可以利用行业先进的技术,还可以根据自己的需求定制、修改内核。对于个人而言,从Linux内核中可以学习先进的设计方法和编程技术,为内核贡献代码可以证明自己的技术实力。可是,当我们准备学习Linux内核时,会发现Linux内核的代码庞大而复杂,在没有专业书籍指导的情况下,读懂代码是一件非常困难的事情。作者编写本书的目的是为想要深入理解Linux内核的软件工程师提供指导。本书介绍4.12版本的Linux内核,建议读者在阅读本书时到Linux内核的官方网站中下载一份代码,对照代码学习。推荐使用“Source Insight”软件阅读代码。Linux内核支持多种处理器架构,处理器架构特定的代码放在“arch”目录下。ARM处理器在手机和平板电脑等移动设备上处于统治地位。ARM处理器从ARMv7演进到支持64位的ARMv8,ARM公司重新设计了处理器架构,ARMv8定义了AArch64和AArch32两种执行状态,AArch64是64位架构;AArch32是32位架构,兼容ARMv7。因为ARMv8和ARMv7的差别很大,所以Linux内核把ARMv8和ARMv7当作两种不同的处理器架构,ARMv7架构的代码放在“arch/arm”目录下,ARMv8架构的代码放在“arch/arm64”目录下。人们通常把ARMv8架构的AArch64执行状态称为ARM64架构。本书在介绍Linux内核时选择ARM64处理器架构。学习本书,需要具备ARM64处理器的基础知识,推荐以下两篇文档,读者可以从ARM公司的网站下载。(1)“ARM Cortex-A Series Programmer’s Guide for ARMv8-A”:这篇文档接近300页,适合入门学习。(2)“ARM Architecture Reference Manual ARMv8, for ARMv8-A architecture profile”:这篇文档有6000多页,写得很详细,适合当作工具书来查询。学习内核,关键是要理解数据结构之间的关系和函数调用关系。内核中数据结构之间的关系错综复杂,函数调用层次深,有些函数中的分支非常多,一个函数就可能涉及很多技术,这些都是初学者学习中的障碍。作者建议读者在学习时抓住主要线索,弄清楚执行流程,刚开始不要过多关注函数的细节。为了方便学习,作者绘制了很多图来描述数据结构之间的关系和函数的执行流程。另外,作者在介绍每种技术时会先介绍使用方法,从使用方法开始学习技术,相信会对读者理解技术有很大的帮助。全书内容共分为6章。第1章介绍内核的引导过程,本书选择常用的引导程序U-Boot,读者可以从德国DENX软件工程中心的网站下载U-Boot的代码,对照学习。第2章介绍内核管理和调度进程的技术原理。第3章介绍内核管理虚拟内存和物理内存的技术原理。第4章介绍内核处理异常和中断的技术原理,以及系统调用的实现方式。第5章介绍内核实现的各种保护临界区的互斥技术。第6章介绍内核的虚拟文件系统,内核使用虚拟文件系统支持各种不同的文件系统。本书适用于维护或者开发Linux内核的软件工程师、基于Linux内核开发设备驱动程序的软件工程师,以及想要学习了解Linux内核的软件工程师和学生。对于从事应用程序开发的软件工程师,是否有必要学习内核呢?应用程序通常使用封装好的库,看起来似乎和内核没有关系,但是库是在内核提供的系统调用的基础上做了一层封装。读者如果研究了库函数和内核配合实现库函数提供的功能,那么对软件运行过程的理解将会更深刻,个人的技术水平也将会提升到新的高度—能够设计开发出高质量的应用程序,在软件运行过程中出现问题时可以快速地分析定位。另外,内核代表了软件行业的最高编程技术,这些编程技术也适用于应用程序。最后,感谢我的家人和朋友在本书编写过程中提供的大力支持,感谢提供宝贵意见的同事们,感谢提供技术支持的朋友们,感恩我遇到的众多良师益友。余华兵2019年春第1章 内核引导和初始化处理器上电以后,首先执行引导程序,引导程序把内核加载到内存,然后执行内核,内核初始化完成以后,启动用户空间的第一个进程。1.1 到哪里读取引导程序处理器到哪里读取引导程序的指令?处理器在上电时自动把程序计数器设置为处理器厂商设计的某个固定值,对于ARM64处理器,这个固定值是0。处理器的内存管理单元(Memory Management Unit,MMU)负责把虚拟地址转换为物理地址,ARM64处理器刚上电的时候没有开启内存管理单元,物理地址和虚拟地址相同,所以ARM64处理器到物理地址0取第一条指令。嵌入式设备通常使用NOR闪存作为只读存储器来存放引导程序。NOR闪存的容量比较小,最小读写单位是字节,程序可以直接在芯片内执行。从物理地址0开始的一段物理地址空间被分配给NOR闪存。综上所述,ARM64处理器到虚拟地址0取指令,就是到物理地址0取指令,也就是到NOR闪存的起始位置取指令。1.2 引导程序嵌入式设备通常使用U-Boot作为引导程序。U-Boot(Universal Boot Loader)是德国DENX软件工程中心开发的引导程序,是遵循GPL条款的开源项目。下面简要介绍ARM64处理器的U-Boot程序的执行过程,入口是文件“arch/arm/cpu/ armv8/start.S”定义的标号_start,我们从标号_start开始分析。1.2.1 入口_start标号_start是U-Boot程序的入口,直接跳转到标号reset执行。arch/arm/cpu/armv8/start.S1 .globl _start2 _start:3 b reset1.2.2 标号reset从标号reset开始的代码如下:arch/arm/cpu/armv8/start.S1 reset:2 /* 允许板卡保存重要的寄存器*/3 b save_boot_params4 .globl save_boot_params_ret5 save_boot_params_ret:6 7 #ifdef CONFIG_SYS_RESET_SCTRL8 bl reset_sctrl9 #endif10 /*11 * 异常级别可能是3、2或者1,初始状态:12 * 小端字节序,禁止MMU,禁止指令/数据缓存13 */14 adr x0, vectors15 switch_el x1, 3f, 2f, 1f16 3: msr vbar_el3, x017 mrs x0, scr_el318 orr x0, x0, #0xf /* 设置寄存器SCR_EL3的NS、IRQ、FIQ和EA四个位 */19 msr scr_el3, x020 msr cptr_el3, xzr /* 启用浮点和SIMD功能*/21 #ifdef COUNTER_FREQUENCY22 ldr x0, =COUNTER_FREQUENCY23 msr cntfrq_el0, x0 /* 初始化寄存器CNTFRQ */24 #endif25 b 0f26 2: msr vbar_el2, x027 mov x0, #0x33ff28 msr cptr_el2, x0 /* 启用浮点和SIMD功能 */29 b 0f30 1: msr vbar_el1, x031 mov x0, #3 << 2032 msr cpacr_el1, x0 /* 启用浮点和SIMD功能 */33 0:34 …35 36 /* 应用ARM处理器特定的勘误表*/37 bl apply_core_errata38 39 /* 处理器特定的初始化*/40 bl lowlevel_init41 42 #if defined(CONFIG_ARMV8_SPIN_TABLE) && !defined(CONFIG_SPL_BUILD)43 branch_if_master x0, x1, master_cpu44 b spin_table_secondary_jump45 /* 绝对不会返回*/46 #elif defined(CONFIG_ARMV8_MULTIENTRY)47 branch_if_master x0, x1, master_cpu48 49 /*50 * 从处理器51 */52 slave_cpu:53 wfe54 ldr x1, =CPU_RELEASE_ADDR55 ldr x0, [x1]56 cbz x0, slave_cpu57 br x0 /* 跳转到指定地址*/58 #endif /* CONFIG_ARMV8_MULTIENTRY */59 master_cpu:60 bl _main第3行代码,调用各种板卡自定义的函数save_boot_params来保存重要的寄存器。第8行代码,调用函数reset_sctrl来初始化系统控制寄存器。由配置宏CONFIG_SYS_RESET_SCTRL控制,一般不需要打开。第15~32行代码,根据处理器当前的异常级别设置寄存器。● 第16~24行代码,如果异常级别是3,那么把向量基准地址寄存

器(VBAR_EL3)设置为异常向量的起始地址;设置安全配置寄

存器(SCR_EL3)的NS、IRQ、FIQ和EA这4个位,也就是异常

级别0和1处于非安全状态,在任何异常级别执行时都把中断、

快速中断、同步外部中止和系统错误转发到异常级别3;把协处

理器陷入寄存器(CPTR_EL3)设置为0,允许访问浮点和单指

令多数据(Single Instruction Multiple Data,SIMD)功能;设置

计数器时钟频率寄存器(CNTFRQ_EL0)。● 第26~28行代码,如果异常级别是2,那么把向量基准地址寄存

器(VBAR_EL2)设置为异常向量表的起始地址;设置协处理器

陷入寄存器(CPTR_EL2),允许访问浮点和SIMD功能。● 第30~32行代码,如果异常级别是1,那么把向量基准地址寄存

器(VBAR_EL1)设置为异常向量表的起始地址;设置协处理器

访问控制寄存器(CPACR_EL1),允许访问浮点和SIMD功能。第37行代码,为处理器的缺陷打补丁。第40行代码,调用函数lowlevel_init以执行函数board_init_f()所需要的最小初始化。当前文件定义了弱符号类型的函数lowlevel_init,处理器厂商可以自定义强符号类型的函数lowlevel_init以覆盖弱符号。第42~58行代码,如果是多处理器系统,那么只有一个处理器是主处理器(也称为引导处理器),其他处理器是从处理器。● 第42~44行代码,如果使用自旋表启动方法,并且不是编译为

第二程序加载器,那么从处理器执行函数

spin_table_secondary_jump。源文件“arch/arm/cpu/armv8/

spin_table.c”中定义了函数spin_table_secondary_jump,执行过

程为:从处理器进入低功耗状态,它被唤醒的时候,从地址

spin_table_cpu_release_addr读取函数地址,如果主处理器还没

有指定函数地址,继续等待;如果主处理器指定了函数地址,就

跳转到指定的函数地址执行。● 第46~57行代码,如果允许多个处理器进入引导程序,那么从

处理器进入低功耗状态,它被唤醒的时候,从地址

CPU_RELEASE_ADDR读取函数地址,如果主处理器还没有指

定函数地址,继续等待;如果主处理器指定了函数地址,就跳转

到指定的函数地址执行。第60行代码,主处理器执行函数_main。下面介绍第二阶段程序加载器。U-Boot分为SPL和正常的U-Boot程序两个部分,如果想要编译为SPL,需要开启配置宏CONFIG_SPL_BUILD。SPL是“Secondary Program Loader”的简称,即第二阶段程序加载器,第二阶段是相对于处理器里面的只读存储器中的固化程序来说的,处理器启动时最先执行的是只读存储器中的固化程序。固化程序通过检测启动方式来加载第二阶段程序加载器。为什么需要第二阶段程序加载器?原因是:一些处理器内部集成的静态随机访问存储器比较小,无法装载一个完整的U-Boot镜像,此时需要第二阶段程序加载器,它主要负责初始化内存和存储设备驱动,然后把正常的U-Boot镜像从存储设备读到内存中执行。1.2.3 函数_main函数_main的代码如下:arch/arm/lib/crt0_64.S1 ENTRY(_main)2 3 /*4 * 设置初始的C语言运行环境,并且调用board_init_f(0)。5 */6 #if defined(CONFIG_SPL_BUILD) && defined(CONFIG_SPL_STACK 7 ldr x0, =(CONFIG_SPL_STACK)8 #else9 ldr x0, =(CONFIG_SYS_INIT_SP_ADDR)10 #endif11 bic sp, x0, #0xf /* 为了符合应用二进制接口规范,对齐到16字节*/12 mov x0, sp13 bl board_init_f_alloc_reserve14 mov sp, x015 /* 设置gd */16 mov x18, x017 bl board_init_f_init_reserve18 19 mov x0, #020 bl board_init_f21 22 #if !defined(CONFIG_SPL_BUILD)23 /*24 * 设置中间环境(新的栈指针和gd),然后调用函数25 * relocate_code(addr_moni)。26 *27 */28 ldr x0, [x18, #GD_START_ADDR_SP] /* 把寄存器x0设置为gd->start_addr_sp */29 bic sp, x0, #0xf /* 为了符合应用二进制接口规范,对齐到16字节 */30 ldr x18, [x18, #GD_BD] /* 把寄存器x18设置为gd->bd */31 sub x18, x18, #GD_SIZE /* 新的gd在bd的下面 */32 33 adr lr, relocation_return34 ldr x9, [x18, #GD_RELOC_OFF] /* 把寄存器x9设置为gd->reloc_off */35 add lr, lr, x9 /* 在重定位后新的返回地址 */36 ldr x0, [x18, #GD_RELOCADDR] /* 把寄存器x0设置为gd->relocaddr */37 b relocate_code38 39 relocation_return:40 41 /*42 * 设置最终的完整环境43 */44 bl c_runtime_cpu_setup /* 仍然调用旧的例程 */45 #endif /* !CONFIG_SPL_BUILD */46 #if defined(CONFIG_SPL_BUILD)47 bl spl_relocate_stack_gd /* 可能返回空指针 */48 /*49 * 执行“sp = (x0 != NULL) ? x0 : sp”,50 * 规避这个约束:51 * 带条件的mov指令不能把栈指针寄存器作为操作数52 */53 mov x1, sp54 cmp x0, #055 csel x0, x0, x1, ne56 mov sp, x057 #endif58 59 /*60 * 用0初始化未初始化数据段61 */62 ldr x0, =__bss_start /* 这是自动重定位*/63 ldr x1, =__bss_end /* 这是自动重定位*/64 clear_loop:65 str xzr, [x0], #866 cmp x0, x167 b.lo clear_loop68 69 /* 调用函数board_init_r(gd_t *id, ulong dest_addr) */70 mov x0, x18 /* gd_t */71 ldr x1, [x18, #GD_RELOCADDR] /* dest_addr */72 b board_init_r /* 相对程序计数器的跳转 */73 74 /* 不会运行到这里,因为函数board_init_r()不会返回*/75 76 ENDPROC(_main)第6~17行代码,设置C代码的运行环境,为调用函数board_init_f做准备。● 第11行代码,设置临时的栈。● 第13行代码,调用函数board_init_f_alloc_reserve,在栈的顶部

为结构体global_data分配空间。● 第17行代码,调用函数board_init_f_init_reserve,初始化结构体

global_data。第20行代码,调用函数board_init_f(f是front,表示前期),执行前期初始化。为了把U-Boot程序复制到内存中来执行,初始化硬件,做准备工作。文件“common/board_f.c”定义了公共的函数board_init_f,函数board_init_f依次执行数组init_sequence_f中的每个函数。第22~45行代码,如果编译为正常的引导程序,那么调用函数relocate_code,把U-Boot程序复制到内存中,重新定位,然后调用函数c_runtime_cpu_setup,把向量基准地址寄存器设置为异常向量表的起始地址。这里是分界线,以前处理器从NOR闪存取指令,这一步执行完以后处理器从内存取指令。第46~57行代码,如果编译为第二阶段程序加载器,那么调用函数spl_relocate_stack_gd重新定位栈。第62~67行代码,用0初始化未初始化数据段。第72行代码,调用函数board_init_r(r是rear,表示后期),执行后期初始化。文件“common/board_r.c”定义了函数board_init_r,依次执行数组init_sequence_r中的每个函数,最后一个函数是run_main_loop。1.2.4 函数run_main_loopU-Boot程序初始化完成后,准备处理命令,这是通过数组init_sequence_r的最后一个函数run_main_loop实现的。函数run_main_loop的执行流程如图 1.1 所示,把主要工作委托给函数main_loop,函数main_loop的执行过程如下。图1.1 函数run_main_loop的执行流程(1)调用bootdelay_process以读取环境变量bootdelay和bootcmd,环境变量bootdelay定义延迟时间,即等待用户按键的时间长度;环境变量bootcmd定义要执行的命令。U-Boot程序到哪里读取环境变量?通常我们把NOR闪存分成多个分区,其中第一个分区存放U-Boot程序,第二个分区存放环境变量。U-Boot程序里面的NOR闪存驱动程序对分区信息硬编码,指定每个分区的偏移和长度。U-Boot程序从环境变量分区读取环境变量。(2)调用函数autoboot_command。函数autoboot_command先调用函数abortboot,等待用户按键。如果在等待时间内用户没有按键,就调用函数run_command_list,自动执行环境变量bootcmd定义的命令。假设环境变量bootcmd定义的命令是“bootm”,函数run_command_list查找命令表,发现命令“bootm”的处理函数是do_bootm。函数do_bootm的执行流程如图1.2所示,把主要工作委托给函数do_bootm_states,函数do_bootm_states的执行过程如下。图1.2 函数do_bootm的执行流程(1)函数bootm_start负责初始化全局变量“bootm_headers_t images”。(2)函数bootm_find_os把内核镜像从存储设备读到内存。(3)函数bootm_find_other读取其他信息,对于ARM64架构,通常是扁平设备树(Flattened Device Tree,FDT)二进制文件,该文件用来传递硬件信息给内核。(4)函数bootm_load_os把内核加载到正确的位置,如果内核镜像是被压缩过的,需要解压缩。(5)函数bootm_os_get_boot_func根据操作系统类型在数组boot_os中查找引导函数,Linux内核的引导函数是do_bootm_linux。(6)第一次调用函数do_bootm_linux时,参数flag是BOOTM_STATE_OS_PREP,为执行Linux内核做准备工作。函数do_bootm_linux(flag=BOOTM_STATE_OS_PREP)把工作委托给函数boot_prep_linux,主要工作如下。1)分配一块内存,把扁平设备树二进制文件复制过去。2)修改扁平设备树二进制文件,例如:如果环境变量“bootargs”指定了内核参数,那么把节点“/chosen”的属性“bootargs”设置为内核参数字符串;如果多处理器系统使用自旋表启动方法,那么针对每个处理器对应的节点“cpu”,把属性“enable-method”设置为“spin-table”,把属性“cpu-release-addr”设置为全局变量spin_table_cpu_release_addr的地址。(7)函数boot_selected_os调用函数do_bootm_linux,这是第二次调用函数do_bootm_linux,参数flag是BOOTM_STATE_OS_GO。函数do_bootm_linux(flag=BOOTM_STATE_OS_GO)调用函数boot_jump_linux,该函数跳转到内核的入口,第一个参数是扁平设备树二进制文件的起始地址,后面3个参数现在没有使用。函数boot_jump_linux负责跳转到Linux内核,执行流程如图1.3所示。图1.3 函数boot_jump_linux的执行流程(1)调用函数smp_kick_all_cpus,如果打开了配置宏CONFIG_GICV2或者CONFIG_GICV3,即使用通用中断控制器版本2或者版本 3,那么发送中断请求以唤醒所有从处理器。(2)调用函数dcache_disable,禁用处理器的缓存和内存管理单元。(3)如果开启配置宏CONFIG_ARMV8_SWITCH_TO_EL1,表示在异常级别1执行Linux内核,那么先从异常级别3切换到异常级别2,然后切换到异常级别1,最后跳转到内核入口。(4)如果在异常级别2执行Linux内核,那么先从异常级别3切换到异常级别2,然后跳转到内核入口。1.3 内核初始化内核初始化分为汇编语言部分和C语言部分。1.3.1 汇编语言部分ARM64架构的内核的入口是标号_head,直接跳转到标号stext。arch/arm64/kernel/head.S1 _head:2 #ifdef CONFIG_EFI3 add x13, x18, #0x164 b stext5 #else6 b stext // 跳转到内核起始位置7 .long0 // 保留8 #endif配置宏CONFIG_EFI表示提供UEFI运行时支持,UEFI(Unified Extensible Firmware Interface)是统一的可扩展固件接口,用于取代BIOS。标号stext开始的代码如下:arch/arm64/kernel/head.S1 ENTRY(stext)2 bl preserve_boot_args3 bl el2_setup // 降级到异常级别1, 寄存器w0存放cpu_boot_mode4 adrp x23, __PHYS_OFFSET5 and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR偏移,默认值是06 bl set_cpu_boot_mode_flag7 bl __create_page_tables8 /*9 * 下面调用设置处理器的代码,请看文件“arch/arm64/mm/proc.S”10 * 了解细节。11 * 返回的时候,处理器已经为开启内存管理单元做好准备,12 * 转换控制寄存器已经设置好。13 */14 bl __cpu_setup // 初始化处理器15 b __primary_switch16 ENDPROC(stext)第2行代码,调用函数preserve_boot_args,把引导程序传递的4个参数保存在全局数组boot_args中。第3行代码,调用函数el2_setup:如果处理器当前的异常级别是2,判断是否需要降级到异常级别1。第6行代码,调用函数set_cpu_boot_mode_flag,根据处理器进入内核时的异常级别设置数组__boot_cpu_mode[2]。__boot_cpu_mode[0]的初始值是BOOT_CPU_MODE_EL2,__boot_cpu_mode[1]的初始值是BOOT_CPU_MODE_EL1。如果异常级别是1,那么把__boot_cpu_mode[0]设置为BOOT_CPU_MODE_EL1;如果异常级别是2,那么把__boot_cpu_mode[1]设置为BOOT_CPU_MODE_EL2。第7行代码,调用函数__create_page_tables,创建页表映射。第14行代码,调用函数__cpu_setup,为开启处理器的内存管理单元做准备,初始化处理器。第15行代码,调用函数__primary_switch,为主处理器开启内存管理单元,搭建C语言执行环境,进入C语言部分的入口函数start_kernel。1.函数el2_setup进入内核的时候,ARM64处理器的异常级别可能是1或者2,函数el2_setup的主要工作如下。(1)如果异常级别是1,那么在异常级别1执行内核。(2)如果异常级别是2,那么根据处理器是否支持虚拟化宿主扩展(Virtualization Host Extensions,VHE),决定是否需要降级到异常级别1。1)如果处理器支持虚拟化宿主扩展,那么在异常级别2执行内核。2)如果处理器不支持虚拟化宿主扩展,那么降级到异常级别1,在异常级别1执行内核。下面介绍ARM64处理器的异常级别和虚拟化宿主扩展。如图1.4所示,通常ARM64处理器在异常级别0执行进程,在异常级别1执行内核。图1.4 普通的异常级别切换虚拟机是现在流行的虚拟化技术,在计算机上创建一个虚拟机,在虚拟机里面运行一个操作系统,运行虚拟机的操作系统称为宿主操作系统(host OS),虚拟机里面的操作系统称为客户操作系统(guest OS)。现在常用的虚拟机是基于内核的虚拟机(Kernel-based Virtual Machine,KVM),KVM的主要特点是直接在处理器上执行客户操作系统,因此虚拟机的执行速度很快。KVM是内核的一个模块,把内核变成虚拟机监控程序。如图1.5所示,宿主操作系统中的进程在异常级别0运行,内核在异常级别1运行,KVM模块可以穿越异常级别1和2;客户操作系统中的进程在异常级别0运行,内核在异常级别1运行。图1.5 支持虚拟化的异常级别切换常用的开源虚拟机管理软件是QEMU,QEMU支持KVM虚拟机。使用QEMU创建一个KVM虚拟机,和KVM的交互过程如下。(1)打开KVM字符设备文件。fd = open("/dev/kvm", O_RDWR);(2)创建一个虚拟机,QEMU进程得到一个关联到虚拟机的文件描述符。vmfd = ioctl(fd, KVM_CREATE_VM, 0);(3)QEMU为虚拟机模拟多个处理器,每个虚拟处理器就是一个线程,调用KVM提供的命令KVM_CREATE_VCPU,KVM为每个虚拟处理器创建一个kvm_vcpu结构体,QEMU进程得到一个关联到虚拟处理器的文件描述符。vcpu_fd = ioctl(vmfd, KVM_CREATE_VCPU, 0);从QEMU切换到客户操作系统的过程如下。(1)QEMU进程调用“ioctl(vcpu_fd, KVM_RUN, 0)”,陷入到内核。(2)KVM执行命令KVM_RUN,从异常级别1切换到异常级别2。(3)KVM首先把调用进程的所有寄存器保存在kvm_vcpu结构体中,然后把所有寄存器设置为客户操作系统的寄存器值,最后从异常级别2返回到异常级别1,执行客户操作系统。如图1.6所示,为了提高切换速度,ARM64架构引入了虚拟化宿主扩展,在异常级别2执行宿主操作系统的内核,从QEMU切换到客户操作系统的时候,KVM不再需要先从异常级别1切换到异常级别2。图1.6 支持虚拟化宿主扩展的异常级别切换2.函数__create_page_tables函数__create_page_tables的主要工作如下。(1)创建恒等映射(identity mapping)。(2)为内核镜像创建映射。恒等映射的特点是虚拟地址和物理地址相同,是为了在开启处理器的内存管理单元的一瞬间能够平滑过渡。函数__enable_mmu负责开启内存管理单元,内核把函数__enable_mmu附近的代码放在恒等映射代码节(.idmap.text)里面,恒等映射代码节的起始地址存放在全局变量__idmap_text_start中,结束地址存放在全局变量__idmap_text_end中。恒等映射是为恒等映射代码节创建的映射,idmap_pg_dir是恒等映射的页全局目录(即第一级页表)的起始地址。在内核的页表中为内核镜像创建映射,内核镜像的起始地址是_text,结束地址是_end,swapper_pg_dir是内核的页全局目录的起始地址。3.函数__primary_switch函数__primary_switch的主要执行流程如下。(1)调用函数__enable_mmu以开启内存管理单元。(2)调用函数__primary_switched。函数__enable_mmu的主要执行流程如下。(1)把转换表基准寄存器0(TTBR0_EL1)设置为恒等映射的页全局目录的起始物理地址。(2)把转换表基准寄存器1(TTBR1_EL1)设置为内核的页全局目录的起始物理地址。(3)设置系统控制寄存器(SCTLR_EL1),开启内存管理单元,以后执行程序时内存管理单元将会把虚拟地址转换成物理地址。函数__primary_switched的执行流程如下。(1)把当前异常级别的栈指针寄存器设置为0号线程内核栈的顶部(init_thread_union + THREAD_SIZE)。(2)把异常级别0的栈指针寄存器(SP_EL0)设置为0号线程的结构体thread_info的地址(init_task.thread_info)。(3)把向量基准地址寄存器(VBAR_EL1)设置为异常向量表的起始地址(vectors)。(4)计算内核镜像的起始虚拟地址(kimage_vaddr)和物理地址的差值,保存在全局变量kimage_voffset中。(5)用0初始化内核的未初始化数据段。(6)调用C语言函数start_kernel。1.3.2 C语言部分内核初始化的C语言部分入口是函数start_kernel,函数start_kernel首先初始化基础设施,即初始化内核的各个子系统,然后调用函数rest_init。函数rest_init的执行流程如下。(1)创建1号线程,即init线程,线程函数是kernel_init。(2)创建2号线程,即kthreadd线程,负责创建内核线程。(3)0号线程最终变成空闲线程。init线程继续初始化,执行的主要操作如下。(1)smp_prepare_cpus():在启动从处理器以前执行准备工作。(2)do_pre_smp_initcalls():执行必须在初始化SMP系统以前执行的早期初始化,即使用宏early_initcall注册的初始化函数。(3)smp_init():初始化SMP系统,启动所有从处理器。(4)do_initcalls():执行级别0~7的初始化。(5)打开控制台的字符设备文件“/dev/console”,文件描述符0、1和2分别是标准输入、标准输出和标准错误,都是控制台的字符设备文件。(6)prepare_namespace():挂载根文件系统,后面装载init程序时需要从存储设备上的文件系统中读文件。(7)free_initmem():释放初始化代码和数据占用的内存。(8)装载init程序(U-Boot程序可以传递内核参数“init=”以指定init程序),从内核线程转换成用户空间的init进程。级别0~7的初始化,是指使用以下宏注册的初始化函数:include/linux/init.h#define pure_initcall(fn) __define_initcall(fn, 0)#define core_initcall(fn) __define_initcall(fn, 1)#define core_initcall_sync(fn) __define_initcall(fn, 1s)#define postcore_initcall(fn) __define_initcall(fn, 2)#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)#define arch_initcall(fn) __define_initcall(fn, 3)#define arch_initcall_sync(fn) __define_initcall(fn, 3s)#define subsys_initcall(fn) __define_initcall(fn, 4)#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)#define fs_initcall(fn) __define_initcall(fn, 5)#define fs_initcall_sync(fn) __define_initcall(fn, 5s)#define rootfs_initcall(fn) __define_initcall(fn, rootfs)#define device_initcall(fn) __define_initcall(fn, 6)#define device_initcall_sync(fn) __define_initcall(fn, 6s)#define late_initcall(fn) __define_initcall(fn, 7)#define late_initcall_sync(fn) __define_initcall(fn, 7s)1.3.3 SMP系统的引导对称多处理器(Symmetric Multi-Processor,SMP)系统包含多个处理器,并且每个处理器的地位平等。在启动过程中,处理器的地位不是平等的,0号处理器称为引导处理器,负责执行引导程序和初始化内核;其他处理器称为从处理器,等待引导处理器完成初始化。引导处理器初始化内核以后,启动从处理器。引导处理器启动从处理器的方法有3种。(1)自旋表(spin-table)。(2)电源状态协调接口(Power State Coordination Interface,PSCI)。(3)ACPI停车协议(parking-protocol),ACPI是高级配置与电源接口(Advanced Configuration and Power Interface)。引导处理器怎么获取从处理器的启动方法呢?读者可以参考函数cpu_read_enable_method,获取方法如下。(1)不支持ACPI的情况:引导处理器从扁平设备树二进制文件中“cpu”节点的属性“enable-method”读取从处理器的启动方法,可选的方法是自旋表或者PSCI。(2)支持ACPI的情况:如果固定ACPI描述表(Fixed ACPI Description Table,FADT)设置了允许PSCI的引导标志,那么使用PSCI,否则使用ACPI停车协议。假设使用自旋表启动方法,编译U-Boot程序时需要开启配置宏CONFIG_ARMV8_SPIN_TABLE。如图1.7所示,SMP系统的引导过程如下。图1.7 ARM64架构下SMP系统的自旋表引导过程(1)从处理器的第一个关卡是U-Boot程序中的函数spin_table_secondary_jump,从处理器睡眠等待,被唤醒后,检查全局变量spin_table_cpu_release_addr的值是不是0,如果是0,继续睡眠等待。引导处理器将会把全局变量spin_table_cpu_release_addr的值设置为一个函数的地址。(2)U-Boot程序:引导处理器执行函数boot_prep_linux,为执行内核做准备工作,其中一项准备工作是调用函数spin_table_update_dt,修改扁平设备树二进制文件如下。1)为每个处理器的“cpu”节点插入一个属性“cpu-release-addr”,把属性值设置为全局变量spin_table_cpu_release_addr的地址,称为处理器放行地址。2)在内存保留区域(memory reserve map,对应扁平设备树源文件的字段“/memreserve/”)添加全局变量spin_table_cpu_release_addr的地址。(3)引导处理器在内核函数smp_cpu_setup中,首先调用函数cpu_read_enable_method以获取从处理器的启动方法,然后调用函数smp_spin_table_cpu_init,从扁平设备树二进制文件中“cpu”节点的属性“cpu-release-addr”得到从处理器的放行地址。(4)引导处理器执行内核函数smp_spin_table_cpu_prepare,针对每个从处理器,把放行地址设置为函数secondary_holding_pen,然后唤醒从处理器。(5)从处理器被唤醒,执行函数secondary_holding_pen,这个函数设置了第二个关卡,当引导处理器把全局变量secondary_holding_pen_release设置为从处理器的编号时,才会放行。(6)引导处理器完成内核的初始化,启动所有从处理器,针对每个从处理器,调用函数smp_spin_table_cpu_boot,把全局变量secondary_holding_pen_release设置为从处理器的编号。(7)从处理器发现引导处理器把全局变量

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载