【Linux:进程间信号】(一)

简介: 【Linux:进程间信号】(一)

1 生活角度的信号

你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”,当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取。在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话。

2 技术应用角度的信号

我们之前提到过,可以通过kill -l 命令来查看信号:

[grm@VM-8-12-centos lesson16]$ kill -l
 1) SIGHUP   2) SIGINT   3) SIGQUIT  4) SIGILL   5) SIGTRAP
 6) SIGABRT  7) SIGBUS   8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH  29) SIGIO 30) SIGPWR
31) SIGSYS  34) SIGRTMIN  35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX  

其中我们将标号34以上的叫做实时信号 ,本文章不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明 man 7 signal每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到。

我们之前讲过的,当显示屏中不断有数据在刷屏时我们可以用ctrl+c来终止进程,其本质用户按下ctrl+c ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。但是我们也提过前台进程可以用ctrl+c终止前台进程,但是却不能终止后台进程,我们将可执行程序运行时加上&就能够变成后台进程。此时我们只有通过kill -9命令来杀死进程。

注意:

Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程结束就可以接受新的命令,启动新的进程。

Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。

前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步。(Asynchronous)的。

3 信号的产生

3.1 由系统调用向进程发信号

3.1.1 signal

其实signal本质上来说并不是向进程发送信号,而是在收到了信号后我们用户自定义处理信号的方式,我们上面提到过,信号的处理方式有三种:

  • 执行默认动作
  • 执行自定义动作
  • 忽略信号

首先我们来看看signal的介绍:

f2a636a1581544f5834b722f0270b578.png

我们观察参数,发现其中有一个函数指针,这个函数指针是由我们自己实现的,也就是当我们收到特定的信号时我们自定义的采取怎样的方式去处理。

我们可以写代码来验证一下:

signal.cc:

#include<iostream>
#include<string>
#include<signal.h>
#include<unistd.h>
using namespace std;
void hander(int signo)
{
    cout<<"get a signal:"<<signo<<endl;
    exit(2);
}
int main()
{
    signal(2,hander);
    while(1)
    {
        cout<<"my pid is:"<<getpid()<<endl;
        sleep(1);
    }
    return 0;
}

当我们在键盘上敲下kill -9 进程pid时我们可以观察到下面现象:

67a1dbcb5ad8427a98eb3c06e7100967.png

这是由于我们自定义了信号的处理方式,当进程收到了2号信号时就不会执行默认2号信号的处理方式,而是执行了我们自己定义的处理方式(用函数指针实现)

注意:signal(2, handler)调用完这个函数的时候,hander方法被调用了吗?

答案是没有的,是接受到2号信号后才回调这个hander方法。

3.1.2 kill

int kill(pid_t pid, int signo);

这个函数的使用很简单,通过参数的命名我们就能够知道如何给指定进程发送信号。

那我们可以自己实现一个mykill:

#include <iostream>
#include <cstring>
#include <cerrno>
#include <string>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
using namespace std;
void Usage(string proc)
{
    std::cout << "\tUsage: \n\t";
    std::cout << proc << " 信号编号 目标进程\n"
              << std::endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }
    int sig = stoi(argv[1]);
    int pid = stoi(argv[2]);
    int n = kill(pid, sig);
    return 0;
}

其实很好理解,杀死目标进程我们是创建了新进程来处理。

3.1.3 raise

int raise(int signo);


7fd268fed78f42a2933f5abbef673db4.png


kill命令是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)。

除此之外还有一个C语言的abort函数(使当前进程接收到信号而异常终止)

#include <stdlib.h>

void abort(void);

就像exit函数一样,abort函数总是会成功的,所以没有返回值。

3.2 由软件条件产生信号

SIGPIPE是一种由软件条件产生的信号,在“管道”中已经介绍过了。本节主要介绍alarm函数 和SIGALRM信号

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。

这个函数的返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数.

我们来看看下面这个程序:

int cnt=1;
void hander(int sigo )
{
    cout<<cnt<<endl;
    exit(1);
}
int main()
{
    signal(14,hander);
    alarm(1);
    while(true)
    {
      cout<<cnt++<<endl;
    }
    return 0;
}

这个程序的作用是统计出1秒中cnt累加的次数,我们运行起来看看:

当我们修改代码,不要加上输出语句再试试:

我们发现第二次的cnt累加次数明显远大于第一次的值,这其实也是很好理解的因为IO很慢


3.3 硬件异常产生信号

硬件异常被硬件以某种方式被硬件检测到并通知内核,然后内核向当前进程发送适当的信号。例如当前进程执行了除以0的指令,CPU的运算单元会产生异常,内核将这个异常解释 为SIGFPE信号(8号)发送给进程。再比如当前进程访问了非法内存地址,MMU会产生异常,内核将这个异常解释为SIGSEGV(11号)信号发送给进程。

我们可以写程序来验证一下:

#include<iostream>
#include<signal.h>
using namespace std;
void hander(int sign)
{
    cout<<"初零错误"<<endl;
}
int main()
{
    signal(SIGFPE,hander);
    int a=10;
    cout<<a/0<<endl;
    return 0;
}

当我们运行时:


f5f2311318bb44caaf2c169d867d335f.png

