Linux信号产生与处理机制学习笔记(二)

来源:互联网 发布:日剧 商务 知乎 编辑:程序博客网 时间:2024/05/16 06:36

接着Linux信号产生与处理机制学习笔记(一) 来说。

一、sigaction()信号注册函数于struct sigaction结构体:

1、sigaction()函数:

int sigaction(int signum, const struct sigaction * act, struct sigaction * oldact);/*参数:int signum:信号编号const struct sigaction:信号新的动作struct sigaction * oldact:信号原有动作一般第三个参数用不到置为NULL即可*/

signal()与sigaction()功能比较:
sigaction()可以注册信号的处理方式,功能和signal一样,但sigaction是增强版的signal,且signal底层是调用sigaction的。另外signal()为C标准库函数,sigaction()为Linux系统函数,signal()接口简单且跨平台,但不能设置信号屏蔽字;sigaction()则只能用于UNIX/LINUX系统,但能设置屏蔽字并允许附带信号之外的数据。

2、sigaction()的第二个参数struct sigaction结构体:

这个结构体据决定了信号屏蔽字的设置,以及是使用附带额外数据还是不附带额外数据。

struct sigaction{    void        (*sa_handler)(int);/*与signal()用法相似:设置为ISG_DFL为默认,SIG_IGN为忽略,设为函数名为自定义*/    void        (*sa_sigaction)(int, siginfo_t *, void *);/*可以附带额外数据与sigqueue()联合使用*/    sigset_t    sa_mask;//设置阻塞信号(临时信号集)    int         sa_flags;//指定是使用第一个函数指针,还是第二个函数指针(这两个函数指针互斥,一次只能用一个)    void        (*sa_restorer)(void);//废弃掉};

flags参数:
SA_SIGINFO:当sa_flags中包含SA_SIGINFO时,则使用第二个函数指针,其它情况使用第一个函数指针(两个不可能同时用)。
SA_NODEFER,SA_NOMASK:解除了在信号函数执行期间,对自身注册信号的原始屏蔽(只屏蔽sa_mask的屏蔽设置的信号);若不设置该flags,则会在信号函数执行期间,屏蔽注册信号相同信号与sa_mask设置的信号。
SA_RESETHAND:第一次信号处理为自定义处理,之后的恢复SIG_DFL默认处理

那么我们就对与signal()类似的结构体第一个成员:sa_handler及其使用作以简单的测试:

#include<stdio.h>#include<signal.h>void deaL_sig(int sig){    printf("----in deal_sig----");    printf("sig = %d\n", sig);    sleep(1);//模拟保证处理信号时间够长,以屏蔽其它同类信号}int main(void){    struct sigaction act;    act.sa_handler = deal_sig;/*设置处理方式,我们采用自定义函数处理方式*/    sigemptyset(&act.sa_mask);/*设置信号屏蔽字为均不阻塞*/    act.sa_flags = 0;//默认属性置零:使用第一个函数指针/*当SIGINT信号注册后且收到SIGINT信号后处于处理阶段时,就只屏蔽SIGINT(系统自动设置,即接收第一个SIGINT,屏蔽第一个处理期间的其它同类信号),处理完成后恢复不阻塞的设置*/    sigaction(SIGINT, &act, NULL);//为SIGINT注册    while(1){        printf("----in main----\n");        sleep(1);    }    return 0;}

测试结果:
这里写图片描述

3、struct sigaction的sa_aigaction成员及使用:

上面说到sa_sigaction可以附带额外数据,而sigqueue()函数就是专门用来与sa_sigaction配合使用在发送信号的同时收发附加数据的。并且,这种附带数据的功能只能在有血缘关系的进程间使用。因为union sigval{}中有一个指针类型,对于无血缘关系的不同进程,其使用的虚拟内存地址不同,而传递的指针也无效。

(1)、sigqueue()函数:
类似于函数kill(),可以发送信号,并且在发送信号的同时附加数据。

int sigqueue(pid_t pid,int sig, const union sigval value);union sigval{    int sival_int;    void * sival_ptr;/*所有类型的指针*/};

(2)、sa_sigaction成员与struct siginfo_t:

void (*sa_sigaction)(int, siginfo_t *, void);siginfo_t结构体(结构体成员较多,只列一部分):struct siginfo_t{    int si_signo;    int si_errno;    int si_code;    int si_trapno;    pid_t si_pid;/*发送信号的进程ID*/    uid_t si_uid;/*发送信号的进程所属组ID*/    int si_status;    clock_t si_utime;    clock_t si_stime;    sigval_t si_value;/*与sigqueue()的value相对应*/    ....};

注意:联合体是sigqueue发送时参数,而siginfo_t结构体是信号处理函数的参数,一个是发送参数一个是接收参数。
(3)、简单测试:

#include<stdio.h>#include<stdlib.h>#include<signal.h>void deal_sig(int sig, siginfo_t *message,void * ptr){    printf("I'am %d process, I accept a signal %d and other message is %s from %d process.\n", getpid(), sig, message->si_value, message->si_pid);    exit(EXIT_FAILURE);}int main(void){    pid_t pid = fork();    if(pid < 0){        perror("fork");        exit(EXIT_FAILURE);    }    struct sigaction act;    act.sa_sigaction = deal_sig;    act.sa_flags = SA_SIGINFO;    sigemptyset(&act.sa_mask);    if(pid == 0){        sigaction(SIGINT, &act, NULL);/*在子进程中注册信号SIHINT(2)*/        while(1){            printf("I'am child\n");            sleep(1);/*5秒内不会被信号中断*/        }    }else{        union sigval message;        message.sival_ptr = "hello myson!";        sleep(5);/*5秒后发送信号中断子进程死循环*/        printf("I'am father %d process, I send the message of %s for my son is %d process\n",getpid(), message.sival_ptr, pid);        sigqueue(pid, SIGINT, message);//发送信号以及附加信息    }}

测试结果:
等5秒后,父进程成功发送“hello myson”中断子进程。而在5秒内Ctrl+c中断子进程,在信号处理函数中打印的附加内容为NULL,且是由0号进程发送的信号(进程0:Linux引导中创建的第一个进程,完成加载系统后,演变为进程调度、交换及存储管理进程):

这里写图片描述

说到0号进程与内核,这里我们就得说一下信号的处理过程是怎样的了,首先信号是由内核负责产生发送的,而父进程只是向内核发送请求,子进程收到的也是请求的结果。信号处理过程如图所示:

这里写图片描述

注意:信号并非能产生立刻中断,当程序在执行过程中,在其拥有的时间片内,接收到一个信号,CPU只知道收到一个信号,不去关心是谁发给谁的,所以CPU不会马上处理,而等待当前时间片到达回收CPU资源,并分给下一个抢占CPU资源的进程。所以先将信号放入内核队列等待,当时间片到,CPU发现有一个发给时间片已经结束的进程的信号时,而该进程暂时被回收资源,只能等待下一次时间片到。当下一次时间片轮转到之前保存现场的进程时,该进程的时间片先用来处理之前等待的信号,到用户空间执行自定义处理,再返回内核,当处理完信号返回内核,时间片还未完就继续到用户空间执行该进程的代码。但是由于信号等待处理的时间很短,所以基本可以认为是实时响应的。

二、进程间信号处理:

fork()创建的子进程会继承父进程的信号屏蔽字与对信号的处理动作(action)(代码区共享,不存在子进程找不到父进程自定义的信号处理函数的情况)。而vfork()+exec创建的子进程:父进程忽略,子进程也会会略,但父进程自定义,子进程会改为默认(因为exec改变代码区之后,找不到父进程中自定义的处理函数)。
另外:SIGUSR1和SIGUSR2这两个信号,系统并未定义这两个信号具体用来干什么,由用户来自己处理,默认是终止进程。虽然其他大多信号也可以注册用以其他功能,但是由于系统已经为这些信号制定了特定的功能,所以我们在自己的使用过程中,凡是要对信号自定义处理的一般都用SIGUSR1和SIGUSR2实现,一般用来实现父子进程间信息同步(我们为了测试方便采用SIGINT信号)。

1、status参数与SIGCHLD信号处理:

(1)SIGCHLD信号:

SIGCHLD信号是在子进程状态变化时,由系统自动发送给子进程的父进程的信号,父进程采用wait/waitpid中status参数接收,并根据该信号来进行相应操作。
SIGCHLD处理(默认忽略Ign):
SIGCHLD产生条件有以下三种:
①子进程终止时,SIGCHLD告诉父进程进行子进程收尸(回收资源)。
②子进程接收到SIGSTOP信号停止时,父进程不予理睬。
③子进程处在停止态,接受到SIGCONT后唤醒时,父进程不予理睬。

(2)、status处理方式:
之前在进程中我们有提过status参数,但是说的不是很透彻,接下来我们就这一点详细总结一下(以waitpid为例):

pid_t waitpid(pid_t pid, int * status, int options);

pid参数可参考: 进程创建与相关函数等知识点总结
options参数

WNOHANG:没有子进程结束立刻返回
WUNTRAECD:如果子进程由于被停职产生的SIGCHLD,waitpid则立即返回
WCONTIBUED:如果子进程由于被SIGCONT唤醒而产生SIGCHLD,waitpid则立即返回

status参数:
status参数中存储了,由父进程获取子进程状态改变时系统发送的信号,以及子进程退出结束返回值(如果是因为子进程返回致使系统发送SIGCHLD)等信息。而获取其中的信息可以使用以下实现:

WIFEXITED(status);
子进程正常exit终止返回真,而我们可以使用WEXITSTATUS(status)函数获取退出值:返回子进程正常退出值
WIFSIGNALED(status)
子进程被信号终止返回真,WIERMSIG(status)返回终止子进程的信号值
WIFSTOPED(status)
子进程被停止返回真,WSTOPSIG(status)返回停止子进程的信号值
WIFCONTINUED(status)
子进程停止态转为就绪态返回真
以上宏函数的使用,对应了SIGCHLD信号的三种产生方式。

(3)简单测试:

#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<signal.h>#include<sys/types.h>void sys_err(const char * ptr){    perror(ptr);    exit(EXIT_FAILURE);}void deal_sig(int signo){    int status;    pid_t pid;        //waitpid(0,,)循环等待同组所有进程,可以保证回收所有子进程资源,    while( (pid = waitpid(0, &status, WNOHANG)) > 0 ){        if(WIFEXITED(status))            printf("child %d exit and return %d\n", pid, WEXITSTATUS(status));        else if(WIFSIGNALED(status))            printf("child %d cancle signal %d\n", pid, WTERMSIG(status));    }//  exit(EXIT_FAILURE);}int main(void){    pid_t pid;    int i;    //先阻塞SIGCHLD(不用设置,SIGCHLD默认忽略)    for(i = 0; i< 5; i++){/*创建5个子进程*/        if((pid = fork()) == 0)            break;        else if(pid < 0)            sys_err("fork");    }    if(pid == 0){        printf("I'am child of %d\n", getpid());        sleep(1);        return i;    }    else if(pid > 0){    //先注册信号,设置捕捉再解除对SIGCHLD的阻塞        struct sigaction act;        act.sa_handler = deal_sig;        act.sa_flags = 0;        sigaction(SIGCHLD, &act, NULL);        while(1){            printf("I'am parent and process id is %d\n", getpid());            sleep(2);        }    }    return 0;}

测试一(按上面代码测试):
这里写图片描述

测试二(将上面代码信号处理时的while循环等待改为不加循环等待,并将父子进程的睡眠时间增大以便测试):
这里写图片描述

对于僵尸进程的产生,可以多创建几个子进程并以非循环等待处理,而子进程在sleep后加上一个空循环,ps -aux便会看到僵尸态。

0 0