信号

信号是事件发生时对进程的通知机制。有时也称之为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程,大多数情况下,无法预测信号到达的精确时间。

因为一个具有合适权限的进程可以向另一个进程发送信号,这可以称为进程间的一种同步技术。当然,进程也可以向自身发送信号。然而,发往进程的诸多信号,通常都是源于内核。引发内核为进程产生信号的各类事件如下。

  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程。比如执行一条异常的机器语言指令(除 0,引用无法访问的内存区域)。
  • 用户键入了能够产生信号的终端特殊字符。如中断字符(通常是 Control-C)、暂停字符(通常是 Control-Z)。
  • 发生了软件事件。如调整了终端窗口大小,定时器到期等。

针对每个信号,都定义了一个唯一的(小)整数,从 1 开始顺序展开。系统会用相应常量表示。Linux 中,1-31 为标准信号;32-64 为实时信号(通过 kill -l 可以查看)。

信号达到后,进程视具体信号执行如下默认操作之一。

  • 忽略信号,也就是内核将信号丢弃,信号对进程不产生任何影响。
  • 终止(杀死)进程。
  • 产生 coredump 文件,同时进程终止。
  • 暂停(Stop)进程的执行。
  • 恢复进程执行。

当然,对于有些信号,程序是可以改变默认行为的,这也就是 os/signal 包的用途。

兼容性问题:信号的概念来自于 Unix-like 系统。Windows 下只支持 os.SIGINT 信号。

程序无法捕获信号 SIGKILL 和 SIGSTOP (终止和暂停进程),因此 os/signal 包对这两个信号无效。

发送信号

  • kill 系统调用(system call)可以用来发送一个特定的信号给进程。
  • kill 命令允许用户发送一个特定的信号给进程。
  • raise 库函数可以发送特定的信号给当前进程。

在Linux下运行man kill可以查看此命令的介绍和用法。

The command kill sends the specified signal to the specified process or process group. If no signal is specified, the TERM signal is sent. The TERM signal will kill processes which do not catch this signal. For other processes, it may be necessary to use the KILL (9) signal, since this signal cannot be caught.

Most modern shells have a builtin kill function, with a usage rather similar to that of the command described here. The ‘-a’ and ‘-p’ options, and the possibility to specify pids by command name is a local extension.

If sig is 0, then no signal is sent, but error checking is still performed.

一些异常比如除以0或者 segmentation violation 相应的会产生SIGFPE和SIGSEGV信号,缺省情况下导致core dump和程序退出。

内核在某些情况下发送信号,比如在进程往一个已经关闭的管道写数据时会产生SIGPIPE信号。

在进程的终端敲入特定的组合键也会导致系统发送某个特定的信号给此进程:

  • Ctrl-C 发送 INT signal (SIGINT),通常导致进程结束
  • Ctrl-Z 发送 TSTP signal (SIGTSTP); 通常导致进程挂起(suspend)
  • Ctrl-\ 发送 QUIT signal (SIGQUIT); 通常导致进程结束 和 dump core.
  • Ctrl-T (不是所有的UNIX都支持) 发送INFO signal (SIGINFO); 导致操作系统显示此运行命令的信息

kill pid与kill -9 pid的区别

kill pid的作用是向进程号为pid的进程发送SIGTERM(这是kill默认发送的信号),该信号是一个结束进程的信号且可以被应用程序捕获。若应用程序没有捕获并响应该信号的逻辑代码,则该信号的默认动作是kill掉进程。这是终止指定进程的推荐做法。

kill -9 pid则是向进程号为pid的进程发送SIGKILL(该信号的编号为9),从本文上面的说明可知,SIGKILL既不能被应用程序捕获,也不能被阻塞或忽略,其动作是立即结束指定进程。通俗地说,应用程序根本无法“感知”SIGKILL信号,它在完全无准备的情况下,就被收到SIGKILL信号的操作系统给干掉了,显然,在这种“暴力”情况下,应用程序完全没有释放当前占用资源的机会。事实上,SIGKILL信号是直接发给init进程的,它收到该信号后,负责终止pid指定的进程。在某些情况下(如进程已经hang死,无法响应正常信号),就可以使用kill -9来结束进程。

若通过kill结束的进程是一个创建过子进程的父进程,则其子进程就会成为孤儿进程(Orphan Process),这种情况下,子进程的退出状态就不能再被应用进程捕获(因为作为父进程的应用程序已经不存在了),不过应该不会对整个linux系统产生什么不利影响。

信号类型

各平台的信号定义或许有些不同。下面列出了POSIX中定义的信号。

Linux 使用34-64信号用作实时系统中。

命令man 7 signal提供了官方的信号介绍。

在POSIX.1-1990标准中定义的信号列表