为什么会一直在重复打印呢?原因是在于我们自定义8号信号时执行但是没有退出程序,而除零错误·一直存在,所以会一直在显示屏上刷屏。野指针问题也类似。


3.4 通过终端按键产生信号

SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且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.

那我们究竟如何查看当前进程的资源上限呢?

我们可以使用ulimit -a:

d2cbb11a0867476e9411cb388d9de861.png

我们发现在云服务器上默认是关闭了核心转储文件的,如果我们想设置可以使用命令:

ulimit -c XXX来自定义的设置好核心转储文件大小。


2b0682d895d74695aa80f66716fe04e7.png

我们之前通过man 7 signal命令查看时:

46a3da1ea788401b96675419b6792f43.png


不难发现有些信号是带有Core的,Term的默认执行动作是终止进程,没有其他动作;而Core是先会进行核心转储,再终止进程的。

我们可以来试试:

当我们写了一个除零错误的代码时让其自定义执行默认动作时:


e9ea9fc6071441b7ade95b32cdcac95f.png

就会生成一个core.xxx的文件,这个文件就是核心转储文件,那么这个文件有啥用呢?

我们在用gdb调试时可以使用core-file core.xxx来帮助我们快速定位到错误所在,但是当我们没重复运行一次时就会重新生成一个core文件,所以云服务器上是默认关闭掉CoreDump文件。不知道大家忘记了没有,我们在讲解进程控制时就已经提过了一个Core Dump标志:

d50e71534bd14df2a7b076b3e65bba23.png

是否具有core dump是由这个标志位所决定的,这个标志位的获取方法有很多种,我这里给出一种:

(status<<7)&1


3.5 总结思考一下

上面所说的所有信号产生,最终都要有OS来进行执行,为什么?OS是进程的管理者.

信号的处理是否是立即处理的?在合适的时候.

信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?

一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?

如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?

4 信号的保存

4.1信号其他相关常见概念

  1. 实际执行信号的处理动作称为信号递达(Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决(Pending)。
  3. 进程可以选择阻塞 (Block )某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
  5. 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。


相关实践学习
CentOS 7迁移Anolis OS 7
龙蜥操作系统Anolis OS的体验。Anolis OS 7生态上和依赖管理上保持跟CentOS 7.x兼容,一键式迁移脚本centos2anolis.py。本文为您介绍如何通过AOMS迁移工具实现CentOS 7.x到Anolis OS 7的迁移。
目录
相关文章
|
2月前
|
网络协议 Linux
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
Linux查看端口监听情况,以及Linux查看某个端口对应的进程号和程序
143 2
|
2月前
|
Linux Python
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
linux上根据运行程序的进程号,查看程序所在的绝对路径。linux查看进程启动的时间
47 2
|
6天前
|
Linux Shell
6-9|linux查询现在运行的进程
6-9|linux查询现在运行的进程
|
1月前
|
Linux C语言
C语言 多进程编程(四)定时器信号和子进程退出信号
本文详细介绍了Linux系统中的定时器信号及其相关函数。首先,文章解释了`SIGALRM`信号的作用及应用场景,包括计时器、超时重试和定时任务等。接着介绍了`alarm()`函数,展示了如何设置定时器以及其局限性。随后探讨了`setitimer()`函数,比较了它与`alarm()`的不同之处,包括定时器类型、精度和支持的定时器数量等方面。最后,文章讲解了子进程退出时如何利用`SIGCHLD`信号,提供了示例代码展示如何处理子进程退出信号,避免僵尸进程问题。
|
1月前
|
NoSQL
gdb中获取进程收到的最近一个信号的信息
gdb中获取进程收到的最近一个信号的信息
|
2月前
|
消息中间件 Linux
Linux进程间通信
Linux进程间通信
35 1
|
2月前
|
Linux 调度
Linux0.11 信号(十二)(下)
Linux0.11 信号(十二)
20 1
|
19天前
|
存储 监控 安全
探究Linux操作系统的进程管理机制及其优化策略
本文旨在深入探讨Linux操作系统中的进程管理机制,包括进程调度、内存管理以及I/O管理等核心内容。通过对这些关键组件的分析,我们将揭示它们如何共同工作以提供稳定、高效的计算环境,并讨论可能的优化策略。
22 0
|
1月前
|
Unix Linux
linux中在进程之间传递文件描述符的实现方式
linux中在进程之间传递文件描述符的实现方式
|
2月前
|
开发者 API Windows
从怀旧到革新:看WinForms如何在保持向后兼容性的前提下,借助.NET新平台的力量实现自我进化与应用现代化,让经典桌面应用焕发第二春——我们的WinForms应用转型之路深度剖析
【8月更文挑战第31天】在Windows桌面应用开发中,Windows Forms(WinForms)依然是许多开发者的首选。尽管.NET Framework已演进至.NET 5 及更高版本,WinForms 仍作为核心组件保留,支持现有代码库的同时引入新特性。开发者可将项目迁移至.NET Core,享受性能提升和跨平台能力。迁移时需注意API变更,确保应用平稳过渡。通过自定义样式或第三方控件库,还可增强视觉效果。结合.NET新功能,WinForms 应用不仅能延续既有投资,还能焕发新生。 示例代码展示了如何在.NET Core中创建包含按钮和标签的基本窗口,实现简单的用户交互。
52 0
下一篇
无影云桌面