Linux之信号(signal kill alarm raise abort settimer sigaction SIGCHLD回收子进程)

一.信号的基本概念:

1.信号的机制

        当进程A用信号给进程B发送信号时,进程B一旦收到信号,就会停下正在执行的进程转去处理信号,处理完信号会继续回来执行刚才的进程,可见信号的优先级比较高。

2.信号的状态

        信号有三种状态,分别是产生、未决、递达。信号的产生可以通过按键 ctrl + \ 、ctrl +c ……等方式产生,或者通过系统调用(kill raise abort 后面会说到),未决从字面意思上理解就是未被处决,也就是没有被处理的意思,处于产生和递达的中间状态,递达就是递送并且已经到达进程,已经被处理。

3.信号的处理方式

         执行默认的处理动作(大部分是终止),忽略信号(丢掉不处理),捕捉信号(注册自定义信号处理函数)

4.信号的四要素

        信号的编号,信号的名字,信号的默认处理动作,信号的产生条件(这些都可以在man 7 signal中查看)

5.未决信号集和阻塞信号集

        阻塞信号集中存放的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号,这些信号需要被阻塞,不予处理。信号产生后由于某种原因没有被处理,那么这类信号的集合就被称为未决信号集。在屏蔽解除前,信号一直处于未决状态,如果信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除。

二.信号相关的函数

 1.signal函数 注册自定义信号处理

typedef void (*sighandler_t)(int);
 sighandler_t signal(int signum, sighandler_t handler);

          signal函数的原型如上,其第一个参数是要注册的信号的值,一般都传入相应的宏,第二个参数是一个回调函数,也就是自己写的处理信号的函数。下面看一个关于signal函数的简单例子。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);
}
int main()
{
    signal(SIGPIPE,handler);
    int fd[2];
    int pip=pipe(fd);
    if(pip<0)
    {   
        perror("pipe error\n");
        return -1; 
    }   
    close(fd[0]);
    while(1)
    {   
        write(fd[1],"hello",sizeof("hello"));
    }   
    return 0;
}

        这个signal注册代码在上次进程之间的通信就已经简单介绍过了,这里是用signal捕捉管道破裂信号,我们关闭了管道的读端,让其一直写入,就会让管道破裂,然后signal函数就会捕捉到这个信号,这个信号的整型值是13,使用的时候还是用SIGPIPE.

        屏幕上会一直打印这个,因为设置了循环写,管道破裂后依然继续执行。这里需要注意回调函数的写法,回调函数是有一个参数的。

 2.kill函数

        kill函数的作用是给指定进程发送信号。

         第一个参数就是传入要杀死的进程的pid,具体的可以去查看手册,我们最常用的是传入进程的pid,第二个参数就是信号的编号。

         利用kill函数可以给进程自己发送一个终止的信号SIGKILL.

          如果后面这句话没有执行就说明进程已经收到信号结束运行了 。

         使用kill函数的时候需要注意传入不同的参数带来的影响不一样,有些参数需要慎重使用,一不小心可能会将整个控制台程序都给关闭了。

3.abort和raise函数

        abort函数给当前进程发送SIGABRT信号,并终止当前进程。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<string.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);
}
int main()
{
    signal(SIGABRT,handler);
    abort();
    printf("----\n");
    return 0;
}

        我们注册一个信号捕捉函数来捕捉SIGABRT信号,并且abort函数执行后,后面的printf就不会执行了。这个abort函数就相当于kill(getpid(),SIGABRT);

         raise函数是给当前进程发送指定信号,相当于kill(getpid(),signo);成功返回0,失败返回非0值。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);
}
int main()
{
    signal(SIGPIPE,handler);
    int ret=raise(SIGPIPE);
    printf("----\n");   
    return 0;
}

        和上面的一样注册一个信号捕捉函数来捕捉当前信号,我们就随便给当前进程发送一个SIGPIPE信号。

4.alarm函数

        alarm函数是用来设置定时器的,就可以理解成设置闹钟,在指定seconds之后,内核会给当前进程发送一个SIGALRM信号,进程收到信号后,默认终止。每个进程都有且仅有唯一的一个定时器。函数返回0或者是还剩多少秒。这个函数第一次调用返回0,第二次返回剩余的秒数。

int alarm (int seconds);

下面看一段测试代码:

#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<unistd.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);
}   
int main()
{
    signal(SIGALRM,handler);
    int ret=alarm(9);
    printf("%d\n",ret);
    sleep(2);
    ret=alarm(1);
    printf("%d\n",ret);
    //alarm(0);//取消设置时钟后面的这句话不会输出
    //printf("%d\n",ret);
    sleep(10);
    return 0;
}

        说明一下最后一个sleep的作用,如果不sleep的话程序执行完了直接推出了然后定时器还没触发。这里我们设置了一个9s的定时器,第一次的返回值应该是0,所以第一次打印的值应该是0,第二次返回的就是定时器还剩下多久了,上面休眠了两秒,因此这里输出的是7.

         答案和我们想象的一样。注意alarm(0)是取消时钟的意思,取消时钟的话就不会发出SIGALRM信号,因此也不会被阻塞。有一个有趣的测试,可以用来测试你的电脑能在一秒中数数多少次。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>
#include<sys/types.h>

int main()
{
    int i=0;
    alarm(1);
    while(1)
    {   
        printf("%d\n",i++);
    }   
    return 0;
}

        好像电脑运算速度越快,一秒内会数更多数。

  5.setitimer函数

         函数原型如下:

int setitimer(int which, const struct itimerval *new_value,
                     struct itimerval *old_value);

        第一个参数which表示指定定时方式,有三个方式:

ITIMER_REAL 计算自然时间,并在最后返回SIGALRM信号。

ITIMER_VIRTUAL 虚拟空间计时,只计算进程占cpu的时间,返回SIGVTALRM信号。

ITIMER_PROF 运行时计时,计算占用cpu和执行系统调用的时间,在最后返回SIGPROF信号。

第二个参数new_value是结构体:

struct itimerval {
               struct timeval it_interval; /* next value */
               struct timeval it_value;    /* current value */
           };

           struct timeval {
               time_t      tv_sec;         /* seconds */
               suseconds_t tv_usec;        /* microseconds */
           };

        it_interval 表示闹钟出发周期 it_value 表示出发时间  it_sec 秒 it_usec 微秒

最后一个参数传入NULL即可。下面举个例子说明setitimer函数如何使用:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/time.h>
#include<unistd.h>
#include<signal.h>

void handler(int signo)
{
    printf("HELLO WORLD %d\n",signo);
}
int main()
{
    signal(SIGALRM,handler);    
    struct itimerval tm; 
    tm.it_interval.tv_sec=1;//设置周期
    tm.it_interval.tv_usec=0;

    tm.it_value.tv_sec=2;
    tm.it_value.tv_usec=0;

    setitimer(ITIMER_REAL,&tm,NULL);
    while(1)
    {   
        sleep(1);//防止上面打印太快
    }   
    return 0;
}

        上面我们设置了一个周期为一的定时器,从第二秒开始,每秒打印一次HELLO WORLD,结构体记不住可以查手册。

 6.未决信号集和阻塞信号集的关系和相关函数

        上面已经说过了阻塞信号集就当前进程要阻塞的信号的集合,未决信号集就是当前进程暂时没有处理的信号的集合。这连个集合都存在进程的PCB中。就用刚才SIGINT信号来解释一下这俩信号集:当进程收到了SIGINT信号时,首先会将这个进程存在未决信号集中,表示该信号处于未决状态,同时会将当时未决信号集中这个位置置为1,在这个信号需要被处理的时候,首先会查询阻塞信号集中对应的位置是不是1,如果是1,那么这个信号就会被阻塞,这个信号就不会被处理,所以其未决信号集对应的位置也会保持为1,如果不是1,那么当前信号就会被处理,执行默认处理动作,并将未决信号集对应位置置为0。当SIGINT信号从阻塞信号集中解除阻塞后,就会被处理。

         下面看看信号集相关函数:首先先了解一下阻塞信号集的创建--sigset_t set 这就创建了一个阻塞信号集,

