OS Programme Lecture #5
1. Scripting for Process Management
(1) BASH - Process pause/continuation with the kill command
在第一个控制台中运行脚本loop.sh
切换到第二个控制台,找到loop.sh进程的PID
我们现在用kill来中止进程,kill -STOP PID
回到第一个控制台,此时loop进程中止,非终止,仍然在就绪状态READY STATE
切换到第二个控制台,ps aux | grep bash,可以看到loop进程没有消失
要继续该进程,kill -CONT PID
最后用kill PID终止执行
(2) BASH - Search and kill process
在第一个控制台中打开一个应用程序,例如网页浏览器firefox,输入firefox
切换到第二个控制台,搜索firefox的进程PID,kill该进程
(3) Processes in Windows
回到Windows系统中,同样写一个脚本实现无限循环,loop.ps1,参考代码如下:
$num=1
while ($true){
$square=$num*$num
echo "$num $square"
$num+=1
}
echo "Programme completed ..."
在第一个控制台中执行脚本loop.ps1
打开一个新的控制台,尝试查找loop进程的PID,Get-Process -name power*,记录该进程PID
在这个控制台中,终止该进程,Stop-Process PID
看一下脚本是否被终止?
2. Parent and Children Processes
在BASH脚本里,我们可以通过$$获得一个进程的PID,例如echo $$,用ps aux | grep bash命令行查看与echo $$输出的PID是否有一致的?
编写以下脚本test.sh:
#!/bin/bash
trap "echo signal received ..." SIGINT
echo "The script PID is $$"
sleep 30
用whatis sleep查看sleep的意思
在控制台运行该脚本,查看最终输出情况
在控制台运行该脚本,用Ctrl+C终止该进程,查看输出情况
在控制台运行该脚本,在另一个控制台用kill终止该脚本,查看输出情况
为什么输出会不一样?!
回答这个问题前,必须了解一下什么是进程组(process groups),前台(forground)和后台(background)任务!
同一个进程组的进程共享一个pgid(process group id)
当进程组的成员创建子进程时,该进程成为同一进程组的成员。每个进程组都有一个领导者;我们可以很容易地识别它,因为它的 pid 和 pgid 是相同的!
我们可以使用 ps 命令可视化正在运行的进程的 pid 和 pgid。命令的输出可以自定义,以便只显示我们感兴趣的字段:在本例中为 CMD、PID 和 PGID。我们通过使用 -o 选项来做到这一点,提供一个逗号分隔的字段列表作为参数:
$ps -a -o pid,pgid,cmd
如果我们执行test.sh,在脚本运行期间在另一个控制台$ps -a -o pid,pgid,cmd,观察结果!
结论:主进程的sleep 30创建了一个子进程,两个进程在一个组!
当我们在启动test.sh脚本的终端上按下 CTRL-C 时,信号不仅发送到父进程,而且发送到整个进程组。
哪个进程组?终端的前台进程组。该组的所有进程成员都称为前台进程,所有其他进程都称为后台进程。
#前台和后台进程#:
为了便于实现作业控制的用户界面,操作系统维护了当前终端进程组 ID 的概念。
该进程组的成员(进程组 ID 等于当前终端进程组 ID 的进程)接收键盘生成的信号,例如 SIGINT。
这些进程在前台。后台进程是那些进程组 ID 与终端不同的进程;这样的进程不受键盘产生的信号的影响。
相反,当我们使用 kill 命令发送 SIGINT 信号时,我们只针对父进程的 pid;
当 Bash 在等待程序完成时收到信号时,它会表现出一种特定的行为:该信号的“陷阱代码”在该进程完成之前不会执行。
这就是为什么只有在 sleep 命令退出后才会显示“收到信号”消息的原因。
要出现当我们使用 kill 命令在终端中按 CTRL-C 发送信号时发生的情况,我们必须以进程组为目标。
$kill -2 -(pid of the process leader)
3. 从脚本内部管理信号传播
考虑一个问题:现在,假设我们从非交互式 shell 启动一个长时间运行的脚本,并且我们希望所述脚本自动管理信号传播,
以便当它接收到诸如 SIGINT 或 SIGTERM 之类的信号时,它会终止其可能长时间运行的子脚本,
最终执行一些清理退出前的任务。我们如何做到这一点?
就像我们之前所做的那样,我们可以处理在陷阱中接收到信号的情况;
然而,正如我们所见,如果在 shell 等待程序完成时收到信号,则“陷阱代码”仅在子进程退出后才会执行。
这不是我们想要的:我们希望在父进程收到信号后立即处理陷阱代码。
为了实现我们的目标,我们必须在后台执行子进程:我们可以通过在命令后放置 & 符号来实现。
稍微改写test.sh脚本:
#!/bin/bash
trap "echo signal received ..." SIGINT
echo "The script PID is $$"
sleep 30 &
观察程序输出,$ps -a -o pid,pgid,cmd
这样写仍然有一个问题:父进程将在执行 sleep 30 命令后立即退出,让我们没有机会在它结束或被中断后执行清理任务。
我们可以使用shell内置的wait来解决这个问题。
#等待#:
等待由 ID 标识的每个进程,该 ID 可以是进程 ID 或作业规范,并报告其终止状态。
如果未给出 ID,则等待所有当前活动的子进程,返回状态为零。
脚本改写如下:
#!/bin/bash
trap "echo signal received ..." SIGINT
echo "The script PID is $$"
sleep 30 &
wait $!
在我们设置一个进程在后台执行后,我们可以将$!作为参数传递给 wait 让父进程等待它的子进程。
但是还有一个问题:接收到在脚本内部的陷阱中处理的信号,导致等待内置函数立即返回,而不是实际等待后台命令的终止。
最终我们可以改变脚本如下:
#!/bin/bash
cleanup() {
echo "cleaning up..."
# Our cleanup code goes here
}
trap 'echo signal received!; kill "${child_pid}"; wait "${child_pid}"; cleanup' SIGINT SIGTERM
echo "The script pid is $"
sleep 30 &
child_pid="$!"
wait "${child_pid}"
在脚本中,我们创建了一个清理函数,我们可以在其中插入清理代码,并使我们的陷阱也捕获 SIGTERM 信号。
以下是当我们运行此脚本并向其发送这两个信号之一时发生的情况:
(1) 启动脚本并在后台执行 sleep 30 命令;
(2) 子进程的pid“存储”在child_pid变量中;
(3) 脚本等待子进程终止;
(4) 脚本接收到 SIGINT 或 SIGTERM 信号;
(5) wait命令立即返回,不等待child终止。
此时陷阱trap被执行:
(1) SIGTERM 信号(kill 默认值)被发送到 child_pid;
(2) 我们等待以确保子进程在收到此信号后终止;
(3) 等待返回后,我们执行清理功能。
此为广大操作系统实践课课件总结,版权归广大所有