您现在的位置是:首页 >学无止境 >Linux-进程信号网站首页学无止境
Linux-进程信号
Linux-进程信号
一,信号入门
信号的概念
?信号:是进程间事件异步通知的一种方式,属于软中断。
生活中的信号
- 你在网上购买了许多商品,在等待快递的到来。即便快递没有到来,但是你也很清楚当快递来的时候,你要怎样处理快递。也就是说你能识别快递。
- 当快递到来的时候,你收到快递到的信息,但是你正在做一件比较重要的事情,所以你并没有直接去取快递,但是你已经知道有快递来了。也就是说取快递的行为不一定要立即执行,可以理解为在“合适的时间”去取。
- 在收到通知有快递要取,再到你拿到快递期间,是有一个时间窗口期的,在这段时间中,你并没有去取快递,但是你已经知道有一个快递已经来了。本质上是你记住了“有一个快递要去取”。
- 到了合适的时间,你来取快递了。取到快递后就要对快递做处理。处理快递的情况有三种:1,执行默认动作(拆开快递立马使用商品)2,执行自定义动作(这个快递你是送给别人的礼物,所以你会把它送给别人)3,忽略快递(你对快递不感兴趣扔到一边继续做其他事情)。
- 快递到来的这个过程对你来说是异步的,你不能准确断定快递具体什么时候到来。
技术应用角度的信号
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "我是一个进程,我的pid : " << getpid() << endl;
sleep(1);
}
return 0;
}
?这段代码运行起来就会每隔一秒打印一句,用户可以通过Ctrl + C 来终止这个进程。Ctrl + C 的原理:首先用户在键盘上按下Ctrl + C按键,这个键盘会产生硬件中断,会被OS获取到,然后将输入的内容转化成信号,然后OS会把信号发送给目标进程。
前台进程收到此信号之后就会终止进程。
注意:
1.Ctrl + C产生的信号只能发送给前台进程。一个命令后加 &(./mytest &)就会放到后台运行,这样Shell就不必等待进程结束就可以接受新的命令,启动新的进程。
2.Shell可以同时运行一个前台进程和多个后台进程,只有前台进程才能接收到像Ctrl + C这种控制键产生的信号。
3.前台程序运行过程中,用户可能随时按下Ctrl + C而产生一个信号,也就是说进程的用户空间代码运行到任何地方都有可能接收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。
使用kill -l 查看信号列表
?1-31号信号是普通信号,其余的信号是实时信号,我们讨论的是普通信号。
?这些信号本质就是用#define 定义的宏。例如SIGINT 就是 #define SIGINT 2。
?可以通过man 7 signal 查看信号详细信息
?验证Ctrl + C控制键产生的是SIGINT信号,下面介绍以下signal这个接口。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
signal函数的作用就是可以用户自定义信号的处理方式,它的第一个参数是信号的编号(你可以使用2,当然也可以使用SIGINT),第二个参数是一个函数指针就是自定义处理该信号的方法,采用回调的方式完成的。
void handler(int signo)
{
cout << "收到了 " << signo << " 号信号" << endl;
}
int main()
{
signal(SIGINT, handler);
while (true)
{
cout << "我是一个进程,我的pid : " << getpid() << endl;
sleep(1);
}
return 0;
}
事实证明了Ctrl + C控制键产生的信号就是2号SIGINT信号,默认执行动作是终止进程。
信号的处理方式
处理信号的方式一般有三种情况:
?执行该信号的默认执行动作。
?忽略此信号。
?自定义信号处理函数,当进程处理该信号时,由内核态切换到用户态时,就会执行该信号处理函数,这种方式叫做捕捉一个信号。
二,信号产生
通过终端按键产生信号
?上面实验的Ctrl+C控制键产生的是SIGINT信号,除此之外,Ctrl + 产生3号SIGQUIT信号。同样也可以试着对3号信号做捕捉。
void handler(int signo)
{
cout << "收到了 " << signo << " 号信号" << endl;
}
int main()
{
signal(SIGQUIT, handler);
while (true)
{
cout << "我是一个进程,我的pid : " << getpid() << endl;
sleep(1);
}
return 0;
}
SIGINT信号默认动作是终止进程,SIGQUIT信号默认动作是终止进程 + Core Dump(核心转储)。
Core Dump
当一个进程要异常终止时,可以选择把进程的用户空间内存数据转储到磁盘上,形成的文件名通常是core.pid,这就叫做Core Dump。异常终止通常是因为存在BUG,例如野指针的访问引起的段错误,或者除0引起的浮点数溢出等等。事后,可以通过调试器gdb查清错误的原因,这叫做事后调试。
?对于云服务器来说是关闭核心转储功能的
云服务器是一个生产环境,默认是关闭核心转储的,因为在生产环境中会上线某种服务,通常来说一个服务挂掉的时候,系统中有检测的机制,当监测到服务挂掉的时候,会重启服务,那么可想而知如果某个服务出现故障挂掉了而系统将其自动重启,刚重启又会挂掉,循环的挂掉重启挂掉…,那么就会有大量的core文件写入磁盘,并且这种循环的速度是惊人的,短时间内就可能写入大量的core文件,当磁盘被写满时就连操作系统都不能启动起来了,整个主机就会挂掉,所以生产环境通常是不开启核心转储功能的。
ulimit -a #查看系统特定资源上限
ulimit -c #设置core file文件的大小
实验:
将核心转储功能打开后,让进程发生除0错误,那么此时OS就会给该进程发送SIGFPE信号(浮点数异常)让进程终止,由于SIGFPE信号的行为是终止进程并且发生核心转储,所以实验的现象应为,发生除0错误的进程被终止并且形成Core.pid文件。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
int main()
{
int a = 10 / 0;
return 0;
}
?可以通过使用gdb并借助Core.pid文件进行事后调试
core-file core.pid #打开core文件定位错误位置
code dump标志位
?子进程终止后会变成僵尸进程,父进程可以通过进程等待的方式来释放子进程的资源避免内存泄漏,同时还可以获取子进程的退出信息。获取子进程的退出信息是通过一个输出型参数status来完成的,status是int型变量,status的低7为表示进程收到的终止信号,第8位是core dump表示位,表示是否发生了核心转储,次低8位表示进程的退出码,所以通过父进程waitpid等待子进程,获取子进程的退出信息,可以知道子进程是否发生了核心转储。
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
int a = 10 / 0;
exit(1);
}
// parent
int status = 0;
waitpid(id, &status, 0);
cout << "exit signal : " << (status & 0x7f) << " exit code : " << ((status >> 8) & 0xff)
<< " core dump : " << ((status >> 7) & 0x1) << endl;
return 0;
}
注意:
core dump标志位表示的是是否发生了核心转储,意味着如果进程收到core类型的信号,但是核心转储功能是关闭的,core dump标志位也不会被置1。
通过系统调用向进程发信号
kill
int kill(pid_t pid, int sig);
?该系统调用的功能就是给指定进程发送指定信号,第一个参数为进程的pid,第二个参数为向进程发送的信号,成功返回0,失败返回-1。
?kill指令就是通过调用kill函数完成的。
?kill pid 默认向进程发送15号信号。
模拟实现一个mykill指令:
void Usage(char *str)
{
cout << "Usage : " << str << " -pid -signo" << endl;
}
int main(int argc, char **argv)
{
if (argc != 3)
{
Usage(argv[0]);
return 1;
}
string str1 = argv[1] + 1; //+1 是将-过滤掉
string str2 = argv[2] + 1;
kill(stoi(str1), stoi(str2));
return 0;
}
通过mykill指令给一个死循环的进程发送SIGINT信号。
raise
int raise(int sig);
?只有一个参数为发送的信号,成功时返回0,失败是返回非0。
?与kill函数不同的是raise只能给当前进程发送信号,kill是可以给指定的进程发送信号。
abort
void abort(void);
?abort函数是让当前进程收到SIGABRT信号而异常终止。与exit函数一样,abort函数总是会成功的,所以没有返回值。
由软件条件产生信号
SIGPIPE
?在之前介绍使用管道来实现进程间通信时提到,如果管道的读端被关闭那么写端OS会杀死写端的进程,因为OS不会维护没有意义,低效率或者浪费资源的事情。OS就是向写端进程发送SIGPIPE信号,来杀死写端进程的。下面来验证以下:
void handler(int signo)
{
cout << "我是父进程,我收到了" << signo << "号信号" << endl;
exit(1);
}
int main()
{
int pipe_fd[2];
int n = pipe(pipe_fd);
if (n == -1)
{
perror("pipe");
exit(1);
}
pid_t id = fork();
if (id == 0)
{
// child
close(pipe_fd[1]);
char buffer[1024];
int cnt = 5;
while (cnt--)
{
int n = read(pipe_fd[0], buffer, sizeof(buffer) - 1);
buffer[n] = '