信号的种类
可以从两个不同的分类角度对信号进行分类:
可靠性方面:可靠信号与不可靠信号;
时间的关系:实时信号与非实时信号;
(1)可靠信号与不可靠信号
Linux信号机制基本上是从Unix系统中继承过来的。早期Unix系统中的信号机制比较简单和原始,信号值小于SIGRTMIN的信号都是不可靠信号。这就是"不可靠信号"的来源。它的主要问题是信号可能丢失。
随着时间的发展,实践证明了有必要对信号的原始机制加以改进和扩充。由于原来定义的信号已有许多应用,不好再做改动,最终只好又新增加了一些信号,并在一开始就把它们定义为可靠信号,这些信号支持排队,不会丢失。
信号值位于SIGRTMIN和SIGRTMAX之间的信号都是可靠信号,可靠信号克服了信号可能丢失的问题。Linux在支持新版本的信号安装函数sigation()以及信号发送函数sigqueue()的同时,仍然支持早期的signal()信号安装函数,支持信号发送函数kill()。
信号的可靠与不可靠只与信号值有关,与信号的发送及安装函数无关。目前linux中的signal()是通过sigation()函数实现的,因此,即使通过signal()安装的信号,在信号处理函数的结尾也不必再调用一次信号安装函数。同时,由signal()安装的实时信号支持排队,同样不会丢失。
两个函数的最大区别在于,经过sigaction安装的信号都能传递信息给信号处理函数,而经过signal安装的信号不能向信号处理函数传递信息。对于信号发送函数来说也是一样的。
进程的task_struct结构中有关于本进程中未决信号的数据成员:
struct sigpending{
struct sigqueue *head, *tail;
sigset_t signal;
};
第三个成员是进程中所有未决信号集,第一、第二个成员分别指向一个sigqueue类型的结构链(称之为"未决信号信息链")的首尾,信息链中的每个sigqueue结构刻画一个特定信号所携带的信息,并指向下一个sigqueue结构:
struct sigqueue{
struct sigqueue *next;
siginfo_t info;
}
信号在进程中注册指的就是信号值加入到进程的未决信号集sigset_t signal(每个信号占用一位)中,并且信号所携带的信息被保留到未决信号信息链的某个sigqueue结构中。只要信号在进程的未决信号集中,表明进程已经知道这些信号的存在,但还没来得及处理,或者该信号被进程阻塞。
当一个实时信号发送给一个进程时,不管该信号是否已经在进程中注册,都会被再注册一次,因此,信号不会丢失,因此,实时信号又叫做"可靠信号"。这意味着同一个实时信号可以在同一个进程的未决信号信息链中占有多个sigqueue结构(进程每收到一个实时信号,都会为它分配一个结构来登记该信号信息,并把该结构添加在未决信号链尾,即所有诞生的实时信号都会在目标进程中注册)。
当一个非实时信号发送给一个进程时,如果该信号已经在进程中注册(通过sigset_t signal指示),则该信号将被丢弃,造成信号丢失。因此,非实时信号又叫做"不可靠信号"。这意味着同一个非实时信号在进程的未决信号信息链中,至多占有一个sigqueue结构。
(2)实时信号与非实时信号
早期Unix系统只定义了32种信号,前32种信号已经有了预定义值,每个信号有了确定的用途及含义,并且每种信号都有各自的缺省动作。如按键盘的CTRL ^C时,会产生SIGINT信号,对该信号的默认反应就是进程终止。后32个信号表示实时信号,等同于前面阐述的可靠信号。这保证了发送的多个实时信号都被接收。
非实时信号都不支持排队,都是不可靠信号;实时信号都支持排队,都是可靠信号。
信号的安装
inux主要有两个函数实现信号的安装:signal()、sigaction()。其中signal()只有两个参数,不支持信号传递信息,主要是用于前32种非实时信号的安装;而sigaction()是较新的函数(由两个系统调用实现:sys_signal以及sys_rt_sigaction),有三个参数,支持信号传递信息,主要用来与 sigqueue() 系统调用配合使用,当然,sigaction()同样支持非实时信号的安装。sigaction()优于signal()主要体现在支持信号带有参数。
signal()函数
程序可用使用signal()函数来处理指定的信号,主要通过忽略和恢复其默认行为来工作。signal()函数的原型如下:
#include <signal.h>
void (*signal(int sig, void (*func)(int)))(int);
signal是一个带有sig和func两个参数的函数,func是一个类型为void (*)(int)的函数指针。该函数返回一个与func相同类型的指针,指向先前指定信号处理函数的函数指针。准备捕获的信号的参数由sig给出,接收到的指定信号后要调用的函数由参数func给出。其实这个函数的使用是相当简单的,通过下面的例子就可以知道。注意信号处理函数的原型必须为void func(int),或者是下面的特殊值:
SIG_IGN : 忽略信号
SIG_DFL : 恢复信号的默认行为
sigaction()函数
struct sigaction {
union{
__sighandler_t _sa_handler; // 信号处理器(普通信号处理函数)
void (*_sa_sigaction)(int, struct siginfo *, void *); // RT signals处理函数
} _u;
sigset_t sa_mask; // 信号掩码,指定在处理信号前要暂时阻塞的附加信号集
unsigned long sa_flags; // 控制信号行为的标志位
};
该结构体用于指定如何处理特定的信号,包含以下成员:
_sa_handler: 类型为 __sighandler_t,用于指定一个普通信号处理函数。当信号触发时,该函数将被调用。
_sa_sigaction: 类型为指向函数的指针,用于指定处理实时(RT)信号的函数。该函数接收三个参数:信号编号、指向siginfo结构体的指针(包含有关信号的详细信息)和用户自定义数据指针。
sa_mask: 类型为 sigset_t,是一个信号集,表示在信号处理函数执行期间应暂时阻塞的附加信号。这样可以避免在处理信号的过程中被相同的或其他指定的信号中断。sa_flags: 类型为 unsigned long,是一组标志位,用于控制信号的行为,如是否重新设置默认处理方式、是否允许信号排队等。具体的标志位常量(如 SA_RESTART, SA_SIGINFO, SA_NODEFER, 等)可以在 <signal.h> 头文件中找到。
该函数与signal()函数一样,用于设置与信号sig关联的动作,而oact如果不是空指针的话,就用它来保存原先对该信号的动作的位置,act则用于设置指定信号的动作。
sigaction结构体定义在signal.h中,但是它至少包括以下成员:
void (*) (int) sa_handler:处理函数指针,相当于signal函数的func参数。
sigset_t sa_mask:指定一个。信号集,在调用sa_handler所指向的信号处理函数之前,该信号集将被加入到进程的信号屏蔽字中。信号屏蔽字是指当前被阻塞的一组信号,它们不能被当前进程接收到。
int sa_flags:信号处理修改器。
sa_mask 的值通常是通过使用信号集函数来设置的。
发送信号
一些进程接收到一个信号之后怎么对这个信号作出反应,即信号的处理的问题。
用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号,比如
linux> /bin/kill -9 15213
发送信号9(SIGKILL)给进程15213。一个负的PID会导致信号被发送到进程组PID中的每个进程。如果上面的15213变成负的,那么会发送信号9给进程组15213的每一个进程。
从键盘发送信号
Unix shell使用作业这个抽象概念来表示对一条命令行求值而创建的进程,在任何时刻,至多只有一个前台作业和0个或多个后台作业,比如输入 ls|sort 会创建一个由两个进程组成的前台作业,这两个进程是通过Unix管道连接起来的,一个进程会运行ls程序,另一个进程运行sort程序。shell为每个作业创建一个独立的进程组,进程组ID通常取自作业中父进程的一个。
在键盘上输入Ctrl+C会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下,结果是终止前台作业,类似的输入Ctrl+Z会发送一个SIGTSTP信号到前台进程组中的每个进程,结果是停止(挂起)前台作业。
kill()函数
进程可以通过kill()函数向包括它本身在内的其他进程发送一个信号,如果程序没有发送这个信号的权限,对kill()函数的调用就将失败,而失败的常见原因是目标进程由另一个用户所拥有。(root除外)
kill()函数的原型为:
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
它的作用把信号sig发送给进程号为pid的进程,成功时返回0。
kill()调用失败返回-1,调用失败通常有三大原因:
信号无效(errno = EINVAL)、发送权限不够( errno = EPERM )、目标进程不存在( errno = ESRCH )
该系统调用可以用来向任何进程或进程组发送任何信号。参数pid的值为信号的接收进程
pid>0 进程ID为pid的进程
pid=0 同一个进程组的进程
pid<0 pid!=-1 进程组ID为 -pid的所有进程
pid=-1 除发送进程自身外,所有进程ID大于1的进程
Kill()最常用于pid>0时的信号发送。该调用执行成功时,返回值为0;错误时,返回-1,并设置相应的错误代码errno。
alarm()函数
这个函数跟它的名字一样,给我们提供了一个闹钟的功能,进程可以调用alarm()函数在经过预定时间后向发送一个SIGALRM信号。
alarm()函数的型如下:
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
alarm()函数用来在seconds秒之后安排发送一个SIGALRM信号,如果seconds为0,将取消所有已设置的闹钟请求。alarm()函数的返回值是以前设置的闹钟时间的余留秒数,如果返回失败返回-1。
下面就给合fork()、sleep()和signal()函数,用一个例子来说明kill()函数的用法吧,源文件为signal3.c,代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
static int alarm_fired = 0;
void ouch(int sig) {
alarm_fired = 1;
}
int main() {
pid_t pid;
pid = fork();
switch (pid) {
case -1:
perror("fork failed\n");
exit(1);
case 0:
// 子进程
sleep(5);
// 向父进程发送信号
kill(getppid(), SIGALRM);
exit(0);
default:
break;
}
// 设置处理函数
signal(SIGALRM, ouch);
while (!alarm_fired) {
printf("Hello World!\n");
sleep(1);
}
if (alarm_fired)
printf("\nI got a signal %d\n", SIGALRM);
exit(0);
}
运行结果如下:
使用fork()调用复制了一个新进程,在子进程中,5秒后向父进程中发送一个SIGALRM信号,父进程中捕获这个信号,并用ouch()函数来处理,变改alarm_fired的值,然后退出循环。从结果中我们也可以看到输出了5个Hello World!之后,程序就收到一个SIGARLM信号,然后结束了进程。
注:如果父进程在子进程的信号到来之前没有事情可做,可以用函数pause()来挂起父进程,直到父进程接收到信号。当进程接收到一个信号时,预设好的信号处理函数将开始运行,程序也将恢复正常的执行。这样可以节省CPU的资源,因为可以避免使用一个循环来等待。以本例子为例,则可以把while循环改为一句pause()。
说明alarm函数和pause函数的用法吧,源文件名为,signal4.c,代码如下:
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
static int alarm_fired = 0;
void ouch(int sig) {
alarm_fired = 1;
}
int main() {
// 关联信号处理函数
signal(SIGALRM, ouch);
// 调用alarm函数,5秒后发送信号SIGALRM
alarm(5);
// 挂起进程
pause();
// 接收到信号后,恢复正常执行
if (alarm_fired == 1) {
printf("Receive a signal %d\n", SIGALRM);
}
exit(0);
}
运行结果如下
进程在5秒后接收到一个SIGALRM,进程恢复运行,打印信息并退出。
函数名:
pause
表头文件:
#include <unistd.h>
int pause(void);
pause()函数用于让当前进程暂停(进入睡眠状态),直到接收到一个信号。当进程接收到任何非阻塞的信号时,pause()函数会被该信号中断并立即返回。
返回值:pause()函数仅在接收到信号时返回,且返回值始终为 -1。
错误代码:返回 -1 时,同时会设置 errno 错误码。
EINTR: 表示有信号到达,中断了此函数的执行。
sigqueue()函数
#include <sys/types.h>
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval val)
调用成功返回 0;否则返回 -1。
sigqueue()是比较新的发送信号系统调用,主要是针对实时信号提出的(当然也支持前32种),支持信号带有参数,与函数sigaction()配合使用。
sigqueue的第一个参数是指定接收信号的进程ID,第二个参数确定即将发送的信号,第三个参数是一个联合数据结构union sigval,指定了信号传递的参数,即通常所说的4字节值。
typedef union sigval {
int sival_int;
void *sival_ptr;
}sigval_t;
sigqueue()比kill()传递了更多的附加信息,但sigqueue()只能向一个进程发送信号,而不能发送信号给一个进程组。如果signo=0,将会执行错误检查,但实际上不发送任何信号,0值信号可用于检查pid的有效性以及当前进程是否有权限向目标进程发送信号。
在调用sigqueue时,sigval_t指定的信息会拷贝到对应sig 注册的3参数信号处理函数的siginfo_t结构中,这样信号处理函数就可以处理这些信息了。由于sigqueue系统调用支持发送带参数信号,所以比kill()系统调用的功能要灵活和强大得多。
abort()函数
#include <stdlib.h>
void abort(void);
向进程发送SIGABORT信号,默认情况下进程会异常退出,当然可定义自己的信号处理函数。即使SIGABORT被进程设置为阻塞信号,调用abort()后,SIGABORT仍然能被进程接收。该函数无返回值。
raise()函数
#include <signal.h>
int raise(int signo)
向进程本身发送信号,参数为即将发送的信号值。调用成功返回 0;否则,返回 -1。
setitimer()函数
现在的系统中很多程序不再使用alarm调用,而是使用setitimer调用来设置定时器,用getitimer来得到定时器的状态,这两个调用的声明格式如下:
int getitimer(int which, struct itimerval *value);
int setitimer(int which, const struct itimerval *value, struct itimerval *ovalue);
在使用这两个调用的进程中加入以下头文件:
#include <sys/time.h>
该系统调用给进程提供了三个定时器,它们各自有其独有的计时域,当其中任何一个到达,就发送一个相应的信号给进程,并使得计时器重新开始。三个计时器由参数which指定,如下所示:
TIMER_REAL:按实际时间计时,计时到达将给进程发送SIGALRM信号。
ITIMER_VIRTUAL:仅当进程执行时才进行计时。计时到达将发送SIGVTALRM信号给进程。
ITIMER_PROF:当进程执行时和系统为该进程执行动作时都计时。与ITIMER_VIR-TUAL是一对,该定时器经常用来统计进程在用户态和内核态花费的时间。计时到达将发送SIGPROF信号给进程。
定时器中的参数value用来指明定时器的时间,其结构如下:
struct itimerval {
struct timeval it_interval; /* 下一次的取值 */
struct timeval it_value; /* 本次的设定值 */
};
该结构中timeval结构定义如下:
struct timeval {
long tv_sec; /* 秒 */
long tv_usec; /* 微秒,1秒 = 1000000 微秒*/
};
在setitimer 调用中,参数ovalue如果不为空,则其中保留的是上次调用设定的值。定时器将it_value递减到0时,产生一个信号,并将it_value的值设定为it_interval的值,然后重新开始计时,如此往复。当it_value设定为0时,计时器停止,或者当它计时到期,而it_interval 为0时停止。调用成功时,返回0;错误时,返回-1,并设置相应的错误代码errno:
EFAULT:参数value或ovalue是无效的指针。
EINVAL:参数which不是ITIMER_REAL、ITIMER_VIRT或ITIMER_PROF中的一个。
接收信号
当内核把进程p从内核模式切换到用户模式时,它会检查进程p的未被阻塞的待处理信号的集合,如果这个集合为空,那么内核将控制传递到p的逻辑控制流中的下一条指令。如果集合是非空的,那么内核选择集合中的某个信号k(通常是最小的k),并且强制p接收信号k,收到这个信号会触发进程采取某种行为,一旦进程完成了这个行为,那么控制就传递回p的逻辑控制流中的下一条指令,每个信号类型都有一个预定的默认行为,是下面的一种:
*进程终止
*进程终止并转储内存。
*进程停止(挂起)直到被SIGCONT信号重启
*进程忽略该信号
比如收到SIGKILL的默认行为,就是终止接收进程,另外,接收到SIGCHLD的默认行为就是忽略这个信号。进程可以通过使用signal函数修改和信号相关联的默认行为,唯一例外的是SIGSTOP和SIGKILL,它们的默认行为是不能被修改的。
#include <signal.h>
typedef void (*sighandler_t)(int);
//成功则指向前次处理程序的指针,出错则为SIG_ERR
sighandler_t signal(int signum, sighandler_t handler);
上图中定义了一个类型sighandler_t,表示指向返回值为void型(参数为int型)的函数(的)指针。它可以用来声明一个或多个函数指针。signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
*如果handler是SIG_IGN,那么忽略类型为signum的信号。
*如果handler是SIG_DFL,那么类型为signum的信号恢复为默认行为。
*否则函数就是用户定义的函数的地址,这个函数就称为信号处理函数。只要进程收到一个类型为signum的信号,就会调用这个程序,通过把处理程序的地址传递到signal函数从而改变默认行为。这叫做设置信号处理程序,调用信号处理程序被称为捕获信号,执行信号处理程序。
当一个进程捕获了一个类型为k的信号时,会调用为信号k设置的处理程序,一个整数参数被设置为k。这个参数允许同一个处理函数捕获不同类型的信号。当处理程序执行它的return语句,控制通常传递回控制流中进程被信号接收中断位置处的指令。
内核处理一个进程收到的软中断信号是在该进程的上下文中,因此,进程必须处于运行状态。
当其由于被信号唤醒或者正常调度重新获得CPU时,在其从内核空间返回到用户空间时会检测是否有信号等待处理。如果存在未决信号等待处理且该信号没有被进程阻塞,则在运行相应的信号处理函数前,进程会把信号在未决信号链中占有的结构卸掉。
对于非实时信号来说,由于在未决信号信息链中最多只占用一个sigqueue结构,因此该结构被释放后,应该把信号在进程未决信号集中删除(信号注销完毕);而对于实时信号来说,可能在未决信号信息链中占用多个sigqueue结构,因此应该针对占用sigqueue结构的数目区别对待:如果只占用一个sigqueue结构(进程只收到该信号一次),则执行完相应的处理函数后应该把信号在进程的未决信号集中删除(信号注销完毕)。否则待该信号的所有sigqueue处理完毕后再在进程的未决信号集中删除该信号。
当所有未被屏蔽的信号都处理完毕后,即可返回用户空间。对于被屏蔽的信号,当取消屏蔽后,在返回到用户空间时会再次执行上述检查处理的一套流程。
内核处理一个进程收到的信号的时机是在一个进程从内核态返回用户态时。所以,当一个进程在内核态下运行时,软中断信号并不立即起作用,要等到将返回用户态时才处理。进程只有处理完信号才会返回用户态,进程在用户态下不会有未处理完的信号。
当进程接收到一个它忽略的信号时,进程丢弃该信号,就象没有收到该信号似的继续运行。如果进程收到一个要捕捉的信号,那么进程从内核态返回用户态时执行用户定义的函数。
执行用户定义的函数的方法很巧妙,内核是在用户栈上创建一个新的层,该层中将返回地址的值设置成用户定义的处理函数的地址,这样进程从内核返回弹出栈顶时就返回到用户定义的函数处,从函数返回再弹出栈顶时,才返回原先进入内核的地方。这样做的原因是用户定义的处理函数不能且不允许在内核态下执行。
信号处理函数的安全问题
试想一个问题,当进程接收到一个信号时,转到你关联的函数中执行,但是在执行的时候,进程又接收到同一个信号或另一个信号,又要执行相关联的函数时,程序会怎么执行?
也就是说,信号处理函数可以在其执行期间被中断并被再次调用。当返回到第一次调用时,它能否继续正确操作是很关键的。这不仅仅是递归的问题,而是可重入的(即可以完全地进入和再次执行)的问题。而反观Linux,其内核在同一时期负责处理多个设备的中断服务例程就需要可重入的,因为优先级更高的中断可能会在同一段代码的执行期间“插入”进来。
简言之,就是说,我们的信号处理函数要是可重入的,即离开后可再次安全地进入和再次执行,要使信号处理函数是可重入的,则在信息处理函数中不能调用不可重入的函数。