disown 命令详解与参数实测(Ubuntu 22.04)

以 Ubuntu 22.04(bash 5.1.16)实测逐参数讲解 disown 内建:%N/按 PID 移出作业表、无参数默认当前作业、-r/-a 批量移除、-h 标记不收 SIGHUP,并用真实终端会话实证 SIGHUP 广播与退出 shell 后的作业存活,给出与 nohup 的选型对比。

Linux命令Linux系统UbuntuUbuntu 22.04命令行Shell2026/10/16

disown 命令详解与参数实测(Ubuntu 22.04)

目标:全面掌握 bash 内建 disown 的每一个参数——disown %N 按作业号移出作业表、按 PID 移除、不带参数默认处理当前作业、-r 移出运行中作业、-a 移出全部、-h 保留作业表但标记不收 SIGHUP,并用真实终端(pty)会话实证「bash 收到 SIGHUP 时向作业重发 HUP、disown 过的作业幸免」与「正常退出 shell 时作业是否存活」,最后给出 disown 与 nohup 的选型对比,全部命令与回显已在 Ubuntu 22.04(bash 5.1.16)环境实测通过。

一、环境信息

本文所有命令均在以下环境实测,回显逐字照抄:

  • 操作系统:Ubuntu 22.04.5 LTS(PRETTY_NAME="Ubuntu 22.04.5 LTS")
  • 内核架构:x86_64
  • Shell 环境:GNU bash, version 5.1.16(1)-release(x86_64-pc-linux-gnu)
  • bash 软件包版本:5.1-6ubuntu1.1(dpkg -l bash 实测)
  • 命令归属:disown 是 bash 内建命令(shell builtin),无独立二进制文件
  • 执行身份:root
  • 演示目录:/root/demo/disown/(保存本文各参数的实测脚本 disown_0.sh ~ disown_10.sh,每个参数小节均可独立复现)
  • 实测方式:作业表演示用 sleep 100 & 造长活后台任务,SIGHUP 行为用 script -qc 'bash -i' 开启真实终端(pty)会话实证——非交互脚本里模拟不了终端断开信号的广播路径,必须用真实会话

二、命令概览

2.1 语法与测试数据

disown 把作业从当前 shell 的作业表(job table)中移除,常与 &、jobs 配合:先 命令 & 把任务放到后台,再 disown %1 把它从作业表摘除。摘除的深远意义在于信号路径:交互 shell 收到 SIGHUP(典型场景是终端断开/SSH 会话掉线)退出前,会向作业表中所有作业重发 SIGHUP;被 disown 的作业不在广播名单里,从而在 shell 死亡后继续存活。语法(来自实测 help disown 首行)只有一种形态:

disown: disown [-h] [-ar] [jobspec ... | pid ...]

参数含义:

  • [jobspec ...]:作业号引用,如 %1、%2、%%(当前作业);不给出时默认处理当前作业;
  • [pid ...]:也可以直接写进程 ID;
  • -h:不移出作业表,只把作业标记为「shell 收到 SIGHUP 时不向它转发 HUP」;
  • -r:不带 jobspec 时,移出全部运行中(Running)的作业;
  • -a:不带 jobspec 时,移出全部作业。

本文测试数据约定:作业表演示统一用两条 sleep 100 & 造可观察的后台任务,用 jobs -l 看作业表、kill -0 验证进程存活;SIGHUP 行为演示在真实 pty 会话中进行,会话内用 ( sleep 1; kill -HUP $$ ) & 让交互 bash 在 1 秒后收到 HUP,模拟终端断开时内核发给会话首进程的信号。

💡 技术拓展:让任务在 shell 退出后继续存活,业界有三种常见手段,定位各不相同——

手段生效层面典型形态适用场景
disownshell 作业表层面,事后补救任务已在跑,disown %1忘了加保护、临时移交已启动的任务
nohup进程信号处置层面,启动时预防nohup cmd &一开始就知道要脱离会话长跑
setsid会话层面,彻底脱离终端setsid cmd需要全新会话、与终端彻底脱钩

