内核加载
在上篇博文里面我说明过计算机如何从远古到近代的启动流程,然后,走到操作系统一层的时候就没有深入阐述了,在这篇博文我将以Linux系统为例来叙述操作系统是如何启动的。
通常在系统/boot
目录下放着内核文件,如下:
1 | initramfs-linux.img |
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 | These are the default runlevels in Slackware: |
每一行可以看作是一个函数。
这样以来,第一列对应单个函数的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脚本,它可以接受:status
,start
,stop
,restart
这样的参数,并把它放在/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的启动详情。
加载图示

由上图可见,在加载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等等