阅读时间:10 minutes

您发起`./gradlew build`, 终端被阻塞 3 分钟,您需要在 在等待 做其他事情吗?不需要打开新标签页。`Ctrl+Z`暂停进程,`fg`它会把你确切地带回你离开的地方。这是开发者日常中最被低估的Unix技巧。

问题:一个繁忙的终端

每个开发者都遇到过这种情况:

$ ./gradlew build
> Building...

终端已被锁定。您无法再输入任何命令。您需要查看一个文件,启动一个`git status`, 或者检查一个被占用的端口。

@startuml
skinparam backgroundColor #FEFEFE

actor Développeur as dev
participant "终端" as term
participant "Gradle\n( 进程 )" as gradle

dev -> term : ./gradlew 构建
term -> gradle : 启动构建
gradle -> term : 占用终端\n(无提示)
dev -> term : 想输入一个命令
term --> dev : ❌ 终端被阻塞

note right of dev
开发者在整个构建期间被瘫痪
end note
@enduml

大多数开发者会打开一个新终端。但有一种更优雅、更快速且原生的解决方案:作业管理。

必不可少的三个命令

Ctrl+Z — 暂停 (SIGTSTP)

`Ctrl+Z`发送信号`SIGTSTP`到前台进程。 该进程是已暂停— 未被杀死 — 终端恢复了提示符。

$ ./gradlew build
> Building...
^Z
[1]+  Stoppé    ./gradlew build
$

括号中的数字`[1]`是工作编号. Le `+`表示默认作业。

fg — 切换到前台

`fg`将暂停的作业调到前台。进程正好在它停止的地方继续执行。

$ fg
./gradlew build
> Building... (reprend où il en était)

使用特定的作业编号:

$ fg %2

bg — 在后台恢复

`bg`恢复进程 sans 不阻塞终端。 适用于长时间的构建。

$ bg
[1]+ ./gradlew build &
$

Le `&`在最后表示进程在后台运行。

jobs — 列出 shell 作业

$ jobs
[1]-  Stoppé    vim README.md
[2]+  En cours   ./gradlew build
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

state "前台\n(终端被阻塞)" as fg #LightCoral
state "已挂起\n(Ctrl+Z)" as stopped #LightYellow
state "后台\n(bg)" as bg_ #LightGreen

[*] --> fg : Lance une commande
fg --> stopped : Ctrl+Z\n(SIGTSTP)
stopped --> fg : fg\n重新置于前景
stopped --> bg_ : bg\n在后台恢复
bg_ --> fg : fg\n将其拉到前景
fg --> [*] : 命令已完成

note right of stopped
进程已冻结
但 _未_ 被杀
end note

note left of bg_
终端空闲
构建继续
end note
@enduml

情景 1:冗长的构建

这是最常见的使用情况。您正在运行 Gradle 构建,终端被阻塞,您需要去做其他事情。

@startuml
skinparam backgroundColor #FEFEFE
autonumber

actor Développeur as dev
participant "终端" as term
participant "Gradle" as gradle
database "状态\n进程" as state

dev -> term : ./gradlew 构建
term -> gradle : 启动构建
gradle -> state : 进行中 (FG)
dev -> term : Ctrl+Z
term -> state : 挂起(作业[1])
term --> dev : 自由提示 $

dev -> term : git 状态
term --> dev : 工作区干净

dev -> term : fg
term -> state : 重新(FG)
gradle --> dev : 构建成功 ✓

note right of state
进程 Gradle 从未被终止,它精确地恢复到之前停止的位置。
end note
@enduml

一步一步

$ ./gradlew build
> Building...
^Z
[1]+  Stoppé    ./gradlew build

$ git status
On branch main
nothing to commit, working tree clean

$ fg
./gradlew build
> Building...
BUILD SUCCESSFUL in 2m 34s

场景 2 : 被遗忘的编辑

您在`vim`, 您需要查看另一个文件而不退出`vim`.

# Dans vim, en mode commande :
:shell
$ cat /path/to/other/file.txt
$ exit
# Retour dans vim

但是很重。与`Ctrl+Z`, 这是即时的 :

# Dans vim, n'importe quel moment :
Ctrl+Z
[1]+  Stoppé    vim README.md

$ cat /path/to/other/file.txt
# ... lire le fichier ...

$ fg
vim README.md
# Retour exact là où on en était, curseur en place
@startuml
skinparam backgroundColor #FEFEFE
autonumber

actor Développeur as dev
participant "vim" as vim
participant "Shell" as shell

dev -> vim : 编辑进行中
dev -> vim : Ctrl+Z
vim -> shell : 挂起 (job [1])
dev -> shell : chat autre_fichier.txt
shell --> dev : 文件内容
dev -> shell : fg
shell -> vim : 重新开始(作业 [1])
vim --> dev : 光标恰好\n位于相同位置

