screen不是“后台运行”,而是让终端会话真正活下来
你有没有过这样的经历:
在树莓派上跑一个 12 小时的 PWM 占空比采集脚本,刚合上笔记本去开会,回来发现 SSH 断了、进程没了、日志只到第 37 分钟;
用minicom调试串口设备,Wi-Fi 切换瞬间连接中断,Ctrl+A, A按了八百遍也救不回那个正在打印寄存器值的窗口;
客户现场一台工业网关卡在固件升级的 CRC 校验阶段,你远程连不上——不是程序崩了,是你的 SSH 会话被路由器踢掉了。
这不是代码的问题,是终端模型本身的脆弱性。而screen,就是那个默默扛住所有网络抖动、休眠唤醒、意外关闭的“终端保险丝”。
它为什么能扛住断连?不是靠nohup,而是重写了终端的契约
很多人以为screen就是nohup + &的高级版,其实完全不是。nohup只是屏蔽SIGHUP,但它不接管终端 I/O——一旦 SSH 断开,伪终端(PTY)从端会被内核自动关闭,子进程读写/dev/tty就会失败,很多交互式程序(比如gdb、vim、minicom)直接崩溃或卡死。
screen做了一件更根本的事:它把自己变成了终端的“中间人”。
当你执行screen -S pwm-log,实际发生的是:
screen进程 fork 出一个子进程,并为它分配一对新的主/从 PTY(比如/dev/pts/5是从端,screen持有主端);- 子进程的 stdin/stdout/stderr 全部重定向到这个从端 PTY;
screen主进程则通过select()持续监听:键盘输入、主 PTY 可读、网络 socket(用于-r)、甚至窗口尺寸变化;- 所有字节流都在
screen内存里中转——它知道哪段是ESC[2J(清屏),哪段是^M(回车),哪段是Ctrl+A, [(进入复制模式); - 当你按
Ctrl+A, D,screen并不 kill 子进程,而是调用kill(-pid, SIGSTOP)暂停它,同时保持主/从 PTY 文件描述符打开——子进程状态变成T(stopped),但环境变量、工作目录、打开的文件句柄、甚至read()中的阻塞状态,全部冻结保存; - 重连时,
screen恢复主 PTY 的读写能力,再发SIGCONT,进程就从断点继续跑,就像什么都没发生过。
✅ 关键洞察:
screen的持久性,来自对PTY 生命周期的接管,而非对信号的屏蔽。它让“终端”不再绑定于一次 TCP 连接,而成为一个可迁移、可挂起、可审计的用户态会话实体。
工程师该记住的 5 个命令,不是全部,但足够救命
别被man screen里 80 多个快捷键吓退。嵌入式现场不需要花哨,只需要这五个动作稳、准、快:
| 场景 | 命令 | 说明 | 避坑提示 |
|---|---|---|---|
| 启动一个不会丢的会话 | screen -dmS log-sensor | -d表示 detached(分离),-m强制新建,-S命名——三者缺一不可。没-d,脚本会卡住等你按Ctrl+A, D | 如果只想前台启动先试试,用screen -S test,进去了再Ctrl+A, D |
| 看还有哪些会话活着 | screen -ls | 输出类似There is a screen on: 12345.log-sensor (Detached)。注意括号里的状态:Detached=可重连,Dead!=真死了,是screen主进程挂了 | 在 BusyBox 环境下可能显示No Sockets found in /var/run/screen.——检查/var/run/screen目录权限和是否存在 |
| 立刻回到那个正在跑的窗口 | screen -r log-sensor | 名称匹配优先级高于 PID,推荐始终用-S命名,避免screen -r 12345这种靠猜的方式 | 如果提示There is a screen on ... but it is not responding,加-D -R强制解离并重连:screen -D -R log-sensor |
| 在会话里新开一个窗口跑别的事 | Ctrl+A, C | C= create,新窗口默认运行$SHELL,环境变量继承自screen启动时的 shell | 新窗口编号从 0 开始递增,Ctrl+A, 0~Ctrl+A, 9快速切换 |
| 把当前窗口的所有输出存成日志 | Ctrl+A, H | 切换日志开关,日志默认写入screenlog.0(当前窗口编号) | 日志是纯文本追加,不带颜色和控制字符,适合grep和awk处理;想带颜色?用script -c "your_cmd" /tmp/out.log |
💡 小技巧:把
Ctrl+A改成Ctrl+B(更顺手),只需在~/.screenrc加一行:escape ^Bb
真正的嵌入式难点:不是怎么用,而是怎么让它“不死”
在工控机、DTU、车载 T-Box 上部署screen,最大的挑战从来不是语法,而是让它融入系统生命周期,而不是成为又一个需要手动维护的孤儿进程。
▶ 场景:设备重启后,pwm-monitor会话必须自动拉起
你不能指望运维每次 reboot 后 ssh 进去敲一遍screen -dmS ...。得交给systemd:
# /etc/systemd/system/pwm-monitor.service [Unit] Description=PWM Monitoring Session via screen After=multi-user.target [Service] Type=forking User=appuser WorkingDirectory=/tmp # 关键:screen -dmS 会 fork 两次,systemd 需要跟踪第一个 fork 后的 PID PIDFile=/var/run/screen/pwm-monitor.pid ExecStart=/usr/bin/screen -dmS pwm-monitor -L -Logfile /tmp/pwm.log -h 1000 /bin/bash -c 'while true; do echo "$(date -Iseconds),$(cat /sys/class/pwm/pwmchip0/pwm0/duty_cycle)"; sleep 1; done' Restart=on-failure RestartSec=10 # 防止日志撑爆 eMMC LimitFSIZE=10M [Install] WantedBy=multi-user.target启用它:
systemctl daemon-reload systemctl enable pwm-monitor.service systemctl start pwm-monitor.service✅ 效果:只要systemd活着,screen会话就活着;即使screen主进程异常退出,Restart=on-failure会拉起新实例;日志大小被硬限制,SD 卡寿命多撑半年。
▶ 场景:多个工程师要同时看同一串口调试输出
screen原生支持多用户会话共享,但默认关闭。安全起见,我们不用multiuser on,而是用更轻量的方式:
- 创建一个专用组
serialgrp,把所有调试人员加入; - 把串口设备权限设为
crw-rw---- 1 root serialgrp /dev/ttyS0; - 启动共享会话:
bash # 由主工程师执行 screen -S debug-uart -L -Logfile /tmp/uart.log /usr/bin/minicom -D /dev/ttyS0 - 其他工程师直接
screen -r debug-uart——他们看到的是同一个 minicom 实例的实时画面,不是各自独立的副本。
🔒 注意:不要用
sudo screen或root运行,minicom需要访问串口,但screen本身不需要特权。权限应落在设备节点和用户组上。
和tmux、nohup的本质区别:别选错工具
| 维度 | nohup + & | tmux | screen |
|---|---|---|---|
| 核心目标 | 让单个命令免于SIGHUP | 提供现代终端工作流(pane/window/session) | 提供最简、最稳、最兼容的会话抽象层 |
| 依赖 | 无 | 依赖libevent、libutf8proc,常需动态链接 | 静态链接即可,musl/uClibc 全支持,OpenWrt 默认预装 |
| 资源占用 | 极低(只是 shell 内置) | 中等(约 2–5MB RSS) | 极低(< 1MB RSS,BusyBox 环境下实测 320KB) |
| 恢复可靠性 | ❌ 进程活着,但终端 I/O 失效,vim/gdb常卡死 | ✅ 完整支持 | ✅ 完整支持,且历史更久、边界 case 更成熟 |
| 嵌入式适配成本 | 低,但功能残缺 | 高(需交叉编译依赖库,Yocto 层需额外IMAGE_INSTALL_append += " tmux") | 极低(Buildroot/Yocto/OpenWrt 均原生支持screen包) |
🧠 一句话决策树:
- 要快速守护一个脚本?用nohup;
- 在开发机上写代码、分屏查文档、切项目?用tmux;
- 在客户现场那台内存 128MB、内核 3.10、没有包管理器的 ARM 设备上做远程固件升级?必须用screen。
最后一条实战经验:日志不是为了“看”,是为了“算”
很多工程师开启screen -L只是为了事后翻记录。但在嵌入式场景,日志真正的价值在于可编程分析。
比如你的screenlog.0里有这样一段:
[2024-05-22 14:23:18] CRC verification passed. [2024-05-22 14:23:19] Booting DSP firmware... [2024-05-22 14:23:22] ERROR: I2C timeout on addr 0x48你可以写个极简 parser(Python 或 AWK)自动告警:
# 检查最近 100 行是否有 ERROR tail -n 100 /tmp/upgrade.log | grep -q "ERROR:" && echo "🚨 I2C failure detected!" | logger -t dsp-upgrade或者用awk统计各阶段耗时:
# upgrade-time.awk /verification/ { t1 = systime(); next } /Booting/ { t2 = systime(); next } /timeout/ { print "CRC→Boot:", t2-t1, "s; Boot→Fail:", systime()-t2, "s" }✅ 这才是
screen日志的工程闭环:不是人工翻页排查,而是机器可读、可触发、可集成进 CI/CD 流水线的数据源。
如果你在树莓派上跑screen -S audio-test正在录 ALSA 音频,手机突然断网,别慌——screen -ls看一眼,screen -r audio-test接回去,Ctrl+A, [进入复制模式,Ctrl+A, ]粘贴最后 200 行发给同事,Ctrl+A, H打开日志,Ctrl+A, :hardcopy导出当前屏幕快照……
这一切,都不依赖于你的本地终端是否还连着,不依赖于你的笔记本有没有合盖,甚至不依赖于你此刻用的是 Termux、iSH 还是 Windows 的 MobaXterm。
因为screen把“会话”从“一次连接”升华为“一个对象”。
它不炫技,不堆功能,不改哲学——它只是坚定地,让终端活下来。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。