飞道的博客

Linux--信号

189人阅读  评论(0)

整体过程

信号的入门

1、生活中的例子

1、 你在网上买了很多件商品(多种信号),再等待不同商品快递的到来。但即便快递没有到来,也就是你能“识别快递”(识别多种信号)。(信号达到之前)
2、当快递员到了你楼下,你收到了通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。(操作系统收到了信号并不立即处理,这是可能还有比处理信号更重要的事要做。)
3、当你觉得时间合适了,拿到快递之后,就要处理快递了。而处理快递(处理信号的三种方式)一般方式有三种:(1)、执行默认动作(幸福的打开快递,使用商品)。(2)、 执行自定义动作(快递是零食,你要送给你你的女朋友)。(3)、 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)。
4、快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

2、技术应用角度的信号

实验代码:

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

int main(){
   
  while(1){
   
    printf("I am running....\n");
    sleep(2);
  }
  return 0;
}

运行截图:

当我在控制台输入命令启动一个进程。当按下ctrl+C,这个键盘输入产生一个硬件中断,被OS所截获,解释成信号,发送目标前台进程一个SIGINT信号,前台进程收到一个信号,进而引起进程退出。

3、注意

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

4、信号的概念

信号是进程之间事件异步通知的一种方式,属于软中断。它通知进程系统中发生了一个某种类型的事件。

5、用kill -l命令可以察看系统定义的信号列表

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,编号34–64的信号实时信号,我们只讨论34以下的信号。注意:其中9号信号是不能不自定义捕捉的。

6.、信号处理常见方式概览

处理动作有以下三种:

1、忽略此信号。
2、执行该信号的默认处理动作。
3、提供一个信号处理函数,用户自定义函数,这种方式称为捕捉。

产生信号

1、调用系统函数向进程发送信号

kill函数

//作用:进程通过调用kill函数发送信号给其他进程(包括它们自己)
#include<sys/types.h>
#include<signal.h>

int kill(pid_t pid,int sig);
//成功返回0,错误返回-1

实例:


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

int main(int argc,char* argv[]){
   
  
  if(argc==3){
   
    kill(atoi(argv[1]),atoi(argv[2]));
  }
  return 0;
}

//通过运行一个后台进程,然后通过调用这个程序来杀死那个后台进程

运行结果:

raise函数

//作用:调用raise函数可以自己给自己发信号
#include <signal.h>

int raise(int sig);
//成功返回0,错误返回-1

实例:


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


void handler(int sno){
   		//自定义一个信号
  printf("I catch a signal..\n");
}

int main(int argc,char* argv[]){
   
  signal(2,handler);	//注册一个信号
  while(1){
   
    sleep(1);
    raise(2);	//自己给自己发送2号信号
  }
  return 0;
}

运行结果:

abort函数

//作用:abort函数使当前进程接收到一个6信号而异常终止
#include <stdlib.h>

void abort(void);

实例:


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

void handler(int sno){
   		//自定义捕捉一个信号
  printf("I catch a signal : %d\n",sno);
}

int main(){
   
  signal(6,handler);	//注册一个信号
  while(1){
   
    sleep(1);
    abort();
  }
  return 0;
}

运行结果:

2、由软件条件产生信号

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发送14号信号(SIGALRM信号),该信号的默认处理动作是终止该进程。

#include<unistd.h>
unsigned int alarm(unsigned int seconds);

//返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0

实例:

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


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

运行结果:

3、 硬件异常产生信号

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

模拟野指针异常

模拟代码:

#include<stdio.h>

int main(){
   
	int *p=NULL;
	*p=100;
	while(1);
	return 0;
}

运行结果:

证明收到了SIGSEGV信号:

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

void handler(int sno){
   
  printf("I catch a signal..\n");
}
int main(){
   

  signal(SIGSEGV,handler);

  int *p=NULL;

  *p=100;

  while(1);
  return 0;
}

运行结果:

我们自定义了捕捉SIGSEGV信号,一直会打印这个结果那条消息,因为我们捕捉了并没有处理这个结果,所以一直会存在段错误那个异常。

模拟除0异常

模拟代码:

#include<stdio.h>

int main(){
   
	int t=1/0;
	return 0;
}

运行结果:

证明收到了SIGFPE信号:

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

void handler(int sno){
   
  printf("I catch a signal..\n");
}
int main(){
   

  signal(SIGFPE,handler);
	int i=1/0;
  return 0;
}

