购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

4.3 Linux的启动原理

一个Linux系统永远由Bootloader、Kernel、文件系统三个元素组成。有的嵌入式Linux系统经过适当调整可以不需要Bootloader,但如果最新的内核也这么做,那么代价还是很大的,所以以后的系统一般都会有Bootloader。内核的文件系统至少要有一个,一般会有两个。一个是initrd,另外一个是实际运行的根文件系统,当然还可以挂载无数的文件系统。

需要注意的是,由于EFI的出现,其功能越来越强大,导致其可能会慢慢实现Bootloader的功能,直到完全实现。因此Bootloader不会消失,但是可能会与EFI合并。

4.3.1 Linux的最小系统制作和启动

思考Linux的启动,最好的方法就是自己制作一个最小Linux系统。其实方法很简单,只是在制作Linux系统时可能会遇到很多问题,下面介绍一下制作Linux系统的方法可以让整个过程一目了然。代码如下。

创建了一个最小文件系统,但是还没有安装Grub,以gz的格式保存时可以发现,这其实是一个initrd类文件系统。下面简单介绍一下如何安装Grub,代码如下。上述代码有很多种实现的方法,也可以使用Loop设备,这个流程可以快速演示一个可启动的文件系统的制作方法。

这里重要的不是制作过程,而是启动过程。我们看到安装了grub之后,该磁盘就能确保启动了,因为grub软件的启动是不需要操作系统的,而是单独地引导程序。只要磁盘可用,它就能工作。grub启动后会弹出grub的命令行界面。grub1和grub2两个不同版本的命令已经有了很大的变化,我们这里说的是grub 2的命令。使用Linux命令指定内核所在的目录(如果你发现没有root,还需要先set root命令,但grub一般能自动发现),然后使用initrd命令指定initrd文件系统,输入boot后,系统就会启动了。内核发行版越新,想要去掉initrd越难。也就是说在Linux“工业界”越来越倾向于认可initrd文件系统作为系统启动过程中不可或缺的一部分。

4.3.2 initrd文件系统

直接使用initrd文件系统其实是不准确的,因为启动过程中的过渡根文件系统可以是initrd,也可以是initramfs。这两个根文件系统的内容是一样的,只是组织方式不一样。

首先建立块(可以使用dd命令),然后对块文件进行格式化(例如可以格式化为ext2文件系统),将所有需要的文件和根文件的目录都建立好,复制了必须的程序进去,这一点两者是一样的。接下来生成initramfs的做法是用cpio命令打包,然后进行zip压缩。而生成initrd的方法则是不使用cpio命令,直接进行zip压缩。

需要注意的是,不要被各个发行版的命名给弄混了。现在的发行版都是initramfs,之前的发行版是initrd。但是由于历史原因,有的发行版现在还是会命名为initrd。两者的不同是修改的方式不同,initrd文件的类型,如图4-2所示。

图4-2

代码如下。

修改一个无格式的img系统文件使用的方法是mount,该文件会以loop设备的形式存在。而修改一个cpio文件的方式是用cpio命令归档成目录,不需要mount。直接修改目录再cpio归档后就又是可以用的文件系统了。后者之所以能取代前者,是因为mount是一个root程序,是系统级的操作,而cpio只是一个文件的操作,既方便又安全。

Linux下的lsinitramfs(lsinitrd)命令可以不用mount或解压就可以查看initrd里的内容。lsinitramfs文件系统内容,如图4-3所示。

图4-3

initrd是一个过渡文件系统,里面就有程序和数据。而所有的程序和数据都是为了它存在的目的服务的,那么它存在的目的是什么呢?我们可以看一下内核启动了initrd里面的什么程序来得出结论。

内核启动initrd里的第一个程序是/init,这是内核“写死的”,想要改变就修改内核代码。这里介绍一种典型的情况,这个init程序是一个脚本文件,任务流程如下(这只是一个案例,各个版本之间可能会有比较大的变化)。

(1)建立一个sysroot根目录。该目录用于挂载之后要启动的真实文件系统。

(2)设置命令的环境变量,这样后面的命令就都可以在环境变量指定的目录中搜索到了。

(3)mount proc文件系统到/proc,这是Linux内核动态状态的一个表现和设置入口。

(4)mount sysfs到/sys,这是Linux内核资源的有组织的表现和设置入口。

(5)mount devtmpfs到/dev,这是临时表征当前设备节点的文件系统,可以加速系统的启动(/dev目录对当前用户程序的运行至关重要)。

(6)准备/dev目录中的一些节点(例如stdin、stdout、stderr等fd,这里的fd是指文件描述符。对内核中已有的静态节点进行创建)。

(7)提供为内核输入额外参数的机会。

(8)解析内核启动参数并执行(例如在udev开始之前执行一些准备),由此可以看出内核参数并不一定都是给内核来解析的。

