当前位置: 首页 > 科技观察

Linux信号(signal)机制分析(一)

时间:2023-03-16 16:54:39 科技观察

【摘要】本文分析了Linux内核对信号的实现机制以及应用层的相关处理。首先介绍了软中断信号的性质和两种不同的信号分类方法,特别是不可靠信号的原理。然后分析内核对信号的处理流程,包括信号触发/注册/执行和取消。***介绍了应用层的相关处理,主要包括信号处理函数的安装、信号传输、屏蔽和阻断等。***给出了几个简单的应用实例。[关键词]软中断信号,signal,sigaction,kill,sigqueue,settimer,sigmask,sigprocmask,sigset_t1。信号本质软中断信号(signal,也简称信号)用于通知进程发生了异步事件。在软件层面,是对中断机制的模拟。原则上,进程接收信号与处理器接收中断请求是一样的。信号是进程间通信机制中唯一的异步通信机制。一个进程不需要执行任何操作来等待信号的到来。实际上,进程并不知道信号何时到达。进程之间可以通过系统调用kill来相互发送软中断信号。内核也可以因为内部事件向进程发送信号,通知进程有事件发生。除了基本的通知功能外,信号机制还可以传递额外的信息。接收信号的进程对各种信号有不同的处理方法。处理方式可以分为三类:第一类是类似于中断的处理程序。对于一个需要处理的信号,进程可以指定一个处理函数,由该函数来处理。第二种方法是忽略一个信号,什么也不做,就好像它从未发生过一样。第三种方法是保留系统对信号处理的默认值。这个默认操作,大多数信号的默认操作是导致进程终止。进程使用系统调用信号来指定进程对某个信号的处理行为。2.信号的类型信号可以从两个不同的分类角度进行分类:可靠性方面:可靠信号和不可靠信号;与时间有关:实时信号和非实时信号。2.1可靠信号与不可靠信号Linux的信号机制基本上是从Unix系统继承而来的。早期Unix系统的信号机制比较简单原始,信号值小于SIGRTMIN的信号是不可靠信号。这就是“不可靠信号”的来源。它的主要问题是信号可能会丢失。随着时间的推移,实践证明有必要改进和扩展原有的信号机制。由于最初定义的信号已经在很多应用中使用,因此不容易进行更改。最后还得加上一些新的信号,一开始就定义为可靠信号。这些信号支持排队,不会丢失。信号值在SIGRTMIN和SIGRTMAX之间的信号是可靠信号,可靠信号克服了可能丢失信号的问题。在支持新版本的信号安装函数sigation()和信号发送函数sigqueue()的同时,Linux仍然支持早期的signal()信号安装函数和信号发送函数kill()。信号的可靠性和不可靠性只与信号值有关,与信号的发送和安装功能无关。目前Linux中的signal()是通过signal()函数实现的。因此,即使是通过signal()安装信号,也不需要在信号处理函数的最后再次调用信号安装函数。同时signal()安装的实时信号支持排队,同样不会丢失。对于linux目前的两个信号安装函数:signal()和sigaction(),都不能把SIGRTMIN之前的信号变成可靠信号(都不支持排队,还是有丢失的可能,还是不可靠信号),并且支持在SIGRTMIN之后排队等待信号。这两个函数最大的区别在于sigaction安装的signal可以给信号处理函数传递信息,而signal安装的signal不能给信号处理函数传递信息。信号发送功能也是如此。2.2实时信号和非实时信号早期的Unix系统只定义了32种信号,前32种信号已经有了预定义的值,每种信号都有明确的用途和意义,每种信号都有自己的默认值行动。例如,当你在键盘上按下CTRL^C时,会产生一个SIGINT信号,对这个信号的默认响应是终止进程。最后32个信号代表实时信号,相当于上面解释的可靠信号。这确保接收到发送的多个实时信号。非实时信号不支持排队,是不可靠的信号;实时信号支持排队,都是可靠信号。3.一个完整的信号生命周期(从信号发送到对应的处理函数执行)信号处理流程可以分为三个阶段:events:硬件来源(比如我们按下键盘或者其他硬件故障);软件源,最常用的发送信号的系统函数有kill、raise、alarm和setitimer和sigqueue函数,软件源还包括一些非法操作等操作。这里按信号产生的原因简单分类一下,了解各种信号:(1)与进程终止相关的信号。这种类型的信号在进程退出或子进程终止时发出。(2)与进程异常事件相关的信号。如进程越界,或试图写入只读内存区(如程序文本区),或执行特权指令等各种硬件错误。(3)与在系统调用期间遇到不可恢复条件相关的信号。比如执行系统调用exec时,原来的资源已经释放,当前系统资源已经耗尽。(4)与执行系统调用时遇到不可预测的错误条件相关的信号。比如执行一个不存在的系统调用。(5)用户态进程发送的信号。例如,一个进程调用系统调用kill向其他进程发送信号。(6)与终端交互相关的信号。例如,用户关闭终端,或按下break键等。(7)跟踪进程执行的信号。Linux支持的信号列表如下。许多信号与机器的架构有关。默认处理操作会导致发送信号。SIGHUP1A.终端挂起或控制进程终止。SIGINT2A.键盘被中断(如break键被按下)。SIGILLpressed4C非法指令SIGABRT6C由abort(3)发出的中止指令SIGFPE8C浮点异常SIGKILL9AEF终止信号SIGSEGV11C无效内存引用SIGPIPE13A损坏的管道:写入没有读取端口的管道SIGALRM14ASignalSIGTERMalarm(2)1***终止信号SIGUSR130,10,16AUser-definedsignal1SIGUSR231,12,17AUser-definedsignal2SIGCHLD20,17,18BSubprocessterminatedSignalSIGCONT19,18,25Processcontinues(processthatwasstopped)SIGS***7,19,23DEFTerminateprocessSIGTSTP18,20,24D按控制终端(tty)上的停止键SIGTTIN21,21,26D后台进程尝试从控制终端读取SIGTTOU。22,22,27D后台进程尝试从控制终端写入。动作项中字母的含义如下A默认动作是终止进程B默认动作是忽略这个信号,Discardthesignal不做处理。C的默认动作是终止进程并转储内核映像(dumpcore)。某种格式转储到文件系统,进程退出执行。这样做的好处是为程序员提供了方便,让他们可以在执行时得到进程的数据值,让他们判断dump的原因,从而调试他们的程序。D默认动作是停止进程,进入停止状态后可以继续,一般在调试过程中(如ptrace系统调用)E信号不能被捕获F信号不能被忽略3.2目标进程中注册信号在进程表的表项中有一个软中断信号域,这个域中的每一位对应一个信号。内核向进程发送软中断信号的方法是在进程所在的进程表项的signal字段中设置该信号对应的位。如果信号发送给一个休眠的进程,如果该进程休眠的优先级可以被中断,则唤醒该进程;否则,只设置进程表中signal域的相应位,不唤醒进程。如果发送给处于可运行状态的进程,则只能设置相应的域。进程的task_struct结构有关于这个进程中挂起信号的数据成员:structsigpendingpending:structsigpending{structsigqueue*head,*tail;sigset_tsignal;};第三个成员是进程中所有挂起的信号集,***,第二个成员指向一条sigqueue类型结构链(称为“挂起信号信息链”)的首尾。信息链中的每个sigqueue结构体描述了一个特定信号携带的信息,并指向下一个sigqueue结构体:structsigqueue{structsigqueue*next;siginfo_tinfo;}过程中的信号注册就是将信号值添加到pending信号集合sigset_t中进程的signal(每个signal占一个bit),signal携带的信息保留在pendingsignal信息链的一个sigqueue结构中。只要信号在进程的挂起信号集中,就说明进程已经知道这些信号的存在,只是还没来得及处理,或者信号被进程阻塞了。当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册过,都会重新注册,这样信号就不会丢失,所以实时信号又称为“可靠的信号”。这意味着同一个实时信号可以在同一个进程的pending信号信息链中占用多个sigqueue结构(进程每收到一个实时信号,都会为其分配一个结构体来注册信号信息,而将结构添加到挂起信号链的末尾,即所有诞生的实时信号都会在目标进程中注册)。当向进程发送非实时信号时,如果该信号已经在进程中注册(由sigset_t信号表示),该信号将被丢弃,导致信号丢失。因此,非实时信号也被称为“不可靠信号”。这意味着同一个非实时信号在进程的未决信号信息链中最多占用一个sigqueue结构。总之,信号是否注册与发送信号的函数(如kill()或sigqueue()等)和信号安装函数(signal()和sigaction())无关,而是只和信号值有关(小于SIGRTMIN的信号值最多只注册一次,SIGRTMIN和SIGRTMAX之间的信号值只要被进程接收到就会被注册)3.3信号执行和注销内核处理软中断进程接收到的信号是在进程的上下文中,因此,进程必须正在运行。当它被信号唤醒或正常调度夺回CPU时,它会在从内核空间返回到用户空间时检测是否有信号等待处理。如果有等待处理的挂起信号,并且该信号没有被进程阻塞,则进程在运行相应的信号处理函数之前,会先卸载该信号在挂起信号链中占用的结构体。对于非实时信号,由于挂起信号信息链中最多只占用一个sigqueue结构,释放该结构后,应在进程挂起信号集中删除该信号(信号取消完成);对于实时信号来说,在pending信号信息链中可能会占用多个sigqueue结构,所以要区别对待占用的sigqueue结构个数:如果只占用一个sigqueue结构(进程只接收一次信号),执行对应的处理函数,最后应该将信号从进程的pendingsignalset中删除(信号取消完成)。否则,在处理完信号的所有信号队列后,从进程的未决信号集中删除该信号。处理完所有未屏蔽的信号后,您可以返回用户空间。对于被屏蔽的信号,取消屏蔽后,返回用户空间时,会再次执行上述的一套检查处理流程。内核处理进程接收到的信号的时机是进程从内核态返回到用户态时。因此,当一个进程运行在内核态时,软中断信号并不会立即生效,直到返回用户态才会进行处理。进程只有处理完信号才会回到用户态,在用户态进程不会有未处理的信号。信号处理分为三种:进程收到信号后退出;该过程忽略该信号;进程接收到系统调用信号后,执行用户设置的功能。当一个进程收到一个它忽略的信号时,进程会丢弃该信号并继续运行,就好像没有收到信号一样。如果进程接收到要捕获的信号,则当进程从内核模式返回到用户模式时执行用户定义的函数。而且,执行用户自定义函数的方法非常巧妙。内核在用户栈上新建一层,在这一层中,将返回地址的值设置为用户自定义处理函数的地址,这样当进程从内核返回时,将栈顶弹出stack返回的是用户自定义函数,当函数返回弹出栈顶时,返回到最初进入内核的地方。这样做的原因是用户自定义的处理函数不能也不允许在内核态下执行(如果用户自定义函数运行在内核态下,用户可以获得任何权限)。4、信号的安装如果进程要处理某个信号,那么必须在进程中安装信号。安装信号主要用于确定信号值与进程对信号值的动作的映射关系,即进程将处理哪个信号;当信号传递给进程时,将执行什么操作。linux主要有两个函数来实现signals的安装:signal()和sigaction()。其中,signal()只有两个参数,不支持信号传输信息,主要用于安装前32个非实时信号;而sigaction()是一个较新的函数(由两个系统调用实现:sys_signal和sys_rt_sigaction),一共有三个参数,支持信号传输信息,主要与sigqueue()系统调用配合使用。当然sigaction()也支持非实时信号的安装。sigaction()优于signal()主要是因为它支持带参数的信号。4.1signal()#includevoid(*signal(intsignum,void(*handler))(int)))(int);如果函数原型不好理解,可以参考下面的分解方法理解:typedefvoid(*sighandler_t)(int);sighandler_tsignal(intsignum,sighandler_thandler));第一个参数指定信号的值,第二个参数指定对前一个信号值的处理,可以忽略(参数设置为SIG_IGN);可以使用系统默认的方式处理信号(参数设置为SIG_DFL);也可以自己实现处理方法(参数指定函数地址)。如果signal()调用成功,则返回最后一次调用signal()时handler的值,安装signalsignum;如果失败,则返回SIG_ERR。传递给信号处理例程的整数参数是信号值,它允许一个信号处理例程处理多个信号。#include#include#includevoidsigroutine(intdunno){/*信号处理例程,其中dunno会获取信号的值*/switch(dunno){case1:printf("Getasignal--SIGHUP");break;case2:printf("Getasignal--SIGINT");break;case3:printf("Getasignal--SIGQUIT");break;}return;}intmain(){printf(“processidis%d”,getpid());signal(SIGHUP,sigroutine);//*下面设置三个信号的处理方式,并通过按Ctrl-发出信号SIGQUIT。程序执行结果如下:localhost:~$./sig_testprocessidis463Getasignal-SIGINT//按Ctrl-C得到结果Getasignal-SIGQUIT//按Ctrl-得到结果//按Ctrl-z把processinthebackground[1]+Stopped./sig_testlocalhost:~$bg[1]+./sig_test&localhost:~$kill-HUP463//发送SIGHUP信号给进程localhost:~$Getasignal–SIGHUPkill-9463//发送SIGKILL向进程发出信号,终止进程localhost:~$4.2sigaction()#includeintsigaction(intsignum,conststructsigaction*act,structsigaction*oldact));sigaction函数用于在接收到特定信号后改变进程的行为。该函数的第一个参数是信号的值,可以是除SIGKILL和SIGSTOP之外的任何特定的有效信号(为这两个信号定义自己的处理函数会导致信号安装错误)。第二个参数是指向结构sigaction实例的指针。在结构体sigaction的实例中,指定了对特定信号的处理。可以为空,进程默认会处理信号;第三个参数oldact指向的对象用于保存对应信号返回的原始处理,oldact可以指定为NULL。如果第二个和第三个参数都设置为NULL,那么这个函数可以用来检查信号的有效性。第二个参数是最重要的,它包括对指定信号的处理,信号传递的信息,信号处理函数执行过程中应该屏蔽哪些信号等等。sigaction结构定义如下:structsigaction{union{__sighandler_t_sa_handler;void(*_sa_sigaction)(int,structsiginfo*,void*);}_usigset_tsa_mask;unsignedlongsa_flags;}1。联合数据结构_sa_handler和*_sa_sigaction中的两个元素指定了信号关联函数,即用户指定的信号处理函数。除了是用户自定义的处理函数外,还可以是SIG_DFL(使用默认的处理方式)或者SIG_IGN(忽略信号)。2、_sa_sigaction指定的信号处理函数,有3个参数,是为实时信号设计的(当然也支持非实时信号)。它指定了一个3参数信号处理函数。第一个参数是信号值,第三个参数不用,第二个参数是指向siginfo_t结构体的指针,里面包含了信号携带的数据值,参数指向的结构体如下:siginfo_t{intsi_signo;/*信号值,对所有信号有意义*/intsi_errno;/*errno值,对所有信号有意义*/intsi_code;/*信号原因,对所有信号有意义*/union{/*联合数据结构,适应不同的成员不同的信号*///确保分配足够大的存储空间int_pad[SI_PAD_SIZE];//结构体{...}对SIGKILL有意义...//对SIGILL、SIGFPE、SIGSEGV、SIGBUS有意义structuresstruct{...}...}}在讨论系统调用sigqueue发送信号时,sigqueue的第三个参数是sigval联合数据结构,当调用sigqueue时,这个数据结构中的数据会被复制到第二个信号处理函数的参数。这样,在发送信号的同时,信号可以传递一些附加信息。信号能够传递信息,对于程序开发是非常有意义的。3.sa_mask指定在信号处理程序执行期间应阻止哪些信号。默认情况下,当前信号本身被阻塞以防止信号的嵌套发送,除非指定了SA_NODEFER或SA_NOMASK标志。注意:请注意sa_mask指定的信号阻塞的前提是sa_mask指定的信号在sigaction()安装的处理函数执行过程中被阻塞。4、sa_flags包含了很多flags,包括刚才提到的SA_NODEFER和SA_NOMASKflags。另一个重要标志是SA_SIGINFO。设置此标志时,意味着可以将附加到信号的参数传递给信号处理函数。因此,应该在sigaction结构体中为sa_sigaction指定处理函数,而不是为sa_handler指定信号处理函数,否则设置该标志就没有意义了。即使为sa_sigaction指定了一个信号处理函数,如果没有设置SA_SIGINFO,信号处理函数就无法获取到信号传递过来的数据,在信号处理函数中访问这些信息会导致段错误(Segmentationfault)。