运行结果:

我们自定义了捕捉SIGFPE信号,一直会打印这个结果那条消息,因为我们捕捉了并没有处理这个结果,所以一直会存在除零那个异常。

由此可以确认,我们在C/C++当中的除零,内存越界等异常,在系统层面上,是被当成信号处理的。

4、 通过键盘发送信号

在键盘上出入Ctrl+C会导致内核发送一个SIGINT信号到前台进程组中的每个进程,默认情况下,结果是终止前台作业。类似地,输入Ctrl+Z会发送一个SIGTSTP信号到前台进程中的每个进程,默认情况下。结果是停止(挂起)前台进程。

思考总结

上面所说的所有信号产生,最终都要有OS来进行执行的,为什么?OS是进程的管理者。
信号的处理是不是立即处理的?是合适的时候被处理。
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?是要被记录下来的,被记录在进程的数据结构(task_struct)中的。
一个进程在没有收到信号的时候,能否知道自己应对合法信号作何处理?是知道的。如果没有自定义捕捉信号那么有自己默认的处理方式。

信号的操作

信号其他相关常见概念

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

在内核表示大意图

信号的进程中的保存示意图:


block、pending位图的意义:

1、每个信号都有两个标志位分别表示阻塞(Block)和未决(pending),还有一个函数指针表示处理动作。其中标志位我们用位图表示34号以下的信号状态,其中比特位的位置:是哪个信号,比特位的内容:是否产生信号。信号产生时,内核在进程控制块中设置该信号的未决状态,直到该信号递达才清除该标志位。
2、例如:对于上图:SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。SIGINT信号产生过,但正在被阻塞,所以暂时不能被递达。虽然它的处理动作是忽略的,但在没有接触阻塞之前不能忽略这个信号,进程有机会在改变处理动作之后再接触阻塞。SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义sighandler函数。
3、如果在解除阻塞之前产生过多次相同的信号,Linux系统对于1-31号的信号只记录一次,但是对于34-64号的实时信号Linux会将这个信号放在一个队列中进行管理。

sigset_t

对于阻塞标志和未决标志,每个信号只用一个bit位标志,非0即1,不记录该信号产生多少次。因此阻塞和未决用同一种类型sigset_t来存储。sigset_t称为信号集,在阻塞信号集中“有效“和”无效“,表示信号是否被阻塞状态,在未决信号集中”有效“和“无效”表示信号是否处于未决状态。

信号集操作函数

我们可以通过以下函数对信号集sigset_t操作,对于系统怎么存储这些bit则依赖于具体的系统。我们只需要使用以下函数操作信号集就行。

#include <signal.h>

int sigemptyset(sigset_t *set);
//sigemptyset初始化set所指向的信号集,使其中所有bit位清零

int sigfillset(sigset_t *set);
//sigfillset初始化set所指向的信号集,使其中所有bit位设为有效

int sigaddset (sigset_t *set, int signo);
//sigaddset将信号集中的某个信号bit位设为有效

int sigdelset(sigset_t *set, int signo);
//sigdelset将信号集中的某个信号bit位设为无效
//对于上面4个函数的返回值:成功返回0,出错则为-1

int sigismember(const sigset_t *set, int signum);
//sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1

sigprocmask函数

调用sigprocmask函数可以读取或更改进程的阻塞信号集(Block位图表)。

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//参数:
	how和set信号阻塞集一起只是如何更改进程的阻塞信号集
	oset若是非空指针,读取当前进程的阻塞信号集通过oset传出,输出型参数。
	set若是非空指针,更改进程的阻塞信号集
	
//返回值:若成功则为0,若出错则为-1

how参数值可选:

可选参数 含义
SIG_BLOCK set包含了我们希望添加到当前阻塞信号集的信号,相当于mask=mask|set
SIG_UNBLOCK set包含了我们希望从当前阻塞信号集中解除的信号,相当于mask=mask&~set
SIG_SETMASK 设置当前阻塞信号集为set所指向的值,相当于mask=set

sigpending函数

//功能:读取当前进程的未决信号集,输出型参数
#include <signal.h>

int sigpending(sigset_t *set);

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

通过上面函数写一个小程序

我们一直读取当前进程的pending信号表,首先在程序的最初阻塞2号信号,然后通过按Ctrl+C向当前进程发送2号进程,这个过程持续15秒,然后解除阻塞。

实验代码:

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

