Linux/UNIX之信号(1)


信号(1)

信号是软件中断。每个信号都有一个名字,这些名字都以SIG开头(如SIGABRT 夭折信号)。

在头文件<signal.h>中,这些信号都被定义成正整数。不存在编号为0的信号,kill函数对信号编号为0有特殊的应用。

当某个信号出现时,可以要求内核按照下列三种方式之一进行处理:

1. 忽略此信号

2. 捕捉信号

3. 执行系统默认动作

signal函数

UNIX信号机制最简单的接口是signal函数。

#include <signal.h>

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum,sighandler_t handler);

此函数中signum是在头文件<signal.h>定义的信号名,handler的值是常量SIG_IGN、常量SIG_DFL或当地接到此信号后要调用的函数地址。如果指定SIG_IGN,则向内核表示忽略此信号。如果指定SIG_DFL,则表示接到此信号后的动作是系统默认动作。当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为捕捉该信号。称此函数为信号处理程序或信号捕捉函数。返回值也是一个函数指针,它是指向之前的信号处理程序的指针。

如下程序显示了一个简单的信号处理程序,它捕捉两个用户定义的信号并打印信号编号。

#include<stdio.h>
#include<signal.h>
 
static voidsig_usr(int);   /* one handler for bothsignals */
 
int main(void)
{
    if (signal(SIGUSR1, sig_usr) == SIG_ERR)
        perror("can't catchSIGUSR1");
    if (signal(SIGUSR2, sig_usr) == SIG_ERR)
        perror("can't catchSIGUSR2");
    for ( ; ; )
        pause();
}
 
static void sig_usr(intsigno)      /* argument is signal number*/
{
    if (signo == SIGUSR1)
       printf("received SIGUSR1\n");
    else if (signo == SIGUSR2)
        printf("received SIGUSR2\n");
    else
        printf("received signal%d\n", signo);
}

执行及输出结果如下:

chen123@ubuntu:~/user/apue.2e$ ./a.out &

[1] 5712

chen123@ubuntu:~/user/apue.2e$kill -USR1 5712

received SIGUSR1

chen123@ubuntu:~/user/apue.2e$kill -USR2 5712

received SIGUSR2

chen123@ubuntu:~/user/apue.2e$ kill 5712

[1]+ Terminated ./a.out

我们在后台调用该程序,并且用kill(1)命令将信号传送给它。注意,在UNIX中,kill不代表杀死进程。kill(1)和kill(2)只是将一个信号送给一个进程或进程组。信号是否终止进程取决于信号的类型,以及进程是否安排了捕捉该信号。

程序启动:当一个程序启动时,所有信号的状态都是系统默认或忽略。在上面的程序中的以下代码:

if(signal(SIGUSR1, sig_usr) == SIG_ERR)

perror("can't catch SIGUSR1");

if(signal(SIGUSR2, sig_usr) == SIG_ERR)

perror("can't catch SIGUSR2");

表示仅当信号当前未被忽略时,进程才会捕捉它们。从signal的这两个调用中可以看到这种函数的限制:不改变信号的处理方式就不能确定信号的当前处理方式。sigaction函数可以确定一个信号的处理方式,而无需改变它。

进程创建:当一个进程调用fork,其子进程继承父进程的信号处理方式。

kill和raise函数

#include <sys/types.h>

#include <signal.h>

int kill(pid_t pid, int sig);

#include <signal.h>

int raise(int sig);

成功返回0,出错返回-1。调用raise(sig)等价于调用kill(getpid(), sig)

kill的pid参数有四种可能:

pid > 0: 将该信号发送给进程ID为pid的进程

pid == 0: 将该信号发送给与发送进程属于同一进程组的所有进程,而且发送进程具有向这些进程发送信号的权限。

pid<0: 将该信号发送给其进程组ID等于pid的绝对值,而且发送进程具有向其发送信号的权限。

pid == -1:将该信号发送给发送进程有权限向他们发送信号的系统上的所有进程。

alarm和pause函数

#include<unistd.h>

unsigned intalarm(unsigned int seconds);

使用alarm函数可以设置一个计时器,在将来某个指定的时间该计时器会超时。当计时器超时时,产生SIGALRM信号。如果不忽略或不捕捉此信号,则其默认动作是终止调用该alarm函数的进程。

其中,参数seconds的值是秒数。要了解的是,经过指定的秒数后,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能处理信号还需要一些时间。

每个进程只能有一个闹钟时钟。

#include<unistd.h>

int pause(void);

pause函数使调用进程挂起直到捕捉到一个信号。只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回-1,并将errno设置为EINTR。

