目录
📚信号
什么是信号?
-
信号是Linux系统提供的让
进程
或则操作系统
给其他进程发送异步信息
的一种方式。 -
信号的产生是随时产生的,进程无法预料,所以信号是异步发送的。(信号的产生是由别的进程产生的,在收到信号之前,我(进程)在做自己的事情,我和发送信号的进程是在并发跑的。所以是异步)
为什么要有信号?
-
Linux系统中要有信号,主要是为了提供一种机制,以便异步地处理系统事件和进程间通信。
进程间通信:
信号是进程间通信(IPC)的一种方式,允许进程之间异步地传递消息。系统事件通知:
操作系统可以使用信号来通知进程发生了系统级事件,如非法内存访问(段错误)、软件终止请求(如用户按下Ctrl+C)等。简化编程模型:
信号提供了一种机制,允许操作系统和应用程序开发者以一种标准化的方式处理各种异步事件,从而简化了编程模型。资源管理:
信号可以用于管理进程占用的资源。例如,当进程占用的资源超出限制时,系统可以发送信号来终止或暂停该进程。用户交互:
用户可以通过发送信号来控制进程的行为,例如,使用kill命令发送信号以终止或暂停进程。错误处理:
当进程执行非法操作时,系统可以发送信号来通知进程发生了错误,进程可以根据信号的指示进行错误处理。
查看Linux系统中信号
- 用
kill -l命令
可以查看系统定义的信号列表:
- 每个信号都有一个编号和一个宏定义名称,这些宏定义可以在
signal.h
中找到,例如其中有定义#define SIGINT 2
- 编号34以上的是
实时信号
,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明:man 7 signal
注意:
没有0 ,32 ,33号信号
🎈信号产生
📕kill 命令产生信号
- 如:杀掉指定进程:
kill -9 进程编号
。
🏠键盘产生
- 如:2号信号 :ctrl + c 可以终止进程 3号信号:ctrl + \ 终止进程
- 会将该组合键解释为信号 --> 向目标进程发送该信号 —> 进程收到信号 —> 响应信号。
🦅系统调用
kill系统调用
函数介绍:
-
功能: 向任意的进程发送任意的信号。
-
参数:参数一:进程pid ; 参数二:信号编号或则信号名称。
-
返回值: 0表示成功,-1表示失败。
示例代码:
void handler(int signo)
{
cout << "receive a signal: " << signo << endl;
}
int main()
{
signal(2,handler);
int n = kill(getpid(),2); //给自己发送2号信号
return 0;
}
运行结果预期:使用kill系统调用给自己发送一个2号信号,2号信号被捕捉,当收到二号信号的时候,执行我们写的handler函数,打印信息 :receive a signal:2
运行结果:
raise系统调用
函数介绍:
-
功能:谁调用,就向哪一个进程发送任意信号
-
参数:信号编号/信号名称
-
返回值:0表示成功,非0表示失败。
代码示例:
void handler(int signo)
{
cout << "receive a signal: " << signo << endl;
}
int main()
{
signal(2,handler);
int n = raise(2);
return 0;
}
运行结果:
abort系统调用
函数介绍:
- 功能:向调用该函数的进程发送6号信号:SIGABRT(终止进程)。
示例代码:
int main()
{
int cnt = 1;
while(cnt++)
{
if(cnt == 5)
{
abort();
}
cout<<"i am a process,pid:"<<getpid()<<endl;
}
return 0;
}
运行结果:
🍑软件条件
SIGPIPE
是一种由软件条件产生的信号(管道的读端不读,还关闭了读文件描述符,则写端就会因为写条件不具备而被OS终止,OS给写端进程发送的信号就是SIGPIPE:13号信号)。本节还介绍alarm函数
和SIGALRM信号
。
函数介绍:
- 调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。比如:当我们设定一个50秒的闹钟,如果在20秒的时候,提前给该进程发送一个SIGALRM信号使进程提前响应,这时,返回值就为50-20=30。
alarm(0):
取消闹钟
示例代码:
int main()
{
alarm(5);
while(true)
{
cout<<"i am a process,pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
运行结果:
对SIGALRM信号进行捕捉,实现2秒循环的闹钟
代码实现:
void handler(int signo){
cout << "receive a signal: " << signo << endl;
alarm(2);
}
int main()
{
signal(SIGALRM,handler);
alarm(2);
while(true)
{
cout<<"i am a process,pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}
运行结果:
🌠硬件异常产生信号
硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。
- 例如当前进程执行了
除以0
的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信
号发送给进程。 - 再比如当前进程
访问了非法内存地址
,MMU会产生异常,内核将这个异常解释为SIGSEGV信号
发送给进程。
除0异常示例代码:
int main()
{
int a = 0;
int b = 4/0;
while(true)
{}
sleep(1);
return 0;
}
运行结果:
访问空指针示例代码:
int main()
{
int *p = 0;
*p=10;
return 0;
}
运行结果:
对于键盘产生信号的理解
字符输入(字符设备),组合键输入(输入的命令),对于键盘来说,输入的一律当成字符处理,至于到底是字符还是命令,是需要OS
和键盘驱动
来联合解释的。
OS怎么知道键盘在输入数据?(简略介绍)
- 使用了一种硬件中断的技术。
- 在开机的时候,OS已经形成了一张
中断向量表
(函数指针数组),这张表可以提前注册很多对软硬件操作的方法,比如下面表中2号位置是从键盘中读取数据,CPU底部由很多的针脚与外设相连接,这些针脚都有自己的编号当OS正在执行常规代码得时候,用户按键盘输入数据得时候,会给CPU上的特定针脚触发特定的硬件中端,CPU就会将该针脚编号放到一个寄存器中,这时候硬件到软件的动作就完成了,然后CPU就会要求OS停止当前正在执行的工作,让OS根据中端号,取中端向量表中查找对应的操作方法,然后去执行读取键盘数据的任务。
通过上面的操作后,已经读取到了数据了,接下来会对读取的数据进行判定
。
如果操作系统判定为是一般字符输入
,则直接放到键盘缓冲区
中,如果判定为控制命令
,OS
则会将控制命令解释成信号
,然后将信号发送给进程
。
对硬件异常产生信号的理解
-
除0错误问题
首先,计算均在CPU中进行的,对于除0错误的计算过程中,会将标志寄存器中的溢出位的0置为1,将计算错误表现到CPU的寄存器上(硬件上),然后CPU发现硬件出现异常,这时OS会介入,发现问题后,将对应的信号发送给相应的进程。
-
野指针问题
CPU有一个硬件MMU,专门负责虚拟地址到物理地址的转换,同时又两个寄存器,CR2与CR3,如果虚拟地址转换成功,会将转换好的物理地址放到CR3中,如果转换出错,则会将错误标志信息放到CR2中,当野指针的时候,首先将野指针地址放到一个寄存器中,然后通过MMU进行转换,在转换的过程中,发现页表中没有这个虚拟地址或则该虚拟地址中没有相应的权限,则会转换出错,将错误标记放在CR2中,表明了CR2出异常,硬件出现异常,这时OS会介入,发现问题后,将对应的信号发送给相应的进程。
一个进程,出现异常大多都是将进程终止,但是man 7 signal 中,终止又两种,一个Term
,一个是Core
,他们有什么区别呢?以及之前在讲进程的时候,进程等待的status位图
中core dump
标记位和这个Core的关系。
Core Dump
首先解释什么是Core Dump。当一个进程要异常终止
时,可以选择把进程的用户空间内存数据
全部保存到磁盘
上,文件名通常是core
,这叫做Core Dump。进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存 在PCB中)。默认
是不允许产生core文件
的,因为core文件中可能包含用户密码等敏感信息,不安全。
-
查看core是否打开 指令:
ulimit -a
-
开启Linuxcore dump功能:用
ulimit命令
改变这个限制,允许产生core文件。 首先用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:$ ulimit -c 1024
打开过后,再运行一个除0错误的程序,会再当前工作目录中形成一个core文件(ubuntu系统),文件的名字在不同的平台不同(centos系统中就是以core.进程编号命名的)。
事后调试
gdb 可执行程序
—>core-file core
就会将相关调试信息列出(比如,在哪一行出错,出什么错)。
为什么默认的是core dump(核心转储)功能是关闭的呢
- 有一些平台的core命名不同,如果每一个进程出现core dump后,都形成不同的core文件,那么很容将服务器的磁盘打满。