【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析

网友投稿 1455 2022-09-28

【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析

【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析

【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析

​​一、Android Kernel 启动流程分析​​

​​1.1 入口汇编代码 arch\arm64\kernel\head.S : 跳转start_kernel() 入口函数​​​​1.2 入口函数 start_kernel()​​​​1.3 一号进程 init_task 结构体​​

本 SemiDrive源码分析 之 Yocto源码分析 系列文章汇总如下:《​​【SemiDrive源码分析】【Yocto源码分析】01 - yocto/base目录源码分析(编译环境初始化流程)​​》《​​【SemiDrive源码分析】【Yocto源码分析】02 - yocto/meta-openembedded目录源码分析​​》《​​【SemiDrive源码分析】【Yocto源码分析】03 - yocto/meta-semidrive目录及Yocto Kernel编译过程分析(上)​​》《​​【SemiDrive源码分析】【Yocto源码分析】04 - yocto/meta-semidrive目录及Yocto Kernel编译过程分析(下)​​》《​​【SemiDrive源码分析】【Yocto源码分析】05 - 找一找Yocto Kernel编译过程中所有Task的源码在哪定义的呢?​​》《​​【SemiDrive源码分析】【Yocto源码分析】06 - Kernel编译生成的Image.bin、Image_nobt.dtb、modules.tgz 这三个文件分别是如何生成的?​​》《​​【SemiDrive源码分析】【Yocto源码分析】07 - core-image-base-x9h_ref_serdes.rootfs.ext4 文件系统是如何生成的​​》《​​【SemiDrive源码分析】【X9芯片启动流程】08 - X9平台 lk 目录源码分析 之 目录介绍​​》《​​【SemiDrive源码分析】【X9芯片启动流程】09 - X9平台系统启动流程分析​​》《​​【SemiDrive源码分析】【X9芯片启动流程】10 - BareMetal_Suite目录R5 DIL.bin 引导程序源代码分析​​》《​​【SemiDrive源码分析】【X9芯片启动流程】11 - freertos_safetyos目录Cortex-R5 DIL2.bin 引导程序源代码分析​​》《​​【SemiDrive源码分析】【X9芯片启动流程】12 - freertos_safetyos目录Cortex-R5 DIL2.bin 之 sdm_display_init 显示初始化源码分析​​》《​​【SemiDrive源码分析】【X9芯片驱动调试】13 - GPIO 配置方法​​》《​​【SemiDrive源码分析】【X9芯片启动流程】14 - freertos_safetyos目录Cortex-R5 SafetyOS/RTOS工作流程分析​​》《​​【SemiDrive源码分析】【X9芯片启动流程】15 - freertos_safetyos目录 R5 SafetyOS 之 tcpip_init() 代码流程分析​​》《​​【SemiDrive源码分析】【X9 Audio音频模块分析】16 - 音频模块框图及硬件原理图分析​​》《​​【SemiDrive源码分析】【X9芯片启动流程】17 - R5 SafetyOS 之 LK_INIT_LEVEL_PLATFORM 阶段代码流程分析(上)dcf_init 核间通信初始化​​》《​​【SemiDrive源码分析】【X9芯片启动流程】18 - R5 SafetyOS 之 LK_INIT_LEVEL_PLATFORM 阶段代码流程(下)启动QNX、Android​​》《​​【SemiDrive源码分析】【X9芯片启动流程】19 - MailBox 核间通信机制介绍(理论篇)​​》《​​【SemiDrive源码分析】【X9芯片启动流程】20 - MailBox 核间通信机制介绍(代码分析篇)之 MailBox for RTOS 篇​​》《​​【SemiDrive源码分析】【X9芯片启动流程】21 - MailBox 核间通信机制介绍(代码分析篇)之 Mailbox for Linux 篇​​》《​​【SemiDrive源码分析】【X9芯片启动流程】22 - MailBox 核间通信机制介绍(代码分析篇)之 RPMSG-VIRTIO Kernel 篇​​》《​​【SemiDrive源码分析】【X9芯片启动流程】23 - MailBox 核间通信机制介绍(代码分析篇)之 RPMSG-IPCC Kernel 篇​​》《​​【SemiDrive源码分析】【X9芯片启动流程】24 - MailBox 核间通信机制相关寄存器介绍​​》《​​【SemiDrive源码分析】【X9芯片启动流程】25 - MailBox 核间通信机制介绍(代码分析篇)之 RPMSG-IPCC RTOS & QNX篇​​》《​​【SemiDrive源码分析】【X9芯片启动流程】26 - R5 SafetyOS 之 LK_INIT_LEVEL_TARGET 阶段代码流程分析(TP Drvier、Audio Server初始化)​​》《​​【SemiDrive源码分析】【X9芯片启动流程】27 - AP1 Android Preloader启动流程分析(加载atf、tos、bootloader镜像后进入BL31环境)​​》《​​【SemiDrive源码分析】【X9芯片启动流程】28 - AP1 Android SMC 指令进入 EL3 环境执行 ATF 镜像(加载并跳转 bootloader)​​》《​​【SemiDrive源码分析】【X9芯片启动流程】29 - AP1 Android Bootloader启动流程分析(加载并跳转kernel)​​》《​​【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析​​》待写: 28. 《【SemiDrive源码分析】【X9芯片启动流程】28 - MailBox 核间通信机制介绍(代码分析篇)之 Property篇》 29. 《【SemiDrive源码分析】【X9芯片启动流程】29 - MailBox 核间通信机制介绍(代码分析篇)之 RPCall篇》 30. 《【SemiDrive源码分析】【X9芯片启动流程】30 - MailBox 核间通信机制介绍(代码分析篇)之 Notify篇》 31. 《【SemiDrive源码分析】【X9芯片启动流程】31 - MailBox 核间通信机制介绍(代码分析篇)之 Socket篇》 32. 《【SemiDrive源码分析】【X9芯片启动流程】32 - MailBox 核间通信机制介绍(代码分析篇)之 /dev/vircan篇》

从接触芯驰开始,边学边总结,不知不觉已经1个多月了,已经写到第30篇文章了, 主要目的,还是支持芯片国产化,加上之前调高通、MTK时,自已带了这么多个项目,却没有想过去写一套完整的总结出来,有点小遗憾, 干脆就从芯驰开始补足这个遗憾,每天抽时间出来总结,希望整理一套完整的总结出来,尽量做到从入门到入土(哈哈,开玩笑的)。 希望的话,这个系列的文章,我能坚持维护到项目量产, 内容争取 涵盖 启动流程代码分析、BSP模块代码移植、各模块硬件工作原理、各模块代码框架从底层到上层的分析、以及项目中遇到的问题分析过程及总结等等,毕竟是私人总结,视精力而定,能写多少写多少,有啥写啥,能共享就共享,不能共享就设私密,这个没啥好说的。

前面刚开始写的文章分析的相对详细,加上平台大差不差,结合之前高通MTK调试经验比较丰富,所以后面对芯驰也越来越熟悉, 主要是时间紧急,一些简单的 或者 对项目意义不大的代码,我也适当的做了省略。 主要目的,还是通过对启动流程代码的分析,从而对整个芯驰X9平台启动流程中有一定的了解, 没必要太深,简单来说,知道哪些时间做了哪些事就够了, 熟悉启动流程最直接的受益就是回板后的 ​​​Bringup​​,比如回板后开不了机,从log就能看到死在哪个阶段了,结合这个段阶要做的事,就很好分析回板不开机的问题,或者说后续项目中要定制一些开机过程中的需求,清楚开机流程就知道需求代码加在哪会相对更加合适,等等。