(9)启动systemd-udevd,并配置其要响应的行为。

(10)循环处理$hookdir/initqueue下的任务。

(11)mount根文件系统。

(12)找到根文件系统的init程序。

(13)停止systemd-udevd服务程序。

(14)做一些清理操作。

(15)切换到根文件系统的init程序,启动完成。

可以看出整个initrd文件系统存在的目的就是挂载根文件系统。所以当根文件系统可以直接挂载的时候就不需要了。但是现在越来越复杂的网络和设备让这个initrd的存在变得非常有必要。可以看出,嵌入式系统是万万不需要initrd文件系统的。例如SCSI设备内核就很难识别,但是现在很多存储设备已经是SCSI的了。因此,也可以将SCSI编译进内核,让内核可以直接识别。

4.3.3 EFI启动桩

由于EFI的强大功能,现在的Linux越来越倾向于不使用Bootloader,EFI启动桩就是一个大胆的尝试,其在压缩后的Linux内核的前面加上了一段可执行程序,完成Bootloader的工作。但是目前该功能还比较弱,对Linux的磁盘的识别和设备驱动的加载也有很大问题。因此,这代表一个发展方向,但还不至于快速淘汰Bootloader。

4.3.4 启动管理程序

我们知道系统第一个启动的进程是init进程,但init进程不仅位于用户空间,而且位于内核代码的进程。确切地说,在系统启动过程中内核中的init进程被用户空间的init进程替换执行。在内核中init执行完操作后,一般会调用初始化系统来初始化整个系统的应用程序或者服务器。这个初始化系统最原始的是linuxrc脚本,当然,包括这个脚本之后的所有脚本都是Linux操作系统的用法,并不是内核的规定。也就是说你可以自由地指定任何脚本的执行顺序和定义不同脚本的意义,而不用修改内核代码。

常用的Linux启动脚本一般有/linuxrc、/etc/rcS、/etc/rc.local、/etc/profile。这些脚本都是可有可无的,如果有需要就一个一个地在脚本中先后调用。必须要注意的是,这些脚本连同名字和路径都是可以随意定制的,不同的发行版可能会选择不同的位置和顺序,但是这几个名字的通用性是Linux操作系统长期演化的结果。

只是脚本无法满足健壮的系统对于启动和服务的管理需求,因为不能要求所有对服务的管理都通过写脚本完成。为此Linux的一般做法是生成一个专门的目录,想要启动的服务就生成一个规定格式的文件放到目录中,Linux发行版一般都有写脚本去遍历执行整个目录的服务,早期常见的这种系统是rcN.d目录,N一般取值0~6,例如执行init 6命令,就会执行rc6.d目录中的脚本以关闭和打开对应的服务。对于这些被定义的服务,甚至通过复杂的语法限制还可以实现精细的启动顺序控制(systemd)。

在嵌入式系统中,一般不使用systemd启动管理系统,而是直接采用rcS等启动脚本。一个内核init程序的示例代码如下。

可以按照上述流程追踪内核的启动代码。我们从底层boot之后的内核正式启动入口。从start_kernel开始,我们能够发现其最终调用到了kernel_init,在这个函数里调用了内核“写死的”约定启动程序/sbin/init、/etc/init、/bin/init、/binsh。在这之前还会尝试首先调用initrd里面的init。

1.Sys V init:runlevel

系统中有很多服务,人们在Linux出现的时候就在想如何管理这些服务了。问题是有些服务需要启动,有些不需要启动。登录图形界面需要某一些服务,但不登录图形界面就不需要这些服务,单用户登录可能需要的服务更少,如此就需要差别化地启动所需要的服务。这些服务的组织早期的方法是使用rcN.d目录,这一整套目录的约定就是Sys V init的runlevel规范。

2.针对网络服务

由于大部分Linux操作系统上运行的后台服务进程都是在监听某一个端口,Windows操作系统的做法是要使用什么就打开什么进程,在内存里一直监听睡眠等待。Linux操作系统认为既然都是监听网络服务,找一个超级进程监听全部的端口,哪个端口有数据来再启动哪个程序,这样节省内存的思想就使得xinetd守护进程诞生了。

xinetd管理监听所有的端口,当该端口有请求到达的时候启动对应的端口处理服务进程,导致的结果是初次响应变慢。而且启动了对应的服务之后没有请求,是不是要关闭该进程呢?否则只要启动了就永远启动。关闭进程后又打开,岂不是更耗费系统资源?由于大部分系统管理员清楚自己的系统要提供什么服务,所以xinetd用得越来越少。

3.开机启动所有需要启动的进程

