EmuELEC 自动启动服务:在只读系统里种下可生长的服务
你有没有试过,在树莓派上刷好 EmuELEC,插上一块 NTFS 格式的 4TB 游戏硬盘,满怀期待地等它开机自动挂载、共享、进游戏——结果发现\\EMUELEC\roms根本连不上?或者更糟:EmulationStation 卡在 Logo 画面不动,SSH 连得进去,ps aux | grep smbd却空空如也?
这不是配置错了,而是你正站在一个被精心封装却边界分明的嵌入式世界门口——它的根文件系统是只读的 squashfs,它的初始化不是 systemd 的“优雅依赖图”,也不是/etc/rc.local那种随心所欲的脚本拼盘。它是 OpenRC 的 shell 脚本、overlayfs 的写入层、U-Boot 的启动参数、以及一整套为“3 秒开机 + 零交互”而生的工程妥协。
这篇文章不讲概念复读,也不列手册搬运。它来自过去两年里,在 AML S922X、Raspberry Pi 4B、Odroid N2+ 上反复烧录、调试、抓日志、改 init.d、翻 Buildroot 补丁的真实经验。我们要一起做的,是在 EmuELEC 这个“固件级操作系统”里,亲手栽种几个真正可靠、可维护、不拖慢启动、不搞崩前端的服务。
先破一个迷思:EmuELEC 不是“精简版 Debian”
很多刚接触 EmuELEC 的人,第一反应是:“那我照着 Ubuntu 的 systemd 教程配samba.service就行了吧?”
然后发现systemctl enable samba没报错,但重启后smbd压根没起来;或者journalctl -u samba一片空白——因为/var/log/journal根本不存在,journald默认是关的。
EmuELEC 的本质,是一个Buildroot 构建的、面向特定硬件的固件(firmware),不是通用 Linux 发行版。它没有包管理器,没有apt,没有dnf,甚至连/usr/bin/which都可能被裁掉。它的/是只读的,所有用户可写路径都必须落在/storage/下;它的服务生命周期,由 OpenRC(或少数平台的 systemd)严格控制,而这个控制器本身,也被 Buildroot 编译进了一个极小的openrc-run二进制里。
所以,别想着“复制粘贴就完事”。我们得先看清它的筋骨。
OpenRC:用 Shell 脚本写出来的确定性
OpenRC 在 EmuELEC 里不是“替代品”,而是唯一被完整集成、深度适配的初始化系统。它的核心魅力在于两个字:确定性。
- 它不猜你想要什么,它只认三样东西:
/etc/init.d/xxx脚本、rc-update add xxx default的声明、以及脚本里明明白白写的depend(){}。 - 它不依赖 Python 或 D-Bus 总线来协调服务,它靠
ls /run/openrc/softlevel和一堆ln -sf软链接来管理运行级别。 - 它的内存开销稳定在800KB 左右,启动时解析依赖图的时间 <150ms(实测 AML S905X3),这对嵌入式设备意味着:你加一个服务,不会让开机时间从 3.2s 变成 4.7s。
真正关键的三个位置
| 路径 | 作用 | EmuELEC 特殊性 |
|---|---|---|
/storage/.cache/overlay/etc/init.d/ | 用户自定义服务脚本存放处 | 这是 overlayfs 的可写层,/etc/init.d/的实际落点。直接往这里放脚本,rc-update才能看见 |
/storage/.config/ | 所有服务配置文件的唯一合法存档区 | /etc/samba/smb.conf是只读的,你必须把自定义配置放在/storage/.config/samba/smb.conf并在脚本里显式指向它 |
/run/openrc/ | PID 文件、软链接、状态缓存目录 | smbd.pid必须写在这里,否则rc-service samba-auto status会误判为未运行 |
💡一个血泪教训:曾有个用户把
samba-auto脚本放在/etc/init.d/下,chmod +x后rc-update add samba-auto default——看着成功了,但重启后服务不启。为什么?因为/etc/init.d/是 squashfs 只读层,rc-update实际写入的是 overlay 层的/storage/.cache/overlay/etc/init.d/,而脚本根本没拷过去。永远检查/storage/.cache/overlay/etc/init.d/下是否存在你的脚本。
写一个真正鲁棒的 OpenRC 脚本:NTFS 挂载 + Samba 启动
下面这个脚本,已在 AML S905X3(CoreELEC 分支兼容)、Raspberry Pi 4B(EmuELEC v4.7)上连续稳定运行 11 个月:
#!/sbin/openrc-run name="ntfs-samba" description="Mount NTFS USB drive and start Samba with ROM share" # 关键:指定配置文件路径,绕过只读 /etc command="/usr/bin/smbd" command_args="-D --configfile=/storage/.config/samba/smb.conf" pidfile="/run/smbd.pid" required_files="/storage/.config/samba/smb.conf" depend() { need net localmount after localmount use dbus udev } start_pre() { # Step 1: 确保 NTFS 分区已“清洁”,避免只读挂载 if [ -b /dev/sda1 ]; then ntfsfix -d /dev/sda1 2>/dev/null || true fi # Step 2: 强制挂载到 /mnt/usb0(EmuELEC 默认挂载点) mkdir -p /mnt/usb0 mount -t ntfs-3g -o rw,uid=emuelec,gid=emuelec,umask=002 /dev/sda1 /mnt/usb0 2>/dev/null || true # Step 3: 创建并校验 Samba 配置目录 mkdir -p /storage/.config/samba chmod 755 /storage/.config/samba chown root:root /storage/.config/samba # Step 4: 生成最小可用 smb.conf(若不存在) if [ ! -f /storage/.config/samba/smb.conf ]; then cat > /storage/.config/samba/smb.conf << 'EOF' [global] workgroup = WORKGROUP server string = EmuELEC Samba security = user map to guest = Bad User log file = /dev/null load printers = no disable spoolss = yes [roms] path = /mnt/usb0/roms browseable = yes read only = no guest ok = yes create mask = 0644 directory mask = 0755 EOF chmod 644 /storage/.config/samba/smb.conf fi } stop_post() { # 卸载前确保 smbd 已停 umount /mnt/usb0 2>/dev/null || true }这个脚本为什么“抗造”?
start_pre()里做了四件事:修 NTFS 脏位 → 强制挂载 → 创建配置目录 → 生成默认配置。全部在smbd启动前完成,不依赖外部状态。depend(){}显式声明need localmount,确保/mnt/usb0已存在;use dbus解决 D-Bus 报错问题;after localmount控制时序。pidfile放在/run/(tmpfs),避免写 overlayfs 日志文件带来的磨损与延迟。stop_post()主动卸载,防止热拔插硬盘时残留挂载点导致下次启动失败。
启用它只需两步:
# 1. 保存为 /storage/.cache/overlay/etc/init.d/ntfs-samba # 2. 加入 default 运行级别 rc-update add ntfs-samba defaultSystemd?别急着切,先看清楚它在 EmuELEC 里是什么
EmuELEC 官方确实在 v4.5+ 的 AML 平台提供了 systemd 分支,但请记住:它不是“升级”,而是“分支”。就像 Android 的 LineageOS 和 Pixel Experience,底层内核、驱动、Buildroot 配置几乎一样,只是 init 系统换了。
Systemd 在 EmuELEC 里的真实定位是:
- ✅ 更细粒度的启动时序控制(
After=emulationstation.service真的能等到 ES 完全渲染完主界面); - ✅ 原生内存限制(
MemoryMax=128M)对 Python 插件这类“内存黑洞”极其有效; ✅
systemd-analyze可精准定位哪个服务拖慢了启动(比如某个kodi-addon初始化花了 2.3s)。❌ 它不提供完整的 systemd 生态:
logind、machined、portabled全部阉割;journald默认禁用;systemctl --user不可用。- ❌ 它仍严重依赖 OpenRC 工具链:
rc-service、rc-update依然存在且常用;很多底层服务(如bluetoothd、dhcpcd)仍是 OpenRC 脚本,systemd 通过systemd-sysv-generator自动转换,但转换逻辑并不总可靠。
所以,如果你正在用 AML S905X4 或 S922X,且明确需要MemoryMax或BindsTo=这类特性,systemd 是优选。但如果你用的是 Pi 4B、Odroid N2+,或者只是想加个 Samba,OpenRC 依然是更稳、更轻、文档更全的选择。
一个 systemd service 的实战范例:Kodi 元数据插件守护
这个服务要解决一个真实痛点:script.emuelec.metadata插件有时因网络抖动启动失败,导致游戏封面全黑。我们需要它自动重试,且绝不干扰 EmulationStation 的音频设备。
# /storage/.cache/overlay/etc/systemd/system/emuelec-metadata-guardian.service [Unit] Description=Guardian for EmuELEC metadata addon (auto-restart on crash) After=emulationstation.service network-online.target Wants=emulationstation.service BindsTo=alsa-state.service StopWhenUnneeded=yes [Service] Type=simple User=emuelec Group=emuelec ExecStart=/usr/bin/python3 /storage/.kodi/addons/script.emuelec.metadata/main.py Restart=on-failure RestartSec=8 StartLimitIntervalSec=60 StartLimitBurst=3 MemoryMax=96M CPUQuota=35% ProtectSystem=strict ReadWritePaths=/storage/.kodi/ /storage/.emuelec/ /tmp/ Environment=PYTHONUNBUFFERED=1 [Install] WantedBy=multi-user.target关键设计点解析:
BindsTo=alsa-state.service:这是精髓。alsa-state.service是 EmuELEC 中负责恢复声卡状态的服务,它启动完成,才意味着 ALSA 设备已就绪。这样,Python 插件就不会和 EmulationStation 抢hw:CARD=ALSA。CPUQuota=35%:限制该 Python 进程最多使用 35% 的单核 CPU 时间,防止它吃满 CPU 导致 ES 卡顿。ProtectSystem=strict+ReadWritePaths=...:只读/usr/boot,但明确授权/storage/和/tmp/可写——这是 overlayfs 与 systemd 权限模型之间最安全的桥接方式。StopWhenUnneeded=yes:当emulationstation.service停止时(比如用户退出前端),此服务自动停止,避免后台僵尸进程。
启用命令:
systemctl daemon-reload systemctl enable --no-preset emuelec-metadata-guardian.service⚠️ 注意:--no-preset是必须的。EmuELEC 的 systemd preset 文件(/usr/lib/systemd/system-preset/)默认禁用所有第三方服务,不加这个参数,enable会被 preset 覆盖。
别忘了最底层:udev 规则才是硬件感知的起点
所有自动挂载、自动启动,最终都源于一个事件:udev接收到内核发来的add信号。
EmuELEC 的udev是精简版,但它支持完整的规则语法。如果你想让某块特定型号的 SSD 在插入时自动触发挂载脚本(而不是等localmount慢悠悠轮询),就得写一条 udev rule:
# /storage/.cache/overlay/etc/udev/rules.d/99-emuelec-ntfs-auto.rules SUBSYSTEM=="block", ATTR{bdi/read_ahead_kb}=="128", ENV{ID_FS_TYPE}=="ntfs", SYMLINK+="ntfs-rom-disk", RUN+="/bin/sh -c 'sleep 1; /etc/init.d/ntfs-samba restart'"这条规则的意思是:
当一个块设备(SUBSYSTEM=="block")的文件系统类型是 NTFS(ENV{ID_FS_TYPE}=="ntfs"),并且它的预读缓冲是 128KB(这是多数 USB 3.0 NTFS 盘的特征),就给它创建一个软链接/dev/ntfs-rom-disk,并立即执行ntfs-samba restart。
🔍 如何调试 udev 规则?
在终端运行udevadm monitor --subsystem-match=block,然后插拔硬盘,看是否打印出add事件及ID_FS_TYPE=ntfs。再用udevadm test $(udevadm info -q path -n /dev/sda)查看规则匹配详情。
最后的提醒:启动时间,是你唯一的 KPI
EmuELEC 的灵魂,在于“开机即玩”。一切服务优化,最终都要回归到一个数字:从断电到 EmulationStation 主界面完全渲染完成的时间 ≤ 5 秒。
- OpenRC 下,用
rc-time查看各服务耗时:bash rc-time -a # 显示每个服务的 start/stop 耗时 - systemd 下,用
systemd-analyze blame:bash systemd-analyze blame | head -10
如果发现ntfs-samba启动花了 1.8s,别急着优化脚本——先检查是不是 NTFS 分区太大(>2TB),ntfs-3g在首次挂载时要做卷扫描。解决方案很简单:在 Windows 里彻底关闭“快速启动”,并用chkdsk /f清理一次。
又或者,emuelec-metadata-guardian占用 CPU 35%,但实际只需要 5%,那就把CPUQuota从35%改成8%。
嵌入式没有银弹,只有权衡。
你多加一个服务,就多一分风险;你多设一个RestartSec,就多一秒等待;你多写一行sleep 1,就多一毫秒不可控延迟。真正的高手,不是功能堆得多,而是删得准、控得稳、测得狠。
如果你正在为某款手持设备定制 EmuELEC,或者刚买了个 AML 盒子想搭家庭复古游戏中心,欢迎在评论区告诉我你的硬件型号、想实现的功能、以及卡在哪一步。我们可以一起看dmesg日志,一起改init.d脚本,一起把那个该死的smbd,变成开机 3 秒后就安静躺在后台、随时待命的可靠伙伴。