一些项目中 BSP 会涉及的模块,我也做了省略,如 开机过程中​​Display​​​我就没怎么分析, 因为我后续会单独起文章来深入分析,主要时间不允许,现在也就没必要看这么深, 这些模块我们后续会从硬件工作原理、代码如何移植、参数如何分析、模块系统架构这几个方面深入分析,敬请期待吧。

从本文开始,我们正式进入安卓分析 ​​Kernel​​ 启动流程,按照计划如下:

​​Kernel​​​ 启动流程,主要分析看看​​start_kernel​​ 和 之前调试的高通平台有什么差别。​​Android System​​​ 启动流程:比如​​init.rc​​ 解析等。

因为​​QNX​​​暂时我不用关心,所以​​QNX710​​​ 源码不急,等后面有时间再分析吧, 等分析完​​​kernel​​​ + ​​android System​​ 后,那启动流程这块也算看完了,也该准备项目相关模块了,

预计20号开始准备项目,预留20多天,差不多,这20多天,主要是跟各供应商要到相关的代码、资料及FAE联系方式, 同时提前开始做项目配置,移植驱动代码到项目代码中,争取回板前能出一版全功能的镜像出来 (代码全功能就行,至行实际功能通不通,不知道 ,也无所谓,回板后再分析嘛)

部分计划如下:

分区表配置:检查配置​​Flash​​ 相关的参数、根据项目需求配置分区表及分区大小检查默认的​​GPIO​​​配置:主要是根据硬件原理图,配置好​​RTOS​​​ 和​​Kernel ​​​中的默认​​GPIO​​状态显示模块 代码移植 + 参数解析 (含​​LVDS​​​、​​TouchScreen​​)​​DP​​​、​​DC Layer​​ 图层相关原理分析摄像头模块 代码移植 + 参数解析(含​​CVDS​​)

以上这些都是暂定的学习计划,没写在里面的也并不代表不分析,后面看啥写啥, 有啥写啥,做啥写啥,加油。

由于前面分析的是基线代码,这些代码只要做芯驰的程序员都能看到,这些不涉及机密 , 等项目启动后,有些可能会涉及项目内容的文章,我写好后,不敢共享,就会设为私密文章,只能我一个人看得到, 所以后面兄弟们如果碰到文章断层,那就可能设了私密,断层部分就是私密文章,还请见谅,哈哈。

前面废话了一大堆,我们正式进入主题吧,加油 ^_^

一、Android Kernel 启动流程分析

1.1 入口汇编代码 arch\arm64\kernel\head.S : 跳转start_kernel() 入口函数

初始化 ​​CPU​​​、页表、​​MMU​​​ 等,最后跳转 ​​start_kernel​​ 函数

# buildsystem\android\kernel\arch\arm64\kernel\head.S/* Kernel startup entry point. * --------------------------- * The requirements are: * MMU = off, D-cache = off, I-cache = on or off, * x0 = physical address to the FDT blob. * This code is mostly position independent so you call this at __pa(PAGE_OFFSET + TEXT_OFFSET). * * Note that the callee-saved registers are used for storing variables that are useful before the MMU is enabled. * The allocations are described in the entry routines. */ __HEAD_head: /* DO NOT MODIFY. Image header expected by Linux boot-loaders. */ b stext // branch to kernel start, magic .long 0 // reserved le64sym _kernel_offset_le // Image load offset from start of RAM, little-endian le64sym _kernel_size_le // Effective size of kernel image, little-endian le64sym _kernel_flags_le // Informative flags, little-endian .quad 0 // reserved .quad 0 // reserved .quad 0 // reserved .ascii "ARM\x64" // Magic number .long 0 // reserved __INIT /* * The following callee saved general purpose registers are used on the primary lowlevel boot path: * Register Scope Purpose * x21 stext() .. start_kernel() FDT pointer passed at boot in x0 * x23 stext() .. start_kernel() physical misalignment/KASLR offset * x28 __create_page_tables() callee preserved temp register * x19/x20 __primary_switch() callee preserved temp registers */ENTRY(stext) bl preserve_boot_args ===============================> //# buildsystem\android\kernel\arch\arm64\kernel\head.S + // 1. 将dtb_p地址存放在 x21寄存器中,在 boot kernel过程中, arg0=dtb_p,arg1=0 + mov x21, x0 // x21=FDT + // 2. 将 boot_args 的偏移地址保存在 x0 中 + adr_l x0, boot_args // record the contents of + // 3. 将x21, x1, x2, x3保存到[x0]指向的地址,也就是boot_args数组中 + stp x21, x1, [x0] // x0 .. x3 at kernel entry + stp x2, x3, [x0, #16] + // 4. 将写入的数据同步到内存中, 类似 fs_sync 的功能 + dmb sy // needed before dc ivac with + // MMU off + // 5. 将 0x20 写入 x1 寄存器,此时 x0=&boot_args[], x1=0x20 + mov x1, #0x20 // 4 x 8 bytes + b __inval_dcache_area // tail call <=============================== // 6. 初始化 EL2 环境,配置 cpu boot mode,初始化页表 bl el2_setup // Drop to EL1, w0=cpu_boot_mode adrp x23, __PHYS_OFFSET and x23, x23, MIN_KIMG_ALIGN - 1 // KASLR offset, defaults to 0 bl set_cpu_boot_mode_flag bl __create_page_tables /* The following calls CPU setup code, see arch/arm64/mm/proc.S for details. * On return, the CPU will be ready for the MMU to be turned on and the TCR will have been set. */ // 7. 初始化CPU bl __cpu_setup // initialise processor // 8. 使能MMU,重定位kernel 地址,将kernel 偏移放入 X0 寄存器中,跳转到__primary_switched 函数 b __primary_switchENDPROC(stext)__primary_switch: bl __enable_mmu bl __relocate_kernel ldr x8, =__primary_switched adrp x0, __PHYS_OFFSET blr x8/* The following fragment of code is executed with the MMU enabled. * x0 = __PHYS_OFFSET */ __primary_switched: adrp x4, init_thread_union // 将 init_thread_union 的地址保存在 X4 中 add sp, x4, #THREAD_SIZE // 设置堆栈指针SP的值,就是内核栈的栈底+THREAD_SIZE的大小 adr_l x5, init_task // 将 init_task 地址保存在 x5 中 msr sp_el0, x5 // Save thread_info adr_l x8, vectors // load VBAR_EL1 with virtual msr vbar_el1, x8 // vector table address isb // 指令同步隔离, 等待将前面处于指令流水线中的所有指令运行完 stp xzr, x30, [sp, #-16]! mov x29, sp str_l x21, __fdt_pointer, x5 // Save FDT pointer ldr_l x4, kimage_vaddr // Save the offset between sub x4, x4, x0 // the kernel virtual and str_l x4, kimage_voffset, x5 // physical mappings // Clear BSS adr_l x0, __bss_start // 清空 BSS 栈 mov x1, xzr adr_l x2, __bss_stop sub x2, x2, x0 bl __pi_memset // 确保屏障前面的存储指令执行完毕,dsb是数据同步屏障,ishst中ish表示共享域是内部共享,st表示存储 ,ishst表示数据同步屏障指令对所有核的存储指令起作用 dsb ishst // Make zero page visible to PTW add sp, sp, #16 mov x29, #0 mov x30, #0 b start_kernel // 正式跳转 start_kernel 函数ENDPROC(__primary_switched)

1.2 入口函数 start_kernel()

入口函数 ​​start_kernel()​​ 主要工作如下:

设置内核 1号任务​​init_task​​的栈最低地址,使得​​init_task->stack​​向栈底最后一个​​long​​的地址,主要作用是栈溢出检测配置触发​​CPU​​中断来获取​​ID​​,返回当前正在执行初始化的处理器​​ID​​,默认为​​CPU0​​, 会打印​​Booting Linux on physical CPU 0x0​​初始化​​hash buckets​​,最大​​1<<14 = 16384​​个对象 ,将​​static object poll ( obj_static_pool[i] ) ​​中的所有元素到链表中,最大为​​1024​​个元素,链表表头为 ​​obj_pool​​。初始化​​cgroup​​,​​cgroup​​是一种将进程按组管理的机制,其根节点为 ​​struct cgroup_root cgrp_dfl_root​​​​cgroup​​ 可以把 ​​linux​​ 系统中所有进程组织成树的形式,每颗树都包含系统中所有的进程, 树的每一个节点就是一个进程组,每颗树又和一个或多个 ​​subsystem​​ 相关联。屏蔽当前​​CPU0​​上的所有中断,此时送到当前​​CPU0​​上的所有中断信号都会被忽略,配置全局变量​​early_boot_irqs_disabled=true​​更新当前​​CPU0​​ 相关状态变量的值,使能为 ​​online、active、present、possible​​ 为 ​​true​​初始化高端内存,主要是初始化​​page_address_htable​​ 链表,每当要申请映射一块 ​​Highmem​​空间时,就会的相应的信息添加到 ​​page_address_htable​​ 链表中打印​​Linux Version​​ 系统版本号等信息为解析​​cmdline​​做准备,初始化​​ioremap​​、​​memlock​​、​​paging​​分页等内存映射相关的代码,将 ​​command_line​​ 指向 ​​boot_command_line​​对应的内存; 调用​​cpu_prepare​​ 初始化所有可用的 ​​cpu​​ 核心,注意此时还是单核 ​​CPU0​​ 在运行这个没看懂里面的​​input_pool​​ 的作用是啥调用​​get_random_bytes​​获取一个内核随机数,赋值给​​canary​​,保存在 ​​init_task->stack_canary​​ 中,主要目的还是防止栈溢出。 按我的理解是,代码会把这个​​canary​​值写在栈的最后一个​​long​​整形位置了,由于它是一个随机数,理论上通过读取它的值是否改变就能知道是否发现了栈溢出问题初始化清除​​cpumask​​中的所有​​CPU​​调用​​__alloc_bootmem​​ 从​​bootmem​​中申请 ​​saved_command_line​​、​​initcall_command_line​​、​​static_command_line​​三块空间,将 ​​boot_command_line​​的内容拷贝到 ​​saved_command_line​​,将 ​​setup_arch()​​ 在 ​​kernel​​ 中动态生成的 ​​command_line​​ 内容,保存在 ​​static_command_line​​ 中​​nr_cpu_ids​​​是指具有当前系统能够使用的​​CPU​​数,默认值为​​NR_CPUS=1​​值,它使用的是​​bit​​位的形式表示,类型是​​unsigned int​​有​​32​​位,所以当前​​kernel​​最大支持​​32​​个​​CPU​​同时使用系统为多核​​CPU​​ 在 ​​percpu​​ 空间申请了对应的内存用于保存每个​​CPU​​ 的私有数据,这块内存只有对应的​​CPU​​能够读写,其他​​CPU​​无权限读写。代码中通过​​__per_cpu_offset​​ 数组来记录每个 ​​CPU​​ 在 ​​percpu​​ 区域中私有数据地址距离 ​​__per_cpu_start​​ 起始地址的偏移量。我们访问每 ​​CPU​​ 变量就要依靠 ​​__per_cpu_offset + __per_cpu_start​​ 来确定访问地址。配置​​CPU0->state = CPUHP_ONLINE​​配置当前​​CPU0​​的 ​​percpu​​域地址偏移,将当前​​CPU​​的 ​​cpuinfo_arm64​​结构体信息写入​​percpu​​对应的内存中,同时写放当前​​CPU​​的运行环境(是否是运行在​​HYP mode​​),同时更新​​CPU​​负载能力等信息启动​​Boot pageset table​​页面集表,初始化所有可用的页表数 保存在 ​​vm_total_pages​​ 中 每个​​cpu​​一个​​Boot pageset table​​,用于所有区域和所有节点。设置参数的方式是,将列表中的项目立即移交给好友列表。 这是安全的,因为页面集操作是在禁用中断的情况下完成的。 即使在未使用的处理器和/或区域的引导完成后,也必须保留​​boot_pagesets​​ 页面集,它们确实在引导热插拔处理器方面发挥了作用。页表分配初始化,为系统设置一个​​page_alloc_cpu_notify​​回调函数,该函数用来实现​​CPU​​的关闭与使能。 在一个​​MPP​​结构的处理器系统或者大型服务器中有大量的​​CPU​​,该函数可以临时打开或者关闭某些​​Core​​或者​​CPU​​,此时​​Linux​​系统会调用​​page_alloc_cpu_notify​​函数。打印​​bootloader​​ 传入的​​command_line​​ 内容解析​​command_line​​中的 ​​early_param​​参数,调用 ​​parse_args​​来解析 ​​command_line​​字符串,解析后如果遇到​​console​​/​​earlycon​​相关的信息,则调用其​​setup_func()​​, 这个函数是 ​​early_param​​结构体中自带的解析所有​​static_command_line​​ 中的信息,这些起信息是通过 ​​setup_arch(&command_line)​​ 来生成的,最终会通过 ​​params[i].ops->set(val, ¶ms[i])​​ 来将其设置在​​params[i]​​中配置​​static_key_initialized = true​​初始化​​kernel ​​的 ​​log buffer​​,同时把 ​​kernel​​启动​​log​​拷贝进来 之前项目中,经常因为​​kernel logbuff ​​太少抓不到完整的​​kernel log​​时,就会把 ​​CONFIG_LOG_BUF_SHIFT​​值改大, 它作用的地方就是​​__log_buf​​,​​__log_buf[]​​是一个静态数组,数组大小就是 ​​1 << CONFIG_LOG_BUF_SHIFT​​,这块内存使用的是在静态变量区的内存初始化进程​​pidhash​​ 双向链表表头 ​​pid_hash​​​​pid​​哈希表根据机器中的内存量进行缩放。从最少​​16​​个插槽到​​4096​​个插槽,每​​GB​​或更多。 进程有自己单独的双向链表叫进程链表,而当我们要知道某个进程的状态时,每个进程描述符是通过​​PID​​来区分的,因为遍历查找效率太低了,且浪费​​CPU​​,此时就是通过​​pidhash​​来实现高效的快速查找​​Linux​​​文件子系统的 ​​vfs caches​​早期初始化,初始化 ​​dentry cache​​ 和 ​​inode cache​​。 初始化 目录项高速缓存​​dhash_entries​​、​​dentry_hashtable​​ 和 文件索引节点缓存 ​​ihash_entries​​、​​inode_hashtable​​,其中​​entries​​用于保存数的居, ​​hastable​​用于快速索引。​​Linux​​为了提高目录项对象的处理效率,设计与实现了目录项高速缓存(​​dentry cache​​,简称​​dcache​​), 目录项高速缓存​​dcache​​是索引节点缓存​​icache​​的主控器(​​master​​),也即​​dcache​​中的​​dentry​​对象控制着​​icache​​中的​​inode​​对象的生命期转换。​​dentry​​ 与 ​​inode​​是多对一的,每个​​inode​​可能有多个​​dentry​​,如硬链接的​​dentry​​不一样,​​inode​​却一样 因此无论何时,只要一个目录项对象存在于​​dcache​​中(非 ​​negative​​状态),则相应的​​inode​​就将总是存在,因为​​inode​​的引用计数​​i_count​​总是大于​​0​​,当​​dcache​​中的一个​​dentry​​被释放时,针对相应 ​​inode​​对象的 ​​iput()​​方法就会被调用。整理内核异常向量表​​exception table​​,分别保存在​​__start___ex_table​​ 和 ​​__stop___ex_table​​中,它们保存在​​__ex_table​​段中​​oops​​​等 ​​kernel​​异常​​BUG​​处理函数的初始化,保存在​​bug_break_hook.fn​​ 中, 在处理函数中如果是​​BUG Type​​, 则调用​​oops_enter()​​、​​console_verbose()​​等打印或保存相关的环境现场, 然后调用​​panic()​​触发一个​​Fata exception​​ 中断内存管理相关初始化:(1)​​page_ext_init_flatmem()​​ 分配​​page_ext​​所需的内存空间,​​page_ext​​主要是用于保存调用栈信息,每个​​page​​页都会在页初始化时存储好相关的调用栈信息(2)​​mem_init()​​:标记内存映射中的可用区域,并告诉我们有多少内存可用 (2.1)​​free_unused_memma()​​:释放所有未使用的内存映射 (2.2)​​free_all_bootmem()​​:​​bootmem​​是开机过程中建立的一个非常简单的临时内存管理系统, 所以当快开机结束时,它也就没有存在的必要了,因为后续会初始化更高级更完善的内存管理系统来接管它, 所以​​free_all_bootmem​​释放的不是已经申请的内存,而是​​bootmem​​没分配出去的内存, 调用​​free_all_bootmem​​以后​​bootmem​​就把自身也释放掉了,不可用了,之后会用更高级的内存管理系统, (2.3)​​kexec_reserve_crashkres_pages()​​:初始化 ​​kdump​​所需要的​​pages​​内存,即基于​​kexec​​的崩溃转储机制所需要的​​pages​​内存 (2.4)​​mem_init_print_info(NULL)​​:统计各内存信息,最后打印当前系统所有的​​Virtual kernel memory layout​​(3)​​kmem_cache_init()​​:初始化​​slab​​ 内存分配器, 初始化 ​​slab​​分配器的 ​​slab_caches​​全局链表 申请一些 ​​kmem_cache​​内存保存在 ​​kemem_cache->list​​链表上,内存大小取决于​​nr_node_ids & nr_cpu_ids​​, 然后将 ​​kemem_cache->list​​链表添加到 ​​slab_caches​​全局链表中, 然后创建 ​​kmem_cache_node​​数组,用于管理 ​​kmem_cache​​链表上的内存(4)​​pgtable_init()​​:页表缓存 ​​Page Cache​​初始化​​Page Cache​​ 的主要作用还是加速内存的访问,省去访问重复内容时频繁进行 ​​IO​​操作,这个效率太慢了。 因为,如果​​CPU​​如果要访问外部磁盘上的文件,需要首先将这些文件的内容拷贝到内存中,由于硬件的限制,从磁盘到内存的数据传输速度是很慢的,但如果,把这些​​page​​的内容提前加载到 ​​cache​​内存中的话,​​CPU​​访问起来就会快速很多。​​CPU​​ 查找内存时,首先会在 ​​page cache​​上找,如果 ​​hit​​ 命中了,也就是​​cache​​上刚好有所需要的内容,就直接读取, 如果未命中的话,再启动 ​​IO​​ 操作,将所需要的内容归属的一个​​page​​,全部加载到 ​​page cache​​中,下次如果还要访问这块数据,就不需要再进行 ​​IO​​操作了,直接从​​page cache​​上读取就好了。 同理,因为 ​​DDR​​物理内存有限,不可能无止境的加载 ​​page​​,同样也会有相应的淘汰机制, 一般 ​​page cache​​淘汰规则:是结合 最久未访问和访问次数来综合考量的,一般来说越久未被访问的​​page​​越容易被淘汰,同样的时间,访问次数越少的 ​​page​​也会相对越容易被淘汰。​​Linux​​系统使用的是最近最少使用(​​LRU​​)页面的衰老算法。(5)​​vmalloc_init()​​:虚拟内存管理初始化​​vmalloc​​用于分配虚拟地址连续(物理地址不连续)的内存空间,​​vzmalloc​​相对于​​vmalloc​​多了个清零初始化步骤;​​vmalloc/vzmalloc​​分配的虚拟地址范围在​​VMALLOC_START​​/​​VMALLOC_END​​之间,属于堆内存; 内核​​vmalloc​​区具体地址空间的管理是通过​​vmap_area​​管理的,该结构体记录整个区间的起始和结束;​​vmalloc​​是内核出于自身的目的使用高端内存页的少数情形之一​​ vmalloc​​ 在申请内存时逐页分配,确保在物理内存有严重碎片的情况下,​​vmalloc​​仍然可以工作​​​vmalloc_init()​​ 函数主要工作为: 遍历每个​​CPU​​的​​vmap_block_queue​​ 和 ​​vfree_deferred​​,​​vmap_block_queue​​是非连续内存块队列管理结构,主要是队列以及对应的保护锁​​vfree_deferred​​ 是 ​​vmalloc​​的内存延迟释放管理 接着将挂接在 ​​vmlist​​链表的各项通过 ​​__insert_vmap_area()​​输入到非连续内存块的管理中 最终配置全局静态变量 ​​vmap_area_pcpu_hole = VMALLOC_END​​ 标志其末尾地址虚拟内存技术主要目的还是解决 如何在有限的内存空间运行较大的应用程序的问题 实践和研究都证明:一个应用程序总是逐段被运行的,而且在一段时间内会稳定运行在某一段程序里。 这就给虚拟内存技术的应用提供了理论基础。 为了让程序顺利运行,最简单的就把要运行的那一段程序复制到内存中来运行,而其他暂时不运行的程序段就让它仍然留在磁盘上待命。 在计算机技术中,把内存中的程序段复制回磁盘 的做法叫做“换出”,而把磁盘中程序段映射到内存的做法叫做“换入”。 经过不断有目的的换入和换出,处理器就可以运行一个大于实际物理内存的应用程序了。 或者说,处理器似乎是拥有了一个大于实际物理内存的内存空间。 于是,这个存储空间叫做虚拟内存空间,而把真正的内存叫做实际物理内存,或简称为物理内存。一个系统采用了虚拟内存技术,那么它就存在着两个内存空间:虚拟内存空间和物理内存空间。 虚拟内存空间中的地址叫做“虚拟地址”;而实际物理内存空间中的地址叫做“实际物理地址”或“物理地址”。 处理器运算器和应用程序设计人员看到的只是虚拟内存空间和虚拟地址,而处理器片外的地址总线看到的只是物理地址空间和物理地址。由于存在两个内存地址,因此一个应用程序从编写到被执行,需要进行两次映射。 第一次是映射到虚拟内存空间,第二次时映射到物理内存空间。 在计算机系统中,这二次映射的工作是由硬件和软件共同来完成的。承担这个任务的硬件部分叫做存储管理单元MMU,软件部分就是操作系统的内存管理模块了。 在映射工作中,为了记录程序段占用物理内存的情况,操作系统的内存管理模块需要建立一个表格,该表格以虚拟地址为索引,记录了程序段所占用的物理内存的物理地址。这个虚拟地址/物理地址记录表便是存储管理单元MMU把虚拟地址转化为实际物理地址的依据。 虚拟内存技术的实现,是建立在应用程序可以分成段,并且具有“在任何时候正在使用的信息总是所有存储信息的一小部分”的局部特性基础上的。它是通过用辅存空间模拟RAM来实现的一种使机器的作业地址空间大于实际内存的技术。以存储单元为单位来管理显然不现实,因此Linux把虚存空间分成若干个大小相等的存储分区,Linux把这样的分区叫做页。 为了换入、换出的方便,物理内存也就按也得大小分成若干个块。 由于物理内存中的块空间是用来容纳虚存页的容器,所以物理内存中的块叫做页框。页与页框是Linux实现虚拟内存技术的基础。(6)​​ioremap_huge_init()​​:​​IO​​ 地址空间大页面映射初始化​​ioremap​​ 的主要作用是将一个​​IO​​地址空间映射到内核的虚拟地址空间上去,这样直接使用虚拟地址就能够对其访问 ,便于编程访问。 如果使能了​​CONFIG_ARM64_4K_PAGES​​,则配置​​ioremap_pud_capable = 1​​,标志可以进行​​4K​​ ​​IO​​大内存的映射分配​​ARM64​​默认支持 ​​ioremap_pmd_capable = 1​​有关​​PUD​​ 和 ​​PMD​​的概念:​​Linux​​系统使用了三级页表结构:页目录(​​Page Directory​​,​​PGD​​)、中间页目录(​​Page Middle Directory​​,​​PMD​​)、页表(​​Page Table​​,​​PTE​​),其中 ​​PGD​​中包含若干​​PUD​​的地址,​​PUD​​中包含若干​​PMD​​的地址,​​PMD​​中又包含若干​​PT​​的地址,每一个页表项指向一个页框,页框就是真正的物理内存页。(7)​​init_espfix_bsp()​​: 将​​espfix pud​​安装到内核页面目录中, 主要作用通过​​page_random​​的方式增加​​linux​​的安全 其中:​​init_espfix_random()​​ 主目创建​​page_random​​ 数。地址空间配置随机加载(​​ASLR​​)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化, 通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。​​Linux​​下的 ​​ASLR​​总共有 ​​3​​ 个级别,​​0、1、2​​​​0​​:就是关闭​​ASLR​​,没有随机化,堆栈基地址每次都相同,而且​​libc.so​​每次的地址也相同。​​1​​:是普通的​​ASLR​​。​​mmap​​基地址、栈基地址、​​.so​​加载基地址都将被随机化,但是堆没用随机化​​2​​:是增强的​​ASLR​​,增加了堆随机化(8)​​pti_init()​​:初始化​​KPTI​​ (​​Kernel PageTable Isolation​​) 内核页表隔离​​KPTI​​是由​​KAISER​​补丁修改而来。之前,进程地址空间被分成了内核地址空间和用户地址空间。 其中内核地址空间映射到了整个物理地址空间,而用户地址空间只能映射到指定的物理地址空间。 内核地址空间和用户地址空间共用一个页全局目录表(​​PGD​​表示进程的整个地址空间),​​meltdown​​漏洞就恰恰利用了这一点。攻击者在非法访问内核地址和​​CPU​​处理异常的时间窗口,通过访存微指令获取内核数据。 为了彻底防止用户程序获取内核数据,可以令内核地址空间和用户地址空间使用两组页表集(也就是使用两个​​PGD​​)。​​​KPTI​​ 实现原理为: 1、在运行应用程序的时候,将​​kernel mapping​​ 减少到最少,只保留必须的​​user​​到 ​​kernel​​ 的​​exception entry mapping​​. 其他的​​kernel mapping​​ 在运行​​user application​​时都去掉,变成无效​​mapping​​,这样的话,如果​​user​​访问​​kernel data​​, 在​​MMU​​地址转换的时候就会被挡掉(因为无效​​mapping​​). 2、设计一个​​trampoline​​ 的​​kernel PGD​​给运行​​user​​时用。​​Trampoline kernel mapping PGD​​只包含​​exception entry​​必需的​​mapping​​. 3、当​​user​​通过系统调用,或是​​timer​​或其他异常进入​​kernel​​是首先用​​trampoline​​的​​mapping​​, 接下来​​tramponline​​的​​vector​​处理会将​​kernel mapping​​ 换成正常的​​kernel mapping(SWAPPER_PGD_DIR)​​, 并直接跳转到​​kernel​​原来的​​vector entry​​, 继续正常处理。我们把上述过程称之为​​map kernel mapping​​. 4、当从​​kernel​​返回到​​user​​时,正常的​​kernel_exit​​会调用​​trampoline​​的​​exit​​,​​tramp_exit​​会重新将​​kernel mapping​​ 换成是​​trampoline​​. 这个过程叫​​unmap kernel mapping​​.