信号 动作 说明
SIGHUP 1 Term 终端控制进程结束(终端连接断开)
SIGINT 2 Term 用户发送INTR字符(Ctrl+C)触发
SIGQUIT 3 Core 用户发送QUIT字符(Ctrl+/)触发
SIGILL 4 Core 非法指令(程序错误、试图执行数据段、栈溢出等)
SIGABRT 6 Core 调用abort函数触发
SIGFPE 8 Core 算术运行错误(浮点运算错误、除数为零等)
SIGKILL 9 Term 无条件结束程序(不能被捕获、阻塞或忽略)
SIGSEGV 11 Core 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作)
SIGPIPE 13 Term 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作)
SIGALRM 14 Term 时钟定时信号
SIGTERM 15 Term 结束程序(可以被捕获、阻塞或忽略)
SIGUSR1 30,10,16 Term 用户保留
SIGUSR2 31,12,17 Term 用户保留
SIGCHLD 20,17,18 Ign 子进程结束(由父进程接收)
SIGCONT 19,18,25 Cont 继续执行已经停止的进程(不能被阻塞)
SIGSTOP 17,19,23 Stop 停止进程(不能被捕获、阻塞或忽略)
SIGTSTP 18,20,24 Stop 停止进程(可以被捕获、阻塞或忽略)
SIGTTIN 21,21,26 Stop 后台程序从终端中读取数据时触发
SIGTTOU 22,22,27 Stop 后台程序向终端中写数据时触发

在SUSv2和POSIX.1-2001标准中的信号列表:

信号 动作 说明
SIGTRAP 5 Core Trap指令触发(如断点,在调试器中使用)
SIGBUS 0,7,10 Core 非法地址(内存地址对齐错误)
SIGPOLL Term Pollable event (Sys V). Synonym for SIGIO
SIGPROF 27,27,29 Term 性能时钟信号(包含系统调用时间和进程占用CPU的时间)
SIGSYS 12,31,12 Core 无效的系统调用(SVr4)
SIGURG 16,23,21 Ign 有紧急数据到达Socket(4.2BSD)
SIGVTALRM 26,26,28 Term 虚拟时钟信号(进程占用CPU的时间)(4.2BSD)
SIGXCPU 24,24,30 Core 超过CPU时间资源限制(4.2BSD)
SIGXFSZ 25,25,31 Core 超过文件大小资源限制(4.2BSD)

Windows中没有SIGUSR1,可以用SIGBREAK或者SIGINT代替。

Shell触发信号

处理信号

Linux系统和应用程序可以生成超过30个信号。下表列出了在Linux编程时会遇到的最常见的Linux系统信号。

默认情况下,bash shell会忽略收到的任何SIGQUIT (3)和SIGTERM (5)信号(正因为这样, 交互式shell才不会被意外终止)。但是bash shell会处理收到的SIGHUP (1)和SIGINT (2)信号。

如果bash shell收到了SIGHUP信号,比如当你要离开一个交互式shell,它就会退出。但在退出之前,它会将SIGHUP信号传给所有由该shell所启动的进程(包括正在运行的shell脚本)。

通过SIGINT信号,可以中断shell。Linux内核会停止为shell分配CPU处理时间。这种情况发生时,shell会将SIGINT信号传给所有由它所启动的进程,以此告知出现的状况。

shell会将这些信号传给shell脚本程序来处理。而shell脚本的默认行为 是忽略这些信号。它们可能会不利于脚本的运行。要避免这种情况,你可以脚本中加入识别信 号的代码,并执行命令来处理信号。

生成信号

bash shell允许用键盘上的组合键生成两种基本的Linux信号。这个特性在需要停止或暂停失 控程序时非常方便。

1. 中断进程

Ctrl+C组合键会生成SIGINT信号,并将其发送给当前在shell中运行的所有进程。可以运行一 条需要很长时间才能完成的命令,然后按下Ctrl+C组合键来测试它。

$ sleep 100 ^C
$

Ctrl+C组合键会发送SIGINT信号,停止shell中当前运行的进程。sleep命令会使得shell暂停指定的秒数,命令提示符直到计时器超时才会返回。在超时前按下Ctrl+C组合键,就可以提前终 止sleep命令。

2. 暂停进程

你可以在进程运行期间暂停进程,而无需终止它。尽管有时这可能会比较危险(比如,脚本 打开了一个关键的系统文件的文件锁),但通常它可以在不终止进程的情况下使你能够深入脚本 内部一窥究竟。

Ctrl+Z组合键会生成一个SIGTSTP信号,停止shell中运行的任何进程。停止(stopping)进程跟终止(terminating)进程不同:停止进程会让程序继续保留在内存中,并能从上次停止的位置 继续运行。

当用Ctrl+Z组合键时,shell会通知你进程已经被停止了。

