linux 系统启动过程分析

时间:2021-10-09 04:31:57

Linux 3.10 版本,平台ARM7,uboot

Linux系统的一般启动过程通常划分为内核引导、内核启动和应用程序启动3个阶段,如下图所示(摘自:ARM 嵌入式LINUX系统开发8.2节)

第一阶段是目标板硬件初始化,解压内核映像,再跳转到内核映像入口。这部分的工作

一般由目标板的引导程序(boot)和内核映像的自引导程序(位于linux内核源代码中,也称为linux bootloader)完成。不同体系结构的目标板引导的方式和程序都有差异。

第二阶段是内核的初始化,初始化设备驱动,挂接根文件系统。这里是 Linux 内核通用的启动函数入口(start_kernel)。所有体系结构的目标板都顺序调用统一的函数,尽管有些函数的代码实现是跟体系结构相关的。

第三阶段是执行用户空间的 init程序,完成系统初始化、启动相关服务和管理用户登录

等工作。这个阶段可以提供给用户交互界面,例如:Shell命令行或者图形化的窗口界面。也可以自动执行应用程序。

在 Linux 系统启动过程中,有两个关键点:

一个是内核映像的解压启动;

另一个是根文件系统的挂接(因为文件和应用程序都要存储在文件系统中,所以 Linux 离不开文件系统。在内核启动到最后,必须挂接一个根文件系统。从文件系统的目录下找到 init程序,启动 init进程。在交叉开发环境中,通常采用 NFS 文件系统。在内核启动过程可以挂接 NFS 根文件系统。)

下面来分别介绍。

一内核引导

包括:bootloader和内核自引导程序两个部分

1.1 Bootloader

从开机上电到操作系统启动的一个引导程序,通过这段小程序,可以初始化硬件设备建立内存空间的映射表,从而建立适当的系统软硬件环境,为最终调用操作系统内核做好准备。

Bootloader的启动

设备一上电就会执行 Bootloader 来初始化系统。系统加电或复位后,所有CPU都会从某个地址开始执行,这是由处理器设计决定的。比如,X86 的复位向量在高地址端,ARM处理器在复位时从地址 0x00000000取第一条指令。嵌入式系统的开发板都要把板上 ROM 或 Flash 映射到这个地址。因此,必须把 Bootloader程序存储在相应的Flash位置。系统加电后,CPU将首先执行它。

网络启动方式:

这种方式开发板不需要配置较大的存储介质,跟无盘工作站有点类似。但是使用这种启动方式之前,需要把Bootloader 安装到板上的 EPROM或者 Flash中。Bootloader 通过以太网接口远程下载 Linux 内核映像或者文件系统。

磁盘启动方式:

FLASH启动方式:

大多数嵌入式系统上都使用Flash存储介质。Flash有很多类型,包括 NOR Flash、NAND Flash和其他半导体盘。其中,NOR Flash(也就是线性 Flash)使用最为普遍。

NOR Flash可以支持随机访问,所以代码是可以直接在Flash上执行的。Bootloader 一般是存储在 Flash芯片上的,Linux内核映像和 RAMDISK 也可以存储在 Flash上。下图是 Bootloader和内核映像以及文件系统的分区表。PS:但在启动时,因为flash速度较慢,有的会先将bootloader程序copy至SDRAM区域,然后在执行bootloader。

Bootloader 一般放在 Flash 的底端或者顶端,这要根据处理器的复位向量设置。要使Bootloader 的入口位于处理器上电执行第一条指令的位置。Bootloader 引导 Linux 内核,就是要从这个地方把内核映像解压到RAM中去,然后跳转到内核映像入口执行。然后是文件系统区。如果使用Ramdisk文件系统,则需要Bootloader把它解压到RAM中。如果使用JFFS2文件系统,将直接挂接为根文件系统。

Uboot:

U-Boot 的 3 种映像格式都可以烧写到Flash中,但需要看加载器能否识别这些格式。一

u-boot.bin最为常用,直接按照二进制格式下载,并且按照绝对地址烧写到 Flash中就可以了。U-Boot和u-boot.srec格式映像都自带定位信息。

Uboot启动

从连接脚本u-boot.lds 可以获知目标程序的连接顺序,第一个连接的是start.o

