17 Linux进程信号

由于操作系统是进程的管理者,因此所有信号都必须经过操作系统发出。


文章目录

  • 一、信号的概念
    • 1.1.ctrl c发送前台信号
      • 1.1.1.用signal系统调用接口验证ctrl c是信号
          • SIGSTOP和SIGKILL不可捕获
      • 1.1.2.小结
  • 二、信号的常见处理方式
  • 三、信号的产生
    • 3.1.通过终端按键产生信号
    • 3.2.通过调用系统函数向进程发信号
      • 3.2.1.kill
      • 3.2.2.raise
      • 3.2.3.abort
    • 3.3.由软件条件产生信号
      • 3.3.1.alarm
      • 3.3.2.利用alarm验证IO对效率的影响
    • 3.4.由硬件异常产生信号
      • 3.4.1.野指针解引用
      • 3.4.2.除0
  • 四、Core Dump
  • 小结
  • 五、信号的流程
    • 5.1.信号的保存和发送
    • 5.2.信号在内核中的表示
      • 5.2.1.普通信号易丢失
  • 六、信号集操作函数
    • 6.1.sigset_t信号集
    • 6.2.信号集操作函数
    • 6.3.信号屏蔽字更改函数sigprocmask
    • 6.4.获取当前未决信号集函数sigpending
  • 七、信号的捕捉
    • 7.1.用户态、内核态
    • 7.2.信号捕捉流程
    • 7.3.信号捕捉函数 sigaction
  • 八、可重入函数
  • 九、volatile
  • 十、SIGCHLD信号
  • 附录---Linux常规信号一览表


一、信号的概念

就像打铃通知我们下课一样。

信号就是事件发生的通知,通知进程哪些事件要发生了。即便信号没发生,进程也知道如何处理这个信号,所以设置信号、捕捉信号等处理信号的动作由进程完成。
通过kill -l指令可以查看信号的种类,其中前31个为普通信号,后31个为实时信号。普通信号和实时的区别在于前者不支持排队,可能会造成信号丢失,而后者不会。
17 Linux进程信号_第1张图片

使用man 7 signalcman 7 signal可以查看其用法和产生条件:
17 Linux进程信号_第2张图片

1.1.ctrl c发送前台信号

以下面的程序为例:
17 Linux进程信号_第3张图片

17 Linux进程信号_第4张图片

可以看到当这个程序运行起来以后,输入除了Ctrl c之外的其他命令是没法运行的,这是因为这个程序是前台进程系统在一个终端中只允许有一个前台进程。因此,当这个程序在前台运行起来以后,bash也是一个进程,此时其处在后台的,所以接收不到我们发送的其他命令。

但是Ctrl c指令是通过硬件的输入方式中断进程,它的本质也是通过系统向进程发送信号

所以如果把这个程序放在后台运行,Ctrl c指令就无法终止整个程序。此时bash处在前台,可以接收到我们输入的其他命令。
17 Linux进程信号_第5张图片

当进程被设置为后台进程时,我们在命令行输入的消息流会和后台进程的信息混合在一起,这是因为bash进程是在前台的,我们可以输入信息,但是显示器只有一个,被两个进程同时使用,说明他是临界资源,而这个临界资源又没有被保护,因此它的数据会发生混乱。

此时输入fg 进程路径就可以让进程从后台回到前台。

1.1.1.用signal系统调用接口验证ctrl c是信号

17 Linux进程信号_第6张图片

这个接口能够捕捉并重定向一个信号的默认处理动作,使信号不执行原来的动作,而是执行我们自定义的动作
 #include 
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signum:对应信号的编号(普通信号编号1-31,实时信号编号34-64)
handler:回调函数(函数指针),传一个函数的地址。这个函数就是我们自定义的处理动作。

ctrl c发送的是2号信号:

#include 
#include   
#include 
void handler(int signo){  
  printf("catch a signal:%d\n",signo); 
}
int main(){ 
  while(1){ 
    signal(2,handler);//收到2号信号就执行我们设定的动作
    printf("I am running..!\n"); 
    sleep(1);
    } 
} 

17 Linux进程信号_第7张图片

由于我们修改了其默认处理动作,所以输入Ctrl c是不会退出的,而是执行我们设定好的动作。

此时可以使用Ctrl \来退出,这个指令是发送的是3号信号SIGQUIT

SIGSTOP和SIGKILL不可捕获