int sigemptyset(sigset_t *set)把集合中的所有位置为0;初始化
int sigfillset(sigset_t *set)把集合中的所有位置全部都置为1
int sigaddset(sigset_t *set,int sognum) 将某个信号添加到信号集中
int sigdelset(sigset_t *set,int signum) 将某个信号从集合中删除
int sigmismember(sigset_t *set  ,int signum) 查看某个信号是否在集合中
int sigprocmask(int how,const sigset_t *set,*oldset)设置阻塞信号集
int sigpending(sigset_t *set) 读取当前进程的未决信号集

        最后一个设置阻塞信号集函数中的how,可以传入SIG_BLOCK 把某个信号设置为阻塞,SIG_UNBLOCK 将某个信号解除阻塞 ,SIG_SETMASK  直接进行设置,包括上面两个的功能,具体使用方法看后面的例子。下面看一个实例我们将SIGINT信号加入阻塞信号信号集,然后通过键盘的ctrl+c来产生SIGINT信号,判断这个信号是否在未决信号集中,如果在,就输出1,否则输出0,程序如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);   
}
int main()
{
    signal(SIGINT,handler);
    sigset_t set;
    sigemptyset(&set);
    
    sigaddset(&set,SIGINT);//将信号加入set中
    sigprocmask(SIG_BLOCK,&set,NULL);//将信号加入阻塞信号集
    
    sigset_t pending;
    sigset_t oldset;
    int i=0;
    int j=0;
    while(1)
    {   
        sigemptyset(&pending);
        sigemptyset(&oldset);
        sigpending(&pending);//获取未决信号集中的信号
        for(i=1;i<32;i++)
        {   
            if(sigismember(&pending,i)==1)//判断信号是否在那个范围之中
            {   
                printf("1");
            }   
            else
            {   
                printf("0");
            }   
        }   
        printf("\n");
        //循环十次就解除阻塞,执行信号处理函数
        if(j++%10==0)
        {   
            //sigprocmask(SIG_UNBLOCK,&set,NULL);       
            sigprocmask(SIG_SETMASK,&oldset,NULL);//解除阻塞
        }   
        else
        {   
            sigprocmask(SIG_BLOCK,&set,NULL);
        }   
        sleep(1);
    
    }    
    return 0;
}

        前面将其加入到阻塞信号集中后,后面每循环十次就将其解解除阻塞一次,解除阻塞后,就会处理我们注册的信号处理函数。

       观察运行结果我们可以发现,尽管我们在前面输入了很多次信号,但是最终这个信号只会被处理一次,这也就证明了信号不支持排队。

7.sigaction函数

        sigaction和signal的作用是差不多的,都是注册信号处理函数,就可以把sigaction当成signal的升级版,sigaction的函数原型如下:

int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);

        第一个参数是你要注册的信号,一般都是传入相应的宏,第二个参数是一个传入参数,一个结构体:

struct sigaction {
               void     (*sa_handler)(int);//信号处理函数
               void     (*sa_sigaction)(int, siginfo_t *, void *);//基本不用
               sigset_t   sa_mask;//信号处理函数执行期间要进行阻塞的函数,就看成一个阻塞信号集
               int        sa_flags;//通常为0,表示默认标识
               void     (*sa_restorer)(void);//这个参数几乎用不到
           };

        下面看一个例子来看一下sigaction的具体用法 :

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<string.h>
#include<signal.h>

void handler(int signo)
{
    printf("signo=[%d]\n",signo);
    sleep(3);
}
int main()
{
    struct sigaction act;
    act.sa_handler=handler;
    sigemptyset(&act.sa_mask);//不阻塞信号就把他设置为空
    sigaddset(&act.sa_mask,SIGQUIT);
    act.sa_flags=0;
    sigaction(SIGINT,&act,NULL);
    signal(SIGQUIT,handler);
    while(1)
    {
        sleep(1);
    }

    return 0;
}

       对于sigaction结构体的第sa_mask成员,如果不需要阻塞信号,就直接调用sigemptyset函数将其初始化为全0即可,如果要阻塞信号,那么就需要调用sigaddset函数来将其加入到阻塞信号集。在本例中我们将SIGQUIT信号在act属性中设置为阻塞,那么只有当SIGINT信号处理函数执行期间,如果收到了SIGQUIT 信号,那么其将会被阻塞,当SIGINT信号处理函数执行完了,就会为SIGQUIT解除阻塞,并执行其相应的信号处理函数。

        值得注意的是,在sleep(3)期间,两个信号同时产生了很多次,但是最终只会被执行一次。这也应证了上面的信号不支持排队这个观点。同时这里需要注意,这里的程序终止需要通过重新拉一个窗口利用kill命令才能将其杀死, 

8..SIGCHLD信号回收子进程

        产生:每个进程运行结束都会给其父进程发送SIGCHLD信号。

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<signal.h>
#include<sys/wait.h>

