【Linux】信号量与信号

目录

先导知识

信号量

信号

信号概念及产生信号的一般方式

进程递达、阻塞和捕捉

信号集操作函数

信号的捕捉

可重入函数


先导知识

信号量与信号没有任何关系,它们是两个完全不同的概念!

操作系统的本质,就是一个死循环;操作系统的执行,时基于各种硬件中断的!

所有用户的行为,都是以进程的形式在 OS 中表现得,操作系统只要把进程调度好,就能完成所有得用户任务。

在终端中将一个可执行程序放在后台运行:./xxx & (在运行前台进程的基础后面加一个 &),前台进程只能有一个,后台进程可以有多个。

volatile 关键字作用:保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被编译器以任何方式优化,对该变量的任何操作,都必须在真实的内存中进行操作。

操作系统的用户态和内核态
用户态受控状态,只能访问自己的 0~3G 空间
内核态可以让用户以 OS 的身份可以多访问通用的那 3~4G 空间
状态转换时间在进行系统调用的时候,从用户态切换至内核态

信号量

信号量维护一个计数器,表示可用资源的数量。当一个执行流想要去访问公共资源内的某一份资源时,需要先去申请信号量资源,并不是直接访问,而申请信号量资源,其实就是对信号量的的计数器进行 -- (减减)操作,当减减操作执行成功后,这个执行流就完成了对资源的预定工作!

那为什么访问公共资源前要在先做这么多事呢?

在进行进程间通信的时候,多个执行流会看到同一份资源,而它们可能会同时对这一份公共资源进行并发访问,这样就会导致数据不一致的问题,所以,就需要将公共数据保护起来,被保护起来的公共资源,叫临界资源,而访问该临界资源的代码,叫临界区!临界资源一次只允许一个进程访问,实现的方法有:互斥和同步

同步的方式有:匿名管道、命名管道,消息队列等

而互斥的实现方法,就是维护一个值为1的信号量,当有执行流申请信号量资源成功的时候,就将信号量的值减减,减减后信号量的值就变为了0,当信号量的值为0的时候,临界资源就不能被访问了,这样就实现了,这块共享内存(临界资源)只能同一时间只能被一个进程访问!

但信号量本质上也是公共资源(因为信号量资源会被多个进程看到),所以在内核系统层面,维护着一个专门设计的管理进程间通信的IPC模块资源,里面使用了类似多态的原理,使用 strcut kern_ipc_perm*  类型的数组来同一管理 IPC 资源,而信号量也是存储在这个 IPC 体系里的,因为在内核中,所以进程都能够访问,也就变成了公共资源。 

信号

信号概念及产生信号的一般方式

在操作西系统中,信号是进程之间事件异步通知的一种方式,属于软中断,例如,当在 shell 命令行启动一个前台进程后,在键盘按下 ctrl + C 组合键,ctrl + C 就会被OS获取,解释成一个信号,前台进程因为受到了信号,进而引起进程退出!信号是一种向目标进程发送通知进程的一种消息机制,本质就是软件,用来模拟中断的行为!

使用 kill -l 命令可以查看系统定义的信号列表

kill -l

进程产生信号有四种方式:

  • 通过终端按键产生信号
  • 通过系统调用函数向进程发信号
  • 由软件条件产生信号
  • 由硬件异常产生信号

1、由终端按键产生信号

常见的有:

ctrl + c —— 发出 2 号信号(中断信号)

ctrl + \ —— 发出 3 号信号(离开信号)

ctrl + z —— 发出 20 号信号(暂停信号) 

可以使用下面的程序来验证,但需要知道当前进程的 pid,否则可能会造成进程无法关闭的情况

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
    cout << "  I get a sig:" << signo << " ___mypid: " << getpid() << endl;
}
int main()
{
    for(int i = 0; i <= 64; i++) signal(i, handler);
    while(1);
    return 0;
}

2、调用系统函数向进程发信号 