9号信号SIGKILL和19号信号 SIGSTOP是不能被signal函数捕捉并修改默认动作的。原因也很简单:如果所有信号都可以被捕捉,病毒可以将所有信号捕捉更改掉,系统就瘫痪了,因此需要这俩信号不能被捕捉,即系统始终拥有对进程的终止能力。

1.1.2.小结

  • Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号。
  • Ctrl c产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。
  • 前台进程在运行过程中用户随时可能按下 Ctrl c而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。

17 Linux进程信号_第8张图片


二、信号的常见处理方式

当进程收到信号以后,可选的处理动作有以下三种:

  • 忽略此信号。
  • 执行该信号的默认处理动作。
  • 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉(Catch)一个信号。

信号产生之后,不是立即被处理的:
17 Linux进程信号_第9张图片

信号从产生到处理的过程中是有时间窗口的,在这个时间窗口之内信号是要排队的,所谓的排队是将这个信号记录或保存下来。


三、信号的产生

3.1.通过终端按键产生信号

信号的第一种产生方式是通过键盘,常见的有以下几种:

  • ctrl+c 2号信号 SIGINT

  • ctrl+z 20号信号 SIGTSTP

  • ctrl+\ 3号信号 SIGQUIT

3.2.通过调用系统函数向进程发信号

3.2.1.kill

这个系统调用函数可以给指定的进程发送信号。
17 Linux进程信号_第10张图片

#include 
#include 
int kill(pid_t pid, int sig);
pid:代表目标进程的pid
sig:代表要发送几号信号

返回值:
调用成功返回0,失败返回-1

采用命令行参数的方式给进程发送信号:

#include      
#include 
#include  
#include    
void handler(int signo){    
  printf("catch a signal: %d\n",signo);    
}    
int main(int argc,char*argv[]){    
  if(argc==3){
  kill(atoi(argv[1]),atoi(argv[2]));   } 
} 

这里的argv是char *类型,而kill的参数是int型,所以需要用atoi函数进行转化。

17 Linux进程信号_第11张图片

3.2.2.raise

这个系统调用函数是自己给自己发信号
17 Linux进程信号_第12张图片

#include 
int raise(int sig);
sig:给自己发送信号的名称

返回值:
调用成功返回0,失败-1

自己给自己发送2号信号来验证这个系统调用函数:

#include      
#include      
#include      
#include      
void handler(int signo){      
  printf("catch a signal: %d\n",signo);      
}      
int main(){    
  signal(2,handler);  
  while(1){    
    sleep(1);    
    raise(2);    
   } 
}  

17 Linux进程信号_第13张图片

3.2.3.abort

abort使当前进程给自己发生6号信号SIGABRT
17 Linux进程信号_第14张图片

#include 
void abort(void);
这个系统调用函数给自己的进程发6号信号,所以没有参数
并且这个函数总是能调用成功,因此也没有返回值
#include      
#include      
#include      
#include      
void handler(int signo){      
  printf("catch a signal: %d\n",signo);      
}      
int main(){    
  signal(6,handler);  
  while(1){    
    sleep(1);    
    abort();    
   } 
}  

17 Linux进程信号_第15张图片

这个信号虽然被捕捉了,但是该信号原来的动作还是被执行了。

3.3.由软件条件产生信号

3.3.1.alarm

alarm相当于系统的闹钟,在seconds秒后告诉内核,给当前进程发送一个SIGALRM信号,该信号默认处理动作是终止当前进程
17 Linux进程信号_第16张图片

#include 
unsigned int alarm(unsigned int seconds);
seconds:秒数,如果seconds值为0,表示取消以前设定的闹钟,
函数的返回值仍然是以前设定的闹钟时间还余下的秒数

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

通过计数器来验证这个函数:

#include    
#include    
#include    
#include    
void handler(int signo){    
  printf("catch a signal: %d\n",signo);    
}    
int main(){    
	alarm(1);//1秒以后给自己发闹钟信号
	int count=0;
	while(1){  	
		 printf("count:%d\n",count++); 
  } 
} 

17 Linux进程信号_第17张图片

在一秒之中count打印了两万次左右,一秒钟到了就被14号SIGALRM信号终止。

一个操作系统中可能有很多个闹钟,因此需要将它管理起来,因此clarm底层也会有对应的数据结构对它进行描述组织当闹钟时间到了,操作系统中也有计时器,比如有个链表,链表中存储着计时器,每隔一秒就将计时器减一,当计数器为0时,则找到对应的进程发送信号。

