进程信号

目录

信号的产生方式

程序的崩溃

通过键盘产生

进程异常产生

系统调用产生

软件条件产生

信号产生中

函数介绍

sigset_t(信号集)

sigprocmask函数

sigpending函数

信号处理 

用户态和内核态的理解

处理信号的过程

信号捕捉

sigaction函数

volatile关键字

SIGCHLD信号


信号和信号量就相当于老婆和老婆饼的关系,其实没有实质联系。

1、信号具有识别信号并处理信号的能力,远远早于信号的产生

2、进程收到某种信号的时候,并不算是立即处理,而是在合适的时候

3、进程收到信号之后,有时需要先保存起来,以供合适的时候处理

4、信号的本质是数据,信号的发送就是向进程的task_struct内写入信号数据

5、无论任何信号的发送,本质都是在底层通过OS发送的

进程收到信号的处理方案有以下几种

1、默认动作---------->一部分是终止自己,暂停等

2、忽略动作---------->是一种信号处理的方式,只不过动作就是什么都不干

3、(信号捕捉)自定义动作-->使用signal方法修改信号的处理动作。默认动作->自定义动作

进程信号_第1张图片

该函数用于修改指定信号的默认行为。

信号的产生方式

程序的崩溃

在阐述信号的产生方式之前,我们需要对程序的崩溃重塑以下系统层面的认识。

进程信号_第2张图片

ps:浮点异常的情况中的计算会加载到cpu中运算,其中除数为0,引发cpu错误。野指针访问的情况,我们需要知道页表映射虚拟地址和物理地址。但是这里的p是一个null指针,没有虚拟地址,何谈映射物理地址,引发错误。 

程序上的错误,通常会体现在硬件或者其他软件上边。

OS作为硬件的管理者,当发现硬件错误的时候,就会寻找引发错误的进程,对其发送信号,终止进程。便引起了进程的崩溃。

通过键盘产生

写入一个死循环

进程信号_第3张图片

键盘输入ctrl+c,终止进程

进程信号_第4张图片

验证该操作是由异常引起的。编写以下代码

void handler(int signo){
  switch(signo){
     case 2:
      printf("hello Linux,get a signal:%d\n", signo);
      break;
     case 3:
       printf("hello world,get a signal:%d\n", signo);
       break;
     case 9:
       printf("hello ...,get a signal:%d\n", signo);
       break;
     default:                                                                                                            
       printf("hello signal:%d\n", signo);
       break;
   }
   exit(1);
}
int main(){
   //捕捉所有信号
   int cnt = 1;
   for( ; cnt < 31; cnt++){                                                                                              
     signal(cnt, handler);//将cnt号信号的默认动作修改成自定义动作
   }
 
   while(1){
     printf("hello linux!\n");
     sleep(1);
   }
   return 0;
}

执行结果:

进程信号_第5张图片

进程异常产生

执行下边的代码,具有浮点溢出错误

void handler(int signo){
  switch(signo){
     case 2:
      printf("hello Linux,get a signal:%d\n", signo);
      break;
     case 3:
       printf("hello world,get a signal:%d\n", signo);
       break;
     case 9:
       printf("hello ...,get a signal:%d\n", signo);
       break;
     default:                                                                                                            
       printf("hello signal:%d\n", signo);
       break;
   }
   exit(1);
}
int main(){
   //捕捉所有信号
   int cnt = 1;
   for( ; cnt < 31; cnt++){                                                                                              
     signal(cnt, handler);//将cnt号信号的默认动作修改成自定义动作
   }
 
   int a = 10;
   a /= 0;//浮点异常

   while(1){
     printf("hello linux!\n");
     sleep(1);
   }
   return 0;
}

执行结果

进程信号_第6张图片

当我们想找到具体崩溃在哪一行的时候。需要用到进程等待的知识。

进程信号_第7张图片

在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)。

当进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因。 

如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转存到磁盘中,方便后期调试。

默认情况下,云服务器上的core dump服务是被关掉的。

使用ulimit -a可以查看系统资源。

我们需要使用ulimit -c + 大小命令将服务其打开。

 然后再运行程序。如下:

进程信号_第8张图片

再使用gdb调试

 进程信号_第9张图片

 ps:并不是所有信号都会引发core dump标志位的重置。

系统调用产生

进程信号_第10张图片

 这里的kill就是一个向进程发送信号的接口。

软件条件产生

进程信号_第11张图片