// kill函数可以给一个指定的进程发送指定的信号。
// raise函数可以给当前进程发送指定的信号(自己给自己发信号)
// 这两个函数都是成功返回0,错误返回-1
#include <signal.h>
int kill(pid_t pid, int signo);
int raise(int signo);
// abort函数使当前进程接收到信号而异常终止。
#include <stdlib.h>
void abort(void);

kill 命令,也都是通过调用 kill 函数来实现的

指定发送某种信号的kill命令可以有多种写法,上面的命令还可以写成 kill -signal id 或 kill - i id , signal 是具体某个信号,i 是这个信号的编号,id 被发送信号的进程 pid

3、通过软件条件产生信号

SIGPIPE 就是管道中一种由软件产生的信号,当读端关闭,写端一直写入,OS会直接杀掉写端进程,通过向目标文件发送 SIGPIPE(13) 信号,终止目标进程,这就是一种软件条件。

还有一种软件条件,叫 “闹钟”

#include <unistd.h>
unsigned int alarm(unsigned int seconds);

调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号,该信号的默认处理动作是终止当前进程。当在 seconds 秒之前,进程被被终止了,而信号还没有发送,它剩余的秒数就会被 “保存” 在 alarm 函数中,当下次再调用 alarm 函数时,只需将 seconds 值设置为0,表示取消以前设定的闹钟,alarm 函数的返回值就可以得到,是以前设定的闹钟时间还余下的秒数。

4、硬件异常产生信号

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

CMOS 周期性的、高频率的在给CPU发送时钟中断,通过这种中断来使操作系统的各种调度方法运行起来了!

进程递达、阻塞和捕捉

实际执行信号的处理动作称为信号递达(Delivery)

信号从产生到递达之间的状态,称为信号未决(Pending)。

进程可以选择阻塞 (Block )某个信号。

被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达(没有恢复不阻塞的情况下),而忽略是在递达之后可选的一种处理动作。

每种信号在进程种都有两个标志位,block 和 pending(比特位的位置表示编号)还有一个函数指针表示处理动作。标志位在进程的 task_struct 中是通过位图来维护的,通过比特位来控制 “有没有”、“在不在”。例如,block 位图的最低一个比特位就表示是否对 1 号信号进行阻塞,pending 位图的最低一个比特位就表示是否收到 1 号信号。

每个信号都只有一个 bit 标志,非0即1,不记录信号产生 / 收到了多少次,未决和阻塞标志可以用相同的数据类型 sigset_t 来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态。

在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。

信号集操作函数

sigset_t 类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释。

1、设置信号集

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);

函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。
函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。 

注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回0,出错返回-1。

而sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。

2、维护信号集

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

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
// 返回值:若成功则为0,若出错则为-1

如果oset是非空指针,则读取进程的当前信号屏蔽字通过 oset 参数传出(输出型参数)。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据 set 和 how 参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

如果调用 sigprocmask 解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

3、读取当前进程的信号集

#include <signal.h>
sigpending(&s);

sigpending 函数可以读取当前进程的未决信号集,通过 s 参数传出。调用成功则返回0,出错则返回-1。

信号的捕捉

进程在从内核态返回到用户态的时候,进行信号的检测和处理。

那么内核如何实现信号捕捉呢?

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。在中断处理完毕后要返回用户态的 main 函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数。sighandler 函数和 main 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态。如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

在信号捕捉中,一共会涉及到四次状态转换!

可重入函数

可重入函数(reentrant function)是指在多个执行流下,能够被同时调用而不会产生冲突或错误的函数。

这种函数能够保证在任意时刻,无论被同一个还是不同执行流调用,都能正确地完成预期的功能。

如果一个函数符合以下条件之一则是不可重入的:

  • 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

下图为函数不可重入的举例:

main 函数调用 insert 函数向一个链表 head 中插入节点 node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler 也调用 insert 函数向同一个链表 head 中插入节点node2,插入操作的 两步都做完之后从sighandler 返回内核态,再次回到用户态就从 main 函数调用的 insert  函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了,与预期结果不符合,因此不是可重入函数!

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Django日志(四)

一、Filters介绍 过滤器用于从logger传递给handler的哪些日志要做额外控制 默认情况下,满足日志级别的任何消息都将处理。只要级别匹配,任何日志消息都会被处理。不过,也可以通过添加 filter 来给日志处理的过程增加额外条件。例如,可以添加一个 filter 只允许某个特定来源…

【C++】模板与泛型编程

文章目录 1. 泛型编程2. 函数模板2.1 函数模板概念2.2 函数模板格式2.3 函数模板的原理2.4 函数模板的实例化2.5 模板参数的匹配原则 3. 类模板3.1 类模板的定义格式3.2 类模板的实例化 4. 非类型模板参数5. 模板的特化5.1 概念5.2 函数模板特化5.3 全特化5.4 偏特化5.5 类模板…

Android 系统应用 pk8签名文件转jks或keystore教程

一、介绍 签名文件对于我们在做应用开发中&#xff0c;经常遇到&#xff0c;且签名文件不仅仅是保护应用安全&#xff0c;还会涉及到应用与底层之间的数据共享和API文件等问题。 在Android中&#xff0c;签名文件同样也存在这个问题。但是android中又区分系统应用和普通应用。系…

汉明校验·简明教程

汉明校验 一、简介 汉明码是由 Richard Hanming 于 1950 年提出的&#xff0c;它具有一位纠错能力。 新增的汉明码校验位数应满足如下关系&#xff1a; 2 k ⩾ n k 1 2^{k}\geqslant nk1 2k⩾nk1&#xff0c;其中k为校验位位数&#xff0c;n位数据位数。 二、汉明码生成 确…

centos7 的redis的安装

文章目录 查看本机redis⾸先安装 scl 源, 再安装 redis 基本配置启动redis停止redis 查看本机redis ⾸先安装 scl 源, 再安装 redis 安装scl源 yum install centos-release-scl-rh安装redis5 yum install rh-redis5-redis安装成功 基本配置 修改etc/redis/redis.conf 文件…

代码随想录算法训练营第二十一天(二叉树VII)| 530. 二叉搜索树的最小绝对差、501. 二叉搜索树中的众数、236. 二叉树的最近公共祖先(JAVA)

文章目录 530. 二叉搜索树的最小绝对差解题思路源码 501. 二叉搜索树中的众数解题思路源码 236. 二叉树的最近公共祖先解题思路源码 530. 二叉搜索树的最小绝对差 给你一个二叉搜索树的根节点 root &#xff0c;返回 树中任意两不同节点值之间的最小差值 。 差值是一个正数&a…

如何在 Ubuntu 安装桌面环境

在 Ubuntu 上安装不同的桌面环境 如果你正在使用官方的 Ubuntu 发行版&#xff0c;它运行在 GNOME 上&#xff0c;那么你可以很容易地从默认的包管理器安装其他流行的桌面环境&#xff08;DE&#xff09;。让我们开始吧… 在 Ubuntu 上安装 KDE Plasma 如果你正在使用 GNOME…

JAVA使用POI实现Excel单元格合并-02

JAVA使用POI实现Excel单元格合并 实现效果 解释&#xff1a;只要是遇见与前一行相同的数据就合并 引入jar <dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>5.2.2</version></depe…

第114讲:Mycat实践指南:按照单位为月的日期实现水平分表

文章目录 1.按月分片的概念1.按月分片的概念 2.按照天数对某张表进行水平拆分2.1.在所有的分片节点中创建表结构2.2.配置Mycat实现字符串按月分片的水平分表2.2.1.配置Schema配置文件2.2.2.配置Rule分片规则配置文件2.2.3.配置Server配置文件2.2.4.重启Mycat 2.3.写入数据观察分…

