Linux进程信号

信号入门

1. 生活角度的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时, 你该怎么处理快递。也就是你能“识别快递” 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那 么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不 是一定要立即执行,可以理解成“在合适的时候去取”。 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知 道有一个快递已经来了。本质上是你“记住了有一个快递要去取” 当你时间合适,顺利拿到快递之后,就要开始处理快递了。

而处理快递一般方式有三种:

1. 执行默认动 作(幸福的打开快递,使用商品)

2. 执行自定义动作(快递是零食,你要送给你你的女朋友)

3. 忽略快 递(快递拿上来之后,扔掉床头,继续开一把游戏) 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话

2. 技术应用角度的信号

1. 用户输入命令,在Shell下启动一个前台进程。

用户按下Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程 . 前台进程因为收到信号,进而引起进程退出

现在发送2号信号(Ctrl+C )观察进程的状态;

 #include<iostream>
 using namespace std;
 #include<signal.h>
 #include<stdlib.h>
 #include<unistd.h>

 void hangder(int signo)
 {
   cout<<"this is signo:"<<signo<<endl;
   exit(1);
 }
 int main()
 {
   signal(2,hangder);
   while(1)
   {
     cout<<"hello c++"<<endl;
     sleep(1);
   }
 }

 现在我们捕捉所有的信号观察结果:

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>

void hangder(int signo)
{
  switch(signo)
  {
    case 2:  //ctrl+c
      cout<<"this is "<< signo<< " signal"<<endl;
      break;
    case 3: //ctrl+"\"

      cout<<"this is "<< signo<< " signal"<<endl;
      break;
    case 9:
      cout<<"this is "<< signo<< " signal"<<endl;
      break;
    case 20: //ctrl+ z
      cout<<"this is "<< signo<< " signal"<<endl;
      break;
    default:
      break;
  }
 // exit(1);
}
int main()
{
  for(int i = 1; i<= 31; ++i)
  {
    signal(i,hangder);
  }
  while(1)
  {
    cout<<"hello c++"<<endl;
    sleep(1);
  }
}

 现在输出ctrl  + c 或者ctrl + z 或者 ctrl + \都是我们自己写的自定义的情况,所以程序不会停下来,我在另一个终端输出命令同理如下图:

 可以看出其他的信号都是可以自定义的,但是9号信号不可以被重定义。

后台进程是不能被键盘发送的信号杀死的

 3. 注意

1. Ctrl-C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程 结束就可以接受新的命令,启动新的进程。

2. Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl-C 这种控制键产生 的信号。

3. 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,也就是说该进程的用户空间代码执行 到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步 (Asynchronous)的。

4. 信号概念

信号是进程之间事件异步通知的一种方式,属于软中断

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

每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2,下图为操作系统内核源码是宏定义。

#ifndef _LINUX_SIGNAL_H
#define _LINUX_SIGNAL_H

typedef unsigned int sigset_t;		/* 32 bits */

#define _NSIG             32
#define NSIG		_NSIG

#define SIGHUP		 1
#define SIGINT		 2
#define SIGQUIT		 3
#define SIGILL		 4
#define SIGTRAP		 5
#define SIGABRT		 6
#define SIGIOT		 6
#define SIGUNUSED	 7
#define SIGFPE		 8
#define SIGKILL		 9
#define SIGUSR1		10
#define SIGSEGV		11
#define SIGUSR2		12
#define SIGPIPE		13
#define SIGALRM		14
#define SIGTERM		15
#define SIGSTKFLT	16
#define SIGCHLD		17
#define SIGCONT		18
#define SIGSTOP		19
#define SIGTSTP		20
#define SIGTTIN		21
#define SIGTTOU		22

/*
 * Most of these aren't used yet (and perhaps never will),
 * so they are commented out.
 */


#define SIGIO		23
#define SIGPOLL		SIGIO
#define SIGURG		SIGIO
#define SIGXCPU		24
#define SIGXFSZ		25


#define SIGVTALRM	26
#define SIGPROF		27

#define SIGWINCH	28

/*
#define SIGLOST		29
*/
#define SIGPWR		30

/* Arggh. Bad user source code wants this.. */
#define SIGBUS		SIGUNUSED

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

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

(sigaction函数稍后详细介绍),可选的处理动作有以下三种:

1. 忽略此信号。

2. 执行该信号的默认处理动作。

3. 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号。

总结:

一般而言,进程收到信号的处理方案有3种情况

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

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

3.(信号的捕捉)自定义动作---我们刚刚就是捕捉signal的方法,默认---自定义动作

2.进程异常退出

上面我们看到的是通过键盘产生的信号,下面我们看一看进程异常产生的信号。

 首先我看看正常退出进程的退出码和进程退出信号和core dump状态

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
  if(fork() == 0)
  {
    cout<<"i am child"<<endl;
  }
  int status = 0;
  printf("core exit:%d ,core signal:%d,core dump:%d\n",(status>>8)& 0XFF,status & 0X7F,(status>>7) & 1);
  waitpid(-1,&status,0);
}

 

 上面是正常退出的状态

我们看一下整数除0的情况:

 #include<iostream>
 using namespace std;
 #include<signal.h>
 #include<stdlib.h>
 #include<unistd.h>
 #include<stdio.h>
 #include<sys/types.h>
 #include<sys/wait.h>

 int main()
 {
   if(fork() == 0)
   {
     cout<<"i am child"<<endl;
     int i = 10;
     i/=0;
     sleep(1);
   }
   int status = 0;
   waitpid(-1,&status,0);
   printf("core exit:%d ,core signal:%d,core dump:%d\n",(status>>8)& 0XFF,status & 0X7F,(status>>7) & 1);
 }

