Linux进程信号
Posted 阿亮joy.
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux进程信号相关的知识,希望对你有一定的参考价值。
🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根
目录
👉信号入门👈
信号是一种软件中断,信号在 Linux 操作系统 中提供了一种处理异步事件的方法,可以很好地在多个进程之间进行同步和简单的数据交互。注:信号和信号是两个东西,没有关系!信号只是用来通知某个进程发生了什么事情,但并不给该进程传递任何数据。
生活中的信号
在生活中,我们会收到很多信号,比如:红绿灯、闹钟、转向灯和狼烟等等。那我们为什么会知道这些生活中的信号呢?其实是我们曾经学习过有关这些生活信号的知识并且记住了对应场景下的信号。有关信号的推论如下:
- 当这些信号产生时,我们就能够识别这些信号,并且执行相应的动作。
- 当特定信号没有产生时,我们依旧知道应该如何处理这个信号。
- 当我们收到信号时,我们可能不会立即处理这个信号。
- 当我们无法立即处理信号的时候,信号也一定要先被临时地记住。
Linux信号
什么是 Linux 信号?Linux 信号本质是一种通知机制,用户或操作系统通过发送一定的信号,通知进程某些时间已经发生了,进程可以在后续进行信号处理。
- 进程要处理信号,那么进程必须具备信号识别的能力(收到信号加上相对应的信号处理动作)。
- 为什么进程能够识别信号呢?进程能够识别信号,肯定是设计操作系统的程序员将常见的信号及信号处理动作内置到进程的代码和属性中。
- 信号产生是随机的,当信号产生时,进程可能正在处理某些任务。所以,信号可能不是立即被进程处理的。
- 信号会被临时地记录下来,方便进程后续进行处理。
- 那进程会在什么时候处理信号呢?合适的时候。
- 一般而言,信号的产生相对于进程而言是异步的。异步指两个或两个以上的对象或事件不同时存在或发生(或多个相关事物的发生无需等待其前一事物的完成)。同步指两个或两个以上随时间变化的量在变化过程中保持一定的相对关系。
- 注:信号也有确定的信号,比如:定下闹钟的时间时,那么闹钟一定会在那个时间点响起来。
- 信号处理的常见方式:
- 默认(进程自带的处理动作,该动作是程序员写好的逻辑)
- 忽略(忽略也是信号处理的一种方式)
- 自定义动作(捕捉信号)
常见信号
kill -l #该命令可以查看常见的信号
man 7 signal #查看信号的相关描述
Linux 内核支持 62 种不同的信号,这些信号都有一个名字,这些名字都以三个字符 SIG 开头。在头文件siganl.h
中你能够,这些信号都被定义为正整数,称为信息编号。其中,编号 1 到 31 的信号称为普通信号,编号 34 到 64 的信号称为实时信号,实时信号对处理的要求比较高。
普通信号和实时信号的关系就像分时操作系统和实时操作系统的关系类似,分时操作系统是基于时间片轮转调度的,而实时操作系统要求要有严格的时序,可以认为是一个队列。将一个任务放入该队列中,那么操作系统就尽量快地将该任务处理完。日常生活中使用最多的就是分时操作系统,而实时操作系统常见于特殊的行业,如军工领域和自动驾驶领域等等。
组合键转化成信号
Ctrl + C 的本质就是给进程发送了 2 信号,进程接收到 2 号信号后的默认处理动作是结束进程。
那如何理解组合键变成信号呢?其实键盘的工作方式是通过中断方式进行的。键盘是槽位的,每个槽位都会对应一个编号。因为有键盘驱动,操作系统是能够识别这些编号的。只要按下了一些键,操作系统立马就能够识别到。那么当你按下组合键,操作系统也是可以识别到的。操作系统既然都识别到了你按下了组合键,那么操作系统给特定的进程发送信号,也就是轻而易举的事情了。
既然进程要接收操作系统发送过来的信号,那么进程必须要具有保存信号的相关数据结构,而该数据结构就是位图(unsigned int),使用比特位信息就可以表示操作系统是否有给进程发送信号。比如:最低位比特位为 1,则说明操作系统给该进程发送了 1 号信号;反之,则操作系统没有给该进程发送 1 号信号。注:该位图结构保存在进程的内核数据结构 task_struct 中,只有操作系统才能修改 task_struct。信号产生的方式有很多种,但其发送的本质就是操作系统向目标进程写信号,操作系统修改 task_struct 中的位图结构,完成信号发送的过程。
那么组合键能够转化成信号也就很好理解了。当你按下组合键 Ctrl + C 时,操作系统识别到该组合键并解释该组合键,然后查找到在前台运行的进程,最后操作系统将 Ctrl + C 对应的信号写入到进程内部的位图结构中就完成了信号发送。现在进程已经将操作系统发给它的信号记录下来了,进程就会在合适的时候处理该信号。
注意:
- Ctrl + C 产生的信号只能发给前台进程。一个命令后面加个 & 可以放到后台运行,这样 shell 不必等待进程结束就可以接受新的命令,启动新的进程。
- shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl + C 这种组合键产生的信号。
- 前台进程在运行过程中用户随时可能按下 Ctrl + C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous) 的。
👉信号产生👈
通过终端按键产生信号
在上面的内容已经提及到,按下组合键 Ctrl + C 可以前台进程发送 2 号信号,那我们可以通过 signal 函数来验证一下。
signal 函数的原型如下:
使用 signal 函数后,当进程接收到 signum 信号时,进程会调用 handler 函数(handler 是回调函数,handler 是 函数指针类型,该函数的返回值是 void,参数是 int)并将 signum 传递给 handler 函数,其实 signal 函数相当于可以自定义捕捉某个信号。signal 函数的返回值是对于 signum 信号的旧的处理方法。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void catchSignal(int signal)
cout << "捕捉到了一个信号: " << signal << endl;
int main()
// signal(2, catchSignal); // 这种写法也可以
// catchSignal是自定义捕捉
signal(SIGINT, catchSignal); // 特定信号的处理动作一般只有一个
while(true)
cout << "我是一个进程,我的pid是: " << getpid() << endl;
sleep(2);
return 0;
注:signal 函数仅仅是修改进程对特定信号的后续处理动作,并不是直接调用对应的处理动作。而是当进程接收到特定信号时,才会去调用对应的处理动作。如果后续没有产生 SIGINT 信号,catchSignal 函数就不会被调用,signal 函数往往放在最前面,先注册特定信号的处理方法。
现在就无法通过 Ctrl + C(2 号信号)终止该进程了,那么我们可以通过 Ctrl + \\ (3 号信号)终止该进程。如果你也将 3 号信号也自定义捕捉了,那么可以发生 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。
一般而言,云服务器(生产环境)的核心转储功能是关闭的。程序员写代码的环境称为开发环境,测试人员的环境是测试环境(测试 Realease 版本),产品上线后用户可以使用的环境就成为生产环境(有对应的服务器)。我们所购买的云服务器是集开发、测试、发布、部署于一体的机器。
打开云服务器的核心转储功能后,我们来验证一下是否真的会产生 core 文件。
注:只有核心转储才会生成 core 文件。
core 文件是以进程 ID 作为后缀,通常该文件是比较大的。生产环境一般会关闭核心转储功能是为了防止生成大量的 core 文件占用磁盘空间。如果磁盘中充满大量的 core 文件,可能会导致服务器无法重启或操作系统挂掉。
通过生成的 core 文件来进行 Debug
验证进程等待中的 core dump 标记位
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <cassert>
using namespace std;
// 验证进程等待中的core dump标记位
int main()
int id = fork();
// 子进程
if(id == 0)
sleep(2);
int a = 100;
a /= 0;
exit(0);
int status = 0;
int ret = waitpid(id, &status, 0);
assert(ret != -1);
(void)ret;
cout << "父进程: " << getpid() << " 子进程: " << id << " exit signal: " \\
<< (status & 0x7F) << " is core: " << ((status >> 7) & 1) << endl;
// 父进程
return 0;
将核心转储功能关闭,就不会生成 core 文件,core dump 标记位始终为 0;当进程不是收到核心转储信号终止进程的,也不会生成 core 文件,core dump 的标记位也始终为 0。
调用系统函数向进程发信号
通过系统调用实现 mykill 命令
系统调用 kill 函数可以想指定的进程发送指定的信号。
// mykill.cc
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <cstring>
#include <stdlib.h>
using namespace std;
static void Usage(string proc)
cout << "Usage:\\r\\n\\t" << proc << " -SignalNumber ProcessID" << endl;
// 通过系统调用向进程发送信号(设计mykill命令)
// ./mykill -2 pid
int main(int argc, char* argv[])
if(argc != 3)
Usage(argv[0]);
exit(1);
int signal = atoi(argv[1] + 1);
int id = atoi(argv[2]);
kill(id, signal);
return 0;
raise 函数可以给调用该函数的进程发信号,
raise(sig)
等价于kill(getpid(), sig)
。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
cout << "我正在运行中..." << endl;
sleep(2);
raise(8);
return 0;
abort 函数给调用该函数的进程发送6号信(SIGABRT)终止进程,6 号信号会引起核心转储,通常用来终止进程。就像 exit 函数一样,abort 函数总是会成功的,所以没有返回值。
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;
int main()
cout << "我正在运行中..." << endl;
sleep(2);
abort();
return 0;
如何理解通过系统调用向进程发信号?用户调用系统接口,执行操作系统对应的系统调用代码,操作系统提取参数或设置特定的数值(信号编号和进程 ID),操作系统向目标进程写信号(修改对应进程的位图结构),进程后续处理信号执行相应的处理动作。
由软件条件产生信号
学习管道的时候,我们说过:当管道读端关闭,写端一直在写,操作系统会自动终止对应的写端进程。操作系统是通过发送 13 号信号(SIGPIPE)来终止写端进程的!
那现在我们来按照一下步骤来验证一下!
-
- 创建匿名管道
-
- 让父进程进行读取,子进程进行写入
-
- 父子进程通行一段时间(该步骤可以省略)
-
- 让父进程先关闭读端,子进程只有一直写入就行
-
- 父进程通过 waitpid 等待子进程拿到子进程的退出信息
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <cassert>
#include <string>
#include <cstring>
using namespace std;
int main()
// 创建匿名管道
int pipefd[2] = 0;
int n = pipe(pipefd);
assert(n != -1);
(void)n;
cout << "创建匿名管道成功" << endl;
// 创建子进程
int id = fork();
if(id == 0)
// 子进程
// 关闭子进程的读端
close(pipefd[0]);
char send_buffer[128] = '\\0';
string s = "我是子进程,我正在给你发消息";
int count = 0;
while(1)
// 构造变化的字符串
snprintf(send_buffer, sizeof(send_buffer), "%s id:%d %d", s.c_str(), getpid(), count++);
write(pipefd[1], send_buffer, strlen(send_buffer));
sleep(1);
// 父进程
// 关闭父进程的写端
close(pipefd[1]);
char read_buffer[128] = '\\0';
int count = 0;
while(1)
ssize_t s = read(pipefd[0], read_buffer, sizeof(read_buffer) - 1);
if(s > 0)
read_buffer[s] = '\\0';
++count;
cout << "父进程:" << getpid() << " 收到消息" << read_buffer << endl;
else
cout << "写端已经关闭,读取结束" << endl;
break;
// 循环5次,关闭读端
if(count == 5)
close(pipefd[0]);
cout << "父进程的读端已关闭!" << endl;
break;
// 获取子进程的退出信息
int status = 0;
int ret = waitpid(id, &status, 0);
assert(ret != -1);
cout << "等待子进程成功 " << "子进程id:" << id << " signal:" << (status & 0x7F) << endl;
return 0;
父进程的读端已经关闭,子进程的写端再进行写入也没有任何的意义,那么操作系统就向子进程发送 13 号信号(SIGPIPE)。像管道的读端关闭写端还在写的这样情况,其实就是不符合软件条件(管道通信的条件,管道也是一种软件),那么操作系统就会向不符合软件条件的进程发送特定的信号终止进程。
alarm 函数可以设定一个闹钟,也就是告诉操作系统在 seconds 秒后给当前进程发送 14 号信号(SIGALRM),该信号的默认处理动作是终止当前进程。
这个函数的返回值是 0 或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为 30 分钟之后响,20 分钟后被人吵醒了,还想多睡一会儿。于是重新设定闹钟为 15 分钟之后响,以前设定的闹钟时间还余下的时间就是 10 分钟。如果 seconds 值为 0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
alarm(1);
int count = 0;
// 验证1s内,count++会进行多少次
// cout + 网络 = IO
while(true)
cout << "count: " << count++ << endl;
return 0;
通过上图可以看到,count 一定被加加了 7w+ 次,这次数是比较少的,其实是由 cout 和网络传输数据慢导致的。如果想单纯看看计算的算力,可以通过下面的程序。
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
unsigned int count = 0;
void catchSignal(int signal)
cout << "count:" << count << endl;
int main()
signal(SIGALRM, catchSignal);
alarm(1);
while(true)
++count;
return 0;
注:设定了一个闹钟,这个闹钟一旦被处罚,就会自动被移除。
下面的代码可以做到每隔一秒就发送 SIGALRM
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
unsigned int count = 0;
void catchSignal(int signal)
cout << "count:" << count << endl;
alarm(1);
int main()
signal(SIGALRM, catchSignal);
alarm(1);
while(true)
++count;
return 0;
以上的代码就简单地实现了定时器的功能,每隔一秒钟做指定的一件事。
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <functional>
#include <vector>
using namespace std;
unsigned int count = 0;
typedef function<void ()> func; // func为函数类型
vector<func> callBacks;
void showCount()
cout << "count:" << count << endl;
void showLog()
cout << "这个是日志功能" << endl;
void logUser()
// 创建子进程执行who命令
if(fork() == 0)
execl("/usr/bin/who", "who", nullptr);
exit(1);
wait(nullptr);
void catchSignal(int signal)
for(auto &func : callBacks)
func();
cout << "-------------------------" << endl;
alarm(1);
int main()
signal(SIGALRM, catchSignal);
alarm(1);
callBacks.push_back(showCount);
callBacks.push_back(showLog)以上是关于Linux进程信号的主要内容,如果未能解决你的问题,请参考以下文章