Linux学习之信号

目录

1.信号的概念

2.信号的产生

3.信号的保存

4.信号的捕捉

信号的其它内容:

SIGCHLD信号


1.信号的概念

在Linux中,信号是一种用于进程之间通信的基本机制。它是一种异步事件通知,用于通知进程发生了某些事件。如下是一些常见的Linux信号类型:

SIGINT (2):中断进程,通常由终端产生,例如用户按下Ctrl+C。
SIGKILL (9):立即终止进程,无法被捕获或忽略。
SIGTERM (15):请求终止进程,可以被捕获或忽略。
SIGQUIT (3):请求进程退出并生成核心转储文件,可以被捕获或忽略。
SIGSTOP (17):暂停进程的执行,无法被捕获或忽略。
SIGCONT (19):恢复进程的执行,无法被捕获或忽略

 这些信号在进程控制、异常处理和进程间通信中扮演着重要角色。请注意,信号只是通知进程发生了什么事件,并不传递任何数据。进程对不同信号有不同的处理方式,可以指定处理函数、忽略或保留系统的默认值。信号机制在Linux编程中非常重要,帮助实现进程之间的协作和控制。

2.信号的产生

先举两个样例:

eg1:

首先我们编写一个死循环代码,编译运行后,我们的命令行就不再有用了,现在是前台程序,只运行当前的程序,当我们编译时加上&,使他成为后台程序,此时的命令行也可以继续使用,

程序在运行的时候,前台程序只能有一个,后台程序可以有多个。后台程序在运行时,我们的键盘可以输入数据,指令可以运行。

一般操作系统会自动根据情况把shell程序提到前台或者后台。下面的指令对shell无效。

前后台程序切换

./可执行 &  把程序放到后台

jobs  查看后台任务

fg number(任务编号) 把任务放到前台

ctrl+z 再加 bg number   把后台任务转到前台

ctrl+\ 默认终止

ctrl + z 暂停程序,先放到后台

而这就是信号的产生,除此之外操作系统知晓键盘的输入也是一种信号:

eg2:当键盘的某个按钮被按下的时候,就会产生高电平信号间接给cpu,cpu得知了之后某个按钮的高电平,发生中断,就产生对应的数据。

而信号的产生就是用软件来模拟中断行为。我们的指令都是发出信号,

例如接口signal

可以发出我们需要的信号。

如下一段代码:

#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>void handler(int signo)
{std::cout<<"获得一个"<<signo<<"信号"<<std::endl;exit(1);
}int main()
{signal(2,handler);while(true){std::cout<<"pid:"<<getpid()<<",i am running......"<<std::endl;sleep(1);}return 0;
}

 再运行的时候,我们ctrl+z,此时退出进程就会获得一个为2的信号。

因此信号的产生可以通过键盘发出,对于我们的linux也是有许多信号的(kill -l):

其中,没有0号信号,从1-31的信号我们把它叫做普通信号,没有32,33信号,从34到64的信号,我们把它叫做实时信号。这些信号的本质就是一些函数指针数组,对应的下标就与他们的编号有关。

对于普通信号,进程是否收到了普通信号,操作系统(pcb中)会用一张位图来表示,利用位图中的第几个比特位表示编号,0表示没收到,1表示收到。

无论信号有多少种,都是只能让os来写(写)信号,因为os是进程的管理者。

了解到了信号的接收,因此我们在编写程序时就可以直接发送信号,之后自动运行对应handler方法,例如之前我们使用kill -9杀进程,现在我们发送一个为9的信号,此时自定义它的处理方法,例如只是打印一句话,那么我们kill -9的指令就不会再杀掉我们的进程,而是打印一句话。

但实际上并不可以,操作系统对于某些信号是不可以被自定义捕捉的。

除此以外,Linux提供了三种接口供我们产生信号。

方式一:通过键盘组合键发送产生信号。

方式二:通过函数接口

接口 raise 可以自己给自己发送任意信号

接口 abort  收到信号后终止运行

方式三,通过异常:

 以我们熟知的除零错误为例,首先除零错误并不是语言错误,而是进程错误,再cpu中通过各个寄存器来计算除零,此时cpu中还有表示状态的寄存器,当发生除零问题后,状态寄存器就会产生溢出标记位,从而转化为信号,就是信号8 SIGFPE  也就是flaot point exception。

当然发出信号也不仅仅可能是因为异常而导致的,也有可能是闹钟响了:

方式四:由软件条件产生信号:

alarm接口可以设置闹钟