alarm接口设置seconds秒后向进程发送SIGALRM信号。

执行以下代码

#include
#include 
#include 
#include 
#include 
#include 
 
int count = 0;
void HandlerAlarm(int signo){
   printf("hello: %d\n", count);
   exit(1);
}                                                                                                                     
int main(){//统计一下1s的时间,服务器能对count递增多少
  signal(SIGALRM, HandlerAlarm);
  alarm(1);//没有设置alarm信号的捕捉动作(即没有自定义),执行默认动作
  while(1){
    count++;
    //printf("hello: %d\n",count++);//这里比较慢,因为有IO
  }
  return 0;
}

执行结果

 总结:信号产生的方式有4种。1、键盘产生2、进程异常产生3、系统调用产生4、软件条件产生。这四种方式最终都是通过OS向目标进程发送的信号

信号产生中

进程的task_struct中存放着进程的各种属性,其中采用位图结构来标识进程是否收到信号。OS向进程发送信号也就能理解成本质是向进程的task_struct中的信号位图中对应比特位写为1,即完成信号的发送。

实际执行信号的处理动作称为信号抵达:可分为自定义捕捉,忽略和默认三种情况。

信号从产生到抵达之间的状态称为信号未决:本质是这个信号被暂存再task_struct信号为途中。

进程可以选择阻塞某个信号:本质是OS允许进程暂时屏蔽指定的信号。

进程信号_第12张图片

进程抵达中的忽略与阻塞是完全不同的概念。忽略只是进程抵达的一种方式,而阻塞是没有抵达,是一种独立的状态。忽略和阻塞是不同阶段的概念。

task_struct中存在指向了存放这三张表结构的变量,如下图

进程信号_第13张图片

其中block位图用于表示进程是否阻塞。pending位图用于是否有信号写入。handler函数指针数组用于进程执行何种动作(其中存放的是动作函数指针,每个信号的编号就是其数组下标)。

这三张表必须横向同时看,也就是说识别一个信号是一个三元组。比如:

第一行表示该进程未被阻塞,但是并未写入0号信号,执行SIG_DFL(默认)动作。

第二行表示该进程被阻塞,且写入了1号信号,执行SIG_IGN(忽略)动作。

第三行表示该进程被阻塞,但是未写入2号信号,执行自定义动作。

每一行的执行逻辑,可参考下边伪代码

int IsHandler(int signo){
    if(block & signo){//该信号被block
        · · · · ·
        return 0;
    }
    else{//未被block
        if(signo & pending){//未被block,且已被写入
            handler_arr[signo](signo);
            return 0;
        }
    }
}

函数介绍

sigset_t(信号集)

进程信号_第14张图片

 从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

sigset_t类型对于每种信号用一个比特位表示“有效”或“无效”状态,至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释

#include

        int sigemptyset(sigset_t *set);//初始化信号集,置0

        int sigfillset(sigset_t *set);//初始化信号集,置1

        int sigaddset (sigset_t *set, int signo);//将信号集的某一信号置为1

        int sigdelset(sigset_t *set, int signo);//将信号集的某一信号置为0

        int sigismember(const sigset_t *set, int signo);//判断某一信号在信号集中是否为1

ps:

        函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。

        函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。

注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

sigprocmask函数

sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)

进程信号_第15张图片

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。 

sigpending函数

sigpending函数可以读取当前进程的未决信号集,通过set参数传出

进程信号_第16张图片

现在我们使用sigprocmask函数和sigpending函数完成一个函数。思路是,

1、对2号信号写入信号集

2、将该信号集使用sigprocmask函数将2号信号block住

3、我们键盘输入ctrl+c,产生2号信号,发送至该进程。

4、使用sigpending函数输出pending位图。

ps:因为我们将2号进程block住了,该信号并不能抵达。我们再向其发送2号信号,信号被block住了并不会影响信号的写入。因此我们再发送2号进程之前pending位图应该全为0,发送2号进程之后会看见pending位图发送由0至1的变化。

