在一台 Android 手机里跑的 Ubuntu 上,我想让 SSH 服务跟着开机自动起来。 于是我敲下那句在服务器上敲了无数次的命令:
$ systemctl enable ssh
System has not been booted with systemd as init system (PID 1). Can't operate.
这台 Ubuntu 跑在 PRoot 里。PRoot 不是一个真正的容器——它更像一个 把系统调用翻译过去的用户态工具,没有内核隔离,也没有 PID 1。 既然没有 init 系统,就不会有人来读你的「开机自启清单」。
所以问题变成了:在一个没有 init 的环境里,「开机」这件事该挂在哪儿?
01把启动挂到「你进来」的那一刻
没有 init,但有别的东西一定会被执行:登录 shell 的启动脚本。
在 PRoot 这种环境里,你每次打开终端(或者通过 SSH 连进来),
~/.bashrc 都会被读取一遍。
把它当成一个「开机钩子」用就行了:
echo 'if ! pgrep -x "sshd" > /dev/null; then /usr/sbin/sshd -o Port=2222 -o UsePAM=no -o PermitRootLogin=yes -o PasswordAuthentication=yes & fi' >> ~/.bashrc
这行命令有点长,拆开看就三件事:
pgrep -x "sshd"—— 先问一句「sshd 已经在跑了吗」。 这是幂等性的来源:如果每次开终端都无条件启动一次, 第二次就会因为端口被占用而失败,屏幕上一串报错。- 只有没在跑的时候,才执行后面的启动命令。注意这里是
sshd而不是service ssh start——既然没有 init,就绕过它,直接叫二进制。 - 结尾的
&让 sshd 转到后台。少了它,你的终端会被 sshd 占住, 看起来像卡死了。
验证一下写进去了没有:
$ tail -n 3 ~/.bashrc
02这个方案的边界,得说清楚
它不是万能的。由于钩子挂在「登录 shell」上,启动时机取决于你什么时候进这个环境:
- 你打开 App 进入 Ubuntu → SSH 跟着起来,正常。
- 手机整个关机再开机 → 你得先手动打开 App 进一次 Ubuntu,SSH 才会起来。
也就是说,它解决的是「不用每次手敲那串长命令」,不是「真正意义上的开机自启」。 在 PRoot 这种没有内核支持的环境里,后者本来就做不到——不丢人,知道边界就行。
由此得到一个可以复用的判断:没有 init 系统时,把「启动」挂到任何一个必然发生的生命周期事件上—— 登录 shell、cron 的
@reboot、supervisor,或者干脆在你自己的主程序里守护。 不要试图安装一个 systemd,那是往错误的方向使劲。
03顺带说说同一类环境里的另一个坑:DISPLAY
在 WSL2 里跑图形程序(RViz、turtlesim)时,会遇到一模一样性质的问题:
Linux 侧的 GUI 程序需要一个 DISPLAY 变量,告诉它「把窗口画到哪台机器的屏幕上」。
这个变量的正确值,就是Windows 主机的 IP 加上 :0。
难点在于怎么把这个 IP 拿到——WSL2 每次重启,这个地址都会变。
最可靠的取法是默认网关,因为 WSL2 的默认网关就是 Windows 主机:
# 方法一:默认网关(最可靠) export DISPLAY=$(ip route | grep default | awk '{print $3}'):0 # 方法二:如果上面取到的是空,用 resolv.conf 的第二个 nameserver export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | sed -n '2p' | awk '{print $2}'):0
为什么强调「第二个」?因为 /etc/resolv.conf 里通常有两个以上的 nameserver,
而第一个往往是 DNS 服务器,不是 Windows 主机。
曾经有人(在我之前)套用网上的命令取了第一个,结果 DISPLAY 变成了
223.5.5.5:0——一个公网 DNS 的地址。程序当然连不上,但它不会告诉你「你填错 IP 了」,
只会沉默或者报一个看不懂的 X11 错误。
所以设置完一定要验证一眼:
$ echo $DISPLAY
172.27.192.1:0 # 对:私网地址 + :0
223.5.5.5:0 # 错:这是 DNS
:0 # 错:空变量
要让它每次开终端都自动生效,同样是往 .bashrc 里塞一行。
这里有个转义上的小麻烦——awk 里的 $3 会被 shell 提前展开,
所以写成外面双引号、里面转义的组合:
echo 'export DISPLAY=$(ip route | grep default | awk "{print \$3}"):0' >> ~/.bashrc
source ~/.bashrc
如果 WSL 走的是无线网卡、默认网关这条路取不到,也可以直接写死 Windows 的局域网 IP,
比如 export DISPLAY=192.168.1.5:0——
代价是 Windows 换网络之后要手动改。
04两个坑,一条共同的线
把这两件事放在一起看,它们是同一个问题:
Windows 侧和 Linux 侧对「环境」的理解不一样。 PRoot 里没有 init,所以没有「开机」这个概念可供挂钩; WSL2 里的网络是虚拟化的,所以主机地址需要推导而不是写死。
遇到这类问题的排查顺序,我现在会这么做:
- 先问这个环境里到底有什么——有 init 吗?网络是谁在管?IP 从哪来?
- 再找一个一定会发生的事件当挂载点(登录、重启、进程启动)。
- 最后把结果打印出来验证,不要假设它生效了。上面
echo $DISPLAY那三行对比,就是这一步的模板。
第三步尤其重要。这两个坑的共同点都是「命令执行成功了,但事情没成」——
systemctl 是明确报错的,算好运气;而 DISPLAY 填错了,
它一声不吭,你只会看到程序打不开窗口。
- 关于本文
- 整理自 2026 年 8 月的三段便签记录(PRoot 下的 sshd 自启、WSL2 取主机 IP、图形界面显示问题)。 环境各不相同,命令请按自己的发行版和网络情况调整。
- 2026.09.20 整理
- 把三段笔记合并时才发现它们的结构是一样的: 都是「在不标准的环境里,找一个标准事件当钩子」。这条线比单个命令更值得留下。
留言
正在载入留言…