在 Android 上折腾 Termux 的人,早晚会撞上同一个坎:脚本写好了,服务手动跑起来了,手机一锁屏、App 一划走,全没了。你要的不是"能跑",而是"服务自启动"——开机之后自己起来,被杀之后自己回来,锁屏之后照样在后台干活。这件事听起来简单,但 Termux 跑在 Android 这套进程管理机制之上,它没有 systemd、没有 init.d、没有 root 权限给你写/etc/rc.local,所以网上那些 Linux 服务器的自启套路在这里基本全废。我前后在几台设备上折腾过 Termux 的服务自启动,从最早的nohup硬顶,到termux-services,再到Termux:Boot配合唤醒锁,坑基本踩了个遍。这篇就把整套思路和可复制的操作流程完整摊开:服务自启动到底分几层、termux-services的 runit 机制怎么用、开机广播怎么接、锁屏后台怎么保命、以及服务起来又挂掉时该往哪儿查。不管你是想把 sshd、crond 常年挂着,还是想让自己写的 Python 常驻脚本开机就跑,看完都能直接抄。
1. 先弄明白 Termux 的服务到底"活"在哪
1.1 Android 的进程回收机制决定了你不能硬顶
很多人第一次配自启动,思路很朴素:写个while true循环,或者nohup python xxx.py &,然后以为万事大吉。结果第二天打开一看,进程没了。这不是你脚本写得不对,而是 Android 的进程管理和桌面 Linux 完全是两套逻辑。
Android 会按照进程的"重要性"分级回收内存。前台可见的 App 优先级最高,退到后台的、长时间不活跃的进程会被逐步降级,内存吃紧时直接杀掉。Termux 虽然是终端,但它在系统眼里仍然是一个普通 App,你里面跑的所有东西——包括那个nohup起来的 Python 进程——都挂在 Termux 进程组下面。父进程(也就是 Termux 本体)被系统干掉,子进程跟着一起走,nohup拦得住 SIGHUP,拦不住整个进程组被清理。
所以"服务自启动"这件事,在 Termux 里实际上要拆成三个独立的问题:第一,服务本身要有正确的生命周期管理,挂了能拉起来;第二,Termux 这个壳要能开机自动启动,并且把服务带起来;第三,Android 要允许 Termux 长期驻留后台,不把它当垃圾回收掉。三个问题缺一个,你的服务都活不长。很多人只解决了第二个(装了Termux:Boot),结果发现开机那一下确实执行了,过一会儿又没了,就是第三层没处理。
1.2 "自启动"的三种语义,别混在一起谈
在动手之前,先明确你要的到底是哪一种,因为不同语义对应完全不同的技术方案。
第一种是开机自启动:手机重启之后,不用手动打开 Termux,服务自己起来。这必须依赖Termux:Boot这个插件,因为只有它能接收到系统的开机完成广播。
第二种是服务进程守护:服务因为异常退出、内存不足被杀、或者代码报错崩溃之后,能自动重新拉起来。这个靠termux-services提供的 runit 机制,不用你自己写循环检测。
第三种是会话内手动管理:你打开 Termux 的时候能方便地start / stop / restart / 看状态,不用每次都去ps aux里捞 PID。这个同样由termux-services的sv命令族解决。
实际生产级的做法是这三种叠加:termux-services负责进程守护和生命周期管理,Termux:Boot负责开机触发,唤醒锁加电池白名单负责让系统别杀它。单用任何一个都只能解决三分之一的问题。
1.3 几种自启方案的真实对比
市面上的方案就那么几种,我把它们的适用边界列一下,省得你一个个试。
| 方案 | 原理 | 优点 | 明显短板 | 适用场景 |
|---|---|---|---|---|
nohup/&后台 | shell 后台任务 | 零依赖,写起来快 | 无守护,Termux 被杀就全没 | 临时测试,跑几分钟的活 |
| 自己写 while 循环守护 | 脚本内轮询检测 | 逻辑可控 | 浪费资源,容易写出僵尸进程 | 简单单进程场景 |
termux-services | runit 服务管理 | 标准化,有日志,有状态 | 需要重启会话生效,学习成本略高 | 长期驻留的守护服务 |
Termux:Boot | 接收开机广播 | 真正开机自启 | 只负责触发,不管守护 | 配合上面两者使用 |
termux-job-scheduler | 系统 JobScheduler | 系统级调度,省电 | 触发时机不精确,最长 15 分钟粒度 | 定时任务而非常驻服务 |
crond | cron 定时 | 经典,可控 | 需要 Termux 本体活着 | 周期性任务 |
看这张表就能明白,为什么我说termux-services是核心:它同时解决了守护和会话内管理两个问题,剩下的开机触发交给Termux:Boot就行。至于termux-job-scheduler,适合"每天早上八点跑一次数据同步"这种场景,不适合"我要一个长期监听端口的服务"。选错方案是新手最容易走的弯路。
2. 用 termux-services 把服务生命周期管起来
2.1 安装与目录结构,先看懂再动手
termux-services不是 Termux 自带的,需要单独装:
pkg update && pkg upgrade pkg install termux-services装完之后,最关键的一步是彻底重启 Termux 会话——不是开个新标签页,而是把 Termux 从最近任务里划掉,重新打开。原因是termux-services需要在登录 shell 的 profile 阶段注入SVDIR环境变量,指向服务目录。你不重启,sv命令能找到,但不知道去哪个目录找服务,会直接报服务不存在。
注入的环境变量是SVDIR=$PREFIX/var/service,也就是/data/data/com.termux/files/usr/var/service。这个目录就是所有服务的家。进去看一眼结构:
ls -l $PREFIX/var/service/你会看到每个服务一个子目录,目录名就是服务名。每个目录里至少有一个run脚本,这是 runit 启动服务时实际执行的入口。可选的还有:
finish:服务退出后执行的清理脚本,常用来记录退出码down:这个文件存在时,服务不会被自动启动(相当于"禁用自动拉起")log/run:日志服务的入口,配合svlogd做日志轮转
理解down文件很关键,因为sv-enable和sv-disable本质上就是在操作这个文件。sv-enable会删掉down并启动服务,sv-disable会创建down并停掉服务。明白了这一点,你就不会对"为什么我手动sv up起来了,重启又没了"感到困惑——因为down文件还在,runit 只负责把标记为自动启动的服务拉起来。
2.2 sv 命令族:日常管理就这几个
sv命令的参数顺序是"动作在前,服务名在后",这点和很多人直觉相反,第一次用容易写反。
sv status sshd # 查看状态 sv up sshd # 启动 sv down sshd # 停止 sv restart sshd # 重启 sv-enable sshd # 启用自动启动 + 立即启动 sv-disable sshd # 禁用自动启动 + 立即停止状态输出的含义值得单独说一下。正常的输出长这样:
run: sshd: (pid 12345) 3600srun:表示服务正在运行,后面是 PID 和已运行秒数。如果看到down:,说明服务被显式停了(通常是down文件存在)。看到fail:就说明服务反复启动失败,runit 会不断重试,这时候必须去看日志。还有一种是uninitialized,一般出现在服务目录里缺少run脚本,或者目录里有个多余的空文件干扰了 runit 的判断。
我自己的习惯是,配置阶段用sv-enable一次到位,调试阶段用sv up/sv down单独控制,避免反复改down文件。
2.3 把 sshd 和 crond 跑起来
这两个是最典型的常驻服务,步骤几乎一样。先说 sshd,它是远程登录服务,默认监听 8022 端口,务必先把依赖装上:
pkg install openssh pkg install termux-services # 重启 Termux 会话 sv-enable sshd sv status sshd第一件事是给当前用户设个密码,否则远程登录进不来:
passwd然后查一下设备在当前网络下的地址:
ifconfig | grep inet或者用whoami拿到用户名,远程连的时候是ssh 用户名@设备地址 -p 8022。注意 Termux 的 sshd 只支持密钥登录和密码登录二选一的管理方式,公钥要放到~/.ssh/authorized_keys,权限必须是600,~/.ssh目录是700,权限不对 sshd 会直接拒绝,这是最常见的"密码对但登不上"的原因。
crond 的安装稍微绕一点,它的服务定义来自cronie包:
pkg install cronie termux-services sv-enable crond sv status crond任务用经典的 crontab 语法写:
crontab -e写进去的内容比如每分钟往日志里追加一行:
* * * * * date >> $HOME/cron-test.log这里有个新手必踩的坑:cron 执行时的环境变量和你手动开 shell 完全不一样,PATH往往只包含极少数目录。所以 crontab 里的命令一律用绝对路径,或者在 crontab 开头显式声明PATH。我自己现在写 cron 任务,第一行永远是PATH=/data/data/com.termux/files/usr/bin:/data/data/com.termux/files/usr/bin/applets,少了这行,脚本在终端里跑得飞起,在 cron 里就报 command not found。
2.4 日志怎么看:排障的命门
服务起不来,90% 的答案在日志里。termux-services的日志目录在$PREFIX/var/log/sv/,每个服务一个子目录,进去之后current就是当前日志文件:
tail -f $PREFIX/var/log/sv/sshd/current注意-f是持续跟踪,排查启动问题时特别有用,你在另一个会话里sv restart,这边能实时看到输出。如果你的服务目录里没有log/run,那这个服务就没有独立日志,输出会混在 Termux 主输出里,基本没法看。所以动手写自定义服务时,我强烈建议顺手把日志服务也配上,后面你会感谢自己。
日志文件会由svlogd自动轮转,默认单个文件到一定大小就切,历史文件按序号保留。如果日志爆量,可以在log/run里给svlogd加参数控制保留数量,比如-b指定目录、-n控制保留份数。这些参数在svlogd的用法说明里都能查到。
2.5 手写一个自己的服务
现成的服务脚本不够用时,就得自己写。流程是:建目录、写run、加执行权限、启用。
mkdir -p $PREFIX/var/service/worker/log先写服务本体,$PREFIX/var/service/worker/run:
#!/data/data/com.termux/files/usr/bin/sh exec 2>&1 exec python $HOME/scripts/worker.py几个细节必须注意。shebang 一定要写 Termux 环境的绝对路径,写/bin/sh在 Android 上指向的是系统自带的受限 shell,环境完全不同,会出各种诡异问题。exec 2>&1是把标准错误合并到标准输出,这样错误信息才能被日志服务捕获。最后用exec执行真正的进程,这是 runit 的要求——它需要目标进程直接接管当前进程,这样才能准确追踪 PID。如果你写成python worker.py &后面再加别的命令,runit 会认为服务瞬间退出了,然后疯狂重启。
再配日志,$PREFIX/var/service/worker/log/run:
#!/data/data/com.termux/files/usr/bin/sh exec svlogd -tt $PREFIX/var/log/sv/worker加权限并启用:
chmod +x $PREFIX/var/service/worker/run chmod +x $PREFIX/var/service/worker/log/run mkdir -p $PREFIX/var/log/sv/worker sv-enable worker sv status workersvlogd的-tt参数是给每条日志加上人类可读的时间戳,排查问题时这个非常重要,不加的话日志里只有原始内容,你根本不知道哪条对应哪次崩溃。
3. Termux:Boot 打通开机自启这一环
3.1 装插件和关键的一次性授权
Termux:Boot是独立插件,不在主程序里。它的获取渠道和 Termux 本体保持一致,从官方渠道装,别去来路不明的地方下载 APK,插件和主程序版本不匹配会导致脚本根本不执行。
装完之后有一件极其容易被忽略的事:必须手动打开一次Termux:Boot这个 App。它需要至少被启动一次,系统才会把这个组件登记进开机广播的接收列表,同时它会在后台完成一次初始化。如果你装完就直接重启手机,大概率什么都不会发生,然后你会怀疑人生地折腾半天。
插件打开之后,主 Termux 里会出现一个新的约定目录:
mkdir -p ~/.termux/boot/这个目录就是开机脚本的投放点。系统开机完成广播到达时,插件会按文件名顺序依次执行这个目录下的所有可执行文件。注意是"所有",不是只执行某一个,所以你可以把不同任务拆成多个脚本,也可以就用一个总入口。
3.2 开机脚本的编写规范
开机脚本和普通脚本最大的区别是执行环境。它不会经过交互式登录 shell,PATH、PREFIX这些变量都不保证存在。所以模板要写得保守一点:
#!/data/data/com.termux/files/usr/bin/bash export PREFIX=/data/data/com.termux/files/usr export HOME=/data/data/com.termux/files/home export PATH=$PREFIX/bin:$PREFIX/bin/applets export SVDIR=$PREFIX/var/service termux-wake-lock sv-enable sshd sv-enable crond sv-enable worker每一行都有存在的理由。显式导出环境变量是因为开机环境干净得可怕;termux-wake-lock后面单独讲;用sv-enable而不是sv up,是因为开机时服务目录里的down状态才是权威的,sv-enable会确保清理掉禁用标记再启动,行为更可预期。
写完之后别忘了权限,这是最高频的"脚本不执行"原因:
chmod +x ~/.termux/boot/start-services.sh文件名建议用有意义的英文加序号前缀,比如10-start-services.sh、20-sync-config.sh,这样执行顺序一目了然。别用中文文件名,任何一层编码出问题都会导致脚本被跳过,而且不报错,非常难查。
3.3 唤醒锁和电池白名单,决定服务能活多久
前面说过,三层问题里的第三层——让 Android 别杀 Termux——才是真正决定稳定性的地方。这里两个动作必须做。
第一个是唤醒锁。Termux 提供了termux-wake-lock和termux-wake-unlock两个内置命令,前者申请一个 CPU 唤醒锁,让系统在设备进入低功耗状态时不要冻结持有锁的进程。这个命令不需要额外装插件,主程序自带。合理的用法是开机脚本里申请一次,服务正常运行期间就一直持有,不要频繁申请和释放。我见过有人在循环里反复调用,结果唤醒锁叠加出问题,反而更耗电。
第二个是关闭电池优化。这个必须手工在系统设置里操作:进入系统设置的电池或应用管理页面,找到 Termux 和 Termux:Boot,把省电策略设为"无限制"或加入"不受电池优化限制"的名单。不同厂商的路径不一样,有的藏在"应用启动管理"里,有的在"后台运行权限"里。但归根到底就一句话:让系统知道这两个 App 是需要长期在后台跑的。
注意:这两个动作缺一个,典型现象是"开机十分钟内一切正常,之后服务静默消失"。很多人这时候去翻日志,发现日志停在某个时间点,没有任何报错,就是这个原因。
3.4 Termux 换个观察角度:服务其实不用非在 Termux 里跑
这里插一个很多教程不讲、但实际很有价值的思路。如果你的服务最终要跑在容器化的 Linux 环境里(比如用proot-distro拉起来的发行版),那么在容器内部是不能用 Termux 的termux-services的——容器里根本没有 Termux 的环境,也没有 runit 的注入。这时候正确的分工是:把服务在 Termux 这一侧用sv-enable拉起,脚本内容里用proot-distro login 发行版名 -- 命令的方式进容器执行。也就是说,自启动的"锚点"永远在 Termux 上,容器只是个被拉起来的执行环境。理解这一点,你就不会白费力气去容器里写 init 脚本。
4. 从零走一遍完整的部署流程
4.1 环境准备清单
把前面散落的步骤串成一条完整流程。先确认基础环境:
pkg update pkg install termux-services openssh cronie python然后彻底重启 Termux。重启后验证环境变量生效:
echo $SVDIR输出应该是/data/data/com.termux/files/usr/var/service。如果这行是空的,说明重启不彻底,回去重做。
接着处理权限和密码,为 sshd 做准备:
passwd mkdir -p ~/.ssh && chmod 700 ~/.ssh如果你打算用密钥登录,把公钥内容写进~/.ssh/authorized_keys,然后chmod 600 ~/.ssh/authorized_keys。
4.2 服务脚本落地
建三个服务:sshd、crond、自定义 worker。前两个前面已经装好,直接启用。第三个按 2.5 节的模板写。
写 worker 脚本之前,先想清楚它的"退出语义"。这是写守护服务时最重要的设计决策:脚本什么时候应该退出?如果脚本执行完一批任务就正常退出,runit 会认为它"挂了",立刻重启,结果就是高频重启,日志刷屏,电池疯狂掉。正确的做法是让它长期驻留——典型写法是一个无限循环加阻塞等待,只有真正的异常才抛出。
#!/data/data/com.termux/files/usr/bin/sh exec 2>&1 exec python $HOME/scripts/worker.py对应的worker.py结构大致是:
import time def main(): while True: try: # 干活的逻辑 time.sleep(60) except Exception as e: print("worker error:", e, flush=True) time.sleep(5) if __name__ == "__main__": main()关键点是任何异常都必须在循环内部消化掉,不能让它冒泡导致进程退出。一旦退出,runit 秒级重启,循环几次之后你就开始排查"为什么我的服务一直重启",其实答案是自己没接住异常。另外print一定要加flush=True,否则输出会被 Python 缓冲住,日志里看不到实时内容,排查时会以为服务卡死了。
4.3 注册与启用
脚本就位后,注册到 runit:
chmod +x $PREFIX/var/service/worker/run chmod +x $PREFIX/var/service/worker/log/run sv-enable sshd sv-enable crond sv-enable worker逐个确认状态:
sv status sshd crond worker三条都应该是run:开头。看到fail:的,直接去看对应日志,不要盲目重启。
4.4 开机自启配置
写~/.termux/boot/10-services.sh,内容用 3.2 节的模板,加执行权限,然后打开一次Termux:Boot,再去系统设置里把电池优化关掉。
4.5 验证:分三层测,别一步到位
验证要分层,否则出问题你不知道是哪一层坏了。
第一层,会话内重启测试:sv restart worker,看状态能不能回到run:,日志有没有异常。
第二层,Termux 进程重启测试:把 Termux 从最近任务划掉,重新打开,sv status worker。如果这时候服务已经在跑,说明down文件处理正确。
第三层,真机重启测试:重启手机,不手动打开 Termux,等一两分钟,然后打开 Termux 看sv status。这一层才能验证Termux:Boot是否真的生效。注意刚开机那一两分钟系统很忙,唤醒锁和后台权限都在协商,服务可能会晚一点起来,别急着下结论,多等一会儿再判断。
5. 常见问题与排查速查
5.1 服务起来了但立刻挂掉,日志刷屏
现象是sv status显示run:但 PID 一直变,日志里全是重复内容。三个方向排查:一是脚本没有真正阻塞,执行完就退出了,检查是不是漏了exec或者主进程是前台快速返回的;二是权限问题,脚本引用的文件没有读权限,报错信息会出现在日志里;三是端口占用,比如 sshd 想监听 8022 但已经被别的进程占了,ss -tlnp能看出来。这一类问题必须靠日志,所以前面反复强调配log/run。
5.2 Termux:Boot 完全没反应
按发生概率从高到低排:脚本没有可执行权限(最常见);Termux:Boot装完之后从来没有手动打开过;脚本的文件名或换行符有问题,Windows 下编辑过的文件带 CRLF,Linux 的 shell 会因为 shebang 行末尾的\r找不到解释器,表现就是静默失败;脚本开头用了错误的 shebang 路径。检验方法很简单,先写一个只做一件事的脚本——往固定文件里追加一行时间戳,chmod +x之后重启手机看文件有没有内容。这一步能把问题范围从"整个自启动机制"缩小到"某个具体脚本"。
5.3 sv 命令报错或找不到服务
sv: command not found说明termux-services没装,或者装了没重启会话。sv: unable to change to service directory说明SVDIR没注入,同样重启解决。sv status返回uninitialized多半是服务目录里有干扰文件,runit 对目录内容比较敏感,用ls -la看一眼是不是混进了.DS_Store或者编辑器留下的临时文件。
5.4 非 root 设备上的边界认知
有个问题必须先说清楚:Termux 在非 root 设备上没有超级用户权限,也没有传统 Linux 的初始化系统。你去尝试su,得到的是明确的失败提示,设备上没有可用的超级用户程序。这意味着两件事:一是不要指望写系统级的 init 脚本,行不通;二是termux-services的 runit 就是你能拿到的最接近"服务管理器"的东西,把精力放在它上面。设备是否具备更高权限,决定了你能做哪些操作,这是前提条件,不是可以通过配置绕过的。
5.5 排查速查表
| 现象 | 最可能的原因 | 处理方式 |
|---|---|---|
| 服务频繁重启 | 脚本未阻塞,执行完就退出 | 加exec,主逻辑用无限循环 |
| 服务逻辑对但没输出 | 标准错误没合并 | run脚本首行加exec 2>&1 |
| 日志看不到内容 | 没配log/run或缓冲未刷新 | 配svlogd,Python 加flush=True |
| 开机不执行 | 脚本无执行权限 / 未打开过插件 | chmod +x,手动启动一次插件 |
| 执行到一半报错 | 开机环境变量缺失 | 脚本内显式导出PATH、PREFIX |
| 运行一段时间后消失 | 被系统省电策略清理 | 唤醒锁 + 电池优化白名单 |
| 手动能起,重启不能 | down文件仍存在 | 用sv-enable而不是sv up |
| cron 任务不执行 | cron 环境PATH不同 | crontab 内声明绝对PATH |
| sshd 登不上 | 密钥文件权限不对 | ~/.ssh700,authorized_keys600 |
6. 踩过坑之后总结的几条实操心得
6.1 关于"持久化"这件事的心理预期
我折腾这套东西最大的体会是:在 Android 上追求"绝对永久后台"是不现实的,你应该追求的是"尽可能长的存活时间 + 快速自愈能力"。系统版本、厂商定制、内存压力、温度控制,任何一个因素都可能让进程消失。所以正确的姿势是让自愈变快——服务挂了 runit 秒级拉起,Termux 被杀了下次使用时自动恢复,开机之后自动重新注册。别去追求绝对不挂,那是跟自己较劲。接受这一点之后,你的配置思路会从"如何防止被杀"转向"被杀之后多久能恢复",后者才是可实现的工程目标。
6.2 旧设备上的兼容性现实
手上如果有年份比较早的 Android 设备,要做个心理准备:系统版本偏低会限制 Termux 主程序和插件的可用版本,进而限制termux-services这类依赖较新环境的组件。这类设备上,nohup加手写守护循环反而是更务实的选择,虽然原始,但依赖最少、故障点最少。选方案要跟着设备能力走,而不是跟着网上的最新教程走。我自己的旧设备上至今还在用最朴素的while重启循环,因为它足够简单,简单就意味着出问题的可能性低。判断标准很简单:如果装上termux-services之后,重启会话仍然拿不到SVDIR,那就别硬磕,退回到脚本守护方案。
6.3 省电这件事,别用力过猛
唤醒锁是把双刃剑。持有唤醒锁确实能显著提升进程存活率,但代价是设备低功耗状态被打断,耗电会变高,机身温度也会上去。我的做法是:只给真正需要的服务持锁,日志类、定时类服务不加锁,让它们随缘执行;常驻的监听类服务才加。手机放一晚上,如果早上起来发现电量掉得离谱,第一个要检查的就是唤醒锁的持有情况——dumpsys power能列出当前持有的唤醒锁,虽然输出很长,但搜关键词还是能定位的。找到一个常年持有但实际没事干的锁,把它去掉,续航往往立刻回来。
6.4 配置改动要留版本痕迹
最后分享一个习惯。每次改服务脚本或者开机脚本之前,先复制一份带时间戳的备份:
cp ~/.termux/boot/10-services.sh ~/.termux/boot/10-services.sh.bak-$(date +%Y%m%d)听起来很琐碎,但自启动这类配置的特点是"改错了当场看不出来,重启之后才发现服务没了",然后你已经忘了上次改了什么。留一份备份,回滚成本从"重新查一遍教程"降到"复制回来重启"。我在一次把SVDIR那行删掉之后,排查了快一个小时才想起来是自己手滑,从那次之后就一直留着这个习惯。
另外,每次系统大版本更新之后,建议把整个自启动链路重新验证一遍——开机脚本执行、唤醒锁申请、电池白名单是否被重置。系统更新重置应用权限是常事,而这类重置通常不会给你任何提示,只会在某天你发现服务不在了才暴露出来。