0%

Linux系统启动流程

内核加载

上篇博文里面我说明过计算机如何从远古到近代的启动流程,然后,走到操作系统一层的时候就没有深入阐述了,在这篇博文我将以Linux系统为例来叙述操作系统是如何启动的。

通常在系统/boot目录下放着内核文件,如下:

1
2
initramfs-linux.img
vmlinuz-linux

GRUB(或者LILO)加载内核的时候,会把vmlinuz-linux映射到内存并会把它的一个初始根目录的文件镜像(initramfs-linux.img)作为文件系统加载到内存,系统会从这个迷你的初始化文件系统来启动,在这个迷你的文件系统里面又会加载真正的磁盘文件系统,带有这种initramfs的都是这种加载流程,但是,对于那种没有initramfs的(例如:Slackware发行版本),它是加载内核后,内核直接从磁盘加载文件系统,这种区分跟内核配置有关。

内核初始化进行哪些操作?

内核初始操作属于操作系统内核部分的知识,一般来说,主体包括以下(还有很多细节可以参看Linux内核方面的书籍):

  • 系统处于实模式需要切换到保护模式,所以需要设置GDT,LDT,设置平坦模式的内存寻址,在GDT,LDT寄存器里面,可以设置内存保护位,大小等等信息
  • 原始的系统没有任何可以使用库,但是不要忘记BIOS提供的中断调用,还有BIOS设定好的各种IO端口,通过它们我们就可以获取和设置计算机信息,从而达到控制计算机,我们可以把它们封装出函数调用,为了兼容历史,我们需要保留部分低位中断,这样把这些函数写成中断调用(即设置IDT中断调用表),按UNIX规范提供一套SYSCALL
  • 实际上,我们的计算机内存已经非常大了,所以我们往往需要一种快速定位寻址的机制,在没有硬件支持的时候用软件算法可以达到相似的效果,但是在Intel平台上,提供了Paging机制,利用这种机制我们只需要设置PDE表格(即页表),那么CPU就会自动就加载寻址,这就达到硬件加速的效果
  • 但我们设置完了进程相关设置,内存相关设置后,接下来的就是文件系统,文件系统是操作系统的基本要素(目前来说是这样的),内核会按着磁盘上数据格式把文件信息读取,并在内存中构建一个文件存留信息的数据结构(一般不一定是位视图这种表格,多数情况要复杂一些,也就是多层次的可以拓展的树状构型)
  • 外设是最复杂的部分,外设需要各种初始化,初始化过的外设才可以正常使用,一般这部分也叫设备驱动流程(这部分也可以延后加载,进行初始化)

初始化程序加载

当内核加载流程全部完成后(只要没有发生出错都会加载完成),内核会启动系统的第一个进程,它就是著名的init进程(pid为1),而现在随着开源社区的发展,出现过好多版本的init程序了,最老也是最稳定的是sysvinit,除此之外还有Upstart之流,不过systemd还是更受青睐,它也是目前互动最多的init初始化开源项目。

Systemd和sysvinit有很大的区别,所以这里分别介绍两者:

Upstart是一个基于事件触发的异步初始化方案,和Sysvinit高度兼容,但这里不作讨论,相关介绍可以查阅其他介绍Upstart的博客或技术Wiki等等

Sysvinit的流程

Sysvinit的启动可以把它看作两个部分:初始化,服务加载。

初始化部分

在Systvinit中,它会读取系统中的 /etc/inittab信息,从而来确定系统下一步的启动步骤。为了判断下一步步骤,系统有一个概念,叫做“运行级别”(runlevel),意思就是要确定以何种场景来启动系统,一般而言,Linux有如下7种运行级别:

  • 0 - 关机模式
  • 1 - 单用户模式
  • 2 - 多用户模式
  • 3 - 多用户网络模式
  • 4 - 自定义模式
  • 5 - 完全模式
  • 6 - 重启模式

Runlevel的定义可以参考维基百科的介绍

针对不同的Linux发行版本,上述的定义值不一定相同。

