Shell信号发送与捕捉


Shell 信号发送与捕捉

在脚本执行过程中, 可能会被一些键盘操作快捷方式所打断, 影响脚本运行

# HUP(1):  挂起、睡眠, 
本信号在用户终端连接(正常或非正常)结束时发出, 通常是在终端的控制进程结束时, 通知同一session内的各个作业, 这时它们与控制终端不再关联。

登录Linux时,系统会分配给登录用户一个终端(Session)。在这个终端运行的所有程序,包括前台进程组和后台进程组,一般都 属于这个 Session。当用户退出Linux登录时,前台进程组和后台有对终端输出的进程将会收到SIGHUP信号。这个信号的默认操作为终止进程,因此前台进 程组和后台有终端输出的进程就会中止。不过可以捕获这个信号,比如wget能捕获SIGHUP信号,并忽略它,这样就算退出了Linux登录wget也 能继续下载。

此外,对于与终端脱离关系的守护进程,这个信号用于通知它重新读取配置文件。

# INT(2):  中断, 通常因为按下ctrl+c而产生的信号,用于通知前台进程组终止进程。
# QUIT(3): 退出,和SIGINT类似, 但由QUIT字符(通常是Ctrl-\)来控制. 进程在因收到SIGQUIT退出时会产生core文件, 在这个意义上类似于一个程序错误信号。

# TSTP(20): 停止进行运行,通常因为按下ctrl+z而产生的信号

# KILL (9)
用来立即结束程序的运行. 本信号不能被阻塞、处理和忽略。如果管理员发现某个进程终止不了,可尝试发送这个信号。
# TERM(15): 
终止,是不带参数时kill默认发送的信号,默认是杀死进程,与SIGKILL不同的是该信号可以被阻塞和处理。通常用TERM信号来要求程序自己正常退出,如果进程终止不了,我们才会尝试SIGKILL

# ===============了解===============
# ABRT(6): 中止, 通常因某些严重错误产生的引号   

# SIGCHLD 
子进程结束时, 父进程会收到这个信号。

如果父进程没有处理这个信号,也没有等待(wait)子进程,子进程虽然终止,但是还会在内核进程表中占有表项,这时的子进程称为僵尸 进程。这种情 况我们应该避免(父进程或者忽略SIGCHILD信号,或者捕捉它,或者wait它派生的子进程,或者父进程先终止,这时子进程的终止自动由init进程 来接管)

# 更多详见:man 7 signal

1、Linux信号类型

信号(Signal):信号是在软件层次上对中断机制的一种模拟,通过给一个进程发送信号,执行相应的处理函数。

进程可以通过三种方式来响应一个信号:

1)忽略信号,即对信号不做任何处理,其中有两个信号不能忽略:SIGKILL及SIGSTOP。

2)捕捉信号。

3)执行缺省操作,Linux对每种信号都规定了默认操作。

Linux究竟采用上述三种方式的哪一个来响应信号呢?取决于传递给响应的API函数。

Linux支持的信号有:

编号 信号名称 缺省动作 描述
1 SIGHUP 终止 终止进程,挂起
2 SIGINT 终止 键盘输入中断命令,一般是CTRL+C
3 SIGQUIT CoreDump 键盘输入退出命令,一般是CTRL+\
4 SIGILL CoreDump 非法指令
5 SIGTRAP CoreDump trap指令发出,一般调试用
6 SIGABRT CoreDump abort(3)发出的终止信号
7 SIGBUS CoreDump 非法地址
8 SIGFPE CoreDump 浮点数异常
9 SIGKILL 终止 立即停止进程,不能捕获,不能忽略
10 SIGUSR1 终止 用户自定义信号1,像Nginx就支持USR1信号,用于重载配置,重新打开日志
11 SIGSEGV CoreDump 无效内存引用
12 SIGUSR2 终止 用户自定义信号2
13 SIGPIPE 终止 管道不能访问
14 SIGALRM 终止 时钟信号,alrm(2)发出的终止信号
15 SIGTERM 终止 终止信号,进程会先关闭正在运行的任务或打开的文件再终止,有时间进程在有运行的任务而忽略此信号。不能捕捉
16 SIGSTKFLT 终止 处理器栈错误
17 SIGCHLD 可忽略 子进程结束时,父进程收到的信号
18 SIGCONT 可忽略 让终止的进程继续执行
19 SIGSTOP 停止 停止进程,不能忽略,不能捕获
20 SIGSTP 停止 停止进程,一般是CTRL+Z
21 SIGTTIN 停止 后台进程从终端读数据
22 SIGTTOU 停止 后台进程从终端写数据
23 SIGURG 可忽略 紧急数组是否到达socket
24 SIGXCPU CoreDump 超出CPU占用资源限制
25 SIGXFSZ CoreDump 超出文件大小资源限制
26 SIGVTALRM 终止 虚拟时钟信号,类似于SIGALRM,但计算的是进程占用的时间
27 SIGPROF 终止 类似与SIGALRM,但计算的是进程占用CPU的时间
28 SIGWINCH 可忽略 窗口大小改变发出的信号
29 SIGIO 终止 文件描述符准备就绪,可以输入/输出操作了
30 SIGPWR 终止 电源失败
31 SIGSYS CoreDump 非法系统调用

CoreDump(核心转储):当程序运行过程中异常退出时,内核把当前程序在内存状况存储在一个core文件中,以便调试。

Linux支持两种信号:

一种是标准信号,编号1-31,称为非可靠信号(非实时),不支持队列,信号可能会丢失,比如发送多次相同的信号,进程只能收到一次,如果第一个信号没有处理完,第二个信号将会丢弃。

另一种是扩展信号,编号32-64,称为可靠信号(实时),支持队列,发多少次进程就可以收到多少次。

信号类型比较多,我们只要了解下,记住几个常用信号就行了,红色标记的我觉得需要记下。

发送信号一般有两种情况:

一种是内核检测到系统事件,比如键盘输入CTRL+C会发送SIGINT信号。

另一种是通过系统调用kill命令来向一个进程发送信号。

2、kill命令

kill命令发送信号给进程。

命令格式:kill [-s sigspec | -n signum | -sigspec] pid | jobspec ...

kill -l [sigspec]
-s  # 信号名称
-n  # 信号编号
-l  # 打印编号1-31信号名称

示例:

给一个进程发送终止信号:
kill -s SIGTERM pid
或kill -n 15 pid
或kill -15 pid
或kill -TREM pid

3、trap命令

trap命令定义shell脚本在运行时根据接收的信号做相应的处理。

我们可以用trap命令捕捉信号(trap命令并不能捕获所有信号,但是常用信号HUP、INT、QUIT、TERM都是可以捕获的),执行我们规定的操作

命令格式:trap [-lp] [[arg] signal_spec ...]

-l      #打印编号1-64编号信号名称
arg     # 捕获信号后执行的命令或者函数
signal_spec # 信号名或编号

一般捕捉信号后,做以下几个动作:

1)清除临时文件

2)忽略该信号

3)询问用户是否终止脚本执行

示例1:按CTRL+C不退出循环

#!/bin/bash
trap "" 2    # 不指定arg就不做任何操作,后面也可以写多个信号,以空格分隔 
for i in {1..10}; do
    echo $i
    sleep 1
done

# bash a.sh
123
^C
456
^C
78910

示例2:循环打印数字,按CTRL+C退出,并打印退出提示

#!/bin/bash
trap "echo 'exit...';exit" 2
for i in {1..10}; do
    echo  $i
    sleep  1
done 
# bash test.sh
123
^Cexit
...

示例3:让用户选择是否终止循环

#!/bin/bash
trap "func" 2

