【SemiDrive源码分析】【X9芯片启动流程】30 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(一)
本 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源码分析】【驱动BringUp】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 函数详细分析(一)》
- 《【SemiDrive源码分析】【X9芯片启动流程】31 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(二)》
- 《【SemiDrive源码分析】【Display模块】32 - RTOS侧 Serdes屏驱动硬件原理及代码配置步骤》
- 《【SemiDrive源码分析】【Display模块】33 - 相关概念解析》
- 《【SemiDrive源码分析】【Display模块】34 - RTOS侧 sdm_display_init 显示初始化源码分析》
- 《【SemiDrive源码分析】【Display模块】35 - RTOS侧 sdm_display_init 显示初始化源码分析 之 MIPI DSI、LVDS屏驱动探测初始化流程》
- 《【SemiDrive源码分析】【Display模块】36 - Android侧 DRM代码分析》
- 《【SemiDrive源码分析】【驱动BringUp】37 - LCM 驱动 Bringup 流程》
- 《【SemiDrive源码分析】【驱动BringUp】38 - NorFlash & eMMC分区配置》
待写:
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篇》
33. 《【SemiDrive源码分析】【Display模块】36 - RTOS侧 LVGL 开源GUI库分析》
边看边总结,不知不觉已经1个多月了,已经写到第30篇文章了,
主要目的,还是支持芯片国产化,加上之前调高通、MTK时,自已带了这么多个项目,却没有想过去写一套完整的总结出来,有点小遗憾,
干脆就从芯驰开始补足这个遗憾,每天抽时间出来总结,希望整理一套完整的总结出来,尽量做到从入门到入土(哈哈,开玩笑的)。
希望的话,这个系列的文章,我能坚持维护到项目量产,
内容争取 涵盖 启动流程代码分析、BSP模块代码移植、各模块硬件工作原理、各模块代码框架从底层到上层的分析、以及项目中遇到的问题分析过程及总结等等,毕竟是私人总结,视精力而定,能写多少写多少,有啥写啥,能共享就共享,不能共享就设私密,这个没啥好说的。
主要目标还是:
芯驰X9HP平台全套总结文档,函盖如下:
- 启动流程代码分析
- 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_switch
ENDPROC(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
. -
ftrace
功能基础初始化
ftrace
是Function Trace
的意思,最开始主要用于记录内核函数运行轨迹;随着功能的逐渐增加,演变成一个跟踪框架。
主要分为两种类型:
(1)静态探测点,是在内核代码中调用ftrace
提供的相应接口实现,称之为静态是因为,是在内核代码中写死的,静态编译到内核代码中的,在内核编译后,就不能再动态修改。在开启ftrace
相关的内核配置选项后,内核中已经在一些关键的地方设置了静态探测点,需要使用时,即可查看到相应的信息。
(2)动态探测点,基本原理为:利用mcount
机制,在内核编译时,在每个函数入口保留数个字节,然后在使用ftrace
时,将保留的字节替换为需要的指令,比如跳转到需要的执行探测操作的代码。 -
使能
trace_printk
功能,并且分配相关的tracing_buffer
-
初始化调度器
sched_init()
,当前kernel
目前支持FAIR sched
和RT sched
两种调度方式:
(1)有五种调度类,优先级从高到低分别为:
a.stop_sched_class
: 优先级最⾼的调度类,它与idle_sched_class
⼀样,是⼀个专⽤的调度类型(除了migration
线程之外,其他的 task 都是不能或者说不应该被设置为stop
调度类)。该调度类专⽤于实现类似active balance
或stop machine
等依赖于migration
线程执⾏的“紧急”任务。
b.dl_sched_class
:deadline
调度类的优先级仅次于stop
调度类,它是⼀种基于EDL
算法实现的实时调度器(或者说调度策略)。
c.rt_sched_class
:rt
调度类的优先级要低于dl
调度类,是⼀种基于优先级实现的实时调度器。
d.fair_sched_class
:CFS
调度器的优先级要低于上⾯的三个调度类,它是基于公平调度思想⽽设计的调度类型,是Linux
内核的默认调度类。
e.idle_sched_class
:idle
调度类型是swapper
线程,主要是让swapper
线程接管CPU
,通过cpuidle/nohz
等框架让CPU
进⼊节能状态。(2)接着初始化每个
CPU
调度时所需要的内存、锁、队列等资源。
(3)根据sched load_weight
来制定调度策略sched policy
,同时配置idle_polic weight = WEIGHT_IDLEPRIO
,其他的任务则是根据sched_prio_to_wmult
数组结合优先及来制定调试策略(4)初始化当前处理器的空闲进程,实际上真正的
idle
进程是init
进程,也就是init_task
静态定义的进程,
init
进程是所有用户空间进程的祖先进程,而idle
进程是所有进程的祖先进程, 每个cpu
都有一个idle
进程,
(5)bootcpu
进入idle
进程的流程为:
在init
进程创建完成后,该进程还会创建kthreadd
进程,然后调用rest_init->cpu_startup_entry
,最后进入do_idle
,让cpu
进入idle
状态
(6)剩下的cpu
的进入idle
的流程为:secondary_startup –> __secondary_switched –> secondary_start_kernel –> cpu_startup_entry->do_idle
(7)初始化fair_sched_class
(8)初始化调度状态
(9)初始化psi
,主要目的是实现周期性的对CPU
、Memory
、IO
等资源的监控与管理,以实现更大的资源利用率
Pressure Stall Information
提供了一种评估系统资源压力的方法。
系统有三个基础资源:CPU
、Memory
和IO
,无论这些资源配置如何增加,似乎永远无法满足软件的需求。
一旦产生资源竞争,就有可能带来延迟增大,使用户体验到卡顿。
如果没有一种相对准确的方法检测系统的资源压力程度,有两种后果。
一种是资源使用者过度克制,没有充分使用系统资源; 另一种是经常产生资源竞争,过度使用资源导致等待延迟过大。
准确的检测方法可以帮忙资源使用者确定合适的工作量,同时也可以帮助系统制定高效的资源调度策略,最大化利用系统资源,最大化改善用户体验。 -
禁止内核抢占,因为在初始化还未完成时,有些资源还没准备好,直接进行进程调试容易出来,这个最好是
cpu
进入idle
时再开启会安全些 -
radix
树初始化:
Radix-Tree
在Linux
内核中有着⼴泛的应⽤,如page cache
,swap cache
通过Radix-Tree
来管理虚拟地址到page cache
之间的映射关系.
Radix-Tree
的特点是可以通过整数作为index
来找到对应的数据结构,⽽⽆需像数组⼀样需要事先定义好整数index
的范围,
也就是Index
可以是离散的,查找速度相较数组也不会逊⾊太多,在空间和时间上取得⼀个均衡.
linux
内存管理是通过radix
树跟踪绑定到地址映射上的核心页,该radix
树允许内存管理代码快速查找标识为dirty
或writeback
的page
页 -
初始化每个
cpu
的worker_pool
,初始化一些系统默认的workqueue
每个执行work
的线程叫做worker
,一组worker
的集合叫做worker_pool
。
每个worker
对应一个kernel thread
内核线程,一个worker_pool
对应一个或者多个worker
。
多个worker
从同一个链表中worker_pool->worklist
获取work
进行处理。workqueue
就是存放一组work
的集合,基本可以分为两类:一类系统创建的workqueue
,一类是用户自己创建的workqueue
。
内核默认创建了一些工作队列(用户也可以创建):
system_wq
:如果work item
执行时间较短,使用本队列,调用schedule_work()
接口就是添加到本队列中;
system_highpri_wq
:高优先级工作队列,以nice
值-20
来运行;
system_long_wq
:如果work item
执行时间较长,使用本队列;
system_unbound_wq
:该工作队列的内核线程不绑定到特定的处理器上;
system_freezable_wq
:该工作队列用于在Suspend
时可冻结的work item
;
system_power_efficient_wq
:该工作队列用于节能目的而选择牺牲性能的work item
;
system_freezable_power_efficient_wq
:该工作队列用于节能或Suspend
时可冻结目的的work item
; -
考虑到篇幅问题,后续代码分析见:
《【SemiDrive源码分析】【X9芯片启动流程】31 - AP1 Android Kernel 启动流程 start_kernel 函数详细分析(二)》
源码如下:
# buildsystem\android\kernel\init\main.c
asmlinkage __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();
+ <=====================
+ }
================================>
// 30. ftrace 功能基础初始化
// ftrace是Function Trace的意思,最开始主要用于记录内核函数运行轨迹;随着功能的逐渐增加,演变成一个跟踪框架。
// 静态探测点,是在内核代码中调用ftrace提供的相应接口实现,称之为静态是因为,是在内核代码中写死的,静态编译到内核代码中的,在内核编译后,就不能再动态修改。在开启ftrace相关的内核配置选项后,内核中已经在一些关键的地方设置了静态探测点,需要使用时,即可查看到相应的信息。
// 动态探测点,基本原理为:利用mcount机制,在内核编译时,在每个函数入口保留数个字节,然后在使用ftrace时,将保留的字节替换为需要的指令,比如跳转到需要的执行探测操作的代码。
ftrace_init();
/* trace_printk can be enabled here */
// 31. 使能trace_printk 功能,并且分配相关的tracing_buffer
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. */
// 32. 初始化调度器
sched_init();
===========================>
+ void __init sched_init(void)
+ {
+ sched_clock_init(); // 启动调度器时钟,配置 sched_clock_running=1
+ wait_bit_init();
+
+ // 当前kernel 目前支持 FAIR sched和 RT sched两种调度方式:
+ // 有五种调度类,优先级从高到低分别为:
+ // (1) stop_sched_class: 优先级最⾼的调度类,它与 idle_sched_class⼀样,是⼀个专⽤的调度类型(除了 migration 线程之外,其他的 task 都是不能或者说不应该被设置为 stop 调度类)。该调度类专⽤于实现类似 active balance 或 stop machine 等依赖于 migration 线程执⾏的“紧急”任务。
+ // (2) dl_sched_class: deadline 调度类的优先级仅次于 stop 调度类,它是⼀种基于 EDL 算法实现的实时调度器(或者说调度策略)。
+ // (3) rt_sched_class: rt 调度类的优先级要低于 dl 调度类,是⼀种基于优先级实现的实时调度器。
+ // (4) fair_sched_class: CFS 调度器的优先级要低于上⾯的三个调度类,它是基于公平调度思想⽽设计的调度类型,是Linux 内核的默认调度类。
+ // (5) idle_sched_class: idle 调度类型是 swapper 线程,主要是让 swapper 线程接管 CPU,通过 cpuidle/nohz 等框架让 CPU 进⼊节能状态。
+ #ifdef CONFIG_FAIR_GROUP_SCHED
+ alloc_size += 2 * nr_cpu_ids * sizeof(void **);
+ #endif
+ #ifdef CONFIG_RT_GROUP_SCHED
+ alloc_size += 2 * nr_cpu_ids * sizeof(void **);
+ #endif
+ // 这部分代码主要是初始化每个CPU调度时所需要的内存、锁、队列等资源。
+ // ......省略一部分代码......
+
+ // 根据sched load_weight 来制定调度策略 sched policy,同时配置 idle_polic weight = WEIGHT_IDLEPRIO,其他的任务则是根据sched_prio_to_wmult数组结合优先及来制定调试策略
+ set_load_weight(&init_task);
+
+ // 初始化当前处理器的空闲进程,实际上真正的idle进程是init进程,也就是init_task静态定义的进程,
+ // init进程是所有用户空间进程的祖先进程,而idle进程是所有进程的祖先进程, 每个cpu 都有一个idle进程,
+ // bootcpu 进入idle进程的流程为:
+ // 在init进程创建完成后,该进程还会创建kthreadd进程,然后调用rest_init->cpu_startup_entry,最后进入do_idle,让cpu 进入idle状态
+ // 剩下的cpu的进入idle的流程为:
+ // secondary_startup –> __secondary_switched –> secondary_start_kernel –> cpu_startup_entry->do_idle
+
+ /** Make us the idle thread. Technically, schedule() should not be called from this thread,
+ * however somewhere below it might be, but because we are the idle thread,
+ * we just pick up running again when this runqueue becomes "idle". */
+ init_idle(current, smp_processor_id());
+
+ // 初始化 fair_sched_class
+ init_sched_fair_class();
+
+ // 初始化调度状态
+ init_schedstats();
+ // 初始化psi,主要目的是实现周期性的对CPU、Memory、IO等资源的监控与管理,以实现更大的资源利用率
+ // Pressure Stall Information 提供了一种评估系统资源压力的方法。
+ // 系统有三个基础资源:CPU、Memory 和 IO,无论这些资源配置如何增加,似乎永远无法满足软件的需求。
+ // 一旦产生资源竞争,就有可能带来延迟增大,使用户体验到卡顿。
+ // 如果没有一种相对准确的方法检测系统的资源压力程度,有两种后果。
+ // 一种是资源使用者过度克制,没有充分使用系统资源;
+ // 另一种是经常产生资源竞争,过度使用资源导致等待延迟过大。
+ // 准确的检测方法可以帮忙资源使用者确定合适的工作量,同时也可以帮助系统制定高效的资源调度策略,最大化利用系统资源,最大化改善用户体验。
+ psi_init();
+ scheduler_running = 1;
+ }
<===========================
// 33. 禁止内核抢占,因为在初始化还未完成时,有些资源还没准备好,直接进行进程调试容易出来,这个最好是cpu进入idle时再开启会安全些
/* Disable preemption - early bootup scheduling is extremely fragile until we cpu_idle() for the first time. */
preempt_disable();
// 34. radix树初始化:
// Radix-Tree在Linux内核中有着⼴泛的应⽤,如page cache,swap cache通过Radix-Tree来管理虚拟地址到page cache之间的映射关系.
// Radix-Tree的特点是可以通过整数作为index来找到对应的数据结构,⽽⽆需像数组⼀样需要事先定义好整数index的范围,
// 也就是Index可以是离散的,查找速度相较数组也不会逊⾊太多,在空间和时间上取得⼀个均衡.
// linux内存管理是通过radix树跟踪绑定到地址映射上的核心页,该radix树允许内存管理代码快速查找标识为dirty或writeback的page页
radix_tree_init();
/* Allow workqueue creation and work item queueing/cancelling early.
Work item execution depends on kthreads and starts after workqueue_init(). */
// 35. 初始化每个cpu的worker_pool,
// 每个执行 work 的线程叫做 worker,一组 worker 的集合叫做 worker_pool。
// 每个 worker 对应一个 kernel thread内核线程,一个 worker_pool 对应一个或者多个 worker。
// 多个 worker 从同一个链表中 worker_pool->worklist 获取 work 进行处理。
// workqueue 就是存放一组 work 的集合,基本可以分为两类:一类系统创建的 workqueue,一类是用户自己创建的 workqueue。
// 内核默认创建了一些工作队列(用户也可以创建):
// system_wq:如果work item执行时间较短,使用本队列,调用schedule_work()接口就是添加到本队列中;
// system_highpri_wq:高优先级工作队列,以nice值-20来运行;
// system_long_wq:如果work item执行时间较长,使用本队列;
// system_unbound_wq:该工作队列的内核线程不绑定到特定的处理器上;
// system_freezable_wq:该工作队列用于在Suspend时可冻结的work item;
// system_power_efficient_wq:该工作队列用于节能目的而选择牺牲性能的work item;
// system_freezable_power_efficient_wq:该工作队列用于节能或Suspend时可冻结目的的work item;
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、页表结构)》
- 《纯干货,PSI 原理解析与应用》