note right of vim
vim 恢复
int 完整恢复状态:
光标位置,
缓冲区,撤销栈
end note
@enduml
vim`在……之后完美地恢复其状态`fg: 光标位置, 缓冲区内容, 撤销历史 — 一切都被保留。

场景 3:opencode 进程已挂起

这是激发本文的情景。您正在使用`opencode`(CLI工具,用于在您的代码上驱动LLM),且过程被挂起。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
skinparam componentStyle rectangle

actor Développeur as dev
participant "opencode\n(LLM agent)" as agent
participant "Shell" as shell
participant "git" as git

dev -> agent : Session active
agent -> agent : Tour de parole\ndu LLM en cours

note over agent
Ctrl+Z 在代码分析的正中间出现
end note

dev -> agent : 撤销
agent -> shell : Suspendu (job [1])
dev -> git : git diff
git --> dev : 可见的变化
dev -> shell : fg
shell -> agent : 重新开始 (job [1])
agent --> dev : Reprend exactement\nl'analyse en cours

note right of agent
没有上下文丢失
LLM 继续它的回答
正好在它停下的地方
end note
@enduml
$ opencode
# ... session en cours, LLM analyse le code ...
^Z
[1]+  Stoppé    opencode

$ git diff HEAD~1
# vérifier les changements récents

$ fg
opencode
# Le LLM reprend là où il s'était arrêté

优势是双重的:

  1. 没有上下文丢失: 会话 LLM, 对话历史, 打开的文件 — 全部都被保留

  2. 无需重启: 不需要重新启动 opencode,重新加载上下文,或向 LLM 重新解释问题

场景4:同时处理多个作业

fg et `bg`接受作业编号以在多个作业被挂起时定位特定进程。

$ vim config.yml
^Z
[1]+  Stoppé    vim config.yml

$ ./gradlew test
^Z
[2]+  Stoppé    ./gradlew test

$ htop
^Z
[3]+  Stoppé    htop

$ jobs
[1]   Stoppé    vim config.yml
[2]-  Stoppé    ./gradlew test
[3]+  Stoppé    htop

$ fg %2
./gradlew test
# Le build reprend en avant-plan

$ bg %3
[3] htop &
# htop reprend en arrière-plan
@startuml
skinparam backgroundColor #FEFEFE

rectangle "已暂停的作业" as jobs #LightYellow {
  card "[1] vim config.yml" as j1
  card "[2] ./gradlew 测试" as j2
  card "[3] htop" as j3
}

rectangle "操作" as actions #LightGreen {
  card "fg %2\n→ 前景" as a1
  card "bg %3\n→ 后台" as a2
  card "fg %1\n→ 前景" as a3
}

jobs --> actions : fg/bg + 作业编号
actions --> j2 : 继续
actions --> j3 : 在背景

note bottom of jobs
每个作业保持
其精确状态
在 suspend/resume 之间
end note
@enduml

作业命令概述

订单 效果 例子

Ctrl+Z

暂停前台进程

发送的`SIGTSTP`

fg

恢复默认任务在前台

fg ou fg %1

fg %n

在前台恢复作业 n

fg %2

bg

在后台恢复默认任务

bg ou bg %1

bg %n

在后台恢复作业 n

bg %2

jobs

列出当前 shell 的作业

jobs -l(带有 PID)

kill %n

完成工作 n

kill %2

fg、bg、Ctrl+Z、Ctrl+C 与 & 的区别

许多开发者混淆了这些机制。以下是一个关键的澄清。

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

rectangle "**Ctrl+Z**\nSIGTSTP" as ctrlz #LightYellow {
  card "暂停进程\n(它仍在内存中)\n可使用 fg/bg" as c1
}

rectangle "**Ctrl+C**\nSIGINT" as ctrlc #LightCoral {
  card "结束进程\n(不可恢复)\n丢失所有状态" as c2
}

rectangle "**& **\n(后台)" as ampersand #LightGreen {
  card "直接启动\n在后台\n终端立即空闲" as c3
}

rectangle "**fg**\n(前台)" as fg_ #SkyBlue {
  card "将作业恢复\n挂起的前台\n终端被占用" as c4
}

rectangle "**bg**\n(后台)" as bg_ #Lavender {
  card "恢复作业\n挂起的后台\n终端空闲" as c5
}

ctrlz -right-> fg_ : fg 重新开始
ctrlz -right-> bg_ : bg 用于继续\n在后台
ctrlc -right-> c2 : 没有返回\n进程已被终止

note bottom of ctrlz
进程 **已冻结** 但 **未被杀死**
end note

note bottom of ampersand
替代方案:启动
./gradlew build &
直接在后台
end note
@enduml
快捷键/命令 信号 效果 返回?

Ctrl+Z

SIGTSTP

