当前位置: 首页 > news >正文

【Linux学习笔记】认识信号和信号的产生

【Linux学习笔记】认识信号和信号的产生

在这里插入图片描述

🔥个人主页大白的编程日记

🔥专栏Linux学习笔记


文章目录

  • 【Linux学习笔记】认识信号和信号的产生
    • 前言
    • 一. 信号快速认识
      • 1.1 生活角度的信号
      • 1.2 技术应用角度的信号
        • 1.2.1 ⼀个样例
        • 1.2.2 一个系统函数
      • 1.3 信号概念
        • 1.3.1 查看信号
        • 1.3.2 信号处理
    • 二. 产生信号
      • 2.1 通过终端按键产生信号
        • 2.1.1 基本操作
        • 2.1.2 理解OS如何得知键盘有数据
        • 2.1.3 初步理解信号起源
      • 2-2 调用系统命令向进程发信号
      • 2.3 使用函数产生信号
        • 2.3.1 kill
        • 2.3.2 raise
        • 2.3.3 abort
      • 2.4 硬件异常产生信号
        • 2.4.1 模拟除0
        • 2.4.2 模拟野指针
    • 后言

前言

哈喽,各位小伙伴大家好!上期我们讲了消息队列和信号量 今天我们讲的是认识信号和信号的产生。话不多说,我们进入正题!向大厂冲锋!
在这里插入图片描述

一. 信号快速认识

1.1 生活角度的信号

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

基本结论:

  • 你怎么能识别信号呢?识别信号是内置的,进程识别信号,是内核程序员写的内置特性。

  • 信号产生之后,你知道怎么处理吗?知道。如果信号没有产生,你知道怎么处理信号吗?知道。所以,信号的处理方法,在信号产生之前,已经准备好了。

  • 处理信号,立即处理吗?我可能正在做优先级更高的事情,不会立即处理?什么时候?合适的时候。

  • 信号到来 | 信号保存 | 信号处理

  • 怎么进行信号处理啊?a.默认 b.忽略 c.自定义,后续都叫做信号捕捉。

1.2 技术应用角度的信号

1.2.1 ⼀个样例
// sig.cc
#include <iostream>
#include <unistd.h>int main()
{while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  • 用户输入命令,在Shell下启动一个前台进程
  • 用户按下 Ctrl+C,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程
  • 前台进程因为收到信号,进而引起进程退出
NAME
signal - ANSI C signal handlingSYNOPSIS
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);参数说明:
signum:信号编号[后面解释,只需要知道是数字即可]
handler:函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法