如果你的shell会话中有一个已停止的作业,在退出shell时,bash会提醒你。

$ sleep 100
^Z
[1]+ Stopped 			sleep 100
$ exit
exit
There are stopped jobs.
$

可以用ps命令来查看已停止的作业。在S列中(进程状态),ps命令将已停止作业的状态为显示为T。这说明命令要么被跟踪,要么被停止了。

如果在有已停止作业存在的情况下,你仍旧想退出shell,只要再输入一遍exit命令就行了。 shell会退出,终止已停止作业。或者,既然你已经知道了已停止作业的PID,就可以用kill命令 来发送一个SIGKILL信号来终止它。

在终止作业时,最开始你不会得到任何回应。但下次如果你做了能够产生shell提示符的操作 (比如按回车键),你就会看到一条消息,显示作业已经被终止了。

每当shell产生一个提示符时,它就会显示shell中状态发生改变的作业的状态。在你终止一个作业后,下次强制shell生成一个提 示符时,shell会显示一条消息,说明作业在运行时被终止了。

捕获信号

也可以不忽略信号,在信号出现时捕获它们并执行其他命令。trap命令允许你来指定shell 脚本要监看并从shell中拦截的Linux信号。如果脚本收到了trap命令中列出的信号,该信号不再 由shell处理,而是交由本地处理。

trap命令的格式是:

trap commands signals

这里有个简单例子,展示了如何使用trap命令来忽略SIGINT信号,并控制脚本的行为。

trap "echo ' Sorry! I have trapped Ctrl-C'" SIGINT

每次使用Ctrl+C组合键,脚本都会执行trap命令中指定的echo语句,而不是处理该信号并允许shell停止该脚本。

捕获脚本退出

除了在shell脚本中捕获信号,你也可以在shell脚本退出时进行捕获。这是在shell完成任务时 执行命令的一种简便方法。

要捕获shell脚本的退出,只要在trap命令后加上EXIT信号就行。

当脚本运行到正常的退出位置时,捕获就被触发了,shell会执行在trap命令行指定的命令。 如果提前退出脚本,同样能够捕获到EXIT。

trap "echo Goodbye..." EXIT

修改或移除捕获

要想在脚本中的不同位置进行不同的捕获处理,只需重新使用带有新选项的trap命令。

trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT
#
count=1
while [ $count -le 5 ]
do
   echo "Loop #$count"
   sleep 1
   count=$[ $count + 1 ]
done
#
trap "echo ' I modified the trap!'" SIGINT

修改了信号捕获之后,脚本处理信号的方式就会发生变化。但如果一个信号是在捕获被修改 前接收到的,那么脚本仍然会根据最初的trap命令进行处理。

也可以删除已设置好的捕获。只需要在trap命令与希望恢复默认行为的信号列表之间加上 两个破折号就行了。

trap "echo ' Sorry... Ctrl-C is trapped.'" SIGINT
trap -- SIGINT

也可以在trap命令后使用单破折号来恢复信号的默认行为。单破折号和双破折号都可以 正常发挥作用。

多线程环境下的信号监听

在多线程环境下,产生的信号是传递给整个进程的,一般而言,所有线程都有机会收到这个信号,进程在收到信号的的线程上下文执行信号处理函数,具体是哪个线程执行的难以获知。也就是说,信号会随机发个该进程的一个线程。

signal函数BSD/Linux的实现并不在信号处理函数调用时,恢复信号的处理为默认,而是在信号处理时阻塞此信号,直到信号处理函数返回。其他实现可能在调用信号处理函数时,恢复信号的处理为默认方式,因而需要在信号处理函数中重建信号处理函数为我们定义的处理函数,在这些系统中,较好的方法是使用sigaction来建立信号处理函数。

发送信号给进程,哪个线程会收到?APUE说,在多线程的程序中,如果不做特殊的信号阻塞处理,当发送信号给进程时,由系统选择一个线程来处理这个信号。

如果进程中,有的线程可以屏蔽了某个信号,而某些线程可以处理这个信号,则当我们发送这个信号给进程或者进程中不能处理这个信号的线程时,系统会将这个信号投递到进程号最小的那个可以处理这个信号的线程中去处理。

如果我们同时注册了信号处理函数,同时又用sigwait来等待这个信号,谁会取到信号?经过实验,Linux上sigwait的优先级高。

在Linux中的posix线程模型中,线程拥有独立的进程号,可以通过getpid()得到线程的进程号,而线程号保存在pthread_t的值中。而主线程的进程号就是整个进程的进程号,因此向主进程发送信号只会将信号发送到主线程中去。如果主线程设置了信号屏蔽,则信号会投递到一个可以处理的线程中去。