这种提前设置好时间点,时间点到了操作系统自动发送信号,这种发送信号的方式就叫做软件条件产生信号。

3.3.2.利用alarm验证IO对效率的影响

上面那个程序count被打印了两万次,如果我们只打印一次呢?
17 Linux进程信号_第18张图片

17 Linux进程信号_第19张图片

可以看到count是一个非常大的数字。

这是因为,count++操作是在CPU中进行的,之前每次输出都是I/O操作(将内存中的数据输出到显示器(外设)上),只有最后一次输出的I/O操作。
可见I/O是非常影响效率的。

3.4.由硬件异常产生信号

硬件的异常被检测到,并且通知内核,内核会向当前进程发送适当的信号。
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为8号SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址(野指针),MMU会产生异常,内核将这个异常解释为11号SIGSEGV信号发送给进程。

3.4.1.野指针解引用

#include    
#include    
#include    
#include         
void handler(int signo){    
    printf("catch a signal: %d\n",signo);    
  }    
int main(){   
    signal(11,handler); 
	int*p;    
	*p=100;
	return 0;   
}

17 Linux进程信号_第20张图片

给野指针赋值,会发生段错误。
在这里插入图片描述

虚拟地址访问数据,虚拟地址需要先转换到物理地址,如果是野指针,那么在页表之个找不到对应的映射关系,这个地址转化就会发生错误。
MMU硬件转化该错误,操作系统就能识别到,然后发送信号,终止此进程。

3.4.2.除0

17 Linux进程信号_第21张图片

17 Linux进程信号_第22张图片

在这里插入图片描述

如果是除0操作,CPU在运算时会发现这个错误(CPU的状态寄存器会记录数据有没有溢出),此时操作系统就会给进程发送8号信号。


四、Core Dump

当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)。默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全。在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c 1024

总结起来就是:因为操作系统是给进程发信号的,操作系统发现进程要崩溃了,在崩溃之前会将内存中的重要信息转储到磁盘上。并且会把core dump标志位设置为1。
17 Linux进程信号_第23张图片

云服务器由于是线上环境,这个功能是关闭的,并且生成的文件大小也是关闭的。这是因为:

  1. 保护服务器
    云服务器默认是关闭Core Dump的,这是因为当发生段错误时,会在磁盘之中生成临时文件,而我们的服务器出现了错误,一般都是先让服务器先恢复使用再进行错误的调式。
    如果服务器重启就发生错误,这样会导致生成很多临时文件,那么生成的临时文件将磁盘堆满,甚至将系统盘堆满,那么我们的系统就会出错,导致无法第一时间恢复使用。
  2. 并不是所有的信号都需要Core Dump
    我们进程退出的时候,会有一个输出型参数status,低8位中的低7位表示退出信号,第8位表示Core Dump,如果为0则表示不需要,为1表示需要。比如我们的kill -9号信号,系统直接终止进程,是不需要调式的。

17 Linux进程信号_第24张图片

此时core file size这个文件的大小是0,通过ulimit -c 10240指令可以设置其大小为10240:
17 Linux进程信号_第25张图片

通过下面代码来验证,由于对空指针解引用,因此会在第5行崩溃:
17 Linux进程信号_第26张图片

17 Linux进程信号_第27张图片

这里其实可以看到从操作系统发现异常到发信号终止这个程序,这期间是有时间窗口的,进程并没有立即处理这个信号。

此时内存中的重要信息转储到磁盘上:
17 Linux进程信号_第28张图片

这个文件后面的数字代表的是形成这个core文件的进程pid。

这个文件是给调试器看的,在调试时输入core-file core文件就可以定位到出错的位置和收到的信号了。这种调试方式叫做事后调试
17 Linux进程信号_第29张图片

17 Linux进程信号_第30张图片


小结

先对上面的内容进行小结:

  • 由于操作系统是进程的管理者,所有信号产生,最终都要由操作系统来进行执行。
  • 信号的处理并不是立即处理的,而是在合适的时候。
  • 信号如果不是被立即处理,那么信号是需要暂时被进程记录下来的。
  • 一个进程在没有收到信号的时候,自己应该知道对合法信号作何处理。

五、信号的流程

