信号入门
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标志位的时候,一旦出现错误会生成调试文件,例如![](https://img-blog.csdnimg.cn/5e8b187a83864899884084126806f5a1.png)
这个时候我们可以通过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.该信号不会被递达,知道解除阻塞,方可递达