暂停进程(冻结,在内存中)

是的:fg ou bg

Ctrl+C

SIGINT

终止进程(已完成)

否:进程已被销毁

Ctrl+\

SIGQUIT

终止进程 + core dump

不是:进程已被销毁

commande &

—

直接在后台启动

`fg`为了把它带到前台

Ctrl+Z`是无损. 该过程被原样冻结,存储在内存中。您可以立即继续与`fg. 暂停,不是 停止。

工作的完整生命周期

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

title 作业 shell 的完整生命周期
state "不存在" as none
state "前景\n(occupe le terminal)" as foreground #LightCoral
state "已挂起\n(Ctrl+Z / SIGTSTP)" as suspended #LightYellow
state "背景\n(bg ou &)" as background #LightGreen
state "已完成\n(exit code 0)" as done #LightGray

[*] --> none
none --> foreground : 输入一条命令
foreground --> suspended : Ctrl+Z
foreground --> done : 正常结束

suspended --> foreground : fg
suspended --> background : bg
suspended --> done : 杀死 %n

background --> foreground : fg
background --> done : 正常结束\n(en arrière-plan)
background --> suspended : Ctrl+Z\n(si fg puis Ctrl+Z)

done --> [*]

note right of suspended
暂停状态是作业系统的核心枢纽
end note
@enduml

高级技巧

fg 与最新的工作和交替一起

fg`无参数返回被标记的作业+(最新的). 但您也可以使用%-`用于定位上一个作业:

$ vim file1.txt
^Z
[1]+  Stoppé    vim file1.txt

$ vim file2.txt
^Z
[2]+  Stoppé    vim file2.txt

$ fg      # ramène job [2] (le plus récent)
$ fg %-   # ramène job [1] (le précédent)

妥善地结束作业

$ jobs
[1]-  Stoppé    vim config.yml
[2]+  Stoppé    ./gradlew test

$ kill %2       # envoie SIGTERM au job 2
$ kill -9 %1    # envoie SIGKILL au job 1 (force)

désown : détacher du shell

disown`从 shell 的作业表中移除一个作业。进程继续运行,但不能再被带回`fg:

$ ./gradlew build &
[1] 12345

$ disown %1
# Le processus continue mais n'est plus lié au shell
# Ça survives à la fermeture du terminal

将 nohup 与长时间运行的进程结合使用

为了让进程在终端关闭后仍然存活:

$ nohup ./gradlew build &
[1] 12345
$ disown %1
# Le build continue même si vous fermez le terminal
@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

rectangle "fg" as fg_ #SkyBlue {
  card "将最近的作业\n调至前台 (+)" as f1
  card "指定特定作业\n:fg %n" as f2
  card "交替: fg %-" as f3
}

rectangle "杀死 %n" as kill_ #LightCoral {
  card "SIGTERM (干净)" as k1
  card "SIGKILL -9 (强制)" as k2
}

rectangle "disown" as disown #Lavender {
  card "从 shell 分离\n(survit à la déconnexion)" as d1
}

rectangle "nohup + &" as nohup_ #LightGreen {
  card "免疫的进程\n对 SIGHUP\n(survit à la fermeture\ndu terminal) 有免疫" as n1
}

fg_ -right-> kill_ : 如果必须\n完成
kill_ -right-> disown_ : 或者分离
nohup_ -left-> fg_ : fg 置于前景

note bottom of disown_
disown` 之后 fg 再也无法将该作业调回前台。
end note
@enduml

陷阱和常见错误

混淆 Ctrl+Z 和 `Ctrl+C

这是最常见的错误。`Ctrl+C`杀死该进程。`Ctrl+Z`暂停它。 如果你按下`Ctrl+C`作为反射,所有状态都丢失了 — 打开的文件、LLM 会话、进行中的构建。

@startuml
skinparam backgroundColor #FEFEFE

rectangle "Ctrl+Z(SIGTSTP)" as z #LightGreen {
  card "内存中冻结的进程" as z1
  card "fg 将其完好无损地带回来" as z2
  card "无损失" as z3
}

rectangle "Ctrl+C(SIGINT)" as c #LightCoral {
  card "进程已完成" as c1
  card "永久丢失的状态" as c2
  card "无法回头" as c3
}

note bottom of z
  Pause → Reprendre
end note

note bottom of c
  Kill → Perdu
end note
@enduml

作业是 shell 本地的

jobs, fg et `bg`仅在启动这些进程的shell中起作用。 如果您打开一个新的终端,您将看不到另一个作业。

# Terminal 1
$ vim file.txt
^Z
[1]+  Stoppé    vim file.txt

# Terminal 2 (nouveau)
$ jobs
# (rien — les jobs sont locaux au shell)

要查看系统范围的进程,请使用`ps` ou htop:

ps aux | grep vim

作业不会在 shell 关闭后存活

