7. 作业控制

作业控制(Job Control)是一种允许用户通过一个终端同时管理多个进程(作业)的机制。

它使得用户可以在一个终端会话中启动、停止、恢复、前后台切换以及终止作业,从而实现对多任务的灵活控制。

本节课我们要学习如下四个命令:

  1. jobs 命令
  2. fg 命令
  3. bg 命令
  4. nohup 命令

以上命令是作业控制(Job Control)相关的命令,主要用于管理进程的前后台运行、终端断开后的持久化以及作业查询等功能。

作业控制中的几个概念。

  1. 作业(Job):
    • 一个作业通常对应一个进程或多个进程通过管道(|)连接的进程组。(管道我们后面再讲)
  2. 前台作业(Foreground Job):
    • 前台作业:独占终端输入/输出,用户需等待其完成才能输入新命令。
  3. 后台作业(Background Job):
    • 在后台运行,不占用终端,用户可继续输入其他命令。

细心的学者会发现,当我们打开一个终端并在终端运行程序时,如果终端关闭了,那么在终端中使用命令启动的进程也停止的运行。为什么呢?终端又有什么作用呢?

先来观察一个现象,我们在终端中运行 python3 always_run.py,如下:

weimingze@mzstudio:~$ python3 always_run.py
PID: 4359

在打来另外一个终端,运行 ps -ejH --forest 查看进程树。我们看到如下:

weimingze@mzstudio:~$ ps -ejH --forest
    PID    PGID     SID TTY          TIME CMD
      2       0       0 ?        00:00:00 kthreadd
    ...
      4       0       0 ?        00:00:00  \_ kworker/R-rcu_gp
   3485    2650    2650 ?        00:00:00  \_ gnome-terminal
   3486    2650    2650 ?        00:00:00  |   \_ gnome-terminal.
   3492    3492    3492 ?        00:00:02  \_ gnome-terminal-
   3502    3502    3502 pts/1    00:00:00      \_ bash
   4359    4359    3502 pts/1    00:03:38          \_ python3

我们会发现 终端进程(gnome-terminal) 启动了 bash 进程,bash 进程启动了 python3 always_run.py 进程。

当终端(gnome-terminal)关闭,则终端进程会向所有的子进程发送 SIGHUP(值为 1) 的信号,子进程收到此信号会执行相应的操作(包括向他的子进程发送SIGHUP信号)然后退出运行。这就是为什么子进程会关闭的原因。

bash 又是什么呢?

bash 是命令的解释执行的程序。bash 接管我们键盘输入,解释执行我们输入的信息并启动进程,并能够让我们能看进程输出的结果。(后面章节会系统全面的讲解 bash)。

如下面这一行提示符就是 bash 的输出。

weimingze@mzstudio:~$ 

任何一个程序都有不变的三个通道:

  1. 标准输入:bash 接管键盘,由 bash 交给前台进程,作为前台进程的输入。
  2. 标准输出:由程序产生的正确信息,交给 bash(默认 bash 会打印在屏幕上)。
  3. 标准错误输出:由程序产生的错误提示信息,交给 bash(默认 bash 会打印在屏幕上)

前面讲述了基础知识,下面我们来讲解作业控制。

启动前台作业

在终端中正常是启动程序是启动前台作业。前台作业会独占标准输入。如:

weimingze@mzstudio:~$ python3 always_run.py

使用 Control + c 向前台作业发送 SIGINT 信号可以终止前台作业。

启动后台作业

在终端中 输入启动程序的命令后,在后面加一个 & 符号则是启动后台作业。后台作业不占标准输入,但默认的标准输出和标准错误输出依旧会在终端显示。如:

weimingze@mzstudio:~$ python3 always_run.py &
[1] 4484
weimingze@mzstudio:~$ PID: 4484

weimingze@mzstudio:~$

再次回车后会显示终端提示符 weimingze@mzstudio:~$ 此时你可以继续输入命令,刚运行的命令启动的进程就是后台作业。此时你的输入是交给 bash,而不是 always_run.py 这个程序。

在运行几次 python3 always_run.py &

查看作业列表

jobs 命令

使用 jobs 命令可以查看当前 bash 启动的所有作业。如:

weimingze@mzstudio:~$ jobs
[1]   Running                 python3 always_run.py &
[2]-  Running                 python3 always_run.py &
[3]+  Running                 python3 always_run.py &
weimingze@mzstudio:~$ jobs -l
[1]   4484 Running                 python3 always_run.py &
[2]-  4486 Running                 python3 always_run.py &
[3]+  4487 Running                 python3 always_run.py &
weimingze@mzstudio:~$

-l 选项会增加 PID 的显示列。

停止作业

在前台作业输入 Control + z 快捷键将向进程发送 SIGTSTP 信号。当前进程进入 停止状态(T)并成为后台作业。如:

weimingze@mzstudio:~$ python3 always_run.py
PID: 4514
^Z
[4]+  Stopped                 python3 always_run.py
weimingze@mzstudio:~$ jobs -l
[1]   4484 Running                 python3 always_run.py &
[2]   4486 Running                 python3 always_run.py &
[3]-  4487 Running                 python3 always_run.py &
[4]+  4514 Stopped                 python3 always_run.py
weimingze@mzstudio:~$

恢复作业

使用 fg 命令bg 命令 可以将进程修改为前台作业或后台作业。

fg命令

将后台作业变为前台作业,如果作业处于 停止状态 则发送 SIGCONT 信号使其进入运行状态。

命令格式

bg %<作业编号>

weimingze@mzstudio:~$ fg %4
python3 always_run.py

PID 为 4514 的进程变成前台作业。再次 Control + z 让其变为后台作业。

bg命令

将作业变为后台作业,向处于 停止状态 的作业发送 SIGCONT 信号使其进入运行状态。

命令格式

fg %<作业编号>

weimingze@mzstudio:~$ jobs -l
[1]   4484 Running                 python3 always_run.py &
[2]   4486 Running                 python3 always_run.py &
[3]-  4487 Running                 python3 always_run.py &
[4]+  4514 Stopped                 python3 always_run.py
weimingze@mzstudio:~$ bg %4
[4]+ python3 always_run.py &
weimingze@mzstudio:~$ jobs -l
[1]   4484 Running                 python3 always_run.py &
[2]   4486 Running                 python3 always_run.py &
[3]-  4487 Running                 python3 always_run.py &
[4]+  4514 Running                 python3 always_run.py &
weimingze@mzstudio:~$

PID 为 4514 的进程依旧是后台作业。但他处于运行状态了。

强制终止作业

使用 kill %<作业编号> 可以向一个后台作业发送 信号 让其退出。如:

weimingze@mzstudio:~$ kill %4
weimingze@mzstudio:~$ jobs -l
[1]   4484 Running                 python3 always_run.py &
[2]   4486 Running                 python3 always_run.py &
[3]-  4487 Running                 python3 always_run.py &
[4]+  4514 Terminated              python3 always_run.py

PID 为 4514 的进程已经终止了,使用 ps 命令也查不到此进程了。

nohup 命令

nohup 用于让 启动的进程 忽略挂断信号(SIGHUP)的操作,让启动的进程编程一个真正的后台进程,即使终端关闭,进程仍会继续执行。

命令格式

nohup 命令 [选项]  &

& 表示推向后台运行。

使用 nohup 执行的程序,他将忽略标准输入,同时将标准输出和标准错误输出不在指向终端,而是保存在文件 'nohup.out' 中。当然我们可以通过输出重定向将其指向其他文件或打印设备。

示例:

weimingze@mzstudio:~$ nohup python3 always_run.py &
[1] 4945
weimingze@mzstudio:~$ nohup: ignoring input and appending output to 'nohup.out'

weimingze@mzstudio:~$ ps -ejH --forest
    PID    PGID     SID TTY          TIME CMD
      2       0       0 ?        00:00:00 kthreadd
      3       0       0 ?        00:00:00  \_ pool_workqueue_release
      ...
   3399    3399    3399 ?        00:00:00  \_ gvfsd-metadata
   3485    2650    2650 ?        00:00:00  \_ gnome-terminal
   3486    2650    2650 ?        00:00:00  |   \_ gnome-terminal.
   3492    3492    3492 ?        00:00:02  \_ gnome-terminal-
   3502    3502    3502 pts/1    00:00:00      \_ bash
   4945    4945    3502 pts/1    00:01:06          \_ python3
weimingze@mzstudio:~$ jobs
[1]+  Running                 python3 always_run.py &
weimingze@mzstudio:~$

可见此时 PID 为 4945 的进程是 bash 进程的子进程。然后我们关闭终端。再查看进程。

weimingze@mzstudio:~$ ps -ejH --forest
    PID    PGID     SID TTY          TIME CMD
      2       0       0 ?        00:00:00 kthreadd
      3       0       0 ?        00:00:00  \_ pool_workqueue_release
      ...
   3399    3399    3399 ?        00:00:00  \_ gvfsd-metadata
   4945    4945    3502 ?        00:05:27  \_ python3

此时 PID 为 4945 的进程依旧存在,但他的父进程已经不在是 bash 进程,而是 systemd 进程了。

此时 PID 为 4945 的进程已经不在依赖任何终端,他变成了 守护进程

守护进程

守护进程是指不依附于某个终端而独立运行的程序。他是一个后台作业,通常提供某种服务,比如 网站前端服务 nginx、ssh 远程连接、Django 或 Flask 后端服务程序等。

练习:

  1. 使用 python3 always_run.py & 运行程序将其推向后台。
  2. 查询后台任务。
  3. 将后台任务恢复到前台。
  4. 使用 Control + c 终止这个程序。