# buildsystem\android\kernel\init\main.casmlinkage __visible void __init start_kernel(void){ char *command_line; char *after_dashes; // 1. 栈溢出检测配置,设置 init_task任务的栈最低地址,使得init_task->stack指向栈底最后一个long的地址 set_task_stack_end_magic(&init_task); =================> + unsigned long *stackend; + stackend = end_of_stack(tsk); // 返回 init_task->stack地址,然后 + *stackend = STACK_END_MAGIC; // 配置init_task->stack = STACK_END_MAGIC = 0x57AC6E9D 为栈度Magic,主要目的是用于栈溢出检测 + --------------------> + - union thread_union { + - struct thread_info thread_info; + - unsigned long stack[THREAD_SIZE/sizeof(long)]; + - }; + <-------------------- <================= // 2. 触发CPU中断来获取ID,返回当前正在执行初始化的处理器ID,默认为CPU0, 会打印log: Booting Linux on physical CPU 0x0 smp_setup_processor_id(); // 3. 初始化hash buckets,最大16384个对象 ,将static object poll(obj_static_pool[i])的所有元素到链表中,最大为1024个元素,表头为 obj_pool debug_objects_early_init(); // 4. 初始化cgroup,cgroup是一种将进程按组管理的机制,其根节点为 struct cgroup_root cgrp_dfl_root cgroup_init_early(); // 5. 屏蔽当前CPU上的所有中断,此时送到当前CPU0上的所有中断信号都会被忽略,配置全局变量early_boot_irqs_disabled=true local_irq_disable(); early_boot_irqs_disabled = true; /* Interrupts are still disabled. Do necessary setups, then enable them. */ // 6. 更新当前CPU0 相关状态变量的值,使能为online、active、present、possible 为true boot_cpu_init(); // 7. 初始化高端内存,主要是初始化page_address_htable 链表,每当要申请映射一块Highmem空间时,就会的相应的信息添加到page_address_htable 链表中。 page_address_init(); // 8. 打印 Linux Version 系统版本号等信息 pr_notice("%s", linux_banner); // 9. 初始化ioremap、memlock、paging分页等内存映射相关的代码,将 command_line 指向 boot_command_line对应的内存; 调用cpu_prepare 初始化所有可用的cpu核心,注意此时还是单核CPU0在运行。 setup_arch(&command_line); /* Set up the the initial canary and entropy after arch and after adding latent and command line entropy.*/ add_latent_entropy(); // 宏没定义,空函数 // 10. 这个没看懂里面的input_pool 的作用是啥 add_device_randomness(command_line, strlen(command_line));+ // 11. 调用 get_random_bytes获取一个内核随机数,赋值给canary,保存在 init_task->stack_canary 中,主要目的还是防止栈溢出。 // 按我的理解是,代码会把这个canary值写在栈的最后一个long整形位置了, // 由于它是一个随机数,理论上通过读取它的值是否改变就能知道是否发现了栈溢出问题 boot_init_stack_canary(); // 12. 初始化清除cpumask中的所有CPU mm_init_cpumask(&init_mm); // 13. 调用__alloc_bootmem 从bootmem中申请 saved_command_line、initcall_command_line、static_command_line三块空间,将 boot_command_line的内容拷贝到 saved_command_line,将setup_arch()在kernel中动态生成的command_line内容,保存在static_command_line中 setup_command_line(command_line); =============================> + saved_command_line = memblock_virt_alloc(strlen(boot_command_line) + 1, 0); + initcall_command_line = memblock_virt_alloc(strlen(boot_command_line) + 1, 0); + static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0); + strcpy(saved_command_line, boot_command_line); + strcpy(static_command_line, command_line); <============================= // 14. nr_cpu_ids具有当前系统能够使用的CPU数,默认值为NR_CPUS=1值,它使用的是bit位的形式表示,类型是unsigned int,所以当前kernel最大支持32个CPU同时使用 setup_nr_cpu_ids(); // 15. 系统为多核CPU在percpu空间申请了对应的内存用于保存每个CPU的私有数据,这块内存只有对应的CPU能够读写,其他CPU无权限读写。代码中通过__per_cpu_offset数组来记录每个CPU在percpu区域中私有数据地址距离__per_cpu_start起始地址的偏移量。我们访问每CPU变量就要依靠__per_cpu_offset + __per_cpu_start 来确定访问地址。 setup_per_cpu_areas(); ===============================> + unsigned long __per_cpu_offset[NR_CPUS] __read_mostly; + delta = (unsigned long)pcpu_base_addr - (unsigned long)__per_cpu_start; + for_each_possible_cpu(cpu) + __per_cpu_offset[cpu] = delta + pcpu_unit_offsets[cpu]; <============================== // 16. 配置CPU0->state = CPUHP_ONLINE boot_cpu_state_init(); // 17. 配置当前CPU0的percpu域地址偏移,将当前CPU的 cpuinfo_arm64结构体信息写入percpu对应的内存中,同时写放当前CPU的运行环境(是否是运行在HYP mode),同时更新CPU负载能力等信息 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */ // 18. 启动Boot pageset table页面集表,初始化所有可用的页表数 保存在 vm_total_pages 中 // 每个cpu一个Boot pageset table,用于所有区域和所有节点。设置参数的方式是,将列表中的项目立即移交给好友列表。 // 这是安全的,因为页面集操作是在禁用中断的情况下完成的。 // 即使在未使用的处理器和/或区域的引导完成后,也必须保留boot_pagesets 页面集,它们确实在引导热插拔处理器方面发挥了作用。 build_all_zonelists(NULL); // 19. 页表分配初始化,为系统设置一个page_alloc_cpu_notify回调函数,该函数用来实现CPU的关闭与使能。 // 在一个MPP结构的处理器系统或者大型服务器中有大量的CPU,该函数可以临时打开或者关闭某些Core或者CPU,此时Linux系统会调用page_alloc_cpu_notify函数。 page_alloc_init(); // 20. 打印 bootloader 传入的command_line内容。 pr_notice("Kernel command line: %s\n", boot_command_line); // 21. 解析command_line中的early_param参数,调用parse_args来解析command_line字符串,解析后如果遇到console/earlycon相关的信息,则调用其setup_func(), 这个函数是 early_param结构体中自带的 parse_early_param(); ===============================> + for (p = __setup_start; p < __setup_end; p++) { + if ((p->early && parameq(param, p->str)) ||(strcmp(param, "console") == 0 && strcmp(p->str, "earlycon") == 0)) { + if (p->setup_func(val) != 0) pr_warn("Malformed early option '%s'\n", param); + } + } + // early_param参数 结构体定义如下: buildsystem\android\kernel\arch\um\include\shared\init.h + struct uml_param { + const char *str; + int (*setup_func)(char *, int *); + }; <=============================== // 22. 解析所有 static_command_line 中的信息,这些起信息是通过 setup_arch(&command_line) 来生成的,最终会通过 err = params[i].ops->set(val, ¶ms[i]); 来将其设置在params[i]中 after_dashes = parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, -1, -1, NULL, &unknown_bootoption); if (!IS_ERR_OR_NULL(after_dashes)) parse_args("Setting init args", after_dashes, NULL, 0, -1, -1, NULL, set_init_arg); // 23. 配置 `static_key_initialized = true` jump_label_init(); /* These use large bootmem allocations and must precede kmem_cache_init()*/ // 24. 初始化 kernel 的log buffer,同时把 kernel启动log拷贝进来 // 之前项目中,经常因为kernel logbuff 太少抓不到完整的kernel log时,就会把 CONFIG_LOG_BUF_SHIFT值改大,它作用的地方就是__log_buf, __log_buf是一个静态数组,数组大小就是 `1 << CONFIG_LOG_BUF_SHIFT`,这块内存使用的是在静态变量区的内存 setup_log_buf(0); =======================> + new_log_buf = memblock_virt_alloc(new_log_buf_len, LOG_ALIGN); + log_buf = new_log_buf; + memcpy(log_buf, __log_buf, __LOG_BUF_LEN); + + // buildsystem\android\kernel\kernel\printk\printk.c + #define __LOG_BUF_LEN (1 << CONFIG_LOG_BUF_SHIFT) + static char __log_buf[__LOG_BUF_LEN] __aligned(LOG_ALIGN); <======================= // 25. 初始化进程 pidhash 双向链表表头pid_hash // pid哈希表根据机器中的内存量进行缩放。从最少16个插槽到4096个插槽,每GB或更多。 // 进程有自己单独的双向链表叫进程链表,而当我们要知道某个进程的状态时,每个进程描述符是通过PID来区分的,因为遍历查找效率太低了,且浪费CPU,此时就是通过pidhash来实现快速查找 pidhash_init(); // 26. Linux文件子系统的 vfs caches早期初始化 // 初始化 目录项高速缓存dhash_entries、dentry_hashtable 和 ihash_entries、inode_hashtable,其中entries用于保存数的居, hastable用于快速索引 // Linux为了提高目录项对象的处理效率,设计与实现了目录项高速缓存(dentry cache,简称dcache) // 目录项高速缓存dcache是索引节点缓存icache的主控器(master),也即dcache中的dentry对象控制着icache中的inode对象的生命期转换。 // dentry与inode是多对一的,每个inode可能有多个dentry,如硬链接的dentry不一样,inode却一样 // 因此无论何时,只要一个目录项对象存在于dcache中(非 negative状态),则相应的inode就将总是存在,因为inode的引用计数i_count总是大于0。 // 当dcache中的一个dentry被释放时,针对相应inode对象的iput()方法就会被调用。 vfs_caches_init_early(); // 27. 整理内核异常向量表 exception table,分别保存在__start___ex_table 和 __stop___ex_table中,它们保存在__ex_table段中 sort_main_extable(); ========================> + . = ALIGN(4); + __ex_table : AT(ADDR(__ex_table) - LOAD_OFFSET) { + __start___ex_table = .; + #ifdef CONFIG_MMU + *(__ex_table) + #endif + __stop___ex_table = .; + } <======================== // 28. oops等kernel异常bug处理函数的初始化,保存在bug_break_hook.fn 中, // 在处理函数中如果是BUG Type, 则调用oops_enter()、console_verbose()等打或保存相关的环境现场,然后调用panic触发一个Fata exception中断 trap_init(); // 29. 内存管理相关初始化:(注意,此时只是释放及统计内存,并未初始化更高级的内存管理系统) mm_init(); ================================> + static void __init mm_init(void) + { + /* page_ext requires contiguous pages, bigger than MAX_ORDER unless SPARSEMEM. */ + // 分配page_ext所需的内存空间,page_ext主要是用于保存调用栈信息,每个page页都会在页初始化时存储好相关的调用栈信息 + page_ext_init_flatmem(); + + // `mem_init()`:标记内存映射中的可用区域,并告诉我们有多少内存可用, + // `bootmem`是开机过程中建立的一个非常简单的临时内存管理系统,所以当快开机结束时,它也就没有存在的必要了, + // 因为后续会初始化更高级更完善的内存管理系统来接管它, + // 所以`free_all_bootmem`释放的不是已经申请的内存,而是`bootmem`没分配出去的内存, + // 调用`free_all_bootmem`以后`bootmem`就把自身也释放掉了,不可用了,之后会用更高级的内存管理系统, + // 初始化 kdump,即基于kexec的崩溃转储机制所需要的pages内存 + // 然后统计各内存信息,最后打印当前系统所有的`Virtual kernel memory layout` + mem_init(); + ================================> + + free_unused_memmap(); // 释放所有未使用的内存映射 + + free_all_bootmem(); // 释放开机过程中建立的简易临时内存管理系统相关的资源 + + kexec_reserve_crashkres_pages(); // 初始化 kdump,即基于kexec的崩溃转储机制所需要的pages内存 + + mem_init_print_info(NULL); + + pr_notice("Virtual kernel memory layout:\n"); + + pr_notice(" kasan : 0x%16lx - 0x%16lx (%6ld GB)\n", MLG(KASAN_SHADOW_START, KASAN_SHADOW_END)); + + pr_notice(" modules : 0x%16lx - 0x%16lx (%6ld MB)\n", MLM(MODULES_VADDR, MODULES_END)); + + pr_notice(" vmalloc : 0x%16lx - 0x%16lx (%6ld GB)\n", MLG(VMALLOC_START, VMALLOC_END)); + + pr_notice(" .text : 0x%p" " - 0x%p" " (%6ld KB)\n", MLK_ROUNDUP(_text, _etext)); + + pr_notice(" .rodata : 0x%p" " - 0x%p" " (%6ld KB)\n", MLK_ROUNDUP(__start_rodata, __init_begin)); + + pr_notice(" .init : 0x%p" " - 0x%p" " (%6ld KB)\n", MLK_ROUNDUP(__init_begin, __init_end)); + + pr_notice(" .data : 0x%p" " - 0x%p" " (%6ld KB)\n", MLK_ROUNDUP(_sdata, _edata)); + + pr_notice(" .bss : 0x%p" " - 0x%p" " (%6ld KB)\n", MLK_ROUNDUP(__bss_start, __bss_stop)); + + pr_notice(" fixed : 0x%16lx - 0x%16lx (%6ld KB)\n", MLK(FIXADDR_START, FIXADDR_TOP)); + + pr_notice(" PCI I/O : 0x%16lx - 0x%16lx (%6ld MB)\n", MLM(PCI_IO_START, PCI_IO_END)); + + pr_notice(" vmemmap : 0x%16lx - 0x%16lx (%6ld GB maximum)\n", MLG(VMEMMAP_START, VMEMMAP_START + VMEMMAP_SIZE)); + + pr_notice(" 0x%16lx - 0x%16lx (%6ld MB actual)\n", MLM((unsigned long)phys_to_page(memblock_start_of_DRAM()), (unsigned long)virt_to_page(high_memory))); + + pr_notice(" memory : 0x%16lx - 0x%16lx (%6ld MB)\n", MLM(__phys_to_virt(memblock_start_of_DRAM()),(unsigned long)high_memory)); + <================================ + + // 初始化slab内存分配器, 初始化slab分配器的slab_caches全局链表 + // 申请一些 kmem_cache内存保存在kemem_cache->list链表上,内存大小取决于nr_node_ids & nr_cpu_ids, + // 然后将kemem_cache->list链表添加到slab_caches全局链表中, + // 然后创建kmem_cache_node数组,用于管理kmem_cache链表上的内存 + kmem_cache_init(); + + // 页表缓存 Page Cache初始化 + // Page Cache 的主要作用还是加速内存的访问, + // 因为,如果CPU如果要访问外部磁盘上的文件,需要首先将这些文件的内容拷贝到内存中,由于硬件的限制,从磁盘到内存的数据传输速度是很慢的,但如果,把这些page的内容提前加载到cache内存中的话,CPU访问起来就会快速很多。 + // CPU查找内存时,首先会在page cache上找,如果命中了,也就是cache上刚好有所需要的内容,就直接读取, + // 如果未命中的话,再启动IO操作,将所需要的内容归属的一个page,全部加载到page cache中,下次如果还要访问这块数据,就不需要再进行IO操作了,直接从page cache上读取就好了。 + // 同理为了DDR物理内存有限,不可能无止境的加载page内存,同样也会有相应的淘汰机制, + // 一般page cache淘汰规则:是结合 最久未访问和访问次数来综合考量的,一般来说越久未被访问越容易被淘汰,同样的时间,访问次数更少的page也会相对更容易被淘汰。 + // `Linux`系统使用的是最近最少使用(`LRU`)页面的衰老算法。 + pgtable_init(); + + // vmalloc用于分配虚拟地址连续(物理地址不连续)的内存空间,vzmalloc相对于vmalloc多了个0初始化; + // vmalloc/vzmalloc分配的虚拟地址范围在VMALLOC_START/VMALLOC_END之间,属于堆内存; + // 内核vmalloc区具体地址空间的管理是通过vmap_area管理的,该结构体记录整个区间的起始和结束; + // vmalloc是内核出于自身的目的使用高端内存页的少数情形之一 + // vmalloc在申请内存时逐页分配,确保在物理内存有严重碎片的情况下,vmalloc仍然可以工作 + // 主要工作为: + // 遍历每个CPU的vmap_block_queue 和 vfree_deferred, + // vmap_block_queue是非连续内存块队列管理结构,主要是队列以及对应的保护锁 + // vfree_deferred是vmalloc的内存延迟释放管理 + // 接着将挂接在vmlist链表的各项通过__insert_vmap_area()输入到非连续内存块的管理中 + // 最终配置全局静态变量 vmap_area_pcpu_hole = VMALLOC_END 标志vmalloc 末尾地址 + vmalloc_init(); + + // `ioremap` 的主要作用是将一个`IO`地址空间映射到内核的虚拟地址空间上去,这样直接使用虚拟地址就能够对其访问 ,便于编程访问 + // 如果使能了CONFIG_ARM64_4K_PAGES,则配置ioremap_pud_capable = 1,标志可以进行4K大内存的映射分配 + // ARM64默认支持 ioremap_pmd_capable = 1 + // 有关PUD和PMD的概念: + // PGD中包含若干PUD的地址,PUD中包含若干PMD的地址,PMD中又包含若干PT的地址。每一个页表项指向一个页框,页框就是真正的物理内存页 + ioremap_huge_init(); // IO 地址空间大页面映射初始化 + /* Should be run before the first non-init thread is created */ + + // 将espfix pud安装到内核页面目录中, 主要作用通过page_random的方式增加linux的安全. + // 其中:init_espfix_random() 主目创建page_random 数。 + // 地址空间配置随机加载(ASLR)是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化, + // 通过增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的的一种技术。 + // Linux下的ASLR总共有3个级别,0、1、2 + // 0就是关闭ASLR,没有随机化,堆栈基地址每次都相同,而且libc.so每次的地址也相同。 + // 1是普通的ASLR。mmap基地址、栈基地址、.so加载基地址都将被随机化,但是堆没用随机化 + // 2是增强的ASLR,增加了堆随机化 + init_espfix_bsp(); + + /* Should be run after espfix64 is set up. */ + // 初始化KPTI(Kernel PageTable Isolation)内核页表隔离 + // KPTI是由KAISER补丁修改而来。之前,进程地址空间被分成了内核地址空间和用户地址空间。 + // 其中内核地址空间映射到了整个物理地址空间,而用户地址空间只能映射到指定的物理地址空间。 + // 内核地址空间和用户地址空间共用一个页全局目录表(PGD表示进程的整个地址空间), + // meltdown漏洞就恰恰利用了这一点。攻击者在非法访问内核地址和CPU处理异常的时间窗口,通过访存微指令获取内核数据。 + // 为了彻底防止用户程序获取内核数据,可以令内核地址空间和用户地址空间使用两组页表集(也就是使用两个PGD)。 + + // KPTI实现原理为: + // 1、在运行应用程序的时候,将kernel mapping 减少到最少,只保留必须的user到kernel的exception entry mapping. + // 其他的kernel mapping 在运行user application时都去掉,变成无效mapping, + // 这样的话,如果user访问kernel data, 在MMU地址转换的时候就会被挡掉(因为无效mapping). + // 2、设计一个trampoline 的kernel PGD给运行user时用。Trampoline kernel mapping PGD只包含exception entry必需的mapping. + // 3、当user通过系统调用,或是timer或其他异常进入kernel是首先用trampoline的mapping, + // 接下来tramponline的vector处理会将kernel mapping 换成正常的kernel mapping(SWAPPER_PGD_DIR), + // 并直接跳转到kernel原来的vector entry, 继续正常处理。我们把上述过程称之为map kernel mapping. + // 4、当从kernel返回到user时,正常的kernel_exit会调用trampoline的exit, + // tramp_exit会重新将kernel mapping 换成是trampoline. 这个过程叫unmap kernel mapping. + pti_init(); + =====================> + + pti_clone_user_shared(); + + pti_clone_entry_text(); + + pti_setup_espfix64(); + + pti_setup_vsyscall(); + <===================== + } ================================> ftrace_init(); /* trace_printk can be enabled here */ early_trace_init(); /* * Set up the scheduler prior starting any interrupts (such as the * timer interrupt). Full topology setup happens at smp_init() * time - but meanwhile we still have a functioning scheduler. */ sched_init(); /* * Disable preemption - early bootup scheduling is extremely * fragile until we cpu_idle() for the first time. */ preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); radix_tree_init(); /* * Allow workqueue creation and work item queueing/cancelling * early. Work item execution depends on kthreads and starts after * workqueue_init(). */ workqueue_init_early(); rcu_init(); /* Trace events are available after this */ trace_init(); context_tracking_init(); /* init some links before init_ISA_irqs() */ early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); time_init(); sched_clock_postinit(); printk_safe_init(); perf_event_init(); profile_init(); call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early\n"); early_boot_irqs_disabled = false; local_irq_enable(); kmem_cache_init_late(); /* * HACK ALERT! This is early. We're enabling the console before * we've done PCI setups etc, and console_init() must be aware of * this. But we do want output early, in case something goes wrong. */ console_init(); if (panic_later) panic("Too many boot %s vars at `%s'", panic_later, panic_param); lockdep_info(); /* * Need to run this when irqs are enabled, because it wants * to self-test [hard/soft]-irqs on/off lock inversion bugs * too: */ locking_selftest(); /* * This needs to be called before any devices perform DMA * operations that might use the SWIOTLB bounce buffers. It will * mark the bounce buffers as decrypted so that their usage will * not cause "plain-text" data to be decrypted when accessed. */ mem_encrypt_init();#ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) { pr_crit("initrd overwritten (0x%08lx < 0x%08lx) - disabling it.\n", page_to_pfn(virt_to_page((void *)initrd_start)), min_low_pfn); initrd_start = 0; }#endif page_ext_init(); kmemleak_init(); debug_objects_mem_init(); setup_per_cpu_pageset(); numa_policy_init(); if (late_time_init) late_time_init(); calibrate_delay(); pidmap_init(); anon_vma_init(); acpi_early_init();#ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode();#endif thread_stack_cache_init(); cred_init(); fork_init(); proc_caches_init(); buffer_init(); key_init(); security_init(); dbg_late_init(); vfs_caches_init(); pagecache_init(); signals_init(); proc_root_init(); nsfs_init(); cpuset_init(); cgroup_init(); taskstats_init_early(); delayacct_init(); check_bugs(); acpi_subsystem_init(); arch_post_acpi_subsys_init(); sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_free_boot_services(); } /* Do the rest non-__init'ed, we're now alive */ rest_init();}

