在一台 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 都会被读取一遍。

把它当成一个「开机钩子」用就行了:

写入 ~/.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 主机:

取 IP 并设置
# 方法一:默认网关(最可靠)
    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 里的网络是虚拟化的,所以主机地址需要推导而不是写死。

遇到这类问题的排查顺序,我现在会这么做:

  1. 先问这个环境里到底有什么——有 init 吗?网络是谁在管?IP 从哪来?
  2. 再找一个一定会发生的事件当挂载点(登录、重启、进程启动)。
  3. 最后把结果打印出来验证,不要假设它生效了。上面 echo $DISPLAY 那三行对比,就是这一步的模板。

第三步尤其重要。这两个坑的共同点都是「命令执行成功了,但事情没成」—— systemctl 是明确报错的,算好运气;而 DISPLAY 填错了, 它一声不吭,你只会看到程序打不开窗口。

关于本文
整理自 2026 年 8 月的三段便签记录(PRoot 下的 sshd 自启、WSL2 取主机 IP、图形界面显示问题)。 环境各不相同,命令请按自己的发行版和网络情况调整。
2026.09.20 整理
把三段笔记合并时才发现它们的结构是一样的: 都是「在不标准的环境里,找一个标准事件当钩子」。这条线比单个命令更值得留下。