【Linux】信号量与信号
时间:2024-04-09 14:35:53 来源:网络cs 作者:纳雷武 栏目:选词工具 阅读:
目录
先导知识
信号量
信号
信号概念及产生信号的一般方式
进程递达、阻塞和捕捉
信号集操作函数
信号的捕捉
可重入函数
先导知识
信号量与信号没有任何关系,它们是两个完全不同的概念!
操作系统的本质,就是一个死循环;操作系统的执行,时基于各种硬件中断的!
所有用户的行为,都是以进程的形式在 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先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了,与预期结果不符合,因此不是可重入函数!
本文链接:https://www.kjpai.cn/news/2024-04-09/156042.html,文章来源:网络cs,作者:纳雷武,版权归作者所有,如需转载请注明来源和作者,否则将追究法律责任!
上一篇:【Linux】信号量与信号
下一篇:返回列表