Linux Kernel 简介
目标文件
内核编译后生成的目标文件是 ELF 格式的 vmlinux,vmlinux文件是各个源代码按照vmlinux.lds设定的规则,链接后得到的Object文件,并不是一个可执行的文件,不能在ARM平台上运行;通常会对其压缩,生成zImage或bzImage;通常内核映像以压缩格式存储,并不是一个可执行的内核;因此内核阶段需要先对内核映像自解压,他们的文件头部打包有解压缩程序;
vmlinux.lds
vmlinux.lds文件是在内核编译时生成的,是被禁止编辑的;vmlinux.lds文件是在编译时,由vmlinux.lds.S文件对链接器ld的输出进行排序后生成;vmlinux.lds.S是用来对输出文件中的段进行排序,并定义相关的符号名;
arch/arm64/kernel/vmlinux.lds.S
在项目中通过make时指定的参数-O,将内核编译生成的所有目标文件,包括vmlinux.lds文件重定向输出到以下目录:
arch/arm64/kernel/vmlinux.lds
vmlinux.lds文件是链接脚本,在内核编译时,作为Makefile的链接器脚本,参与链接生成内核映像vmlinux;
总之:vmlinux是按照vmlinux.lds链接生成的,而vmlinux.lds是由vmlinux.lds.S生成的;
// arch/arm64/kernel/vmlinux.lds
OUTPUT_ARCH(aarch64) // 指定架构为aarch64
ENTRY(_text) // 定义入口为_text
jiffies = jiffies_64; // 定义为64位计数器
SECTIONS
{
......
// 指定链接地址
. = ((((0xffffffffffffffff - (1 << (48)) + 1) + (0)) + (0x08000000))) + 0x00080000;
......
}
关于段的信息
text段,代码段,用来存放程序执行代码的一块内存区域;大小在程序运行前确定; data段,数据段,用来存放程序中已初始化的全局变量的内存区域;数据段属于静态内存分配; bss段,用来存放程序中未初始化的全局变量和静态变量的一块内存区域;属于静态内存分配; init段,Linux定义的一种初始化过程中才能用到的段;初始化完成后,该段内存会被释放;
关于地址的信息
加载地址:程序中指令和变量等加载到RAM上的地址; 运行地址:CPU执行一条程序的指令时的执行地址,即PC寄存器的值;就是要寻址到一个指令或变量所使用的地址; 链接地址:链接过程中链接器为指令和变量分配的地址;
内核映像
vmlinux是未压缩的内核,是生成的纯内核二进制文件,具有用户定义的所有内核组件,但是这个vmlinux二进制文件是无法启动系统的;为了将 Linux 内核映像加载到内存并处于可执行状态,内核构建系统使用objcopy命令清除不必要的节区,压缩ELF格式的vmlinux,通过引导程序加载项和链接,生成可启动的最终的二进制文件zImage;
vmlinux 通过gzip压缩成piggy.o,和head.o、misc.o链接生成zImage二进制文件;
vmlinuz 是vmlinux的压缩文件
zImage 默认的压缩内核映像文件,压缩 vmlinux,加上一段解压启动代码,压缩而成;
uImage 是u-boot使用bootm命令引导的 Linux 压缩内核映像文件格式,是使用 mkimage 工具对普通的压缩内核映像文件(zImage)加工而成;
uImage 是 uboot 专用的内核映像文件,是在zImage之前加上一个长度为64字节的“头”,说明内核的版本、加载位置、生成时间、大小等信息;在地址0x40之后的部分,和zImage一样;其大小比zImage大64字节;
设备树文件
Linux内核从3.x版本开始引入设备树的概念,用于实现驱动代码与设备信息分离;设备树出现之前,所有关于设备的具体信息都写在驱动中,外围设备变化,驱动代码就要跟着修改甚至是重写;引入设备树之后,驱动代码只负责处理驱动代码的逻辑,而关于设备的具体信息存放到设备树文件中,这样硬件接口信息变化时,只需要修改设备树文件信息,不需要修改驱动代码就可以;
一般情况下,在编译设备树之前,先在 scripts/dtc/ 目录下,编译生成dtc工具 scripts/dtc/dtc,再使用生成的 dtc 工具编译设备树源码,生成设备树文件;设备树源码和目标文件在arch/arm64/boot/dts/freescale/目录;
在Lx2160板项目中,目标文件会被重定向输出到target目录;
具体的设备树文件的加载、解析过程,会在下文 [[ #setup_arch ]] 部分描述;
内核启动第一阶段
Linux内核启动第一阶段,也就是我们常说的汇编阶段,也就是stext函数的实现内容;这部分主要完成的工作:
- CPU ID检查,
- machine ID检查,
- 创建初始化页表,
- 设置C代码运行环境,
- 跳转到内核第一个真正的C函数start_kernel执行;
设置为SVC模式,关闭所有中断
获取CPUID,提取相应的proc info
验证tags或dtb
创建页表项
head.S文件中,
校验启动合法性
建立段式映射的页表并开启MMU
构建C运行环境,跳入C阶段
内核启动入口点
内核是一个庞大的系统,通过vmlinux反向追踪启动入口点
Lx2160le板的交叉编译工具链:
CROSS_COMPILE=aarch64-linux-gnu-
通过对目标文件vmlinux的反向追溯,找到Linux执行的入口;
使用readelf命令可以查看vmlinux的入口地址为:0xffff000008080000
# readelf -h vmlinux
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0xffff000008080000
Start of program headers: 64 (bytes into file)
Start of section headers: 267014976 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 4
Size of section headers: 64 (bytes)
Number of section headers: 39
Section header string table index: 36
这个入口地址是怎么来的,对应内核代码中的哪个部分,可以通过反汇编来分析;
# aarch64-linux-gnu-objdump -dxh vmlinux > vmlinux.s
在得到的汇编文件vmlinux.s中查找入口地址:0xffff000008080000
start address 0xffff000008080000
Disassembly of section .head.text:
ffff000008080000 <_text>:
ffff000008080000: 91005a4d add x13, x18, #0x16
ffff000008080004: 14437fff b ffff000009160000 <stext>
可以得到,Linux入口的第一条指令为:add x13, x18, #0x16;对应的符号是:.head.text _text;
而.head.text段,通过include/linux/init.h文件中的宏定义__HEAD来表示:
/* For assembly routines */
#define __HEAD .section ".head.text","ax" // "ax"表示所在段区域有可执行权限
#define __INIT .section ".init.text","ax"
内核启动的入口点,在arch/arm64/kernel/head.S文件中;
__HEAD
_head:
/*
* DO NOT MODIFY. Image header expected by Linux boot-loaders.
*/
#ifdef CONFIG_EFI
/*
* This add instruction has no meaningful effect except that
* its opcode forms the magic "MZ" signature required by UEFI.
*/
add x13, x18, #0x16
b stext
#else
b stext // branch to kernel start, magic
.long 0 // reserved
#endif
到此,已经找到,目标文件 vmlinux的入口是 arch/arm64/kernel/head.S文件中的 __HEAD,在执行完第一条语句(add x13, x18, #0x16)之后,跳转到stext函数执行(b stext);所以,加载 vmlinux后,第一个运行的函数是 stext;
stext 函数
启动过程中的汇编阶段,是从arch/arm64/kernel/head.S文件开始,执行的起点是 stext 函数,入口函数是通过 vmlinux.lds链接而成,在head.S中ENTRY(stext)指定;
在汇编代码中,宏定义ENTRY和ENDPROC是成对出现的,表示定义的一个函数,同时也要指明当前代码所在的段,如:__INIT;
// arch/arm64/kernel/head.S
#define __INIT .section ".init.text","ax"
__INIT
ENTRY(stext)
......
ENDPROC(stext)
内核启动的必要条件:MMU关闭,D-cache关闭,x0是传递给FDT blob的物理地址;
* The requirements are:
* MMU = off, D-cache = off, I-cache = on or off,
* x0 = physical address to the FDT blob.
stext函数开始执行;
// arch/arm64/kernel/head.S
__INIT
ENTRY(stext)
bl preserve_boot_args // Preserve the arguments passed by the bootloader in x0 ... x3
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.
*/
bl __cpu_setup // initialise processor
b __primary_switch
ENDPROC(stext)
preserver_boot_args
保存从bootloader传递过来的x0 ~ x3参数;
// arch/arm64/kernel/head.S
preserve_boot_args:
mov x21, x0 // 将dtb的地址暂存在x21寄存器,释放出x0使用
adr_l x0, boot_args // x0保存boot_args变量的地址
stp x21, x1, [x0] // 将x0、x1的值保存到boot_args[0]、boot_args[1]
stp x2, x3, [x0, #16] // 将x2、x3的值保存到boot_args[2]、boot_args[3]
dmb sy // needed before dc ivac with
// MMU off
add x1, x0, #0x20 // x0、x1作为nval_cache_range函数的参数
b __inval_cache_range // tail call
ENDPROC(preserve_boot_args)
el2_setup
到此,CPU处于哪个exception level?根据ARM64 boot protocol,CPU处于EL2(推荐)或者secure EL1;如果处于EL2,需要将CPU退回到EL1;此部分还没有搞明白,暂时先跳过;
// arch/arm64/kernel/head.S
ENTRY(el2_setup)
......
ENDPROC(el2_setup)
set_cpu_boot_mode_flag
set_cpu_boot_mode_flag函数,用来设置__boot_cpu_mode flag;需要一个前提条件:w20寄存器中保存了CPU启动时的异常等级(Exception level);
// arch/arm64/kernel/head.S
set_cpu_boot_mode_flag:
adr_l x1, __boot_cpu_mode
cmp w0, #BOOT_CPU_MODE_EL2
b.ne 1f
add x1, x1, #4
1: str w0, [x1] // This CPU has booted in EL1
dmb sy
dc ivac, x1 // Invalidate potentially stale cache line
ret
ENDPROC(set_cpu_boot_mode_flag)
由于系统启动之后,需要了解CPU启动时候的Exception level,因此需要一个全局变量__boot_cpu_mode来保存启动时的 CPU mode
全局变量__boot_cpu_mode定义:
ENTRY(__boot_cpu_mode)
.long BOOT_CPU_MODE_EL2
.long BOOT_CPU_MODE_EL1
__create_page_tables
建立页表初始化的过程;
// arch/arm64/kernel/head.S
__create_page_tables:
......
ENDPROC(__create_page_tables)
__cpu_setup
CPU的初始化设置;
// arch/arm64/mm/proc.S
ENTRY(__cpu_setup)
......
ENDPROC(__cpu_setup)
主要的内容包括:
- cache和TLB的处理
- Memory attributes lookup table的创建
- SCTLR_EL1、TCR_EL1的设定
__primary_switch
主要工作是为打开MMU做准备;
// arch/arm64/kernel/head.S
__primary_switch:
......
bl __enable_mmu // 开启MMU
......
ldr x8, =__primary_switched
adrp x0, __PHYS_OFFSET
blr x8
ENDPROC(__primary_switch)
在函数中通过__enable_mmu函数来开启MMU,并调用__primary_switched函数;
// arch/arm64/kernel/head.S
__primary_switched:
......
b start_kernel
ENDPROC(__primary_switched)
在__primary_switched函数中,进行一些C环境的准备,并在最后,调用执行start_kernel函数,内核的启动进入到C语言环境阶段;
参考资料
https://blog.csdn.net/xiaohua0877/article/details/78615776 http://www.wowotech.net/sort/armv8a_arch
内核启动第二阶段
start_kernel
Linux内核启动的第二阶段也就是常说的C语言阶段,从start_kernel()函数开始;start_kernel()函数是所有Linux平台进入系统内核初始化后的入口函数;主要完成剩余的与硬件平台相关的初始化工作,这些初始化操作,有的是公共的,有的是需要配置才会执行的;内核工作需要的模块的初始化依次被调用,如:内存管理、调度系统、异常处理等;
start_kernel()函数在 init/main.c 文件中,主要完成Linux子系统的初始化工作;此部分初始化内容繁多,暂时先略过,此处省略好多字;
// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
......
}
...
pr_notice("%s", linux_banner);
...
const char linux_banner[] =
"Linux version " UTS_RELEASE " (" LINUX_COMPILE_BY "@"
LINUX_COMPILE_HOST ") (" LINUX_COMPILER ") " UTS_VERSION "\n";
执行的效果是,在内核启动初期,打印内核版本号和构建信息:
Linux version 4.9.115 (root@localhost.localdomain) (gcc version 6.2.0 20170314 ZTE Embsys-TSP V3.06.40 (GCC) ) #2 SMP PREEMPT Tue Mar 10 11:21:00 CST 2020
setup_arch
setup_arch()函数,是体系结构相关的,该函数根据处理器、硬件平台具体型号设置系统;及解析系统命令行,系统内存管理初始化,统计并注册系统各种资源等;每个体系都有自己的setup_arch()函数,是由顶层Makefile中的ARCH变量定义的,我们使用的是ARCH=arm64,因此,这里的setup_arch()函数,也是arm64的体系结构相关的;参数是未被初始化的内部变量command_line;
// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
......
setup_arch(&command_line);
......
}
setup_arch()函数中的初始化内容比较多,目前只对设备树相关的部分进行简要描述;
// arch/arm64/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
......
setup_machine_fdt(__fdt_pointer);
......
if (acpi_disabled)
unflatten_device_tree();
......
}
setup_machine_fdt
setup_machine_fdt()函数的输入参数是设备树(DTB)首地址;uboot启动程序把设备树读取到内存中,之后在启动内核的同时,将设备树首地址传给内核,setup_machine_fdt()函数的参数__fdt_pointer就是uboot传给内核的设备树地址;函数中的fdt(flat device tree)表示,设备树在内存中是在一块连续地址存储的;
// arch/arm64/kernel/setup.c
static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
void *dt_virt = fixmap_remap_fdt(dt_phys);
if (!dt_virt || !early_init_dt_scan(dt_virt)) {
......
while (true)
cpu_relax();
}
dump_stack_set_arch_desc("%s (DT)", of_flat_dt_get_machine_name());
}
// arch/arm64/kernel/setup.c
phys_addr_t __fdt_pointer __initdata;
全局变量__fdt_pointer指向内存中的DTB,是设备树的物理地址;这个物理地址是由bootloader传递给内核的,在内核中使用,是需要转换为虚拟地址才能访问,而这个转换,由fixmap_remap_fdt()函数来完成;
// drivers/of/fdt.c
bool __init early_init_dt_scan(void *params)
{
bool status;
status = early_init_dt_verify(params);
if (!status)
return false;
early_init_dt_scan_nodes();
return true;
}
接下来调用的early_init_dt_scan()函数,通过进一步调用early_init_dt_verify()函数来检查DTB数据是否完整,经过内存映射之后,就可以直接访问DTB中的内容了;
unflatten_device_tree
unflatten_device_tree()函数完成对设备树的解析,所做的工作是将设备树各节点转换成相应的struct device_node结构体;
// drivers/of/fdt.c
void __init unflatten_device_tree(void)
{
__unflatten_device_tree(initial_boot_params, NULL, &of_root,
early_init_dt_alloc_memory_arch, false);
/* Get pointer to "/chosen" and "/aliases" nodes for use everywhere */
of_alias_scan(early_init_dt_alloc_memory_arch);
}
// drivers/of/base.c
struct device_node *of_root;
static void *__unflatten_device_tree(const void *blob,
struct device_node *dad,
struct device_node **mynodes,
void *(*dt_alloc)(u64 size, u64 align),
bool detached)
{
......
/* First pass, scan for size */
size = unflatten_dt_nodes(blob, NULL, dad, NULL);
......
/* Allocate memory for the expanded device tree */
mem = dt_alloc(size + 4, __alignof__(struct device_node));
......
/* Second pass, do actual unflattening */
unflatten_dt_nodes(blob, mem, dad, mynodes);
......
}
unflatten_dt_nodes()函数,就是从根节点开始,对子节点依次调用populate_node(),为当前节点申请内存空间,并对node进行初始化;并根据读取到的dtb中的内容,按节点进行填充;设备树由dtb二进制文件,经过解析为每一个节点生成一个struct device_node结构体,就完成了dtb的加载过程;
console_init
console_init()函数执行控制台的初始化操作;在console_init()函数执行之前的printk打印信息,需要在console_init()函数执行之后才能打印出来;因为在console_inie()函数之前,printk的打印信息都保存在一个缓存区中,等到console_init()函数执行之后,控制台被初始化完成,就可以将缓冲区中的内容打印出来;
// drivers/tty/tty_io.c
void __init console_init(void)
{
initcall_t *call;
/* Setup the default TTY line discipline. */
n_tty_init();
/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
在console_init()函数中,指定initcall_t类型的函数指针,从__con_initcall_start开始,到__con_initcall_end结束,遍历这个范围之间的函数,依次运行;
__con_initcall_start和__con_initcall_end这两个地址,可以在vmlinux.lds文件找到;如下:
// vmlinux.lds
__con_initcall_start = .;
KEEP(*(.con_initcall.init))
__con_initcall_end = .;
这两个地址之间,存放的是.con_initcall.init段的内容;
// include/linux/init.h
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.con_initcall.init) = fn
通过宏定义console_initcall(fn),将initcall_t类型的函数指针fn,存放到.con_initcall.init段;之后在调用console_init()函数时,就会通过遍历\con_initcall_start到__con_initcall_end的地址区域,依次运行存放在其中的函数fn;
// drivers/tty/serial/8250/8250_core.c
console_initcall(univ8250_console_init);
rest_init
在进行一系列与内核相关的初始化后,在rest_init()函数中,启动了三个进程来开始操作系统的正式运行:
- idle
- kernel_init
- kthreadd
// init/main.c rest_init() kernel_thread(kernel_init, NULL, CLONE_FS); // 创建kernel_init内核线程,即init,1号进程; pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES); // 创建kthreadd内核线程,2号进程,用于管理和调度其他内核线程; ...... init_idle_bootup_task(current); // 设置当前进程(0号进程)为idle进程; schedule_preempt_disabled(); // 调用进程调度,并禁止内核抢占; /* Call into cpu_idle with preempt disabled */ cpu_startup_entry(CPUHP_ONLINE); // 0号进程完成kernel初始化工作,进入idle循环,编程idle进程; - idle进程是操作系统的空闲进程,CPU空闲的时候会去运行它;
- kernel_init进程最开始只是一个函数,作为进程被启动,init进程是永远存在的,PID是1;
- kthreadd是内核守护进程,始终运行在内核空间,负责所有内核线程的调度和管理,PID是2;
也就是说,系统启动后的第一个进程是idle,idle进程是唯一没有通过kernel_thread或fork产生的进程;idle创建了kernel_init进程作为1号进程,创建了kthreadd进程作为2号进程;
kernel_init
kernel_init()函数在创建kernel_init进程时,作为进程被启动;虽然kernel_init最开始只是一个函数,但是在最后,通过系统调用,将读取根文件系统下的init进程,完成从内核态到用户态的转变,转变为用户态的1号进程;这个init进程是所有用户态进程的父进程,产生了大量的子进程;init进程是1号进程,是永远存在的;
kernel_init_freeable
等待内核线程kthreadd创建完成、注册内核驱动模块do_basic_setup、启动默认控制台/dev/console
完成设备初始化以及模块加载工作;
wait_for_completion
// init/main.c
kernel_init_freeable()
{
wait_for_completion(&kthreadd_done);
}
使用完成量等待kthreadd_done,等待内核线程kthreadd创建完成;虽然kernel_init进程先创建,但是要在kthreadd线程创建完成才能执行;在threadd线程创建完成后才唤醒完成量,开始kernel_init进程的工作;
do_basic_setup
driver_init
driver_init()函数完成与驱动程序相关的所有子系统的构建,实现了Linux设备驱动的一个整体框架,但是它只是建立了目录结构,是设备驱动程序初始化的第一部分,具体驱动模块的装载在do_initcalls()函数中实现;
// drivers/base/init.c
void __init driver_init(void)
{
/* These are the core pieces */
devtmpfs_init(); // 注册devtmpfs文件系统,启动devtmpfsd进程
devices_init(); // 初始化驱动模型中的部分子系统,/dev/devices, /dev/char, /dev/block
buses_init(); // 初始化驱动模型中的bus子系统
classes_init(); // 初始化驱动模型中的class子系统
firmware_init(); // 初始化驱动模型中的firmware子系统
hypervisor_init(); // 初始化驱动模型中的hypervisor子系统
/* These are also core pieces, but must come after the
* core core pieces.
*/
platform_bus_init(); // 初始化驱动模型中的bus/platform子系统,此节点是所有platform设备和驱动的总线模型;所有platform设备和驱动都会挂载到这个总线上;
cpu_dev_init(); // 初始化驱动模型中的device/system/cpu子系统,该节点包含CPU相关属性;
memory_dev_init(); // 初始化驱动模型中的device/system/memory子系统,该节点包含了内存相关属性;
container_dev_init(); // 初始化系统总线类型为容器;
of_core_init(); // 初始化创建,访问和结时设备树的过程;
}
do_initcalls
编译器在编译内核时,将一系列模块初始化函数的起始地址按照一定顺序,放在名为section的段中;在内核启动的初始化阶段,do_initcalls()函数中以函数指针的形式取出这些函数的其实地址,依次运行,以完成相应模块的初始化操作,是设备驱动程序初始化的第二部分;由于内核模块可能存在依赖关系,即某些模块的初始化需要依赖其他模块的初始化来完成,因此这些模块的初始化顺序非常重要;
依次调用不同等级的初始化函数:
// init/main.c
static void __init do_initcalls(void)
{
int level;
// 依次调用不同level等级的初始化函数
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
对于同一个level等级下的函数,依次遍历执行:
// init/main.c
static void __init do_initcall_level(int level)
{
initcall_t *fn;
strcpy(initcall_command_line, saved_command_line);
parse_args(initcall_level_names[level],
initcall_command_line, __start___param,
__stop___param - __start___param,
level, level,
NULL, &repair_env_string);
// 对于同一个level等级下的函数,依次遍历执行;
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
开始执行某一个确定的函数:
// init/main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
int count = preempt_count();
int ret;
char msgbuf[64];
if (initcall_blacklisted(fn))
return -EPERM;
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
msgbuf[0] = 0;
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}
WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);
add_latent_entropy();
return ret;
}
编译到内核中的模块,是按照执行的level等级,将模块的初始化函数指针地址,分别放到相对应level等级的section中的;
initcall_t是一个函数指针类型:
typedef int (*initcall_t)(void);
// include/linux/init.h
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn;
__attribute__((__section__())) 表示把对象放在这个由括号中的名称所指代的section中;
__define_initcall()宏的含义是:
- 声明一个名称为
__initcall_##fn的函数指针(其中##表示将两边的变量连接为一个变量); - 将这个函数指针初始化为
fn; - 编译时,要将这个函数指针变量放到名称为
”.initcall” #id “.init”的section中;(比如:level=”2”,表示这个section的名称为”.initcall2.init”)
举例:
\_\_define\_initcall(6, pci\_init)
含义:
- 声明一个函数指针,并赋值:
__initcall_pci_init = pci_init - 编译时要将函数指针变量
__initcall_pci_init放到名称为initcall6.init的section中;(其实就是将pci_init函数的首地址放到名称为initcall6.init的section中)__define_initcall()宏并不会直接使用,而是被定义为其他的宏定义形式使用: ```c // include/linux/init.h
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
通过``core_initcall()``来声明的函数指针,将被放到名为``.initcall1.init``的section中;通过``postcore_initcall()``来声明的函数指针,将被放到名为``.initcall2.init``的section中;以此类推;
举例:
```c\
device_initcall(pci_init);
含义:
- 声明一个函数指针,并赋值:
__initcall_pci_init = pci_init; - 编译时要将函数指针变量
__initcall_pci_init放到名称为initcall6.init的section中;(其实就是将pci_init函数的首地址放到名称为initcall6.init的section中)
在编译生成的vmlinux.lds文件中,可以找到 initcall 相关的定义:
// tmp/bsp/DBG/BOARDLX2160LE/ARMQORIQLE/kernel/kernels/linux-4.9.115-cgel/arch/arm64/kernel/vmlinux.lds
__initcall_start = .; KEEP(*(.initcallearly.init))
__initcall0_start = .; KEEP(*(.initcall0.init)) KEEP(*(.initcall0s.init))
__initcall1_start = .; KEEP(*(.initcall1.init)) KEEP(*(.initcall1s.init))
__initcall2_start = .; KEEP(*(.initcall2.init)) KEEP(*(.initcall2s.init))
__initcall3_start = .; KEEP(*(.initcall3.init)) KEEP(*(.initcall3s.init))
__initcall4_start = .; KEEP(*(.initcall4.init)) KEEP(*(.initcall4s.init))
__initcall5_start = .; KEEP(*(.initcall5.init)) KEEP(*(.initcall5s.init))
__initcallrootfs_start = .; KEEP(*(.initcallrootfs.init)) KEEP(*(.init callrootfss.init))
__initcall6_start = .; KEEP(*(.initcall6.init)) KEEP(*(.initcall6s.init))
__initcall7_start = .; KEEP(*(.initcall7.init)) KEEP(*(.initcall7s.init))
__initcall_end = .;
在这些section中,总的开始位置被标识为__initcall_start,而在结尾被标识为__initcall_end;
do_initcalls()函数,会从这些section中依次取出所有的函数指针,并按顺序调用这些函数指针调用的函数,来分别完成内核中驱动模块的初始化操作;
函数指针被放到哪个section中,是由宏定义__define_initcall(fn, id)的参数id,也就是level决定的,对应level更小的子section的位置更靠前;而位于同一个子section中的函数指针顺序不定,由编译器按照编译顺序随机决定;
// init/main.c
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
ramdisk_execute_command
// init/main.c
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
kernel_init_freeable()函数会去判断ramdisk_execute_command是否为空,如果不为空,就直接运行ramdisk_execute_command指定的程序;ramdisk_execute_command的取值分以下情况:
- 如果命令行参数中指定了
“rdinit=…”,则ramdisk_execute_command等于这个参数指定的程序; - 否则,如果/init程序存在,
ramdisk_execute_command=/init; - 否则,
ramdisk_execute_command为空;// init/main.c if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) { ramdisk_execute_command = NULL; prepare_namespace(); }
free_initmem
free_initmem()函数用来释放所有init.段中的内存;
// arch/arm64/mm/init.c
void free_initmem(void)
{
free_reserved_area(__va(__pa(__init_begin)), __va(__pa(__init_end)),
0, "unused kernel");
/*
* Unmap the __init region but leave the VM area in place. This
* prevents the region from being reused for kernel modules, which
* is not supported by kallsyms.
*/
unmap_kernel_range((u64)__init_begin, (u64)(__init_end - __init_egin));
}
启动用户态 init 进程
// kernel/init/main.c
kernel_init()
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
static int try_to_run_init_process(const char *init_filename)
{
int ret;
ret = run_init_process(init_filename);
return ret;
}
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
return do_execve(getname_kernel(init_filename),
(const char __user *const __user *)argv_init,
(const char __user *const __user *)envp_init);
}
在大多数系统中,bootloader会传递参数给内核的main函数,而这些参数中会包含init=/linuxrc参数,于是在kernel_init进程中,如果有execute_command = “linuxrc”,在经过run_init_process()函数的解析之后,得到需要运行的linuxrc,于是linuxrc程序在run_init_process()函数中被执行,通过do_execve()函数进入用户态,开始文件系统的初始化init进程;
如果boot没有传递init=/linuxrc参数给内核,ramdisk_execute_command和execute_command都为空,则开始按顺序执行/sbin/init、/etc/init、/bin/init、/bin/sh程序,只要有一个可以执行,系统就能够继续运行;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/init.txt for guidance.");
出现这种异常错误,可能是以下几个原因造成:
- 启动参数配置错误,指定了init,但是未找到;
- 文件系统挂载出错;
- 四个应用程序找不到,或者没有可执行权限;
到此,Linux内核部分启动结束,下一步会在文件系统中启动用户空间的init进程,并进行下一步的启动操作。
文件系统启动
filesystem 构建
文件系统可以通过busybox工具来构建;构建成功之后,一般情况下是不需要修改的;我们使用的文件系统,由xxx提供;具体的构建方法此处先省略,有时间再补上;
busybox 程序
在Kernel挂载文件系统后,通过kernel_init()函数,准备运行init进程;
在kernel/init/main.c文件中
// kernel/init/main.c
kernel_init()
{
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
pr_err("Failed to execute %s (error %d)\n",
ramdisk_execute_command, ret);
}
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
}
经过run_init_process()函数的解析之后,得到需要运行的linuxrc,于是linuxrc程序在run_init_process()函数中被执行;而linuxrc文件是一个指向/bin/busybox的链接,也就是说,系统启动后运行的第一个程序是/bin/busybox;
$> ls linuxrc -l
lrwxrwxrwx 1 root root 11 Mar 4 10:04 linuxrc -> bin/busybox
如果ramdisk_execute_command和execute_command都为空,则开始按顺序执行/sbin/init、/etc/init、/bin/init、/bin/sh程序,只要有一个可以执行,系统就能够继续运行;而/sbin/init文件也是链接到/bin/busybox的,最终还是会执行/bin/busybox程序;创建用户空间运行的第一个进程;
$> ls /sbin/init -l
lrwxrwxrwx 1 root root 14 Dec 12 2019 /sbin/init -> ../bin/busybox
init 进程
busybox程序运行,会启动init进程,init进程在Linux系统中是最早运行的进程,也就是1号进程;
init进程进行的工作:
- 为init设置信号处理过程
- 初始化控制台
- 解析
/etc/inittab文件 - 执行系统初始化命令,一般情况下会使用
/etc/init.d/rcS - 执行所有导致init暂停的
inittab命令(动作类型:wait) - 执行所有仅执行一次的
inittab命令(动作类型:once)
执行完以上工作后,init进程会循环执行以下进程:
- 执行所有终止时必须重新启动的inittab命令(动作类型:respawn)
- 执行所有终止时必须重新启动但启动前必须询问用户的inittab命令(动作类型:askfirst)
inittab
busybox程序解析/etc/inittab文件,而/etc/inittab是进行初始化的配置文件;busybox运行时会按照格式解析inittab文件,根据解析内容决定具体工作;
# cat /etc/inittab
::sysinit:/etc/init.d/rcS
#::once:/bin/sw &
#The followed sentence does't need password.
$:respawn:-/bin/ash
#The followed sentence does need password.
#::respawn:-/bin/login
#tty2::askfirst:-/bin/ash
::ctrlaltdel:/bin/umount -a -r
inittab的内容以行为单位,行与行之间没有关联,每行都是一个独立的配置项;每一行的配置项都是由3个冒号分隔开的4个配置值组成,冒号是分隔符,分隔开各个部分;
inittab文件里的代码格式:
<id>:<runlevels>:<action>:<process>
说明:
- id:/dev/id,用作终端terminal:stdin、stdout、stderr、printf、scanf、err
- runlevels:
- action:执行时机;包括:sysinit、respawd、askfirst、wait、once、restart、ctrialtdel、shutdown
- process:应用程序和脚本
rcS
在/etc/inittab配置文件中,action为sysinit的行,表示在Linux系统初始化文件系统时执行的第一个脚本,即:/etc/init.d/rcS;
::sysinit:/etc/init.d/rcS
主要进行一些初始化工作:启动交换分区、检查磁盘、设置主机名、检查并挂载文件系统、加载并初始化硬件模块等;
/etc/init.d/rcS文件是Linux运行时非常重要的一个脚本程序;其他的配置在rcS文件中进行,rcS文件中的配置可以根据需求进行扩展;/etc/init.d/rcS完成各个文件系统的挂载(mount),以及文件硬件模块的初始化;
应用进程启动
应用进程,可以在文件系统的/etc/init.d/rcS脚本运行时,调用xxx.sh脚本拉起来的;
This line appears after every note.