当init程序读取了/etc/inittab中的信息时,该配置文件里面存放了各种运行级别所要进行的操作,假设有如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# These are the default runlevels in Slackware:
# 0 = halt
# 1 = single user mode
# 2 = unused (but configured the same as runlevel 3)
# 3 = multiuser mode (default Slackware runlevel)
# 4 = X11 with KDM/GDM/XDM (session managers)
# 5 = unused (but configured the same as runlevel 3)
# 6 = reboot

# Default runlevel. (Do not set to 0 or 6)
id:3:initdefault:

# System initialization (runs when system boots).
si:S:sysinit:/etc/rc.d/rc.S

# Script to run when going single user (runlevel 1).
su:1S:wait:/etc/rc.d/rc.K

# Script to run when going multi user.
rc:2345:wait:/etc/rc.d/rc.M

# What to do at the "Three Finger Salute".
ca::ctrlaltdel:/sbin/shutdown -t5 -r now

# Runlevel 0 halts the system.
l0:0:wait:/etc/rc.d/rc.0

# Runlevel 6 reboots the system.
l6:6:wait:/etc/rc.d/rc.6

# What to do when power fails.
pf::powerfail:/sbin/genpowerfail start

# If power is back, cancel the running shutdown.
pg::powerokwait:/sbin/genpowerfail stop

# These are the standard console login getties in multiuser mode:
c1:12345:respawn:/sbin/agetty --noclear 38400 tty1 linux
c2:12345:respawn:/sbin/agetty 38400 tty2 linux
c3:12345:respawn:/sbin/agetty 38400 tty3 linux
c4:12345:respawn:/sbin/agetty 38400 tty4 linux
c5:12345:respawn:/sbin/agetty 38400 tty5 linux
c6:12345:respawn:/sbin/agetty 38400 tty6 linux

# Local serial lines:
#s1:12345:respawn:/sbin/agetty -L ttyS0 9600 vt100
#s2:12345:respawn:/sbin/agetty -L ttyS1 9600 vt100

# Dialup lines:
#d1:12345:respawn:/sbin/agetty -mt60 38400,19200,9600,2400,1200 ttyS0 vt100
#d2:12345:respawn:/sbin/agetty -mt60 38400,19200,9600,2400,1200 ttyS1 vt100

# Runlevel 4 also starts /etc/rc.d/rc.4 to run a display manager for X.
# Display managers are preferred in this order: gdm, kdm, xdm
x1:4:respawn:/etc/rc.d/rc.4

每一行可以看作是一个函数。

这样以来,第一列对应单个函数的ID(2-4个字母数字组合,只要不重复即可),第二列对应函数Runlevel的值,第三列对应内部Runlevel操作入口(action),第四列是函数要执行的程序或者脚本。

对于函数的操作入口(action)存在如下几种:initdefault, sysinit, boot, bootwait, wait, respawn

例如,以这个函数举例来说:

1
id:3:initdefault:

当运行Runlevel为3时,就会执行initdefault的入口,它不需要执行脚本或程序,表示默认的Runlevel,其他的函数配置同理。

再看上面的inittab配置,如下这行:

1
si:S:sysinit:/etc/rc.d/rc.S

它表示系统启动是就运行/etc/rc.d/rc.S脚本,在/etc/rc.d/rc.S脚本里面则包含:

  • 挂载sysfs文件目录节点
  • 挂载tmpfs文件目录节点
  • 挂载proc文件目录节点
  • 挂载cgroup文件目录节点
  • 启动其他的rc.XX脚本
  • 加载需要的内核模块
  • 检测文件系统
  • 随机数设置
  • 等等

对于用多个Runlevel的函数:

1
rc:2345:wait:/etc/rc.d/rc.M

表示在2345这4个Runlevel下,都会执行/etc/rc.d/rc.M,并等待它执行完成。

init的配置非常的清晰明了,它的启动脚本可以在目录/etc/rc.d下查看。

系统就是这样加载rc.d下的初始化脚本,序列化地完成所有的init初始化流程。

加载服务

Sysvinit中对于服务的加载是通过/etc/rc.d下的初始化脚本来执行的,一般情况下,一个服务都编写一个服务管理的Shell脚本,它可以接受:statusstartstoprestart这样的参数,并把它放在/etc/rc.d/init.d目录下面。

例如,有如下服务(/etc/rc.d/init.d/vbox-usb-service):

