以arm64为例,简单讲述Linux在ARM64架构设备上系统启动的重要流程;
目录
[TOC]
0. 简述
以arm64为例,讲述Linux在ARM64架构设备上的系统启动流程;
上电启动,系统启动要经过uboot、kernel、filesystem、ADM几个过程,如下:
graph LR uboot(uboot)-->kernel(kernel) -->filesystem(filesystem) -->Software(Software)
在大多数系统中,内核镜像在uboot阶段被加载到内存中,并获得控制权开始内核的启动流程;
graph LR uboot(uboot)-->kernel(kernel)
Linux内核版本:
1 | linux-4.9.115 |
说明:由于Linux内核系统庞大繁杂,要想把内核启动中的每个细节都描述清楚,基本上不可能,更何况作者能力水平也达不到;因此,就在追踪内核启动流程代码的同时,只描述比较重要的几个部分;可以在以后的工作、学习中,随着水平的提高,不断地增加内容;
1. 目标文件
内核编译后生成的目标文件是ELF格式的vmlinux,vmlinux文件是各个源代码按照vmlinux.lds设定的规则,链接后得到的Object文件,并不是一个可执行的文件,不能在ARM平台上运行;通常会对其压缩,生成zImage或bzImage;通常内核映像以压缩格式存储,并不是一个可执行的内核;因此内核阶段需要先对内核映像自解压,他们的文件头部打包有解压缩程序;
1.1 vmlinux.lds
vmlinux.lds文件是在内核编译时生成的,是被禁止编辑的;vmlinux.lds文件是在编译时,由vmlinux.lds.S文件对链接器ld的输出进行排序后生成;vmlinux.lds.S是用来对输出文件中的段进行排序,并定义相关的符号名;
1 | arch/arm64/kernel/vmlinux.lds.S |
在项目中通过make时指定的参数-O,将内核编译生成的所有目标文件,包括vmlinux.lds文件重定向输出到以下目录:
1 | arch/arm64/kernel/vmlinux.lds |
vmlinux.lds文件是链接脚本,在内核编译时,作为Makefile的链接器脚本,参与链接生成内核映像vmlinux;
总之:vmlinux是按照vmlinux.lds链接生成的,而vmlinux.lds是由vmlinux.lds.S生成的;
1 | // arch/arm64/kernel/vmlinux.lds |
关于段的信息
text段,代码段,用来存放程序执行代码的一块内存区域;大小在程序运行前确定;
data段,数据段,用来存放程序中已初始化的全局变量的内存区域;数据段属于静态内存分配;
bss段,用来存放程序中未初始化的全局变量和静态变量的一块内存区域;属于静态内存分配;
init段,Linux定义的一种初始化过程中才能用到的段;初始化完成后,该段内存会被释放;
关于地址的信息
加载地址:程序中指令和变量等加载到RAM上的地址;
运行地址:CPU执行一条程序的指令时的执行地址,即PC寄存器的值;就是要寻址到一个指令或变量所使用的地址;
链接地址:链接过程中链接器为指令和变量分配的地址;
1.2 内核映像
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字节;
1.3 设备树文件
Linux内核从3.x版本开始引入设备树的概念,用于实现驱动代码与设备信息分离;设备树出现之前,所有关于设备的具体信息都写在驱动中,外围设备变化,驱动代码就要跟着修改甚至是重写;引入设备树之后,驱动代码只负责处理驱动代码的逻辑,而关于设备的具体信息存放到设备树文件中,这样硬件接口信息变化时,只需要修改设备树文件信息,不需要修改驱动代码就可以;
一般情况下,在编译设备树之前,先在scripts/dtc/目录下,编译生成dtc工具scripts/dtc/dtc,再使用生成的dtc工具编译设备树源码,生成设备树文件;设备树源码和目标文件在arch/arm64/boot/dts/freescale/目录;
在Lx2160板项目中,目标文件会被重定向输出到target目录;
具体的设备树文件的加载、解析过程,会在下文setup_arch部分描述;
2. 内核启动第一阶段
Linux内核启动第一阶段,也就是我们常说的汇编阶段,也就是stext函数的实现内容;这部分主要完成的工作:CPU ID检查,machine ID检查,创建初始化页表,设置C代码运行环境,跳转到内核第一个真正的C函数start_kernel执行;
设置为SVC模式,关闭所有中断
获取CPUID,提取相应的proc info
验证tags或dtb
创建页表项
head.S文件中,
校验启动合法性
建立段式映射的页表并开启MMU
构建C运行环境,跳入C阶段
2.1 内核启动入口点
内核是一个庞大的系统,通过vmlinux反向追踪启动入口点
Lx2160le板的交叉编译工具链:
1 | CROSS_COMPILE=aarch64-linux-gnu- |
通过对目标文件vmlinux的反向追溯,找到Linux执行的入口;
使用readelf命令可以查看vmlinux的入口地址为:0xffff000008080000
1 | readelf -h vmlinux |
这个入口地址是怎么来的,对应内核代码中的哪个部分,可以通过反汇编来分析;
1 | aarch64-linux-gnu-objdump -dxh vmlinux > vmlinux.s |
在得到的汇编文件vmlinux.s中查找入口地址:0xffff000008080000
1 | start address 0xffff000008080000 |
可以得到,Linux入口的第一条指令为:add x13, x18, #0x16;对应的符号是:.head.text _text;
而.head.text段,通过include/linux/init.h文件中的宏定义__HEAD来表示:
1 | /* For assembly routines */ |
内核启动的入口点,在arch/arm64/kernel/head.S文件中;
1 | __HEAD |
到此,已经找到,目标文件vmlinux的入口是arch/arm64/kernel/head.S文件中的__HEAD,在执行完第一条语句(add x13, x18, #0x16)之后,跳转到stext函数执行(b stext);所以,加载vmlinux后,第一个运行的函数是stext;
2.2 stext函数
启动过程中的汇编阶段,是从arch/arm64/kernel/head.S文件开始,执行的起点是stext函数,入口函数是通过vmlinux.lds链接而成,在head.S中ENTRY(stext)指定;
在汇编代码中,宏定义ENTRY和ENDPROC是成对出现的,表示定义的一个函数,同时也要指明当前代码所在的段,如:__INIT;
1 | // arch/arm64/kernel/head.S |
内核启动的必要条件:MMU关闭,D-cache关闭,x0是传递给FDT blob的物理地址;
1 | * The requirements are: |
stext函数开始执行;
1 | // arch/arm64/kernel/head.S |
1. preserve_boot_args
保存从bootloader传递过来的x0 ~ x3参数;
1 | // arch/arm64/kernel/head.S |
2. el2_setup
到此,CPU处于哪个exception level?根据ARM64 boot protocol,CPU处于EL2(推荐)或者secure EL1;如果处于EL2,需要将CPU退回到EL1;此部分还没有搞明白,暂时先跳过;
1 | // arch/arm64/kernel/head.S |
3. set_cpu_boot_mode_flag
set_cpu_boot_mode_flag函数,用来设置__boot_cpu_mode flag;需要一个前提条件:w20寄存器中保存了CPU启动时的异常等级(Exception level);
1 | // arch/arm64/kernel/head.S |
由于系统启动之后,需要了解CPU启动时候的Exception level,因此需要一个全局变量__boot_cpu_mode来保存启动时的CPU mode;
全局变量__boot_cpu_mode定义:
1 | ENTRY(__boot_cpu_mode) |
4. __create_page_tables
建立页表初始化的过程;
1 | // arch/arm64/kernel/head.S |
5. __cpu_setup
CPU的初始化设置;
1 | // arch/arm64/mm/proc.S |
主要的内容包括:
1
2
3 1、cache和TLB的处理
2、Memory attributes lookup table的创建
3、SCTLR_EL1、TCR_EL1的设定
6. __primary_switch
主要工作是为打开MMU做准备;
1 | // arch/arm64/kernel/head.S |
在函数中通过__enable_mmu函数来开启MMU,并调用__primary_switched函数;
1 | // arch/arm64/kernel/head.S |
在__primary_switched函数中,进行一些C环境的准备,并在最后,调用执行start_kernel函数,内核的启动进入到C语言环境阶段;
graph TB subgraph 入口函数 A_Explain(保存boot参数) --> B_Explain(EL2设置) --> C_Explain(设置CPU启动模式flag) --> D_Explain(创建页表) --> E_Explain(CPU配置) --> F_Explain(主要的转换操作) --> G_Explain(运行start_kernel函数) end subgraph stext A_func(preserve_boot_args) --> B_func(el2_setup) --> C_func(set_cpu_boot_mode_flag) --> D_func(__create_page_tables) --> E_func(__cpu_setup) --> F_func(__primary_switch) --> G_func(start_kernel) end
2.3 参考资料
https://blog.csdn.net/xiaohua0877/article/details/78615776
http://www.wowotech.net/sort/armv8a_arch
3. 内核启动第二阶段
Linux内核启动的第二阶段也就是常说的C语言阶段,从start_kernel()函数开始;start_kernel()函数是所有Linux平台进入系统内核初始化后的入口函数;主要完成剩余的与硬件平台相关的初始化工作,这些初始化操作,有的是公共的,有的是需要配置才会执行的;内核工作需要的模块的初始化依次被调用,如:内存管理、调度系统、异常处理等;
graph TB A(start_kernel)-->B(一系列的初始化操作) subgraph start_kernel B(一系列的初始化操作) --> C(setup_arch) --> D(一系列的初始化操作) --> E(rest_init) end
3.1 start_kernel
start_kernel()函数在init/main.c文件中,主要完成Linux子系统的初始化工作;此部分初始化内容繁多,暂时先略过,此处省略好多字;
1 | // init/main.c |
1 | pr_notice("%s", linux_banner); |
1 | const char linux_banner[] = |
执行的效果是,在内核启动初期,打印内核版本号和构建信息:
1 | 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 |
3.2 setup_arch
setup_arch()函数,是体系结构相关的,该函数根据处理器、硬件平台具体型号设置系统;及解析系统命令行,系统内存管理初始化,统计并注册系统各种资源等;每个体系都有自己的setup_arch()函数,是由顶层Makefile中的ARCH变量定义的,我们使用的是ARCH=arm64,因此,这里的setup_arch()函数,也是arm64的体系结构相关的;参数是未被初始化的内部变量command_line;
1 | // init/main.c |
setup_arch()函数中的初始化内容比较多,目前只对设备树相关的部分进行简要描述;
1 | // arch/arm64/kernel/setup.c |
graph LR A(setup_arch)-->B(加载设备树) B(加载设备树)-->C(setup_machine_fdt) B(加载设备树)-->D(unflatten_device_tree)
1. setup_machine_fdt
setup_machine_fdt()函数的输入参数是设备树(DTB)首地址;uboot启动程序把设备树读取到内存中,之后在启动内核的同时,将设备树首地址传给内核,setup_machine_fdt()函数的参数__fdt_pointer就是uboot传给内核的设备树地址;函数中的fdt(flat device tree)表示,设备树在内存中是在一块连续地址存储的;
1 | // arch/arm64/kernel/setup.c |
1 | // arch/arm64/kernel/setup.c |
全局变量__fdt_pointer指向内存中的DTB,是设备树的物理地址;这个物理地址是由bootloader传递给内核的,在内核中使用,是需要转换为虚拟地址才能访问,而这个转换,由fixmap_remap_fdt()函数来完成;
1 | // drivers/of/fdt.c |
接下来调用的early_init_dt_scan()函数,通过进一步调用early_init_dt_verify()函数来检查DTB数据是否完整,经过内存映射之后,就可以直接访问DTB中的内容了;
2. unflatten_device_tree
unflatten_device_tree()函数完成对设备树的解析,所做的工作是将设备树各节点转换成相应的struct device_node结构体;
1 | // drivers/of/fdt.c |
1 | // drivers/of/base.c |
1 | static void *__unflatten_device_tree(const void *blob, |
__unflatten_device_tree()函数中主要的解析函数是unflatten_dt_nodes(),在这里被调用了两次,第一次是扫描出设备树转换成struct device_node所需要的空间,然后系统申请内存空间,第二次是进行真正解析的工作;
1 | // drivers/of/fdt.c |
unflatten_dt_nodes()函数,就是从根节点开始,对子节点依次调用populate_node(),为当前节点申请内存空间,并对node进行初始化;并根据读取到的dtb中的内容,按节点进行填充;设备树由dtb二进制文件,经过解析为每一个节点生成一个struct device_node结构体,就完成了dtb的加载过程;
3.3 console_init
console_init()函数执行控制台的初始化操作;在console_init()函数执行之前的printk打印信息,需要在console_init()函数执行之后才能打印出来;因为在console_inie()函数之前,printk的打印信息都保存在一个缓存区中,等到console_init()函数执行之后,控制台被初始化完成,就可以将缓冲区中的内容打印出来;
1 | // drivers/tty/tty_io.c |
在console_init()函数中,指定initcall_t类型的函数指针,从__con_initcall_start开始,到__con_initcall_end结束,遍历这个范围之间的函数,依次运行;
__con_initcall_start和__con_initcall_end这两个地址,可以在vmlinux.lds文件找到;如下:
1 | // vmlinux.lds |
这两个地址之间,存放的是.con_initcall.init段的内容;
1 | // include/linux/init.h |
通过宏定义console_initcall(fn),将initcall_t类型的函数指针fn,存放到.con_initcall.init段;之后在调用console_init()函数时,就会通过遍历\con_initcall_start到__con_initcall_end的地址区域,依次运行存放在其中的函数fn;
1 | // drivers/tty/serial/8250/8250_core.c |
3.4 rest_init
在进行一系列与内核相关的初始化后,在rest_init()函数中,启动了三个进程:idle、kernel_init、kthreadd,来开始操作系统的正式运行;
1 | // init/main.c |
graph TB A(rest_init)-->B(idle进程) A(rest_init)-->C(kernel_init进程) A(rest_init)-->D(kthreadd进程)
- idle进程是操作系统的空闲进程,CPU空闲的时候会去运行它;
- kernel_init进程最开始只是一个函数,作为进程被启动,init进程是永远存在的,PID是1;
- kthreadd是内核守护进程,始终运行在内核空间,负责所有内核线程的调度和管理,PID是2;
也就是说,系统启动后的第一个进程是idle,idle进程是唯一没有通过kernel_thread或fork产生的进程;idle创建了kernel_init进程作为1号进程,创建了kthreadd进程作为2号进程;
graph TB A(idle进程)-->B(kernel_init进程) A(idle进程)-->C(kthreadd进程)
3.5 kernel_init
kernel_init()函数在创建kernel_init进程时,作为进程被启动;虽然kernel_init最开始只是一个函数,但是在最后,通过系统调用,将读取根文件系统下的init进程,完成从内核态到用户态的转变,转变为用户态的1号进程;这个init进程是所有用户态进程的父进程,产生了大量的子进程;init进程是1号进程,是永远存在的;
graph TB A(kernel_init)-->B(kernel_init_freeable) subgraph kernel_init B(kernel_init_freeable) -->C(free_initmem) -->D(ramdisk_execute_command) -->E(execute_command) -->F(用户空间init进程) end
3.6 kernel_init_freeable
等待内核线程kthreadd创建完成、注册内核驱动模块do_basic_setup、启动默认控制台/dev/console
完成设备初始化以及模块加载工作;
graph TB A(kernel_init_freeable)-->B(wait_for_completion) subgraph kernel_init_freeable B(wait_for_completion) -->C(set_mems_allowed) -->D(do_basic_setup) -->E(open console) -->X(ramdisk_execute_command) end
3.6.1 wait_for_completion
1 | // init/main.c |
使用完成量等待kthreadd_done,等待内核线程kthreadd创建完成;虽然kernel_init进程先创建,但是要在kthreadd线程创建完成才能执行;在threadd线程创建完成后才唤醒完成量,开始kernel_init进程的工作;
3.6.2 do_basic_setup
graph TB A(do_basic_setup)-->C(driver_init) subgraph do_basic_setup C(driver_init) -->D(do_initcalls) end
2. driver_init
driver_init()函数完成与驱动程序相关的所有子系统的构建,实现了Linux设备驱动的一个整体框架,但是它只是建立了目录结构,是设备驱动程序初始化的第一部分,具体驱动模块的装载在do_initcalls()函数中实现;
1 | // drivers/base/init.c |
3. do_initcalls
编译器在编译内核时,将一系列模块初始化函数的起始地址按照一定顺序,放在名为section的段中;在内核启动的初始化阶段,do_initcalls()函数中以函数指针的形式取出这些函数的其实地址,依次运行,以完成相应模块的初始化操作,是设备驱动程序初始化的第二部分;由于内核模块可能存在依赖关系,即某些模块的初始化需要依赖其他模块的初始化来完成,因此这些模块的初始化顺序非常重要;
依次调用不同等级的初始化函数:
1 | // init/main.c |
对于同一个level等级下的函数,依次遍历执行:
1 | // init/main.c |
开始执行某一个确定的函数:
1 | // init/main.c |
编译到内核中的模块,是按照执行的level等级,将模块的初始化函数指针地址,分别放到相对应level等级的section中的;
initcall_t是一个函数指针类型:
1 | typedef int (*initcall_t)(void); |
1 | // include/linux/init.h |
__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()宏并不会直接使用,而是被定义为其他的宏定义形式使用:
1 | // include/linux/init.h |
通过core_initcall()来声明的函数指针,将被放到名为.initcall1.init的section中;通过postcore_initcall()来声明的函数指针,将被放到名为.initcall2.init的section中;以此类推;
举例:
device_initcall(pci_init);
含义:
- 声明一个函数指针,并赋值:__initcall_pci_init = pci_init;
- 编译时要将函数指针变量__initcall_pci_init放到名称为initcall6.init的section中;(其实就是将pci_init函数的首地址放到名称为initcall6.init的section中)
在编译生成的vmlinux.lds文件中,可以找到initcall相关的定义:
1 | // tmp/bsp/DBG/BOARDLX2160LE/ARMQORIQLE/kernel/kernels/linux-4.9.115-cgel/arch/arm64/kernel/vmlinux.lds |
在这些section中,总的开始位置被标识为__initcall_start,而在结尾被标识为__initcall_end;
do_initcalls()函数,会从这些section中依次取出所有的函数指针,并按顺序调用这些函数指针调用的函数,来分别完成内核中驱动模块的初始化操作;
函数指针被放到哪个section中,是由宏定义__define_initcall(fn, id)的参数id,也就是level决定的,对应level更小的子section的位置更靠前;而位于同一个子section中的函数指针顺序不定,由编译器按照编译顺序随机决定;
1 | // init/main.c |
3.6.3 ramdisk_execute_command
1 | // init/main.c |
kernel_init_freeable()函数会去判断ramdisk_execute_command是否为空,如果不为空,就直接运行ramdisk_execute_command指定的程序;ramdisk_execute_command的取值分以下情况:
- 如果命令行参数中指定了“rdinit=…”,则ramdisk_execute_command等于这个参数指定的程序;
- 否则,如果/init程序存在,ramdisk_execute_command=/init;
- 否则,ramdisk_execute_command为空;
1 | // init/main.c |
3.7 free_initmem
free_initmem()函数用来释放所有init.段中的内存;
1 | // arch/arm64/mm/init.c |
3.8 启动用户态init进程
1 | // kernel/init/main.c |
1 | static int try_to_run_init_process(const char *init_filename) |
1 | static int run_init_process(const char *init_filename) |
在大多数系统中,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程序,只要有一个可以执行,系统就能够继续运行;
1 | panic("No working init found. Try passing init= option to kernel. " |
出现这种异常错误,可能是以下几个原因造成:
- 启动参数配置错误,指定了init,但是未找到;
- 文件系统挂载出错;
- 四个应用程序找不到,或者没有可执行权限;
到此,Linux内核部分启动结束,下一步会在文件系统中启动用户空间的init进程,并进行下一步的启动操作;
4. filesystem启动
graph LR kernel_init("kernel_init()")--"init=linuxrc"-->linuxrc(linuxrc) --链接-->busybox("/bin/busybox") kernel_init("kernel_init()")--default-->init("/sbin/init") --链接-->busybox("/bin/busybox") --inittab-->rcS("/etc/init.d/rcS")
4.1 filesystem构建
文件系统可以通过busybox工具来构建;构建成功之后,一般情况下是不需要修改的;我们使用的文件系统,由成研提供;具体的构建方法此处先省略,有时间再补上;
4.2 busybox程序
在Kernel挂载文件系统后,通过kernel_init()函数,准备运行init进程;
在kernel/init/main.c文件中
1 | // kernel/init/main.c |
经过run_init_process()函数的解析之后,得到需要运行的linuxrc,于是linuxrc程序在run_init_process()函数中被执行;而linuxrc文件是一个指向/bin/busybox的链接,也就是说,系统启动后运行的第一个程序是/bin/busybox;
1 | ls linuxrc -l |
如果ramdisk_execute_command和execute_command都为空,则开始按顺序执行/sbin/init、/etc/init、/bin/init、/bin/sh程序,只要有一个可以执行,系统就能够继续运行;而/sbin/init文件也是链接到/bin/busybox的,最终还是会执行/bin/busybox程序;创建用户空间运行的第一个进程;
1 | ls /sbin/init -l |
4.3 init进程
busybox程序运行,会启动init进程,init进程在Linux系统中是最早运行的进程,也就是1号进程;
1 | ps -aux | grep init |
init进程进行的工作:
- 为init设置信号处理过程
- 初始化控制台
- 解析/etc/inittab文件
- 执行系统初始化命令,一般情况下会使用/etc/init.d/rcS
- 执行所有导致init暂停的inittab命令(动作类型:wait)
- 执行所有仅执行一次的inittab命令(动作类型:once)
执行完以上工作后,init进程会循环执行以下进程:
1.执行所有终止时必须重新启动的inittab命令(动作类型:respawn)
2.执行所有终止时必须重新启动但启动前必须询问用户的inittab命令(动作类型:askfirst)
4.4 inittab
busybox程序解析/etc/inittab文件,而/etc/inittab是进行初始化的配置文件;busybox运行时会按照格式解析inittab文件,根据解析内容决定具体工作;
1 | cat /etc/inittab |
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:应用程序和脚本
4.5 rcS
在/etc/inittab配置文件中,action为sysinit的行,表示在Linux系统初始化文件系统时执行的第一个脚本,即:/etc/init.d/rcS;
1 | ::sysinit:/etc/init.d/rcS |
主要进行一些初始化工作:启动交换分区、检查磁盘、设置主机名、检查并挂载文件系统、加载并初始化硬件模块等;
/etc/init.d/rcS文件是Linux运行时非常重要的一个脚本程序;其他的配置在rcS文件中进行,rcS文件中的配置可以根据需求进行扩展;/etc/init.d/rcS完成各个文件系统的挂载(mount),以及文件硬件模块的初始化;
5. 应用进程启动
应用进程,可以在文件系统的/etc/init.d/rcS脚本运行时,调用xxx.sh脚本拉起来的;