C0:
bit31:24: 厂商编号, 0X41, ARM。
bit23:20: 内核架构的主版本号, ARM 内核版本一般使用 rnpn 来表示,
比如 r0p1,其中r0后面的 0 就是内核架构主版本号。
bit19:16: 架构代码, 0XF, ARMv7 架构。
bit15:4: 内核版本号, 0XC07, Cortex-A7 MPCore 内核。
bit3:0: 内核架构的次版本号, rnpn 中的 pn,比如 r0p1 中 p1 后面的 1 就是次版本号。
C1:
bit13: V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可以使用 VBAR 来重映射此基地址,也就是中断向量表重定位。为 1 的话中断向量表基地址为0XFFFF0000,此基地址不能被重映射。
bit12: I, I Cache 使能位,为 0 的话关闭 I Cache,为 1 的话使能 I Cache。
bit11: Z,分支预测使能位,如果开启 MMU 的话,此为也会使能。
bit10: SW, SWP 和 SWPB 使能位,当为 0 的话关闭 SWP 和 SWPB 指令,当为 1 的时候就使能 SWP 和 SWPB 指令。
bit9:3: 未使用,保留。
bit2: C, D Cache 和缓存一致性使能位,为 0 的时候禁止 D Cache 和缓存一致性,为 1 时
使能。
bit1: A,内存对齐检查使能位,为 0 的时候关闭内存对齐检查,为 1 的时候使能内存对齐检查。
bit0: M, MMU 使能位,为 0 的时候禁止 MMU,为 1 的时候使能 MMU。
C12:
向量表基地址寄存器。设置中断向量表偏移的时候就需要将新的中断向量表基地址写入 VBAR 中,比如在前面的例程中,代码链接的起始地址为0X87800000,而中断向量表肯定要放到最前面,也就是 0X87800000 这个地址处。
所以就需要设置 VBAR 为 0X87800000
C15:
GIC 的基地址就保存在 CBAR
获取到 GIC 基地址以后就可以设置 GIC 相关寄存器了,比如我们可以读取当前中断 ID,当前中断 ID 保存在 GICC_IAR 中,寄存器 GICC_IAR 属于 CPU 接口端寄存器,寄存器地址相对于 CPU 接口
端起始地址的偏移为 0XC,因此获取当前中断 ID 的代码如下:
MRC p15, 4, r1, c15, c0, 0 ;获取 GIC 基地址
ADD r1, r1, #0X2000 ;GIC 基地址加 0X2000 得到 CPU 接口端寄存器起始地址
LDR r0, [r1, #0XC] ;读取 CPU 接口端起始地址+0XC 处的寄存器值,也就是寄存器
;GIC_IAR 的值
_start //uboot程序入口点 arch/arm/lib/vectors.S
--> b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq
ldr pc, _fiq
_undefined_instruction: .word undefined_instruction
_software_interrupt: .word software_interrupt
_prefetch_abort: .word prefetch_abort
_data_abort: .word data_abort
_not_used: .word not_used
_irq: .word irq
_fiq: .word fiq
undefined_instruction:
bl do_undefined_instruction
software_interrupt:
bl do_software_interrupt
prefetch_abort:
bl do_prefetch_abort
data_abort:
bl do_data_abort
not_used:
bl do_not_used
irq:
bl do_irq
fiq:
bl do_fiq
-->save_boot_params //arch/arm/cpu/armv7/start.S
-->save_boot_params_ret //设置SVC模式,关闭中断,设置cp15的c1的V位为0,将_start的地址写入C12,即中断向量偏移地址
V , 中断向量表基地址选择位,为 0 的话中断向量表基地址为 0X00000000,软件可以使用 VBAR 来重映射此基地址,也就是中断向量表重定位。
为 1 的话中断向量表基地址为0XFFFF0000,此基地址不能被重映射。
-->cpu_init_cp15 //设置cache,MMU,TLBs及指令cache
禁止从TLB中取地址描述符,也就是禁止虚拟地址到物理地址的转换,因为刚开始操作的都是物理寄存器
指令cache可关闭,可不关闭,这里关闭
关闭分支预测
DSB数据同步隔离:仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令
ISB指令同步隔离:它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令
清除c1的V,即0x00000000,允许C12中断向量映射
清除CAM,禁止相应功能,即C:D Cache 和缓存一致性,A:内存对齐检查,M:MMU
数据缓存一定要关,这时候RAM中数据还没有cache过来,导致数据预取异常
使能对齐检测和使能分支预测,根据CONFIG_SYS_ICACHE_OFF选择是否使能指令cache
读取cpu内核主次版本号
-->cpu_init_crit //特定板级初始化,片上ROM已经初始化基础内存。
-->lowlevel_init //arch/arm/cpu/armv7/lowlevel_init.S
为调用c函数准备一个临时堆栈,根据CONFIG_SYS_INIT_SP_ADDR设置临时堆栈,这个堆栈在cpu的片上内存
并对堆栈进行8字节对齐:
AAPCS 规则要求堆栈保持 8 字节对齐。如果不对齐,调用一般的函数也是没问题的。但是当调用需要严格遵守 AAPCS 规则的函数时可能会出错。
例如调用 sprintf 输出一个浮点数时,栈必须是 8 字节对齐的,否则结果可能会出错。
栈指针减去GD_SIZE,并7字节对齐,为全局变量预留临时空间
//GD_SIZE定义在include/generated/generic-asm-offsets.h,为global_data结构体大小248字节
struct global_data是描述板子信息的全局结构体(临时的),定义在include/asm-generic/global_data.h
-->s_init // arch/arm/cpu/armv7/mx6/soc.c
-->is_cpu_type //判断CPU型号,这里imx6ull返回
-->_main //根据CONFIG_SYS_INIT_SP_ADDR设置堆栈,8字节对齐 arch/arm/lib/crt0.S
-->board_init_f_alloc_reserve //堆栈下移,预留全局的数据空间gd和early malloc common/init/board_init.c
-->board_init_f_init_reserve //gd的初始化,清零处理,gd->malloc_base指向early malloc的基地址
-->board_init_f //设置gd->flags、gd->have_console为0, common/board_f.c
初始化一系列外设
初始化 gd 的各个成员变量,形成完整的内存分配图,便于uboot重定位到DRAM的最后面地址区域,为kernel腾出空间
-->initcall_run_list //依次执行init_sequence_f指向的各个板级初始化函数
--> setup_mon_len //设置gd->mon_len:即由链接脚本指定的uboot的整个长度
根据uboot.lds,整个uboot由:
text:代码段
rodata:只读数据段
data:读/写数据段
u_boot_list:存放u-boot自有的一些function,例如u-boot command等
got:uboot自定义的一个段
u_boot_cmd:uboot把所有的uboot命令放在该段
bss:bss段
initf_malloc //初始化 gd 中跟 malloc 有关的成员变量,gd->malloc_limit表示内存池的大小
arch_cpu_init //相关体系结构cpu初始化
-->init_aips //aips初始化
-->clear_mmdc_ch_mask //
-->init_bandgap //
-->imx_set_wdog_powerdown //关闭看门狗?
-->init_src //使能热重启?
initf_dm //
-->dm_init_and_scan //驱动模型的一些初始化,执行bind操作,初始化一个dm模型的树形结构 (driver module)
和DM相关的初始化流程主要有两次,分别是initf_dm和initr_dm
-->dm_init //驱动模型的一些初始化,将根节点绑定到gd->dm_root上,初始化根节点设备
-->device_bind_by_name //struct driver_info {
const char *name;
const void *platdata;
}
此处name = "root_driver"
gd->dm_root和gd->uclass_root均为struct udevice指针
-->lists_driver_lookup_name //找到u_boot_list_2_#_list_2段中struct driver->name为root_driver的struct driver结构体
#define ll_entry_declare(_type, _name, _list) \
_type _u_boot_list_2_##_list##_2_##_name \
__attribute__((unused, \
section(".u_boot_list_2_"#_list"_2_"#_name)))
//定义了一个变量,并指定了变量的unused、section属性。变量类型是_type, 变量名
是_u_boot_list_2_##_list##_2_##_name, 变量属性是unused,section(".u_boot_list_2_"#_list#_2_"#_name")。
虽然变量名很长,但功能很简单:定义一个全局变量,并将变量放入到特定段中
ll_entry_start(_type, _list)
定义了一个_list元素,类型为 char [0], 即char ×类型,放置在u_boot_list_2_#_list_1段中。
由于char 类型、长度为0的数组并不占用空间,只定义了一个符号
由ll_entry_declare()定义的元素,都放在u_boot_list_2_#_list_2_*段中。
在链接器脚本中,对u_boot_list段的定义如下:
. = ALIGN(4);
.u_boot_list : {
KEEP(*(SORT(.u_boot_list*)));
}
经过SORT()后,u_boot_list_2_#_list_1会排在所有前缀为u_boot_list_2_#_list_2_段之前。
这样就会使ll_entry_start()中定义的符号,指向所有前缀为u_boot_list_2_#_list_2段的第一个段,即_list的起始位置。
ll_entry_end(_type, _list)
声明了一个符号end, 定义在u_boot_list_#_list_3段中,sort之后,排在u_boot_list_2_#_list_2段的最后一个
-->device_bind //绑定udevice gd->dm_root指向分配的udevice设备和driver_info root_driver指向的u_boot_list_2_#_list_2段驱动
-->uclass_get
-->calloc //分配struct udevice
-->device_probe //未执行
-->dm_scan_platdata //扫描U_BOOT_DEVICE定义的设备,与U_BOOT_DRIVER定义的driver进行查找,并绑定相应driver
-->lists_bind_drivers
-->device_bind_by_name
-->dm_scan_fdt //可选,若支持设备树,则需要逐步扫描并挂载相应的节点
-->lists_bind_fdt
-->dm_scan_other //留空
arch_cpu_init_dm //留空
mark_bootstage
-->bootstage_mark_name
-->bootstage_add_record //record数组,以id号为下标,记录时间、函数名、引导的阶段id号(引导的不同阶段有不同的id号)
-->show_boot_progress //空函数
board_early_init_f //串口io相关初始化,这里不进入这个函数
timer_init //初始化定时器
board_postclk_init //设置VDDSOC电压为1.175V
-->set_ldo_voltage
get_clocks //设置gd->arch.sdhc_clk的值,即根据CONFIG_SYS_FSL_ESDHC_ADDR定义为哪个SD卡的宏,例如USDHC2_BASE_ADDR来获取
-->mxc_get_clock //获取指定的时钟值
env_init //设置环境变量的保存地址gd->env_addr为default_environment[index]
根据CONFIG_ENV_IS_IN_XXX编译对应存储设备的环境变量存储驱动文件
init_baud_rate //设置gd->baudrate的值
-->getenv_ulong //CONFIG_BAUDRATE宏设置的默认波特率,若有,则读取,否则设置为CONFIG_BAUDRATE默认的
-->getenv //获取波特率数值字符串
-->getenv_f
-->env_get_char
-->env_get_char_init
-->default_environment[index]
-->simple_strtoul //将一个字符串(波特率)转换成unsigend long long型数据
serial_init //设置gd->flags的对应位,初始化串口
console_init_f //设置 gd->have_console 为 1,表示有个控制台,此函数也将前面暂存在缓冲区中的数据通过控制台打印出来
display_options //输出uboot版本相关信息
display_text_info //打印代码起止地址和debug信息
print_cpuinfo //打印CPU信息
show_board_info //打印板子信息
-->checkboard //打印固定信息
INIT_FUNC_WATCHDOG_INIT //初始化看门狗,这里为空
INIT_FUNC_WATCHDOG_RESET //复位看门狗,这里为空
init_func_i2c //初始化i2c,打印"I2C: ready"
init_func_spi //初始化spi并打印
announce_dram_init //输出字符串“DRAM:”
dram_init //设置gd->ram_size,不是真的初始化DRAM
-->imx_ddr_size
post_init_f //依次调用一些函数进行测试,并初始化gd->post_init_f_time
setup_dest_addr //设置gd->ram_size,gd->ram_top,gd->relocaddr
reserve_round_4k //对gd->relocaddr做4k对齐
reserve_mmu //留出 MMU 的 TLB 表的位置,分配 MMU 的 TLB 表内存以后会对 gd->relocaddr 做 64K 字节对
齐,赋值给gd->arch.tlb_size
reserve_trace //留出跟踪调试用的内存空间,未用到
reserve_uboot //留出重定位后的 uboot 所占用的内存区域,uboot 所占用大小由gd->mon_len 所指定,留出 uboot 的空间以后
还要对 gd->relocaddr 做 4K 字节对齐,并且重新设置 gd->start_addr_sp
reserve_malloc //留出 malloc 区域,调整 gd->start_addr_sp 位置
reserve_board //留出板子 bd 结构体所占的内存区,设置gd->bd,清零结构体
setup_machine //设置机器ID,用于匹配启动linux,已弃用此方式
reserve_global_data //分配出新的global_data结构体,并设置gd->new_gd
reserve_fdt //留出设备树相关的内存区域,未用到
reserve_stacks //对gd->start_addr_sp,减去16,再做16字节对齐
-->arch_reserve_stacks //如果使能 IRQ 的话还要留出 IRQ 相应的内存,这里并未用到
setup_dram_config //设置 dram 信息,就是设置gd->bd->bi_dram[0].start和gd->bd->bi_dram[0].size,后面会传递给 linux 内
核,告诉linux DRAM的起始地址和大小
-->dram_init_banksize
show_dram_config //显示 DRAM 的配置
display_new_sp //显示最后一次修改的gd->start_addr_sp
reloc_fdt //重定位ftd,根据gd->new_fdt进行重定位,这里未用到
setup_reloc //将以前的gd拷贝到gd->new_gd处,设置gd->reloc_off为重定位偏移量
NULL,
-->imx6_light_up_led1 //点亮led灯
-->gpio_direction_output //设置gpio引脚电平以及输入输出
uboot内存分布图如下:
reserved(4k)
TLB(16k) <---------gd->arch.tlb_addr
uboot <---------gd->relocaddr
malloc(3M)
bd <---------gd->bd
new_gd <---------gd->new_gd
<---------gd->start_addr_sp
text_base (uboot编译地址0x87800000)
内存起始地址(0x80000000)
//设置sp指向gd->start_addr_sp,算出重定位后的here,赋值给lr,重定位之后返回重定位之后的here
-->relocate_code // arch/arm/lib/relocate.S
-->copy_loop //复制uboot代码
-->fixloop //对arch/arm/config.mk中LDFLAGS_u-boot += -pie生成位置无关的可执行文件,会生成一个.rel.dyn 段
-->relocate_vectors //这里已经重定位,运行于DRAM中,用于重定位向量表,将r0=gd->relocaddr写入到CP15的VBAR寄存器中
即设置向量表偏移地址为重定位后的uboot首地址
-->c_runtime_cpu_setup //关闭指令cache? arch/arm/cpu/armv7/start.S
清除BSS段数据
-->board_init_r //board_init_r(gd_t *new_gd, ulong dest_addr)
-->initcall_run_list //依次执行init_sequence_r指向的各个板级初始化函数
initr_trace //和调试跟踪有关
-->trace_init
initr_reloc //设置 gd->flags,标记重定位和malloc已完成
initr_caches //初始化并使能Dcache,Icache已经在start.S里面使能了
-->enable_caches
initr_reloc_global_data //对gd的一些成员重新初始化,如gd->env_addr
initr_malloc //CONFIG_SYS_MALLOC_CLEAR_ON_INIT为重定位后malloc的清零控制开关
-->mem_malloc_init //对malloc进行清零
-->malloc_bin_reloc //空函数
bootstage_relocate //对启动状态进行重定位,在堆区存放信息,record组数name指向这些堆区
initr_dm //gd->dm_root_f=gd->dm_root,gd->dm_root=NULL,即使用gd->dm_root_f保存之前的driver model,然后重
新调用dm_init_and_scan初始化一遍driver model
initr_bootstage //在record数组中写入board_init_r的bootstage信息
board_init //板级初始化,包括 74XX 芯片,I2C、FEC、USB 和 QSPI 等
stdio_init_tables //stdio 相关初始化
initr_serial //初始化串口
-->serial_initialize
-->serial_find_console_or_panic
initr_announce //打印gd->relocaddr,通知已运行在RAM中
power_init_board //初始化电源芯片,未用到
initr_mmc //初始化mmc
-->mmc_initialize
initr_dataflash
initr_env //初始化环境变量
-->env_relocate
-->env_relocate_spec
initr_secondary_cpu //初始化其他CPU核
-->cpu_secondary_init_r //空函数,需要特定架构专门实现
initr_pci
-->pci_init //打开CONFIG_DM_PCI才会执行
-->pci_init_board //特定架构相关,特定芯片特定实现
stdio_add_devices //各种输入输出设备的初始化,类似于键盘,lcd之类的
-->drv_video_init //初始化lcd屏
-->drv_system_init //串口的输入输出的驱动函数注册
-->serial_stdio_init
initr_jumptable
-->jumptable_init //使gd->jt指向malloc分配的一块区间,初始化跳转表
console_init_r //指定控制台输入输出设备inputdev和outputdev,设置控制台标准输入输出
interrupt_init //初始化中断
initr_enable_interrupts //使能中断
-->enable_interrupts
initr_ethaddr //获取mac地址,即读取ethaddr环境变量的值,赋值给gd->bd->bi_enetaddr
board_late_init //板子后续初始化
-->add_board_boot_modes //
-->setenv //设置board_name?无关紧要
-->board_late_mmc_env_init //设置mmcroot环境变量为/dev/mmcblk%dp2 rootwait rw,运行mmc dev dev_no切换到存储环境变量的存储设备中
-->set_wdog_reset //设置看门狗复位
initr_fastboot_setup //fastboot:手机刷机的一种模式,这里使用不到
initr_net //初始化网络设备
-->eth_initialize
-->eth_common_init //网络设备公用初始化
-->miiphy_init //mii:一种用于连接phy芯片和mac设备的总线
-->phy_init //针对不同phy芯片厂家,初始化不同的phy设备(实际是注册一个厂家不同型号芯片的驱动)
-->board_eth_init //网络设备初始化
-->setup_iomux_fec //即设置控制网络设备的芯片引脚之类的
-->fecmxc_initialize_multi //初始化网络设备
imx6_light_up_led2 //点灯,就是设置了对应的gpio引脚
run_main_loop //初始化完毕,进入循环
-->main_loop //此函数被放在循环里,初始化完毕,准备执行命令
-->bootstage_mark_name //打印出启动进度"main_loop"
-->cli_init
-->u_boot_hush_start
-->run_preboot_environment_command //执行"preboot"定义的命令列表,即uboot一旦启动就会自动执行的命令
-->run_command_list
-->update_tftp //类似用于自动更新?一般不会调用
-->bootdelay_process //读取"bootdelay"环境变量,打印"### main_loop entered: bootdelay=%d\n\n"
设置全局变量stored_bootdelay为超时时间,返回"bootcmd"环境变量
-->bootretry_init_cmd_timeout //"bootretry"环境变量,用于超时重复引导
-->cli_process_fdt(&s) //使用设备树时读取设备树的bootcmd变量
-->cli_secure_boot_cmd
-->autoboot_command //检查倒计时是否结束?倒计时结束之前有没有被打断?
-->run_command_list //如果被打断,就会执行bootcmd变量
-->cli_loop //uboot的命令行处理函数
-->parse_file_outer //
-->parse_stream_outer //hush shell的命令解释器,负责循环接收命令行输入,然后解析并执行相应的命令
-->parse_stream //解析命令行
-->run_list //执行命令行
-->run_list_real
-->run_pipe_real
-->cmd_process //实际执行命令行的函数
-->find_cmd //查找u_boot_list段的某条命令的信息
-->cmd_call //会调用cmdtp->cmd指向的do_xxx函数执行命令,并传入其它的参数作为函数参数,若传入bootm即可引导系统
cmd_tbl_t //uboot中用于定义命令的结构体
#define U_BOOT_CMD(_name, _maxargs, _rep, _cmd, _usage, _help) \
U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, NULL) //include/command.h
#define U_BOOT_CMD_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
ll_entry_declare(cmd_tbl_t, _name, cmd) = \
U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, \
_usage, _help, _comp);
#define ll_entry_declare(_type, _name, _list) \
_type _u_boot_list_2_##_list##_2_##_name __aligned(4) \
__attribute__((unused, section(".u_boot_list_2_"#_list"_2_"#_name)))
#define U_BOOT_CMD_MKENT_COMPLETE(_name, _maxargs, _rep, _cmd, _usage, _help, _comp) \
{ #_name, _maxargs, _rep, _cmd, _usage, _CMD_HELP(_help) _CMD_COMPLETE(_comp) }
# define _CMD_COMPLETE(x) x,
# define _CMD_HELP(x) x,
U_BOOT_CMD(
dhcp, 3, 1, do_dhcp,
"boot image via network using DHCP/TFTP protocol",
"[loadAddress] [[hostIPaddr:]bootfilename]"
);
cmd_tbl_t _u_boot_list_2_cmd_2_dhcp __aligned(4) \
__attribute__((unused,section(.u_boot_list_2_cmd_2_dhcp))) \
{ "dhcp", 3, 1, do_dhcp, \
"boot image via network using DHCP/TFTP protocol", \
"[loadAddress] [[hostIPaddr:]bootfilename]",\
NULL}
定义了一个 cmd_tbl_t 类型的变量,变量名为_u_boot_list_2_cmd_2_dhcp,使用__attribute__关键字设置变量 _u_boot_list_2_cmd_2_dhcp存储在.u_boot_list_2_cmd_2_dhcp
段中。u-boot.lds 链接脚本中有一个名为“.u_boot_list”的段,所有.u_boot_list 开头的段都存放到.u_boot.list 中
_u_boot_list_2_cmd_2_dhcp.name = "dhcp"
_u_boot_list_2_cmd_2_dhcp.maxargs = 3
_u_boot_list_2_cmd_2_dhcp.repeatable = 1
_u_boot_list_2_cmd_2_dhcp.cmd = do_dhcp
_u_boot_list_2_cmd_2_dhcp.usage = "boot image via network using DHCP/TFTP protocol"
_u_boot_list_2_cmd_2_dhcp.help = "[loadAddress] [[hostIPaddr:]bootfilename]"
_u_boot_list_2_cmd_2_dhcp.complete = NULL
在 uboot 的命令行中输入“dhcp”这个命令的时候,最终执行的是 do_dhcp 这个函数。
uboot 中使用 U_BOOT_CMD 来定义一个命令,最终的目的就是为了定义一个cmd_tbl_t 类型的变量,并初始化这个变量的各个成员。uboot 中的每个命令都存储在.u_boot_list
段中,每个命令都有一个名为 do_xxx(xxx 为具体的命令名)的函数,这个 do_xxx 函数就是具体的命令处理函数。
bootm_headers_t images是个 boot 头结构体,在文件include/image.h 中的定义
image_info_t,也就是系统镜像信息结构体,此结构体在文件include/image.h 中的定义
-->do_bootz //images.os.os设置在关闭中断之后 cmd/bootm.c
-->bootz_start //执行BOOTM_STATE_START阶段,设置images的ep成员变量,也就是系统镜像的入口点,images->ep=0X80800000
-->do_bootm_states //执行BOOTM_STATE_START阶段
-->bootm_start
-->bootz_setup //会判断当前的系统镜像文件是否为Linux的镜像文件,并且会打印出镜像相关信息(这里查不到镜像)
-->bootm_find_images //查找ramdisk和设备树(dtb)文件,查找设备树(dtb)文件,找到以后就将设备树的起始地址和长度分别写到images的ft_addr和ft_len
-->bootm_disable_interrupts //关中断
-->do_bootm_states //执行不同的BOOT阶段,这里要执行的BOOT阶段有:BOOTM_STATE_OS_PREP、BOOTM_STATE_OS_FAKE_GO和BOOTM_STATE_OS_GO
-->bootm_os_get_boot_func //查找系统启动函数,根据images->os.os系统类型查找数组boot_os_fn *boot_os[]选择对应的启动函数
-->boot_fn //实际是个函数指针,指向系统启动函数do_bootm_linux
-->do_bootm_linux //处理BOOTM_STATE_OS_PREP阶段
-->boot_prep_linux //处理环境变量bootargs,bootargs保存着传递给Linux kernel的参数
-->boot_selected_os //未定义CONFIG_TRACE,BOOTM_STATE_OS_FAKE_GO不会执行
-->boot_selected_os //执行BOOTM_STATE_OS_GO阶段,调用boot_fn即do_bootm_linux启动内核
-->boot_jump_linux //arch/arm/lib/bootm.c
gd->bd->bi_arch_number 机器id,在不适用设备树情况下,linux内核通过这个值匹配比较是否支持该设备
如果使用设备树的话这个machid就无效了,设备树存有一个“兼容性”这个属性,Linux内核会比较“兼容性”属性
的值(字符串)来查看是否支持这个机器
-->kernel_entry = (void (*)(int, int, uint))images->ep //images->ep保存着 Linux内核镜像的起始地址,起始地址保存的正是Linux内核第一行代,就是函数kernel_entry
-->announce_and_cleanup //熟悉的"Starting kernel ..."
-->kernel_entry(0, machid, r2) //第一个参数为零,第二个参数为机器id,第三个参数为ATAGS或者设备树首地址
ATAGS 是传统的方法,用于传递一些命令行信息啥的
start //7或者8次空操作(用以存放中断向量),然后128M对齐,arch/arm/boot/compressed/head.S
-->decompress_kernel //arch/arm/boot/compressed/misc.c
-->arch_decomp_setup
-->do_decompress //内核解压函数
-->__decompress
-->__gunzip //实际执行解压的函数
-->__enter_kernel // @ restore architecture number
@ restore atags pointer
@ call kernel
@ 会跳转到CONFIG_AUTO_ZRELADDR这里赋值的r4,即解压后的内核镜像首地址stext
-->stext //实际解压后的内核镜像入口点
-->__lookup_processor_type //arch/arm/kernel/head-common.S
-->__vet_atags
-->__create_page_tables
-->__enable_mmu
-->__turn_mmu_on
-->__mmap_switched //arch/arm/kernel/head-common.S
-->start_kernel //内核的初始化函数
--> set_task_stack_end_magic(&init_task) //设置init_task进程的栈溢出标志,task_struct的stack指针指向进程栈,栈顶向下到栈底设置栈溢出标志,栈溢出
标志值被修改则表示栈溢出,栈溢出标志的下方为thread_info
smp_setup_processor_id() //获取当前正在执行初始化的处理器ID,可以不调用setup_arch()初始化函数就可以使用,是直接获取对称多处理器的ID
//每次都要中断CPU去获取ID,这样效率比较低。
//而smp_processor_id()函数是一定要调用setup_arch()初始化函数后,才能使用,获取变量保存的处理器ID
debug_objects_early_init() //初始化一些调试对象
cgroup_init_early() //系统初始阶段需要使用一些 subsystem,先对这一部分进行初始化
cgourp是一种机制
作用:集成各个进程,对进程分组实现进程组,分配或限制进程组使用的资源(这部分主要有个各个subsystem完成)。
subsystem:不同subsystem对应控制不同的资源:
cpu subsystem:指定进程组能使用的CPU;
memmory subsystem:指定进程组能使用的内存量,并编写内存使用量报告;
cpuset subsystem:指定进程组能使用的各CPU和节点集;
freezer subsystem:可停止进程组状态或启动;
cpuacct subsystem:对进程组使用的CPU资源编写报告;
blkio:设置对模块设备的输入输出限制;
devices:设置进程组能使用的设备;
local_irq_disable() //屏蔽当前CPU上的所有中断
//注:disable_irq在全局范围内屏蔽某一个中断号
boot_cpu_init(); //作用是设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好。
在多CPU的系统里,内核需要管理多个CPU,那么就需要知道系统有多少个CPU,在内核里使用cpu_present_map位图表达有多少个CPU,每
一位表示一个CPU的存在。如果是单个CPU,就是第0位设置为1。
虽然系统里有多个CPU存在,但是每个CPU不一定可以使用,或者没有初始化,在内核使用cpu_online_map位图来表示那些CPU可以运行内
核代码和接受中断处理。
当CPU处于active的时候,说明它已经准备好了一切,可以参与进程调度了。
随着移动系统的节能需求,需要对CPU进行节能处理,比如有多个CPU运行时可以提高性能,但花费太多电能,导致电池不耐用,需要减
少运行的CPU个数,或者只需要一个CPU运行。这样内核又引入了一个cpu_possible_map位图,表示最多可以使用多少个CPU。
在本函数里就是依次设置这三个位图的标志,让引导的CPU物理上存在,已经初始化好,最少需要运行的CPU。
对于其他CPU来说,需要在DTS中描述,否则boot CPU不知道其他CPU的存在
page_address_init(); //初始化高端内存的映射表
在这里引入了高端内存的概念,那么什么叫做高端内存呢?为什么要使用高端内存呢?其实高端内存是相对于低端内存而存在的,那么先
要理解一下低端内存了。
在32位的系统里,最多能访问的总内存是4G,其中3G空间给应用程序,而内核只占用1G的空间。
因此,内核能映射的内存空间,只有1G大小,但实际上比这个还要小一些,大概是896M,另外128M空间是用来映射高端内存使用的。
因此0到896M的内存空间,就叫做低端内存,而高于896M的内存,就叫高端内存了。
如果系统是64位系统,当然就没未必要有高端内存存在了,因为64位有足够多的地址空间给内核使用,访问的内存可以达到10G都没有问题。
在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。
比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。
使用高端内存之后,32位的系统也可以访问达到64G内存。
linux中,地址空间映射是这样的,把0xc0000000~0xffffffff这1GB内核地址空间划分成2个部分低端的796MB + 高端的128MB,低端796MB
就使用f映射,直接映射到物理内存的前796MB上,而高端128MB就用来随时变更g来映射到物理内存超过796MB的范围上,这里对应了3种映
射算法:动态映射,永久内核映射,临时映射
pr_notice("%s", linux_banner); //打印内核版本信息
early_security_init(); //早期安全初始化
setup_arch(&command_line); //体系结构相关的,并输出uboot传递的命令行参数
-->setup_processor //查找CPU信息,可以结合串口打印的信息来分析。
-->read_cpuid_id //读取cpu id
-->lookup_processor //确定内核是否支持当前CPU,若支持,则返回描述处理器的proc_info_list结构体,否则陷入死循环
-->lookup_processor_type //C接口版本的__lookup_processor_type arch/arm/kernel/head-common.S
-->__get_cpu_architecture //获取cpu架构
-->init_proc_vtable //
-->cpuid_init_hwcaps
-->patch_aeabi_idiv
-->init_default_cache_policy
-->elf_hwcap_fixup
-->cacheid_init
-->cpu_init //初始化cpu并设置cpu的栈
-->smp_processor_id //获取当前cpu的id
-->set_my_cpu_offset
-->cpu_proc_init //空函数
-->setup_machine_fdt //函数参数为设备树(DTB)首地址,返回的machine_desc是描述平台信息的结构体。获取内核前期初始化所需的bootargs,cmd_line等系
统引导参数
-->early_init_dt_verify //检查fdt头部的合法性,设置fdt全局首地址指针initial_boot_params以及计算crc
-->fdt_check_header
-->crc32_be
-->of_flat_dt_match_machine //寻找设备树中根节点属性"compatible"的属性,找到最匹配的machine_desc
-->early_init_dt_scan_nodes //解析chosen节点中bootargs属性的值,得到启动参数, 存入全局变量:boot_command_line;解析 /memory 节点,获取内存配置信息
-->of_scan_flat_dt //调用具体的执行函数
-->dump_stack_set_arch_desc
-->early_fixmap_init //在完整的paging建立起来之前,添加一种小的机制,在当前的阶段,建立所必须资源的映射;不是建立固定的映射,而是在一段固定的
虚拟地址建立映射
-->early_ioremap_init //早期内存映射初始化,依赖于early_fixmap_init
-->early_ioremap_setup
-->parse_early_param //早期串口控制台参数解析
-->parse_early_options
-->parse_args
-->do_early_param
-->early_mm_init //早期的内存初始化
-->build_mem_type_table
-->early_paging_init
-->setup_dma_zone //设置dma区域大小和地址
-->xen_early_init
-->efi_init //efi相关初始化
-->adjust_lowmem_bounds
-->arm_memblock_init //内存初始化,保留相关内存区域
-->memblock_reserve //保留内核的内存区域
-->arm_initrd_init //对页表相关区域进行保存
-->early_init_fdt_reserve_self //把设备树所占内存区域保留下来
-->early_init_fdt_scan_reserved_mem //DTB保留内存信息中的节点,即根据dtb中的memreserve信息, 调用memblock_reserve
-->of_scan_flat_dt //解析设备树节点
-->__fdt_scan_reserved_mem //找到设备树的reserved-memory节点属性
-->fdt_init_reserved_mem //初始化保留内存
-->dma_contiguous_reserve //dma内存预留和cma_areas内存信息设置
-->dma_contiguous_reserve_area
-->cma_declare_contiguous
-->dma_contiguous_early_fixup
-->memblock_dump_all
-->__memblock_dump_all
-->adjust_lowmem_bounds
-->early_ioremap_reset
-->early_ioremap_shutdown //空函数
-->paging_init //负责建立仅用于kernel而用户空间不可访问的页表
PTE:页表项(page table entry)
PGD(Page Global Directory)
PUD(Page Upper Directory)
PMD(Page Middle Directory)
PT(Page Table)
-->prepare_page_table //在映射页表之前,把内核页表的页表项清零
-->map_lowmem //初始化内核的页表,即重新建立页表
-->memblock_set_current_limit
-->dma_contiguous_remap //dma重映射
-->early_fixmap_shutdown //清楚早期的固定虚拟地址内存映射
-->devicemaps_init //设置向量表映射;调用mdesc->map_io()将物理IO映射到虚拟地址,设置了时钟,gpio等
-->kmap_init //linux内存管理系统初始化
-->tcm_init //TIghtly Coupled Memory:紧致耦合内存
为了弥补Cache访问的不确定性,而增加的OnChip Memory。
TCM使用物理地址,对TCM的写访问,受到MMU内部保护信息的控制。向TCM中的内存位置写入时,不会发生任何外部写入。
TCM用于向处理器提供低延迟内存,它没有高速缓存特有的不可预测性。 可以使用 TCM 来存放重要例程,如中断处理例程或者极需要避免
高速缓存不确定性的实时任务
ARM的ram包括静态ram,动态ram, TCM---紧耦合内存
TCM是一个固定大小的RAM,紧密地耦合至处理器内核,提供与cache相当的性能,相比于cache的优点是,程序代码可以精确地控制什么函
数或代码放在哪儿(RAM里)
紧致内存是指片上快速存储区,与片上缓存具有同等的性能,但因为程序可完全控制紧致内存,因而比统计复用的缓存有更好的可预测性。
TCM有两种使用方式:作为快缓存使用,和作为本地内存使用。
TCM的配置:通过CP15的0、1、9号寄存器进行
-->pmd_off_k //设置pmd表的偏移
-->early_alloc //分配zero_page,即零页
-->memblock_alloc
-->bootmem_init //内存基本数据结构(内存结点pg_data_t,内存域zone和页帧)的初始化工作
-->memblock_allow_resize
-->find_limits
-->early_memtest
-->memblocks_present
-->sparse_init
-->zone_sizes_init
-->virt_to_page //零页逻辑地址转换成指针
-->__flush_dcache_page //写回内存,更新零页
-->request_standard_resources //完成对从内核启动到此时,cpu使用到的所有外围总线设备实体的注册登记工作
-->unflatten_device_tree //解析设备树,设备树信息描述依据于/Documentation/devicetree/bindings,即绑定文档
-->__unflatten_device_tree
-->unflatten_dt_nodes //第一轮的scan,主要目的是为了获取设备树的大小
-->dt_alloc //将获取的大小在内存中申请一段内存(device_node数量的大小)
-->unflatten_dt_nodes //实际解析设备树节点,第二轮scan,将设备树中所有信息挂在上一步申请内存中(以结构体device_node为链表)
-->of_alias_scan //解析alias节点,alias节点是别名相关的节点
-->unittest_unflatten_overlay_base //应该是设备树解析之后的一个节点测试函数,准备节点测试相关
-->arm_dt_init_cpu_maps //从设备树节点中获取cpu节点
-->psci_dt_init //PSCI, Power State Coordination Interface,由ARM定义的电源管理接口规范
-->reserve_crashkernel //用于内核崩溃时的内核捕获的内存区分配(coredump)
此功能通过内核command line参数boot_command_line中的"crashkernel="保留下内存用于主内核崩溃时获取内核信息的导出。
setup_command_line(command_line) //存储命令行参数
setup_nr_cpu_ids() //设置最多有多少个nr_cpu_ids结构
setup_per_cpu_areas() //设置SMP体系每个CPU使用的内存空间,同时拷贝初始化段里数据,用于保存cpu的状态和配置信息
smp_prepare_boot_cpu() //设置cpu偏移数组
boot_cpu_hotplug_init() //Linux CPU热插拔,支持在系统启动后,关闭任意一个secondary cpu(在ARM架构中,CPU0为boot cpu,不能被关闭),并在需要时重新打开它。
//CPU-hotplug的一个用处是,支持SMP的Suspend和Resume
-->cpumask_set_cpu
build_all_zonelists(NULL) //支持NUMA模型,建立内存管理区(zone)链表
page_alloc_init() //处理用于热插拔cpu的页
pr_notice("Kernel command line: %s\n", boot_command_line) //打印命令行信息
jump_label_init() //初始化所有的__jump_table段,jump table机制修改分支处的代码,消除高频调用处的分支
parse_early_param()
-->parse_early_options
-->parse_args
-->do_early_param //解析命令行中的 console 参数
setup_log_buf(0) //为真正的console申请buffer,earlycon和console的两个buffer已经分开了。这里传进来的early参数为0
vfs_caches_init_early() //VFS虚拟文件系统的早期初始化,主要负责dentry和inode的hashtable的初始化工作
sort_main_extable() //异常处理调用函数表排序
trap_init() //对系统保留中断向量初始化
mm_init() //内存管理初始化
-->page_ext_init_flatmem() //和cgroup的初始化相关
-->init_debug_pagealloc()
-->report_meminit()
-->mem_init() //标记空闲内存区域并告知有多少空闲内存
-->set_max_mapnr
-->free_unused_memmap
-->memblock_free_all
-->free_highpages
-->mem_init_print_info
-->kmem_cache_init() //slab内存分配器早期初始化
-->kmemleak_init() //kmemleak初始化,kmemleak用于检查内核内存泄漏,当独立的对象没有被释放时,其报告记录在 /sys/kernel/debug/kmemleak中
-->pgtable_init() //
-->ptlock_cache_init
-->pgtable_cache_init
-->debug_objects_mem_init()
-->vmalloc_init() //linux内核中对于物理上连续的分配方式,采用伙伴系统和slub分配器分配内存,但是我们知道物理上连续的映射
是最好的分配方式,但并不总能成功地使用。在分配一大块内存时,可能竭尽全力也无法找到连续的内存块。
针对这种情况内核提供了一种申请一片连续的虚拟地址空间,但不保证物理空间连续,也就是vmalloc接口。
vmalloc的工作方式类似于kmalloc,只不过前者分配的内存虚拟地址连续,而物理地址则无需连续,因此不能用于dma缓冲区
通过vmalloc获得的页必须一个一个地进行映射,效率不高,因此不得已时才使用,同时vmalloc分配的一般是大块内存
vmalloc分配的一般是高端内存,只有当内存不够的时候,才会分配低端内存
-->ioremap_huge_init() //ioremap功能的初始化
-->init_espfix_bsp()
-->pti_init()
ftrace_init() //Ftrace是直接内置在内核的跟踪程序。
ftrace给Linux提供可以查看追踪内核内部事情的能力,可以更好地查找问题区域并跟踪错误。
ftrace可以跟踪内核崩溃的events,这样可以准确的找到导致内核崩溃的原因
可以理解成ftrace是内核态的strace,用于追踪内核态的调用记录,但是功能比strace强大的多的多。
ftrace的API接口位于内核Debugfs文件系统中,挂载在/sys/kernel/debug。当ftrace使能后,会在debugfs中创建tracing目录
Ftrace依赖内核开关使能:
CONFIG_FUNCTION_TRACER
CONFIG_FUNCTION_GRAPH_TRACER
CONFIG_STACK_TRACER
CONFIG_DYNAMIC_FTRACE
early_trace_init()
sched_init() //调度器初始化
preempt_disable() //关闭优先级抢占,因为早期引导调度不够稳定在cpu_idle第一次启动之前
local_irq_disable()
radix_tree_init() //Linux基数树(radix tree)是将指针与long整数键值相关联的机制,它存储有效率,并且可快速查询
housekeeping_init()
workqueue_init_early();
rcu_init() //RCU(Read-Copy Update)是数据同步的一种方式
RCU主要针对的数据对象是链表,目的是提高遍历读取数据的效率,为了达到目的使用RCU机制读取数据的时候不
对链表进行耗时的加锁操作。这样在同一时间可以有多个线程同时读取该链表,并且允许一个线程对链表进行修
改(修改的时候,需要加锁)。
trace_init() //调试跟踪初始化
context_tracking_init()
early_irq_init() //先进行架构无关初始化
初始中断相关初始化,主要是注册irq_desc结构体变量,因为Linux内核使用irq_desc来描述一个中断
-->arch_early_irq_init //架构相关初始化,arm下无此特定函数,为空
init_IRQ() //中断初始化
tick_init()
-->tick_broadcast_init
-->tick_nohz_init
rcu_init_nohz()
init_timers() //PIT系统定时器初始化
-->init_timer_cpus
-->open_softirq //设置TIMER_SOFTIRQ的软中处理函数为run_timer_softirq,在run_timer_softirq将完成对到期的定时器实际的处理工作
hrtimers_init() //初始化高精度定时器
-->hrtimers_prepare_cpu
-->open_softirq //设置HRTIMER_SOFTIRQ的软中处理函数为hrtimer_run_softirq
softirq_init() //下半部软中断初始化
-->open_softirq //注册TASKLET_SOFTIRQ和HI_SOFTIRQ的软中断处理函数
timekeeping_init() //每次启动时都要通过timekeeping_init从RTC中同步正确的时间信息,墙上时间
一旦初始化完成后,timekeeper就开始独立于RTC,利用自身关联的clocksource进行时间的更新操作,根据内核的
配置项的不同,更新时间的操作发生的频度也不尽相同
rand_initialize()
add_latent_entropy()
add_device_randomness(command_line, strlen(command_line))
boot_init_stack_canary() //初始化栈canary值,canary值用于防止栈溢出攻击的堆栈的保护字
time_init() //初始化系统时间
perf_event_init() //Perf 是内置于Linux 内核源码树中的性能剖析(profiling)工具。它基于事件采样原理,以性能事件为基
础,支持针对处理器相关性能指标与操作系统相关性能指标的性能剖析。可用于性能瓶颈的查找与热点代码的定位。
linux2.6及后续版本都自带该工具,几乎能够处理所有与性能相关的事件。
profile_init() //对内核的profile(一个内核性能调式工具)功能进行初始化
call_function_init()
early_boot_irqs_disabled = false
local_irq_enable() //使能中断
kmem_cache_init_late() //mem_init调用kmem_cache_init,slab初始化,slab是Linux 内存分配器
console_init() //初始化控制台,之前printk打印的信息都存放缓冲区中,并没有打印出来。只有调用此函数初始化控制台以后
才能在控制台上打印信息。
-->n_tty_init //注册tty驱动函数
lockdep_init() //Lockdep是死锁检测模块,这里仅仅打印一些信息
locking_selftest() //锁自测
mem_encrypt_init() //x86有使用到,内存加密相关?
setup_per_cpu_pageset() //为每个cpu分配页并初始化他们,在这之前仅有boot cpu的页可用
numa_policy_init() //numa内存分配相关
acpi_early_init() //初始化ACPI电源管理。高级配置与能源接口(ACPI)ACPI规范介绍ACPI能使软、硬件、操作系统(OS),主机板
和外围设备,依照一定的方式管理用电情况,系统硬件产生的Hot-Plug事件,让操作系统从用户的角度上直接支
配即插即用设备,不同于以往直接通过基于BIOS 的方式的管理。
sched_clock_init()
calibrate_delay() //测定BogoMIPS值,可以通过BogoMIPS来判断CPU的性能BogoMIPS设置越大,说明CPU性能越好
calibrate_delay()函数可以计算出cpu在一秒钟内执行了多少次一个极短的循环,最终计算出来的值赋给
变量loop_per_jiffs,经过处理后得到BogoMIPS 值,Bogo是Bogus(伪)的意思,MIPS是
millions of instructions per second(百万条指令每秒)的缩写。这样我们就知道了其实这个函数是linux内核
中一个cpu性能测试函数。对于内核延时函数,关键点就是loop_per_jiffs,udelay函数就是利用这个变量计算出
自己延时所需要的短循环。来实现微秒级延时。由于内核对这个数值的要求不高,所以内核使用了一个十分简单
而有效的算法用于得到这个值。如果想了解自己机器的BogoMIPS,可以察看 /proc/cpuinfo文件中的bogomips
pid_idr_init() //设置pid范围?
anon_vma_init() //这个函数是初始化反向映射的匿名内存,提供反向查找内存的结构指针位置,快速地回收内存。
if (efi_enabled(EFI_RUNTIME_SERVICES)) //定义CONFIG_X86宏时,设置efi相关的东西
efi_enter_virtual_mode()
thread_stack_cache_init() //设置线程栈缓存,用于copy到用户空间?
cred_init() //初始化分配凭证缓存
fork_init() //根据当前物理内存计算出来可以创建进程(线程)的数量,并进行进程环境初始化。
proc_caches_init() //对进程资源缓存初始化
uts_ns_init() //命名空间相关缓存分配
buffer_init() //初始化文件系统的缓冲区,并计算最大可以使用的文件缓存
key_init() //初始化密钥相关链表
security_init() //初始化安全管理框架,以便提供访问文件/登录等权限
dbg_late_init() //初始化kdb_printf,断点及kdb状态
vfs_caches_init() //为VFS创建缓存
一般情况下,会首先在虚拟的根文件系统中做一部分工作,然后切换到真实的根文件系统下面
虚拟的根文件系统包括三种类型,即Initramfs、cpio-initrd和image-initrd
在系统启动的过程中,首先会执行Initrd中的“某一个文件” 来完成驱动模块加载的任务,第二阶段才会执行真正
的根文件系统中的/sbin/init。这里提到的第一阶段是为第二阶段服务的,主要是用来加载根文件系统以及根文件
系统存储介质的驱动程序。
实际应用中包括无Initrd、Linux Kernel和Initrd打包、Linux Kernel和Initrd分离以及RAMDisk Initrd
-->dcache_init() //完成dentry_hashtable的初始化。在路径查找中就会用到这个hashtable
-->inode_init() //完成inode_hashtable的初始化
-->files_init() //流指针缓冲区?
-->files_maxfiles_init() //初始化内存中可以打开的最大的文件数
-->mnt_init() //创建一个rootfs,这是个虚拟的rootfs,即内存文件系统,后面还会指向真实的文件系统
-->kernfs_init
-->sysfs_init //sysfs文件系统初始化
-->shmem_init
-->init_rootfs //创建虚拟根文件系统
-->init_mount_tree //注册根文件系统(内核此时没有真实根文件系统驱动,相关根文件系统设备驱动在后面的kernel_init中加载
(在initrd中存放着),且Root设备都是以设备文件的方式指定的,如果没有虚拟根文件系统,设备文件不可能存在)
-->vfs_kern_mount(&rootfs_fs_type, 0, "rootfs", NULL) //创建虚拟文件系统
-->init_task.nsproxy->mnt_ns = ns
-->set_fs_root(current->fs, &root) //将当前的文件系统配置为根文件系统
-->bdev_cache_init() //每个块设备用一个块设备结构体进行描述,即block_device结构,它描述一个逻辑上的块设备,可以是一整个
磁盘,也可以仅仅是磁盘上的一个分区。
块设备的管理是以一个块设备伪文件系统组织的,每个块设备是这个文件系统上的一个块设备文件,其对应的文件
索引节点结构为bdev_inode,其定义为:
struct bdev_inode {
struct block_device bdev;
struct inode vfs_inode;
};
它只是把一个block_device结构和一个虚拟文件系统索引节点结构结合在一起了而已。
块设备子系统的初始化由两个部分组成,第一个是vfs_caches_init()中调用的bdev_cache_init()函数,它初始
化bdev伪文件系统。另外一个genhd_device_init()函数
bdev_cache_init初始化bdev虚拟文件系统
-->chrdev_init()
pagecache_init()
signals_init() //初始化内核信号队列
seq_file_init()
proc_root_init() //注册并挂载proc文件系统
nsfs_init() //nsfs文件系统初始化?
cpuset_init() //初始化cpuset,cpuset是将CPU和内存资源以逻辑性和层次性集成的一种机制,是cgroup使用的子系统之一
cgroup_init() //初始化cgroup以及其他没有早期初始化的子系统
taskstats_init_early() //任务状态早期初始化函数
delayacct_init() //init_task延时相关?
poking_init()
check_bugs() //检查写缓冲一致性
acpi_subsystem_init() //acpi子系统初始化
arch_post_acpi_subsys_init() //架构相关的acpi子系统初始化
sfi_init_late() arch_call_rest_init()
-->rest_init //(1)rest_init中调用kernel_thread函数启动了2个内核线程,分别是:kernel_init和kthreadd
(2)调用schedule函数开启了内核的调度系统,从此linux系统开始转起来了。
(3)rest_init最终调用cpu_idle函数结束了整个内核的启动。也就是说linux内核最终结束了一个函数cpu_idle。这个函数里面肯定是死循环。
(4)简单来说,linux内核最终的状态是:有事干的时候去执行有意义的工作(执行各个进程任务),实在没活干的时候就去死循环(实际上死循环也可以看成是一个任务)。
(5)之前已经启动了内核调度系统,调度系统会负责考评系统中所有的进程,这些进程里面只有有哪个需要被运行,调度系统就会终止cpu_idle死循环进程(空闲进程)转而去执行有意义的干活的进程。这样操作系统就转起来了。
-->rcu_scheduler_starting
-->kernel_thread //创建kernel_init进程
-->kernel_init_freeable
-->wait_for_completion //kthreadd_done完成量,等待kernel_initkthreadd创建完才开始执行
-->set_mems_allowed
-->task_pid
-->smp_prepare_cpus
-->workqueue_init //下半部机制:初始化工作队列
-->init_mm_internals
-->do_pre_smp_initcalls //#define early_initcall(fn) __define_initcall(fn, early),即定义init段,相关early_initcall函数都由init进程执行
-->lockup_detector_init
-->smp_init
-->sched_init_smp
-->page_alloc_init_late
-->page_ext_init
-->do_basic_setup
-->cpuset_init_smp()
-->driver_init() //完成 Linux 下驱动模型子系统的初始化
-->devtmpfs_init()
-->devices_init()
-->buses_init()
-->classes_init()
-->firmware_init()
-->hypervisor_init()
-->of_core_init()
-->platform_bus_init()
-->cpu_dev_init()
-->memory_dev_init()
-->container_dev_init()
-->init_irq_proc()
-->do_ctors()
-->usermodehelper_enable()
-->do_initcalls() //以函数指针的形式取出这些编译到内核的驱动模块中初始化函数起始地址,来依次完成相应的初始化
-->do_initcall_level
-->do_one_initcall //依次执行init_callx_start段的函数指针
-->ksys_open //打开"/dev/console",即为bootargs中“console=ttymxc0,115200”,作为0标准输入
-->ksys_dup(0) //将0即控制台复制两遍作为标准输出及错误输出
-->ksys_dup(0)
-->prepare_namespace //挂载根文件系统bootargs环境变量“root=/dev/mmcblk1p2 rootwait rw”
-->integrity_load_keys
-->async_synchronize_full
-->ftrace_free_init_mem
-->free_initmem //释放init初始化数据段
-->mark_readonly
-->pti_finalize
-->numa_default_policy
-->rcu_end_inkernel_boot
-->run_init_process //运行多次,尝试执行init进程
1. ramdisk_execute_command此变量值为“/init”,也就是根目录下的init程序
也可以通过 uboot 传递,在 bootargs 中使用“rdinit=xxx”即可
2. execute_command execute_command的值是通过uboot传递,在bootargs中使用
“init=xxxx”就可以了,比如“init=/linuxrc”
3. 依次查找"/sbin/init"、"/etc/init"、"/bin/init"和"/bin/sh",这四个相
当于备用 init 程序
-->rcu_read_lock
-->find_task_by_pid_ns
-->set_cpus_allowed_ptr
-->rcu_read_unlock
-->numa_default_policy
-->kernel_thread //创建kthreadd进程,管理和调度其他内核线程kernel_thread
会循环运行kthread_create_list全局链表中维护的kthread
当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程
都是直接或者间接的以kthreadd为父进程
-->rcu_read_lock
-->find_task_by_pid_ns
-->rcu_read_unlock
-->complete //设置的kthreadd_done完成量,当kthreadd进程执行完毕之后会设置这个量,从而通知kernel_init开始执行
-->schedule_preempt_disabled
-->sched_preempt_enable_no_resched
-->schedule //进程切换函数
-->preempt_disable
-->cpu_startup_entry //idle进程pid为0,由主进程演变而来
-->arch_cpu_idle_prepare
-->local_fiq_enable
-->cpuhp_online_idle
-->do_idle //循环
-->schedule_idle
-->__schedule
-->prevent_tail_call_optimization //阻止尾调用优化