目录
- [1. 进程的异常终止与core dump标志位](#1. 进程的异常终止与core dump标志位)
-
- [1.1 进程终止的方式](#1.1 进程终止的方式)
- [1.2 core方案的作用与使用方式](#1.2 core方案的作用与使用方式)
- [2. 信号的保存](#2. 信号的保存)
-
- [2.1 信号的阻塞](#2.1 信号的阻塞)
- [2.2 操作系统中的sigset_t信号集类型](#2.2 操作系统中的sigset_t信号集类型)
- [2.3 进程PCB中修改block表的系统调用接口](#2.3 进程PCB中修改block表的系统调用接口)
- [2.4 信号阻塞的相关问题验证](#2.4 信号阻塞的相关问题验证)
1. 进程的异常终止与core dump标志位
1.1 进程终止的方式
进程的终止方式大体可以分为两种,正常终止 与异常终止 。这两种进程终止方式的不同会表现在进程的退出码上,进程终止退出时会返回一个int
类型的变量。此变量中只有低16bit位
会被利用起来存储信息,具体存储方式如下:
当进程正常结束,进程的退出码会写在次低8bit位(16 ~ 8之间)
的范围中(下标从0开始),退出码会表示进程退出的状态。而当进程被信号终止,异常退出时,退出码的低7bit位(7 ~ 0之间)
的范围内会写入进程收到的信号。
而信号异常终止的情况又被分为两种,分别为Term
方案与Core
方案处理:
term
方案(termination): 直接退出,然后进程释放其的占用的资源core
方案: 被称之为核心转储 ,将核心信息转移储存到生成的Core文件
中,此文件存储着进程异常终止的错误信息(发生了什么错误,导致错误发生的代码是哪一行) ,便于后续排查错误。
哪种信号导致的进程终止会使用哪个方案,可使用指令man 7 signal
浏览手册,查看具体信息。
退出码的第8个bit位,则是表示此进程的退出是否为core退出,值为1表示是,值为0表示否 。当core方案默认没有打开时,core退出的信号最后的退出方式也是term,core dumped
位为0。
观察异常退出进行的退出码:
cpp
//打开core方案
int main()
{
pid_t pid = fork();
if(pid == 0)
{
int a = 10;
a /= 0;
exit(0);
}
int status = 0;
pid_t rid = waitpid(pid, &status, 0);
if(rid == pid)
{
cout << "exit code : " << ((status >> 8) & 0xFF) << endl;//退出码 0
cout << "exit signal : " << ((status) & 0x7F) << endl; //退出信号 SIGFPE : 8
cout << "core dump : " << ((status >> 7) & 0x1) << endl; //core dump位 1
cout << "status : " << status << endl;
}
return 0;
}
1.2 core方案的作用与使用方式
以下进行试验与得出的结论都是基于Ubuntu 20.04 LTS
系统版本
core模式的打开与关闭:
云服务器中,core模式是被默认关闭的,在想要使用core退出之前,需要先将core模式打开。当前进行的版本中,必须先执行sudo bash -c "echo core.%p > /proc/sys/kernel/core_pattern"
指令,后续才能打开core模式。
- 指令
ulimit -a
: 查看core方案是否打开
- 指令
ulimit -c file-size
: 设置core文件的大小,当core文件大小不为0时,core模式就被打开了。旧版内核中,只有root用户才能够执行此条命令,而新版本内核中则没有此限制
- 指令
ulimit -c 0
: 将core文件的大小设置为0时,core模式就会被关闭
core退出后core文件的使用方式:
当进程因为收到core
方案退出的信号而终止后,会打印出错误类型,错误信息后带注释信息(core dumped)
。然后会生成一个core文件
,core文件一般会有两种,core
文件或core.pid
文件。
当进程core方案退出后,生成了对应的core文件后,我们就可以在Linux下的gdb
调试工具中,通过core-file core/core.pid
的方式,查看core文件中的异常错误信息。此种core文件协助调试的方式,被称之为事后调试。

云服务器默认与重启后关闭core退出的原因:
云服务器一般都是会一直运行的,以此来持续给客户端提供各种服务。当然,在此过程中会有程序异常中断的可能。
-
- 但因为一些服务程序的重要性,不能使其因为中断就一直停止运行。
-
- 所以,云服务器上的重要服务异常中断时,会使用软件方式让其自动重启,以求让其能够正常给大部分客户端提供服务。
-
- 可若是云服务器默认打开了core异常退出方案的设置,在不断重启程序 的过程中,就会导致生成大量的
core.pid
文件 ,最后就会使得服务器的磁盘被打满 ,服务器最后整个崩溃。
- 可若是云服务器默认打开了core异常退出方案的设置,在不断重启程序 的过程中,就会导致生成大量的
在版本的较新的内核中,会将core方案退出生成的文件都命名为core
。这样即使服务不断重启,每次生成的core文件也只会不断覆盖,core文件始终只存在一份。服务器也就不会因此而导致崩溃。
2. 信号的保存
概念补充:
- 信号被进程接收后进行处理,信号的处理也被称之为信号递达
- 信号从产生到递达之间的状态,被称为信号未决
2.1 信号的阻塞
1. 信号阻塞的概念:
操作系统中,进程可以选择对指定的信号进行阻塞 ,也可以称之为屏蔽 。被阻塞的信号,即使被进程接收保存,也不会被进程处理。如果一个信号被阻塞,则该信号永远也不会被递达,除非进程对指定信号解除阻塞 。进程PCB中存在着一个变量int block
,此变量实际上是一张位图,此位图中的bit位都代指着一种信号,bit位的位置表示信号的编号,bit位的内容则是标识着进程对此信号是否屏蔽 。若bit位的值是1,则表示进行了屏蔽,若值是0,则表示没有进行屏蔽 。
对应信号的处理方法,在进程PCB中也有对应的一个函数指针数组handler_t* handler
做存储与管理。再综合之前信号产生中的知识,进程存储信号的pending位图。可以得知,操作系统中,进程PCB对于信号的阻塞、保存、处理分别都有一张表用于描述与管理。
2. 信号的忽略与阻塞:
信号的阻塞与忽略并不是一种行为,对信号进行阻塞,进程是无法识别到有对应的信号到了,所以也就不会做出响应处理。而信号的忽略,则是进程已经识别到了信号,但对信号采用了忽略这一处理方式,所以,表现出的现象就是进程什么都没有做。总而言之,阻塞就是进程没有识别到信号,而忽略则是进程识别到了但什么都不做。
当信号被阻塞屏蔽时,此种信号在一段时间内大量的被发送给此进程。对于普通信号pending表中只会存储一个,在接触阻塞之后,进程只会对最近的一次信号做处理。而对于实时信号,在递达之前同种信号产生多次,则是会依次放在一个队列里,此处不做详细讨论。
2.2 操作系统中的sigset_t信号集类型
操作系统中,用户是不能对系统的内核资源直接做修改的,只能通过操作系统提供系统调用接口来间接达成修改的目的。block信号阻塞表就属于操作系统的内核资源,操作系统对此提供了专用的用户级数据类型sigset_t
与系统调用接口。sigset_t
也被称为信号集 。
此类型为操作系统提供的位图类型,创建一个此类型变量修改其中内容并配合相应的系统调用接口就可以达到对block表的修改,即控制进程阻塞哪些信号。此外,sigset_t
类型同样可以用于接收pending表中的内容,以此来达到让用户查看pending的目的。sigset_t
类型相关的系统调用接口具体如下:
- 1. 将
sigset_t
变量内容置0
cpp
#include <signal.h>
int sigemptyset(sigset_t* set);
- 2. 将所有bit位都设置为1(用于阻塞所有信号)
cpp
int sigfillset(sigset_t* set);
- 3. 将指定bit位设置为1(用于阻塞指定信号)
cpp
int sigaddset(sigset_t* set, int signo);
- 4. 将指定bit位设置为0(用于解除阻塞信号)
cpp
int sigdelset(sigset_t* set, int signo);
- 5. 用于查看信号集中是否存在某个信号(配合查看pending表)
cpp
int sigismember(const sigset_t* set, int signo);
2.3 进程PCB中修改block表的系统调用接口
cpp
#include <signal.h>
//signal mask信号屏蔽字
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
- 参数1
int how
: 系统内置宏参数,用于指定调用接口的模式
宏常量 | 作用 |
---|---|
SIG_BLOCK |
将 set 中的信号添加到当前信号屏蔽字中(阻塞这些信号) |
SIG_UNBLOCK |
将 set 中的信号从当前信号屏蔽字中移除(解除阻塞) |
SIG_SETMASK |
将当前信号屏蔽字直接设置为 set(完全覆盖) |
- 参数2
const sigset_t* set
: block表修改的新参考信号集 - 参数3
sigset_t* oldset
: 被修改前的就信号集
sigset_t信号集
在不同平台下的实现方式不同,此系统内置类型不支持cout流插入或printf直接打印。sigset_t信号集
被定义与修改好后,并没有直接达到修改block表的效果,需要配合sigprocmask接口
才能真的将数据设置进内核。
2.4 信号阻塞的相关问题验证
- 问题1:如果一个进程将所有信号都进行屏蔽,那么这个进程是否就无法被外部终止了
验证方式: 创建一个死循环的进程,使用系统调用接口让此进程屏蔽所有信号,最后让其运行起来。待其运行之后,再使用指令对此进程尝试一个一个发出所有信号,观察进程反应。进程运行时,可以循环打印自己的pending表,当表bit位为1时,表示着进程收到了信号,但信号并没有被处理,这代表信号成功被阻塞。
实验代码:
cpp
void PrintPending()
{
sigset_t sig;
sigemptyset(&sig);
sigpending(&sig);
for(int i = 31; i > 0; i--)
{
if(sigismember(&sig, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << " , pid is : " << getpid() << endl;
}
int main()
{
sigset_t sig;
sigemptyset(&sig);
for(int i = 1; i <= 31; i++)
sigaddset(&sig, i);
int n = sigprocmask(SIG_SETMASK, &sig, nullptr);
assert(n == 0);
(void)n;//如此强转,是因为在release版本下,变量n后续若是没有使用编译器会报警
cout << "signal mask success..." << endl;
while(true)
{
PrintPending();
sleep(1);
}
return 0;
}
验证结果:
进程运行后,使用指令c=1; while [ $c -le 31 ]; do kill -$c pid; echo kill -$c; let c++; sleep 1; done
发送信号,观察现象。
运行程序执行指令后,可以发现,9号信号被发送后成功递达,SIGKILL:9
号信号并不能被屏蔽。
跳过9号信号之后继续验证其他信号,由运行结果发现SIGSTOP:19
号信号也没有被屏蔽。而18号信号虽然能够正常屏蔽,但屏蔽18号信号之后又会导致一些已经被屏蔽的信号接触阻塞。
操作系统如此设置的原因,是为了预防出现非法的病毒进程将自己的所有信号都设置阻塞,导致操作系统无法杀死病毒进程。
问题2:操作系统中进程在进行递达操作时,是先将pending表中的信号bit位清0,还是先执行handler处理函数
验证方法: 自定义一个信号的递达处理函数handler,让其在handler函数查看当前进程的pending表中的对应信号bit位。若pending表中的对应信号bit位清0了,代表清0操作于handler方法之前执行,若bit位置未清0,则代表清0操作于handler方法之后。
验证代码:
cpp
void handler(int signo)
{
sigset_t sig;
sigemptyset(&sig);
sigpending(&sig);
if(sigismember(&sig, signo))
{
cout << "先执行handler" << endl;
}
else
{
cout << "先清0" << endl;
}
}
int main()
{
signal(SIGINT, handler);
while(true)
{
cout << "process pid is : " << getpid() << endl;
sleep(1);
}
return 0;
}
验证结果:先清0