我们就可以执行:

1
/etc/rc.d/init.d/vbox-usb-service status

来查看此服务是否已经启动,但是一般情况下,系统会为了方便创建了一个软链接(如果系统没有创建该软链接,我们可以手动创建之):

1
/etc/init.d

该软链接指向/etc/rc.d/init.d,因此,我们也可以这样重启服务:

1
/etc/init.d/vbox-usb-service restart

但是,这不是必须这样的组织方式,也可以把服务管理的脚本直接就放在/etc/rc.d目录下,例如:

1
/etc/rc.d/rc.httpd

该脚本是用于Apache的httpd服务的管理脚本,执行:

1
/etc/rc.d/rc.httpd start

即可完成httpd服务的启动。

对于一些外部的第三方服务都是放在/etc/rc.d/init.d下面,这样这些脚本只要添加执行权限,就会被初始化脚本自动加载,而不用去更改初始化脚本;而在/etc/rc.d下的服务管理脚本则是Linux发行版本自带的服务,它已经在初始化脚本中做过判断设置,如果我们要把一个第三方的服务管理脚本放到/etc/rc.d下,就意味着我们必须要修改初始化脚本。

Systemd的流程

Systemd中,所有的都以单元组件的形式来加载,这些单元组件很多都可以并发执行,达到快速加载的目的,单元组件的前件和后件构成一套稳定的加载系统。

初始化加载

在以Systemd为init服务的Linux系统中,参看man 7 bootup,以了解更多有关Systemd的启动详情。

加载图示

systemd-graph

由上图可见,在加载sysinit之前,可以加载很多的服务,而这些服务是互相独立无依赖的,因此可以并行地加载,当所有的服务都加载完后,sysinit服务就算加载成功,之后进行下面的服务加载,后面的加载逻辑以此类推。注意到,类似rescue这种是具有特殊用途的服务(这里的服务是用于系统崩溃后的拯救),还有一个独立依赖链,或者没有依赖的服务,被加载到目前的服务的情况,例如上图的remote-fs就是这种。

上图绘制的systemd加载图比较粗糙,细节要复杂一些,有些一个单元的加载可能分好几个步骤来加载,有的可能细分更多的加载逻辑。

用户登录系统

当系统必要的服务都启动后,会分为几种场景:

Runlevel 3 登录

在该模式下,用户自动进入ttyS1来登录系统,init会启动/sbin/agetty提示用户输入用户名和密码,getty和login判断用户合法就让用户进入系统,打开用户的Shell程序。

上述配置,会产生6个用户可以同时登录。

此外,getty和login这种登录验证需要验证密码,通常是读取/etc/passwd下的保存设置来验证用户有效性。

(细节可以参考系统函数:getpass

Runlevel 4 登录

如果是多用户模式,会启动一个用于管理系统登录的服务(一般PolKit/ConsoleKit最常见),这里以ConsoleKit举例来说,它允许用户在同一硬件设备登录的情况下,不用登出系统还可以再次登录,ConsoleKit以抽象化成会话的模型,从而实现这种登录机制。

在图形登录下,都会使用这种方式来登录系统。

在ConsoleKit的登录验证下,也是通过读取/etc/passwd配置文件的信息来验证用户有效性的。

(对于如何验证的细节,可以参考一个系统提供的安全相关的函数:crypt

打开用户SHELL/WDM程序

当用户输入名称和密码通过验证后,init程序需要为用户执行一个Shell运行程序,以便使用户能够对系统来进行管理,Shell的种类很多,根据用户的爱好,可以自由选择一款适合的Shell。

常见的Shell有:Bash, Csh, Ash, Tcsh, Zsh等等

Shell被启动后,它就接管了用户的空间,用户可以通过它以命令方式来完成任何操作。

Shell的启动会读取系统全局配置,再基于用户$HOME目录下的配置文件,读取自定义的配置。

通常对于图形界面的登录有一些不同,图形界面的登录启动的不是Shell,而是一个XWindows的WDM,即一个窗口管理器,它接管用户空间,为用户初始化图形化设置,并展示图形界面。

常见的 WDM有:GDM,SDDM,KDM等等

欢迎关注我的其它发布渠道