📝个人主页🌹:Eternity._
⏩收录专栏⏪:Linux “ 登神长阶 ”
🌹🌹期待您的关注 🌹🌹
🔍前言:在Linux操作系统中,信号(Signal)是一种重要的进程间通信机制,它允许一个进程向另一个进程发送异步通知。这些通知可以是简单的消息,如用户按下了中断键(如Ctrl+C),也可以是复杂的系统事件,如除零错误或定时器到期。信号的产生和处理是Linux系统编程中的一个关键方面,对于理解和优化系统行为至关重要
本文旨在深入探讨Linux中信号的产生机制。我们将从信号的基本概念出发,逐步解析信号的来源、触发条件。通过本文的学习,你将能够了解信号在Linux内核中的实现原理,掌握如何编写代码来捕获和处理信号,以及如何利用信号来实现进程间的同步和通信
信号在Linux系统中的应用非常广泛,从简单的用户中断到复杂的系统监控和管理,都离不开信号的支持。因此,掌握信号的产生和处理机制,对于提高Linux系统编程能力、优化系统性能以及开发高效、稳定的系统应用程序具有重要意义
让我们一起踏上这段探索之旅,共同揭开Linux信号产生机制的神秘面纱!
📒1. 信号入门
信号概念:
在操作系统中,信号是一种异步通知机制,用于在进程之间或同一进程的不同线程之间传递信息。这种机制允许一个进程(或线程)在不影响其正常执行流程的情况下,通知另一个进程(或线程)某个事件的发生。信号在Linux和其他类Unix操作系统中扮演着非常重要的角色
在生活中例如:我们在网上购物时,即便快递没有到来,我们也知道快递来临时,该怎么处理快递。也就是能
识别快递
,但是当快递到达的时候,我们也没有立马过去取快递,也就是取快递的行为并不是一定要立即执行,可以理解成在合适的时候去取
,在收到通知,再到我们拿到快递期间,是有一个时间窗口的,在这段时间,并没有拿到快递,但是我们知道有一个快递已经来了。本质上是记住了有一个快递要去取
,当我们时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作
(打开快递)2. 执行自定义动作
(给别人的礼物)3. 忽略快递
(不急着用,先丢一边),快递到来的整个过程,对你来讲是异步的
,你不能准确断定快递员什么时候给你打电话。
在技术应用角度:用户输入命令,在Shell下启动一个前台进程,用户按下
Ctrl-C
,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
进程就是我们,操作系统就是快递员,信号就是快递
信号的基本概念:
异步性
:信号是异步事件,可以在任何时候、由任何进程(包括内核进程和用户进程)发送给另一个进程通知性
:信号的主要目的是通知接收进程某个事件的发生,而不是传递数据。虽然信号可以携带一些简单的信息(如信号编号),但通常不用于传输大量数据处理机制
:每个信号都有一个默认的处理程序,通常是由操作系统提供的。但进程也可以指定自己的信号处理程序来替代默认处理
注意事项:
Ctrl-C
产生的信号只能发给前台进程。一个命令后面加个&
可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程- Shell
可以同时运行一个前台进程和任意多个后台进程
,只有前台进程才能接到像 Ctrl-C 这种控制键产生的信号- 前台进程在运行过程中用户随时可能按下
Ctrl-C
而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步
系统信号列表:
指令:
kill -l
实际上每个进程都有一张自己的函数指针数组,数组的下标就和信号编号强相关
这些信号各自在什么条件下产生,默认的处理动作是什么
在signal(7)中都有详细说明:man 7 signal
信号处理常见方式:
- 忽略此信号
- 执行该信号的默认处理动作
- 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉一个信号
📜2. 信号捕捉初识
信号是可以被自定义捕捉的,
siganl
函数就是来进行信号捕捉的
代码示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void headler(int signo)
{
cout << "收到一个" << signo << "号信号" << endl;
}
int main()
{
signal(2, headler);
while(1)
{
cout << "running ... pid: " << getpid() << endl;
sleep(1);
}
return 0;
}
注意:并不是所有的信号都能捕捉,比如
SIGKILL
📚3. 信号的产生
在每个进程的PCB中,都会有一个信号位图,由操作系统修改其中的比特位(0->1),完成信号的发送,发送信号只能由操作系统发送
🌊通过终端按键产生信号
Ctrl+C
:当用户在前台进程运行时按下Ctrl+C组合键,会产生一个SIGINT(中断信号),通常用于终止前台进程。Ctrl+Z
:当用户按下Ctrl+Z组合键时,会产生一个SIGTSTP(停止信号),用于暂停前台进程的执行,并将其置于停止状态。Ctrl+\
:当用户按下Ctrl+\组合键时,会产生一个SIGQUIT(退出信号),用于终止进程并生成核心转储文件(Core Dump),这有助于开发者事后调试和查找错误。
Core Dump:
Core Dump
:用于在进程因某些信号而异常终止时,将其当时的内存状态记录下来,并保存在一个文件中。一般为core.pid
的形式,这个文件通常被称为core dump文件或核心转储文件
查看Core Dump是否开启:
指令:
ulimit -a
如果返回值为0,则表示Core Dump被禁用;如果返回值为unlimited或具体的大小值,则表示Core Dump被启用,并设置了最大文件大小
开启Core Dump:
指令:
ulimit -c 大小
Core Dump是默认关闭的,要想实现Core Dump的持久化开启,需要更改配置文件
Core Dump文件:
查看Core Dump文件:
- 可以使用GDB(GNU Debugger)等调试工具来加载Core Dump文件,并查看程序崩溃时的现场信息,以便进行调试和错误定位
🍂调用系统函数向进程发信号
系统调用是操作系统提供给用户程序与硬件进行交互的一组函数接口。通过系统调用,用户程序可以请求操作系统执行某些操作
kill:
kill
:kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号
代码示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
// 输入准则
static void Usage(const string &proc)
{
cout << "\nUsage: " << proc << "-signumber process" << endl;
}
void headler(int signo)
{
cout << "收到一个" << signo << "号信号" << endl;
}
int main(int argc, char *argv[])
{
if(argc != 3)
{
Usage(argv[0]);
exit(0);
}
// "-"
int signumber = stoi(argv[1]+1);
int processid = stoi(argv[2]);
kill(processid, signumber);
return 0;
}
raise:
raise
:用于向当前进程发送信号。与kill不同,raise不需要指定进程ID,因为它默认作用于当前进程
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
// raise
void headler(int signo)
{
cout << "get a signo: " << signo << endl;
sleep(1);
}
int main()
{
signal(2, headler);
while(1)
{
raise(2);
}
return 0;
}
abort:
abort
:用于向当前进程发送SIGABRT信号,通常用于程序遇到无法恢复的错误时主动终止执行
代码示例:
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
// abort
void headler(int signo)
{
cout << "get a signo: " << signo << endl;
}
int main()
{
signal(SIGABRT, headler);
abort();
while(1)
{
cout << "running ... " << endl;
sleep(1);
}
return 0;
}
🌸由软件条件产生信号
由软件条件产生信号,通常指的是在软件应用程序中,根据某些特定的条件或逻辑判断来触发或生成信号
定时器到期
:通过调用alarm函数设置一个定时器,当定时器到期时会产生SIGALRM信号非法内存访问
:如访问未分配的内存或越界访问数组等,会产生SIGSEGV(段错误)信号除零错误
:进行浮点数除法运算时,如果除数为零,会产生SIGFPE(浮点异常)信号
void headler(int signo)
{
cout << "get a signo: " << signo << endl;
sleep(1);
}
int main()
{
signal(8, headler);
int x = 10;
int y = 0;
int z = x / y;
return 0;
}
当我们捕捉一个除零错误的信号时,我们明明没有循环结构,但是它一直在循环打印,其实是,我们捕捉信号时,进程并没有被杀掉,它就一直在调度,异常一次就发送一次信号,所以我们在捕捉信号时,都要进行终止进程
定时器:
alarm:用于设置一个定时器,当定时器到期时会向进程发送一个
SIGALRM
信号
边输出边打印涉及到访问外设,效率会低,而我们只做一次打印效率就会更快,所以外设其实是很慢的
alarm的返回值是它所剩余的时间
代码示例:
int n = 0;
void headler(int signo)
{
n = alarm(0);
cout << "result: " << n << endl;
//cout << "get a signo: " << signo << " alarm: " << cnt << endl;
exit(0);
}
int main()
{
signal(14, headler);
cout << "pid: " << getpid() << endl;
alarm(30);
while(1)
{
sleep(1);
}
return 0;
}
操作系统中的时间:
- 1.所有用户的行为,都是以进程的形式在OS中表现的
- 2.操作系统只要把进程调度好,就能完成所有的用户任务
- 3.CMOS,周期性的,高频率的,向CPU发送时钟中断
🌵硬件异常产生信号
硬件异常产生信号是指硬件设备在运行时遇到错误或异常情况时向操作系统发送信号
例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释为SIGFPE信号发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV信号发送给进程
📖4. 总结
随着我们对Linux中信号产生的深入学习,我们已逐渐揭开了这一进程间通信机制的神秘面纱。从信号的起源、类型到其在系统中的传递和处理,每一个细节都充满了智慧与巧妙的设计
信号的魅力在于它的简洁与高效。作为进程间异步通信的桥梁,信号无需复杂的协议或数据结构,仅凭一个数字编号就能传递丰富的信息。这种设计不仅降低了系统的开销,还提高了通信的灵活性
然而,信号的学习之旅并未结束。随着技术的不断进步和Linux系统的广泛应用,信号在进程管理、资源控制以及系统安全等方面的作用将越来越重要。掌握信号的产生和处理机制,将使我们能够更好地应对各种复杂的系统场景,为系统的稳定运行提供有力保障
最后,让我们以一颗好奇和敬畏的心,继续这段信号之旅!
希望本文能够为你提供有益的参考和启示,让我们一起在编程的道路上不断前行!
谢谢大家支持本篇到这里就结束了,祝大家天天开心!