笼统的讲,嵌入式设备的启动流程可以分为:bootloader --> kernel --> rootfs --> app。
接下来就按照系统启动顺序来进行分析。
一、bootloader
开源社区使用的引导加载程序(bootloader)有很多,而U-Boot是其中最流行的,适用于Power、MIPS、ARM等架构。本篇文章就以U-Boot为例来进行讲述。
相信大多数人都接触过“Hello World”这样一个打印函数,但即使是这样简单的一个程序,也需要在很多硬件得到初始化之后才能运行,而引导加载程序就负责完成这项任务。
当第一次加电时,目标板上的U-Boot立即获得处理器的控制权,该程序执行一些非常底层的硬件初始化,包括:
- 初始化关键的硬件,比如SDRAM控制器、I/O控制器、以太网控制器。
- 初始化处理器和内存,启用内存子系统。
- 初始化UART用于控制串行端口。
- 为外设控制器分配必要的系统资源,比如内存和中断电路。
- 提供一个定位和加载操作系统镜像的机制。
- 加载操作系统,并将控制权移交给它,同时传递必要的启动信息。这些信息可能包括内存总容量、时钟频率、串行端口速率和其他与底层硬件相关的配置数据。
U-Boot完成硬件的初始化之后,在其短暂但有益的生命中还剩最后的也是最重要的一件工作:加载并引导Linux内核。
在U-Boot的自动启动过程中,U-Boot会通过一条命令来启动内核,这条命令会影响内核以及文件系统的启动动作,这条命令一般会是这样的:
“bootcmd=nfs 0x30008000 192.168.1.100:/aa/bb/uImage;bootm”
表示uboot以nfs的方式加载位于192.168.1.100:/aa/bb/这个目录下面的uImage文件,加载之后再执行bootm。
这条命令涉及到 U-Boot 的两个内容:bootcmd环境变量 以及 bootm命令。
1、自动运行命令:bootcmd
U-Boot 启动后会开机自动倒数 bootdelay 秒。如果没人按回车来中断启动,则uboot会自动执行启动命令(bootcmd)来启动内核。
bootcmd 是 U-Boot 自动启动时默认执行的一些命令集合,可自定义配置自己常用的一些参数。
系统自动启动会默认执行bootcmd,所以一般 bootcmd 命令的最后一句都会是;bootm ${address}。
2、bootm命令
bootm要做的事情:
a. 读取内核镜像 uImage 头部,把内核拷贝到合适的地方
b. 将启动参数给内核准备好,并告诉内核参数的首地址
c. 设置cpu寄存器,禁止中断,关闭 MMU 和 cache
d. 跳转到内核的入口地址,kernel 开始运行
注1:bootm 准备的这个启动参数,就是 U-Boot 的环境变量 bootargs 中的值,bootargs 是 U-Boot 传递给内核的启动字符串,通过它可以设定内核的一些运行细节。
注2:bootm指令是专门用于启动在 SDRAM 中的用 U-boot 的 mkimage 工具处理过的内核镜像。因此在执行 bootm 命令的时候必须确保 image 文件已经在内存中。
内核镜像介绍:
- vmlinux:Linux内核编译生成的ELF格式的原始内核主题镜像,包含符号、注释、调试信息(如果编译时使用了-g选项)和与架构相关的部分,该镜像可用于定位内核问题(readelf -s vmlinux),但不能直接引导Linux系统启动。
- Image :使用 objcopy 处理 vmlinux 后( 去除其中的符号、标记和注释等 )生成的二进制内核模块。该镜像未压缩,可直接引导Linux系统启动。
- zImage:经过 gzip 压缩 Image 后生成的 Linux 内核镜像。
- uImage:使用工具 mkimage 对普通的压缩内核映像文件(zImage)加工而得。它是 U-Boot 专用的映像文件,它是在zImage之前加上一个长度为64字节的头,说明这个内核的版本、加载位置、生成时间、大小等信息。
注:U-Boot加载内核时,使用的是uImage镜像。
3、bootargs变量
3.1、bootargs简介
bootargs 是 U-Boot 的一个环境变量,它的种类非常多,使用非常灵活,内核和文件系统的不同搭配就会有不同的设置方法,甚至也可以不设置 bootargs,而直接将其写到内核中去(在内核配置时可以设置)。
bootargs 由许多用空格隔开的项目组成,每个项目中都是“项目名=项目值”。它会被内核解析成一个个“项目名=项目值”的字符串,这些字符串又会被再次解析从而影响启动过程。
3.2、bootargs中的常见项目
root=
这个项目用来指定 rootfs 的位置, 常见的情况有:
//mtd是字符设备,而mtdblock是块设备 //下面的'x'在具体的例子里需要用数字表示 root=/dev/nfs rw //表示使用基于NFS的文件系统来启动系统,后面的rw表示对该文件系统具有可读可写的权限。 root=/dev/mtdx rw //mtd字符设备 root=/dev/mtdblockx rw //mtd块设备 root=/dev/mtdblock/x rw //mtd块设备 root=/dev/mmcblk0px rw //emmc设备 root=31:0x //如果可以直接指定设备名,那么也可以使用此设备的设备号
root的情形如下:
(1)如果在nandflash上,则root=/dev/mtdblock2;
(2)如果在inand/sd上,则root=/dev/mmcblk0p2(设备0的第二分区))。
(3)如果是nfs,则root=/dev/nfs。此情形还需要指定nfsroot=serverip:nfs_dir,即指明文件系统存在哪个主机的哪个目录下面。
rootfstype=
这个项目表示 rootfs 的文件系统类型,需要与 root 配合使用。
根文件系统的类型一般有jffs2、yaffs2、ext2、ext3、ext4、ubi等等。
如果是 ext2 文件系统,有没有这个选项是无所谓的,但如果是 jffs2、squashfs 等文件系统,就需要 rootfstype 指明文件系统的类型,不然会无法挂载根文件系统。
console=
(1)这个项目是控制台信息声明,比如“console=/dev/ttySAC0,115200”,就表示控制台使用串口0,波特率是115200。
(2)内核启动时,会根据console=这个项目来初始化硬件,并且重定位console到具体的一个串口上,所以这里的传参会影响后续是否能从串口终端上接收到内核的信息。
(3)console常见的情形
- 比如console=tty,表示使用虚拟串口终端设备。
- 比如console=ttyS[n,options],表示使用特定的串口n,options的形式为“bbbpm”,这里bbb是指串口的波特率,p是奇偶位(没有使用过),m是指的bits。
- 比如console=ttySAC[n,options],含义同上面。比如console=/dev/ttySAC0,115200,表示控制台使用串口0,波特率是115200。
mem=
这个项目用来告诉内核当前系统的内存有多少,不是必须的。
initrd=, noinitrd
当没有使用 ramdisk 启动系统的时候,需要使用 noinitrd 这个项目。
如果使用了,就需要指定“ initrd=r_addr,size ”。其中 r_addr 表示 initrd 在内存中的位置,size表示 initrd 的大小。
init=
这个项目用来指定Linux内核启动完毕后调用的第一个、也是唯一的一个用户态进程,即进程号为1的进程,参数一般为 init=/linuxrc,或者init=/etc/preinit。
很多初学者以为init=/linuxrc是固定写法,其实不然,/linuxrc指的是/目录下面的linuxrc脚本,一般是busybox的软链接。
preinit的内容一般是创建console、null设备节点,运行init程序,挂载一些文件系统等等操作。
由于 init 进程是 kernel 启动后的第一个、也是唯一的一个用户态进程,用户的许多应用程序,比如Mplayer、Qt、Boa都由该进程来启动,换句话说,init进程是所有进程的发起者。
mtdparts=
mtdparts=fc000000.nor_flash:1920k(linux),128k(fdt),20M(ramdisk),4M(jffs2),38272k(user),256k(env),384k(uboot)
要想这个项目起作用,内核必须要支持mtd驱动,即在内核配置时需要选上
Device Drivers ---> Memory Technology Device (MTD) support ---> Command line partition table parsing
mtdparts的格式如下:
mtdparts=[; := :[,] := [@offset][][ro] := unique id used in mapping driver/device := standard linux memsize OR "-" to denote all remaining space := (NAME)
因此在使用的时候需要按照下面的格式来设置:
mtdparts=mtd-id:@(),@()
这里面有几个必须要注意的。
(1)mtd-id 必须要跟你当前平台的flash的mtd-id一致,不然整个mtdparts会失效。
(2)size在设置的时候可以为实际的size(xxM,xxk,xx),也可以为'-'这表示剩余的所有空间。举例:假设flash 的mtd-id是sa1100,那么你可以使用下面的方式来设置:
mtdparts=sa1100:- //只有一个分区 mtdparts=sa1100:256k(ARMboot)ro,-(root) //有两个分区
可以查看drivers/mtd/cmdlinepart.c中的注释找到相关描述。
ip=
这个项目用来设置系统启动之后网卡的ip地址,如果你使用基于nfs的文件系统,那么必须要有这个参数,其他的情况下就看你自己的喜好了。
设置ip有两种方法:
ip = ip addr ip = ip addr:server ip addr:gateway:netmask::which netcard:off
这两种方法都可以用,不过很明显第二种要详细很多。
请注意第二种中 which netcard 是指开发板上的网卡,而不是主机上的网卡。
initcall_debug=
设置为0表示关闭,设置为1表示打开。
打开时,内核启动过程中会增加如下形式的日志,在调用每一个init函数前有一句打印,结束后再有一句打印并且输出了该Init函数运行的时间,通过这个信息可以用来定位启动过程中哪个init函数运行失败以及哪些init函数运行时间较长。这是个查看内核初始化细节的好办法,特别是可以了解内核调用各个子系统和模块的顺序。
3.3、 bootargs常用的几种组合
1、假设文件系统是ramdisk,且直接就在内存中,bootargs的设置应该如下:
setenv bootargs ‘initrd=0x32000000,0xa00000 root=/dev/ram0 console=ttySAC0 mem=64M init=/linuxrc’
2、假设文件系统是ramdisk,且在 flash 中,bootargs的设置应该如下:
setenv bootargs ‘mem=32M console=ttyS0,115200 root=/dev/ram rw init=/linuxrc’
注意这种情况下你应该要在bootm命令中指定ramdisk在flash中的地址,如bootm kernel_addr ramdisk_addr (fdt_addr)
3、假设文件系统是jffs2类型的,且在flash中,bootargs的设置应该如下:
setenv bootargs ‘mem=32M console=ttyS0,115200 noinitrd root=/dev/mtdblock2 rw rootfstype=jffs2 init=/linuxrc’
4、假设文件系统是基于nfs的,bootargs的设置应该如下:
setenv bootargs ‘noinitrd mem=64M console=ttySAC0 root=/dev/nfs nfsroot=192.168.0.3:/nfs ip=192.168.0.5:192.168.0.3:192.168.0.3:255.255.255.0::eth0:off’
或者
setenv bootargs 'noinitrd mem=64M console=ttySAC0 root=/dev/nfs nfsroot=192.168.0.3:/nfs ip=192.168.0.5'
5、rootfs 在 SD/iNand/Nand/Nor 等物理存储器上。这种对应产品正式出货工作时的情况。
setenv bootargs 'console=ttySAC2,115200 root=/dev/mmcblk0p2 rw init=/linuxrc rootfstype=ext3'
6、rootfs在nfs上,这种对应我们实验室开发产品做调试的时候。
setenv bootargs 'root=/dev/nfs nfsroot=192.168.1.100:/root/rootfs/ ip=192.168.1.20:192.168.1.100:192.168.1.1:255.255.255.0::eth0:off init=/linuxrc console=ttySAC2,115200'
3.4、打印系统环境变量
printenv / print 命令是不带参命令,作用是打印出系统中所有的环境变量。
环境变量被存储在Flash的一块专门区域(Flash上有一个环境变量分区),一旦我们在程序中保存了该环境变量,那么下次开机时该环境变量的值将维持上一次更改保存后的值。
二、kernel
当引导加载程序完成一些底层的硬件初始化后,会将控制权转交给Linux内核。但对于很多架构来说,在Linux内核代码执行之前,还有一些初始化的动作必须要执行,例如内核代码运行所需的堆栈环境等,而这部分的工作,是由启动加载程序来完成的。
1、启动加载程序
很多架构都使用启动加载程序(第2阶段加载程序)将Linux内核镜像加载到内存中,可以将其看作是引导加载程序与linux内核镜像之间的粘合剂。启动加载程序负责提供合适的上下文让内核运行于其中,并且执行必要的步骤以解压和重新部署内核二进制镜像。有些启动加载程序还会对内核镜像进行校验和检查。
如图所示,针对ARM架构的启动加载程序与内核镜像拼接在一起,用于加载。
这个启动加载程序完成以下功能:
- 底层的、用汇编语言实现的处理器初始化,这包括支持处理器内部指令和数据缓存、禁止中断并建立C语言运行环境。这部分功能由 head.o 和 head-xscale.o 完成。
- 解压和重新部署镜像。这部分功能由 misc.o 完成。
- 其他与处理器相关的初始化。比如 big-endian.o,将特定处理器的字节序设置为大端字节序。
启动加载程序先初始化处理器和必须的内存区域,然后解压二进制内核镜像(piggy.gz),并将解压后的内核镜像(Image)加载到系统内存的合适位置,最后将控制权转交给它。
2、初始化时的控制流
系统第一次加电时,引导加载程序开始执行,然后会加载操作系统。当引导加载程序部署并加载了操作系统镜像(这个镜像可能存储在本地的闪存中,硬盘驱动器中,或通过局域网或其他设备)之后,就将控制权转交给那个镜像。
对于ARM平台来说,引导加载程序将控制权转交给启动加载程序的 head.o;启动加载程序完成它的工作后,将控制权转交给内核主体的 head.o,之后再转到文件 main.c 中的函数start_kernel(),如图所示:
2.1 内核入口:head.o
head.o 模块完成与架构和 CPU 相关的初始化,为内核主体的执行做好准备。同时,head.o 还要执行下列底层任务:
检查处理器和架构的有效性;
创建初始的页表(page table)表项;
启用处理器的内存管理单元(MMU);
进行错误检测并报告;
跳转到内核主体的起始位置,也就是文件 main.c 中的函数 start_kernel()。
虚实地址的转换
当启动加载程序将控制权转交给内核的 head.o 时,此时处理器依然运行于我们常说的实地址模式,在这种模式下,逻辑地址和物理地址的值是相同的。
为了启用内存地址转换,需要先初始化相关的寄存器和内核数据结构,当这些初始化完成后,就会开启处理器的 MMU。在MMU开启的一瞬间,处理器看到的地址空间就从物理地址替换成了虚拟地址,这个空间的结构和形式是由内核开发者决定的。
2.2 内核启动:main.c
内核自身的 head.o 模块完成的最后一个任务,就是将控制权转交给一个由C语言编写的,负责内核启动的源文件 main.c 。
不同架构的 head.o 模块在转交控制权时会使用不同的方法,汇编语言的语法也不同,但它们的代码结构是类似的。对于 ARM 架构,如下所示:
b start_kernel //32位
bl start_kernel //64位
汇编语言之后的大部分 Linux 内核启动工作都是由 main.c 来完成的,从初始化第一个内核线程开始,直到挂载根文件系统并执行最初的用户空间 Linux 应用程序。
2.3 架构设置
.../init/main.c 中的函数 start_kernel() 在其执行的开始阶段会调用 setup_arch(),该函数接受一个参数——一个指向内核命令行的指针:
setup_arch(&command_line);
该语句调用一个与具体架构相关的设置函数,负责完成那些对具体架构通用的初始化工作。
3、内核命令行的处理
在设置了架构之后,main.c 开始执行一些通用的早期内核初始化工作,并显示内核命令行:
Kernel command line:console=ttyS0,115200 root=/dev/nfs ip=dhcp
Linux一般是由一个引导加载程序(或启动加载程序)启动的,它会向内核传递一系列参数,这些参数称为内核命令行。这些命令行参数相当于一种引导机制,用于设置一些必需的初始配置,以正确引导特定的机器。
命令行是全局可访问的,并且可以由很多模块处理。我们前面说过,mian.c 中的函数start_kernel() 在调用函数 setup_arch() 时会传入内核命令行作为函数的参数,这也是唯一的参数。通过这个调用,与架构相关的参数和配置就被传递给了那些与架构和机器相关的代码。
_ _setup宏
_ _setup 宏是在文件 .../include/linux/init.h 中定义的,用于将内核命令行字符串的一部分同某个函数关联起来,而这个函数会处理字符串的那个部分。
#define __setup(str, fn) __setup_param(str, fn, fn, 0)
最终所有传入的命令行参数,会构建成为一个静态列表,其中的每一项包含了一个字符串字面量及其关联的函数指针。这个列表由编译器生成,存放在一个单独命名的 ELF 段中,而该段是最终的 ELF 镜像 vmlinux 的一部分。
4、init线程(内核)
函数 start_kernel() 执行基本的内核初始化之后,就会生成第一个内核线程。这个线程就是称为 init 的内核线程,同时也是所有用户空间 Linux 进程的父进程,它的进程 ID 为1。
内核代码中生成 init 的流程如图所示:
注:内核 init 线程是通过调用 user_mode_thread() 函数生成的,以函数 kernel_init 作为其第一个参数,作为内核 init 线程的执行函数。
kernel_init 在调用各个初始化函数之后,内核开始执行引导过程的最后一些步骤。这包括释放初始化函数和数据所占用的内存,打开系统控制台设备,并启动第一个用户空间进程。如图代码清单显示了内核的 init 进程执行的最后一些步骤,代码来自文件main.c 。
static int __ref kernel_init(void *unused)
{
<... 为了简化,这里省略了一些代码行 ...>
...
/* 如果内核命令行参数带有 init=xxx,则进入该条语句 */
if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
panic("Requested init %s failed (error %d).",
execute_command, ret);
}
/* .../init/Kconfig中的一个配置项,如果内核配置了默认init文件,则进入该语句 */
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (ret)
pr_err("Default init %s failed (error %d)\n",
CONFIG_DEFAULT_INIT, ret);
else
return 0;
}
/* 如果上面两个条件判断都没设定,则尝试执行这几个默认的init应用程序 */
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;
panic("No working init found. Try passing init= option to kernel. "
"See Linux Documentation/admin-guide/init.rst for guidance.");
}
从该代码的可以看出,用户 init 程序调用的优先级:用户命令行指定 > 内核配置文件设定 > 默认执行程序。
- kernel_init 执行 run_init_process 函数时,入参代表的是一个由用户空间编译链接的应用程序,如果该程序执行成功,内核 init 线程会将自身转换为用户 init 进程,并且用户 init 进程会继承内核 init 线程的所有内容,包括PID(=1)。
- 内核 init 线程和用户 init 进程(linuxrc程序)是有区别的。init 线程一开始就有,它运行于内核态,属于一个内核线程。后来 init 线程挂载根文件系统,并运行应用程序 init 程序后,init 进程才从内核态转变为用户态。
4.1 init进程内核态切换至用户态
从上图可以看出 init 进程先是在内核空间运行,然后切换到用户空间运行。
(1)一个进程先后两种状态
init进程刚开始运行的时候是内核态,它属于一个内核线程,然后运行一个用户态下面的程序后,把自己强行转成用户态(后面的进程需要工作在用户态下)。
init进程完成了从内核态到用户态的过渡,因此后续的其他进程都可以工作在用户态。
(2)init进程在内核态下的工作内容
主要是挂载根文件系统,并试图找到用户态下的 init 程序。(这句话看出,init进程是早于init程序运行的)
内核源代码中的所有函数都处于内核态,执行其中任何一个都不能脱离内核态。应用程序必须不属于内核源代码,这样才能保证应用程序处于用户态。这里执行的init程序和内核不在一起,是由根文件系统另外提供的。
(3)init进程在用户态下的工作内容
由 init 进程直接或间接派生其他所有的用户进程。
4.2 挂载根文件系统
init进程要把自己从内核态转成用户态,就必须运行一个用户态的应用程序。而用户态的应用程序都在文件系统中,所以就必须得挂载文件系统,也就必须得挂载根文件系统。
代码调用流程:kernel_init -> kernel_init_freeable -> prepare_namespace
prepare_namespace 函数会完成挂载根文件系统的操作。
(1)根文件系统镜像位置
U-Boot 传参中的 root=/dev/mmcblk0p2 rw ,这一句就是告诉内核根文件系统镜像在哪里,以及根文件系统的权限。
(2)根文件系统类型
U-Boot 传参中的 rootfstype=ext4,这一句就是告诉内核 rootfs 的类型。
(3)挂载结果
如果内核挂载根文件系统成功,则会打印出:VFS: Mounted root (ext4 filesystem) readonly on device 179:6.(最后的这个数字表示根文件系统所挂在设备的主设备号:次设备号)
如果挂载根文件系统失败,则会打印:No filesystem could mount root, tried: yaffs2
注意:如果内核启动时挂载 rootfs 失败,则后面无法执行。内核中设置了启动失败休息5s自动重启的机制,因此这里会自动重启,所以有时候会看到反复重启的情况。
(4)根文件系统挂载失败的原因
- uboot的bootargs设置不对。(最常见错误)
- rootfs烧录失败。(fastboot烧录不容易出错)
- rootfs镜像制作的有问题。
三、FileSystem
内核线程 init 挂载根文件系统成功后,会执行内核命令行中指定的 init 路径的程序,该程序由根文件系统提供,一般指向busybox(若未指定,会去默认路径寻找)。
在内核的引导过程完成后,init程序会成为第一个获得控制权的进程,同时也是用户空间的第一个进程(PID=1),执行一组由开发人员定义的初始化例程。
1、init进程
在一个运行中的Linux系统中,每个进程都会和另外某个进程之间存在父子关系。init 是Linux系统中所有用户空间进程的最终父进程。此外,init 提供了一组默认的环境参数(比如初始的系统路径PATH),而所有其他进程都会继承这组参数。
init的主要功能是根据一个特定的配置文件生成其他进程,这个配置文件通常是指 /etc/inittab。init 有运行级别(runlevel)的概念,可以将运行级别看做系统状态。每个运行级别是由进入这个级别时所运行的服务和生成的程序决定的。
任意时刻,init只能处于一种运行级别之中。init 使用的运行级别为0~6。每个运行级别一般都有一组相关的启动和关闭脚本,它们定义了系统处于这个运行级别时的动作和行为。配置文件/etc/inittab决定了系统处于某个运行级别时所执行的动作。
1.1、运行级别(runlevel)
简单的说,运行级别就是操作系统当前正在运行的功能级别。这个级别从 0 到 6,具有不同的功能。不同的运行级定义如下:
运行级别 | 作用 |
0 | 系统关机 |
1 | 单用户系统配置,用于维护 |
2 | 不完全的命令行模式,不含NFS服务 |
3 | 完全的多用户配置(标准的运行级) |
4 | 系统保留(未使用) |
5 | 多用户配置,启动后进入图形界面 |
6 | 系统重启 |
0:数字0表示停机,当运行级别切换至0时,系统会立即关闭正在运行的服务,并关闭系统电源。
1:数字1表示单用户模式,单用户模式类似于Windows系统中的安全模式。当系统的运行级别切换至1时,系统只允许root用户登录,单用户模式一般用于对系统进行维护。
2:多用户模式,当系统处于运行级别2时,用户不能使用NFS(网络文件系统)。在运行级别2之下系统将会拒绝向网络中的其他计算机提供服务,此模式一般用于维护系统。
3:完全多用户模式:完全多用户模式是Linux系统在命令行模式中正常工作的运行级别,目前许多服务器都使用这一运行级别。
4:未分配使用。此级别主要由开发人员定制其功能,目前主要用于单片机或其他系统(例如手机操作系统)的开发和应用。
5:图形模式。这一运行级别和运行级别3基本相同,不同的是该模式下用户将使用图形界面登录并使用Linux系统。
6:重新启动。在这一运行级别下系统会立即重新启动。
技巧:如果忘记root用户密码,可以在系统启动时,将系统的运行级别切换到单用户模式,然后再重新设置root用户密码。
1.2、运行级别命令
命令: runlevel
作用:查看运行级别命令命令: init 运行级别
作用:改变运行级别命令小提示:
(1) 查询当前的级别runlevel 结果是 N 3,N代表的是进入三级别之前,在哪个级别,Null空的意思,代表开机直接进入了3级别。结果是 5 3 代表从5级别进入了3级别,相当于从图形界面进入了字符界面。
(2) 除了shutdown可以关机外,init 0 也可以关机,但是这个命令在关机的时候,不会保存正在运行的服务,所以不一定安全。 如果装了图形界面,init 5 会进入图形界面。init 6 就是重启。
1.3、系统默认运行级别
系统默认运行级别在 /etc/inittab 中指定,通过修改配置文件中默认运行级别对应的条目信息,来定义开机之后进入哪个级别。
注:千万不要把initdefault设置为0(关机)或者6(重启),否则系统将反复地关机或重启。
2、inittab
如果存在 /etc/inittable 文件,Busybox init 程序解析它,然后按照他的指示创建各种子进程,否则使用默认的配置创建子进程。
当 init 启动时,它会读取系统配置文件 /etc/inittab。inittab 是一个不可执行的文本文件,它由若干行指令组成,告诉 init 要进入什么运行级别,以及在哪里可以找到该运行级别的配置文件。
2.1、inittab指令格式
inittab文件中用每个条目来定义一个子进程,并确定它的启动方法,格式如下:
<id> : <runlevels> : <action> : <process>
- id:表示这个子进程要使用的控制台(即标准输入、标准输出、标准错误设备);如果省略,则使用与 init 进程一样的控制台。
- runlevels:表示这一行适用于运行哪些级别(可以有多个,表示在相应的运行级均需要运行)。另外嵌入式Linux中会省略该值。sysinit、boot、bootwait这三个运行方式也会忽略这个设置值。(对于busybox init程序,这个字段没有意思,可以省略)。
- action:表示进入对应的runlevels时,init应该运行process字段的命令的方式。具体取值见下表。
- process:要执行的程序,它可以是可执行程序,也可以是脚本。如果process字段前有“-”字符,这个程序被称为“交互的”。
action取值 | 执行条件 | 说明 |
sysinit | 系统启动后最先执行 | 指定初始化脚本路径,只执行一次,init进程等待它结束才继续执行其它动作 |
wait | 系统执行完sysinit进程后 | 只执行一次,init进程等待它结束才继续执行其它动作 |
once | 系统执行完wait进程后 | 只执行一次,init进程不等待它结束 |
respawn | 启动完once进程后 | init进程监测发现子进程退出时,重新启动它,永不结束。如Shell 命令解释器 |
askfirst | 启动完respawn进程后 | 与respawn类似,不过init进程先输出“Please press Enter to activate this console”,等用户输入回车后才启动子进程 |
shutdown | 当系统关机时 | 即重启、关闭系统时执行的程序 |
restart | 当系统重启时 | init进程重启时执行的程序,通常是init程序本身先重新读取、 解析/etc/inittab文件,再执行restart程序 |
ctrlaltdel | 按下Ctrl+Alt+Del键时 | 按Ctrl+Alt+Del组合键时执行的程序 |
boot | 无 | 随系统启动运行,runlevel值对其无效 |
bootwait | 无 | 随系统启动运行,并且init应该等待其结束 |
off | 无 | 没有任何意义 |
initdefault | 无 | 系统启动后的默认运行级别。由于进入相应的运行级别会激活对应级别的进程,所以对其指定process字段没有任何意义。如果inittab文件内不存在这一条记录,系统启动时将在控制台上询问进入的运行级。 |
2.2、inittab指令例程
<1> ::sysinit:/etc/init.d/rcS
1.该条目的id省略,表示使用与init进程一样的控制台。
2.该条目的runlevels在嵌入式Linux中都会省略。
3.该条目的action是sysinit,表示系统启动后最先执行。
4.该条目的process是/etc/init.d/rcS,表示系统启动后最先执行脚本/etc/init.d/rcS。
<2> ::respawn:-/bin/login
1.该条目的id省略,表示使用与init进程一样的控制台。
2.该条目的runlevels在嵌入式Linux中都会省略。
3.该条目的action是respawn,表示init进程监测发现子进程退出时,重新启动它,永不结束。
4.该条目的process是/bin/login,表示init进程监测发现/bin/login子进程退出时,重新启动它,永不结束。
2.3、inittab用例
# /etc/inittab
#
# Format for each entry: <id>:<runlevels>:<action>:<process>
#
# id == tty to run on, or empty for /dev/console
# runlevels == ignored
# action == one of sysinit, respawn, askfirst, wait, and once
# process == program to run
# now run any rc scripts
::sysinit:/etc/init.d/rcS
# Put a getty on the serial port
# ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
::respawn:-/bin/sh
# Stuff to do for the 3-finger salute
#::ctrlaltdel:/sbin/reboot
# Stuff to do before rebooting
::shutdown:/etc/init.d/rcK
::shutdown:/bin/umount -a -r
1) ::sysinit:/etc/init.d/rcS,目的是指定初始化要执行的脚本配置文件,用于配置系统。
2) ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100,监听串口的连接,如果有连接,则以 respawn 方式运行 getty 程序,在 ttyS0 终端上开启 shell 服务。(这里屏蔽该行,更换为 “ ::respawn:-/bin/sh ”,表示绕过登录验证过程,直接进入shell界面;登陆终端标准的命令是使用 getty 来登陆,但直接使用这种写法也是可以的,并且兼容性强一点,因为它不需要指定对应的串口设备)
3) ::shutdown:/etc/init.d/rcK,指定去初始化时要执行的脚本配置文件。
4) ::shutdown:/bin/umount -a -r ,在系统关机的时候执行 umount 命令卸载所有文件系统,并且在卸载失败时用只读模式重新安装以保护文件系统。
2.4、inittab执行流程
1)初始化系统 ::sysinit:/etc/init.d/rcS
sysinit 告诉 init 程序,运行 /etc/init.d/rcS 脚本来初始化系统,rcS 脚本中执行 /etc/init.d/目录下的所有的 rcX 启动脚本。而这些 rcX 启动脚本有着类似的用法,它们一般能接受 start、stop、restart、status等参数。
rcS 执行完毕后,返回 init。这时基本系统环境已经设置好了,各种守护进程也已经启动了。init 接下来会打开终端,以便用户登录系统。2)建立终端 ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100
监听串口的连接,如果有连接,则以respawn方式运行getty程序。它会显示一个文本登录界面,在这个登录界面中会提示输入用户名,而输入的用户名将作为参数传给 login 程序来验证用户的身份。
注意:如果想绕过登录验证过程,直接进入shell界面的话,可以把这一行改为:::respawn:-/bin/sh
建立终端具体流程:
a) getty进程接收到用户名后,启动login进程。b) login进程要求用户输入口令。
c) 用户输入口令。
d) login进程对 username 和 password 进行检查。
e) login启动shell进程。
f) shell进程根据 /etc/password 中的shell类型,启动相应的shell。并启动/etc/profile文件(此文件为系统的每个用户设置环境信息,当用户第一次登录时,该文件被执行。并从 /etc/profile.d 目录的配置文件中搜集 shell 的设置)和 $HOME/.bash_profile 文件。最后出现shell提示符,等待用户输入命令。
至此,启动过程结束。
3)关机 ::shutdown:/bin/umount -a -r
告诉init,在系统关机的时候执行 umount 命令卸载所有文件系统,并且在卸载失败时用只读模式重新安装以保护文件系统。
3、rcS
inittab文件中,一般会指定运行 /etc/init.d/rcS 脚本来初始化系统;初始化系统完成后,可以在该文件中添加自定义的命令。
注:并不是必须要运行 rcS 来执行初始化动作,也可以运行其它什么名字的文件,只要在inittab中显式指定运行就好。但一般约定俗成的,基本都是在inittab中运行的 rcS 文件,也建议按这样的规范来执行。
rcS例程
#!/bin/sh
/bin/mount -t proc proc /proc
/bin/mount -o remount,rw /
/bin/mkdir -p /dev/pts /dev/shm
/bin/mount -a #挂载/etc/fstab文件中的内容
/sbin/swapon -a #交换分区
/bin/ln -sf /proc/self/fd /dev/fd
/bin/ln -sf /proc/self/fd/0 /dev/stdin
/bin/ln -sf /proc/self/fd/1 /dev/stdout
/bin/ln -sf /proc/self/fd/2 /dev/stderr
/bin/hostname -F /etc/hostname
# Start all init scripts in /etc/init.d
# executing them in numerical order.
#
for i in /etc/init.d/S??* ;do
# Ignore dangling symlinks (if any).
[ ! -f "$i" ] && continue
case "$i" in
*.sh)
# Source shell script for speed.
(
trap - INT QUIT TSTP
set start
. $i
)
;;
*)
# No sh extension, so fork subprocess.
$i start
;;
esac
done
echo /sbin/mdev > /proc/sys/kernel/hotplug
mdev -s
/etc/adb_conf.sh start &
4、profile
inittab 在调用 /etc/init.d/rcS 完成工作后,会执行“ ::respawn:-/bin/sh ”命令来以免登录的方式进入终端系统。
注:命令中必须加上”-”,否则登录终端之后不会调用 /etc/profile 文件。而且会报错“ /bin/sh: can't access tty; job control turned off ”。
1)登录 shell 首先从 /etc/profile 文件和 .profile文件(若存在的话)读取命令并执行,来初始化系统所用到的变量,其中:
- /etc/profile 是全局 profile 文件,设置后会影响到所有用户。当用户第一次登录时,该文件被执行,并从 /etc/profile.d 目录的配置文件中搜集 shell 的设置。
- $HOME/.profile 是针对特定用户的,可以针对用户来配置自己的环境变量。
执行顺序:
- 先执行全局 /etc/profile
- 接着 bash 会检查使用者的 HOME 目录中,是否有 .bash_profile 或者 .bash_login 或者 .profile,若有,则会执行其中一个,执行顺序为: .bash_profile (最优先) > .bash_login(其次) > .profile
2)如果在进入 shell 时设置了 ENV 环境变量,或者在登录 shell 的 /etc/profile 文件中设置了该变量,则 shell 下一步会从该变量命名的文件中读取命令并执行。因此用户应该把每次登录时都要执行的命令放在 /etc/profile 文件中,而把每次运行 shell 都要执行的命令放在 ENV 变量指定的文件中。
设置ENV环境变量的方法,是把下列语句放在 /etc/profile 文件中: ENV=$HOME/.anyfilename; export ENV
3)最后根据 /etc/profile 中的设置,执行对应的启动程序。
profile例程
export PATH="/bin:/sbin:/usr/bin:/usr/sbin"
if [ "$PS1" ]; then
if [ "`id -u`" -eq 0 ]; then
export PS1='# '
else
export PS1='$ '
fi
fi
export PAGER='/bin/more'
export EDITOR='/bin/vi'
# Source configuration files from /etc/profile.d
for i in /etc/profile.d/*.sh ; do
if [ -r "$i" ]; then
. $i
fi
done
unset i
export XDG_RUNTIME_DIR=/dev/shm
ulimit -s 4096
/root/app_demo #启动app程序
5、挂载设备或其它文件系统
因为 Linux 所有的硬件设备必须挂载之后才能使用,所以在挂载根文件系统后,可能还要挂载其他设备或文件系统。
挂载的方式有两种:手动挂载和自动挂载。
5.1、手动挂载
手动挂载需要在进入命令行后,通过mount命令来逐条挂载,而且每次系统启动都需要再重新执行一遍。
5.2、自动挂载
自动挂载用到了/etc/fstab文件,fstab 是用来存放文件系统的静态信息的文件。
当执行命令 “ mount -a ” 时,系统会自动地从这个文件读取信息,并且会自动将此文件中指定的文件系统挂载到指定的目录(可在 inittab 文件中加入命令 “ ::sysinit:/bin/mount -a”,实现系统启动时自动挂载文件系统)。
fstab文件格式
/etc/fstab 文件包含了如下字段,通过空格或 Tab 分隔:
<file system> <mount point> <type> <options> <dump> <pass>
说明如下:
- <file system> -- 要挂载的设备或文件系统。如/dev/mtdblock1,对于文件系统,这个字段可以为任意值。
- <mount point> -- 挂载点。
- <type> -- 要挂载设备或分区的文件系统类型,支持多种不同的文件系统:ext2, ext3, ext4, reiserfs, xfs, jfs, smbfs, iso9660, vfat, ntfs, swap 及 auto。 设置成auto类型,mount 命令会猜测使用的文件系统类型,对 CDROM 和 DVD 等移动设备非常有用。
- <options> -- 挂载时使用的参数,注意有些 mount 参数是特定文件系统才有的。
一些比较常用的参数:
参数名 说明 默认值 auto / noauto
执行 “ mount -a ” 时是否自动挂接
auto:挂接;noauto:不挂接
auto user / nouser
user:允许普通用户挂接设备
nouser:只允许 root 用户挂接设备
nouser exec / noexec
exec:允许运行所挂接设备上的程序(有可执行权限)
noexec:不允许运行所挂接设备上的程序(无可执行权限)
exec ro / rw ro:以只读方式挂接文件系统
rw:以读写方式挂接文件系统
rw suid / nosuid suid:允许 suid 操作和设定 sgid 位。这一参数通常用于一些特殊任务,使一般用户运行程序时临时提升权限
nosuid:禁止 suid 操作和设定 sgid 位
suid sync / async
sync:修改文件时,它会同步写入设备中(I/O同步进行)
async:不会同步写入(I/O异步进行)
sync dev / nodev
dev:允许从该文件系统的 block 文件中提取数据
nodev:不允许从该文件系统的 block 文件中提取数据
nodev defaults rw、suid、dev、exec、auto、nouser、async等的组合(ext4的默认参数) - remount 重新挂载已挂载的文件系统,一般用于指定修改特殊权限 - noatime 不更新文件系统上 inode 访问记录,可以提升性能(参见 atime 参数) - nodiratime 不更新文件系统上的目录 inode 访问记录,可以提升性能(参见 atime 参数) - relatime 实时更新 inode access 记录。只有在记录中的访问时间早于当前访问才会被更新。可以提升性能(参见 atime 参数)。 - flush - vfat 的选项,更频繁的刷新数据,复制对话框或进度条在全部数据都写入后才消失。 -
- <dump> -- dump 工具通过它决定何时作备份.。dump 会检查其内容,并用数字来决定是否对这个文件系统进行备份。允许的数字是 0 和 1 。0 表示忽略,1 则进行备份。大部分的用户是没有安装 dump 的 ,对他们而言 <dump> 应设为 0。
- <pass> -- 决定fsck程序行为,其与磁盘检查有关。fsck 读取 <pass> 的数值来决定需要检查的文件系统的检查顺序。允许的数字是0,1,和2。 根目录应当获得最高的优先权 1,其它所有需要被检查的设备设置为 2。 0 表示设备不会被 fsck 所检查。
文件系统标识
在 /etc/fstab
配置文件中,可以用三种不同的方法表示文件系统:内核名称、UUID 或者 label。使用 UUID 或是 label 的好处在于它们与磁盘顺序无关。如果你在 BIOS 中改变了你的存储设备顺序,或是重新拔插了存储设备,或是因为一些 BIOS 可能会随机地改变存储设备的顺序,那么用 UUID 或是 label 来表示将更有效。参见 持久化块设备名称 。
要显示分区的基本信息请运行:
$ lsblk -f
NAME FSTYPE LABEL UUID MOUNTPOINT
sda
├─sda1 ext4 Arch_Linux 978e3e81-8048-4ae1-8a06-aa727458e8ff /
├─sda2 ntfs Windows 6C1093E61093B594
└─sda3 ext4 Storage f838b24e-3a66-4d02-86f4-a2e73e454336 /media/Storage
sdb
├─sdb1 ntfs Games 9E68F00568EFD9D3
└─sdb2 ext4 Backup 14d50a6c-e083-42f2-b9c4-bc8bae38d274 /media/Backup
sdc
└─sdc1 vfat Camera 47FA-4071 /media/Camera
内核名称
可以使用 fdisk -l
来获得内核名称,前缀是 dev。
标签
注意: 使用这一方法,每一个标签必须是唯一的。
要显示所有设备的标签,可以使用 lsblk -f
命令。在 /etc/fstab
中使用 LABEL=
作为设备名的开头 :
/etc/fstab# <file system> <dir> <type> <options> <dump> <pass> tmpfs /tmp tmpfs nodev,nosuid 0 0 LABEL=Arch_Linux / ext4 defaults,noatime 0 1 LABEL=Arch_Swap none swap defaults 0 0
UUID
所有分区和设备都有唯一的 UUID。它们由文件系统生成工具 (mkfs.*
) 在创建文件系统时生成。
lsblk -f
命令将显示所有设备的 UUID 值。/etc/fstab
中使用 UUID=
前缀:
/etc/fstab
# <file system> <dir> <type> <options> <dump> <pass>
tmpfs /tmp tmpfs nodev,nosuid 0 0
UUID=24f28fc6-717e-4bcd-a5f7-32b959024e26 / ext4 defaults,noatime 0 1
UUID=03ec5dd3-45c0-4f95-a363-61ff321a09ff /home ext4 defaults,noatime 0 2
UUID=4209c845-f495-4c43-8a03-5363dd433153 none swap defaults 0 0
提示和技巧
自动挂载
如果 /home
分区较大,可以让不依赖 /home
分区的服务先启动。把下面的参数添加到 /etc/fstab
文件中 /home
项目的参数部分即可:
noauto,x-systemd.automount
这样 /home
分区只有需要访问时才会被挂载。内核会缓存所有的文件操作,直到 /home
分区准备完成。
注意: 这样做会使 /home
的文件系统类型被识别为 autofs
,造成 mlocate 查询时忽略该目录。实际加速效果因配置而异,所以请自己权衡是否需要。
路径名有空格
如果挂载的路径中有空格,可以使用 "\040" 转义字符来表示空格(以三位八进制数来进行表示)
/etc/fstab
UUID=47FA-4071 /home/username/Camera\040Pictures vfat defaults,noatime 0 2
/dev/sda7 /media/100\040GB\040(Storage) ext4 defaults,noatime,user 0 0
tmpfs
tmpfs 是一个临时文件系统,驻留于你的交换分区或是内存中(取决于你的使用情况)。使用它可以提高文件访问速度,并能保证重启时会自动清除这些文件。
经常使用 tmpfs 的目录有 /tmp、/var/lock、/var/run.。不要将之使用于 /var/tmp,因为这一目录中的临时文件在重启过程中需要被保留。默认 /etc/fstab 中的 /tmp 也是 tmpfs。
默认情况下, tmpfs 分区被设置为总的内存的一半,当然可以自由设定这一值。注意实际中内存和交换分区的使用情况取决于你的使用情况,而 tmpfs 分区在其真正使用前是不会占用存储空间的。
要将
/tmp
放到 tmpfs,将下行加入/etc/fstab
:/etc/fstab ..... tmpfs /tmp tmpfs nodev,nosuid 0 0 .....
可以指定大小,但不要修改
mode
选项,以保证文件具有正确的访问权限(1777)。在上例中/tmp
将最多使用一半内存,要指定最大空间,使用size
挂载选项:/etc/fstab ..... tmpfs /tmp tmpfs nodev,nosuid,size=2G 0 0 .....
一般需要大量读写操作的程序在使用 tmpfs 时都会提升性能。
注: tmpfs 目录(
/tmp
) 挂载时需要去掉noexec
参数,否则有些编译程序无法执行,此外,tmpfs 的默认大小是内存的一般,可能会产生空间不够的问题。
外部设备(热插拔设备)
外部设备在插入时挂载,在未插入时忽略。这需要 nofail
选项,可以在启动时若设备不存在直接忽略它而不报错。
/etc/fstab
/dev/sdg1 /media/backup jfs defaults,nofail 0 2
atime 参数
使用
noatime
,nodiratime
或relatime
可以提升 ext2, ext3 及 ext4 格式磁盘的性能。 Linux 在默认情况下使用atime
选项,每次在磁盘上读取(或写入)数据时都会产生一个记录。这是为服务器设计的,在桌面使用中意义不大。默认的atime
选项最大的问题在于即使从页面缓存读取文件(从内存而不是磁盘读取),也会产生磁盘写操作!使用
noatime
选项阻止了读文件时的写操作。大部分应用程序都能很好工作。只有少数程序如 Mutt 需要这些信息。Mutt 的用户应该使用relatime
选项。使用relatime
选项后,只有文件被修改时才会产生文件访问时间写操作。nodiratime
选项仅对目录禁用了文件访问时间。relatime
是比较好的折衷,Mutt 等程序还能工作,但是仍然能够通过减少访问时间更新提升系统性能。注意:
noatime
已经包含了nodiratime
。不需要同时指定。