本文解释bash内置命令的特殊性、前台、后台任务的"本质",以及前、后台任务和bash进程、终端的关系。网上没类似的资料,所以都是自己的感悟和总结,如有错误,120分的期待盼请指正。

因为要详细分析每一个涉及到的内容,我用了很多示例,所以结论比较分散。因此在文章的结尾,我将这些结论大概做了个总结。

1.引子:一个示例

首先通过一个示例做个引子。

当直接在当前bash环境下执行一个普通命令,这个普通命令的进程会挂在当前bash进程之下(即父进程为当前bash进程)。

例如:

# 在窗口1执行:
[root@xuexi ~]# sleep 30 # 在窗口2查看sleep进程信息:
[root@xuexi ~]# pstree -p | grep slee[p]
|-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5599)

如果,在当前bash环境下将普通命令放入后台执行,这个命令的进程还是会挂在当前bash进程下。

如果是在当前bash环境下执行一个内置命令呢?因为是内置命令,它不会有自己的进程(原因后文解释)。

# 窗口1查询当前bash进程信息
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5642)
| `-bash(5557)-+-grep(5644) # 窗口2执行bash内置命令if
[root@xuexi ~]# if true;then sleep 30;fi # 回到窗口1查询当前bash进程信息,和前面是一样的
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)---sleep(5642)
| `-bash(5557)-+-grep(5644)

发现bash进程没有任何变化。

再如果一次,将bash内置命令放入后台执行呢?

例如:

# 当前bash进程信息
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)
| `-bash(5557)-+-grep(5634) [root@xuexi ~]# if true;then sleep 30;fi &
[1] 5635 # 再次查看bash进程信息
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)---bash(5635)---sleep(5636)
| `-bash(5557)-+-grep(5638)

结果发现,多了一个bash进程。为什么会如此?

2.bash进程和bash内置命令

当登陆Linux系统时,会为用户分配一个shell。如果在/etc/passwd中该用户配置的shell为 /bin/bash ,那么就为用户分配一个bash shell。

[root@xuexi ~]# head -1 /etc/passwd
root:x:0:0:root:/root:/bin/bash

