详解systemd与init.d区别,选择更适合的开机方式
1. 启动机制的本质差异
1.1 init.d:线性执行的老派方式
init.d 是 SysV init 的实现,它把启动过程看作一条单行道:系统按固定顺序依次执行脚本,前一个没结束,后一个就只能等着。
它的核心逻辑藏在/etc/rc?.d/目录里。比如/etc/rc2.d/下的文件名S01gpio-init.sh中的S表示“start”,01是执行序号。系统会按数字从小到大,一个接一个地运行这些脚本。
每个脚本都是独立的黑盒,只认三个参数:start、stop、restart。它不关心别的服务是否已就绪,也不管自己依赖的资源有没有准备好。如果某个脚本卡住或失败,整个启动流程可能就停在那里,排查起来像在迷宫里找出口。
这种模式简单直接,但就像用算盘处理大数据——能用,但效率和可控性都有限。
1.2 systemd:并行协作的现代引擎
systemd 则彻底重构了这套逻辑。它不再是一条线,而是一张网。每个服务被定义为一个unit(单元),最常见的就是.service文件,存放在/etc/systemd/system/或/lib/systemd/system/。
关键在于,unit 文件里可以明确声明依赖关系。比如:
[Unit] Description=GPIO initialization After=multi-user.target Wants=network.target这段配置告诉 systemd:“这个服务必须在 multi-user.target 完成之后启动,并且希望 network.target 也一起运行”。systemd 会自动解析这张依赖图,把没有依赖关系的服务并行拉起,把有先后顺序的排好队。
更进一步,它自带日志中枢journalctl,所有服务的输出都统一归档;支持失败自动重启、健康检查(watchdog)、资源限制等高级特性。它不是在“执行脚本”,而是在“管理服务生命周期”。
1.3 为什么不能只说“新旧”?——兼容层的真实作用
很多人误以为 Armbian 等系统是“在 systemd 和 init.d 之间二选一”。事实恰恰相反:systemd 是唯一的主角,init.d 只是一个被兼容的配角。
当你在/etc/init.d/下放了一个脚本,并用update-rc.d gpio-init.sh defaults注册它时,systemd 并不会去调用传统的init程序。它会悄悄生成一个临时的 unit 文件,把你的脚本包装进去,然后像管理其他服务一样管理它。
你可以用这个命令验证:
systemctl status gpio-init.sh你会看到输出中明确写着Loaded: loaded (/etc/init.d/gpio-init.sh; generated)。那个generated就是关键——说明这不是原生 unit,而是 systemd 动态生成的兼容封装。
这意味着,即使你写的是 init.d 脚本,最终的启动顺序、超时控制、日志归属,全由 systemd 决定。你只是在用老语法,跑在新引擎上。
2. 实战对比:从编写到调试的全流程
2.1 编写一个 GPIO 初始化脚本
无论选择哪种方式,脚本的核心逻辑是一样的。我们以点亮一个 LED 为例,创建/usr/local/bin/gpio-init.sh:
#!/bin/bash # /usr/local/bin/gpio-init.sh # 导出并配置 GPIO 引脚 for pin in 6 7 8 9 10; do if [ ! -d "/sys/class/gpio/gpio${pin}" ]; then echo ${pin} > /sys/class/gpio/export sleep 0.1 fi done # 设置方向 echo "out" > /sys/class/gpio/gpio6/direction echo "in" > /sys/class/gpio/gpio7/direction echo "out" > /sys/class/gpio/gpio8/direction echo "out" > /sys/class/gpio/gpio9/direction echo "out" > /sys/class/gpio/gpio10/direction # 点亮 LED(假设 GPIO6 连接 LED) echo "1" > /sys/class/gpio/gpio6/value exit 0注意两点:一是脚本放在/usr/local/bin/(标准可执行路径),二是加了简单的存在性检查和延时,避免因内核初始化未完成而报错。
2.2 方式一:用 init.d 兼容模式
给脚本加执行权限:
sudo chmod +x /usr/local/bin/gpio-init.sh创建/etc/init.d/gpio-init(注意,这里不带.sh后缀,是惯例):
#!/bin/sh ### BEGIN INIT INFO # Provides: gpio-init # Required-Start: $local_fs $network # Required-Stop: $local_fs # Default-Start: 2 3 4 5 # Default-Stop: 0 1 6 # Short-Description: Initialize GPIO pins # Description: Set up GPIO for LED and sensors ### END INIT INFO case "$1" in start) echo "Starting GPIO initialization..." /usr/local/bin/gpio-init.sh ;; stop) echo "Stopping GPIO initialization..." # 可选:添加清理逻辑,如关闭 LED echo "0" > /sys/class/gpio/gpio6/value 2>/dev/null || true ;; restart|force-reload) $0 stop $0 start ;; *) echo "Usage: $0 {start|stop|restart}" >&2 exit 3 ;; esac注册并启用:
sudo update-rc.d gpio-init defaults sudo systemctl daemon-reload此时,systemctl list-unit-files | grep gpio会显示gpio-init.service,状态为enabled,但类型是generated。
2.3 方式二:用原生 systemd unit
创建/etc/systemd/system/gpio-init.service:
[Unit] Description=GPIO Initialization Service Documentation=https://github.com/armbian/gpio-init After=multi-user.target Wants=local-fs.target [Service] Type=oneshot ExecStart=/usr/local/bin/gpio-init.sh RemainAfterExit=yes StandardOutput=journal StandardError=journal SysMaxUse=50M [Install] WantedBy=multi-user.target关键字段说明:
Type=oneshot:表示这是一个只运行一次就退出的脚本。RemainAfterExit=yes:告诉 systemd,即使脚本退出了,服务状态仍视为“active”,这对初始化类服务至关重要。StandardOutput=journal:确保所有echo输出都能被journalctl捕获。
启用服务:
sudo systemctl daemon-reload sudo systemctl enable gpio-init.service2.4 调试与验证:两套工具,一套真相
验证 PID 1 是否为 systemd:
ps -p 1 -o comm= # 输出应为:systemd查看两个服务的状态:
# 查看 init.d 兼容服务 systemctl status gpio-init.service # 查看原生 systemd 服务 systemctl status gpio-init.service你会发现,前者显示generated,后者显示loaded (/etc/systemd/system/gpio-init.service; enabled)。
查看日志(这才是最大优势):
# 查看原生服务的完整日志 journalctl -u gpio-init.service -n 20 --no-pager # 查看所有启动阶段的日志 journalctl -b -u multi-user.target --no-pager如果你的脚本里有echo "LED initialized",这条信息会清晰地出现在 journal 日志里,而 init.d 模式下,它可能直接被丢弃或混在系统日志里难以查找。
3. 关键决策点:什么情况下该选哪一种?
3.1 优先选择 systemd unit 的三大场景
场景一:需要精确控制启动时机
比如你的设备需要等网络完全就绪、NTP 时间同步完成后再初始化传感器。init.d 脚本只能粗略地写Required-Start: $network,但无法指定“等 DHCP 获取 IP 后”。而 systemd 可以:
[Unit] After=network-online.target Wants=network-online.targetnetwork-online.target是一个专门设计的“网络真正可用”的信号点。
场景二:服务需要长期存活或自动恢复
如果你的脚本不只是初始化,还要持续监听 GPIO 中断或轮询状态,那么Type=simple或Type=forking配合Restart=on-failure就非常必要。init.d 对此无能为力,它只负责“启动”,不管“活着”。
场景三:团队协作与可维护性要求高
一个.service文件是自描述的:它清楚地列出了依赖、执行路径、重启策略、日志设置。新同事接手时,看一个文件就能理解全部。而 init.d 脚本需要同时看脚本本身、update-rc.d的注册记录、/etc/rc?.d/的软链接,信息是割裂的。
3.2 init.d 模式仍有价值的两种情况
情况一:快速移植遗留脚本
你手头有一个为 Ubuntu 14.04(纯 init.d)编写的复杂脚本,里面包含了大量start|stop|status的分支逻辑和状态文件管理。直接重写为 unit 文件成本过高。此时,利用 systemd 的兼容层是最务实的选择——先让它跑起来,再逐步重构。
情况二:极简嵌入式环境,追求最小化
在某些资源极度受限的 ARM 设备上,你可能禁用了部分 systemd 功能,或者使用了systemd-sysv-generator的轻量模式。此时,init.d 脚本因其零依赖、零额外开销的特性,反而成为最可靠的选择。
但请注意,这并非推荐,而是权衡下的妥协。
4. 常见陷阱与避坑指南
4.1 “脚本不执行”的五大元凶
陷阱一:权限与路径错误
- 错误:脚本放在
/root/下,或没有+x权限。 - 正解:脚本必须放在
PATH包含的目录(如/usr/local/bin/),且ls -l显示rwxr-xr-x。
陷阱二:环境变量缺失
- 错误:脚本里用了
$(pwd)或~,但在 systemd 的干净环境中,HOME和PATH都被重置。 - 正解:在 unit 文件中显式声明:
[Service] Environment="PATH=/usr/local/bin:/usr/bin:/bin" Environment="HOME=/root"
陷阱三:内核 GPIO 接口未就绪
- 错误:脚本在
multi-user.target之前就尝试写/sys/class/gpio/,但内核模块还没加载。 - 正解:添加
Wants=sys-devices-platform-gpiochip0.device或更通用的After=sysinit.target。
陷阱四:RemainAfterExit忘记设置
- 错误:
Type=oneshot但没设RemainAfterExit=yes,导致systemctl is-active gpio-init.service返回inactive,后续依赖服务无法启动。 - 正解:初始化类服务,几乎都需要这一行。
陷阱五:日志被静默丢弃
- 错误:脚本里
echo "debug",但journalctl里找不到。 - 正解:确认 unit 文件中有
StandardOutput=journal,并且脚本没有重定向>/dev/null。
4.2 一条命令,看清全局启动图谱
想一眼看懂整个系统的启动脉络?运行:
systemctl list-dependencies --all --reverse multi-user.target这个命令会反向列出所有“想要”或“需要”multi-user.target的服务,也就是所有在多用户模式下会被拉起的单元。它会清晰地展示出你的gpio-init.service是如何被multi-user.target所包含,以及它又依赖哪些基础目标。
这比翻/etc/rc2.d/下几十个Sxx文件直观得多。
5. 总结:面向未来的启动方式选择
5.1 核心结论
- 技术本质:Armbian 的启动管理器只有一个,就是 systemd。init.d 是它提供的一个向下兼容的翻译层,而非并列选项。
- 工程实践:对于新开发的启动任务,无条件优先选用 systemd unit 文件。它提供了 init.d 无法企及的可靠性、可观测性和可维护性。
- 演进路径:不要把 init.d 当作“简单”,而应视其为“过渡”。今天花 10 分钟写一个 unit 文件,能为你未来半年的调试省下数小时。
5.2 行动清单
- 新项目:直接创建
/etc/systemd/system/xxx.service,按本文模板填写。 - 老项目:用
systemctl status xxx.service检查当前状态,若为generated,则计划迁移。 - 调试必做:养成
journalctl -u xxx.service -f实时跟踪日志的习惯。 - 避免踩坑:永远检查
RemainAfterExit、Environment和After=三个字段。
启动脚本不是系统里的一个“小配件”,它是系统稳定性的第一道防线。选择 systemd,就是选择一个有日志、有依赖、有保障的未来。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。