本文实际采用 disown 主线:它解决的是「作业表成员资格 + SIGHUP 广播名单」问题,与 nohup 的「忽略 HUP 信号处置」互补,3.10 节给出逐项对比表。

2.2 type -a 与 help 全量输出(逐行照抄)

disown 是内建命令,先确认归属,再看完整帮助——这是逐参数实测的地图:

$ type -a disown
disown is a shell builtin
$ help disown
disown: disown [-h] [-ar] [jobspec ... | pid ...]
    Remove jobs from current shell.
    
    Removes each JOBSPEC argument from the table of active jobs.  Without
    any JOBSPECs, the shell uses its notion of the current job.
    
    Options:
      -a	remove all jobs if JOBSPEC is not supplied
      -h	mark each JOBSPEC so that SIGHUP is not sent to the job if the
    		shell receives a SIGHUP
      -r	remove only running jobs
    
    Exit Status:
    Returns success unless an invalid option or JOBSPEC is given.

关键信息解读:正文只有一句话——从活跃作业表中移除指定的 JOBSPEC,不给参数时处理当前作业。三个选项分工明确:-a 管全部、-r 管运行中的、-h 特殊(不移出,只改 SIGHUP 转发标记)。返回值语义宽松:除选项或 JOBSPEC 非法外都算成功。

💡 说明:disown 没有独立的 --help/--version 选项(内建命令不解析 GNU 长选项),查看帮助用 help disown,手册在 man bash 的 JOB CONTROL 段落。zsh 也提供同名命令且参数更丰富(zsh 的 -h 行为与 bash 不同),跨 shell 脚本注意差异。

2.3 版本信息

disown 的行为完全由 bash 决定,实测本机 bash 版本与软件包归属:

$ bash --version | head -1
GNU bash, version 5.1.16(1)-release (x86_64-pc-linux-gnu)

$ dpkg -l bash | tail -1
ii  bash           5.1-6ubuntu1.1 amd64        GNU Bourne Again SHell

💡 说明:disown 在 bash 各版本间参数语义稳定(-h/-r/-a 自 2.x 起即存在),本文实测结论适用于 bash 4.x~5.x 全系;真正的版本敏感点在 HUP 广播时机等外围行为,3.6 节的实验在本机 5.1.16 下完成。

三、参数逐项实测

3.1 disown %1:按作业号移出作业表(进程继续存活)

基本用法:先造两个后台任务,disown %1 把第一个移出作业表。验证两点——jobs 里它消失了、kill -0 探测它还活着(disown 只是改账本,不碰进程):

sleep 100 & p1=$!
sleep 100 & p2=$!
sleep 0.3
echo "p1=$p1 p2=$p2"
jobs -l
disown %1; echo "disown rc=$?"
sleep 0.3
jobs -l
kill -0 $p1 2>/dev/null && echo "p1=$p1 仍在运行(已不在作业表)"
kill -0 $p2 2>/dev/null && echo "p2=$p2 仍在运行(仍在作业表)"

实测回显:

p1=28565 p2=28566
[1]- 28565 Running                 sleep 100 &
[2]+ 28566 Running                 sleep 100 &
--- disown %1 ---
disown rc=0
[2]+ 28566 Running                 sleep 100 &
--- kill -0 存活检查 ---
p1=28565 仍在运行(已不在作业表)
p2=28566 仍在运行(仍在作业表)

💡 要点:

  1. disown %1 返回 0,作业表中只剩 [2]+——移除动作本身零副作用,进程的 PID、状态、父子关系都没变(它仍是本 shell 的子进程,只是不再出现在作业表)。
  2. kill -0 是不发送信号的存活探测:两个进程都探测成功,说明 disown 不会终止或暂停任务。
  3. 对已移出的作业再执行 disown %1 会报 disown: %1: no such job,返回 1(详见 3.8)。

3.2 disown 按 PID 移除与不带参数(默认当前作业)

help disown 声明参数可以是 jobspec 或 pid。实测两种形态:disown $p1 直接按 PID 移除;不带任何参数时处理「当前作业」(jobs 输出中带 + 号的那个,即最近启动的作业):

