命令加入rc.local没生效排查方法
背景
一个服务器需要加一条静态路由,于是使用route add加了一条静态路由,但是route add添加的路由重启会失效,就将命令加入到了/etc/rc.local这个脚本中。
开机重启的时候发现route add命令没生效,最后发现是一个命令调用的脚本进入死循环导致下面的命令都没法执行的原因;
原因
早期SysV init 系统中,/etc/rc.local 是在所有其他 init 脚本执行完毕后自动执行的一个脚本,为系统管理员提供了一个在系统启动时运行自定义命令的便捷方法。后面systemd 成为许多主流 Linux 发行版的默认 init 系统,其使用服务单位(unit files)来控制启动进程,而不是传统的 init 脚本。所以/etc/rc.local 不再自动开机执行。
但是为了向后兼容,许多基于 systemd 的系统提供了一个名为 rc-local.service 的特殊服务,它专门用于执行 /etc/rc.local 脚本。
这样,那些还依赖于 /etc/rc.local 的系统管理员或旧的应用程序仍然可以在 systemd 环境中运行它。
排查过程
服务器是ubuntu2004系统,采用的是systemd来作为init系统,所以查看了rc-local.service状态,是否是服务没启动,导致rc.local脚本没有开机执行。
查看服务,服务一直是启动中的状态
查看rc-local.service的Unit文件时如何实现,发现rc-local.service服务采用的启动类似时forking。
forking一般是用来托管守护进程,这里给rc.local脚本使用forking模式,应该是为了兼容性,因为rc.local脚本里面可以放一系列的命令、或者后台进程、守护进程等。
选择 forking 类型可以让 systemd 更好地处理这种情况,因为很多传统的守护进程在启动时都会 fork。
forking这种启动模式下:当父进程(由ExecStart启动的原始进程)退出,并且返回退出码0时,systemd会认为该服务已成功启动,也就是当"/etc/rc.local start"这个命令执行完成后,rc-local.service服务才会显示启动成功。
rc.local脚本特点:
-
脚本中的命令是按照顺序执行的,rc.local脚本通常以#!/bin/sh -e开始。这里的-e选项意味着如果任何命令返回非零退出状态(通常表示命令失败),则整个脚本会立即退出。
-
如果在/etc/rc.local中调用了一个进入死循环的外部脚本,那么这个脚本会持续运行,不会返回到rc.local,因此后面的命令也将不会执行。
解决方法:
使用 command || true 可以确保即使 command 执行失败(返回非零退出状态)也不会导致整个脚本退出。
command1 || true
command2 || true
command3 || true
如果一个命令可能需要很长时间才能完成,或者它需要持续运行(例如,守护进程),将其放入后台执行。这可以通过在命令后面添加 & 来实现
#!/bin/sh -e
/path/to/looping_script.sh &
command2
command3
这里的“ExecStart=/etc/rc.local start”不是守护进程,为什么也可以使用forking这种启动类型?
因为forking这种启动类型判断是否启动成功是判断ExecStart指定的命令是否退出,如果正常推出了就认为启动成功。
故此及时/etc/rc.local脚本并没有进行fork操作,但仍然正常退出(例如返回退出码0),systemd仍然会认为这个服务按预期启动了,即使实际上并没有任何后台进程在运行。