信号在最早的Unix系统中即被引入,用于在用户态进程间通信。内核也用信号通知进程系统所发生的事件。
一、信号的作用
信号(signal)是很短的消息,可以被发送到一个进程或一组进程。发送给进程的唯一信息通常是一个数,以此来标识信号。在标准信号中,对参数、消息或者其他相随的信息没有给予关注。
当一个进程引用无效的内存时,SIGSEGV宏产生发送给进程的信号标识符。
使用信号的两个主要目的是:
* 让进程知道已经发生了一个特定的事件。
* 强迫进程执行它自己代码中的信号处理程序。
当然,这两个目的不是互斥的,因为进程经常通过执行一个特定的例程来对某一事件做出反应。
除了常规信号(regular signal)外,POSIX标准还引入了一类新的信号,叫做实时信号(real-time signal);在Linux中它们的编码范围为32-64。它们与常规信号有很大的不同,因为它们必须排队以便发送的多个信号能被接收到。另一方面,同种类型的常规信号并不排队:如果一个常规信号被连续发送多次,那么,只有其中的一个发送到接收进程。尽管Linux内核并不使用实时信号,它还是通过几个特定的系统调用完全实现了POSIX标准。
信号的一个重要特点是它们可以随时被发送给状态经常不可预知的进程。发送给非运行进程的信号必须由内核保存,直到进程恢复执行。阻塞一个信号要求信号的传递拖延,直到随后解除阻塞,这使得信号产生一段时间之后才能对其传递这一问题变得更加严重。
因此,内核区分信号传递的两个不同阶段:
信号产生:内核更新目标进程的数据结构以表示一个新信号已被发送。
信号传递:内核强迫目标进程通过以下方式对信号做出反应:或改变目标进程的执行状态,或开始执行一个特定的信号处理程序,或两者都是。
每个所产生的信号至多被传递一次。信号是可消费资源:一旦它们已传递出去,进程描述符中有关这个信号的所有信息都被取消。
一般来说,信号可以保留不可预知的挂起时间。必须考虑下列因素:
* 信号通常只被当前正运行的进程传递(即由current进程传递)。
* 给定类型的信号可以由进程选择性地阻塞(blocked)。这种情况下,在取消阻塞前进程将不接收这个信号。
* 当进程执行一个信号处理程序的函数时,通常“屏蔽”相应的信号,即自动阻塞这个信号直到处理程序结束。因此,所处理的信号另一次出现不能中断信号处理程序,所以,信号处理函数不必是可重入的。
尽管信号的表示比较直观,但内核的实现相当复杂。内核必须:
* 记住每个进程阻塞哪些信号。
* 当从内核态切换到用户态时,对任何一个进程都要检查是否有一个信号已到达。这几乎在每个定时中断时都发生(大约每毫秒发生一次)。
* 确定是否可以忽略信号。这发生在下列所有的条件都满足时:
--目标进程没有被另一个进程跟踪(进程描述符中ptrace字段的PT_PTRACED标志等于0)。
--信号没有被目标进程阻塞。
--信号被目标进程忽略(或者因为进程已显示地忽略了信号,或者因为进程没有改变信号的缺省操作且这个缺省操作就是“忽略”)。
* 处理这样的信号,即信号可能在进程运行期间的任一时刻请求把进程切换到一个信号处理函数,并在这个函数返回以后恢复原来执行的上下文。
1.1、传递信号之前所执行的操作
进程以三种方式对一个信号做出应答:
1、显示地忽略信号。
2、执行与信号相关的缺省操作。由内核预定义的缺省操作取决于信号的类型,可以是下列类型之一:
Terminate:进程被终止(杀死)。
Dump:进程被终止(杀死),并且,如果可能,创建包含进程执行上下文的核心转储文件。这个文件可以用于调试。
Ignore:信号被忽略。
Stop:进程被停止,即把进程置为TASK_STOPPED状态。
Continue:如果进程被停止(TASK_STOPPED),就把它置为TASK_RUNNING状态。
3、通过调用相应的信号处理函数捕获信号。
注意,被对一个信号的阻塞和忽略是不同的:只要信号被阻塞,它就不被传递;只有在信号解除阻塞后才传递它。而一个被忽略的信号总是被传递,只是没有进一步的操作。
如果信号的传递会引起内核杀死一个进程,那么这个信号对该进程就是致命的。SIGKILL信号总是致命的;而且,缺省操作为Terminate的每个信号,以及不被进程捕获的信号对该进程也是致命的。注意,如果一个被进程捕获的信号,其对应的信号处理函数终止了这个进程,那么这个信号就不是致命的,因为进程自己选择了终止,而不是被内核杀死。
1.2、POSIX信号和多线程应用
POSIX 1003.1标准对多线程应用的信号处理有一些严格的要求:
1、信号处理程序必须在多线程应用的所有线程之间共享;不过,每个线程必须有自己的挂起信号掩码和阻塞信号掩码。
2、POSIX库函数kill()和sigqueue()必须向所有的多线程应用而不是某个特殊的线程发出信号。所有由内核产生的信号如此(如:SIGCHLD、SIGINT或SIGQUIT)。
3、每个发送给多线程应用的信号仅传递给一个线程,这个线程是由内核在从不会阻塞该信号的线程中随意选择出来的。
4、如果向多线程应用发送了一个致命的信号,那么内核将杀死该应用的所有线程,而不仅仅是杀死接收信号的那个线程。
如果一个挂起信号被发送给了某个特定进程,那么这个信号是私有的;如果被发送给了整个线程组,它就是共享的。
1.3、与信号相关的数据结构
对系统中的每个进程来说,内核必须跟踪什么信号当前正在挂起或被屏蔽,以及每个线程组是如何处理所有信号的。为了完成这些操作,内核使用几个从处理器描述符可存取的数据结构。
因为每个无符号长整数由32位组成,所以在Linux中可以声明的信号最大数是64(_NSIG宏表示这个值)。没有值为0的信号,因此,信号的编号对应于sigset_t类型变量中的相应位下标加1。1~31之间的编号对应于表所列出的信号,而32~64之间的编号对应于实时信号。
1.3.1、信号描述符和信号处理程序描述符
进程描述符的signal字段指向信号描述符(signal descriptor)——一个signal_struct类型的结构,用来跟踪共享挂起信号。实际上,信号描述符还包括与信号处理关系并不密切的一些字段,如:每进程的资源限制数组rlim、分别用于存放进程的组领头进程和会话领头进程PID的字段pgrp和session。实际上,信号描述符被属于同一组线程组的所有进程共享,也就是被调用clone()系统调用(CLONE_THREAD标志置位)创建的所有进程共享,因此,对属于同一线程组的每个进程而言,信号描述符中的字段必须都是相同的。
除了信号描述符以外,每个进程还引用一个信号处理程序描述符(signad handler descriplor),它是一个sighand_struct类型的结构,用来描述每个信号必须怎样被线程组处理。
在调用clone()系统调用时设置CLONE_SIGHAND标志,信号处理程序描述符就可以由几个进程共享。描述符的count字段表示共享该结构的进程个数。在一个POSIX的多线程应用中,线程组中的所有轻量级进程都引用相同的信号描述符和信号处理程序描述符。
1.3.2、sigaction数据结构
k_sigaction结构只不过简化为类型为sigaction的单个sa结构,该结构包含下列字段:
sa_handler:这个字段指定要执行操作的类型。它的值可以是指向信号处理程序的一个指针,SIG_DFL(即值0,指定执行缺省操作),或者SIG_IGN(即值1,指定忽略信号)。
sa_flags:这是一个标志集,指定必须怎样处理信号。
sa_mask:这是类型为sigset_t的变量,指定当运行信号处理程序时要屏蔽的信号。
1.3.3、挂起信号队列
为了跟踪当前的挂起信号是什么,内核把两个挂起信号队列与每个进程相关联:
* 共享挂起信号队列,它位于信号描述符的shared_pending字段,存放整个线程组的挂起信号。
* 私有挂起信号队列,它位于进程描述符的pending字段,存放特定进程(轻量级进程)的挂起信号。
1.4、在信号数据结构上操作
二、产生信号
很多内核函数都会产生信号:它们完成信号处理第一步的工作,即根据需要更新一个或多个进程的描述符。它们不直接执行第二步的信号传递操作,而是可能根据信号的类型和目标进程的状态唤醒一些进程,并促使这些进程接收信号。
当发送给进程一个信号时,这个信号可能来自内核,也可能来自另一个进程。
2.1、specific_send_sig_info()函数
specific_send_sig_info()函数向指定进程发送信号,它作用于三个参数:
sig:信号编号。
info:或者是siginfo_t表的地址,或者是三个特殊值中的一个:0意味着信号是由用户态进程发送的,1意味着是由内核发送的,2意味着是由内核发送的SIGSTOP或SIGKILL信号。
t:指向目标进程描述符的指针。
2.2、send_signal()函数
send_signal()函数在挂起信号队列中插入一个新元素,它接收信号编号sig、siginfo_t数据结构的地址info(或一个特殊编码)、目标进程描述符的地址t以及挂起信号队列的地址signals作为它的参数。
即使在挂起队列中没有空闲存放相应的挂起信号,让目标进程能接收信号也是至关重要的。假设一个进程正在消耗过多内存的情形。内核必须保证即使没有空闲内存,kill()系统调用也能够成功执行,否则,系统管理员就没有机会通过终止有害进程来恢复系统。
2.3、group_send_sig_info()函数
group_send_sig_info()函数向整个线程组发送信号。它作用于三个参数:信号编号sig、siginfo_t表的地址info以及进程描述符的地址p。
函数__group_complete_signal()扫描线程组中的进程,查找能接收新信号的进程,满足下述所有条件的进程可能被选中:
* 进程不阻塞信号。
* 进程的状态不是EXIT_ZOMBIE、EXIT_DEAD、TASK_TRACED或TASK_STOPPED(作为一种异常情况,如果信号是SIGKILL,那么进程可能处于TASK_TRACED或者TASK_STOPPED状态)。
* 进程没有正在被杀死,即它的PF_EXITING标志没有置位。
* 进程或者当前正在CPU上运行,或者它的TIF_SIGPENDING标志还没有设置。(实际上,唤醒一个有挂起信号的进程是毫无意义的;通常,唤醒操作已经由设置了TIF_SIGPENDING标志的内核控制路径执行;另一方面,如果进程正在执行,则应该向它通报有新的挂起信号)。
一个线程组可能有很多满足上述条件的进程,函数按照下面的规则传递其中的一个进程:
* 如果p标识的进程(由group_send_sig_info()的参数传递的描述符地址)满足所有的优先准则,并因此而能接收信号,函数就选择该进程。
* 否则,函数通过扫描线程组的成员搜索一个适当的进程,搜索从接收线程组最后一个信号的进程(p->signal->curr_target)开始。
如果函数__group_complete_signal()成功地找到一个适当的进程,就开始向被选中的进程传递信号。首先,函数检查信号是否是致命的,如果是,通过向线程组中的所有轻量级进程发送SIGKILL信号杀死整个线程组。否则,函数调用signal_wake_up()函数通知被选中的进程:有新的挂起信号到来。
三、传递信号
内核在允许进程恢复用户态下的执行之前,检查进程TIF_SIGPENDING标志的值。每当内核处理完一个中断或异常时,就检查是否存在挂起信号。
为了处理非阻塞的挂起信号,内核调用do_signal()函数,它接收两个参数:
regs:栈区的地址,当前进程在用户态下寄存器的内容存放在这个栈中。
oldset:变量的地址,假设函数把阻塞信号的位掩码数组存放在这个变量中。如果没有必要保存位掩码数组,则它为NULL。
3.1、执行信号的缺省操作
do_signal_stop()函数检查current是否是线程组中第一个被停止的进程,如果是,它激活“组停止”:本质上,该函数把一个正数值赋给信号描述符中的group_stop_count字段,并唤醒线程组中的所有进程。所有这样的进程都检查该字段以确认正在进行“组停止”操作,然后把进程的状态置为TASK_STOPPED,并调用schedule()。如果线程组领头进程的父进程没有设置SIGCHLD的SA_NOCLDSTOP标志,那么do_signal_stop()函数还要向它发送SIGCHLD信号。
缺省操作为Dump的信号可以在进程的工作目录中创建一个“转储”文件,这个文件列出进程地址空间和CPU寄存器的全部内容。do_signal()创建了转储文件后,就杀死这个线程组。剩余18个信号的缺省操作是Terminate,它仅仅是杀死线程组。为了杀死整个线程组,函数调用do_group_exit()执行彻底的“组退出”过程。
3.2、捕获信号
如果所接收信号的SA_ONESHOT标志被置位,就必须重新设置它的缺省操作,以便同一信号的再次出现不会再次触发这一信号处理程序的执行。注意do_signal()在处理了一个单独的信号后怎样返回。直到下一次调用do_signal()时才考虑其他挂起的信号。这种方式确保了实时信号将以适当的顺序得到处理。
3.2.1、建立帧
setup_frame()函数把一个叫做帧(frame)的数据结构推进用户态堆栈中,这个帧含有处理信号所需要的信息,并确保正确返回到handle_signal()函数,一个帧就是包含下列字段的sigframe表:
pretcode:信号处理函数的返回地址,它指向__KERNEL_sigreturn标记处的代码。
sig:信号编号。这是信号处理程序所需的参数。
sc:类型为sigcontext的结构,它包含正好切换到内核态前用户态进程的硬件上下文(这种信息是从current的内核态堆栈中拷贝过来的),它包含进程被阻塞的常规信号的位数组。
fpstate:类型为_fpstate的结构,可以用来存放用户态进程的浮点寄存器内容。
extramask:被阻塞的实时信号的位数组。
retcode:发出sigreturn()系统调用的8字节代码。
setup_rt_frame()函数与setup_frame()非常相似,但它把用户态堆栈存放在一个扩展的帧中(保存在rt_sigframe数据结构中),这个帧也包含了与信号相关的siginfo_t表的内容。此外,该函数设置pretcode字段以使它指向vsyscall页中__kernel_rt_sigreturn代码。
3.2.2、检查信号标志
3.2.3、开始执行信号处理程序
3.2.4、终止信号处理程序
3.3、系统调用的重新执行
内核并不总是能立即满足系统调用发出的请求,在这种情况发生时,把发出系统调用的进程置位TASK_INTERRUPTIBLE或TASK_UNINTERRUPTIBLE状态。
3.3.1、重新执行被未捕获信号中断的系统调用
3.3.2、为所捕获的信号重新执行系统调用
如果信号被捕获,那么handle_signal()分析出错码,也可能分析sigaction表的SA_RESTART标志来决定是否必须重新执行未完成的系统调用。
如果系统调用必须被重新开始执行,handle_signal()就与do_signal()完全一样地继续执行;否则,它向用户态进程返回一个出错码-EINTR。
四、与信号处理相关的系统调用
4.1、kill()系统调用
一般用kill(pid, sig)系统调用向普通进程或多线程应用发送信号,其相应的服务例程是sys_kill()函数。整数参数pid的几个含义取决于它的值:
pid > 0:把sig信号发送到其PID等于pid的进程所属的线程组。
pid = 0:把sig信号发送到与调用进程同组的进程的所有线程组。
pid = -1:把信号发送到所有进程,除了swapper(PID 0)、init(PID 1)和current以外。
pid < -1:把信号发送到进程组-pid中进程的所有线程组。
kill()系统调用能发送任何信号,即使编号在32~64之间的实时信号。然而,我们在前面“产生信号”一节已看到,kill()系统调用不能确保把一个新的元素加入到目标进程的挂起信号队列,因此,挂起信号的多个实例可能被丢失。实时信号应当通过rt_sigqueueinfo()系统调用进行发送。
4.2、tkill()和tgkill()系统调用
tkill()和tgkill()系统调用向线程组中的指定进程发送信号。所有遵循POSIX标准的pthread库的pthread_kill()函数,都是调用这两个函数中的任意一个向指定的轻量级进程发送信号。
4.3、改变信号的操作
sigaction(sig, act, oact)系统调用允许用户为信号指定一个操作。当然,如果没有自定义的信号操作,那么内核执行与传递的信号相关的缺省操作。
POSIX标准规定,当缺省操作是"Ignore"时,把信号操作设置成SIG_IGN或SIG_DFL将引起同类型的任一挂起信号被丢弃。此外还要注意,对信号处理程序来说,不论请求屏蔽信号是什么,SIGKILL和SIGSTOP从不被屏蔽。
4.4、检查挂起的阻塞信号
4.5、修改阻塞信号的集合
sigprocmask()系统调用允许进程修改阻塞信号的集合。这个系统调用只应用于常规信号(非实时信号)。相应的sys_sigprocmask()服务例程作用于三个参数:
oset:进程地址空间的一个指针,指向存放以前位掩码的一个位数组。
set:进程地址空间的一个指针,指向包含新位掩码的位数组。
how:一个标志,可以有下列的一个值:
SIG_BLOCK:*set位掩码数组,指定必须加到阻塞信号的位掩码数组中的信号。
SIG_UNBLOCK:*set位掩码数组,指定必须从阻塞信号的位掩码数组中删除的信号。
SIG_SETMASK:*set位掩码数组,指定阻塞信号新的位掩码数组。
4.6、挂起进程
sigsuspend()系统调用把进程置为TASK_INTERRUPTBLE状态,当然这是把mask参数指向的位掩码数组所指定的标准信号阻塞以后设置的。只有当一个非忽略、非阻塞的信号发送到进程以后,进程才被唤醒。
4.7、实时信号的系统调用
阅读(876) | 评论(0) | 转发(0) |