前言
本文是博主对学习完Linux系统中的进程信号部分的知识点总结,在阅读完该文章之后,我们会对进程信号有更深层次的理解。学习完本文后我们可以掌握以下内容:Linux信号的基本概念、掌握信号产生的一般方式、理解信号递达和阻塞的概念和原理、掌握信号捕捉的一般方式、重新了解可重入函数的概念、了解竞态条件的情景和处理方式、了解SIGCHLD信号, 重新编写信号处理函数的一般处理机制。
一、信号初识
1. 信号的概念
对于 Linux来说,实际信号是软中断,许多重要的程序都需要处理信号。信号为 Linux 提供了一种处理异步事件的方法。在Linux当中,每个信号都有一个名字和编号,这些名字都以“SIG”开头,例如“SIGIO ”、“SIGCHLD”等等。信号定义在signal.h头文件中,信号名都定义为正整数。具体的信号名称可以使用kill -l来查看信号的名字以及序号,信号是从1开始编号的,不存在0号信号。kill对于信号0又特殊的应用。
2. Linux中的普通信号
如上文所示,一共有62个信号,实际上缺少了32和33号信号。编号34以上的是实时信号,本文只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal。
以下是对Linux系统中31个普通信号的说明:
3. 信号的处理
信号的处理方式有三种,分别是:忽略、捕捉和默认动作。
- 忽略信号
大多数信号都可以采用这个方式进行处理,但有两种信号不能被忽略(即SIGKILL
和SIGSTOP
)。因为它们向内核和超级用户提供了进程终止和停止的可靠方法。如果忽略了,那么这个要处理的进程就变成了没人能管理的进程,显然是内核设计者不希望看到的场景。
- 捕捉信号
捕捉信号,就是需要告诉内核用户希望如何处理某一种信号,其实就是写一个信号处理的函数,然后将这个函数告诉内核。当该信号产生时,内核来调用用户自定义的函数,以此来达到处理信号的目的。
- 系统默认
对于每个信号来说,系统都对应由默认的处理动作,当发生了该信号,系统会自动执行。不过,对系统来说,大部分的处理方式都比较粗暴,就是直接杀死该进程。具体的信号默认动作可以使用man 7 signal
来查看系统的具体定义。
二、信号产生
1. 终端按键产生信号
对于下面的死循环程序,我们可以通过键盘的ctrl+c
进行终止:
#include <stdio.h> #include <unistd.h> int main() { while (1){ printf("hello signal!\n"); sleep(1); } return 0; }
当我们运行程序,在通过键盘ctrl+c
就终止了该进程。
但实际上除了按ctrl+c
之外,按ctrl+\
也可以终止该进程。
二者都是通过按键终止进程,又有什么区别呢?
通过man 7 signal 我们可以发现按ctrl+c实际上是向进程发送2号信号SIGINT,而按ctrl+\实际上是向进程发送3号信号SIGQUIT。查看这两个信号的默认处理动作,可以看到这两个信号的Action是不一样的,2号信号是Term,而3号信号是Core。
Term和Core都代表着终止进程,但是Core在终止进程的时候会进行一个动作,那就是核心转储。
核心转储的概念:
核心转储(core dump),在汉语中有时戏称为吐核,是操作系统在进程收到某些信号而终止运行时,将此时进程地址空间的内容以及有关进程状态的其他信息写出的一个磁盘文件。
在云服务器中,核心转储是默认被关掉的,我们可以通过使用ulimit -a
命令查看当前资源限制的设定。
我们可以发现第一行是core文件的大小,它的默认大小被设定为0,因此就相当于关闭了核心转储的功能。我们可以通过ulimit -c size
命令来设置core文件的大小。
此时我们再次运行程序,并用ctrl+\
终止。就会发现终止进程后会显示core dumped
。
并且会在当前路径下生成一个core文件,该文件以一串数字为后缀,而这一串数字实际上就是发生这一次核心转储的进程的PID。
核心转储的功能:
当我们的代码出错了,我们最关心的是我们的代码是什么原因出错的。如果我们的代码运行结束了,那么我们可以通过退出码来判断代码出错的原因,而如果一个代码是在运行过程中出错的,那么我们也要有办法判断代码是什么原因出错的。
当我们的程序在运行过程中崩溃了,我们一般会通过调试来进行逐步查找程序崩溃的原因。而在某些特殊情况下,我们会用到核心转储,核心转储指的是操作系统在进程收到某些信号而终止运行时,将该进程地址空间的内容以及有关进程状态的其他信息转而存储到一个磁盘文件当中,这个磁盘文件也叫做核心转储文件,一般命名为core.pid。
而核心转储的目的就是为了在调试时,方便问题的定位。我们用下面这段代码进行演示:
#include <cstdio> #include <unistd.h> int main() { printf("Hello Linux...\n"); sleep(3); int a = 1/0; return 0; }
该代码当中出现了除0错误,该程序运行3秒后便会崩溃。
使用gdb对当前可执行程序进行调试,然后直接使用core-file core
文件命令加载core文件,即可判断出该程序在终止时收到了8号信号,并且定位到了产生该错误的具体代码。
说明:
事后用调试器检查core文件以查清错误原因,这种调试方式叫做事后调试。
core dump 标志:
此时我们回忆前面所学习的【进程控制】中的获取子进程的退出码部分。
进程等待函数waitpid函数的第二个参数:
pid_t waitpid(pid_t pid, int *status, int options);
waitpid函数的第二个参数status是一个输出型参数,用于获取子进程的退出状态。status是一个整型变量,但status不能简单的当作整型来看待,status的不同比特位所代表的信息不同,具体细节如下(只关注status低16位比特位):
若进程是正常终止的,那么status的次低8位就表示进程的退出状态,即退出码。若进程是被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,即进程终止时是否进行了核心转储。
打开Linux的核心转储功能,通过以下的程序查看core dump 的标志位信息。代码中父进程使用fork函数创建了一个子进程,子进程所执行的代码当中存在野指针问题,当子进程执行到*p = 1000时,必然会被操作系统所终止并在终止时进行核心转储。此时父进程使用waitpid函数便可获取到子进程退出时的状态,根据status的第7个比特位便可得知子进程在被终止时是否进行了核心转储。
#include <iostream> #include <cstdio> #include <unistd.h> #include <sys/wait.h> #include <sys/types.h> using namespace std; int main() { pid_t id = fork(); if (id == 0) { //子进程 int *p = nullptr; *p = 1000; exit(1); } //父进程 int status = 0; waitpid(id, &status, 0); printf("exit code %d, sigo: %d, core dump flag: %d\n", (status >> 8) & 0xFF, (status >> 7) & 0x7F, (status >> 7) & 0x1); return 0; }
可以看到,所获取的status的第7个比特位为1,即可说明子进程在被终止时进行了核心转储。
因此,core dump标志实际上就是用于表示程序崩溃的时候是否进行了核心转储。
2. 系统调用发送信号
2.1 kill函数
当我们继续运行死循环程序,可以使用kill -信号 进程ID
的方式向进程发送信号。
实际上kill命令是通过调用kill函数实现的,kill函数可以给指定的进程发送指定的信号,kill函数的函数原型如下:
#include <signal.h> int kill(pid_t pid, int sig);
kill函数用于向进程ID为pid的进程发送sig信号,如果发送成功返回0,失败则返回-1。
此时我们可以之间模拟实现一个kill命令:
#include <iostream> #include <string> #include <unistd.h> #include <signal.h> using namespace std; static void Usage(const string& proc) { cerr << "Usage:\n\t" << proc << " signo pid" << endl; } int main(int argc, char *argv[]) { if(argc !=3) { Usage(argv[0]); exit(1); } if(kill(static_cast<pid_t>(atoi(argv[2])), atoi(argv[1])) == 1) { cerr << " kill: " << strerror(errno) << endl; } return 0; }
【注意】
此时要想执行mykill程序时不带路径,就需要我们提前导入环境变量:
2.2 raise函数
raise函数原型:
#include <signal.h> int raise(int sig);
raise函数用于给当前进程发送信号,发送成功返回0,发送失败则返回非0。例如,下列代码当中用raise函数每隔一秒向自己发送一个2号信号。
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int cnt = 0; void handler(int signo) { cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl; } int main() { signal(2, handler); while(true) { sleep(1); cnt++; raise(2); } return 0; }
运行结果就是该进程每隔一秒收到一个2号信号。
2.3 abort函数
abort函数的函数原型如下:
#include <stdlib.h> void abort(void);
abort函数使当前进程接收到信号而异常终止。就像exit函数一样,abort函数总是会成功的,所以没有返回值。
例如使用下面的程序,运行5秒后调用abort终止程序。
#include <iostream> #include <cstdlib> #include <unistd.h> #include <signal.h> using namespace std; int cnt = 0; void handler(int signo) { cout << "我是一个进程,刚刚获取了一个信号: " << signo << " cnt: " << cnt << endl; } int main() { signal(2, handler); while (true) { sleep(1); cnt++; if (cnt == 5) abort(); } return 0; }
【注意】
abort函数的作用是异常终止进程,exit函数的作用是正常终止进程,而abort本质是通过向当前进程发送SIGABRT信号而终止进程的,因此使用exit函数终止进程可能会失败,但使用abort函数终止进程总是成功的。
3. 由软件条件产生信号
3.1 SIGPIPE信号
SIGPIPE信号是一种由软件条件产生的信号,当进程在使用管道通信时,如果读端进程将读端关闭后,另一个进程还在不断向管道写入数据,那么此次写端进程就会收到SIGPIPE信号而终止程序。
例如使用下面的一段代码来模拟以上这种情况,创建管道进行父子进程间通信,其中父进程是读端,子进程是写端,但父进程关闭了自己的读端,那么此时子进程在向管道写入数据时就会收到SIGPIPE信号,进而被终止。
#include <iostream> #include <cstring> #include <unistd.h> #include <sys/wait.h> using namespace std; int main() { int fd[2] = {0}; if (pipe(fd) < 0) { cerr << "pipe error" << endl; exit(1); } pid_t id = fork(); if (id == 0) { //子进程 --- 写端 close(fd[0]); const char *msg = "父进程你好,我是子进程..."; int count = 5; while (count) { write(fd[1], msg, strlen(msg)); sleep(1); count--; } close(fd[1]); exit(0); } //父进程 --- 读端 close(fd[1]); close(fd[0]); //父进程关闭自己的写端,导致子进程写入会被操作系统终止 int status = 0; waitpid(id, &status, 0); cout << "子进程收到信号:" << (status & 0x7F) << endl; return 0; }
结果子进程收到13号信号,即SIGPIPE信号。
3.2 alarm函数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该函数的原型如下:
#include <unistd.h> unsigned alarm(unsigned seconds);
alarm函数的作用就是让操作系统在seconds秒之后给当前进程发送SIGALRM信号,, 该信号的默认处理动作是终止当前进程。
返回值:
- 若调用alarm函数之前,进程已经设置了闹钟,则返回上一个闹钟剩余的时间,并且本次闹钟会覆盖上次闹钟的设置。
- 若调用alarm函数之前,没有设置闹钟,则返回0。
以下是一个小例子,统计一个我们的进程1S cnt++ 多少次。
#include <iostream> #include <unistd.h> using namespace std; int cnt = 0; int main() { alarm(1); //设置1s while (true) { cnt++; printf("cnt = %d\n", cnt); } return 0; }
执行结果如下:
【Linux学习】进程信号2:https://developer.aliyun.com/article/1383970