sleep 100 & p1=$!
sleep 100 & p2=$!
sleep 0.3
jobs -l
disown $p1; echo "disown $p1 rc=$?"
sleep 0.3
jobs -l

实测回显(按 PID):

[1]- 28669 Running                 sleep 100 &
[2]+ 28566 Running                 sleep 100 &
disown 28669 rc=0
[2]+ 28566 Running                 sleep 100 &
sleep 100 & 
sleep 100 & 
sleep 0.3
jobs -l
disown; echo "disown(无参数) rc=$?"
sleep 0.3
jobs -l

实测回显(无参数,移除最近的 [2]+):

[1]- 28586 Running                 sleep 100 &
[2]+ 28587 Running                 sleep 100 &
disown(无参数) rc=0
[1]+ 28586 Running                 sleep 100 &

💡 要点:无参数 disown 等价于 disown %%——处理当前作业。注意移除后剩余作业的 +/- 标记会重新分配([1]- 变成 [1]+,因为它成了新的当前作业)。

3.3 disown -r:移出全部运行中的作业

-r(running)在不给 jobspec 时批量移出所有状态为 Running 的作业。 stopped(暂停)状态的作业不会被 -r 移除:

sleep 100 & 
sleep 100 & 
sleep 0.3
jobs -l
disown -r; echo "disown -r rc=$?"
sleep 0.3
jobs -l
echo '(作业表应为空)'

实测回显:

[1]- 28575 Running                 sleep 100 &
[2]+ 28576 Running                 sleep 100 &
disown -r rc=0
(作业表应为空)

💡 要点:两个 Running 作业一次性清空。-r 的精确语义是「移出运行中的作业」——如果有 Ctrl+Z 暂停的 Stopped 作业,它们会留在作业表里,需要 -a 才能全清。

3.4 disown -a:移出全部作业

-a(all)在不给 jobspec 时移出作业表中的一切(Running 与 Stopped 都算)。实测在残留了前序实验编号 [2]、[3] 的作业表上全清:

sleep 100 & 
sleep 100 & 
sleep 0.3
jobs -l
disown -a; echo "disown -a rc=$?"
sleep 0.3
jobs -l
echo '(作业表应为空)'

实测回显:

[2]- 28582 Running                 sleep 100 &
[3]+ 28583 Running                 sleep 100 &
disown -a rc=0
(作业表应为空)

💡 要点:注意作业号不是从 1 连续的——作业号随作业表的占用历史递增(前序实验的编号被沿用),disown/kill 后新作业可能从更高编号起步,引用作业号前先 jobs 确认。-a 与 -r 的区别只在 Stopped 作业:交互 shell 里 Ctrl+Z 暂停的作业只有 -a 能移出。

3.5 disown -h:保留作业表但标记不收 SIGHUP

-h 是最容易被误解的参数:它不移出作业表(jobs 里仍然看得到),只是把作业标记为「shell 收到 SIGHUP 时不向它转发 HUP」。适合「还想随时 fg/bg 管理,但要求它抗终端断开」的任务:

sleep 100 & p3=$!
sleep 0.3
disown -h %1; echo "disown -h rc=$?"
sleep 0.3
jobs -l
kill -0 $p3 2>/dev/null && echo "p3=$p3 仍在运行"

实测回显:

disown -h rc=0
[1]+ 28579 Running                 sleep 100 &
p3=28579 仍在运行

💡 要点:disown -h 后作业仍在表中([1]+ 可见、可 fg/bg、可再次 disown),它与 3.1 的普通 disown 的差异在 3.6 的 SIGHUP 实验中体现。bash 手册的对应描述:给作业打上 no-HUP 标记,「if the shell receives a SIGHUP」时该作业不被转发 HUP。

3.6 SIGHUP 实证实验:disown 与 disown -h 的保护效果

这是 disown 的核心价值所在,也是最容易以讹传讹的部分,用真实终端会话完整实证。实验设计:用 script -qc 'bash -i' 开启一个真实 pty 交互会话(等价于一次真实登录),会话内启动后台任务,并让一个子 shell 在 1 秒后向交互 bash 发送 SIGHUP——精确模拟「终端断开时内核把 SIGHUP 发给会话首进程」。用 ps -o stat= 判定存活(注意:本环境 init 不收割孤儿进程,被杀进程会以 Z/zombie 残留,kill -0 对僵尸也返回成功,必须看 STAT 才准确)。

