MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Bash进程管理:后台运行与作业控制

2021-08-254.6k 阅读

Bash进程管理基础概念

进程的概念

在计算机系统中,进程是程序的一次执行实例。当一个程序被加载到内存并开始执行时,它就成为了一个进程。每个进程都有自己独立的地址空间、系统资源(如文件描述符)以及执行上下文。在Bash中,每当你运行一个命令,实际上就是启动了一个新的进程。例如,当你在终端中输入 ls 命令,系统就会创建一个新的进程来执行 ls 程序,该进程会读取当前目录的内容并将其显示在终端上。

前台与后台进程

在Bash中,进程可以以前台或后台的方式运行。前台进程是指在终端中直接运行,并且会占用终端输入输出的进程。例如,当你运行 vim 编辑器时,vim 进程是前台进程,此时你无法在同一终端输入其他命令,直到你退出 vim。而后台进程则是在终端的后台运行,不会占用终端的输入输出,你可以继续在终端中输入其他命令。后台进程通常用于执行一些长时间运行的任务,比如数据处理、文件下载等,这样你就可以在任务执行的同时进行其他操作。

后台运行进程

使用 & 符号启动后台进程

在Bash中,最常用的启动后台进程的方法是在命令的末尾加上 & 符号。例如,假设你有一个需要长时间运行的脚本 long_running_script.sh,内容如下:

#!/bin/bash
for i in {1..1000000}
do
    echo $i
    sleep 0.01
done

你可以使用以下命令将其作为后台进程运行:

./long_running_script.sh &

当你执行这个命令后,脚本会在后台开始运行,终端会立即返回提示符,你可以继续执行其他命令。同时,终端会输出类似如下的信息:

[1] 12345

这里的 [1] 表示作业编号,12345 是该进程的进程ID(PID)。作业编号是Bash为当前会话中运行的后台作业分配的编号,而进程ID是系统为进程分配的唯一标识符。

查看后台运行的进程

  1. 使用 jobs 命令jobs 命令用于查看当前终端会话中所有后台作业的状态。例如,如果你已经启动了上述的 long_running_script.sh 脚本作为后台作业,运行 jobs 命令会得到类似如下的输出:
[1]+  Running                 ./long_running_script.sh &

这里的 [1] 是作业编号,+ 号表示这是最近运行的后台作业,Running 表示作业的当前状态是正在运行。 2. 使用 ps 命令ps 命令是一个更通用的查看系统进程的工具。你可以使用 ps -ef 命令查看系统中所有进程的详细信息。例如,要查找刚刚启动的 long_running_script.sh 脚本对应的进程,可以使用 ps -ef | grep long_running_script.sh,输出可能如下:

user     12345 12340  0 10:00 pts/0    00:00:00 ./long_running_script.sh

这里显示了进程的所有者(user)、进程ID(12345)、父进程ID(12340)、启动时间(10:00)、终端设备(pts/0)以及进程执行的命令。

后台进程的输出处理

当一个进程在后台运行时,它的标准输出(stdout)和标准错误输出(stderr)默认还是会输出到终端。这可能会导致终端输出混乱,尤其是当后台进程输出大量信息时。为了避免这种情况,你可以将后台进程的输出重定向到文件。例如,将 long_running_script.sh 的输出重定向到 output.log 文件:

./long_running_script.sh > output.log 2>&1 &

这里 > 表示将标准输出重定向到 output.log 文件,2>&1 表示将标准错误输出也重定向到标准输出所指向的文件,即 output.log 文件。这样,后台进程的所有输出都会写入到 output.log 文件中,而不会在终端上显示。

作业控制