当调用SYSTEM函数去执行SHELL命令时,可以放心的阻塞SIGCHLD,因为SYSTEM会自己处理子进程终止的问题。

使用sleep()时,要以放心的去阻塞SIGALRM信号,目前sleep函数都不会依赖于ALRM函数的SIGALRM信号来工作。

默认情况下,信号将由主进程接收处理,就算信号处理函数是由子线程注册的

每个线程均有自己的信号屏蔽字,可以使用sigprocmask函数来屏蔽某个线程对该信号的响应处理,仅留下需要处理该信号的线程来处理指定的信号。

对某个信号处理函数,以程序执行时最后一次注册的处理函数为准,即在所有的线程里,同一个信号在任何线程里对该信号的处理一定相同

可以使用pthread_kill对指定的线程发送信号

  1. APUE的说法:每个线程都有自己的信号屏蔽字,但是信号的处理是进程中所有的线程共享的,这意味着尽管单个线程可以阻止某些信号,但当线程修改了与某个信号相关的处理行为后,所有的线程都共享这个处理行为的改变。这样如果一个线程选择忽略某个信号,而其他线程可以恢复信号的默认处理行为,或者为信号设置一个新的处理程序,从而可以撤销上述线程的信号选择。
  2. 进程中的信号是送到单个线程的,如果信号与硬件故障或者计时器超时有关,该型号就被发送到引起该事件的线程中去,而其他的信号则被发送到任意一个线程。
  3. sigprocmask的行为在多线程的进程中没有定义,线程必须使用pthread_sigmask
  4. 总结:一个信号可以被没屏蔽它的任何一个线程处理,但是在一个进程内只有一个多个线程共用的处理函数。

Linux 多线程应用中,每个线程可以通过调用pthread_sigmask() 设置本线程的信号掩码。一般情况下,被阻塞的信号将不能中断此线程的执行,除非此信号的产生是因为程序运行出错如SIGSEGV;另外不能被忽略处理的信号SIGKILL 和SIGSTOP 也无法被阻塞。

当一个线程调用pthread_create() 创建新的线程时,此线程的信号掩码会被新创建的线程继承。

信号安装最好采用sigaction方式,sigaction,是为替代signal 来设计的较稳定的信号处理,signal的使用比较简单。signal(signalNO,signalproc);

不能完成的任务是:

  1. 不知道信号产生的原因;
  2. 处理信号中不能阻塞其他的信号

而signaction,则可以设置比较多的消息。尤其是在信号处理函数过程中接受信号,进行何种处理。

sigaction函数用于改变进程接收到特定信号后的行为。

sigprocmask函数只能用于单线程,在多线程中使用pthread_sigmask函数。

信号是发给进程的特殊消息,其典型特性是具有异步性。

信号集代表多个信号的集合,其类型是sigset_t。

每个进程都有一个信号掩码(或称为信号屏蔽字),其中定义了当前进程要求阻塞的信号集。

所谓阻塞,指Linux内核不向进程交付在掩码中的所有信号。于是进程可以通过修改信号掩码来暂时阻塞特定信号的交付,被阻塞的信号不会影响进程的行为直到该信号被真正交付。

忽略信号不同于阻塞信号,忽略信号是指Linux内核已经向应用程序交付了产生的信号,只是应用程序直接丢弃了该信号而已。

sleep和nanosleep,如果没有在它调用之前设置信号屏蔽字的话,都可能会被信号处理函数打断。参见sleep和nanosleep的mannual。

在linux下,每个进程都有自己的signal mask,这个信号掩码指定哪个信号被阻塞,哪个不会被阻塞,通常用调用sigmask来处理。同时每个进程还有自己的signal action,这个行为集合指定了信号该如何处理,通常调用sigaction来处理。但不同的信号处理方式不同。

  1. 如果是异常产生的信号(比如程序错误,像SIGPIPE、SIGEGV这些),则只有产生异常的线程收到并处理。

  2. 如果是用pthread_kill产生的内部信号,则只有pthread_kill参数中指定的目标线程收到并处理。

  3. 如果是外部使用kill命令产生的信号,通常是SIGINT、SIGHUP等job control信号,则会遍历所有线程,直到找到一个不阻塞该信号的线程,然后调用它来处理。(一般从主线程找起),注意只有一个线程能收到。

  4. 每个线程都有自己独立的signal mask,但所有线程共享进程的signal action。这意味着,你可以在线程中调用pthread_sigmask(不是sigmask)来决定本线程阻塞哪些信号。但你不能调用sigaction来指定单个线程的信号处理方式。如果在某个线程中调用了sigaction处理某个信号,那么这个进程中的未阻塞这个信号的线程在收到这个信号都会按同一种方式处理这个信号。所以每个线程不能按自己的方式处理信号。

参考

linux 多线程信号总结