一、信号基础知识
1、信号是什么
在日常生活中,我们在遇到十字路口过马路,我们知道红灯停绿灯行。为什么我们能够知道红灯停绿灯行呢?这不难理解,这些规则都刻画在我们的脑海中了,也就是说红灯其实就是一种信号,当我们识别红灯信号,大脑就会做出反应控制身体不动,当然有时候会人闯红灯,信号肯定是会传递了的,也就是说其实信号也是可以被忽略的。
那在Linux下的信号又是指的是什么
下面我们见一个现象
这里我们运行程序,当我们按下Ctrl+c将的时候,我们发现进程退出。
这也就说明,操作系统肯定给进程发送了一个信号。
操作系统发送一个SIGINT信号给当前正在运行的进程。SIGINT信号表示一个中断请求,它的默认行为是终止进程。
当你按下Ctrl+C时,终端会捕获这个键盘事件,并转发一个SIGINT信号给当前在前台运行的进程。这个信号告诉进程有一个中断请求,通常意味着用户希望终止该进程。
2、信号的定义
信号是一个用来表示事件或消息的物理量。在操作系统中,信号是一种软件中断,用于通知进程发生了某个事件。这种事件可能是硬件异常、用户键入特殊的终端控制字符,或者其他进程发送给该进程的消息。每个信号都有一个唯一的正数描述和一个符号名。例如,SIGINT是用于表示中断的信号,其编号通常是2。信号可以用于进程间的简单通信,也可以用于处理异步事件
在操作系统下我们可以通过:
//查看系统定义的命令 kill -l
在Linux中总共62共信号
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2
- 编号34以上的是实时信号,这里只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下 产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal
1. //查看7号手册 2. man 7 signal
3、信号的处理方式
在Linux中,信号处理的常见方式包括以下几种:
- 忽略信号:进程可以选择忽略某些信号,即当这些信号发生时,进程不会执行任何操作。这种方式可以用于屏蔽一些不必要的信号干扰。
- 默认行为:对于每个信号,系统都定义了一个默认行为。如果进程没有为某个信号设置自定义处理函数,那么信号发生时将会执行默认行为。默认行为可以是终止进程、忽略信号、停止进程等。
- 自定义处理函数:进程可以通过信号处理函数来捕获信号,并在信号发生时执行自定义的操作。这种方式常用于实现一些特定的行为,比如在接收到某个信号时进行日志记录、清理资源等。进程可以使用
signal
函数或者sigaction
函数来设置信号处理函数。
二、有关信号操作的函数
1、signal函数(捕捉信号)
功能:用于捕捉信号并执行相应的处理操作
原型: sighandler_t signal(int signum, sighandler_t handler);
参数 :
signum:要捕捉的信号编号
handler:指定一个函数指针,它指向的信号处理函数将在接收到指定的信号时被调用
返回值:
signal
函数返回之前为sig
信号设置的处理函数的地址
typedef void (*sighandler_t)(int);
这里我们要特别 sighandler_t其实是一个函数指针。
signal
函数的第二个参数handler
是一个指向信号处理函数的指针,也就是sighandler_t
类型的函数指针。这个参数用于指定当信号发生时应该执行的函数。
使用handler
参数的方式如下:
- 自定义信号处理函数:首先,你需要定义一个符合
sighandler_t
类型的函数,即接受一个整数参数(信号编号)并返回void
的函数。这个函数将用于处理信号。例如:
void my_handler(int signal_num) { // 处理信号的代码 }
- 设置信号处理函数:使用
signal
函数来设置信号的处理函数。将信号编号和自定义的处理函数作为参数传递给signal
函数。例如,为了设置SIGINT信号(通常由Ctrl+C发送)的处理函数为my_handler
signal(SIGINT, my_handler);
注意:
- 处理函数通常会根据信号编号采取不同的操作。因此,在处理函数中,你可以使用传递的信号编号来判断是哪个信号被接收。
- 如果你将
handler
设置为SIG_IGN
,那么信号将被忽略。- 如果你将
handler
设置为SIG_DFL
,那么将采取信号的默认行为。
举例用法:
#include<iostream> #include<unistd.h> #include<signal.h> using namespace std; void my_handler(int signal_num) { cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl; } //测试 int main() { signal(SIGINT,my_handler); while(1) { cout<< "我是一个进程,我正在运行" <<endl; sleep(1); } return 0; }
这里我们发现,当我们在按Ctrl+ c,进程不再中止,而是去执行my_handler函数中的操作,singal也捕获了2号命令。(这里我们是用kill -9 pid中止进程的) 。
2、kill函数
功能:用于向指定进程发送信号
原型: int kill(pid_t pid, int sig)
参数 :
pid:数指定要发送信号的进程ID
sig:参数指定要发送的信号类型
返回值:当调用成功时,kill函数返回0;否则,返回-1并设置errno来表示错误原因
kill函数可以发送多种类型的信号,例如:
SIGINT
:中断进程。通常是通过用户按下Ctrl+C来产生的。SIGTERM
:终止进程。这是一个终止进程的请求,进程可以捕获该信号进行清理操作,然后退出。SIGKILL
:强制终止进程。这个信号会直接终止进程,进程无法捕获或忽略它。
3、raise函数
功能:用于发送信号给当前进程
原型:int raise(int sig)
参数 :
sig:参数指定要发送的信号类型
当调用raise
函数时,它会向当前进程发送指定的信号。如果信号处理程序已经注册了对应的信号,它将被调用进行处理。否则,默认的信号处理动作将被执行,可能会导致进程终止、停止或忽略信号。
举例:
#include<iostream> #include<unistd.h> #include<signal.h> #include<sys/types.h> using namespace std; void my_handler(int signal_num) { cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl; } //测试 int main() { //注册信号处理程序 signal(SIGINT,my_handler); cout<<"SIGINT to slef"<<endl; //发送SIGINT给当前进程 raise(SIGINT); cout<<" signal after" <<endl; return 0; }
4、abort函数
功能:用于异常终止程序的函数
原型:void abort(void);
调用abort函数将导致程序异常终止,并返回一个非零的退出码给操作系统,以指示程序异常结束。
由于abort函数会立即终止程序,所以它通常会在发现严重错误的情况下使用,例如内存泄漏、无效参数等。它向程序员提供了一种机制,用于在无法恢复正常执行流程的情况下紧急停止程序。
然而,由于abort函数不进行任何清理操作,因此不建议在正常情况下使用它来结束程序。在大多数情况下,应该尝试通过其他方式恢复程序的执行,或者使用更适当的函数来进行正常的程序终止,如exit函数。
三、信号的产生
1、通过终端按键产生信号
通过终端按键产生信号是指用户在终端(命令行界面)按下特定的按键组合,向当前进程发送一种信号。这种信号可以是一种通知或请求,告诉进程执行某种特定的操作。
在Unix和类Unix系统中,终端按键可以产生信号,这些信号可以与进程进行交互。例如,当用户按下 Ctrl+C 组合键时,会向当前正在运行的进程发送一个 SIGINT 信号。这个信号的默认处理动作是终止进程。
类似地,还有其他按键组合可以发送不同的信号,比如 Ctrl+\ 会发送 SIGQUIT 信号,默认处理动作也是终止进程,并生成 core dump 文件。
这种通过终端按键产生信号的方式,提供了一种用户与进程交互的手段,用户可以通过这种方式控制进程的行为,比如终止进程、暂停进程等。同时,进程也可以根据自身需要对这些信号进行处理,比如忽略信号、自定义处理函数等。
那么core dump (核心转储)又是指的什么呢?
首先解释什么是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
这里本系统出于上面所说的安全等因素考虑core file size文件数量是0
2、调用系统函数向进程发信号
这里我们可以通过系统函数kill,raise,abort函数来向进程发送我们想要的信息
int cnt = 0; while(cnt <= 10) { printf("cnt : %d ,pid: %d\n",cnt++,getpid()); sleep(1); // if(cnt >= 5) abort(); if(cnt >= 5) raise(9); }
调用abort终止程序
调用raise(9)终止程序
3、硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程。
除以0的指令
当我们在编写的程序进行除0操作,为什么就会发送8好信号终止进程呢?
通过下图进行理解:
当我们进行除0操作,在CPU中有一个专门用来检测运算的,状态寄存器,里面有一个溢出标记位当除0时,标记位就会溢出,从而被操作系统知晓,发送8好信号给进程。
非法内存地址
OS怎么知道呢??我野指针了呢?,为什么程序中有野指针就会崩溃?
我们知道,其实指针指向的是虚拟地址,通过页表映射到物理地址,当指针越界访问的时候,操作系统就可以察觉到,从而对进程发送11号信号终止进程。
void my_handler(int signal_num) { cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl; sleep(1); } //测试 int main(int argc, char *argv[]) { //硬件异常产生信号 signal(11,my_handler); while(true) { std::cout << "我在运行中...." << std::endl; sleep(1); int *p = nullptr; //写一个野指针 *p = 1; } return 0; }
4、由软件条件产生信号
在理解软件条件产生信号之前,我们需要理解allarm函数
头文件:#include unsigned
类型:int alarm(unsigned int seconds);
用途:调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动 作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后 响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就 是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数
void my_handler(int signal_num) { cout<< "接收到的信号码: "<<signal_num << " "<<"my_pid: "<< getpid()<<endl; exit(1); } //测试 int main(int argc, char *argv[]) { //4、软件异常产生信号 int cnt = 0; signal(SIGALRM,my_handler); //统计1秒数据能够累计到多少 alarm(1); while(true) { cnt++; cout<< cnt <<endl; } return 0; }
这里是没有用signal捕捉信号
这里是用singal捕捉了14号信号
从现象上来看,当调用alarm函数,在经过设置的时间后,会给进程发送14号信号。
总结思考
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
这是因为操作系统是计算机硬件和软件之间的中介,负责管理计算机的资源,并提供一个运行环境,使得程序能够在计算机上正确、高效地运行。
OS是进程的管理者 信号的处理是否是立即处理的?
信号的处理并不一定是立即的,它取决于信号的类型以及接收进程的状态
在合适的时候 信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?
是的,如果一个信号在适当的时候不能立即处理,操作系统通常会将信号暂时记录下来,以便稍后处理。这种记录信号的机制通常称为信号队列(Signal Queue)。
信号队列允许操作系统将接收到的信号排队,而不会丢失它们。每个进程都有一个相关联的信号队列,用于存储接收到的信号。当信号到达时,它会被添加到接收进程的信号队列中。接收进程可以随后检查队列中的信号并决定如何处理它们。
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
一个进程可以预先定义信号的处理方式,无论是否收到信号。这就是通过设置信号处理程序(Signal Handler)来实现的。信号处理程序是一段特定的代码,它规定了进程在接收特定信号时应采取的操作。
进程可以使用操作系统提供的系统调用(如signal()
、sigaction()
等,具体取决于操作系统和编程语言)来注册信号处理程序。一旦设置了信号处理程序,当进程接收到相应的信号时,操作系统将执行信号处理程序中定义的操作。
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
操作系统(OS)可以向进程发送信号,这是一种进程间通信的机制,用于通知接收进程发生了某些事件或条件。下面是完整的发送信号的处理过程:
- 信号产生:信号的产生通常是由操作系统、其他进程或硬件事件引发的。信号可以表示不同的事件,如用户按下终止键(Ctrl+C),进程除零错误,或者定时器超时等。
- 信号的种类:每个信号都有一个唯一的标识符,如SIGTERM、SIGINT、SIGSEGV等,用来表示不同的事件类型。操作系统和程序员可以根据需要定义自定义信号。
- 信号发送:操作系统将信号发送给目标进程。这通常涉及到操作系统查找目标进程的进程标识符(PID)或进程组标识符(PGID),然后将信号发送给目标。
- 信号传递:操作系统将信号传递给目标进程。目标进程的执行可能会被中断,以便它可以处理接收到的信号。这意味着操作系统在进程执行期间会修改进程的执行流程,以引发信号处理。
- 信号处理程序执行:目标进程在接收到信号后,会查找它为该信号注册的信号处理程序。信号处理程序是一段用户定义的代码,用于定义在接收信号时应执行的操作。处理程序可以是默认的操作,用户自定义的操作,或者忽略信号。
- 信号处理:根据信号处理程序的定义,进程采取相应的操作。这可以包括终止进程、忽略信号、记录事件、执行自定义操作等。处理程序执行后,进程可以继续执行原来的任务。
- 信号传递结果:在信号处理程序执行后,进程可以向操作系统报告信号的处理结果,通常以退出状态码的形式。这可以帮助操作系统了解信号处理的成功与否,以及进程是否需要进一步处理。
总结来说,操作系统向进程发送信号的过程涉及信号的产生、发送、传递、处理,以及可能的反馈。这种机制使得操作系统和不同进程之间能够进行通信和协作,通常用于处理异常情况、用户交互、进程控制等。