而且其实, Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明⼀下,这⾥需要引入一个系统调用函数

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}int main()
{std::cout << "我是进程:" << getpid() << std::endl;signal(SIGINT/*2*/, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程:212569
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是:212569,我获得了一个信号:2
I am a process, I am waiting signal!
1.2.2 一个系统函数
### NAME
signal - ANSI C signal handling参数说明:- signum: 信号编号 [后面解释,只需要知道是数字即可]- handler: 函数指针,表示更改信号的处理动作,当收到对应的信号,就回调执行handler方法- 参数说明:
signum:信号编号[后⾯解释,只需要知道是数字即可]
handler:函数指针,表⽰更改信号的处理动作,当收到对应的信号,就回调执⾏handler⽅法

而其实,Ctrl+C 的本质是向前台进程发送 SIGINT 即 2 号信号,我们证明一下,这里需要引入一个系统调用函数

开始测试

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGINT, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!
// 用本机编译运行
$ g++ sig.cc -o sig
$ ./sig
我是进程: 212569
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C我是: 212569, 我获得了一个信号: 2
I am a process, I am waiting signal!
I am a process, I am waiting signal!

思考:

  • 这里进程为什么不退出?
  • 这个例子能说明哪些问题?信号处理,是自己处理
  • 请将生活例子和 Ctrl+C 信号处理过程相结合,解释一下信号处理过程:进程就是你,操作系统就是快递员,信号就是快递,发信号的过程就类似给你打电话

注意:

  • 要注意的是,signal函数仅仅是设置了特定信号的捕捉行为处理方式,并不是直接调用处理动作。如果后续特定信号没有产生,设置的捕捉函数永远也不会被调用!!
  • Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样 Shell 不必等待进程结束就可以接受新的命令,启动新的进程。
  • Shell 可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,所以信号相对于进程的控制流程来说是异步(Asynchronous)的。
  • 关于进程间关系,我们在网络部分会专门来讲,现在就了解即可。
  • 可以渗透 &nohup

1.3 信号概念

信号是进程之间事件异步通知的⼀种方式,属于软中断。

1.3.1 查看信号

每个信号都有⼀个编号和⼀个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定义 #define SIGINT 2

编号34以上的是实时信号,本章只讨论编号34以下的信号,不讨论实时信号。这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal

1.3.2 信号处理

可选的处理动作有以下三种:

  1. 忽略此信号
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}int main()
{std::cout << "我是进程:" << getpid() << std::endl;signal(SIGINT/*2*/, SIG_IGN); // 设置忽略信号的宏while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
  1. 执行该信号的默认处理动作
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是:" << getpid() << ",我获得了一个信号:" << signumber << std::endl;
}int main()
{std::cout << "我是进程:" << getpid() << std::endl;signal(SIGINT/*2*/, SIG_DFL);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}$ g++ sig.cc -o sig
$ ./sig
我是进程:212791
I am a process, I am waiting signal!
I am a process, I am waiting signal!
  1. 提供⼀个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种⽅式称为自定义捕捉(Catch)⼀个信号。
#define SIG_DFL ((__sighandler_t) 0) /* Default action. */
#define SIG_IGN ((__sighandler_t) 1) /* Ignore signal. *//* Type of a signal handler. */
typedef void (*__sighandler_t)(int);// 其实SIG_DFL和SIG_IGN就是把0,1强转为函数指针类型
^C // 输入ctrl+c,进程退出,就是默认动作

在这里插入图片描述

二. 产生信号

当前阶段:

在这里插入图片描述

2.1 通过终端按键产生信号

2.1.1 基本操作
  • Ctrl+C (SIGINT) 已经验证过,这里不再重复
  • Ctrl+\ (SIGQUIT) 可以发送终止信号并生成core dump文件,用于事后调试(后面详谈)
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGQUIT, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
$ g++ sig.cc -o sig
$ ./sig
我是进程: 213056
^Z  // 注释掉13行代码
$ ./sig
我是进程: 213146
I am a process, I am waiting signal!
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^C //Quit
  • Ctrl+Z (SIGTSTP) 可以发送停止信号,将当前前台进程挂起到后台等。
#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGTSTP, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
2.1.2 理解OS如何得知键盘有数据
2.1.3 初步理解信号起源

注意:

  • 信号其实是从纯软件角度,模拟硬件中断的行为
  • 只不过硬件中断是发给CPU,而信号是发给进程
  • 两者有相似性,但是层级不同,这点我们后面的感觉会更加明显
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>void handler(int signumber)
{std::cout << "我是: " << getpid() << ", 我获得了一个信号: " << signumber << std::endl;
}int main()
{std::cout << "我是进程: " << getpid() << std::endl;signal(SIGTSTP, handler);while(true){std::cout << "I am a process, I am waiting signal!" << std::endl;sleep(1);}
}
$ ./sig
我是进程: 213552
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z // 多按一次回车
[1]+  Stopped                 ./sig
$ ./sig
我是进程: 213627
I am a process, I am waiting signal!
I am a process, I am waiting signal!
^Z
[1]+  Stopped                 ./sig
$ web: http://code/tests_jobs

在这里插入图片描述

2-2 调用系统命令向进程发信号

示例代码:

#include <iostream>
#include <unistd.h>
#include <signal.h>int main()
{while(true){sleep(1);}
}
$ g++ sig.cc -o sig  // step 1
$ ./sig &           // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
PPID   PID  PGRP  SID  TTY      TPGID STAT  UID   TIME COMMAND
211805 213784 213784 211805 pts/0  213792 S   1002  0:00 ./sig
$ kill -SIGSEGV 213784
$ ./sig
[1]+  Segmentation fault      ./sig
  • 213784 是 sig 进程的pid。之所以要再次回车才显示 Segmentation fault,是因为在 213784进程终止掉之前已经回到了Shell提示符等待用户输入下一条命令,Shell 不希望 Segmentationfault信息和用户的输入交错在一起,所以等用户输入命令之后才显示
  • 指定发送某种信号的 kill 命令可以有多种写法,上面的命令还可以写成 kill -11 213784,11 是信号 SIGSEGV的编号。以往遇到的段错误都是由非法内存访问产生的,而这个程序本身没错误,给它发 SIGSEGV 也能产生段错误。

2.3 使用函数产生信号

2.3.1 kill

kill 命令是调用 kill 函数实现的。kill 函数可以给一个指定的进程发送指定的信号。

NAMEkill - send signal to a processSYNOPSIS#include <sys/types.h>#include <signal.h>int kill(pid_t pid, int sig);RETURN VALUEOn  success  (at  least  one  signal  was  sent),  zero  is  returned.  On  error,-1  is  returned,  and  errno  is  set  appropriately.

示例:实现自己的 kill 命令

#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>// mykill -signumber pid
int main(int argc, char *argv[])
{if(argc != 3){std::cerr << "Usage: " << argv[0] << " -signumber pid" << std::endl;return 1;}int number = std::stoi(argv[1]+1); // 去掉-pid_t pid = std::stoi(argv[2]);kill(pid, number);return 0;
}
$ g++ sig.cc -o sig  // step 1
$ ./sig &            // step 2
$ ps ajx | head -1 && ps ajx | grep sig // step 3
$ kill -SIGSEGV 213784
$ ./sig
[1]+  Segmentation fault
2.3.2 raise

raise 函数可以给当前进程发送指定的信号(自己给自己发信号)。

NAMEraise - send a signal to the callerSYNOPSIS#include <signal.h>int raise(int sig);RETURN VALUEraise() returns 0 on success, and nonzero for failure.

示例:

#include <iostream>
#include <unistd.h>
#include <signal.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了一个信号: " << signumber << std::endl;
}// mykill -signumber pid
int main()
{signal(2, handler);    // 先对2号信号进行捕捉// 每隔1s,自己给自己发送2号信号while(true){sleep(1);raise(2);}
}
$ g++ raise.cc -o raise
$ ./raise
获取了一个信号: 2
获取了一个信号: 2
获取了一个信号: 2
2.3.3 abort