1.3 一号进程 init_task 结构体

# buildsystem\android\kernel\include\linux\init_task.h#define INIT_TASK(tsk)\{\ INIT_TASK_TI(tsk)\ .state = 0,\ .stack = init_stack,\ .usage = ATOMIC_INIT(2),\ .flags = PF_KTHREAD,\ .prio = MAX_PRIO-20,\ .static_prio = MAX_PRIO-20,\ .normal_prio = MAX_PRIO-20,\ .policy = SCHED_NORMAL,\ .cpus_allowed = CPU_MASK_ALL,\ .nr_cpus_allowed= NR_CPUS,\ .mm = NULL,\ .active_mm = &init_mm,\ .restart_block = {\ .fn = do_no_restart_syscall,\ },\ .se = {\ .group_node = LIST_HEAD_INIT(tsk.se.group_node),\ },\ .rt = {\ .run_list = LIST_HEAD_INIT(tsk.rt.run_list),\ .time_slice = RR_TIMESLICE,\ },\ .tasks = LIST_HEAD_INIT(tsk.tasks),\ INIT_PUSHABLE_TASKS(tsk)\ INIT_CGROUP_SCHED(tsk)\ .ptraced = LIST_HEAD_INIT(tsk.ptraced),\ .ptrace_entry = LIST_HEAD_INIT(tsk.ptrace_entry),\ .real_parent = &tsk,\ .parent = &tsk,\ .children = LIST_HEAD_INIT(tsk.children),\ .sibling = LIST_HEAD_INIT(tsk.sibling),\ .group_leader = &tsk,\ RCU_POINTER_INITIALIZER(real_cred, &init_cred),\ RCU_POINTER_INITIALIZER(cred, &init_cred),\ .comm = INIT_TASK_COMM,\ .thread = INIT_THREAD,\ .fs = &init_fs,\ .files = &init_files,\ .signal = &init_signals,\ .sighand = &init_sighand,\ .nsproxy = &init_nsproxy,\ .pending = {\ .list = LIST_HEAD_INIT(tsk.pending.list),\ .signal = {{0}}},\ .blocked = {{0}},\ .alloc_lock = __SPIN_LOCK_UNLOCKED(tsk.alloc_lock),\ .journal_info = NULL,\ INIT_CPU_TIMERS(tsk)\ .pi_lock = __RAW_SPIN_LOCK_UNLOCKED(tsk.pi_lock),\ .timer_slack_ns = 50000,/* 50 usec default slack */ \ .pids = {\ [PIDTYPE_PID] = INIT_PID_LINK(PIDTYPE_PID),\ [PIDTYPE_PGID] = INIT_PID_LINK(PIDTYPE_PGID),\ [PIDTYPE_SID] = INIT_PID_LINK(PIDTYPE_SID),\ },\ .thread_group = LIST_HEAD_INIT(tsk.thread_group),\ .thread_node = LIST_HEAD_INIT(init_signals.thread_head),\ INIT_IDS \ INIT_PERF_EVENTS(tsk)\ INIT_TRACE_IRQFLAGS \ INIT_LOCKDEP \ INIT_FTRACE_GRAPH \ INIT_TRACE_RECURSION \ INIT_TASK_RCU_PREEMPT(tsk)\ INIT_TASK_RCU_TASKS(tsk)\ INIT_CPUSET_SEQ(tsk)\ INIT_RT_MUTEXES(tsk)\ INIT_PREV_CPUTIME(tsk)\ INIT_VTIME(tsk)\ INIT_NUMA_BALANCING(tsk)\ INIT_KASAN(tsk)\ INIT_LIVEPATCH(tsk)\ INIT_TASK_SECURITY \}

《android\kernel\Documentation\translations\zh_CN\arm64》《​​Linux的虚拟内存详解(MMU、页表结构)​​》

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:全局记录Feign的请求和响应日志方式
下一篇:【无服务器架构】跨平台的无服务器计算Knative 简介
相关文章

 发表评论

暂时没有评论,来抢沙发吧~