Unix信号机制(上)

来源:互联网 发布:网狐精华版 源码 编辑:程序博客网 时间:2024/05/16 15:29

(一).信号基本概念

相关概念:

信号是软件中断,它提供了一种处理异步事件的方法。当A和B进行通信时,B收到了信号,不管程序执行到了哪,都会暂停去处理信号。并且每个进程收到的所有信号,都是内核负责发送和处理,而我们捕捉到信号编写的处理函数,只是相当于内核处理的时候调用的一个函数。在头文件signal.h中,信号名都被定义为正整数变量(信号编号),在linux下,可以使用kill -l查看。

信号产生方法:

1.按键产生:比如Ctrl+c可以杀掉当前进程,其实是产生了一个中断信号(SIGINT)
2.硬件异常产生:如果我们编写的代码中有除以0这样的操作,程序就会异常中止,实质是产生了SIGFPE信号,还有非法访问内存会提示段错误,也是产生了一个名为SIGSEGV的信号
3.软件产生:比如调用alarm函数
4.命令产生:我们经常用来关进程的kill命令,会产生SIGKILL信号
5.系统调用:比如kill函数和abort函数等

信号的状态:

由于信号是借助软件方法实现的,所以它的延迟肯定要比硬件实现的高,但是对于用户来说,延迟时间还是很短,不易察觉。我们把信号发送之后到达进程这种状态称为递达,那么信号处于产生和递达之间的这种状态就称为未决,但是前面说到了,对于用户来说,延迟时间很短,所以是未决态的时间应该很短才对,但是还是把这种状态提了出来,这是因为造成信号未决的主要原因是屏蔽信号,而不是正常发送和接收的这个过程

信号的处理方式:

前面也提到了一个很重要的概念,就是信号都是由内核负责发送和处理,从这点就可以大约猜到,信号有自己默认的处理方式。还有另外两种处理方式,一种是忽略该信号,还有一种就是捕捉该信号

信号的编号:

前面提到了可以使用kill -l命令查看信号名,结果如下:

column column column column column 1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM 16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ 26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13 48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX

编号1-31的信号称为常规信号,34-64称为实时信号,驱动编程与硬件相关的信号。所以不从事硬件方面的只用了解并熟悉前31个信号就行了。

信号的四要素:

1.编号
2.名称
3.事件
4.默认处理动作

通过命令man 7 signal查看帮助文档,编号就是对应的值那一列,名称对应信号那一列,默认处理动作对应动作那一列,事件对应说明那一栏。在值的那一列,有个别信号居然有多个值,这是在不同的架构下,信号拥有不同的值而已,我们平时用的多数都是x86和arm架构,对应中间的值。
默认处理动作也有如下几个行为:
1.终止进程
2.终止进程并产生core文件(用于检查进程死亡原因
3.忽略信号
4.暂停进程
5.继续运行

并不是所有的信号都能被捕获或者忽略,9)SIGKILL和19)SIGSTOP信号就不允许忽略和捕捉,只能执行默认动作
这里有必要提一下,前面写道信号的三个处理方式中也有一个忽略,但是这两个忽略并不同,默认处理动作中的忽略是默认就忽略该信号了。

信号集:

顾名思义,就是信号的集合。这样便于进程相关的捕捉或者屏蔽操作等。
linux内核的进程控制块pcb是一个结构体,其中包含了进程id,状态,工作目录,用户id,组id,文件描述符表也包含了阻塞信号集以及未决信号集。
阻塞信号集(信号屏蔽字):将某些信号加入集合,一旦设置了屏蔽,那么当该信号被接收时,不会马上处理该信号,而是在解除屏蔽后再处理
未决信号集:信号产生时,未决信号集中对应的这个信号置为1,表示处于未决态,当信号递达后,又重新置为0。还有另外一种情况,就是当信号产生后由于一些原因导致不能递达,就会处于未决态。并且如果信号被屏蔽了,那么在屏蔽被解除前,该信号都一直处于未决态。

(二).常见的一些产生信号的函数

函数原型:int kill(pid_t pid, int sig)
返回值:成功返回0,失败返回-1,并设置errno
参数:第一个参数代表进程的id,pid>0:发送信号给指定的进程,pid=0:发送信号给和调用kill函数进程属于同一进程组的所有进程,pid<0:发送给对应|pid|的进程组,pid=-1:发送给进程有权限发送的所有进程(root可以向任何用户发送信号,而普通用户不能向root用户和其他普通用户发送信号)

例子:

#include <stdio.h>#include <unistd.h>#include <signal.h>int main(){    pid_t pid;    int i;    for(i = 0; i < 5; i++)  //循环创建5个子进程    {        pid = fork();        if(pid == 0)            break;    }    if(pid == 0 && i == 0)  //让第一个子进程杀掉当前进程组全部进程,即父进程以及另外的四个子进程    {        printf("kill father process\n");        sleep(1);           kill(0, SIGINT);    }           else if(pid > 0)    //主进程    {        sleep(5);    }    return 0;}

函数原型:int raise(int sig)
返回值:成功返回0,失败返回非0值
参数:sig代表信号名
这个函数和kill的区别就是,该函数只能自己给自己发信号,即调用该函数的进程