17 Linux进程信号_第31张图片

  • 实际执行信号的处理动作称为信号递达(Delivery)。
  • 信号从产生到递达之间的状态,称为信号未决(Pending)。
  • 进程可以选择阻塞 (Block )某个信号。
  • 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
  • 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。

5.1.信号的保存和发送

信号是给进程发的,而进程中有task_struct结构体,该结构体中存在一个32位的位图(默认初始化为0),如果操作系统想给该进程发送信号,只需要将该进程的位图指定的为修改成1即可:
17 Linux进程信号_第32张图片

5.2.信号在内核中的表示

我们的内核之中有两张结构一样的位图,来表示当前信号是否被阻塞,和是否处于未决状态,还有一个函数指针表示处理的动作信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达,信号标志才会被清除
17 Linux进程信号_第33张图片

  • block表和pending表在结构上是一样的,但是比特位的含义不一样
  • block位图:记录当前信号是否被阻塞(屏蔽),0表示未阻塞,1表示阻塞。阻塞意为着信号永远也不会递达,也就是说永远都不会执行handler表中的函数
  • pending位图:保存当前的信号(未决),1表示信号已经产生(处于未决状态),0表示信号未产生
  • handler:存储函数指针的数组,表示某个信号处理时的默认动作,SIG_DFL表示默认处理,SIG_IGN表示忽略该信号,其它表示自定义处理

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。
信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志

在上图的例子中:
SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT3号信号会被阻塞,当前没有产生,如果信号产生了,并且解除阻塞,它的处理动作是用户自定义的处理动作。

信号也有可能处于未被阻塞(block表为0),但是未决(pending表为1)的状态,因为进程收到信号并不是立即被处理的

综上,一个进程是通过三张表来完成一个信号处理的:前两张表是位图结构,后一张表示数组结构

5.2.1.普通信号易丢失

由于普通信号是由位图记录的,只能记录一次,并不能记录个数,因此会发生信号的丢失。
而实时信号是由链表保存的,实时性比较强,信号来了就会立即处理,并且链表也会被管理起来,因此不易丢失


六、信号集操作函数

6.1.sigset_t信号集

每个信号的阻塞或未决都是由一个比特位来表示的,不是0就是1,因此未决和阻塞标志可以使用同一样数据类型sigset_t来进行存储
sigset_t被称为信号集,表示每个信号是有效还是无效

因此用户可以通过函数修改这个信号集然后填到block表或pending表中,修改这两个表。

在阻塞状态中,有效(1)、无效(0)表示是否被阻塞,阻塞信号集(block表)也被叫做当前进程的信号屏蔽字
在未决状态中,有效(1)、无效(0)表示信号是否处于未决状态

6.2.信号集操作函数

sigset_t是表示信号有效、无效状态的信号集,这个类型的实现是由系统来决定的,因此使用者只能调用下列函数的接口来操作sigset_t变量,而不是直接对其内部数据进行解释(比如,直接打印sigset_t类型变量,或者做位运算操作,这些都是没有意义的):

#include 
int sigemptyset(sigset_t *set);//清空信号集,使其比特位全部置为0,表示该信号集不包含 任何有效信号
int sigfillset(sigset_t *set);//把所有比特位全部置为1,表示该信号集的有效信号包括系统支持的所有信号。
int sigaddset (sigset_t *set, int signo);//向信号集中添加signo信号(将比特位由0设置为1)
int sigdelset(sigset_t *set, int signo);//向信号集中删除signo信号(将比特位由1设置为0)
int sigismember(const sigset_t *set, int signo);//判断signo信号是否在信号集中

所有信号的处理动作不能使用按位与按位或这样的动作(因为不同的系统sigset_t的实现方式不同),必须使用这些操作函数。
17 Linux进程信号_第34张图片

6.3.信号屏蔽字更改函数sigprocmask

block表有个专业的名称叫做信号屏蔽字
17 Linux进程信号_第35张图片

#include 
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
how:一共会被设置为三种值:
1.SIG_BLOCK:set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set(老位图|新位图)
2.SIG _UNBLOCK: set包含了,当前希望从信号屏蔽字解除阻塞的信号,想当于mask=mask&~set
3.SIG_SETMASK:设置当前信号屏蔽字为set所指向的值,相当于mask=set
set:
新的信号屏蔽字位图
oldset:
输出型参数,老的位图,用来备份的

返回值:
成功返回0,失败返回-1

