(一)通过终端按键产生信号
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,现在我们来验证一下。
我们之前学习进程等待的时候,给大家介绍了以下这张图片,其中【core dump】没有讲,今天我将给大家解释这个词的含义。
【解释说明】
- 首先解释什么是Core Dump。Linux提供了一种能力,当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump(在云服务器上默认是关闭这个功能的!!!);
- 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试);
- 一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中);
- 默认是不允许产生core文件的,因为core文件中可能包含用户密码等敏感信息,不安全;
- 在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K: ulimit -c1024
命令用于显示有关用户当前资源限制(ulimits)的信息。此命令显示各种系统资源的硬限制和软限制,以下是一个示例输出:
【解释说明】
- core文件大小: core转储的最大大小(以块为单位)。
- 数据段大小: 进程的数据区大小(以千字节为单位)。
- 文件大小: 文件的最大大小(以块为单位)。
- 最大锁定内存: 锁定在内存中的地址空间的最大大小(以千字节为单位)。
- 最大内存大小: 进程可以拥有的最大数据大小(以千字节为单位)。
- 打开文件数: 进程可以拥有的最大文件描述符数量。
- 堆栈大小: 进程的堆栈大小(以千字节为单位)。
- CPU时间: 进程可以消耗的最大CPU时间(以秒为单位)。
- 最大用户进程数: 用户可以创建的最大进程数。
- 虚拟内存大小: 进程可用的虚拟内存大小(以千字节为单位)。
【注意】
- 值为“无限制”表示该特定资源没有设置特定限制;
- 可以使用 ulimit 命令或通过修改配置文件来调整这些限制。请注意,特定资源及其限制可能因操作系统和系统配置而异。
接下来,我们手动设置核心转储文件的大小。具体如下:
【解释说明】
- 上述命令用于设置核心文件的最大大小为10240个块(通常每个块大小为字节,具体大小取决于系统设置);
-c
: 表示设置或显示核心文件的最大大小限制。10240
: 表示核心文件的最大大小限制为10240个块。如果以字节计算,这意味着核心文件的最大大小为10240个块乘以每个块的字节数。
- 接下来,我们查询 signal信号。发现信号后面的有的是【term】,有的是【core】。具体如下:
【解释说明】
- 【term】 是一个信号名称,它代表着进程正常终止信号,无其他操作;
- 【core】通常指的是在进程异常终止(如段错误)时生成的核心转储文件。这个文件包含了进程在崩溃时的内存映像,可以用于调试和分析问题。当一个进程崩溃时,操作系统通常会生成一个名为
core
的文件,其中包含了进程在崩溃瞬间的内存状态。
接下来,我们就去验证上述结论:
- 首先,这里有几行简单的代码:
int main(int argc, char *argv[]) { while (true) { cout << "我是一个正常运行的进程:" << getpid() << endl; sleep(1); } return 0; }
- 紧接着,我们先让程序正常的跑起来:
- 程序可以正常运行之后,接下来,我先测试上述信号中后面标志为【term】的,看测试效果:
- 接下来,我先测试上述信号中后面标志为【core】的,看测试效果:
- 当我们打开这个文件时,发现全是乱码(因为这是给OS看的,不是给用户看的):
ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具 有和Shell进程相同的Resource Limit值,这样就可以产生Core Dump了。 使用core文件:
(二)调用系统函数向进程发信号
首先在后台执行死循环程序,然后用kill命令给它发SIGSEGV信号
【解释说明】
- 3995是test进程的id。之所以要再次回车才显示 Segmentation fault ,是因为在3995进程终止掉 之前已经回到了Shell提示符等待用户输入下一条命令,Shell不希望Segmentation fault信息和用 户的输入交错在一起,所以等用户输入命令之后才显示。
- 指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -SIGSEGV 3995或 kill -11 3995, 11是信号SIGSEGV的编号。以往遇 到的段错误都是由非法内存访问产生的,而这个程序本身没错,给它发SIGSEGV也能产生段错误
kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
- 函数的原型如下:
#include <signal.h> int kill(pid_t pid, int sig);
pid_t pid
: 要发送信号的进程的进程ID。如果pid
为正数,信号将发送到具有该进程ID的进程。如果pid
为0,信号将发送到与调用进程属于同一进程组的所有进程。如果pid
为-1,信号将发送到调用进程有权限发送信号的所有进程。int sig
: 要发送的信号的编号。
而raise函数可以给当前进程发送指定的信号(自己给自己发信号)
- 函数的原型如下:
#include <signal.h> int raise(int sig);
- int signo:要发送的信号的编号。
abort函数使当前进程接收到信号而异常终止
- 函数的原型如下:
#include <cstdlib> void abort(void);
使用abort函数非常简单,只需在需要终止进程的地方调用它即可。当调用abort函数时,以下操作将被执行:
- 向当前进程发送
SIGABRT
信号。 - 默认情况下,
SIGABRT
信号会导致进程终止,并生成一个核心转储文件。 - 终止处理程序会被启动,这是一个特殊的信号处理程序,可以用来执行一些清理工作或记录错误信息。
- 如果没有安装终止处理程序,或者终止处理程序调用了
_Exit
函数或返回,则进程会异常终止,并打印一条错误消息到标准错误流(stderr)。
代码展示:
void cleanup() { std::cout << "Performing cleanup before aborting..." << std::endl; // 执行一些清理工作 } void handler(int signo) { std::cout << "Received signal " << signo << std::endl; // 自定义信号处理逻辑 exit(signo); } int main() { // 注册终止处理程序 if (atexit(cleanup) != 0) { std::cerr << "Failed to register cleanup function" << std::endl; exit(EXIT_FAILURE); } // 注册信号处理函数 if (signal(SIGABRT, handler) == SIG_ERR) { std::cerr << "Failed to register signal handler" << std::endl; exit(EXIT_FAILURE); } std::cout << "Starting program..." << std::endl; std::cout << "Triggering abort..." << std::endl; // 调用abort函数,触发进程终止 abort(); std::cout << "This line will not be reached" << std::endl; return 0; }
输出展示:
【解释说明】
- 在调用abort函数后,程序收到了
SIGABRT
信号,并执行了注册的终止处理程序和信号处理函数; - 就像exit函数一样,abort函数总是会成功的,所以没有返回值
(三)由软件条件产生信号
SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数 和SIGALRM信号。
#include <unistd.h> unsigned int alarm(unsigned int seconds); 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
- 接下来,我简单的演示一下这个函数:
//io的效率低下 int main() { alarm(1); int count = 0; while (true) { // 打印,显示器打印网络 std::cout << "count : " << count++ << std::endl; //1s之内计算机将一个正数累计到多少 } return 0; }
- 多跑几次程序我们可以发现打印出来的结果都是不同的:
- 当我们此时真正想测试计算机的算力时,我们可以像如下这样:
int count = 0; void myhandler(int signo) { std::cout << "get a signal: " << signo << " count: " << count << std::endl; exit(0); } int main(int argc, char *argv[]) { signal(SIGALRM,myhandler); alarm(1); while (true) count++; return 0; }
- 多跑几次程序我们可以发现打印出来的结果跟上完全是天差地别:
- 上述 alarm 这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数;
- 打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟;
- 如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
接下来,我们代码简单的演示一下:
void myhandler(int signo) { std::cout << "get a signal: " << signo << " count: " << count << std::endl; int n = alarm(10); std::cout << "return: " << n << std::endl; } int main(int argc, char *argv[]) { std::cout << "pid: " << getpid() << std::endl; signal(SIGALRM,myhandler); alarm(10); while(true) { sleep(1); } return 0; }
输出展示:
(四)硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
(五)小结
以上便是本文的主要内容,接下来简单小结本文都讲了些什么!!!
在Linux中,信号可以通过多种方式产生,包括:
- 硬件异常:这些是由硬件引起的异常事件,例如:
- 除零错误(SIGFPE): 当程序尝试执行除以零的操作时,会产生
SIGFPE
信号。 - 无效内存访问(SIGSEGV): 当程序尝试访问未分配给它的内存地址时,会产生
SIGSEGV
信号。 - 内存访问越界(SIGBUS): 当程序尝试访问无效的内存地址时,会产生
SIGBUS
信号。
- 软件中断:这些是由软件引发的事件,通常是为了通知进程已经达到了某个预定条件,例如:
- 定时器超时(SIGALRM): 当一个定时器达到设定的时间时,会产生
SIGALRM
信号。
- 其他进程发送信号:一个进程可以通过系统调用向另一个进程发送信号,例如:
- kill 命令: 通过命令行的
kill
命令或者在程序中使用kill
函数可以向指定的进程发送信号。 - 终端操作: 用户在终端中执行特定的操作,例如按下 Ctrl+C 组合键,会向当前前台进程发送
SIGINT
信号。
- 进程自身发送信号:进程可以通过调用
raise
函数或者kill
函数向自身发送信号。
- 调用 kill 函数: 进程可以使用
kill
函数向自身发送信号,使用进程ID为getpid()
。
- 软件条件满足: 在程序中,当特定的条件满足时,可以使用信号来通知其他部分程序执行某些动作,这通常需要由程序本身显示地触发。
以上便是本文的全部内容了,感谢大家的观看和支持!!!