Linux从UNIX演化而来,所以最初的启动管理程序是在UNIX的init基础上创新的。定义的启动服务程序是runlevel机制,系统默认会定义6种或更多的runlevel,每一种runlevel都会启动不同的程序。例如约定的1是只启动单用户无图形界面;3是多用户无图形界面;5是多用户图形界面;0是关机;6是重新启动。通常默认的启动runlevel值放在/etc/inittab文件中。init进程通过读取这个文件获得runlevel值,然后去对应的/etc/rc3.d等不同的目录下执行在该目录定义的属于这个运行级别的程序。启动之后可以通过/sbin/telinit程序改变运行级别。

4.upstart

为了克服init的同步顺序启动带来的效率低下的困难,upstart实现了异步事件驱动的启动模式,在某些情况下提高了系统的启动速度。由于upstart对init进程的提升和采用的异步机制,使得开机启动速度和组织有了更多的改善空间,有很多发行版采用了。但是由于systemd的迅速崛起,很多采用upstart的系统也迅速切换到systemd,upstart作为一个过渡版本的启动管理程序已基本退出历史舞台。

5.systemd

systemd起源于Tizen(由Intel和三星研发的Linux操作系统),经过完善和丰富形成了现在的程序集。起初systemd设计只是用来取代init和startup的,但在逐渐开发的过程中其功能越来越丰富,没有一个发行版不愿意接受如此强大的质量优秀的开源代码。传统的init开机启动进程是顺序的,并且由Shell脚本执行很多开机指令来完成系统的初始化。systemd将启动尽可能地并行化,并且将很多本应由Shell执行的逻辑移到systemd程序中来,提高执行速度。

目前systemd除了对启动进行管理,还对用户端封装了几乎所有的系统服务,只是很多系统服务还没有被广泛应用。例如原来的cron被systemd的调度执行部分取代,udev被其device hotplugging取代。为了向上提供ipc,systemd还封装提供了UNIX Domain Socket和D-Bus给其他服务程序。其用统一的原语实现了几乎整个Linux系统服务,并且提供对其他外置服务的支持(其本身就是作为服务管理程序存在的)。

systemd进程是系统的第一个启动进程,也是系统的最后一个结束进程,是所有用户端进程的根进程。其比传统的init在处理子进程上有很多改进,例如可以支持进程关闭后自动重启(用init的respawn语义也可以),不产生僵尸进程等。

为了启动的并行化,systemd定义了一整套脚本语义。所有要启动的进程服务都要使用其规定的语义完成unit文件。在init系统中,每个进程都是由各自的独立脚本完成启动的,在systemd的unit文件中,service、socket、device、mount、automount、swap、target、path、timer(替代cron)、snapshot、slice和scope这12种语义可以定义丰富的启动信息。还有一个target的概念,一个target就是一群Unit的集合,也就是一个启动组。

systemd是守护进程;systemctl用来定义systemd的服务和行为;systemd-analyze用来分析启动的效率。systemd目前完全使用内核的cgroup接口进行开发,所以cgroup也变成了现代Linux操作系统的标配。代码如下。

比较重要的systemd服务有以下6个。

· consoled:取代传统的虚拟终端。

· journald:取代传统的syslog、syslog-ng、rsyslog。

· logind:取代传统的用户登录服务(ConsoleKit、gnome-session)。

· networkd:取代传统的网络配置(如Network Manager)。

· timedated:所有与时间有关的操作都将在此集成。

· udevd:udev的代码被systemd完全吸收合并。

基于systemd的成功,很多人希望继续创新,试图更好地使用或取代systemd。比较好的成果有endev、uselessd、systemdbsd、console kit2,但是都没有真正地对systemd构成威胁。

4.3.5 Linux内核启动顺序

B ootloader将内核加载到内存后,需要将控制权交给内核。第一步是要解压内核,由于内核是自解压的,解压的入口相关文件是arch/arm/boot/compressed/head.S。完成解压后就是搬运,因为解压并不把内核放在其最终执行的位置,移动之前还要进行保存可能被其覆盖的代码的操作。

第二阶段从\arch\arm\kernel\head.S开始,是内核的实际功能地点。主要完成的工作有CPU ID检查、machine ID检查、创建初始化页表、设置C语言代码运行环境、跳转到内核第一个真正的C函数startkernel开始执行。

第三阶段从start_kernel开始完全是C语言了。其首先用大内核锁锁住内核,保证独占,然后调用平台相关的初始化操作(arch/arm/kernel/setup.c里的setup_arch()),在这里内核启动后可使用的所有内存被初始化。被初始化的还有页表结构、MMU、中断、内存区域、计时器、Slab、VFS等。所以,如果要预留不被内核使用的内存空间,应该在这里预留,然后跳到init内核进程(不是用户空间的init进程)。

第四阶段就是内核的init进程将自身替换为用户端的init进程进行执行。 fF4k8L1uHPLdK0dhQYUIHz3QN8f+rOJXqDAZc0L2xYp/sjGNjKZgWq41hQ8qvOBx

点击中间区域
呼出菜单
上一章
目录
下一章
×