Linux系统之----信号
1.信号的引入
在生活中有很多信号,例如红绿灯,闹钟,鸣笛声等等。。。这些信号的作用就是通知事件产生的!这些信号,都是异步产生的,即信号到达的时刻与你当前在执行的代码路径没有任何先后顺序约定。说人话就是:你永远不知道 下一条指令 和 下一个信号 谁先来到。那么为什么我们接收到信号就要知道该做什么呢?比如看到红灯就不能继续过马路了,闹铃响铃了,就不能睡觉了,原因就是我们都被“教育”过,知道这些信号代表的什么,那也就是说,我们是通过信号特征和信号处理方法来记住这些信号的!
同理,在计算机内部,也有信号的存在,在计算机中,进程和信号都是程序员写的,进程内部已经内置了对信号的识别,和处理机制。
2.Linux系统中的信号
看似有64个信号,实际62个,其中1-31我们称为普通信号,34-64我们称为实时信号!这些信号都有一个共同的,那就是他们都是宏!都是这样定义的:#define SIGHUP 1
对于具体的信号的作用,我们可以这样看:man 7 signal
之后/standard signal搜索可以查看到~
3.信号的处理
首先我们要明白一点,就是信号在到来时,我们可能有更重要的事情要做,所以信号处理的过程,可能不是立即处理的~,但是终究要处理的!总共有3种方式来处理信号,分别为 1)默认动作 2)忽略信号 3)自定义捕捉
我们写如下代码:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>int main()
{while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
ctrl c后进程被杀掉了,说明ctrl c是一个信号
那我们如何忽略ctrl c这个信号,让其不起作用呢?这里用signal函数,第一个参数全称叫signal interupt,第二个叫做signal ignore
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{signal(SIGINT/*2*/,SIG_IGN);while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
运行之后,ctrl c杀不掉,只能kill -9杀掉!
3)我们将其恢复为默认:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
int main()
{//signal(SIGINT/*2*/,SIG_IGN);signal(SIGINT/*2*/, SIG_DFL);while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
第二个参数代表signal default,默认
最后我们在看一下自定义方式:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signal)
{std::cout << "我这个进程: " << getpid() << ", 抓到了一个信号: " << signal << std::endl;
}
int main()
{//signal(SIGINT/*2*/,SIG_IGN);//signal(SIGINT/*2*/, SIG_DFL);signal(2,handler);while (true){std::cout << "我是一个进程: " << getpid() << std::endl;sleep(1);}return 0;
}
对于signal函数的讲解,可以看一下老师的板书:
4.信号的本质
信号不会立即被处理,进程需要记录下来以便后续处理。记录信号的方式是通过一个位图(bitmap),每个信号对应位图中的一个位。位图中的每个位可以是0或1,表示是否收到该信号。
下面还有几个小问题:
问题1:
如何理解给进程发送信号?
答:
只需要修改目标进程的task_struct信号位图的特定位0->1,即可。本质就是向目标进行写信号!!
task struct是内核的数据结构对象!修改位图,本质是修改内核数据!只有操作系统有资格修改内核数据!!
问题2:
进入如何部分识别信号?
答:
通过位图对应的位置,是0还是1
结论: 无论信号发送的方式有多少种,最终,全部都是通过OS向目标进程发送信号的!!
5.信号产生
5.1键盘产生信号(用户交互)
用户可以通过键盘输入特定的组合键来产生信号。例如,按下Ctrl + C
通常会产生SIGINT
信号,这个信号用于请求前台进程组终止运行。它是用户从终端发送给进程的最常见的信号之一,通常用于中断正在运行的命令或程序。Ctrl + \
通常用于产生 SIGQUIT
信号。这个信号会终止程序用于请求进程终止。我们设计代码看一下:
#include<iostream>
#include<cstdio>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int signal)
{printf("\nReceived SIGINT signal. Exiting gracefully.\n");exit(0);
}
int main()
{// std::cout<<"hello";signal(SIGINT,handler);printf("Program is running. Press Ctrl+C to send SIGINT signal.\n");while(1){sleep(1);std::cout<<getpid()<<std::endl;}return 0;
}
运行一下:
如此,证明了键盘产生了信号,并且还被我捕捉了!
但是也有一些不可被捕捉的信号,如9和19号信号,下面我们设计代码证明一下:
#include<iostream>
#include<cstdio>
#include <signal.h>
#include <stdlib.h>
#include <unistd.h>
void handler(int signal)
{std::cout<<"收到信号:"<<signal<<std::endl;}
int main()
{for(int sig=1;sig<=31;sig++)signal(sig,handler);while(1){sleep(1);std::cout<<getpid()<<std::endl;}return 0;
}
感兴趣的可以自己挨个信号试一下,验证一下,这里仅验证一下19号信号~
5.2 kill命令产生
我们man 2 kill 查看一下:
实际上,我们Linux命令行kill -signum pid 调用的就是这个函数!
简单看一个代码:
#include<iostream>
#include <cstdio>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void signal_handler(int sig) {std::cout<<sig<<std::endl;exit(0);
}int main() {signal(SIGTERM, signal_handler); // 设置信号处理程序printf("Process will be killed after 5 seconds\n");sleep(5);kill(getpid(), SIGTERM); // 发送SIGTERM信号给自身return 0;
}
运行一下:
5.3 系统调用
我们直接看代码:
#include<iostream>
#include <cstdio>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>void signal_handler(int sig) {std::cout<<sig<<std::endl;exit(0);
}int main() {signal(SIGSEGV, signal_handler); // 设置信号处理程序int *ptr=nullptr;std::cout<<"Attempting to access invalid memory address..."<<std::endl;*ptr=4;// 尝试访问无效内存地址std::cout<<"wrong"<<std::endl;return 0;
}
运行:
#include<iostream>
#include <cstdio>
#include <stdlib.h>
#include <signal.h>
using namespace std;
void signal_handler(int sig) {cout<<sig<<endl;
}int main() {signal(SIGABRT, signal_handler); // 设置信号处理程序printf("Process will be aborted after 5 seconds\n");sleep(5);abort(); // 产生SIGABRT信号return 0;
}
5.4 abort
函数、raise函数
我们看一下代码:
#include<iostream>
#include <cstdio>
#include <stdlib.h>
#include <signal.h>
using namespace std;
void signal_handler(int sig) {cout<<sig<<endl;
}int main() {signal(SIGABRT, signal_handler); // 设置信号处理程序printf("Process will be aborted after 5 seconds\n");sleep(5);abort(); // 产生SIGABRT信号return 0;
}
运行结果:
5.5 alarm函数
它用于设置一个定时器,当定时器到期时,它会向进程发送 SIGALRM
信号。这个函数通常用于实现简单的超时机制。
返回值:如果成功,返回之前设置的定时器剩余时间(以秒为单位)。如果失败,返回 0。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>void handle_alarm(int sig) {printf("Caught SIGALRM\n");exit(0);
}int main() {// 设置信号处理函数signal(SIGALRM, handle_alarm);printf("Setting alarm for 5 seconds...\n");alarm(10); // 设置定时器为5秒//alarm(1);// 执行其他任务,等待定时器到期while(1) {sleep(1);printf("Waiting for alarm...\n");}return 0;
}
5.5.1 对alarm的管理
在操作系统中,对闹钟(alarm)的管理方案通常采用最小堆(或优先队列)来实现。最小堆是一种特殊的二叉树,其中每个节点的值都小于或等于其子节点的值,这使得最小堆的根节点(堆顶)总是包含最小值。这种数据结构非常适合用于定时器管理,因为它可以快速地找到最近到期的定时器,并且支持快速的插入和删除操作。
最小堆的使用在定时器管理中至关重要,因为它可以高效地检测和处理超时的闹钟。操作系统定期检查堆顶的闹钟,判断是否已超时。如果堆顶的闹钟超时,操作系统会从堆中删除这个闹钟,并执行相应的处理(如发送信号)。
此外,最小堆在定时器的触发是由于时间到了,因此只有时间最短的定时器会首先被触发,通过这个原理,我们可以采用最小堆,将按时间顺序排序,堆顶元素是时间最短的定时器,因此只要判断堆顶元素是否被触发即可。最小堆的这种特性使得它在处理大量定时任务时,如libevent、go、libev等,能够提供较高的性能。
在实现上,最小堆通过数组来组织其中的元素,与用链表表示堆相比,数组表示堆不仅节省空间,而且更容易实现堆的插入、删除等操作。最小堆只关注父子的大小,不关注兄弟的大小。增加节点只有上升操作,删除节点有上升和下降,个人理解不管是删除还是增加,都是要先对树进行操作,而后维护树的正常秩序。