abort 函数使当前进程接收到信号而异常终止

NAMEabort - cause abnormal process terminationSYNOPSIS#include <stdlib.h>void abort(void);RETURN VALUEThe abort() function never returns.// 就像exit函数一样,abort函数总是会成功的,所以没有返回值。

示例:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>void handler(int signumber)
{// 整个代码就只有这一处打印std::cout << "获取了一个信号: " << signumber << std::endl;
}// mykill -signumber pid
int main()
{signal(SIGABRT, handler);while(true){sleep(1);abort();}
}
$ g++ Abort.cc -o Abort
$ ./Abort
获取了一个信号: 6  // 实验可以得知,abort给自己发送的是固定6号信号,虽然捕捉了,但是还是要退出
Aborted
// 注释掉15行代码
$ ./Abort
Aborted

在这里插入图片描述

2.4 硬件异常产生信号

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

2.4.1 模拟除0

在这里插入图片描述

#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{//signal(SIGFPE, handler); // 8) SIGFPEsleep(1);int a = 10;a/=0;while(1);return 0;
}
2.4.2 模拟野指针

#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{//signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}
[hblocalhost code_test]$ .(sig)
Segmentation fault (core dumped)
[hblocalhost code_test]$ 
[hblocalhost code_test]$ cat sig.c
#include <stdio.h>
#include <signal.h>void handler(int sig)
{printf("catch a sig : %d\n", sig);
}int main()
{//signal(SIGSEGV, handler);sleep(1);int *p = NULL;*p = 100;while(1);return 0;
}

由此可以确认,我们在C/C++当中除零,内存越界等异常,在系统层面上,是被当成信号处理的。

注意: 通过上面的实验,我们可能发现:

  • 发现一直有8号信号产生被我们捕获,这是为什么呢?上面我们只提到CPU运算异常后,如何处理后续的流程,实际上OS会检查应用程序的异常情况,其实在CPU中有一些控制和状态寄存器,主要用于控制处理器的操作,通常由操作系统代码使用。状态寄存器可以简单理解为一个位图,对应着一些状态标记位、溢出标记位。OS会检测是否存在异常状态,有异常存在就会调用对应的异常处理方法。
  • 除零异常后,我们并没有清理内存,关闭进程打开的文件,切换进程等操作,所以CPU中还保留上下文数据以及寄存器内容,除零异常会一直存在,就有了我们看到的一直发出异常信号的现象。访问非法内存其实也是如此,大家可以自行实验。

在这里插入图片描述

后言

这就是认识信号和信号的产生。大家自己好好消化!今天就分享到这! 感谢各位的耐心垂阅!咱们下期见!拜拜~

http://www.lryc.cn/news/586851.html

相关文章:

  • JAVA JVM虚拟线程
  • HTML 初体验
  • 软件文档体系深度解析:工程视角下的文档架构与治理
  • OneCode3.0 VFS分布式文件管理API速查手册
  • jenkins使用Jenkinsfile部署springboot+docker项目
  • 代码随想录|图论|15并查集理论基础
  • Docker一键安装中间件(RocketMq、Nginx、MySql、Minio、Jenkins、Redis)脚步
  • SDN软件定义网络架构深度解析:分层模型与核心机制
  • Redis缓存设计与性能优化指南
  • 解码冯・诺依曼:操作系统是如何为进程 “铺路” 的?
  • [Nagios Core] CGI接口 | 状态数据管理.dat | 性能优化
  • 基于Redis Streams的实时消息处理实战经验分享
  • Appium源码深度解析:从驱动到架构
  • 使用macvlan实现容器的跨主机通信
  • 在Intel Mac的PyCharm中设置‘add bin folder to the path‘的解决方案
  • React强大且灵活hooks库——ahooks入门实践之常用场景hook
  • p4 大小写检查
  • Rust赋能文心大模型4.5智能开发
  • QCustomPlot绘图保存成PDF文件
  • 软考中级学习系列-- 阶码与尾数
  • 香港服务器Python自动化巡检脚本开发与邮件告警集成
  • 详解Linux下多进程与多线程通信(一)
  • Leetcode 3615. Longest Palindromic Path in Graph
  • OpenLoong技术观察 | 卓益得十年磨一剑:“行者”系列人形机器人技术演进观察
  • 构造函数延伸应用
  • DH(Denavit–Hartenberg)矩阵
  • redis汇总笔记
  • JAVA生成PDF(itextpdf)
  • 译码器设计
  • 论意识与人工智能:跨越鸿沟的艰难求索