函数原型:void abort(void)
这个函数作用是给自己发送SIGABRT异常终止信号,并生成core文件

函数原型:unsigned int alarm(unsigned int seconds)
返回值:在调用此alarm之前,如果进程之前也调用过alarm,那就返回上一个定时器定时的剩余时间,否则返回0。
注意:一个进程只有一个定时器

(三).信号集操作函数

内核通过读取未决信号集来判断信号是否应被处理。而未决信号集会被阻塞信号集(信号屏蔽字)所影响。所以我们可以通过设置信号屏蔽字来达到屏蔽指定信号的目的

设定信号集:
函数原型:

int sigemptyset(sigset_t *set);                     //将某个信号集清0int sigfillset(sigset_t *set);                      //将某个信号集置1int sigaddset(sigset_t *set, int signum);           //将某个信号加入信号集int sigdelset(sigset_t *set, int signum);           //将某个信号清出信号集int sigismember(const sigset_t *set, int signum);   //判断某个信号是否在信号集中

返回值:除了最后一个函数外,都是成功返回0,失败返回-1。最后一个函数如果传入的信号在信号集中则返回1,不在返回0,失败返回-1。
参数:sigset_t set定义为typedef unsigned long sigset_t,它的本质是位图。signum代表要操作的信号名。

屏蔽/解除屏蔽:

函数原型:int sigprocmask(int how, const sigset_t *set, sigset_t *oldset)
返回值:成功返回0,失败返回-1
参数:
how:这个参数对应三个值。SIG_BLOCK,代表要进行屏蔽;SIG_UNBLOCK,代表要解除屏蔽;SIG_SETMASK,直接把旧的信号屏蔽字替换成我们传入的信号屏蔽字
set:传入参数,是一个位图,哪位置成1,就代表当前进程要屏蔽哪个信号
odlset:传出参数,用于保存旧的信号屏蔽字

读取未决信号集:

函数原型:int sigpending(sigset_t *set)
返回值:成功返回0,失败返回-1
参数:set:传出参数,代表当前进程的未决信号集

(四).信号捕捉

比较简单的捕捉函数是signal函数。
函数原型:sighandler_t signal(int signum, sighanler_t handler);
返回值:成功返回以前的信号处理方式,失败返回SIG_ERR
参数:signum:代表要捕捉的信号名,handler:代表捕捉后,进行的动作。sighandler_t定义为typedef void (*sighandler_t)(int)

例子:

#include <stdio.h>#include <unistd.h>#include <signal.h>#include <stdlib.h>typedef void (*sighandler_t)(int);      //我使用的当前linux版本,需要自己定义,不然会报错void catch_sigint(int signo)            //捕捉指定信号后执行的函数{    printf("catch your ctrl+c\n");    }int main(){    sighandler_t handler;               //用于接收返回值    handler = signal(SIGINT, catch_sigint); //将SIGINT信号捕捉,并设置相应的处理动作    if(handler == SIG_ERR)    {        perror("signal error");        exit(1);    }    while(1);       //死循环,等待信号发送    return 0;}

结果就是以前按ctrl+c可以杀掉这个进程,但是却输出了”catch your ctrl+c”。(ps:关不了不要惊慌,按ctrl+\或者使用kill命令杀掉即可)

接下来的另外一个捕捉信号的函数是sigaction函数
函数原型:int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)
返回值:成功返回0,失败返回-1,并设置errno
参数:
signum:代表要捕捉的信号名
act:传入参数,代表新的处理方式
oldact:传出参数,旧的处理方式

struct sigaction{    void (*sa_handler)(int); //指定信号捕捉后进行处理的函数名。可以赋值为SIG_IGN代表忽略,SIG_DFL代表执行默认动作    void (*sa_sigaction)(int, siginfo_t *, void *); //很少使用    sigset_t sa_mask; //调用信号处理函数时,要屏蔽的信号屏蔽字(只在处理函数被调用期间生效)    int sa_flags;   //通常设置为0,表示使用默认属性    void (*sa_restorer)(void);  //已废弃};

例子:

#include <stdio.h>#include <signal.h>#include <unistd.h>void sig_int(int signo)  //处理SIGINT信号的方式{    printf("catch SIGINT\n");    sleep(5);}int main(){    struct sigaction act;    act.sa_handler = sig_int;   //指定处理的函数    sigemptyset(&act.sa_mask);  //清空sa_mask信号屏蔽字    sigaddset(&act.sa_mask, SIGQUIT);   //将SIGQUIT加入该信号屏蔽字,表示屏蔽SIGQUIT    act.sa_flags = 0;   //使用默认属性    sigaction(SIGINT, &act, NULL);  //指定捕捉SIGINT,并且将新的处理方式传入    while(1);    return 0;}

结果是当我们键入ctrl+c时,在信号处理的那个函数是,sleep了5秒,在5秒内,该函数仍在处于被调期间,所以导致我们键入ctrl+\也终止不了该进程,这是因为设置了屏蔽,处理函数调用完之后,会自动处理被屏蔽的信号。这里需要注意一下,阻塞的常规信号不会进行排队,只会记录最近的一次。并且被捕捉的信号在捕捉函数执行期间,会自动被屏蔽,意思就是本例中连续多次按ctrl+c,多余的ctrl+c会被屏蔽掉。

0 0