void sighandler(int signo)
{
    printf("signo=%d\n",signo);
}
int main()
{
    pid_t pid=fork();
    signal(SIGCHLD,sighandler);
    if(pid<0)
    {   
        perror("fork error\n");
        return -1; 
    }   
    else if(pid==0)
    {   
        printf("child process pid=%d,ppid=%d\n",getpid(),getppid());
        while(1)
        {sleep(1);}
    }   
    else if(pid>0)
    {   
        printf("father process pid=%d,ppid=%d\n",getpid(),getppid());
        while(1)
        {sleep(1);}
    }   

    return 0;
}

        借用前面父子进程的代码,在里面为SIGCHLD信号注册一个处理函数,然后我们手动杀死子进程,观察SIGCHLD信号的捕捉。

9.SIGCHILD回收多个子进程

     这里我们使用SIGCHLD信号回收子进程,首先来看一下SIGCHLD信号的产生条件,当一个进程运行结束时,或者是收到SIGSTOP SIGCONT信号,就会给其父进程发送一个SIGCHLD信号,父进程在收到这个信号后,从而对开始对子进程进行回收。下面看一个使SIGCHILD信号回收子进程的实例,首先是循环创建三个子进程和之前的方式一样,注意在子进程中要有break,防止子进程反复创建,后面要在父进程中注册信号处理函数,使用sigaction函数,在信号处理函数中使用waitpid函数对子进程进行回收,当然在这个过程中会有很多情况会产生僵尸进程,首先,如果你的信号处理函数还没有完成注册,三个子进程都已经退出了,此时父进程没有完成对子进程的回收,会产生三个僵尸进程:

将程序跑起来,通过观察进程运行情况会发现三个子进程:

         对于这种情况,我们可以在信号注册函数完成前先将SIGCHILD信号设置为阻塞,当完成信号处理函数的注册后,再将其解除阻塞;当然在信号处理函数中也存在问题,因为信号是不支持排队的,连续发送多次信号这个信号只会被处理一次,因此我们就需要在收到一个信号就把所有的子进程就全部回收了,就需要在信号处理函数中设置循环回收子进程;以上的这两点是容易忽视的小细节,值得注意!下面是全部的代码:

#include<stdio.h>
#include<string.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<unistd.h>
#include<sys/wait.h>

void handler(int signo)
{
    while(1)
    {   
        pid_t pid=waitpid(-1,NULL,WNOHANG);
        if(pid>0)
        {   
            printf("回收了%d\n",pid);
        }   
        else if(pid==0)
        {   
            continue;
        }   
        else if(pid==-1)
        {   
            printf("recycle over\n");
            break;
        }    
    }   
}
int main()
{
    sigset_t set;
    sigemptyset(&set);
    sigaddset(&set,SIGCHLD);
    sigprocmask(SIG_BLOCK,&set,NULL);
    int i=0;
    for(i=0;i<3;i++)
    {   
        pid_t pid=fork();
        if(pid<0)
        {   
            perror("fork errror\n");
            return -1; 
        }   
        else if(pid==0)//子进程
        {   
            printf("child process pid=[%d],ppid=[%d]\n",getpid(),getppid());
            break;//当检查到是子进程时候就会跳出,子进程就不会循环创建孙子进程
        }   
        else if(pid>0)//父进程
        {
            printf("father process pid=[%d],ppid=[%d]\n",getpid(),getppid());
        }

    }
    if(i==0)//第一个子进程
    {
        printf("child 1 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
    }
    if(i==1)//第二个子进程
    {
        printf("child 2 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
    }
    if(i==2)//第三个子进程
    {
        printf("child 3 :pid = [%d],ppid = [%d]\n",getpid(),getppid());
    }
    if(i==3)//在父进程中回收子进程
    {
        printf("father :pid = [%d],ppid = [%d]\n",getpid(),getppid());
        struct sigaction act;
        act.sa_handler=handler;
        sigemptyset(&act.sa_mask);
        act.sa_flags=0;
        sleep(5);
        sigaction(SIGCHLD,&act,NULL);
        sigprocmask(SIG_UNBLOCK,&set,NULL);
        while(1)
        {
            sleep(1);
        }
    }
    return 0;
}

        理清整个处理逻辑后再看这段代码会简单许多,同时还有主要waitpid函数的使用,其相关参数的设置。

         观察发现,子进程已经全部回收,没有产生僵尸进程。最后注意一下SIGKILL和SIGSTOP不能被捕获!

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值