systemd初探

国庆在家,整天就是玩电脑,越玩越觉得有意思。这两天直在看《Linux就是这个范儿》加上前面看的差不多看完了。理解多少不知道,不过这本书确实写的很有意思。

昨天萌生了一个想法,把家里的那台老电脑24小时开机,当做服务器来用。本来想在路由上配个ddns,网上看了下,之前用的花生壳现在都实名了。并且说实在的花生壳之前用着也并不怎么好用。

想了想家里的宽带是公网ip,直接在路由上配置了端口映射好了。不过虽说是公网ip可不代表人家给的是静态ip,还是上游dhcp服务分配到的动态ip,于是写了个python脚本,执行后每5分钟爬取一下公网ip。ip变动后发邮件通知。

代码很简单,也可能是太蠢,就不贴了。

写完之后跑了下,美滋滋。

下午出门买了点菜,晚上吃的排骨,真好吃,额,跑题了。。

吃饱了饭还是不知足,想着把这个脚本做成服务,开机自启动。网上看了下,大部分还是init或者Upstart作为服务管理时的做法。

可从我开始用Arch的时候,Arch都已经采用了systemd了,况且刚装的lubuntu用的也是systemd,ubuntu17.04应该也是采用systemd了,虽然网上对systemd的争论很多,不过本人觉得还是很好用的,规范了很多东西。

《Linux就是这个范儿》里面有一小章,章节名叫配置乱生根,linux用的时间久了确实深有体会。Unix文化里的kiss原则带来的影响也是有好有坏的,个人觉得这种服务管理程序还是规范点好,毕竟是内核初始化后运行的第一个程序。

一般情况下所安装软件提供的service在/usr/lib/systemd/system下,lubuntu在/lib/systemd/system下。开机自启的服务单元其实就是在/etc/systemd/system下创建的同名软连接。

一个单元配置文件可以描述如下内容之一:系统服务(.service)、挂载点(.mount)、sockets(.sockets) 、系统设备(.device)、交换分区(.swap)、文件路径(.path)、启动目标(.target)、由 systemd 管理的计时器(.timer)。详情参阅 systemd.unit(5) 。

闲话不多说,直接动手写第一个服务单元。

cd /lib/systemd/system

vim ip.service

[Unit]
Description=Get wanIp daemon
After=network.target
Requires=pppd-dns.service

[Service]
Type=simple
ExecStart=/home/tql/Python/WanIp/bin/python /home/tql/Python/WanIp/ip.py

[Install]
WantedBy=multi-user.target

这里注意一点,ExecStart里要写绝对路径,对于python来说,pip所安装的包,直接执行命令不一定能找到。我用虚拟环境打包了整个脚本。

接着

sudo systemctl start ip.service 

开启服务 。

systemctl status ip.service 

查看服务运行正常。

systemctl enable ip.service 

在/etc/systemd/system/下创建软链,开机自启。

按说到这一步就好了,可世上还真没什么事是容易的。重启机器后,查看服务总是运行失败,报的错误像是网络错误。于是正如上面服务所写,我又是After=network.target, 又是Requires=pppd-dns.service,事实证明,这些其实是没什么用处的。

后来想起来之前看的一篇文章,systemd是并行启动。

Systemd 的并发启动原理
在 Systemd 中,所有的服务都并发启动,比如 Avahi、D-Bus、livirtd、X11、HAL 可以同时启动。乍一看,这似乎有点儿问题,比如 Avahi 需要 syslog 的服务,Avahi 和 syslog 同时启动,假设 Avahi 的启动比较快,所以 syslog 还没有准备好,可是 Avahi 又需要记录日志,这岂不是会出现问题?

Systemd 的开发人员仔细研究了服务之间相互依赖的本质问题,发现所谓依赖可以分为三个具体的类型,而每一个类型实际上都可以通过相应的技术解除依赖关系。

并发启动原理之一:解决 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 节点,或者是加密盘等,需要耗费较长的时间才可以准备就绪的情况下,因为并发启动,这段时间内,系统并不是完全无事可做,而是可以利用这段空余时间做更多的启动进程的事情,总的来说就缩短了系统启动时间。

总之,对于我的python脚本里所发起的网络服务来说,不是立即执行的,他会被内核缓存知道真正的网络通常后,才能发出,所以这就要求我的脚本有容错能力或者超时设置,按照此思路改了代码后,果然顺畅执行。

最后附上一些相关资料链接,包括服务单元格式,以及systemd更多的资料。

浅析 Linux 初始化 init 系统,第 3 部分: Systemd

[Systemd 入门教程:实战篇](http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial- part-two.html)

[Systemd 入门教程:命令篇](http://www.ruanyifeng.com/blog/2016/03/systemd-tutorial- commands.html)