进程信号

  • Post author:
  • Post category:其他



信号的概念

每个信号都有一个编号和一个宏定义的名称,这些宏定义可以在 signal.h 中找到。

kill -l 命令可以查看系统中的信号

普通信号

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

Ctrl+C -> SIGINT 信号默认是终止进程,Ctrl+\ -> SIGQUIT 的默认处理动作是终止进程并且产生 Core Dump 文件。

实时信号

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

用户可以在 man 7 signal 中找到相应的信号描述。


信号的产生

1.说明:

1. 信号一般是由于硬件中断而产生的,由内核将中断解释为相对于的信号,然后发送给进程。信号也可以由软件给进程发送。

2. 用户在终端按下某些键的时候,驱动程序会发送信号给前台进程,Ctrl-C 产生SIGINT,Ctrl-\ 产生SIGQUIT、 Ctrl-Z产生SIGTSTP(使前台进程停止)。

3. 当内核检测到某种软件条件发生时也可以通过信号通知进程,例如闹钟超时产生SIGALRM信号、向读端已经关闭的的管道写数据是产生SIGPIPE信号。如果不想按默认动作处理信号,用户程序可以调用sigaction()函数告诉内核如何处理某种信号。

4. 信号的发送就是把进程PCB结构体中相应的比特位由0置为1的过程。

5. 进程可以调用 kill( pid_t  pid, int  sig ) 给一个指定进程发送指定的信号。raise ( int sig ) 函数可以给当前进程发送指定的信号。


2.通过系统调用函数产生信号。

#include<signal.h>

int kill(pit_t pid,int signo);

int raise(int signo);

这两个函数成功返回0,失败返回-1。

abort函数使当前进程接收到信号而异常终止。

#include<stdlib.h>

void abort(void);

就像exit函数一样,abort函数总是会成功的,所以没有返回值。

调用alarm函数设置闹钟

#include<unistd.h>

unsigned int alarm(unsigned int seconds);

这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。

如果 seconds 值设置为 0 ,表示取消以前设定的闹钟,alarm(0)的返回值 仍然是之前设定闹钟还剩下的秒数。

alarm函数的测试:

测试cpu在1秒之内的自加次数:

1.设置 alarm 为1秒

2.让 g_count 自加

3.等待闹钟响后用 signal 函数捕捉 SIGALRM,

4.在 handler 函数中打印 g_count 的值,并出程序。

alarm.c

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


  size_t g_count;
  
 void handler(int sig)
{
  (void)sig;
  printf("%lu\n",g_count);
  _exit(0);
}


int main()
{
    signal(SIGALRM,handler);
    alarm(1);
    while(1)
    {
        ++g_count;
    }
  return 0;  


}

测试结果:


我的cpu在一秒钟自加了4亿8千万次,但是cpu如果在自加的过程中不断的访问I O设备,速度就会成数量级的递减。I O拉低了cpu的效率,我们在写程序时应注意让cpu尽可能少的访问I O设备,以提高计算效率。

如果每次自加都打印的话,最后的数值大概是1万多次。

mykill的实现:

通过调用kill函数实现,在运行时向main函数传递两个参数  signal(要传递的信号) pid(要处理的进程的id)

kill.c

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

int main(int argc,const char* argv[])
{
   if(argc!=3)
  {
    printf("Useage:./file [signo] [pid]\n");
    return 1;
  }
   pid_t pid=atoi(argv[1]);
   int sig=atoi(argv[2]);
   kill(pid,sig);    

    return 0;

}

1.将之前的alarm.c中的闹钟时间设定为100秒,并重新编译运行。

2.开启第二个终端,准备运行我的kill程序。

执行 ps aux |  grep  alarm

找到 alarm 进程的 pid  为 10694

执行 ./kill 10694 3

3. 此时运行alarm的终端下产生 core dump 并且程序退出。

结果如下图:




信号的处理



1.说明:

信号的block,pending表

执行信号处理的动作称为递达(Delivery)

信号从产生到递达之间的状态,称为信号未决。

进程可以选择阻塞(Block)某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

阻塞和忽略不同,只要信号被阻塞就不会被递达。而忽略是在递达之后可选择的一种处理动作。

sigset _t 被称为一个信号集。

阻塞信号集也叫做当前的信号屏蔽字。

2.信号操作函数

#include<signal.h>

int sigemptyset(sigset_t *set);  将信号集全部置为零。

int sigfillset(sigset_t * set);       将将信号集全部置为一。

int sigaddset(sigset_t *set,int signo);  将signo所对应的比特位设置为一。

int sigdelset(signset_t *set ,int signo); 将signo所对应的比特位设置为零。