当登陆用户的身份审核通过后,就会加载bash进程,bash进程再加载它的各个配置文件(/etc/profile、/etc/profile.d/*.sh、~/.bashrc等),从而配置好bash的执行环境。注意"执行环境"这个词,它将贯穿本文。

再来说bash内置命令。

bash内置命令和普通的命令都能在bash环境下执行,并实现它们对应的功能。但它们却有很大区别,最典型的一个区别是ps等工具能捕捉到普通命令的进程,却捕捉不到bash内置命令的进程。

那么哪些是bash内置命令呢?只要查它的man文档时,给出bash文档的都是bash内置命令,如cd、declare、read等。但不代表man时不是bash手册的就不是内置命令,例如kill、echo、pwd、test等。

以下是内置命令列表。

bash, :, ., [, alias, bg, bind, break, builtin, caller, cd, command, compgen, complete, compopt, continue, declare, dirs, disown, echo, enable, eval, exec, exit, export, false, fc, fg, getopts, hash, help, history, jobs, kill, let, local, logout, mapfile, popd, printf, pushd, pwd, read, readonly, return, set, shift, shopt, source, suspend, test, times, trap, true, type, typeset, ulimit, umask, unalias, unset, wait

除此之外,还有一些保留关键字也是bash内置命令。包括:

! case do done elif else esac fi for function if in select then until while { } time [[ ]]

那么bash内置命令和bash进程有什么关系?

bash内置命令和普通命令不一样。普通命令可以直接执行,不依赖于某种执行环境。例如,sleep命令,可以直接以pid=1的init/systemd为父进程而执行。那些daemon类的服务进程更是如此,它们不依赖于终端,也不依赖于执行环境,只要给它们配置好,就可以直接找init/systemd当爹。

而bash内置命令,既然称之为"bash内置命令",顾名思义是bash内置的。当我们在当前bash环境下执行bash内置命令,经过shell的一轮解析之后,发现这是个bash内置命令,于是直接在当前bash进程的内部调用执行它们。所以bash内置命令自身是没有进程的。

换句话说,bash内置命令的执行是由它们的bash爹带着它们执行的。这个bash爹是一个负责任的好爹,什么都帮它们准备好,还带着它们一起浪。但正因为爹太负责,把孩子们给宠坏了,这些bash内置命令无论什么时候执行都必须先找好bash爹为它们提供执行环境。

于是问题出现了,如果它们的bash爹死了怎么办(即bash进程被杀或者已经结束)?这个问题并不像想象中的那么简单。下面会非常详细地结合后台任务来分析它。

3.前台任务和后台任务的本质

后台任务,是专业术语"作业"的一种。作业是指"能选择性地停止、暂停、继续运行某个进程的能力",通俗地说就是作业用来控制谁可以获得终端(前台进程)、谁不能获得终端(后台进程)。这和我们理解的"放进看不见的后台默默地执行"好像有点区别啊?这无关紧要。

关于后台任务,首先要说明的是后台任务是怎么实现的:通过bash和系统终端的驱动共同提供的交互式界面来实现后台作业能力。在bash手册中是如此解释的:

A user typically employs this facility via an interactive interface supplied jointly by the operating system kernel’s terminal driver and Bash.

其实,当一个进程的进程组号和当前终端的进程组号相同,则这个进程是前台进程,受键盘影响,可以读、写终端。当一个进程的进程组号和当前终端的进程组号不同时,则这个进程是后台进程,它们不受键盘影响,读、写终端时需要发送特定的信号。

换句话说,后台任务依赖于当前所在的终端,因为它可能会恢复到前台去运行。但当前终端往往会和一个bash进程关联绑定,该bash进程具有当前终端的控制权,所以杀掉终端进程和杀掉当前所在bash进程都能结束一个终端,但是它们却有本质上的区别,后文我用了军营、将军来比喻当前终端、当前bash进程的关系,这就是它们的区别,详细内容见后文一步一步的分析。

当一个进程是前台进程时,它是在当前bash进程下执行的,此时该bash进程失去控制权,也就是被阻塞。当进程进入后台,意味着它会离开当前bash环境,进入后台执行环境,因为只有离开当前bash环境,才能立刻将终端的控制权还给当前bash进程

于是,可以将当前终端进程、当前bash进程、前台进程、后台进程的关系大概理解为下图形式:

3.1 普通命令和bash内置命令放入后台的区别

回头看本文开头的引子,为什么sleep 30 &的sleep进程是在当前bash进程下的,而if true;then sleep 30;fi &则会新开一个bash进程?

上一小节中说过:bash内置命令的执行依赖于bash进程提供的执行环境,而普通命令则没有依赖性。

sleep 30 &的sleep是普通命令,不依赖于bash进程,所以它可以直接进入后台,但它毕竟是后台任务,它暂时还依赖于当前终端,且受当前bash进程的控制(例如能放回前台,能被bash查看后台任务信息),所以它暂时还必须挂在当前bash进程下。之所以是暂时,稍后就解释。

[root@xuexi ~]# sleep 30 &
[1] 6300 [root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)-+-sshd(5230)---bash(5557)-+-grep(6302)
| `-sshd(6047)---bash(6049)---sleep(6300)

if true;then sleep 30;fi &是bash内置命令要放入后台,放入后台意味着它要离开当前bash环境,所以它在进入后台开始执行前,必须新找一个bash爹为它提供执行环境,所以它新生成了一个bash进程。

# 当前bash进程信息
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)
| `-bash(5557)-+-grep(5634) [root@xuexi ~]# if true;then sleep 30;fi &
[1] 5635 # 再次查看bash进程信息
[root@xuexi ~]# pstree -p | grep bas[h]
|-sshd(1145)---sshd(5230)-+-bash(5232)---bash(5635)---sleep(5636)
| `-bash(5557)-+-grep(5638)

补充一个小知识:其实这个新的bash爹和当前bash进程是不一样的,这个新bash爹是非交互式的shell,可以直接使用kill -15杀掉这个新bash进程。而交互式shell下,在没有设置任何陷阱(trap)时,默认是忽略TERM信号的,无法直接kill -15杀掉一个当前活动的bash进程。

所以,完善一下上面的图:

3.2 杀掉后台任务的父进程

还是围绕普通命令的后台和bash内置命令的后台任务来说明。

上一小节在解释普通命令放入后台执行时,后台进程会挂在当前bash进程下,还特地加上了"暂时"两个字。其实,普通命令的进程进入后台时,它不是一定要挂在当前bash进程下的,甚至它不再依赖于终端,之所以还暂时挂在当前bash进程下,是因为它还是个后台任务,还需要被当前bash管理。例如将其放回前台,查看后台任务列表,如果不在当前bash进程下,当前bash进程必然无法管理它。

如果将当前bash进程或者当前终端进程杀掉,对普通命令的后台任务会造成什么影响?试试看。

# 在窗口1执行:
[root@xuexi ~]# sleep 67 &
[1] 6464 # 在窗口2查看sleep进程的父进程
[root@xuexi ~]# pstree -p | grep sleep
| `-sshd(6047)---bash(6049)---sleep(6464) # 杀掉父进程:bash进程。因为是交互式shell,所以必须使用SIGKILL信号
[root@xuexi ~]# kill -9 6049 # 再查看sleep进程
[root@xuexi ~]# pstree -p | grep sleep
|-sleep(6464)

从结果中不难发现,杀掉后台sleep进程的父进程bash(因为和终端绑定,所以也是杀掉终端)后,sleep进程没有随之中止,而是挂在init/systemd下。前面分析过,它是暂时挂在bash进程下的,它不依赖于bash进程,也不依赖于终端。

再来分析bash内置命令放入后台时,杀掉它的父进程会如何。

if true;then sleep 55;fi &为例,这里还要再细致一点地分析它的父进程。

[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6520
[root@xuexi ~]# pstree -p | grep sleep
| `-sshd(6476)---bash(6478)-+-bash(6520)---sleep(6521)

这里的sleep有两个父bash进程,其中pid=6520的是新生成的bash爹,pid=6478的是当前bash进程。是否注意到上面if放入后台时返回的进程号为6520,这个进程号对应的是新bash爹。换句话说,pid=6520的bash进程对应的是if命令,而这才是后台进程,但因为sleep在if所在bash进程的进程组内,所以sleep也是后台进程。

所以,当杀掉pid=6520的bash进程后,表示杀掉的是普通命令sleep后台的父进程,也就是if结构,所以sleep进程会直接挂在init/systemd下;当杀掉pid=6478的bash进程后,表示杀掉内置命令if(对应的是pid=6520的bash进程)的父进程,所以if命令的bash爹将带着整个进程组挂在init/systemd下。

分别验证它们。

# 杀掉新生成的bash爹
[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6551 [root@xuexi ~]# pstree -p | grep sleep
| `-sshd(6476)---bash(6478)-+-bash(6551)---sleep(6552) [root@xuexi ~]# kill 6551 # 查看sleep进程,发现确实已经挂在Init/systemd下了
[root@xuexi ~]# pstree -p | grep sleep
|-sleep(6552)
# 杀掉新生成的bash爹的父bash进程,也就是左边那个bash进程

# 在窗口1执行
[root@xuexi ~]# if true;then sleep 55;fi &
[1] 6563 # 在窗口2执行
[root@xuexi ~]# pstree -p | grep sleep
| `-sshd(6476)---bash(6478)-+-bash(6563)---sleep(6564)
[root@xuexi ~]# kill -9 6478 # 窗口2查看sleep进程,发现bash爹带着整个进程组都挂在init/systemd下
[root@xuexi ~]# pstree -p | grep sleep
|-bash(6563)---sleep(6564)

如果此时把pid=6563的bash爹杀了,会如何呢?如果前面都理解了的话,这里很容易知道答案。这个进程组是个后台进程组,包括其中的sleep进程,把sleep的父进程杀掉,sleep当然是直接挂在init/systemd下。

过程参考下图:

还没完呢。上面杀的都是它们的直系爹bash进程,如果把它们的爷爷杀掉呢?或者直接把终端关掉呢?这时又不一样了。

3.3 杀掉后台任务所在的终端

杀掉终端进程和杀掉当前bash进程对后台任务的影响不一样:杀掉当前bash进程后,后台任务会挂在init/systemd下,而杀掉终端后,后台任务也会中止。

来一个示例。

# 窗口1执行
[root@xuexi ~]# sleep 65 &
[1] 7108 # 窗口2查看
[root@xuexi ~]# pstree -p | grep sleep
| `-sshd(7014)---bash(7016)---sleep(7108) # 窗口2杀掉pid=7014,或者直接关掉窗口1
[root@xuexi ~]# kill 7014 # 窗口2再查看sleep进程信息,啥也没有
[root@xuexi ~]# pstree -p | grep sleep

前面说过,后台任务依赖于当前所在的终端。但当前终端往往会和一个bash进程关联绑定,该bash进程具有当前终端的控制权,所以杀掉终端进程和杀掉当前所在bash进程都能结束一个终端,但是它们却又本质上的区别。

其实,可以将当前终端、当前bash进程(更确切地说是后台任务的父进程)、后台的关系看作军营、将军、小兵的关系。当军营中分配了一个将军后,军营为将军和小兵提供休息、商量等环境,将军具有军营的控制权,负责管理军营中的一切,包括小兵。如果杀掉军营中的将军,小兵们发现"营中无大王",于是立刻收拾行李就走,投奔皇帝去了(init/systemd)。但如果直接把军营给炸了,那么将军和小兵将无一幸免,全军覆没。

实际上,杀掉终端进程时,终端进程会给自己进程组内的所有进程包括bash进程发送一个SIGHUP信号,正是因为收到这个信号,进程组内的所有进程才会中止。

如何让后台进程不依赖于终端?不考虑借助nohup、screen、tmux等第三方工具实现,bash其实也提供了多种解决方案,介绍其中两种方法:

1.将后台任务放入子shell。

这算是对bash深刻认识后才能想到或真正理解的方法。因为将后台任务放入子shell(子shell下一节说明),当子shell结束后,其内后台任务会立即挂到init/systemd下,这样就脱离了终端。

[root@xuexi ~]# (sleep 30 &)
[root@xuexi ~]# pstree -p | grep sleep
|-sleep(2392)

或者,放进一个脚本(执行脚本也是进入一个子shell)。例如以下是a.sh的内容。

#!/bin/bash

sleep 60 &

sleep 20

在执行该脚本的20秒内,两个sleep进程都是在a.sh进程下的。

[root@xuexi ~]# pstree -p | grep sleep
|---sshd(2317)-+-bash(2348)---a.sh(2449)-+-sleep(2450)
| | `-sleep(2451)

20秒后,脚本结束,也就是子shell退出,该子shell中的后台sleep将挂在init/systemd下。

[root@xuexi ~]# pstree -p | grep sleep
|-sleep(2450)

如果把脚本中的sleep 20去掉,那么后台sleep也将是瞬间就挂到init/systemd下的。

2.利用bash内置命令disown将任务移出后台或设置为忽略SIGHUP信号。

# 在窗口1执行
[root@xuexi ~]# sleep 60 & # jobs的作业号码:%1
[root@xuexi ~]# disown %1 # 将后台作业%1移出后台
[root@xuexi ~]# jobs # 返回空

当进程disown移出后台后,虽然暂时还挂在bash进程下,但结束终端进程时,该进程将挂到init/systemd下。所以,这样做也将脱离终端。

或者,disown -h设置后台作业忽略SIGHUP信号。前文说过,当终端进程退出时,将会向终端进程组中的所有进程发送SIGHUP信号,收到这些信号,终端下的所有进程都会终止。

[root@xuexi ~]# sleep 60 &
[root@xuexi ~]# disown -h %1 # 为%1后台作业打上忽略SIGHUP的标记

然后关闭终端,会发现sleep进程也将挂在init/systemd下。

4."奇怪"的问题以及解决方案

前面验证过杀掉if true;then sleep 55;fi &的bash爹的父进程(也就是sleep的爷爷进程)后,bash爹将带着sleep一起挂在init/systemd进程下。

但是考虑一个"极端"一点的问题。如果这里不是if,而是while/for/until循环,如果这个命令不是在当前bash下执行,而是在一个脚本中执行,杀掉脚本进程后,会如何?

比如,某脚本test.sh内容如下:

#!/bin/bash

while true;do
sleep 10
done &

或者如下:

#!/bin/bash

while true;do
sleep 10
done & sleep 50

执行这个脚本时,将有两个test.sh进程,其中一个是test.sh进程自身(称之为进程A),一个是为while提供bash环境的子shell进程(称之为进程B)。

当在脚本运行时,杀掉进程A时或者按下CTRL+C(第二个脚本情况)时,如果查看进程的话,会发现后台永远有一个 "test.sh进程+sleep进程" 在运行,如果在while循环中有输出语句的话(比如echo),那么还会时不时地向终端输出点东西。这不是我们想要的结果,我们想要的是脚本结束时,里面的进程也一起结束,而不是有个进程在后台"偷偷地"运行。

之所以出现这样的问题,是因为进程A终止后,while的bash爹(进程B)带着while结构里的进程sleep一起挂到init/systemd下了,而且很不幸,这是个while循环,会一直不断地在后台运行。

应该怎样解决这种问题?可以将所有的test.sh进程都杀掉,比如使用killall sleep test.sh命令。还有更好的方法,见我的另一篇文章,专门写这个问题:如何让shell脚本自杀

5.本文的总结

  1. 其实本文讲的全是子shell的内容,尽管文中很少出现子shell的字眼。。
  2. bash内置命令的执行依赖于bash进程提供的执行环境。
  3. 当前bash环境下执行bash内置命令不会新生成bash进程,除非将它放进后台。
  4. 杀掉后台任务的父进程,后台任务会挂到pid=1的init/systemd进程下。
  5. 终端进程、bash进程和后台任务之间的关系:军营、将军、小兵。
    • (1).终端进程为bash进程和其他进程提供生存环境。
    • (2).终端进程往往会和一个bash进程绑定,这个bash进程具有终端的控制权,也就是管理军营。
    • (3).杀掉终端的管理bash进程,终端进程也会随之终止。
    • (4).bash进程是后台任务的暂时管理者。当bash进程终止时,后台任务就会挂到pid=1的进程下接受init/systemd的管理。
  6. 杀掉终端进程,会发送SIGHUP信号给终端进程组中的所有进程。收到SIGHUP信号后,这些进程都会终止,包括bash进程和后台任务。
  7. 让后台任务脱离终端的方法,除了nohup、screen、tmux等三方工具,bash自身也能实现。只需将后台任务放进子shell中执行即可(最简单的方法(sleep 30 &)),或者用disown命令将后台任务移除后台,或disown -h设置后台进程忽略SIGHUP信号。
  8. 分析下面的脚本,为什么执行过程中按下 CTRL+C 后,还会时不时地向终端上输出点东西,如何解决这个问题?
#!/bin/bash
while true;do
sleep 3
echo "hello world! hello world! "
done &
sleep 60

最后,本文的姊妹篇:

  1. 子shell以及什么时候进入子shell
  2. 如何让shell脚本自杀

bash内置命令的特殊性,后台任务的"本质"的更多相关文章

  1. Bash内置命令exec和重定向

    Bash内置命令exec可以替换当前程序而不需要启动一个新的进程,可以改变标准输入和输出而不需要启动一个新的子进程.如果文件用exec打开,read命令就会把文件指针每次指向下一行直到文件的末尾,如果 ...

  2. Bash内置命令

    Bash有很多内置命令,因为这些命令是内置的,因此bash不需要在磁盘上为它们定位,执行速度更快. 1)列出所有内置命令列表$enable 2)关闭内置命令test$enable -n test 3) ...

  3. bash内置命令mapfile:读取文件内容到数组

    bash提供了两个内置命令:readarray和mapfile,它们是同义词.它们的作用是从标准输入读取一行行的数据,然后每一行都赋值给一个数组的各元素.显然,在shell编程中更常用的是从文件.从管 ...

  4. Linux bash内置命令集

    man cd  -->查询不到,所以会提示bash的内置命令 . alias bg bind break builtin caller cd command compgen complete c ...

  5. 哪一个 bash 内置命令能够进行数学运算?

    bash shell 的内置命令 let 可以进行整型数的数学运算. #! /bin/bash--let c=a+b--

  6. 哪一个 bash 内置命令能够进行数学运算?

    bash shell 的内置命令 let 可以进行整型数的数学运算. #! /bin/bash - - let c=a+b - -

  7. Linux内置命令

    主要Shell内置命令 Shell有很多内置在其源代码中的命令.这些命令是内置的,所以Shell不必到磁盘上搜索它们,执行速度因此加快.不同的Shell内置命令有所不同. A.2.1 bash内置命令 ...

  8. Shell内置命令

    主要Shell内置命令 Shell有很多内置在其源代码中的命令.这些命令是内置的,所以Shell不必到磁盘上搜索它们,执行速度因此加快.不同的Shell内置命令有所不同. A.2.1  bash内置命 ...

  9. Bash基础——内置命令

    前言 Shell有很多内置在其源代码中的命令.由于命令是内置的,所以Shell不必到磁盘上搜索它们.内置命令执行速度更快,不同的Shell内置命令有所不同. 如何查找内置命令 之前查了好久怎么收索内置 ...

随机推荐

  1. 第47章:MongoDB-用户管理

    ①用户管理 在MongoDB里面默认情况下只要是进行连接都可以不使用用户名与密码,因为要想让其起作用,则必须具备以下两个条件: ·条件一:服务器启动的时候打开授权认证: ·条件二:需要配置用户名和密码 ...

  2. Java List中迭代器遍历

    在java中,List接口从Collection接口中继承了 iterator()函数,返回值是一个T类型的迭代器(泛型),T是List中元素的类型 public class TestListAndI ...

  3. 20155205 郝博雅 《网络对抗技术》Exp1 PC平台逆向破解

    20155205 郝博雅 <网络对抗技术>Exp1 PC平台逆向破解 一.实验准备 1. 掌握NOP.JNE.JE.JMP.CMP汇编指令的机器码 NOP:NOP指令即"空指令& ...

  4. Servlet中的jsp内置对象

    Servlet和jsp本质相同,那么为什么还要使用jsp呢,原来的servlet又有什么不好的呢. Servlet和jsp可以做完全相同的事情,就要借助jsp的内置对象们,比如request,resp ...

  5. 《python语言程序设计》_第二章笔记

    #2.2_编写一个简单的程序 项目1: 设计:radius=20,求面积area? 程序: radius=20 #给变量radius复制area=radius*radius*3.14159 #编写ar ...

  6. W3C标准和规范

    W3C标准万维网联盟标准. 万维网联盟(外语缩写:W3C)标准不是某一个标准,而是一系列标准的集合.网页主要由三部分组成:结构(Structure).表现(Presentation)和行为(Behav ...

  7. sphinx-doc的中文搜索

    第一,你的系统需要安装jieba类库, pip install jieba 第二,接下来修改sphinx的conf.py文件,为项目设置为中文的搜索配置. # Language to be used ...

  8. 一篇入门 -- Scala 反射

    本篇文章主要让大家理解什么是Scala的反射, 以及反射的分类, 反射的一些术语概念和一些简单的反射例子. 什么是反射 我们知道, Scala是基于JVM的语言, Scala编译器会将Scala代码编 ...

  9. Python爬虫1-使用urlopen

    GitHub代码练习地址:https://github.com/Neo-ML/PythonPractice/blob/master/SpiderPrac01_urlopen.py 爬虫简介- 爬虫定义 ...

  10. 吴恩达机器学习笔记25-神经网络的模型表示2(Model Representation of Neural Network II)

    ( FORWARD PROPAGATION ) 相对于使用循环来编码,利用向量化的方法会使得计算更为简便.以上面的神经网络为例,试着计算第二层的值: 这只是针对训练集中一个训练实例所进行的计算.如果我 ...