作业的暂停与恢复

  1. 暂停作业(Ctrl + Z:在前台运行的进程可以通过按下 Ctrl + Z 组合键来暂停。例如,假设你正在运行 top 命令来实时查看系统资源使用情况,按下 Ctrl + Z 后,top 进程会暂停,终端会输出类似如下的信息:
[1]+  Stopped                 top

这表明 top 进程已经被暂停,并且作业编号为 [1]。此时,该作业并没有结束,只是暂时停止了执行。 2. 恢复作业到前台(fg 命令):使用 fg 命令可以将暂停的作业恢复到前台继续运行。例如,如果要恢复刚刚暂停的 top 作业,可以运行 fg %1,其中 %1 表示作业编号为 1 的作业。如果只有一个暂停的作业,也可以直接运行 fg,Bash会默认恢复最近暂停的作业。 3. 恢复作业到后台(bg 命令)bg 命令用于将暂停的作业恢复到后台继续运行。同样以暂停的 top 作业为例,运行 bg %1 会将 top 作业恢复到后台运行,终端会输出如下信息:

[1]+  top &

表示 top 作业已经在后台继续运行。

作业的切换

  1. 在后台作业之间切换:如果你有多个后台作业在运行,可以使用 fgbg 命令结合作业编号来在不同的后台作业之间切换。例如,假设你有两个后台作业,作业编号分别为 [1][2]。要将作业 [2] 切换到前台运行,可以使用 fg %2;要将作业 [1] 恢复到后台运行,可以使用 bg %1
  2. 将前台作业切换到后台:有时候你可能希望将正在前台运行的作业切换到后台。首先,使用 Ctrl + Z 暂停该作业,然后使用 bg 命令将其恢复到后台运行。例如,正在前台运行 ping 命令,按下 Ctrl + Z 暂停后,再运行 bg %1 就可以将 ping 命令作为后台作业继续运行。

作业的终止

  1. 使用 kill 命令终止作业kill 命令用于向进程发送信号,默认情况下发送的是 SIGTERM 信号,该信号会请求进程正常终止。你可以使用进程ID或作业编号来终止作业。例如,要终止作业编号为 [1] 的作业,可以运行 kill %1;要终止进程ID为 12345 的进程,可以运行 kill 12345
  2. 使用 SIGKILL 信号强制终止作业:有时候,某些进程可能无法响应 SIGTERM 信号,这时可以使用 SIGKILL 信号(信号编号为 9)来强制终止进程。例如,要强制终止进程ID为 12345 的进程,可以运行 kill -9 12345。但是需要注意,使用 SIGKILL 信号强制终止进程可能会导致数据丢失或系统状态不一致,所以应该尽量在其他方法无效时才使用。

复杂场景下的进程管理与作业控制

进程组与会话

  1. 进程组:进程组是一组相关进程的集合,它们共享一个进程组ID(PGID)。在Bash中,当你启动一个命令时,该命令及其所有子进程通常属于同一个进程组。例如,当你运行一个脚本,脚本中又启动了其他命令,这些命令都属于同一个进程组。进程组可以方便地对一组相关进程进行管理,比如向整个进程组发送信号。可以使用 ps -ejH 命令查看进程组信息,输出中 PGID 列表示进程组ID。
  2. 会话:会话是一组进程组的集合,一个会话可以包含一个前台进程组和多个后台进程组。每个会话都有一个会话ID(SID),通常与创建会话的进程的进程ID相同。会话用于管理终端相关的操作,例如控制终端的输入输出。在Bash中,当你打开一个新的终端会话时,就创建了一个新的会话。可以使用 ps -ejH | grep SID 命令来查看会话相关信息,其中 SID 列表示会话ID。

守护进程的创建

  1. 守护进程的概念:守护进程是一种在后台运行,并且不依赖于终端的进程。它们通常在系统启动时自动启动,并一直运行直到系统关闭,用于提供系统级别的服务,如网络服务(httpdsshd 等)、文件系统服务(nfsd 等)。守护进程与普通后台进程的区别在于,守护进程脱离了终端控制,不会因为终端关闭而终止。
  2. 使用 nohup 命令创建简单守护进程nohup 命令可以用于创建一个不依赖于终端的进程,即简单的守护进程。例如,要运行 long_running_script.sh 作为守护进程,可以使用以下命令:
nohup ./long_running_script.sh &

nohup 命令会忽略 SIGHUP 信号(该信号在终端关闭时会发送给相关进程),从而确保进程不会因为终端关闭而终止。同时,nohup 会将进程的标准输出重定向到 nohup.out 文件,如果当前目录下没有该文件,会自动创建。 3. 手动创建守护进程的步骤:手动创建守护进程需要一些系统调用和步骤。首先,使用 fork() 系统调用创建一个子进程,父进程退出,这样可以使子进程脱离终端控制。然后,子进程使用 setsid() 系统调用创建一个新的会话,使其成为会话组长,进一步脱离终端。接着,通常会将标准输入、标准输出和标准错误输出重定向到 /dev/null,以避免不必要的输出。以下是一个简单的手动创建守护进程的示例脚本:

#!/bin/bash
# 创建子进程
pid=$(fork)
if [ $pid -eq 0 ]; then
    # 子进程继续执行
    # 创建新的会话
    setsid
    # 重定向标准输入、输出和错误
    exec </dev/null
    exec >/dev/null 2>&1
    # 守护进程执行的任务
    while true
    do
        echo "守护进程正在运行"
        sleep 5
    done
else
    # 父进程退出
    exit 0
fi

这个脚本首先使用 fork() 创建子进程,父进程直接退出。子进程使用 setsid() 创建新会话,然后将标准输入、输出和错误重定向到 /dev/null,最后进入一个无限循环,模拟守护进程执行的任务。

处理并发进程

  1. 使用 wait 命令等待多个进程完成:在脚本中,有时候你可能需要启动多个进程并等待它们全部完成后再继续执行后续操作。可以使用 wait 命令来实现这一点。例如,假设你有两个脚本 script1.shscript2.sh,你希望同时启动它们并等待它们都完成:
./script1.sh &
pid1=$!
./script2.sh &
pid2=$!
wait $pid1 $pid2
echo "两个脚本都已完成"

这里 $! 表示最近一个后台运行命令的进程ID。通过记录两个脚本的进程ID,然后使用 wait 命令等待这两个进程完成,最后输出提示信息。 2. 使用 parallel 工具进行并行处理parallel 是一个强大的工具,用于在多个CPU核心上并行执行命令。首先需要安装 parallel,在基于Debian或Ubuntu的系统上,可以使用 sudo apt install parallel 命令安装。安装完成后,假设你有一个包含多个命令的文件 commands.txt,内容如下:

echo "任务1"
echo "任务2"
echo "任务3"

你可以使用以下命令并行执行这些命令:

parallel < commands.txt

parallel 会自动将这些命令分配到多个CPU核心上并行执行,大大提高执行效率。

进程管理与作业控制的应用场景

自动化任务执行

在系统管理和运维中,经常需要执行一些自动化任务,如定期备份数据、更新软件包等。可以通过将这些任务编写成脚本,并使用Bash的进程管理和作业控制功能来实现自动化执行。例如,编写一个备份脚本 backup.sh

#!/bin/bash
# 备份数据库
mysqldump -u root -p password mydatabase > /backup/mydatabase_$(date +%Y%m%d).sql
# 备份文件
tar -czvf /backup/files_$(date +%Y%m%d).tar.gz /var/www/html

然后使用 crontab 来定期运行这个脚本。编辑 crontab 文件(例如运行 crontab -e),添加如下内容:

0 2 * * * /path/to/backup.sh &

这表示每天凌晨2点执行 backup.sh 脚本,并将其作为后台进程运行,这样就不会影响系统的其他操作。

系统监控与故障处理

  1. 使用进程管理进行系统监控:可以编写脚本来监控系统关键进程的状态。例如,监控 httpd 服务是否在运行,如果未运行则重新启动。以下是一个简单的监控脚本 monitor_httpd.sh
#!/bin/bash
pid=$(pgrep httpd)
if [ -z "$pid" ]; then
    echo "httpd服务未运行,正在启动..."
    systemctl start httpd
else
    echo "httpd服务正在运行,进程ID:$pid"
fi

可以使用 crontab 定期运行这个脚本,以确保 httpd 服务始终处于运行状态。 2. 故障处理中的作业控制:在系统出现故障时,可能需要终止一些异常运行的进程,并重新启动相关服务。例如,当系统内存不足时,可能需要终止一些占用大量内存的进程。可以编写一个脚本 handle_memory_issue.sh

#!/bin/bash
# 查找占用大量内存的进程
high_mem_processes=$(ps -eo pid,pmem --sort=-pmem | head -n 10 | awk '{print $1}')
for pid in $high_mem_processes
do
    echo "终止进程 $pid,因为其占用大量内存"
    kill -9 $pid
done
# 重启相关服务
systemctl restart some_service

这个脚本首先查找占用内存最多的10个进程,并强制终止它们,然后重启相关服务,以尝试恢复系统正常运行。

大数据处理与分析

在大数据处理和分析场景中,经常需要运行一些长时间运行的任务,如数据清洗、模型训练等。可以使用Bash的进程管理功能将这些任务作为后台作业运行,并通过作业控制来管理它们。例如,假设你有一个数据处理脚本 data_processing.sh,它需要处理大量数据文件:

#!/bin/bash
for file in /data/*.csv
do
    python process_csv.py $file > processed_$file.log 2>&1
done

你可以将这个脚本作为后台作业运行:

./data_processing.sh &

同时,可以使用 jobs 命令查看作业状态,使用 kill 命令在必要时终止作业,以确保数据处理任务的顺利进行。如果数据量非常大,可以结合 parallel 工具来并行处理数据文件,提高处理效率。例如:

parallel python process_csv.py ::: /data/*.csv > processed.log 2>&1 &

这样可以同时处理多个数据文件,大大缩短数据处理时间。

注意事项与常见问题

后台进程与终端的关系

  1. 终端关闭对后台进程的影响:默认情况下,当终端关闭时,与该终端相关的后台进程会收到 SIGHUP 信号,通常会导致进程终止。这是因为终端关闭时,系统会清理与该终端相关的资源。为了避免这种情况,可以使用 nohup 命令启动后台进程,或者在脚本中捕获并忽略 SIGHUP 信号。例如,在脚本开头添加如下代码:
#!/bin/bash
trap "" SIGHUP
# 脚本主体内容

这样脚本就会忽略 SIGHUP 信号,即使终端关闭,脚本也会继续运行。 2. 后台进程的输入问题:后台进程默认无法从终端获取输入,因为它们没有与终端的交互连接。如果你的进程需要用户输入,应该在启动进程前将输入准备好,或者通过其他方式(如文件读取)提供输入。例如,如果你有一个脚本 input_script.sh 需要用户输入一个数字:

#!/bin/bash
read -p "请输入一个数字:" num
echo "你输入的数字是:$num"

你不能直接将其作为后台进程运行并期望从终端输入。可以将输入写入文件,然后让脚本从文件读取,修改后的脚本如下:

#!/bin/bash
num=$(cat input.txt)
echo "你输入的数字是:$num"

然后运行 echo 10 > input.txt;./input_script.sh &,这样就可以在后台运行脚本并提供输入。

作业控制中的常见错误

  1. 作业编号与进程ID混淆:在使用 fgbgkill 等命令时,需要正确区分作业编号和进程ID。作业编号是Bash为当前会话中的作业分配的编号,格式为 [n],而进程ID是系统为进程分配的唯一标识符,是一个数字。例如,kill %1 是终止作业编号为 1 的作业,而 kill 12345 是终止进程ID为 12345 的进程。如果混淆使用,可能会导致错误的进程被终止。
  2. 信号处理不当:在使用 kill 命令发送信号时,需要注意信号的含义和影响。如前面提到的,SIGKILL 信号是强制终止进程,可能会导致数据丢失或系统问题,应该谨慎使用。另外,不同的进程对不同信号的处理方式可能不同,有些进程可能会忽略某些信号。例如,bash 进程默认会忽略 SIGPIPE 信号,这是在管道的一端关闭时发送的信号。如果你的脚本涉及管道操作并且需要处理 SIGPIPE 信号,应该在脚本中显式捕获并处理。

进程管理在不同系统中的差异

  1. Bash版本差异:不同版本的Bash可能在进程管理和作业控制的某些功能上存在差异。例如,一些较新的Bash版本可能支持更高级的作业控制语法或功能。在编写脚本时,应该尽量使用通用的、兼容性好的语法,以确保脚本在不同版本的Bash中都能正常运行。可以通过查看Bash的手册页(man bash)来了解当前版本支持的功能。
  2. 操作系统差异:不同的操作系统对进程管理的底层实现可能存在差异。例如,在Linux系统和macOS系统中,虽然都支持Bash,但一些系统调用和命令的行为可能略有不同。在编写跨系统的脚本时,需要注意这些差异。例如,在Linux系统中,ps 命令的输出格式和选项在不同的发行版中可能会有一些变化,而在macOS系统中,ps 命令的输出格式和行为也有其特点。可以使用一些跨系统兼容的工具或编写条件判断来处理这些差异。例如,在脚本中可以使用如下代码来判断操作系统:
if [ -f /etc/os-release ]; then
    source /etc/os-release
    if [ "$ID" = "ubuntu" ]; then
        # Ubuntu系统相关操作
    elif [ "$ID" = "centos" ]; then
        # CentOS系统相关操作
    fi
elif [ "$(uname)" = "Darwin" ]; then
    # macOS系统相关操作
fi

通过这种方式,可以根据不同的操作系统执行不同的进程管理和作业控制操作,提高脚本的兼容性。