int sigismember(const sigset_t *set ,int signo);  该函数是一个布尔函数,判断信号集中是否包含某种信号,如果包含就返回1,不包含就返回0,出错返回-1。   包含的意义就是该signo所对应的sigset集中的比特位是否为1.

#include<signal.h>

int sigprocmask(int how,  const sigset_t *set,  sigset_t  *oset);

how的可选值为

SET_BLOCK表示set中包含了我们希望添加到当前信号屏蔽字的信号,相当于mask= mask | set。

SET_UNBLOCK表示set中包含我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mast=mask& ~set。

SIG_SETMASK表示设置当前信号屏蔽字为set所指向的值,相当于mask=set。

ps:如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

sigpending(sigset_t *set); set为输出型参数,系统未决信号集通过set传出。调用成功返回0,调用出错返回-1.

实现mask.c :

1.通过调用sigprocmask函数设置 SIGINT 的信号屏蔽字。

2.通过调用sigpending函数获得未决信号集,通过print函数打印进程的未决信号。(print函数通过sigismember的返回值打印出未决信号集)。

3.向进程发送SIGINT信号,观察未决信号集的变化。

mask.c

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


void print(sigset_t *set)
{
   int i=1;
   for(;i<32;i++)
   {

     if(sigismember(set,i))
     {
         printf("1 ");
     }
     else
     {
         printf("0 ");
     }

   }
   printf("\n");
}
int main()
{
    sigset_t set,oset;
    sigset_t pending;
    sigemptyset(&set);
    sigaddset(&set,SIGINT);
    sigprocmask(SIG_BLOCK,&set,&oset);
    while(1)
    {
        sigpending(&pending);
        print(&pending);
        sleep(1);
    }

    sigprocmask(SIG_SETMASK,&oset,NULL);
    return 0;
}

结果如下:


起初未决信号集为全零,然后向进程发送SIGINT信号,内核中pending的2号比特位被置为1。

然后Ctrl +\ 退出进程。


信号的捕捉

#include <signal.h>

int  sigaction( int signo , const struct sigaction*act,struct sigaction  *oact);

根据act修改对该信号的处理动作,若oact用了保存信号原理的处理动作。

struct sigaction {

void     (*sa_handler)(int);                                      //信号处理函数

void     (*sa_sigaction)(int, siginfo_t *, void *);     //对实时信号进行处理

sigset_t   sa_mask;                                          //在对该信号进行处理时,对其他信号的屏蔽操作。

int        sa_flags;                                           /flag =0 就行 (目前用不到)

void     (*sa_restorer)(void);                      //对实时信号进行处理

};

sigaction函数如果多次被调用,则后面的调用会将之前的覆盖。


可重入函数与volatile的使用

volatile可以让编译器在优化的时候对相应的变量每次都从寄存器中读取。

防止在高度优化等级的时候出现逻辑不符的错误。

volatile.c的实现:

1.设置全局变量 g_value=0

2.调用signal函数捕捉SIGALRM信号,并在信号处理函数中改变g_value的值。

3.将alarm函数设置为3秒,之后程序进入死循环。

4.当闹钟时间到了之后,g_value的值被修改,程序退出循环。

gcc -O3  -o volatile volatile.c

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

volatile int g_value=0;

void handler(int sig)
{
    (void)sig;
    ++g_value;
  return ;
}
int main()
{

     signal(SIGALRM,handler);

     alarm(3);
     while(g_value==0)
     {
         ;
     }
     
     return 0;
     
        
}

可是如果在程序进行 -O3 的高优化等级时,如果 g_value变量之前不加volatile 程序就会一直在死循环中。因为编译器对

while(g_value == 0) 这一步进行了优化,g_value当成固定的零值,并且不再关心g_value是否改变。

加上volatile之后,再次编译, while(g_value == 0 )这一步就不会被优化,每次判断前,寄存器都要到内存中读取g_value的值然后再与0比较。

运行结果:


volatile在运行3秒后自行退出。


sigsuspend函数的利用与mysleep的实现

sigsuspend 将pause与sigaction的还原操作合并为了一个原子操作。防止程序被切走之后,再回来出现错误。

mysleep.c

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

void hander(int sig)
{
   (void)sig;
   return;
}

unsigned int mysleep(unsigned int seconds)
{
     struct sigaction set,oset;
    
     sigset_t newmask,oldmask,suspmask;

     
     sigemptyset(&set.sa_mask); 
     set.sa_handler=hander;
     set.sa_flags=0;

     sigaction(SIGALRM,&set,&oset);
    
     sigaddset(&newmask, SIGALRM);

     sigprocmask(SIG_BLOCK, &newmask,&oldmask);

     suspmask=oldmask;
     sigdelset(&suspmask,SIGALRM);
    
     alarm(seconds);
    
     sigsuspend(&suspmask); 
    
     unsigned int ret=alarm(0); 

     sigaction(SIGALRM,&oset,NULL); 
    
     sigprocmask(SIG_SETMASK,&oldmask,NULL);
    
     return ret; 
}