func() {
    read -p "Terminate theprocess? (Y/N): " input
    if [ $input == "Y"] ;
    then
        exit
    fi 
} 
 for i in {1..10}; do
    echo $i   
    sleep 1
done 
# bash a.sh
123
^CTerminate the process? (Y/N): Y

# bash a.sh 
123
^CTerminate the process? (Y/N): N
456...

其他案例

# 操作1:捕捉信号、执行引号内的操作
trap "echo 已经识别中断信号:ctrl+c" INT

# 示例2:捕捉信号、不执行任何操作
trap "" INT  

# 示例3:也可以同时捕获多个信号
trap "" HUP INT QUIT TSTP

例1:

[root@egon test]# cat m.sh 
#!/bin/bash

trap "echo 已经识别到中断信号:ctrl+c" INT
trap 'echo 已经识别到中断信号:ctrl+\\' QUIT
trap 'echo 已经识别到中断信号:ctrl+z' TSTP

read -p "请输入你的名字: " name
echo "你的名字是:$name"
[root@egon test]# chmod +x m.sh 
[root@egon test]# ./m.sh 
请输入你的名字: ^C已经识别到中断信号:ctrl+c
^C已经识别到中断信号:ctrl+c
^C已经识别到中断信号:ctrl+c
^\已经识别到中断信号:ctrl+\
^\已经识别到中断信号:ctrl+\
^Z已经识别到中断信号:ctrl+z
^Z已经识别到中断信号:ctrl+z
egon
你的名字是:egon
[root@egon test]#

例2:

#!/bin/bash
trap "" HUP INT QUIT TSTP  # kill -HUP 当前进程的pid,也无法终止其运行

clear
i=0
while true
do
    [ $i == 0 ] && i=1 || i=0
 if [ $i == 0 ];then
        echo -e "\033[31m 红灯亮 \033[0m"
    else
        echo -e "\033[32m 绿灯亮 \033[0m"
    fi
    sleep 1
    clear
done

可以使用kill -9终止以上进程

4、 关于HUP信号

要了解Linux的HUP信号,需要从hangup说起

在 Unix 的早期版本中,每个终端都会通过 modem 和系统通讯。
当用户 logout 时,modem 就会挂断(hang up)电话。 
同理,当 modem 断开连接时,就会给终端发送 hangup 信号来通知其关闭所有子进程。 

综上,我们知道,当用户注销(logout)或者网络断开或者终端关闭时,终端都会收到Linux HUP信号(hangup)信号,然后终端在结束前会关闭其所有子进程。

如果我们想让我们的进程在后台一直运行,不要因为用户注销(logout)或者网络断开或者终端关闭而一起被干掉,那么我们有两种解决方案

  • 方案1:让进程忽略Linux HUP信号
  • 方案2:让进程运行在新的会话里,从而成为不属于此终端的子进程,就不会在当前终端挂掉的情况下一起被带走。

4.1 nohup命令

针对方案1,我们可以使用nohup命令,nohup 的用途就是让提交的命令忽略 hangup 信号,该命令通常与&符号一起使用

nohup 的使用是十分方便的,只需在要处理的命令前加上 nohup 即可,但是 nohup 命令会从终端解除进程的关联,进程会丢掉STDOUT,STDERR的链接。标准输出和标准错误缺省会被重定向到 nohup.out 文件中。一般我们可在结尾加上"&"来将命令同时放入后台运行,也可用">filename 2>&1"来更改缺省的重定向文件名。

nohup 是一个 Unix 和类 Unix 系统(如 Linux)中的命令,用于运行另一个命令,并且忽略挂起(hangup)信号。

这通常在你想要运行一个长时间的任务,并且不希望它在你关闭终端或断开 SSH 连接时被终止时非常有用。

这里是 nohup 命令的基本用法和一些选项的详细解释:

nohup COMMAND [ARG]...