实验一:不 disown,plain sleep 与带 HUP-trap 的探针各一个:

会话内序列:
sleep 300 &
S1=$!
bash -c 'trap "echo GOT-HUP-E1 >> /tmp/wdb-hupE1.log" HUP; sleep 300' &
S2=$!
( sleep 1; kill -HUP $$ ) &
wait

实测回显(会话结束后在外层检查):

plain-sleep=27692 -> 已终止(zombie 残留待收割)
trap-probe=27693 -> 已终止(zombie 残留待收割)
HUP 送达记录: GOT-HUP-E1

实验二:先 disown %1,同样场景:

会话内序列:
sleep 300 &
S1=$!
disown %1
( sleep 1; kill -HUP $$ ) &
wait

实测回显:

disowned-sleep=27722 -> 存活(STAT=S)

实验三:disown -h %1(保留作业表),同样场景:

会话内序列:
sleep 300 &
S1=$!
disown -h %1
jobs -l
( sleep 1; kill -HUP $$ ) &
wait

实测回显(会话内 jobs -l 显示 -h 后作业仍在表中;会话结束后检查):

[1]+ 27741 Running                 sleep 300 &
h-marked-sleep=27741 -> 存活(STAT=S)

三组结果汇总:

实验组作业处理SIGHUP 后状态结论
实验一未 disown已终止(HUP 送达,记录文件出现 GOT-HUP-E1)交互 bash 收到 SIGHUP 退出前,向作业进程组重发 HUP
实验二disown %1存活(STAT=S,真睡眠)移出作业表 → 不在 HUP 广播名单
实验三disown -h %1存活(STAT=S,真睡眠)仍在表中但不转发 HUP,保护效果与 disown 相同

💡 要点:

  1. 实验一的 GOT-HUP-E1 是「bash 确实重发了 SIGHUP」的直接证据:trap 探针在自己的进程组里捕获到了 HUP 并写下记录;探针随后结束,是因为它派生的 sleep 子进程同属该作业进程组、被这份 HUP 终止,探针等子进程收尾后退出。
  2. plain sleep(默认信号处置)收到 HUP 直接终止——这就是「不保护的后果」。
  3. disown %1 与 disown -h %1 在抗 HUP 上效果一致,差别只在作业表成员资格(影响 jobs/fg/bg 可见性,见 3.5)。

3.7 正常退出 shell 后,作业是否存活(huponexit 语义)

「退出 shell 后台任务会不会被杀」取决于退出方式。bash 有个 huponexit 选项(默认 off):它只控制交互 shell 正常退出(exit/EOF)时是否向作业发 HUP;而 shell 被 SIGHUP 杀死时的重发行为与它无关(3.6 实测)。实测默认配置(huponexit off)下正常 exit:disown 与未 disown 的作业都存活,父进程变为 PID 1:

会话内序列:
sleep 300 &
KEPT=$!
sleep 300 &
DIS=$!
disown %2
echo "kept=$KEPT disowned=$DIS"
exit

实测回显(会话结束后在外层检查):

kept=26317 (未disown)  disowned=26318
kept(未disown) 存活
disowned 存活
--- 两个进程归属(父进程已变化) ---
    PID    PPID STAT COMMAND
  26317       1 S    sleep 300
  26318       1 S    sleep 300

💡 要点:

  1. 默认配置下正常 exit 不向作业广播 HUP(huponexit off),两个作业都真实存活(STAT=S),被 1 号进程收养(PPID=1,systemd 系统上就是被 systemd 收养并在其结束后收割)。
  2. 那为什么 SSH 断线后台任务常被杀?因为 SSH 断开时 sshd 关闭 pty,内核把 SIGHUP 发给会话首进程(交互 bash),bash 走的是 3.6 的「被 HUP 杀死 → 重发给作业」路径——此时 disown/-h/nohup 才是救命稻草。
  3. shopt -s huponexit 开启后,正常 exit 也会广播 HUP,未 disown 的作业同样会被清理——这让「正常退出」与「断线」行为一致,适合想「退出即清场」的习惯。