int main()
{
      int ret =mysleep(2);
      printf("The alarm return is %d \n",ret);
     return 0;
}

程序睡眠2秒后,打印闹钟的返回值。

关于mysleep函数:

说明:

1.设置alarm闹钟后,在相应的秒数后系统会对该进程发送alarm信号,该进程就会被终止。

所以需要用sigaction函数来捕捉 SIGALRM 信号,然后将其忽略。这样之后的程序就不会受到影响。

2.使用 sigemptyset 函数将 sigaction 结构体类型 set 中的 sa_mask 清零。

sa_mask表示在处理接受到的当前信号时还应该对哪些其他的信号进行屏蔽。

设置 set中的 sa_handler = handler 函数,并将flag设置为1.

之后将set注册打sigaction函数中,并接受其原有的处理方式到 oset中

sigaction(SIGALRM,&set,&oset);

3.设置信号屏蔽字

调用 sigaddset()将 SIGALRM 加入到newmask中,然后调用 sigprocmask 将 newmask设置到系统中,并接收系统原理的信号屏蔽字到 oldmask 中。

将oldmask 设置到赋给 suspmask 。并且调用sigdelset()去掉suspmask中的SIGALRM信号。

4.设置闹钟

5.调用sigsuspend函数传入suspmask。(此时取消了对 SIGALRM信号的屏蔽,然后该进程被挂起,等待alarm信号的到来)

6.alarm到来时,sigaction函数调用handler函数进行处理(忽略操作),handler函数返回,sigsuspend函数失败返回。

程序继续向下执行。

7. alarm(0)取消该闹钟。(如果第6步中接受到的SIGCHLD信号不来自于我们设置的alarm ,来自于其他程序或操作的话,我们设置的闹钟还没有响,但由于程序已经继续执行,这个闹钟信号可能会造成程序的错误,所有我们要把这个闹钟取消,并接受它的返回值,返回值为闹钟还剩下的时间)

8.这时我们已经完成了SIGALRM的捕捉与处理,我们需要将sigaction的处理动作恢复,将sigprocmask恢复为原来的样子。

sigaction(SIGALRM,&oset,NULL) 此时我们不关心它现在的处理状况,所以第三个参数为NULL。

sigprocmask(SIG_SETMASK, &oldmask,NULL) 同样的我们也不关心sigprocmask现在的处理状况。

9.mysleep函数返回,main函数打印它的返回值然后退出,perfect!


SIGCHILD信号

操作系统在设计的时候,子进程在退出的时候会给父进程发送SIGCHILD信号。

父进程可以通过signal函数捕捉这个信号,并在handler函数中对子进程进行处理。

这样父进程就不用自己阻塞的等待子进程,可以做自己的事情。

关于sigchild.c:

父进程通过循环 fork 出10个子进程,子进程打印自己的信息,sleep 3秒 然后退出,(这样子进程就不会参与fork操作)。

sigchild.c

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

void handler(int sig)
{
    (void)sig;

    pid_t id;

    while((id=waitpid(-1,NULL,WNOHANG))>0)
    {
       printf("wait child succes:%d \n", id);
    } 

    printf("child is quite!  %d\n",getpid());

    return;

}

int main()
{
    signal(SIGCHLD,handler);

    int i=0;
    
    for(;i<100;i++)
    {
       pid_t pid=fork();
       
       if(pid==0)
       {
          printf("I am %d child.\n",i);
          sleep(3);
          _exit(2);
       }

    }

    while(1)
    {
        sleep(1);
    }
}


三点解释:

1.用 waitpid 来等待子进程,pid 为-1 表示等待所有子进程。第二个参数一般设置为 NULL,options 设置为 WNOHANG 表示如果没有退出的子进程,waitpid 就立刻返回,不再进行等待。

2.在每次接收到 SIGCHLD 信号后,该进程的 pending 表和 block 表中 SIGCHLD 对应的比特位依次设置为1 ,直到 SIGCHLD 信号递达后,pending 表中的信号置为。 此时如果有新的 SIGCHLD 信号递达,pending 表会再次被设置为1,但此时信号被阻塞。处理函数返回后,block 表中对应的比特位设置为 0。

3.从执行结果可以看出 handler 函数被调用了3 次,接受处理了10个子进程的。(说明在 handler 函数运行的时候,有多个 SIGCHLD 到达,但都被忽略了,其对应的子进程均被  waitpid 回收)



版权声明:本文为Alsnoa原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。