这里,COMMAND 是你想要运行的命令,而 [ARG]... 是该命令可能需要的任何参数。

  • 选项:
    • --help:显示帮助信息并退出。
    • --version:输出版本信息并退出。
  • 标准输入、输出和错误:
    • 如果标准输入(stdin)是一个终端,nohup 会将其重定向到一个不可读的文件。
    • 如果标准输出(stdout)是一个终端,nohup 会尝试将其追加到 'nohup.out' 文件中(如果可能)。如果当前目录没有写入权限,它会尝试写入到 $HOME/nohup.out
    • 如果标准错误(stderr)是一个终端,nohup 会将其重定向到标准输出。
  • 保存输出到文件:
    • 如果你想要将输出保存到特定的文件中,而不是默认的 'nohup.out' 或 $HOME/nohup.out,你可以使用 shell 的重定向功能。例如:nohup COMMAND > FILE 会将输出保存到 FILE 文件中。

注意:正如帮助信息中提到的,你的 shell 可能有自己的 nohup 版本或与之相关的功能。因此,在使用时,最好查看你当前 shell 的文档,以了解它所支持的具体选项和行为。

最后,当你使用 nohup 运行一个命令后,它会返回一个进程 ID(PID),你可以使用 ps 命令或其他进程管理工具来查看和管理这个进程。

后台运行脚本并将输出重定向到文件

nohup ./my_script.sh > output.txt &
  • nohup:启动命令,忽略挂起信号。
  • ./my_script.sh:要运行的脚本。
  • > output.txt:将脚本的标准输出重定向到output.txt文件。
  • &:将命令放到后台执行。

后台运行命令并将输出重定向到/dev/null

nohup command > /dev/null &
  • /dev/null:是一个特殊的设备文件,它会丢弃所有写入它的数据。这个选项用于当你不需要保存命令的输出时。

后台运行命令并将输出和错误信息都重定向到文件

nohup command > output.log 2>&1 &
  • > output.log:将命令的标准输出重定向到output.log文件。
  • 2>&1:将标准错误(文件描述符2)重定向到标准输出(文件描述符1)的当前位置,即output.log文件。

使用nohup时忽略SIGHUP信号

nohup -i command > output.log &
  • -i:在某些系统中,这个选项用于忽略SIGHUP信号,但请注意,并非所有版本的nohup都支持这个选项。

查看后台运行的nohup命令

使用jobs命令(仅在当前终端中有效):

jobs

使用ps命令(跨终端查看):

ps -ef | grep nohup
  • jobs命令可以列出当前终端中后台运行的任务。
  • ps -ef | grep nohup命令可以查找所有与nohup相关的进程,并列出它们的信息。

nohup命令在Unix和类Unix系统中非常有用,特别是在需要长时间运行任务或确保任务在终端关闭或断开连接后仍能继续运行的情况下。

通过结合重定向和后台执行,nohup可以确保任务的输出被妥善保存,并且任务能够持续运行直到完成。

4.2 setsid命令

针对方案1,我们还可以用setsid命令实现,原理与4.1是一样的,setid是直接将进程的父pid设置成1,即让运行的进程归属于init的子进程,那么除非init结束,该子进程才会结束,当前进程所在的终端结束后并不会影响进程的运行

# 1、在终端2中执行命令
[root@egon ~]# setsid ping www.baidu.com  # 也可以在后面加&符号

# 2、关闭终端2

# 3、在终端1中查看
[root@egon ~]# ps -ef |grep [p]ing
root     102335      1  0 17:53 ?        00:00:00 ping www.baidu.com

4.3 在子shell中提交任务

原理同4.2

# 1、在终端2中执行命令
[root@egon ~]# (ping www.baidu.com &)  # 提交的作业并不在作业列表中

# 2、关闭终端2

# 3、在终端1中查看
[root@egon ~]# ps -ef |grep [p]ing
root     102361      1  0 17:55 ?        00:00:00 ping www.baidu.com

可以看到新提交的进程的父 ID(PPID)为1(init 进程的 PID),并不是当前终端的进程 ID。