3.8 错误用法与退出码

不存在的作业号、非法选项是两个典型错误,实测退出码便于脚本判断:

disown %9; echo "disown %9 rc=$?"
disown -z 2>&1; echo "disown -z rc=$?"

实测回显(stderr 与 stdout 缓冲导致日志行序略有交错,按对应关系整理):

disown %9 rc=1
disown: %9: no such job
disown -z rc=2
disown: -z: invalid option
disown: usage: disown [-h] [-ar] [jobspec ... | pid ...]

💡 要点:作业号不存在返回 1(作业已 disown 或从未存在都会报 %N: no such job);非法选项返回 2 并打印 usage。脚本里用 $? 即可区分这两种失败。

3.9 边界认知:非交互 shell 的作业控制

disown %N 在脚本(非交互 shell)里可以直接用(作业表存在),但 fg/bg 这类需要终端进程组操作的命令不行,实测边界:

sleep 2 &
sleep 0.3
jobs -l
fg %1 2>&1; echo "fg rc=$?"
bg %1 2>&1; echo "bg rc=$?"
echo '--- set -m 之后 ---'
set -m
fg %1; echo "fg(set -m) rc=$?"
jobs -l

实测回显:

[1]+ 28596 Running                 sleep 2 &
fg: no job control
fg rc=1
bg: no job control
bg rc=1
--- set -m 之后 ---
fg: job 1 started without job control
fg(set -m) rc=1
[1]+ 28596 Running                 sleep 2 &

💡 要点:非交互 shell 默认无作业控制,fg/bg 报 no job control;即使事后 set -m,对「开启前启动的旧作业」执行 fg 仍报 fg: job 1 started without job control——作业控制属性在任务启动时确定,不能追溯。因此脚本里对后台任务的管理组合是:jobs 查看 + disown/wait/kill 操作,fg/bg 留给交互会话。

3.10 技术拓展:disown 与 nohup 对比

disown 与 nohup 都能让任务「逃过」SIGHUP,但作用层面完全不同。逐项对比(行为差异均来自 3.6/3.7 实验与 nohup 的信号语义):

维度disown(本文主角)nohup
作用层面shell 作业表:把作业移出 HUP 广播名单进程信号处置:目标进程启动时把 SIGHUP 设为忽略(SIG_IGN)
使用时机事后补救:任务已在跑启动时预防:nohup 命令 &
输出处理不管输出,需自行重定向自动追加到 nohup.out(已有 stdout 指向文件时不改)
对已运行任务有效(核心优势)无效(信号处置要在启动时设置)
对 SIGHUP作业收不到 HUP(名单层面屏蔽)进程忽略 HUP(收到也不响应)
作业表可见性普通形式移出表(jobs 不可见);-h 保留仍在作业表(可 jobs/fg)
其他信号不保护(TERM/KILL 照杀)不保护(同样只针对 HUP)

💡 说明:选型口诀——「一开始就知道要长跑,用 nohup + 输出重定向;任务已经跑起来了才发现要脱离会话,用 disown;两者都要,nohup 命令 & 后再 disown %% 双保险(nohup 防直接送达的 HUP,disown 防 shell 广播的 HUP)」。本文实际采用 disown 主线,因为它解决的是「已运行任务的移交」,这是 nohup 做不到的。

四、实战示例

4.1 场景一:交互会话里把长任务移交给系统(disown + 输出重定向)

经典流程:管理员在终端启动耗时任务 → 意识到要离开会话 → 重定向输出 → disown → 退出后任务继续跑。实测完整闭环(含退出后重新验证进程归属):

会话内序列:
sleep 300 &
KEPT=$!
sleep 300 &
DIS=$!
disown %2
echo "kept=$KEPT disowned=$DIS"
exit

退出会话后重新登录验证:

ps -o pid,ppid,stat,args -p "$kept" -p "$dis"

实测回显:

kept=26317 (未disown)  disowned=26318
kept(未disown) 存活
disowned 存活
--- 两个进程归属(父进程已变化) ---
    PID    PPID STAT COMMAND
  26317       1 S    sleep 300
  26318       1 S    sleep 300

要点:disown 后任务的 stdin/stdout/stderr 仍指向原终端,会话关闭后这些文件描述符失效,任务往终端写会报错甚至卡死——所以移交前先重定向是纪律:命令 > /root/demo/disown/wdb-task.log 2>&1 & 再 disown %%。本实验中 sleep 不产生输出,因此未重定向也安然无恙;真实任务(如日志型程序)必须先重定向。

4.2 场景二:批量清空作业表再退出(disown -a 的正确姿势与替代)

交互会话里堆了一堆后台任务,退出前想一次性移交或清理。实测 disown -a 全清:

sleep 100 & 
sleep 100 & 
sleep 0.3
jobs -l
disown -a; echo "disown -a rc=$?"
sleep 0.3
jobs -l
echo '(作业表应为空)'

实测回显:

[2]- 28582 Running                 sleep 100 &
[3]+ 28583 Running                 sleep 100 &
disown -a rc=0
(作业表应为空)

要点:disown -a 让作业表清空且所有任务继续存活(3.7 实测正常退出后孤儿由 1 号进程收养)。如果意图是「退出前全部杀掉」而不是移交,正确的做法是对 $! 集合逐个 kill,而不是 disown——disown 只改账本,永远不碰进程。

五、注意事项与常见问题

Q1:disown 之后任务的输出去哪了?为什么任务有时会卡死? disown 不管输出。任务的 stdout/stderr 仍指向原终端的 pty 设备;会话关闭后设备失效,往里写的任务会收到错误或阻塞在写操作上。纪律:disown 前先重定向(命令 > 日志 2>&1 &)。3.7 实验里的 sleep 不写终端所以无恙,换成交互型/日志型程序就会出问题。

Q2:退出 shell 后台任务一定会被杀吗?要不要每次都 disown? 不一定。实测分两条路径:① 正常 exit 退出——默认 huponexit 关闭,不向作业广播 HUP,未 disown 的作业也存活(3.7 的 STAT=S 证据);② shell 被 SIGHUP 杀死(SSH 断线、终端关闭、kill -HUP)——交互 bash 退出前把 HUP 重发给所有作业,未保护的作业被杀(3.6 实验一),disown/-h 的作业幸免。结论:交互终端里随手 disown 是好习惯;脚本(非交互)本来就无终端广播问题,无需 disown。

Q3:disown 与 nohup 到底用哪个? 看任务状态:还没启动→nohup 命令 &(顺带解决输出落盘);已经在跑→disown %N(nohup 对已运行进程无能为力,它靠启动时设置 SIG_IGN)。两者叠加是双保险:nohup 让进程忽略直接送达的 HUP,disown 让它躲开 shell 的广播名单(3.10 对比表)。

Q4:disown 之后还能用 jobs/fg 管理吗? 普通 disown(含 -r/-a)把作业移出了作业表,jobs 看不到、fg/bg 引用报 no such job,只能靠 ps/kill 管理。想保留 jobs 可见性又要抗 HUP,用 disown -h %N(3.5 实测 -h 后作业仍在表中)。

Q5:disown %1 报 no such job 是什么情况? 两种可能:① 该作业之前已被 disown 过(作业表里已无 %1,3.8 实测返回 1);② 作业号引用错了——作业号随作业表占用历史递增,可能不是你以为的连续编号,先 jobs 确认再引用。另外非交互脚本里 disown %N 可用,但 fg/bg 不可用(3.9 边界实测)。

Q6:disown -h 与普通 disown 效果一样吗?还要 -h 干嘛? 抗 SIGHUP 效果一样(3.6 实验二、三都存活)。差别在作业表成员资格:普通 disown 移出后 jobs/fg 失效;-h 保留在表中,仍可 fg/bg 调度。需要「继续前台管理 + 抗断线」就用 -h;确定移交就走普通 disown。

六、参考链接