Linux 内核启动及文件系统加载过程
当u-boot 开始执行 bootcmd 命令,就进入 Linux 内核启动阶段。普通 Linux 内核的启动过程也可以分为两个阶段。本文以项目中使用的 linux-2.6.37 版源码为例分三个阶段来描述内核启动全过程。第一阶段为内核自解压过程,第二阶段主要工作是设置ARM处理器工作模式、使能 MMU 、设置一级页表等,而第三阶段则主要为C代码,包括内核初始化的全部工作。
一、 Linux 内核自解压过程
在 linux 内核启动过程中一般能看到图1内核自解压界面,本小节本文重点讨论内核的自解压过程。
UncompressingLinux...done, booting the kernel.
这也是由 decompress_kernel函数输出的,执行完解压过程,再返回到head.S中的583行,启动内核
call_kernel: bl cache_clean_flush
bl cache_off
mov r0, # @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
其中 r4 中已经在head.S的第180行处预置为内核镜像的地址,如下代码:
#ifdef CONFIG_AUTO_ZRELADDR
@determine final kernel image address
mov r4, pc
and r4, r4, #0xf8000000
add r4, r4, #TEXT_OFFSET
#else
ldr r4, =zreladdr
#endif
这样就进入Linux内核的第一阶段,我们也称之为stage1 。
二、 Linux 内核启动第一阶段 stage1
承接上文,这里所以说的第一阶段 stage1 就是内核解压完成并出现 Uncompressing Linux...done,booting the kernel. 之后的阶段。该部分代码实现在arch/arm/kernel 的 head.S中,该文件中的汇编代码通过查找处理器内核类型和机器码类型调用相应的初始化函数,再建 立页表,最后跳转到start_kernel() 函数开始内核的初始化工作。检测处理器类型是在汇编子函数__lookup_processor_type 中完成的,通过以下代码可实现对它的调用: bl__lookup_processor_type (在文件head-commom.S 实现)。 __lookup_processor_type调用结束返回原程序时,会将返回结果保存到寄存器中。其中r5 寄存器返回一个用来描述处理器的结构体地址,并对r5进行判断,如果r5的值为0则说明不支持这种处理器,将进入 __error_p 。r8 保存了页表的标志位, r9 保存了处理器的 ID 号,r10保存了与处理器相关的struct proc_info_list结构地址。Head.S 核心代码如下:
ENTRY(stext)
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9 @设置SVC模式关中断
mrc p15, , r9, c0, c0 @ 获得处理器ID,存入r9寄存器
bl __lookup_processor_type @ 返回值r5=procinfo r9=cpuid
movs r10, r5
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @如果返回值r5=,则不支持当前处理器'
bl __lookup_machine_type @ 调用函数,返回值r5=machinfo
movs r8, r5 @ 如果返回值r5=,则不支持当前机器(开发板)
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_a @ 机器码不匹配,转__error_a并打印错误信息
bl __vet_atags
#ifdef CONFIG_SMP_ON_UP @ 如果是多核处理器进行相应设置
bl __fixup_smp
#endif
bl __create_page_tables @最后开始创建页表
检测机器码类型是在汇编子函数__lookup_machine_type (同样在文件head-common.S 实现) 中完成的。与 __lookup_processor_type类似,通过代码:“ bl __lookup_machine_type”来实现对它的调 用。该函数返回时,会将返回结构保存放在r5、r6 和 r7三个寄存器中。其中r5 寄存器返回一个用来描述机器(也就是开发板)的结构体地址,并对r5进行判断,如果r5的值为0 ,则说明不支持这种机器(开发板),将进入__error_a, 打印出内核不支持u-boot传入的机器码的错误如图2。 r6保存了 I/O基地址, r7保存了 I/O 的页表偏移地址。
当检测处理器类型和机器码类型结束后,将调用 __create_page_tables子函数来建立页表,它所要做的工作就是将 RAM 基地址开始的1M 空间的物理地址映射到 0xC0000000开始的虚拟地址处。对本项目的开发板DM3730 而言,RAM 挂接到物理地址 0x80000000 处,当调用 __create_page_tables 结束后 0x80000000 ~ 0x80100000 物理地址将映射到 0xC0000000~0xC0100000 虚拟地址处。当所有的初始化结束之后,使用如下代码来跳到 C 程序的入口函数start_kernel()处,开始之后的内核初始化工作:bSYMBOL_NAME(start_kernel) 。
三、Linux内核启动第二阶段 stage2
从start_kernel函数开始
Linux内核启动的第二阶段从start_kernel函数开始。start_kernel 是所有Linux 平台进入系统内核初始化后的入口函数,它主要完成剩余的与 硬件平台相关的初始化工作,在进行一系列与内核相关的初始化后,调用第一个用户进程-init进程并等待用户进程的执行,这样整个Linux内核便启动完毕。该函数位于 init/main.c文件中,主要工作流程如图 所示:
图3 start_kernel流程图
该函数所做的具体工作有 :
1) 调用 setup_arch() 函数进行与体系结构相关的第一个初始化工作;对不同的体系结构来说该函数有不同的定义。对于ARM平台而言,该函数定义在arch/arm/kernel/setup.c 。它首先通过检测出来的处理器类型进行处理器内核的初始化,然后 通过bootmem_init()函数根据系统定义的 meminfo结构进行内存结构的初始化,最后调用paging_init()开启MMU,创建内核页表,映射所有的物理内存和 IO空间。
2) 创建异常向量表和初始化中断处理函数;
3) 初始化系统核心进程调度器和时钟中断处理机制;
4) 初始化串口控制台(console_init);
ARM-Linux 在初始化过程中一般都会初始化一个串口做为内核的控制台,而串口Uart驱动却把串口设备名写死了,如本例中 linux2.6.37串口设备名为 ttyO0,而不是常用的ttyS0。有了控制台内核在启动过程中就可以通过串口输出信息以便开发者或用户了解系统的启动进程。
5) 创建和初始化系统 cache,为各种内存调用机制提供缓存,包括;动态内存分配,虚拟文件系统(VirtualFile System )及页缓存。
6) 初始化内存管理,检测内存大小及被内核占用的内存情况;
7) 初始化系统的进程间通信机制(IPC); 当以上所有的初始化工作结束后, start_kernel() 函数会调用 rest_init() 函数来进行最后的初始化,包括创建系统的第一个进程-init 进程来结束内核的启动。
挂载根文件系统并启动 init
Linux 内核启动的下一过程是启动第一个进程 init ,但必须以根文件系统为载体,所以在启动init 之前,还要挂载根文件系统。
四、挂载根文件系统
根文件系统至少包括以下目录:
/etc/ :存储重要的配置文件。
/bin/ :存储常用且开机时必须用到的执行文件。
/sbin/ :存储着开机过程中所需的系统执行文件。
/lib/ :存储/bin/及/sbin/的执行文件所需的链接库,以及Linux的内核模块。
/dev/ :存储设备文件。
注:五大目录必须存储在根文件系统上,缺一不可。
以只读的方式挂载根文件系统,之所以采用只读的方式挂载根文件系统是因为:此时Linux内核仍在启动阶段,还不是很稳定,如果采用可读可写的方式挂载根文件系统,万一Linux不小心宕机了,一来可能破坏根文件系统上的数据,再者Linux下次开机时得花上很长的时间来检查并修复根文件系统。
挂载根文件系统的而目的有两个:一是安装适当的内核模块,以便驱动某些硬件设备或启用某些功能;二是启动存储于文件系统中的init 服务,以便让 init服务接手后续的启动工作。
执行 init 服务
Linux内核启动后的最后一个动作,就是从根文件系统上找出并执行init服务。 Linux内核会依照下列的顺序寻找init服务:
1) /sbin/ 是否有 init 服务
2) /etc/ 是否有init 服务
3) /bin/ 是否有 init 服务
4)如果都找不到最后执行/bin/sh
找到 init服务后, Linux会让 init 服务负责后续初始化系统使用环境的工作, init启动后,就代表系统已经顺利地启动了linux内核。
启动init服务时,init服务会读取/etc/inittab文件,根据/etc/inittab中的设置数据进行初始化系统环境的工作。 /etc/inittab定义 init 服务在 linux启动过程中必须依序执行以下几个Script :
/etc/rc.d/rc.sysinit
/etc/rc.d/rc
/etc/rc.d/rc.local
/etc/rc.d/rc.sysinit主要的功能是设置系统的基本环境,当init服务执行rc.sysinit时 要依次完成下面一系列工作:
(1)启动udev
(2)设置内核参数
执行sysctl –p ,以便从 /etc/sysctl.conf 设置内核参数
(3)设置系统时间
将硬件时间设置为系统时间
(4)启用交换内存空间
执行 swpaon –a –e,以便根据/etc/fstab的设置启用所有的交换内存空间。
(5)检查并挂载所有文件系统
检查所有需要挂载的文件系统,以确保这些文件系统的完整性。检查完毕后以可读可写的方式挂载文件系统。
(6)初始化硬件设备
Linux除了在启动内核时以静态驱动程序驱动部分的硬件外,在执行rc.sysinit 时,也会试着驱动剩余的硬件设备。 r c.sysinit 驱动的硬件设备包含以下几项:
a)定义在/etc/modprobe.conf 的模块
b) ISA PnP的硬件设备
c) USB设备
(7)初始化串行端口设备
Init服务会管理所有的串行端口设备,比如调制解调器、不断电系统、串行端口控制台等。Init 服务则通过rc.sysinit来初始化linux 的串行端口设备。当rc.sysinit 发现 linux 才能在这 /etc/rc.serial 时,才会执行 /etc/rc.serial ,借以初始化所有的串行端口设备。因此,你可以在 /etc/rc.serial 中定义如何初始化 linux所有的串行端口设备。
(8)清除过期的锁定文件与IPC文件
(9)建立用户接口
在执行完3个主要的 RC Script 后, init服务的最后一个工作,就是建立linux的用户界面,好让用户可以使用 linux 。此时init 服务会执行以下两项工作:
(10)建立虚拟控制台
Init 会在若干个虚拟控制台中执行 /bin/login,以便用户可以从虚拟控制台登陆 linux 。 linux 默认在前6个虚拟控制台,也就是 tty1~tty6 ,执行 /bin/logi 登陆程序。当所有的初始化工作结束后,cpu_idle()函数会被调用来使系统处于闲置( idle)状态并等待用户程序的执行。至此,整个Linux内核启动完毕。整个过程见图4。
图4:linux内核启动及文件系统加载全过程