ora-00314 00312

背景&#xff1a;某医院数据库打不开&#xff0c;alter database open报错&#xff08;跟我说是被勒索了。。&#xff09; 查看日志组信息&#xff1a; select group#,sequence#,archived,status from v$log;处理方法&#xff1a; 若该组是非当前状态&#xff0c;而且未归档&…

Kubernetes Pod深度解析:构建可靠微服务的秘密武器(上)

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 目录 一、引言 1、Kubernetes概述 2、Pod概述 二、Po…

FastAPI+React全栈开发02 什么是FARM技术栈

Chapter01 Web Development and the FARM Stack 02 What is the FARM stack and how does it fit together? FastAPIReact全栈开发02 什么是FARM技术栈 It is important to understand that stacks aren’t really special, they are just sets of technologies that cover…

Python学习:条件控制

Python条件控制概念 条件控制是编程中的一个重要概念&#xff0c;用于根据不同情况执行不同的代码逻辑。在Python中&#xff0c;条件控制通常使用if语句来实现。if语句的基本语法如下&#xff1a; if 条件:执行语句 elif 其他条件:执行语句 else:执行语句其中&#xff0c;if…

2016年认证杯SPSSPRO杯数学建模C题(第二阶段)如何有效的抑制校园霸凌事件的发生全过程文档及程序

2016年认证杯SPSSPRO杯数学建模 C题 如何有效的抑制校园霸凌事件的发生 原题再现&#xff1a; 近年来&#xff0c;我国发生的多起校园霸凌事件在媒体的报道下引发了许多国人的关注。霸凌事件对学生身体和精神上的影响是极为严重而长远的&#xff0c;因此对于这些情况我们应该…

网络七层模型之网络层:理解网络通信的架构(三)

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

基于傅里叶描述子和HSV颜色特征的KNN水果类型识别,Matlab实现

博主简介&#xff1a; 专注、专一于Matlab图像处理学习、交流&#xff0c;matlab图像代码代做/项目合作可以联系&#xff08;QQ:3249726188&#xff09; 个人主页&#xff1a;Matlab_ImagePro-CSDN博客 原则&#xff1a;代码均由本人编写完成&#xff0c;非中介&#xff0c;提供…

【物联网】Qinghub Kafka 数据采集

基础信息 组件名称 &#xff1a; kafka-connector 组件版本&#xff1a; 1.0.0 组件类型&#xff1a; 系统默认 状 态&#xff1a; 正式发布 组件描述&#xff1a;通用kafka连接网关&#xff0c;消费来自kafka的数据&#xff0c;并转发给下一个节点做相关的数据解析。 配置文…

【智能算法】乌鸦搜索算法(CSA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2016年&#xff0c;Askarzadeh等人受到乌鸦觅食自然行为启发&#xff0c;提出了乌鸦搜索算法&#xff08;Crow Search Algorithm, CSA&#xff09;。 2.算法原理 2.1算法思想 CSA模拟了乌鸦进行觅…

CUDA从入门到放弃(四):CUDA 编程模式 CUDA Programming Model

CUDA从入门到放弃&#xff08;四&#xff09;&#xff1a;CUDA 编程模式 CUDA Programming Model 1 Kernels CUDA C 扩展了 C&#xff0c;允许定义名为内核的函数&#xff0c;这些函数可以被不同的 CUDA 线程并行执行多次&#xff0c;而不是像普通 C 函数那样只执行一次。内核…

Python数据结构实验 递归算法设计

一、实验目的 1&#xff0e;掌握递归程序设计的基本原理和方法&#xff1b; 2&#xff0e;熟悉数据结构中顺序表和单链表下的递归算法设计思想&#xff1b; 3&#xff0e;掌握并灵活运用递归算法解决一些较复杂的应用问题。 二、实验环境 1&#xff0e;Windows操作系统的计…