如果您关闭终端,所有挂起的或在后台运行的作业都将被销毁。为了让进程继续运行,请使用`nohup` + disown:

$ nohup ./gradlew build &
[1] 12345
$ disown %1
# Fermer le terminal → le build continue

交互式进程和 fg

一些交互式程序在一个…之后表现不佳`Ctrl+Z`+fg. 虽然罕见,但会发生:

  • 某些使用信号的程序 (vim, htop, less 处理得很好`Ctrl+Z`)

  • 长时间运行的网络程序(ssh, opencode)通常能够正确恢复

  • 修改终端的程序(ncurses)有时可能会使终端处于意外状态。

如果终端在一个…​之后损坏`fg`, 请输入 :

reset

概览图

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif
skinparam ArrowColor #444444

title "fg — 这个技巧改变一切"

rectangle "**问题**" as problem #LightCoral {
  card "Terminal 卡住\n由一个进程" as p1
  card "打开 一个新的\n终端 = 笨重的解决方案" as p2
}

rectangle "**解决方案**" as solution #LightGreen {
  card "Ctrl+Z → 挂起" as s1
  card "fg → 恢复" as s2
  card "bg → 后台" as s3
  card "jobs → 查看已挂起的作业" as s4
}

rectangle "**优势**" as advantages #SkyBlue {
  card "无上下文丢失" as a1
  card "无需重启" as a2
  card "只需要一个终端" as a3
}

problem -right-> solution : "此解决方案 native\nUnix 已有40年"
solution -right-> advantages : "结果"

note bottom of advantages
fg 不是一个晦涩的技巧
这是一个基本机制
Unix shell 的
end note
@enduml

为什么fg被低估了

大多数现代开发者发现`fg`迟到,甚至可能永不。一些原因:

  1. IDE 隐藏了终端: VS Code 提供了内置终端,但多个选项卡会导致`fg`表面上看起来不那么必要

  2. 图形工具替代 CLI: 文件管理器,系统监视器,图形化 Git 客户端

  3. tmux 和 screen: 这些多路复用器以不同的方式解决相同的问题,但`fg`更简单且始终可用

然而,`fg`是 :

  1. 普遍: 存在于所有 POSIX shell 中(bash, zsh, fish…​)

  2. 无依赖: 不需要安装 tmux 或 screen

  3. 快照: 两次打击 (Ctrl+Z`然后`fg) 暂停并恢复

  4. 上下文保存者: 进程状态完好

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

rectangle "为什么 fg 被低估了?" as why #LightCoral {
  card "集成开发环境,带多标签终端" as w1
  card "替代 CLI 的图形工具" as w2
  card "tmux/screen\n以另一种方式解决问题" as w3
}

rectangle "为什么 fg 更优?" as superior #LightGreen {
  card "通用:bash,\nzsh, fish, sh..." as s1
  card "无依赖:\n无需安装" as s2
  card "两次按键:\nCtrl+Z 然后 fg" as s3
  card "上下文已保留:\n状态零丢失" as s4
}

why -right-> superior : "简约\n永远胜出"
@enduml

技术备忘录:信号

对于好奇的人,以下是涉及的信号:

信号 名称 由…​发送 效果

2

SIGINT

Ctrl+C

中断 — 终止进程

3

SIGQUIT

Ctrl+\

带有核心转储退出

18

SIGTSTP

Ctrl+Z

交互式停止 — 暂停进程

19

SIGCONT

fg/bg

继续 — 恢复一个暂停的进程

15

SIGTERM

kill %n

干净终止

9

SIGKILL

kill -9 %n

强制终止(无法拦截)

完整的流程:

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultFontName sans-serif

participant "用户" as user
participant "Shell" as shell
participant "进程" as proc

user -> shell : Ctrl+Z
shell -> proc : SIGTSTP (信号 18)
note right of proc : 卡住的进程\n内存中

user -> shell : fg
shell -> proc : SIGCONT (信号 19)
note right of proc : 进程恢复\n 在前台

user -> shell : bg
shell -> proc : SIGCONT (信号 19)
note right of proc : 进程在后台继续

user -> shell : Ctrl+C
shell -> proc : SIGINT (信号 2)
note right of proc : 进程已完成\n状态丢失
@enduml

结论

`fg`这并不是一个晦涩的技巧 — — 而是 Unix shell 的一个基本机制,自 70 年代起就已存在。多数开发者对此视而不见,反映了我们这个时代的特点:IDE 和复用器让我们远离了基础。

但当你在 SSH 服务器上、在最小化终端中,或者只是在使用时`opencode`而且 LLM 正在全力工作 —Ctrl+Z, fg, bg, `jobs`是你们最好的朋友。

_ Ctrl+Z 挂起,fg 恢复。这是终端的暂停/播放。 _

参考文献

相关文章