入口程序:\u-boot\--\arch\arm\cpu\armv7\start.s(start.s是boot组成文件中唯一的汇编程序,其他的都是C程序)。Start.s中执行的第一条指令就是reset,其对应的是复位向量代码

UBOOT启动的其他阶段就不做分析了,其整体流程如图(摘自ARM 嵌入式LINUX 开发 P124)

1.2 内核自引导程序

下面开始分析,系统启动的内核的自引导程序(代码位于kernel下):

入口代码:

kernel\--\3.10\arch\arm\boot\compressed\head.s

decompress_kernel和enter_kernel是其中两个重要的步骤(参见最上面的流程图)

decompress_kernel:负责调用解压影像的C函数decompress_kernel

decompress_kernel定义在kernel\--\3.10\arch\arm\boot\compressed\Misc.c中

enter_kernel(也可称为call_kernel):负责跳转到镜像的入口

镜像入口对应的程序为: \kernel\--\3.10\arch\arm\kernel\head.s

mmap_swithced 定义位于\kernel\--\3.10\arch\arm\kernel\head-common.s中, head.s中包含了head-common.s。

在head-common.s中找到mmap_swithced的定义:

可见mmap_swithced调用了start_kernel 函数,由此进入下一个阶段:linux系统初始化:

二内核启动

start_kernel 是通用的linux内核初始化函数,通过此函数可以分析出内核初始化的基本过程。

start_kernel定义于\kernel\--\3.10\init\main.c中。start_kernel()函数负责初始化内核各子系统,最后调用 rest_init(),启动一个叫作init的内核线程,继续初始化。

rest_init中分别启动init 和kthreadd 这两个内核线程

在 init 内核线程中实现的功能很多(详细可参考书ARM 嵌入式LINUX开发),最重要的是负责完成挂接根文件系统、初始化设备驱动和启动用户空间的init进程等重要工作。

 

挂载根文件系统

1.      populate_rootfs 安装初始化虚拟文件系统

(在这之前,roofs文件系统已经挂载(start_kernel->vfs_caches_init->mnt_init中再分别调用init_rootfs和init_mount_tree)

populate_rootfs 调用关系:

start_kernel->kernel_init->kernel_init_freeable->do_basic_setup-> do_initcalls->rootfs_initcall->populate_rootfs->

ps:do_basic_setup()是一个很关键的函数,所有直接编译在kernel中的模块都是由它启动的。Do_initcalls()用来启动所有在__initcall_start__initcall_end段的函数,而静态编译进内核的modules也会将其入口放置在这段区间里。跟根文件系统相关的初始化函数都会由rootfs_initcall()所引用(rootfs_initcall(populate_rootfs))。

 

2. 根文件系统的挂载

populate_rootfs的代码可知,对于Image-Initrd或者VFS(即InitRamfs或者CPIO-Initrd)中不存在文件ramdisk_execute_command的情况,则执行prepare_namespace()。prepare_namespace中调用mount_root函数完成真实文件系统的挂载。

Kernel_init 中判断如果存在/init,则运行之(ramdisk_execute_command在稍前调用的kernel_init_freeable做了赋值)

 

Roofs文件系统挂载流程可参考博文:http://blog.csdn.net/guopeixin/article/details/5962482

 

初始化设备驱动

start_kernel->kernel_init->kernel_init_freeable->do_basic_setup-> do_initcalls->

Linux内核映像把设备驱动程序的初始化函数指针链接成数组, 即__initcall_start和__initcall_end之间的数据。 do_initcalls()函数就是通过调用数组中的函数指针,完成驱动程序的初始化。 

 

启动用户空间init进程

Linux 系统在挂接根文件系统之后,要执行文件系统的中的应用程序。有些应用程序可能要完成嵌入式系统的。init进程是通过执行根文件系统中 init程序启动的。

内核挂接根文件系统成功以后,将通过run_init_process()函数执行应用程序。这是一个尝试的过程,如果execute_command存在,则执行execute_command;如果不存在,则顺序执行/sbin/init、/etc/init、/bin/init、/bin/sh,直到有一个执行成功为止。如果都不存在,则会调用panic()上报错误。