我们通过运行发现core dump标志位被设置为1,并且退出信号为

8) SIGFPE 这是浮点数错误

我们在看一下野指针的情况:

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>



int main()
{
  if(fork() == 0)
  {
    cout<<"i am child"<<endl;
//    int i = 10;
 //   i/=0;
   int *p = NULL;
   *p = 100;
    sleep(1);
  }
  int status = 0;
  waitpid(-1,&status,0);
  printf("core exit:%d ,core signal:%d,core dump:%d\n",(status>>8)& 0XFF,status & 0X7F,(status>>7) & 1);
}

 

野指针的情况core dump标志位为1,退出signal = 11;

当我们 收到错误信号的时候我们最想干什么

最想知道哪里错误了,Linux就提供这种事后调试的功能 当我们设置了core dump标志位的时候,一旦出现错误会生成调试文件,例如

 这个时候我们可以通过gbd调试找到哪一行出现错误

有些计算机可能没有设置core dump可以用ulimit -a 查看 然后用ulimit -c +大小设置。 

 11) SIGSEGV 是段错误

3.系统调用产生信号

#include<iostream>                             
using namespace std;                           
#include<signal.h>                             
#include<stdlib.h>                             
#include<unistd.h>                             
#include<stdio.h>                              
#include<sys/types.h>                          
#include<sys/wait.h>                           
#include<signal.h>                             
                                               
                                               
                                               
int main(int argc,char* argv[])                
{                                              
  if(argc < 3)                                 
  {                                            
    printf("Usage: argv[0] signo pid\n");      
    exit(1);                                   
  }                                            
  kill(atoi(argv[2]),atoi(argv[1]));           
}                                              

我们输出./test 9 1449 就可以杀死进程了

4.软件条件产生信号

前几张我们写管道,如果读端关闭,写端还一直写,也会产生信号终止进程,正常代码如下:

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<signal.h>


int main()
{
 int pip[2];
 int ret =  pipe(pip);
 if(ret < 0)
 {
   perror("pip()\n");
   exit(1);
 }
  if(fork() == 0)
  {
    close(pip[0]);

    while(1)
    {
      char buf[64] = "hello c++";
      write(pip[1],buf,strlen(buf));
      sleep(5);
    }
    close(pip[1]);
  }

  close(pip[1]);
  while(1)
  {
    char buf[64] = {0};
    int res = read(pip[0],buf,sizeof(buf));
    if(res > 0)
    {
      printf("%s\n",buf);
    }
    else if(res == 0)
    {
      printf("read is over\n");
    }
    else
    {
      break;
    }
  }
  int status = 0;
  waitpid(-1,&status,0);
  close(pip[0]);
}

每个5秒子进程向匿名管道写,父进程一直在读,接下我们让读端关闭,观察子进程的退出状态

kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定 的信号(自己给自己发信号)

#include <signal.h>

int kill(pid_t pid, int signo);

int raise(int signo);

这两个函数都是成功返回0,错误返回-1

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>



int main()
{
  while(1)
  {
    cout<<"hello c++"<<endl;

    sleep(5);
    raise(9);
  }
}

该进程5秒后会自己杀死自己

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

 #include<iostream>
 using namespace std;
 #include<signal.h>
 #include<stdlib.h>
 #include<unistd.h>
 #include<stdio.h>
 #include<sys/types.h>
 #include<sys/wait.h>



 int main()
 {
   while(1)
   {
     cout<<"hello c++"<<endl;

    sleep(5);

    abort();
   }
 }

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

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

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
    alarm(5);
    int count = 0;
    while(1)
    {
      ++count;
      printf("%d\n",count);
    }
}

该程序5秒加到了670124次,在看下面的程序

#include<iostream>
using namespace std;
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>

int count = 0;
void hangder(int signo)
{
  cout<<count<<endl;
  exit(1);
}
int main()
{
    signal(SIGALRM,hangder);
    alarm(1);
    while(1)
    {
      ++count;
    }
}

可以看出io是非常慢的 

总结思考一下

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者

信号的处理是否是立即处理的?在合适的时候

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

 如何理解操作系统给进程发送信号-> 操作系统发送数据给task_struct ->本质是向指定的进程task_struct 中的信号图写入比特位1,即能完成信号的发送->信号的写入

进程采用位图来标识是否收到信号

struct task_struct {
/* these are hardcoded - don't touch */
	volatile long state;	/* -1 unrunnable, 0 runnable, >0 stopped */
	long counter;
	long priority;
	unsigned long signal;
	unsigned long blocked;	/* bitmap of masked signals */
	unsigned long flags;	/* per process flags, defined below */
	int errno;
	int debugreg[8];  /* Hardware debugging registers */
}

这是内核task_struct中一部分,可以看出有task_struct 种有对应的把变量标识信号

 用了一个32位的数标识这个位图,对应的二进制位为1,代表改信号是否被收到

a.实际执行信号的处理动作被称为信号的递达

        1.默认处理动作

        2.忽略

        3.自定义处理动作

b.信号从产生到递达之间的状态被称为未决(pending)------本质这个信号被暂存到task_struct信号位图中,未决

c.进程可以选择阻塞(block)某个信号

        本质是操作系统,允许进程暂时屏蔽指定的信号

                1.该信号依旧是未决

                2.该信号不会被递达,知道解除阻塞,方可递达

block表:标识改位置的信号是否被阻塞(阻塞位图)也叫信号屏蔽字

pending表:标识是否收到该信号

handler:0是默认,1是忽略,也可以是自定义的方法。

上面的解释是:

a:1号信号没有被阻塞,1号信号没有被收到,1号信号的方法时候默认

b:2号信号被阻塞,收到2号信号,方法是忽略

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值