void show_pending(sigset_t *pending){
   //显示当前的pending信号集
  int i;

  for(i=1;i<=31;++i){
   
    if(sigismember(pending,i)){
   
      printf("1");
    }else{
   
      printf("0");
    }
  }
  printf("\n");
}
int main(){
   

  sigset_t pending,block,oblock;
  
  sigemptyset(&pending);
  sigemptyset(&block);
  sigemptyset(&oblock);
  sigaddset(&block,2);//阻塞2号信号集

  sigprocmask(SIG_SETMASK,&block,&oblock);//将进程的阻塞信号集设为block
  
  int count=0;
  while(1){
   
    sigemptyset(&pending);
    sigpending(&pending);
    show_pending(&pending);//显示当前进程pending位图
    sleep(1);
    count++;
    if(count==15){
   
      printf("recover sig mask\n");
      sigprocmask(SIG_SETMASK,&oblock,NULL);//当恢复以前的阻塞信号集时,解除阻塞,然后信号递达,信号终止
    }
  }

  return 0;
}

实验截图:

信号的捕捉

信号捕捉大致过程

内核如何实现信号捕捉

信号捕捉:如果信号的处理动作是用户自定义函数,信号递达时就调用这个函数。

其中内核实现捕捉信号就是上图中的大致过程以及状态转换。为了更好的理解举例说明:用户注册了SIGQUIT信号的处理函数sighandler。当前正在执行main函数,这时发生中断或异常调用切换到内核。在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。内核决定返回用户态执行处理函数sighandler而不是main函数执行。由于main函数和sighandler函数使用不同的堆栈空间,是两个独立的执行流。sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态,如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

问题

为什么要返回用户态执行用户自定义处理函数为什么不在内核态处理?

我们知道内核态的权限更大,一定是可以执行用户态的自定义函数,但是为什么要返回用户态执行用户态的函数?因为内核态的权限更大,在内核态下运行自定义函数,如果这个自定义函数不是一个正常的函数是一个非法的动作,那么这时在内核态下执行这个函数,很有可能破坏操作系统,其他进程和操作系统还不能终止这个进程。

volatile关键字

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

站在信号的角度理解volatile关键字

实验代码:

//mybin.c
#include<stdio.h>
#include<signal.h>

int quit=0;
void handler(int son){
   
  quit=1;
  printf("change quit 0 to 1..!\n");
}
int main(){
   

  signal(2,handler);
  
  while(!quit);
  printf("end of process....!!\n");
  return 0;
}

//Makefile:
mybin:mybin.c
	gcc mybin.c -o mybin 
.PHONY:clean
clean:
	rm -f mybin
	

运行截图:

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

当编译的时候对程序进行优化:

#Makefile

mybin:mybin.c
	gcc mybin.c -o mybin -O2
.PHONY:clean
clean:
	rm -f mybin

运行截图:

优化的情况下,键入Ctrl+C,2号信号被捕捉,执行自定义动作,修改quit=1,但是while条件依旧满足,继续执行进程运行!编译器将程序做了一定的优化,当发现quit在main函数中不会对quit做任何修改的动作,所以将quit优化到了CPU寄存器中,但是修改quit=1,实际是修改的内存中的quit,并没有修改寄存器中的quit,所以while条件依旧满足程序不会退出。所以我们可以用volatile关键字避免这个问题。

添加volatile关键字:

//mybin.c
#include<stdio.h>
#include<signal.h>

volatile int quit=0;
void handler(int son){
   
  quit=1;
  printf("change quit 0 to 1..!\n");
}
int main(){
   

  signal(2,handler);
  
  while(!quit);
  printf("end of process....!!\n");
  return 0;
}

//Makefile:
mybin:mybin.c
	gcc mybin.c -o mybin -O2
.PHONY:clean
clean:
	rm -f mybin
	

运行截图:

SIGCHLD关键字

子进程在终止时会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略的,父进程可以自定义SIGCHLD信号处理函数,这样父进程只需要专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

代码验证:

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

void handler(int sno){
   
  printf("catch a signum : %d ,i pid is : %d\n",sno,getpid());
  
}

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

  if(fork()==0){
   
    printf("I am child,I am running.I pid is %d, I ppid is %d\n",getgid(),getppid());
    sleep(5);
    printf("I will exit...\n");
    exit(1);
  }
  while(1);
  return 0;
}

运行结果:


转载:https://blog.csdn.net/weixin_44627813/article/details/115607180
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场