信号集

我们需要一个能表示多个信号的——信号集的数据类型。在诸如siprocmask之类的函数中使用这种数据类型,以便告诉内核不允许发生该信号集中的信号。POSIX.1定义了数据类型sigset_t以包含一个信号集,并且定义了下列五个处理信号集的函数。

#include<signal.h>

intsigemptyset(sigset_t *set);

intsigfillset(sigset_t *set);

intsigaddset(sigset_t *set, int signum);

intsigdelset(sigset_t *set, int signum);

intsigismember(const sigset_t *set, int signum);

函数sigemptyset初始化由set指向的信号集,清除其中所有的信号。函数sigfillset初始化由set指向的信号集,使其包括所有信号。所有应用程序在使用信号集前,要对该信号集调用sigempty或sigfillset一次。这是因为C编译器把未赋初值的外部和静态变量都初始化为0,而这是否与给定系统上信号集的实现相对应却并不清楚。

一旦已经初始化了一个信号集,以后就可在该信号集中增、删特定的信号。函数sigaddset 将添加一个信号到现有集中,sigdelset 则从信号集中删除一个信号。对所有以信号集作为参数的函数,我们总是以信号集地址作为向其传送的参数。

sigprocmask函数

#include<signal.h>

intsigprocmask(int how, const sigset_t *set, sigset_t *oldset);

返回值:若成功则返回0,若出错则返回-1。

首先,若oset是非空指针,那么进程的当前信号屏蔽字通过oset返回。

其次,若set是一个非空指针,则参数how指示如何修改当前信号屏蔽字。下面说明了how可选用的值。

SIG_BLOCK 该进程的信号屏蔽字是其当前信号屏蔽字和set指向信号集的并集。set包含了我们希望阻塞的附加信号。

SIG_UNBLOC 该进程的信号屏蔽字是其当前信号屏蔽字和set所指信号集补集的交集。set包含了我们希望解除阻塞的信号。

SIG_SETMASK 该进程新的信号屏蔽字将被set指向的信号集的值替换。

如果set是空指针,则不改变进程的信号屏蔽字。how的值也无意义。

sigpending函数

#include<signal.h>

intsigpending(sigset_t *set);

sigpending函数返回信号集,其中的各个信号对于调用进程时阻塞的而不能递送,因而也一定是当前未决的。该信号集通过set参数返回。

下面的程序使用了很多前面说过的信号功能。

#include <stdio.h>
#include <signal.h>
 
static void sig_quit(int);
 
int
main(void)
{
   sigset_t    newmask, oldmask,pendmask;
 
   if (signal(SIGQUIT, sig_quit) == SIG_ERR)
       perror("can't catch SIGQUIT");
 
   /*
    * Block SIGQUIT and save current signal mask.
    */
   sigemptyset(&newmask);
   sigaddset(&newmask, SIGQUIT);
   if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
       perror("SIG_BLOCK error");
 
sleep(5);   /* SIGQUIT here will remain pending */、
exit(0);
}
 
static void
sig_quit(intsigno)
{
    printf("caught SIGQUIT\n");
    if (signal(SIGQUIT, SIG_DFL) == SIG_ERR)
        perror("can't reset SIGQUIT");
}

执行及输出结果:

./a.out

^\ 产生信号一次(在5秒钟之内)

SIGQUIT pending 从sleep返回后

caught SIGQUIT 在信号处理程序中

SIGQUIT unblocked 从sigprocmask返回后

^\Quit 再次产生信号

./a.out

^\^\^\^\^\^\^\^\^\^\ 产生信号10次

SIGQUIT pending

caught SIGQUIT 只产生信号一次

SIGQUIT unblocked

^\Quit 再次产生信号

进程阻塞SIGQUIT信号,保存了当前信号屏蔽字(以便以后恢复),然后休眠5秒钟。再次期间所产生的退出信号SIGQUIT都会被阻塞,而不递送至该进程,直到该信号不再被阻塞。在5秒钟休眠结束后,检查该信号是否是未决的,然后将SIGQUIT设置为不再阻塞。

在设置SIGQUIT为阻塞时,我们保存了旧屏蔽字。为了解除对该信号的阻塞,用旧屏蔽字重新设置了进程信号屏蔽字(SIG_SETMASK)。

在休眠期间如果产生退出信号,那么此时该信号是未决的,但是不再受阻塞,所以在sigprocmask返回之前,它被递送到调用进程。从程序的输出可以看出:SIGQUIT处理程序中的printf语句先执行,然后再执行sigprocmask之后的printf语句。

相关内容