6.4.获取当前未决信号集函数sigpending

17 Linux进程信号_第36张图片

#include 
int sigpending(sigset_t *set);
set:输出型参数,获取当前信号集的pending位图

返回值:
成功返回0,失败返回-1

以下面的代码演示这俩函数:

#include    
#include    
#include    

void handler(int signo){ 
  printf("catch a signal: %d\n",signo);
}                 
void show_pending(sigset_t* pending){    
  int sig=1;
  for(;sig<=31;sig++){
    if(sigismember(pending,sig)){//如果sig信号在pending中
        printf("1");
    } 
    else{
      printf("0");
    } 
  }
  printf("\n"); 

}                  
int main(){ 

  signal(2,handler);//捕捉并自定义2号信号    
  sigset_t pending;                          

  sigset_t block,oblock;           
  sigemptyset(&block);          
   sigemptyset(&oblock);                           
  sigaddset(&block,2);//向block信号集中添加2号信号
  sigprocmask(SIG_SETMASK,&block,&oblock);//将当前的信号屏蔽字设置为2号  
  
  int count=1;
  while(1){
    sigemptyset(&pending);//清理信号集
    sigpending(&pending);//获取当前pending信号集
    show_pending(&pending);//显示pending信号
    sleep(1);
    count++;
    if(count==10){
      //5秒后接触2号信号的阻塞
      printf("recover sig mask!\n");
      sigprocmask(SIG_SETMASK,&oblock,NULL);
    }
  }
}

17 Linux进程信号_第37张图片


七、信号的捕捉

操作系统向进程发出信号,进程并不是立即执行信号的,而是在合适的时候,这个合适的时候是信号被递达的时候。
一个信号递达的时间点,是在内核态切换回用户态时,进行信号相关检测的时候。

7.1.用户态、内核态

每个进程都有自己的用户区,用户区指向的映射区域是不一样的。但是内核区的代码和数据只有一份,所有进程的内核区映射是一样的,因为操作系统只有一个。
17 Linux进程信号_第38张图片

用户态:
当进程执行我们自己写的代码的时候,比如在栈上定义一个变量,写一个while循环,这时候进程就处于用户态
内核态:
当调用系统函数接口的时候,当前进程时间片到了,开辟一个空间分配内存等等都会切换到内核态。

用户态切换到内核态的三种情况:

  1. 系统调用,用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,系统调用本身就是中断,但是软件中断,跟硬中断不同。
  2. 异常:如果当前进程运行在用户态,如果这个时候发生了异常事件,就会触发切换。例如:缺页异常。
  3. 外设中断:当外围设备完成用户请求的操作后,会向CPU发出相应的中断信号,这时CPU会暂停执行下一条即将要执行的指令转而去执行与中断信号对应的处理程序。比如硬盘读写操作完成,系统会切换到硬盘读写的中断处理程序中执行后续操作等;时间片结束以后发生中断。

当一个进程执行系统调用接口时,该进程的状态会由用户态变成内核态,该进程不再是当前的用户进程,而是操作系统。换句话说,当处于内核态时,进程其实只是一个外壳,本质是操作系统。
17 Linux进程信号_第39张图片

CPU如何切换用户态和内核态:
CPU中有一个标志字段,标志着线程的运行状态。用户态和内核态对应着不同的值,用户态为3,内核态为0。所以当执行系统调用时,操作系统会将CPU的执行模式由用户改成内核。

7.2.信号捕捉流程

17 Linux进程信号_第40张图片

为什么需要这样来回的切换呢?
用户切内核:
因为操作系统的代码用户是没有权限去执行的,比如我们调用函数printf,但是底层实际是系统调用接口write在用户层面只能执行用户级页表映射的区域,而操作系统的内核页表映射的区域,用户态是无法访问的。
内核切用户:
内核是有权限执行用户态的代码的,如果signal信号处理动作是非法操作,而内核态的权限又很高,操作系统和其他程序就无法终止这个动作。

信号是如何检测的?
内核态(操作系统)的壳是当前进程,使用的是当前进程的的地址空间。因此可以通过当前地址空间找到当前进程,找到当前进程的PCB,然后访问block、pending位图,获取信号信息

这也就解释了当代码在上面就已经发生了段错误时,为什么还是以执行下面的输出语句呢?
17 Linux进程信号_第41张图片
17 Linux进程信号_第42张图片

