在打开或重新启动一台计算机时,计算机必须加载一个操作系统,然后您才能开始做任何有用的工作。这个过程称为引导计算机。计算机使用一个引导加载程序,通过引导程序来启动自身。
计算机启动流程
- 加载BIOS
BIOS的代码存储在ROM,EEPROM或闪存等非易失性存储器中。在启动或重新引导PC时,会执行此代码。BIOS从第一个扇区加载的这段代码称为第一阶段引导加载程序或阶段1引导加载程序。
- 硬件自检
执行开机自检(POST)来检查机器
- BIOS按照启动顺序转交控制权
确定来自可用的可移动设备或固定存储设备的引导驱动器,并从该驱动器上的主引导记录(MBR)加载第一个扇区。MS DOS、PC DOS 和Windows操作系统所使用的标准硬盘驱动器MBR会检查分区表,查找引导驱动器上标为active的主分区,加载该分区的第一个扇区,并将控制权传递到已加载的代码的开头处。这段新代码也称为分区引导记录。
分区引导记录实际上是另一个阶段1引导加载程序,但这个加载程序只能从分区加载一组数据块。
这组新数据块中的代码称为阶段2启动加载程序 。在由MS-DOS和PC-DOS使用时,阶段2加载程序直接继续加载操作系统的其余部分。这就是操作系统在正常运行之前通过其引导程序启动自身的过程。
- 计算机读取该设备的第一个扇区,也就是读取最前面的512个字节(主引导记录)
- 如果这512个字节的最后两个字节是
0x55
和0xAA
,表明这个设备可以用于启动 - 如果不是,表明设备不能用于启动,控制权于是被转交给”启动顺序”中的下一个设备。
- 主引导记录:告诉计算机到硬盘的哪一个位置去找操作系统
- 第1-446字节:调用操作系统的机器码
- 第447-510字节:分区表
- 第511-512字节:主引导记录签名(
0x55
和0xAA
)
- 如果这512个字节的最后两个字节是
- 运行启动管理器(Bootloader)
- 根据grub设定的内核映像所在路径,系统读取内存映像,并进行解压缩
- 初始化硬件设备
- 建立内存空间的映射图
- 例子:Grub
- 加载内核
当内核初始化完成后,接下来的工作就是加载初始进程了,内核初始化的最后一步就是启动pid
为1的init进程,init以守护进程方式存在,是所有其他进程的祖先。init进程非常独特,能够完成其他进程无法完成的任务。
启动初始进程
linux操作系统有3种方式来加载初始进程,linux系统在启动时要进行大量的初始化工作,比如挂载文件系统和交换分区、启动各类进程服务等,这些都可以看作是一个一个的单元(Unit)
System V init
多数Linux发行版的init系统是和System V相兼容的,被称为System V init
init亦即系统内核第一个调用的程序(进程)。然后init再去运行所有的系统所需要的服务:不论是本地服务还是网络服务。
- 相对于新的Systemd启动进程,init算是传统的、老旧的系统启动进程
- Centos 7.x开始,传统的init已经被舍弃,取而代之的是Systemd
System V init用术语runlevel
来定义预订的运行模式,检查/etc/inittab
文件中是否含有initdefault
项。 这告诉init系统是否有一个默认运行模式。如果没有默认的运行模式,那么用户将进入系统控制台,手动决定进入何种运行模式。
所有服务的启动脚本放置于/etc/init.d/
目录,/etc/rc?.d
链接到/etc/init.d/
,System V init巧妙地用脚本,文件命名规则和软链接来实现不同的runlevel
,System V init需要读取/etc/inittab
文件。分析这个文件的内容,它获得以下一些配置信息:
- 系统需要进入的
runlevel
- 捕获组合键的定义
- 定义电源
fail/restore
脚本 - 启动getty和虚拟控制台
得到配置信息后,System V init顺序地执行以下这些步骤,从而将系统初始化为预订的runlevel
/etc/rc.d/rc.sysinit
- 执行一些重要的系统初始化任务
- 激活udev和selinux
- 设置定义在
/etc/sysctl.conf
中的内核参数 - 设置系统时钟
- 加载keymaps
- 使能交换分区
- 设置主机名(hostname)
- 根分区检查和 remount
- 激活RAID和LVM设备
- 开启磁盘配额
- 检查并挂载所有文件系统
- 清除过期的locks和PID文件
- 执行一些重要的系统初始化任务
/etc/rc.d/rc
和/etc/rc.d/rcX.d/
(X代表运行级别 0-6)- 运行存放在该目录下的所有启动脚本
- 文件名以
S
开头的脚本就是启动时应该运行的脚本,S
后面跟的数字定义了这些脚本的执行顺序 - 在
/etc/rc.d/rcX.d
目录下的脚本其实都是一些软链接文件,真实的脚本文件存放在/etc/init.d
目录
/etc/rc.d/rc.local
- 所有的初始化脚本执行完毕,运行这里
- X Display Manager(如果需要的话)
System V init还负责关闭系统,在系统关闭时,为了保证数据的一致性,需要小心地按顺序进行结束和清理工作。
- 启动:
/etc/init.d/daemon start
- 关闭:
/etc/init.d/daemon stop
- 重新启动:
/etc/init.d/daemon restart
- 查看状态:
/etc/init.d/daemon status
inti服务的分类中,根据服务是独立启动或被一个总管程序管理分为两大类
- 独立启动模式(stand alone):服务独立启动,该服务直接常驻于内存中,提供本机或用户的服务操作
- 超级守护进程:由特殊的xinetd或inetd这两个总管程序提供socket对应或端口对应的管理。当没有用户要求某socket或端口时,所需要服务不会被启动。若有用户要求时,xinetd才会唤醒相对应的服务进程。当该要求结束时,此服务也会被结束
在Linux主要应用于服务器和PC机的时代,System V init运行非常良好,概念简单清晰。它主要依赖于Shell脚本,这就决定了它的最大弱点:启动太慢。而当Linux被应用到移动终端设备的时候,启动慢就成了一个大问题。
程序运行级别
运行模式描述了系统各种预订的运行模式。通常会有8种运行模式,每种Linux发行版对运行模式的定义都不太一样。但0
,1
,6
却得到了大家的一致赞同
0
:关机1
:单用户模式6
:重启
Upstart
当Linux内核进入2.6时代时,内核功能有了很多新的更新。新特性使得Linux不仅是一款优秀的服务器操作系统,也可以被用于桌面系统,甚至嵌入式设备。桌面系统或便携式设备的一个特点是经常重启,而且要频繁地使用硬件热插拔技术。因此,当系统上电启动时,一些外设可能并没有连接。而是在启动后当需要的时候才连接这些设备。在2.6内核支持下,一旦新外设连接到系统,内核便可以自动实时地发现它们,并初始化这些设备,进而使用它们。这为便携式设备用户提供了很大的灵活性。
而System V init要求在系统初始化时,被初始化的设备需要连接到系统上,否则如果没有连接,就启动服务,是一种浪费,而且不知道需要启动什么服务。同时System V init对服务启动顺序控制起来并不方便,所以需要一个新的启动程序。
RHEL6使用新的Upstart启动服务替换先前的System V init,Upstart是事件驱动型的,因此,它只包含按需启动的脚本,这将使启动过程变得更加迅速。经过良好调优并使用Upstart启动方式的Linux服务器的启动速度要明显快于原有的使用System V init的系统。同时,当设备插入的时候,Upstart在感知到该事件之后触发相应的等待任务,解决了设备的即插即用
同时,事件驱动型可以并行启动,不像System V init是同步阻塞的,只能串行执行
- 更快地启动系统
- 当新硬件被发现时动态启动服务
- 硬件被拔除时动态停止服务
Upstart本身并没有运行级别的概念,但完全可以用UpStart的工作模拟出来。因为历史的原因,Linux上的多数软件还是采用传统的System V init脚本启动方式,并没有为UpStart开发新的启动脚本,因此即便在Debian和Ubuntu系统上,还是必须模拟老的 System V init的运行级别模式,以便和多数现有软件兼容
/etc/inittab
只用来配置系统默认运行级别,所有先前由/etc/inittab
来设定的条目,现在都在/etc/init/
目录中以单个文件的形式存在。/etc/init/rcS.conf
通过启动大部分的基本服务来对系统进行初始化的设定/etc/init/rc.conf
对启动各自的运行级别(runlevel
)的设定/etc/init/control-alt-delete.conf
定义当用户按control-alt-delete
三个键时的系统行为/etc/init/tty.conf、/etc/init/serial.conf
定义系统处理终端登录/etc/sysconfig/init
中ACTIVE_CONSOLES
决定了虚拟控制台的创建,AUTOSWAP
是否自动检测交换分区,单用户模式下的root使用的SHELL,默认为/sbin/sushell
,另外/sbin/sulogin
会在单用户模式启动之前弹出登录提示。
Systemd已经取代Upstart
Upstart启动过程
- 系统上电后运行GRUB载入内核。内核执行硬件初始化和内核自身初始化
- 启动pid为1的init进程,即UpStart进程
- Upstart进程在执行了一些自身的初始化工作后,立即发出
startup
事件 - 所有依赖于
startup
事件的工作被触发,其中最重要的是mountallmountall
任务负责挂载系统中需要使用的文件系统,完成相应工作后,mountall
任务会发出以下事件local-filesystem
virtual-filesystem
- 触发
udev
任务开始工作- 触发
upstart-udev-bridge
的工作- 发出
net-device-up IFACE=lo
事件
- 发出
- 发出
filesystem
事件- 触发
rc-sysinit
开始工作- 任务
rc-sysinit
调用telinit
。telinit
任务会发出runlevel
事件,触发执行/etc/init/rc.conf
rc.conf
执行/etc/rc$.d/
目录下的所有脚本,和System V init非常类似
- 任务
- 触发
- 触发
- 触发
all-swaps
Upstart概念
- Job:一个工作的单元,一个任务或者一个服务。相当于System V init中一个脚本,有三种类型的工作
- task job:在一定时间内会执行完毕的任务,比如删除一个文件
- service job:后台服务进程(守护进程)
- abstract job:仅Upstart内部使用
Upstart 为每个工作都维护一个生命周期。一般来说,工作有开始,运行和结束这几种状态。
状态名 | 含义 |
---|---|
Waiting | 初始状态 |
Starting | Job 即将开始 |
pre-start | 执行 pre-start 段,即任务开始前应该完成的工作 |
Spawned | 准备执行 script 或者 exec 段 |
post-start | 执行 post-start 动作 |
Running | interim state set after post-start section processed denoting job is running (But it may have no associated PID!) |
pre-stop | 执行 pre-stop 段 |
Stopping | interim state set after pre-stop section processed |
Killed | 任务即将被停止 |
post-stop | 执行 post-stop 段 |
事件Event
一旦某个事件发生了,Upstart就向整个系统发送一个消息。事件一旦发生,整个Upstart系统中所有工作和其它的事件都会得到通知。
- Signals:非阻塞的,异步的。发送一个信号之后控制权立即返回。
- Methods:阻塞的,同步的。
- Hooks:阻塞的,同步的。介于Signals和Methods之间,调用发出Hooks事件的进程必须等待事件完成才可以得到控制权,但不检查事件是否成功。
事件例子:
- 根文件系统可写时,相应job会发送文件系统就绪的事件
- 一个块设备被发现并初始化完成,发送相应的事件
- 某个文件系统被挂载,发送相应的事件
- 类似atd和 cron,可以在某个时间点,或者周期的时间点发送事件
- 另外一个job开始或结束时,发送相应的事件
- 一个磁盘文件被修改时,可以发出相应的事件
- 一个网络设备被发现时,可以发出相应的事件
- 缺省路由被添加或删除时,可以发出相应的事件
Upstart就是由事件触发工作运行的一个系统,每一个程序的运行都由其依赖的事件发生而触发的。
- init进程自身会发出不同的事件,这些最初的事件会触发一些工作运行
- 每个工作运行过程中会释放不同的事件,这些事件又将触发新的工作运行
究竟哪些事件会触发某个工作的运行?这是由工作配置文件定义的。
工作配置文件
任何一个工作都是由一个工作配置文件(Job Configuration File)定义的。这个文件是一个文本文件,包含一个或者多个小节(stanza)。每个小节是一个完整的定义模块,定义了工作的一个方面.比如author小节定义了工作的作者。工作配置文件存放在/etc/init
下面,是以.conf
作为文件后缀的文件。
1 | #This is a simple demo of Job Configure file |
Session Init
UpStart还可以用于管理用户会话的初始化。每个用户的使用习惯和使用方法都不相同,因此用户往往需要为自己的会话做一个定制,比如添加特定的命令别名,启动特殊的应用程序或者服务,等等。这些工作都属于对特定会话的初始化操作,因此可以被称为Session Init。
用户使用Linux可以有两种模式:字符模式和图形界面。在字符模式下,会话初始化相对简单。用户登录后只能启动一个Shell,通过shell命令使用系统。各种shell程序都支持一个自动运行的启动脚本,比如~/.bashrc
。用户在这些脚本中加入需要运行的定制化命令。字符会话需求简单,因此这种现有的机制工作的很好。
一个桌面环境包括window manager,panel以及其它一些定义在/usr/share/gnome-session/sessions/下面的基本组件;此外还有一些辅助的应用程序,共同帮助构成一个完整的方便的桌面,比如system monitors,panel applets,NetworkManager,Bluetooth,printers等。当用户登录之后,这些组件都需要被初始化,这个过程比字符界面要复杂的多。目前启动各种图形组件和应用的工作由gnome-session完成。过程如下:
以Ubuntu为例,当用户登录Ubuntu图形界面后,显示管理器(Display Manager)lightDM 启动Xsession。Xsession 接着启动gnome-session,gnome-session 负责其它的初始化工作,然后就开始了一个desktop session。
1 | init |
一些应用和组件其实并不需要在会话初始化过程中启动,比如Network Manager,一天之内用户很少切换网络设备,所以大部分时间Network Manager服务仅仅是在浪费系统资源
Upstart命令
UpStart提供了一系列的命令来完成这些工作。其中的核心是initctl
。initctl stop
停止一个正在运行的工作;用initctl start
开始一个工作;还可以用initctl status
来查看一个工作的状态;initctl restart
重启一个工作;initctl reload
可以让一个正在运行的服务重新载入配置文件。这些命令和传统的service
命令十分相似。
Service 命令 | UpStart initctl 命令 |
---|---|
service start |
initctl start |
service stop |
initctl stop |
service restart |
initctl restart |
service reload |
initctl reload |
Systemd
Systemd是Linux系统中最新的初始化系统(init),它主要的设计目标是克服System V init固有的缺点,提高系统的启动速度。Systemd和Ubuntu的Upstart是竞争对手,且已经取代Upstart(Ubuntu16.04)
Systemd是一个"新来的",Linux上的很多应用程序并没有来得及为它做相应的改变。和UpStart一样,Systemd引入了新的配置方式,对应用程序的开发也有一些新的要求。如果Systemd想替代目前正在运行的初始化系统,就必须和现有程序兼容。任何一个Linux发行版都很难为了采用Systemd而在短时间内将所有的服务代码都修改一遍。
Systemd提供了比UpStart更激进的并行启动能力,采用了socket/D-Bus activation等技术启动服务。一个显而易见的结果就是:更快的启动速度。
- 尽可能启动更少的进程
- 尽可能将更多进程并行启动
Systemd能够更进一步提高并发性,即便对于那些UpStart认为存在相互依赖而必须串行的服务,比如Avahi和D-Bus也可以并发启动。所有的任务都同时并发执行,总的启动时间被进一步降低为T1。
按需启动
System V init会将所有可能用到的后台服务进程全部启动运行,系统必须等待所有的服务都启动就绪之后,才允许用户登录。Systemd可以提供按需启动的能力,只有在某个服务被真正请求的时候才启动它。当该服务结束,Systemd可以关闭它,等待下次需要时再次启动它。
Systemd采用Linux的Cgroup特性跟踪和管理进程的生命周期
服务进程一般都会作为精灵进程(daemon)在后台运行,为此服务程序有时候会fork
两次。在UpStart中,需要在配置文件中正确地配置expect小节。这样UpStart通过对fork
调用进行计数,从而获知真正的精灵进程的PID号。如果UpStart找错了,那就不能控制服务了。后来又通过strace
来跟踪fork
、exit
等系统调用,但是这种方法很笨拙,且缺乏可扩展性。
Systemd则利用了Linux内核的特性即CGroup来完成跟踪的任务。当停止服务时,通过查询CGroup,Systemd可以确保找到所有的相关进程,从而干净地停止服务。
CGroup已经出现了很久,它主要用来实现系统资源配额管理(如:docker)。CGroup提供了类似文件系统的接口,使用方便。当进程创建子进程时,子进程会继承父进程的CGroup。因此无论服务如何启动新的子进程,所有的这些相关进程都会属于同一个CGroup,Systemd只需要简单地遍历指定的CGroup即可正确地找到所有的相关进程,将它们一一停止即可。
启动挂载点和自动挂载
传统的Linux系统中,用户可以用/etc/fstab
文件来维护固定的文件系统挂载点。这些挂载点在系统启动过程中被自动挂载,一旦启动过程结束,这些挂载点就会确保存在。和System V init一样,Systemd管理这些挂载点,以便能够在系统启动时自动挂载它们。Systemd还兼容/etc/fstab
文件,您可以继续使用该文件管理挂载点。
Systemd内建了自动挂载服务,无需另外安装autofs服务,可以直接使用Systemd提供的自动挂载管理能力来实现autofs的功能。
依赖关系管理
系统启动过程是由很多的独立工作共同组成的,这些工作之间可能存在依赖关系,比如挂载一个NFS文件系统必须依赖网络能够正常工作。Systemd虽然能够最大限度地并发执行很多有依赖关系的工作,但是类似"挂载 NFS"和"启动网络"这样的工作还是存在天生的先后依赖关系,无法并发执行。对于这些任务,Systemd维护一个"事务一致性"的概念,保证所有相关的服务都可以正常启动而不会出现互相依赖,以至于死锁的情况。
Systemd用配置单元定义文件中的关键字来描述配置单元之间的依赖关系。比如:unit A依赖unit B,可以在unit B的定义中用"require A"来表示。这样Systemd就会保证先启动A再启动B。
快照和恢复
Systemd支持按需启动,因此系统的运行状态是动态变化的,人们无法准确地知道系统当前运行了哪些服务。Systemd快照提供了一种将当前系统运行状态保存并恢复的能力。
比如系统当前正运行服务A和B,可以用Systemd命令行对当前系统运行状况创建快照。然后将进程A停止,或者做其他的任意的对系统的改变,比如启动新的进程C。在这些改变之后,运行Systemd的快照恢复命令,就可立即将系统恢复到快照时刻的状态,即只有服务A,B在运行。一个可能的应用场景是调试:比如服务器出现一些异常,为了调试用户将当前状态保存为快照,然后可以进行任意的操作,比如停止服务等等。等调试结束,恢复快照即可。
日志服务
Systemd自带日志服务journald,该日志服务的设计初衷是克服现有的syslog服务的缺点。
- syslog不安全,消息的内容无法验证。每一个本地进程都可以声称自己是PID 4711,而syslog也就相信并保存到磁盘上
- 数据没有严格的格式,非常随意。自动化的日志分析器需要分析人类语言字符串来识别消息。一方面此类分析困难低效;此外日志格式的变化会导致分析代码需要更新甚至重写。
Systemd Journal用二进制格式保存所有日志信息,用户使用journalctl命令来查看日志信息。无需自己编写复杂脆弱的字符串分析处理程序。
- 简单性:代码少,依赖少,抽象开销最小。
- 零维护:日志是除错和监控系统的核心功能,因此它自己不能再产生问题。举例说,自动管理磁盘空间,避免由于日志的不断产生而将磁盘空间耗尽。
- 移植性:日志 文件应该在所有类型的Linux系统上可用,无论它使用的何种CPU或者字节序。
- 性能:添加和浏览 日志 非常快。
- 最小资源占用:日志 数据文件需要较小。
- 统一化:各种不同的日志存储技术应该统一起来,将所有的可记录事件保存在同一个数据存储中。所以日志内容的全局上下文都会被保存并且可供日后查询。例如一条固件记录后通常会跟随一条内核记录,最终还会有一条用户态记录。重要的是当保存到硬盘上时这三者之间的关系不会丢失。Syslog将不同的信息保存到不同的文件中,分析的时候很难确定哪些条目是相关的。
- 扩展性:日志的适用范围很广,从嵌入式设备到超级计算机集群都可以满足需求。
- 安全性:日志 文件是可以验证的,让无法检测的修改不再可能。
Systemd概念
单元
系统初始化需要做的事情非常多,这个过程中的每一步都被Systemd抽象为一个配置单元,即Unit。可以认为一个服务是一个配置单元,一个挂载点是一个配置单元,一个交换分区的配置是一个配置单元。
Systemd将配置单元归纳为以下一些不同的类型
- service:代表一个后台服务进程,比如mysqld。这是最常用的一类。
- socket:此类配置单元封装系统和互联网中的一个套接字 。当下,Systemd支持流式、数据报和连续包的
AF_INET
、AF_INET6
、AF_UNIX socket
。每一个套接字配置单元都有一个相应的服务配置单元。相应的服务在第一个"连接"进入套接字时就会启动(例如:nscd.socket
在有新连接后便启动nscd.service
) - device:此类配置单元封装一个存在于Linux设备树中的设备。每一个使用udev规则标记的设备都将会在Systemd中作为一个设备配置单元出现
- mount:此类配置单元封装文件系统结构层次中的一个挂载点。Systemd将对这个挂载点进行监控和管理。比如可以在启动时自动将其挂载;可以在某些条件下自动卸载。Systemd会将
/etc/fstab
中的条目都转换为挂载点,并在开机时处理 - automount:此类配置单元封装系统结构层次中的一个自挂载点。每一个自挂载配置单元对应一个挂载配置单元,当该自动挂载点被访问时,Systemd执行挂载点中定义的挂载行为。
- swap: 和挂载配置单元类似,交换配置单元用来管理交换分区。用户可以用交换配置单元来定义系统中的交换分区,可以让这些交换分区在启动时被激活。
- target:此类配置单元为其他配置单元进行逻辑分组。它们本身实际上并不做什么,只是引用其他配置单元而已。这样便可以对配置单元做一个统一的控制。这样就可以实现大家都已经非常熟悉的运行级别概念。比如想让系统进入图形化模式,需要运行许多服务和配置命令,这些操作都由一个个的配置单元表示,将所有这些配置单元组合为一个目标(target),就表示需要将这些配置单元全部执行一遍以便进入目标所代表的系统运行状态。 (例如:multi-user.target 相当于在传统使用 SysV的系统中运行级别 5)
- timer:定时器配置单元用来定时触发用户定义的操作,这类配置单元取代了atd、crond等传统的定时服务
- snapshot:与target配置单元相似,快照是一组配置单元。它保存了系统当前的运行状态。
每个配置单元都有一个对应的配置文件,系统管理员的任务就是编写和维护这些不同的配置文件,比如一个MySQL服务对应一个mysql.service
文件。这种配置文件的语法非常简单,用户不需要再编写和维护复杂的系统 5 脚本了。
事务
Systemd 能保证事务完整性。Systemd的事务概念和数据库中的有所不同,主要是为了保证多个依赖的配置单元之间没有环形引用。
存在循环依赖,那么Systemd将无法启动任意一个服务。此时Systemd将会尝试解决这个问题,因为配置单元之间的依赖关系有两种
- required:强依赖
- want:弱依赖
Systemd将去掉wants关键字指定的依赖看看是否能打破循环。如果无法修复,Systemd会报错。
Target 和运行级别
Systemd用目标(target)代替了System V init中运行级别的概念,您可以继承一个已有的目标,并添加其它服务,来创建自己的目标。下表列举了Systemd下的目标和常见runlevel的对应关系:
级别 | Systemd目标名称 | 作用 |
---|---|---|
0 | runlevel0.target, poweroff.target | 关机 |
1, s, single | runlevel1.target, rescue.target | 单用户模式 |
2, 4 | runlevel2.target, runlevel4.target, multi-user.target | 用户定义/域特定运行级别。默认等同于 3。 |
3 | runlevel3.target, multi-user.target | 多用户,非图形化。用户可以通过多个控制台或网络登录。 |
5 | runlevel5.target, graphical.target | 多用户,图形化。通常为所有运行级别 3 的服务外加图形化登录。 |
6 | runlevel6.target, reboot.target | 重启 |
emergency | emergency.target | 紧急shell |
并发启动原理
- 并发启动原理之一:解决Socket依赖
绝大多数的服务依赖是套接字依赖。比如服务A通过一个套接字端口S1提供自己的服务,其他的服务如果需要服务A,则需要连接S1。因此如果服务A尚未启动,S1就不存在,其他的服务就会得到启动错误。所以传统地,人们需要先启动服务A,等待它进入就绪状态,再启动其他需要它的服务。Systemd认为,只要我们预先把S1建立好,那么其他所有的服务就可以同时启动而无需等待服务A来创建S1了。如果服务A尚未启动,那么其他进程向S1发送的服务请求实际上会被Linux操作系统缓存,其他进程会在这个请求的地方等待。一旦服务A启动就绪,就可以立即处理缓存的请求,一切都开始正常运行。
那么服务如何使用由init进程创建的套接字呢?
Linux操作系统有一个特性,当进程调用fork
或者exec
创建子进程之后,所有在父进程中被打开的文件句柄(file descriptor)都被子进程所继承。套接字也是一种文件句柄,进程A可以创建一个套接字,此后当进程A调用exec
启动一个新的子进程时,只要确保该套接字的close_on_exec
标志位被清空,那么新的子进程就可以继承这个套接字。子进程看到的套接字和父进程创建的套接字是同一个系统套接字,就仿佛这个套接字是子进程自己创建的一样,没有任何区别。
这个特性以前被一个叫做inetd的系统服务所利用。inetd进程会负责监控一些常用套接字端口,比如Telnet,当该端口有连接请求时,inetd才启动telnetd进程,并把有连接的套接字传递给新的telnetd进程进行处理。这样,当系统没有telnet客户端连接时,就不需要启动telnetd进程。inetd可以代理很多的网络服务,这样就可以节约很多的系统负载和内存资源,只有当有真正的连接请求时才启动相应服务,并把套接字传递给相应的服务进程。
和inetd类似,Systemd是所有其他进程的父进程,它可以先建立所有需要的套接字,然后在调用exec
的时候将该套接字传递给新的服务进程,而新进程直接使用该套接字进行服务即可。
- 并发启动原理之二:解决D-Bus依赖
D-Bus是desktop-bus的简称,是一个低延迟、低开销、高可用性的进程间通信机制。它越来越多地用于应用程序之间通信,也用于应用程序和操作系统内核之间的通信。很多现代的服务进程都使用D-Bus取代套接字作为进程间通信机制,对外提供服务。比如简化Linux网络配置的NetworkManager服务就使用D-Bus和其他的应用程序或者服务进行交互:邮件客户端软件evolution可以通过D-Bus从NetworkManager服务获取网络状态的改变,以便做出相应的处理。
D-Bus支持所谓"bus activation"功能。如果服务A需要使用服务B的D-Bus服务,而服务B并没有运行,则D-Bus可以在服务A请求服务B的D-Bus时自动启动服务B。而服务A发出的请求会被D-Bus缓存,服务A会等待服务B启动就绪。利用这个特性,依赖D-Bus的服务就可以实现并行启动。
- 并发启动原理之三:解决文件系统依赖
系统启动过程中,文件系统相关的活动是最耗时的,比如挂载文件系统,对文件系统进行磁盘检查(fsck),磁盘配额检查等都是非常耗时的操作。在等待这些工作完成的同时,系统处于空闲状态。那些想使用文件系统的服务似乎必须等待文件系统初始化完成才可以启动。但是Systemd发现这种依赖也是可以避免的。
Systemd参考了autofs的设计思路,使得依赖文件系统的服务和文件系统本身初始化两者可以并发工作。autofs可以监测到某个文件系统挂载点真正被访问到的时候才触发挂载操作,这是通过内核automounter模块的支持而实现的。比如一个open()系统调用作用在/misc/cd/file1
的时候,/misc/cd
尚未执行挂载操作,此时open()
调用被挂起等待,Linux内核通知autofs,autofs执行挂载。这时候,控制权返回给open()
系统调用,并正常打开文件。
Systemd集成了autofs的实现,对于系统中的挂载点,比如/home
,当系统启动的时候,Systemd为其创建一个临时的自动挂载点。在这个时刻/home
真正的挂载设备尚未启动好,真正的挂载操作还没有执行,文件系统检测也还没有完成。可是那些依赖该目录的进程已经可以并发启动,他们的open()
操作被内建在Systemd中的autofs捕获,将该open()
调用挂起(可中断睡眠状态)。然后等待真正的挂载操作完成,文件系统检测也完成后,Systemd将该自动挂载点替换为真正的挂载点,并让open()
调用返回。由此,实现了那些依赖于文件系统的服务和文件系统本身同时并发启动。
当然对于/
根目录的依赖实际上一定还是要串行执行,因为Systemd自己也存放在/
之下,必须等待系统根目录挂载检查好。
不过对于类似/home
等挂载点,这种并发可以提高系统的启动速度,尤其是当/home
是远程的NFS节点,或者是加密盘等,需要耗费较长的时间才可以准备就绪的情况下,因为并发启动,这段时间内,系统并不是完全无事可做,而是可以利用这段空余时间做更多的启动进程的事情,总的来说就缩短了系统启动时间。
Systemd的使用
- 后台服务进程代码不需要执行两次派生来实现后台精灵进程,只需要实现服务本身的主循环即可。
- 不要调用
setsid()
,交给Systemd处理 - 不再需要维护pid文件。
- Systemd提供了日志功能,服务进程只需要输出到stderr即可,无需使用syslog。
- 处理信号SIGTERM,这个信号的唯一正确作用就是停止当前服务,不要做其他的事情。
- SIGHUP信号的作用是重启服务。
- 需要套接字的服务,不要自己创建套接字,让Systemd传入套接字。
- 使用
sd_notify()
函数通知Systemd服务自己的状态改变。一般地,当服务初始化结束,进入服务就绪状态时,可以调用它。
Unit文件的编写
开发人员开发了一个新的服务程序,比如httpd,就需要为其编写一个配置单元文件以便该服务可以被Systemd管理,类似UpStart的工作配置文件。在该文件中定义服务启动的命令行语法,以及和其他服务的依赖关系等。服务配置单元文件以.service为文件名后缀。
文件分为三个部分。
- 第一部分:
[Unit]
部分,这里仅仅有一个描述信息 - 第二部分:
[Service]
部分,其中,ExecStartPre定义启动服务之前应该运行的命令;ExecStart定义启动服务的具体命令行语法 - 第三部分:
[Install]
部分,WangtedBy表明这个服务是在多用户模式下所需要的。
此外在/etc/systemd/system
目录下还可以看到诸如*.wants
的目录,放在该目录下的配置单元文件等同于在[Unit]
小节中的wants关键字,即本单元启动时,还需要启动这些单元。比如您可以简单地把您自己写的foo.service
文件放入multi-user.target.wants
目录下,这样每次都会被默认启动了。
Systemd的指令
Systemd的主要命令行工具是systemctl
。多数管理员应该都已经非常熟悉系统服务和init系统的管理,比如service
、chkconfig
以及telinit
命令的使用。systemctl
只是语法有所不同而已
Sysvinit 命令 | Systemd 命令 | 备注 |
---|---|---|
service foo start |
systemctl start foo.service |
用来启动一个服务 (并不会重启现有的) |
service foo stop |
systemctl stop foo.service |
用来停止一个服务 (并不会重启现有的)。 |
service foo restart |
systemctl restart foo.service |
用来停止并启动一个服务。 |
service foo reload |
systemctl reload foo.service |
当支持时,重新装载配置文件而不中断等待操作。 |
service foo condrestart |
systemctl condrestart foo.service |
如果服务正在运行那么重启它。 |
service foo status |
systemctl status foo.service |
汇报服务是否正在运行。 |
ls /etc/rc.d/init.d/ |
systemctl list-unit-files --type=service |
用来列出可以启动或停止的服务列表。 |
chkconfig foo on |
systemctl enable foo.service |
在下次启动时或满足其他触发条件时设置服务为启用 |
chkconfig foo off |
systemctl disable foo.service |
在下次启动时或满足其他触发条件时设置服务为禁用 |
chkconfig foo |
systemctl is-enabled foo.service |
用来检查一个服务在当前环境下被配置为启用还是禁用。 |
chkconfig –list |
systemctl list-unit-files --type=service |
输出在各个运行级别下服务的启用和禁用情况 |
chkconfig foo –list |
ls /etc/systemd/system/*.wants/foo.service |
用来列出该服务在哪些运行级别下启用和禁用。 |
chkconfig foo –add |
systemctl daemon-reload |
当您创建新服务文件或者变更设置时使用。 |
telinit 3 |
systemctl isolate multi-user.target (OR systemctl isolate runlevel3.target OR telinit 3 ) |
改变至多用户运行级别。 |
系统管理员还需要了解其他一些系统配置和管理任务的改变。
命令 | 操作 |
---|---|
systemctl reboot |
重启机器 |
systemctl poweroff |
关机 |
systemctl suspend |
待机 |
systemctl hibernate |
休眠 |
systemctl hybrid-sleep |
混合休眠模式(同时休眠到硬盘并待机 |
传统的Linux系统使用ConsoleKit跟踪用户登录情况,并决定是否赋予其关机的权限。现在ConsoleKit已经被systemd的logind所替代。logind不是pid-1 的init进程。它的作用和UpStart的session init类似,但功能要丰富很多,它能够管理几乎所有用户会话(session)相关的事情。logind不仅是ConsoleKit的替代,它可以:
- 维护,跟踪会话和用户登录情况。如上所述,为了决定关机命令是否可行,系统需要了解当前用户登录情况,如果用户从SSH登录,不允许其执行关机命令;如果普通用户从本地登录,且该用户是系统中的唯一会话,则允许其执行关机命令;这些判断都需要logind维护所有的用户会话和登录情况。
- Logind也负责统计用户会话是否长时间没有操作,可以执行休眠/关机等相应操作。
- 为用户会话的所有进程创建CGroup。这不仅方便统计所有用户会话的相关进程,也可以实现会话级别的系统资源控制。
- 负责电源管理的组合键处理,比如用户按下电源键,将系统切换至睡眠状态。
- 多席位(multi-seat) 管理。如今的电脑,即便一台笔记本电脑,也完全可以提供多人同时使用的计算能力。多席位就是一台电脑主机管理多个外设,比如两个屏幕和两个鼠标/键盘。席位一使用屏幕 1 和键盘 1;席位二使用屏幕 2 和键盘 2,但他们都共享一台主机。用户会话可以自由在多个席位之间切换。或者当插入新的键盘,屏幕等物理外设时,自动启动gdm用户登录界面等。所有这些都是多席位管理的内容。ConsoleKit始终没有实现这个功能,Systemd的logind能够支持多席位。