Linux中有两种类型信号
常规信号(regular signal):编码范围为1~31,同种类型的常规信号如果被发送多次,那么,只有其中的一个发送到接收进城
实时信号(real-time signal):编码范围32~64,它与常规信号不通,因为他们必须排队以便发送的多个信号能被接收到。尽管linux内核不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。
与信号相关的最重要的系统调用
kill() 向线程组发送一个信号
tkill() 向进程发送一个信号
tgkill() 向一个特定线程组中的进程发送信号
sigaction() 改变与信号相关的操作
signal() 类似于sigaction()
sigpending() 检查是否有挂起信号
sigprocmask() 修改阻塞信号的集合
sigsuspend() 等待一个信号
信号的一个重要特点是它们可以随时随地发送给状态经常不可预知的进程。发送给非运行进程的信号必须由内核保存,直到进程恢复执行。
内核区分信号传递的两个不同阶段:
信号产生
内核更新目标进程的数据结构以表示一个新信号已被发送。
信号传递
内核强迫目标进程通过以下方式对信号做出反应:或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或者两者都是。
已经产生但还没有传递的信号称为挂起信号(pending signal)。任何时候,一个进程仅存在给定类型的一个挂起信号,同一进程同种类型的其他信号不被排队,只被简单的丢弃。但是,实时信号时不同的:同种类型的挂起信号可以多个。
尽管信号表项比较直观,但内核实现相当复杂:
1、记住每个进程阻塞哪些信号
2、当从内核态切换到用户态时,对任何一个进程都要检查是否有一个信号到达。这几乎在每个定时器中断都发送
3、确定是否可以忽略信号。这发生在下列所有条件都满足时:
a、目标进程没有被另一个进程跟踪
b、信号没有被目标进程阻塞
c、信号被目标进程屏蔽
4、处理这样的信号,即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,并在这个函数返回后恢复原来执行的上下文。
进程以三种方式对一个信号做出应答:
1、显示地忽略信号
2、执行与信号相关的缺省操作
3、通过调用相应的信号处理函数捕获信号
注意,对一个信号的阻塞和忽略是不同的:只要信号被阻塞,它就不被传递;只有信号解除阻塞后才传递它。而一个被忽略的信号总是被传递,只是没有进一步的操作。
SIGKILL和SIGSTOP信号不可以被显式地忽略、捕获或阻塞,因此,通常必须执行它们的缺省操作。
POSIX标准对多线程应用的信号处理有一些严格的要求:
1、信号处理程序必须在多线程应用的所有线程之间共享;不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码
2、每个发送给多线程应用的信号仅传送给一个线程,这个线程是由内核在从不阻塞该信号的线程中随意选择出来的
3、如果向多线程应用发送了一个致命的信号,那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。
task_struct线程描述符中与信号相关的字段
struct signal_struct *signal;
struct sighand_struct *sighand;
struct sigpending pending;
产生信号
很多内核函数都会产生信号:它们完成信号处理第一步的工作,即根据需要更新一个或多个进程的描述符。它们不直接执行第二步的信号传递操作,而是可能可能根据信号的类型和目标进程的状态唤醒一下进程,并促使这些进程接收信号。
当发送给进程一个信号时,这个信号可能来自内核,也可能来自另一个进程
传递信号
假定内核已经注意到一个信号的到来,并调用前面介绍的函数为接收此信号的进程准备描述符。但万一这个进程在那一刻并不在CPU运行,内核就延迟传递函数的任务。那么如何确保进程的挂起信号得到处理呢?
内核在运行进程恢复用户态下的执行前,检查进程TIF_SIGPENDING标志的值。每当内核处理完一个中断或异常时,就检查是否存在挂起的信号。
捕获信号
上图说明了有关捕获一个信号的函数的执行流。一个非阻塞的信号发送给一个进程。当中断或异常发生时,进程切换到内核态。正要返回到用户态前,内核执行do_signal()函数,这个函数又依次处理信号和建立用户态堆栈。当进程又切换到用户态时因为信号处理程序的起始地址被强制放进程序计数器中,因此开始执行信号处理程序。当处理程序终止时,setup_frame()函数放在用户态堆栈中的返回代码被执行。这个代码调用sigreturn()系统调用,相应的服务例程把正常程序的用户态堆栈硬件上下文拷贝到内核态堆栈,并把用户态堆栈恢复到原来的状态。当这个系统调用结束时,普通进程就因此能恢复自己的执行。