这是因为,上面的代码都在用户区,发生错误的时候操作系统发送信号,信号的处理并不是及时的信号的处理,而是在内核态转化成用户态的时候被处理。
printf的底层也调用了操作系统接口wirte,是在内核态时进行处理的,因此将“run here”能够被打印出来。

7.3.信号捕捉函数 sigaction

这个函数和signal函数的定位是一样的,只是这个函数的功能更丰富一些。
17 Linux进程信号_第43张图片

#include 
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
signum:要捕捉信号的编号
act:捕捉到信号后的重定向动作
oldact:输出型参数,保存老的信号处理动作

struct sigaction {
               void (*sa_handler)(int);//重定向动作
               void(*sa_sigaction)(int, siginfo_t *, void *);//实时信号
               sigset_t  sa_mask;//当处理某个信号时,屏蔽的信号集,默认用函数设置为0
               int sa_flags;//默认设置为0
               void (*sa_restorer)(void);
           };

17 Linux进程信号_第44张图片

#include
#include
void handler(int signo){
  printf("catch a signal: %d\n",signo);
}
int main(){
 
  struct sigaction act,oact;
  act.sa_handler=handler;
  act.sa_flags=0;
  sigemptyset(&act.sa_mask);//使用函数情况信号集
  sigaction(2,&act,&oact);

  while(1){
  }
	return 0;
}

17 Linux进程信号_第45张图片


八、可重入函数

可重入的函数简单来说就是可以被中断的函数,也就是说,可以在这个函数执行的任何时刻中断它,转入操作系统调度下去执行另外一段代码,而返回控制时不会出现什么错误。

不可重入的函数由于使用了一些系统资源,比如全局变量区,中断向量表等,所以它如果被中断的话,可能会出现问题,这类函数是不能运行在多任务环境下的。比如链表插入的过程中收到信号要执行另一个链表插入的动作,信号处理完以后原来的插入节点可能会丢失,导致内存泄漏。

常见不可重入函数:

  1. 调用malloc/free的函数,因为malloc也是用全局链表来管理堆的
  2. 调用了I/O函数库,标准I/O库中很多实现都以不可重入的方式使用全局数据结构
  3. STL库,STL考虑的是效率问题,安全问题需要操作者自己考虑(比如迭代器失效问题)

九、volatile

在C语言中volatile关键字这篇博客中提到过volatile这个关键字

#include      
#include      
      
int quit=0;      
      
void handler(int sig){      
  quit=1;      
  printf("quit is alrady set to:%d\n",sig);      
}      
int main(){      
      
  signal(2,handler);      
  while(!quit);//只要quit为0,取反就为真,则一直执行while循环
  printf("end process!\n");                     
}  

17 Linux进程信号_第46张图片

标准情况下,键入CTRL C,2号信号被捕捉,执行自定义动作,修改 quit=1 , while 条件不满足,退出循环,进程退出。

gcc中指定优化级别的参数有:-O0、-O1、-O2、-O3、-Og、-Os、-Ofast。

以-O2优化:
17 Linux进程信号_第47张图片

优化情况下,键入CTRL C,2号信号被捕捉,执行自定义动作,修改 quit=1 ,但是 while 条件依旧满足,进程继续运行!这说明 while 循环检查的quit,并不是内存中最新的quit,而是寄存器中的quit。这就存在了数据二异性的问题。

为了解决这个问题,加入volatile关键字:
17 Linux进程信号_第48张图片

17 Linux进程信号_第49张图片

volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。


十、SIGCHLD信号

子进程退出会向父进程发送SIGCHLD信号,此时如果父进程如果通过signal或sigaction设置忽略这个信号(signal(SIGCHLD, SIG_IGN)),子进程终止时自动释放自己的资源,父进程不必再等待子进程(linux下可用,不保证其它系统上可用)或者自定义SIGCHLD信号处理动作,调用wait等资源释放函数,也可以完成子进程资源的释放:

#include     
#include     
#include     
#include     
#include     
#include      
void handler(int sig){       
  //wiat(NULL);//直接忽略该信号也可以实现资源的释放。       
   pid_t id;  
   while( (id = waitpid(-1, NULL, WNOHANG)) > 0){  
   printf("wait child success: %d\n", id);  
} 
  printf("child is quit! %d\n", getpid()); 
}      
int main(){   
   signal(SIGCHLD, handler);
   pid_t pid;  
   if((pid = fork()) == 0){//child 
   printf("child : %d\n", getpid()); 
   printf("child running!\n"); 
	 sleep(3);   
   exit(1);    
}
 while(1){ 
   printf("father proc is doing some thing!\n");
	sleep(1);  
} 
   return 0; 
  } 