#include<iostream>
#include<unistd.h>
#include <signal.h>
#include <stdlib.h>
int cnt=0;
void handler(int signo)
{std::cout<<"获得一个"<<signo<<"信号"<<"alarm is:"<<cnt<<std::endl;exit(1);
}int main()
{std::cout<<"pid:"<<getpid()<<std::endl;//本质上就是修改函数指针数组的位置signal(14,handler);//设置1s闹钟,到点了终止进程alarm(1);while(true){//cout<<cnt++<<endl;  可以看出外设是很慢的cnt++;}}

 操作系统的时间:

当我们电脑关机了,程序结束了,再次重新启动,我们会发现,时间永远是跟着走的,实际上,即使关机了,在电脑里也会有一个纽扣电池一直给硬件供电,固定时间间隔计数,再将计数器转换为时间戳给我们的电脑。CMOS周期性的高频的发送时间中断。

3.信号的保存

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

 递达就是开始处理信号,当信号被记录再为途中时就是信号未决状态,阻塞:被阻塞的信号一直处在未决状态,只有当阻塞取消时,才进入递达状态。

阻塞与忽略是有区别的,忽略本身没有阻塞而是递达,处理了信号,效果为忽略,而阻塞是没有抵达,且没处理。

了解了以上概念,因此再管理信号的状态时,os就需要维护这三张位图表,用来表示阻塞,未决,递达这三个状态的信号。

比特位的位置:代表信号的编号

比特位的内容:对特定信号进行阻塞还是屏蔽。 

每个信号都有两个标志位分别表示block(阻塞)和pending(未决),其次还有一个函数指针表示要处理的方法。

void handler(int signo)
{cout<<"signo is "<<signo<<endl;exit(1);
}
int main()
{//发送2信号signal(2,signo);//把信号的粗粒设置为原来默认的signal(2,SIG_DFL);//当然还可以把信号忽略signal(2,SIG_IGN);std::cout<<"my pid id:"<<getpid()<<endl;while(true){cout<<"i am running....."<<endl;sleep(1);}}

由于有这么多信号集,操作系统还提供了许多信号及操作接口:

sigset_t 类型对于每种信号用一个 bit 表示 有效 无效 状态 , 至于这个类型内部如何存储这些 bit 则依赖于系统 实现, 从使用者的角度是不必关心的 , 使用者只能调用以下函数来操作 sigset_ t 变量 , 而不应该对它的内部数据做 任何解释, 比如用 printf 直接打印 sigset_t 变量是没有意义的。
#include <signal.h>
int sigemptyset(sigset_t *set);  //对指定的位图进行清零
int sigfillset(sigset_t *set);   //对指定的位图进行置1
int sigaddset (sigset_t *set, int signo); //对指定信号添加到指定的位图中
int sigdelset(sigset_t *set, int signo);
int sigismember const sigset_t *set, int signo); //判定一个信号是否在为位图中

对于block表的修改:

sigprocmask 调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)

如下代码:


int main()
{//例如对2号信号屏蔽cout<<"my pid is"<<getpid()<<endl;//先定义两个信号集位图sigset_t block,oblock;//先对信号集清空sigemptyset(&block);sigemptyset(&oblock);//其次对2号信号添加到信号集sigaddset(&block,2);  //当前并没有让操作系统2信号屏蔽,只是语言层面的定义sigaddset(&oblock,2);sigprocmask(SIG_BLOCK,&block,&oblock);   //真正让操作系统屏蔽、更改信号while(true){sleep(1);}return 0;
}

 此时我们再发2号信号就没有作用了,ctrl+c也无法中断程序。

既然如此,那么我们是否可以将一个程序的所有信号屏蔽,这样他就有金刚不坏之身,谁也干不掉他,实际上并是不是所有的信号你都能屏蔽,就跟不是所有的信号的处理可以自定义是一样的。

比如说9号信号就无法被屏蔽。

那么pending表的修改:接口 sigpending

重要的是获取pending表.

接下来我们用一个整体的实例来认识这些接口:

void printpending(const sigset_t &pending)
{for(int signo=31;signo>0;signo--){if(sigismember(&pending,signo)){cout<<"1";}else{cout<<"0";}}cout<<"\n";
}
//自定义捕捉
void handler(int signo)
{cout<<"已接受到信号"<<signo<<endl;//exit(1);}int main()
{//例如对2号信号屏蔽cout<<"my pid is"<<getpid()<<endl;signal(2,handler);//先定义两个信号集位图sigset_t block,oblock;//先对信号集清空sigemptyset(&block);sigemptyset(&oblock);//其次对2号信号添加到信号集sigaddset(&block,2);  //当前并没有让操作系统2信号屏蔽,只是语言层面的定义sigaddset(&oblock,2);sigprocmask(SIG_BLOCK,&block,&oblock);   //真正让操作系统屏蔽、更改信号//下打印pending表int cnt=0;sigset_t pending;while(true){sigpending(&pending);printpending(pending);sleep(1);cnt++;if(cnt==5){//直到5S,解除2信号的屏蔽cout<<"解除对2号信号的屏蔽,2号准备抵达"<<endl;sigprocmask(SIG_SETMASK,&oblock,nullptr); //设置为旧的信号 }}return 0;
}

 运行结果如图:

4.信号的捕捉

信号在什么时候去被捕捉处理呢,在合适的时候---从内核态返回到用户态的时候,进行信号的检测和信号的处理。

内核态:内核态是操作系统的一种状态,能够大量访问资源

用户态:用户态是一种受控的转台,能够访问的资源是有限

用户想要访问操作系统只能通过系统调用的方式访问。

首先无论进程如何调度,cpu都会找到os,我们的进程的所有代码的执行,都可以在地址空间中通过跳转的方式进行调用和返回。

 那么对于系统的信号的捕捉,首先介绍第一个接口sigaction

第三个参数表示把旧的handler表返回给我,达尔戈参数就是新的handler的设置,第一个参数为信号编号,接口的作用是检测和修改信号动作。

返回类型是sigaction的结构体类型,其中有五个字段。其中我们比较重点关注的是sa_mask字段,

如果在调用信号处理函数时,除了当前信号被屏蔽外,还希望屏蔽些别的信号,此时sa_mask就是需要被额外屏蔽的信号。

以该代码为例:

#include<signal.h>
#include<unistd.h>
#include<iostream>
using namespace std;
void print(sigset_t &pending);
void handler(int signo)
{cout<<"接收到信号"<<signo<<"......"<<endl;while(true){//获取当前pending列表sigset_t pending;sigpending(&pending);print(pending);sleep(1);}
}
void print(sigset_t &pending)
{for(int signo=31;signo>0;signo--){if(sigismember(&pending,signo)){cout<<"1";}else{cout<<"0";}}cout<<endl;
}
int main()
{cout<<"my pid is "<<getpid()<<endl;//定义新的与旧的actstruct sigaction act,oact;//设置handler为当前自定义的处理方法act.sa_handler=handler;sigaction(2,&act,&oact);while(true) sleep(1);return 0;
}

用改接口接受2号信号时,和之前一样,运行程序,第一次我们ctrl+c,发出2信号时接收到2好信号,但自此之后的2好信号都被屏蔽掉了,再次crtl+c时,信号无法被接受处于未决状态。

例如:当我们要修改信号2时,这里默认会自动屏蔽信号2,如下图

信号的其它内容:

可重入函数

数被不同的控制流程调用 , 有可能在第一次调用还没返回时就再次进入该函数 , 这称 为重入,insert 函数访问一个全局链表 , 有可能因为重入而造成错乱 , 像这样的函数称为 不可重入函数 , 反之 , 如果一个函数只访问自己的局部变量或参数, 则称为可重入 (Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的 :
调用了 malloc free, 因为 malloc 也是用全局链表来管理堆的。
调用了标准 I/O 库函数。标准 I/O 库的很多实现都以不可重入的方式使用全局数据
在这里我们就这样理解,住执行流与信号捕捉流是两种不同的流。
关键字volatile
volatile 作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量 的任何操作,都必须在真实的内存中进行操作。
那么对于信号有什么作用呢?
int flag = 0;
void handler(int sig)
{printf("chage flag 0 to 1\n");flag = 1;
}
int main()
{signal(2, handler);while(!flag);printf("process quit normal\n");return 0;
}
优化情况下,键入 CTRL - C ,2 号信号被捕捉,执行自定义动作,修改 flag 1 ,但是 while 条件依旧满足 , 进 程继续运行!但是很明显flag 肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的 flag , 并不是内存中最新的flag ,这就存在了数据二异性的问题。 while 检测的 flag 其实已经因为优化,被放在了 CPU寄存器当中。如何解决呢?很明显需要 volatile。
实际中在gcc中,也是有自带优化的选项。

SIGCHLD信号

我们 早已经了解到子进程在退出的时候,是要给父进程发送退出信息的,不然父进程还要维护一份没必要的资源,而子进程是给父进程发送什么样的信号呢?---SIGCHLD

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
void handler(int sig)
{pid_t id;//收到退出信号  等待子进程while( (id = waitpid(-1, NULL, WNOHANG)) > 0){printf("wait child success: %d\n", id);}printf("child is quit! %d\n", getpid());
}
int main()
{signal(SIGCHLD, handler);pid_t cid;if((cid = fork()) == 0){//childprintf("child : %d\n", getpid());sleep(3);exit(1);}while(1){printf("father proc is running\n");sleep(1);}return 0;
}

可以看到子进程退出时,时回给父进程发信号的。

在Linux中支持手动忽略信号SIGCHDL,可以不用wait子进程。退出自动回收。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/504830.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络工程师笔记6

ICMP协议 Internet控制报文协议ICMP(InternetControlMessage Protocol)是网络层的一个重要协议。ICMP协议用来在网络设备间传递各种差错和控制信息&#xff0c;它对于收集各种网络信息、诊断和排除各种网络故障具有至关重要的作用。使用基于ICMP的应用时&#xff0c;需要对ICMP…

Docker中使用nginx-rtmp推拉网络摄像头视频流

前言&#xff1a; 该部分比较麻烦&#xff0c;闹腾了好久&#xff08;ffmpeg推拉流没学过&#xff0c;事实证明依葫芦画瓢是不行滴&#xff0c;后面有时间再学吧&#xff09;&#xff0c;后来借助chatGPT勉强解决&#xff0c;但不是很懂。因个人能力有限&#xff0c;只复述操作…

Unity(第十七部)Unity自带的角色控制器

组件Character Controller 中文角色控制器 using System.Collections; using System.Collections.Generic; using UnityEngine;public class player : MonoBehaviour {private CharacterController player;void Start(){player GetComponent<CharacterController>();}v…

DbSchema导出HTML/PDF版表结构

一、连接数据库 登录成功默认显示当前用户的所有资源&#xff08;表、视图、序列、方法、触发器等&#xff09;&#xff0c;如果不操作将导出此用户的全部信息。 至此连接数据库完成 二、表结构导出 本次不想给用户全部导出&#xff0c;只给导出几张&#xff0c;选择需要…

记录西门子:SCL博图

算术表达式: 关系表达式&#xff1a; 逻辑表达式&#xff1a; 赋值运算

数据结构与算法:堆

朋友们大家好啊&#xff0c;本篇文章来到堆的内容&#xff0c;堆是一种完全二叉树&#xff0c;再介绍堆之前&#xff0c;我们首先对树进行讲解 树与堆 1.树的介绍1.1节点的分类 2.树的存储结构3.二叉树的概念和结构3.1 二叉树的特点3.2 特殊的二叉树3.3二叉树的存储结构 4.堆的…

Java学习--学生管理系统(残破版)

代码 Main.java import java.util.ArrayList; import java.util.Scanner;public class Main {public static void main(String[] args) {ArrayList<Student> list new ArrayList<>();loop:while (true) {System.out.println("-----欢迎来到阿宝院校学生管理系…

STM32 | 零基础 STM32 第一天

零基础 STM32 第一天 一、认知STM32 1、STM32概念 STM32:意法半导体基于ARM公司的Cortex-M内核开发的32位的高性能、低功耗单片机。 ST:意法半导体 M:基于ARM公司的Cortex-M内核的高性能、低功耗单片机 32&#xff1a;32位单片机 2、STM32开发的产品 STM32开发的产品&a…

Transformer中的自注意力机制计算过程分析

目录 1 什么是自注意力机制 2 自注意力的计算过程 1 什么是自注意力机制 自注意力机制&#xff08;Self-Attention&#xff09;顾名思义就是关注单个序列内部元素之间的相关性&#xff0c;不仅可以用于 seq2seq 的机器翻译模型&#xff0c;还能用于情感分析、内容提取等场景…

Python列表生成式你学会了吗

1.最基本的列表生成方式 生成 1-10 之间的整数的一个列表 list1 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] print(list1) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] list2 list(range(1, 11)) print(list2) # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 2.通过程序的方式生成[4, 9, 16, 25,…

【软考】设计模式之访问者模式

目录 1. 说明2. 应用场景3. 结构图4. 构成5. java示例5.1 喂动物5.1.1 抽象访问者5.1.2 具体访问者5.1.3 抽象元素5.1.4 具体元素5.1.5 对象结构5.1.6 客户端类5.1.7 结果示例 5.2 超市销售系统5.2.1 业务场景5.2.2 业务需求类图5.2.3 抽象访问者5.2.4 具体访问者5.2.5 抽象元素…

3.1日学习打卡----初学FastDFS(一)

3.1日学习打卡 目录: 3.1日学习打卡一. 为什么要使用分布式文件系统二. FastDFS简介核心概念上传机制下载机制FastDFS环境搭建_LinuxFastDFS指令 一. 为什么要使用分布式文件系统 单机时代 初创时期由于时间紧迫&#xff0c;在各种资源有限的情况下&#xff0c;通常就直接在项…