void show_pending(sigset_t* set){
  int i = 1;
  for(i; i<= 31; i++){
    if(sigismember(set, i)){
      printf("1");
    }
    else{
      printf("0");
    }
  }
  printf("\n");
}
void handler(int signo ){
  printf("%d号信号被抵达了,已经处理完成!\n", signo);
}
int main(){

  signal(2, handler);//捕捉2号信号,完成自定义动作
  sigset_t iset, oset;

  //对集合做清空
  sigemptyset(&iset);
  sigemptyset(&oset);

  sigaddset(&iset, 2);//将2号信号写入iset集合

  //1、设置当前进程的屏蔽字
  //2、获取当前进程老的屏蔽字
  sigprocmask(SIG_SETMASK, &iset, &oset);

  int count = 0;
  sigset_t pending;
  while(1){
    sigemptyset(&pending);

    sigpending(&pending);

    show_pending(&pending);
    sleep(1);

    count++;
    if(count == 10){
      sigprocmask(SIG_SETMASK, &oset, NULL);
      //2号信号执行自定义动作
      printf("恢复2号信号,可以被抵达了\n");
    }
  }
  return 0;
}

运行结果如下

进程信号_第17张图片

信号处理 

进程从内核态返回到用户态的时候,进行信号检测和处理

用户态和内核态的理解

进程信号_第18张图片

 关于进程如何切换,我们都能找到同一个OS,因为使用同一张内核页表

关于系统调用:就是进程的身份转化为内核态,然后根据内核页表找到系统函数,进行执行。

大部分情况下,我们的OS都是可以在进程的上下文中直接运行。

处理信号的过程

进程信号_第19张图片

ps:用户态的时候执行用户代码,在调用系统接口或者进程切换的时候切换到内核态,查看进程的三张表。(信号捕捉过程)当需要执行自定义动作的时候,切回用户态执行自定义动作,自定动作完成之后,再切到内核态执行sys_sigreturn()函数,最后再切回用户态中用户代码的下一行,继续执行。当执行默认动作或者忽略动作的时候,直接可以再内核中完成,完成之后切回用户态中的用户代码的下一行,继续执行。

信号捕捉

sigaction函数

sigaction函数可以读取和修改与指定信号相关联的处理动作

signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

进程信号_第20张图片

 当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。

示例

#include 
#include 
#include 
#include 
void handler(int signo){
  while(1){
    printf("get a signo: %d\n", signo);
    sleep(1);
  }
}
int main(){
  struct sigaction act;
  memset(&act, 0, sizeof(act));

  act.sa_handler = handler;//执行自定义动作
  //act.sa_handler = SIG_IGN;
  //act.sa_handler = SIG_DFL;

  sigemptyset(&act.sa_mask);
  sigaddset(&act.sa_mask, 3);

  sigaction(2, &act, NULL);//本质是修改当前进程的handler函数指针数组特定内容

  while(1){
    printf("hello linux!\n");
    sleep(1);
  }

  return 0;
}

上边的代码代码对2号信号执行自定义动作,并将3号信号添加到了block位图中,因此无论我们输入ctrl+c(向该进程发送2号信号)还是ctrl+\(向该进程发送3号信号)都无法终止进程,只能发送9号管理员信号终止。执行结果如下:

进程信号_第21张图片

volatile关键字

#include 
#include 

int flag = 0;

void handler(int signo){
  flag = 1;
  printf("change flag 0 to 1\n");
}
int main(){
  signal(2, handler);

  while(!flag);

  printf("这个进程是正常退出的!\n");
  return 0;
}

我们使用gcc  -O3的方式优化上述代码。

进程信号_第22张图片

无法出现我们预期的结果。原因如下图。 

进程信号_第23张图片

ps:我们使用gcc优化的方式,全局变量flag的值会直接缓存在cpu中的寄存器里,当flag值发生变化的时候,cpu也不会从内存中再次读取flag,而是使用缓存的flag值。

volatile的作用则是:告诉编译器,不要对该变量做任何优化,必须贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据。

在flag变量前加上volatile关键字的运行结果。

SIGCHLD信号

子进程在终止时会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略。验证如下:

#include 
#include 
#include 
#include 

void handler(int signo){
  printf("get a signal: %d, pid: %d\n", signo, getpid());
}

int main(){

  signal(SIGCHLD, handler);
  //signal(SIGCHLD, SIG_IGN);//显式设置忽略SIGCHLD信号,当进程退出后,自动释放僵尸进程

  pid_t id = fork();
  if(id == 0){
    int cnt = 5;
    while(cnt){
      printf("I am child: %d\n", getpid());
      sleep(1);
      cnt--;
    }
    exit(0);
  }
  while(1);
  return 0;
}

运行结果

进程信号_第24张图片

你可能感兴趣的:(Linux,linux)