17 Linux进程信号_第50张图片

17 Linux进程信号_第51张图片
17 Linux进程信号_第52张图片


附录—Linux常规信号一览表

17 Linux进程信号_第53张图片

  1. SIGHUP: 当用户退出shell时,由该shell启动的所有进程将收到这个信号,默认动作为终止进程

  2. SIGINT:当用户按下了组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。

  3. SIGQUIT:当用户按下组合键时产生该信号,用户终端向正在运行中的由该终端启动的程序发出些信号。默认动作为终止进程。

  4. SIGILL:CPU检测到某进程执行了非法指令。默认动作为终止进程并产生core文件

  5. SIGTRAP:该信号由断点指令或其他 trap指令产生。默认动作为终止里程 并产生core文件

  6. SIGABRT: 调用abort函数时产生该信号。默认动作为终止进程并产生core文件。

  7. SIGBUS:非法访问内存地址,包括内存对齐出错,默认动作为终止进程并产生core文件。

  8. SIGFPE:在发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。

  9. SIGKILL:无条件终止进程。本信号不能被忽略,处理和阻塞。默认动作为终止进程。它向系统管理员提供了可以杀死任何进程的方法。

  10. SIGUSE1:用户定义 的信号。即程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  11. SIGSEGV:指示进程进行了无效内存访问。默认动作为终止进程并产生core文件。

  12. SIGUSR2:另外一个用户自定义信号,程序员可以在程序中定义并使用该信号。默认动作为终止进程。

  13. SIGPIPE:Broken pipe向一个没有读端的管道写数据。默认动作为终止进程。

  14. SIGALRM: 定时器超时,超时的时间 由系统调用alarm设置。默认动作为终止进程。

  15. SIGTERM:程序结束信号,与SIGKILL不同的是,该信号可以被阻塞和终止。通常用来要示程序正常退出。执行shell命令Kill时,缺省产生这个信号。默认动作为终止进程。

  16. SIGSTKFLT:Linux早期版本出现的信号,现仍保留向后兼容。默认动作为终止进程。

  17. SIGCHLD:子进程结束时,父进程会收到这个信号。默认动作为忽略这个信号。

  18. SIGCONT:如果进程已停止,则使其继续运行。默认动作为继续/忽略。

  19. SIGSTOP:停止进程的执行。信号不能被忽略,处理和阻塞。默认动作为暂停进程。

  20. SIGTSTP:停止终端交互进程的运行。按下组合键时发出这个信号。默认动作为暂停进程。

  21. SIGTTIN:后台进程读终端控制台。默认动作为暂停进程。

  22. SIGTTOU: 该信号类似于SIGTTIN,在后台进程要向终端输出数据时发生。默认动作为暂停进程。

  23. SIGURG:套接字上有紧急数据时,向当前正在运行的进程发出些信号,报告有紧急数据到达。如网络带外数据到达,默认动作为忽略该信号。

  24. SIGXCPU:进程执行时间超过了分配给该进程的CPU时间 ,系统产生该信号并发送给该进程。默认动作为终止进程。

  25. SIGXFSZ:超过文件的最大长度设置。默认动作为终止进程。

  26. SIGVTALRM:虚拟时钟超时时产生该信号。类似于SIGALRM,但是该信号只计算该进程占用CPU的使用时间。默认动作为终止进程。

  27. SGIPROF:类似于SIGVTALRM,它不公包括该进程占用CPU时间还包括执行系统调用时间。默认动作为终止进程。

  28. SIGWINCH:窗口变化大小时发出。默认动作为忽略该信号。

  29. SIGIO:此信号向进程指示发出了一个异步IO事件。默认动作为忽略。

  30. SIGPWR:关机。默认动作为终止进程。

  31. SIGSYS:无效的系统调用。默认动作为终止进程并产生core文件。

(34)SIGRTMIN ~ (64) SIGRTMAX:LINUX的实时信号,它们没有固定的含义(可以由用户自定义)。所有的实时信号的默认动作都为终止进程。

你可能感兴趣的:(Linux,1024程序员节,linux,信号处理)