信号量:
system V版本信号量:(并不能支持跨平台)
1. 本质: 是一个计数器(保存资源数量) + PCB等待队列(存放被阻塞的进程的PCB)
两个进程都想去访问临界资源(共享内存当中保存了一个变量shm_cout = 10), 一旦涉及
到修改变量shm_cout, 容易造成数据二义, 要访问依赖CPU, 帮我们进程A和进程B计算, A拿
着CPU去计算,先从共享内存读出变量的值, 放到寄存器当中, 寄存器 = 10, 想要执行10 +
1, 但被打断了,A把CPU让出来了, B拿去计算, 完成对变量的++, 又把CPU让出来, A要用
CPU计算,得先恢复现场, 而且寄存器的值还是10, ++后shm_cout变成11, 又回写到共享内存
中,
理论情况下, 进程A+1了, 进程B也加1了, 对于shm_cout理论上值应为12, 但是由于进
程A在+1时被打断了, 导致最终shm_cout的值为11
方案:
在访问临界资源之前, 先去获取一个信号量, 这样的一个信号量中有两个量(计数器和
PCB等待队列), 若资源中变量数变为2 ,则计数器的值+1也为2, 要拿信号量,需要对我们
的计数器进行预减操作, 1 --> 0,如果计数器的值大于等于0(x-1>=0),x>=1,>0, 也就
意味着资源是有空闲的, 我们进程是可以去获取资源的(对计数器减1, 访问临界资源), 如
果/当计数器的值是小于0的, 也就意味着资源不是空闲的, 进程不可用访问资源, 如果这
个时候还需要访问资源, 则将该进程的PCB放到PCB等待队列中, 当进程A把资源归还给共
享内存时, 将计数器当中的值进行+1, 然后唤醒PCB等待队列中的进程B
获取信号量:
1. 对信号量当中的计数器进行预减操作
如果计数器的值是大于等于0, 也就是意味着资源是有空闲的, 进程可以先去获取资源
对计数器进行减1操作之后, 去获取资源
如果计数器的值是小于0的, 也就是意味着资源不是空闲的, 进程不可以访问资源, 如果这个时候还需要访问资源,则将该进程的PCB放到PCB等待队列当中去.
归还临界资源:
将计数器当中的值进行加1操作, 唤醒PCB等待队列当中的进程
如何保证互斥:
只需要将信号量当中的计数器的值设置为1, 也就是意味着同一时刻只有一个进程可以访问到临界资源.
1. 信号的基本概念
信号是进程之间事件异步通知的一种方式,属于软中断, 当一个进程收到一个信号的时候, 信号就会打断当前进程, 处理不
处理取决于内核(我们), 死了怪自己
红绿灯,释放的信号促使我们等待和通过, 通过这样的信号给我们传递信息, 撞死不负
责任(只是告诉信息,并没有拉住你) --> 软件中断
信号的种类:
linux操作系统中有62个信号(kill -l),
前31个(1-31): 不可靠信号, 非实时信号, 信号有可能会丢失,
后31个(34-64): 可靠信号, 信号不会丢失
2. 信号的产生方式
硬件产生:
ctrl + c: 给进程发送SIGINT信号, 这样的一个信号会导致进程退出;
相当于 kill + 信号序号 + 进程号
ctrl + z: 给进程发送SIGTSTP信号, 让一个前台进程放到后台, 并且进程状态为T, 暂停状态
ctrl + |: 给进程发送SIGQUIT信号, 产生coredump文件, 从键盘退出
SIGINT的默认处理动作是终止进程,SIGQUIT的默认处理动作是终止进程并且Core Dump,
软件产生:
1. int kill(pid_t pid, int sig); 包含signal.h头文件
pid: 相应发送信号给哪一个进程
sig: 给自己发送什么信号, 信号序号
2. void abort(void)
abort函数, 谁用谁退出
3. unsigned int alarm(unsigned int seconds),
触发之后接收到SIGALRM信号, 在几秒后执行这个信号
注意:
3. 信号的原理
信号注册
1. 非可靠信号
第一次注册
更改sig位图当中信号对应的比特位, 就是将对应的比特位由0置为1, 在sigqueue队列中添加相应信号的处理节点(包含该信号将要处理的行为);
再次注册
背景: 意味着还没有来得及处理已经注册的非可靠信号, 这时进程又收到了同样的非可靠信号,
1. 先判断对应信号的比特位是否为1, 如果为1, 则不会添加sigqueue节点到sigqueue队列中,
这就是为什么1-31是非可靠信号, 就是由于再次注册的时候会判断是否已经有该信号了, 如果有, 则丢弃新来的信号(就没有做任何新的处理)
2. 可靠信号
第一次注册
更改sig位图当中信号对应的比特位, 就是将对应的比特位由0置为1, 在sigqueue队列中添加相应信号的处理节点(包含该信号将要处理的行为);
再次注册
1. 判断信号对应的比特位是否为1, 如果为1, 则不修改比特位; 如果为0, 则修改为1
2. 修改完比特位后, 还需要添加对应的sigqueue节点到sigqueue队列当中去, 不管sigqueue当中是否有相同的可靠信号的sigqueue节点.
3. 小结:
非可靠信号 : 当进程还没处理非可靠信号的时候,信号又多次注册,但我们不会添加sigqueue节点
可靠信号 : 当进程还没有处理可靠信号的时候, 可靠信号又多次注册, 会依次添加sigqueue节点
当进程在处理信号时, 是根据sigqueue队列当中的节点来依次处理的, 所以
非可靠信号注册多次, 只会处理一次
可靠信号注册多次, 会处理多次
位图中比特位清零的情况是信号的注销(就是进程处理完信号之后才会清零), 置为0, sigqueue节点也就出队了,再来个同样信号, 比特位置为1, sigqueue节点入队,
信号注销过程:
非可靠信号的注销:
将sig位图当中对应信号的比特位置为0, 并且将sigqueue队列当中对应信号的sigqueue节点进行出队操作
出队操作: 内核执行相应信号的处理方式
可靠信号的注销:
1. 判断对应信号在sigqueue队列当中的节点数量,
如果信号节点数量等于1, 将sig位图当中对应信号的比特位置为0, 并且将sigqueue队列当中对应信号的sigqueue节点进行出队操作
如果信号节点数量等于大于1, 就不能将对应信号的比特位置为0(也就意味着还是为1), 将对应信号的一个sigqueue节点进行出队操作
4. 信号的捕捉
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,
举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
信号处理器程序(也称为信号捕捉器)是当指定信号传递给进程时将会调用的一个函数。
调用信号处理器程序,可能会随时打断主程序流程。内核代表进程来调用处理器程序,当处理器返回时,主程序会在处理器打断的位置恢复执行。
1. 信号都有哪些处理方式?
默认处理方式: SIG_DEL
忽略处理方式: SIG_IGN ~~ 不干任何事情
面试题: 当一个子进程退出的时候, 为什么会形成僵尸进程???
之前: 进程退出的时候, 父进程来不及回收子进程的退出信息, 导致子进程的退出信息(PCB)还在内核当中
反问: 为什么来不及回收?????
答案: 子进程退出的时候 会给父进程发送SIGCHLD信号, 而操作系统定义SIGCHLD信号的处理方式为忽略(不会儿子)
自定义处理方式: 程序员自己定义信号的处理函数
typedef void(*signhandler_t)(int)
sighandler_t signal(int signum, sighandler_t handler);
signum: 哪一个信号需要重新自定义
handler:
sighandler_t 是一个函数指针, 接收的是没有返回值(void), 有一个int类型参数的函数地址(也就是自己定义的函数)
int参数表示说: 哪一个信号触发了内核调用该自定义处理函数, 传递给自定义信号那个触发他的那个信号的值(序号)
理解自定义信号处理方式
struct task_struct这个结构体有struct sighand_struct* sighand结构体指针 --> 指向struct sighand_struct结构体 --> 里面有struct k_sigaction action[_NSIG]数组 --> action[xxx]这个数组中的每一个元素的类型都是struct k_sigaction --> 这是一个结构体类型, 这个结构体中还有struct sigaction sa这样的结构体变量, --> struct sigaction也是一个结构体, 这个结构体中有_sighandler sa_handler这个变量 —> 而这个_sighandler其实就是typedef void(*sighandler_t)(int); —> 所以sa_handler这个变量是一个函数指针, 他保存的就是函数地址,即就是在sa_handler当中保存信号的处理函数地址, --> action[xxx]这个数组里有很多元素,对应了62个信号 每个方式的处理方式,
所以当用户调用signal函数更改信号的自定义处理函数的时候, 其实就是更改掉了sa_handler中保存的处理函数的地址(更改为我们现在的自定义函数的地址 而不是之前的处理函数) sa_handler当中保存信号的处理函数的地址, 这个地址决定最终的执行方式 决定sigqueue节点是否出队
那么所有的信号都可以自定义处理函数吗? ? ? ? ? ? ? ? ? ?
答案:
不是的, 9号信号(SIGKILL)是不可以自定义信号处理函数的;
19号信号也是不可以自定义信号处理函数的
int sigaction(int signum, const struct signaction* act, const struct signaction* oldact)
这个函数也是可以自定义信号的处理函数的
signum: 需要自定义的信号
signal函数本身没有阻塞作用, 我们只是更改了sa_handler里面保存的地址, 更改完成之后就退出了; 但是如果signal函数中加入一个循环, 当收到一个信号后, 内核去调用sigcb(我们自己定义的函数), 导致当前执行流被中断,
sigaction函数有阻塞的功能,同时也可以自定义心寒的处理函数的; 比如SIGINT信号来了,进入信号处理函数,默认情况下,在信号处理函数未完成之前,如果又来了一个SIGINT信号,其将被阻塞,只有信号处理函数处理完毕,才会对后来的SIGINT再进行处理,同时后续无论来多少个SIGINT,仅处理一个SIGINT,sigaction会对后续SIGINT进行排队合并处理。
struct sigaction{
void (*sa_handler)(int) 操作系统为每一个信号定义的默认调用函数
void (*sa_sigaction)(int, siginfo_t*, void*); 这也是一个函数指针, 但是这个函数指针一般是预留的, 需要配合sa_flags进行使用
sigset_t sa_mask; 当一个进程在处理信号的时候, 也有可能接受都按新的信号, 将新的信号放在sa_mask当中, sa_mask的类型是sigset_t(unsigned long sig[_NSIG_WOEDS]数组, 这样的数组被叫做sig位图),其实就是一个sig位图
int sa_flags; 当sa_flags为SA_SIGINFO的时候, 配合sa_sigaction进行使用
};
1. 默认情况下, 在处理一个信号的时候, 使用sa_handler当中保存的函数地址
2. 当sa_flags为SA_SIGINFO的时候,
sigaction函数是直接更改struct sigaction这样的结构体的;
在这个结构体中,
void(*sa_handler)(int); 这里保存的是信号默认的处理函数, 改其实就是改这里的
signal函数是直接改函数指针, 而sigaction是直接改结构体 函数指针是包含在结构体里的
signal函数就是调用sigaction函数的
act:需要更改信号的处理函数而传入的struct sigaction结构体
oldact: 信号之前的struct sigaction结构体
可重入&不可重入
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函 数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的:
1. 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
2. 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
6. 啥时候处理信号
执行系统调用和库函数时, 其实是从用户空间切换到操作系统内核空间去执行的
在内核空间执行main函数里的sleep函数完后,若接收到一个信号时, 会调用一个do_signal函数返回到用户空间去执行sigcb函数,执行完后又要调用sigreturn函数返回到内核空间,再调用do_signal函数, 若是没有下一个信号, 再调用sysreturn函数返回到用户空间
do_signal函数会先判断有没有信号,是处理信号的函数,
每次系统调用进入内核系统后, 处理完成系统调用函数的逻辑后在返回用户空间之前一定要调用do_signal函数
7. 信号的阻塞
进程处理信号的时候, 会先判断block位图当中对应的比特位是否为1, 如果为1, 则暂时不处理;只有置为0 后才会处理这个信号
但是信号的注册还是可以正常注册的
信号的阻塞并不是说信号不能被注册, 不影响信号更改pending位图和增加sigqueue节点, 只不过是让我们暂时不去处理他
用户空间 --> 内核空间 --> 返回前调用do_signal函数, 发现某一个信号的比特位被置为1了, 想要处理这个信号之前, 先判断block位图当中对应信号的比特位是否为1,
输入信号, 先找到pending位图(判断信号是否注册了), 再看block(看是否阻塞, 为0则其sigqueue队列拿节点, 若为1,就不处理), 处理时才去到sigqueue队列中拿不阻塞的信号节点, 只有在处理的时候才去拿sigqueue结点,
1. 如果block位图当中对应的比特位为1, 则暂时不处理该信号, 当接触阻塞后, 才会被处理
2. 如果block位图当中对应的比特位为0, 则表示没有阻塞该信号, 正常处理
如何让某一个信号变成阻塞的?
接口:
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);
how: 通过不同的宏定义让sigprocmask执行不同的逻辑
SIG_BLOCK --> 设置某个信号阻塞
SIG_UNBLOCK --> 设置某个信号为非阻塞
SIG_SETMASK --> 设置新的block位图
set: 要设置新的block位图
oldset: 老的block位图
使用:
SIG_BLOCK:
block(new) = block(old) | set (若前者阻塞2号, 后者想阻塞3号, 则新的block位图则会阻塞2和3)
SIG_UNBLOCK:
block(new) = block(old) & (~set) (将想要取消阻塞状态的这个比特位置为1,取反后该位变为0, 其他位全变为1, 但是老block位图中除了阻塞信号比特位为1之外, 其他的比特位都是1, 此时将取反的set和老block按位或, 0和1按位与后导致全变为了0 --> 非阻塞), 想要取消哪个比特位的阻塞状态就在set位图把这个比特位置为1,经过取反与老block按位与后最终就变为0而成为非阻塞了.
SIG_SETMASK:
block(new) = set (不关心老block位图, 想让现在的位图变啥样 就是啥样)
8. 竞态条件
多个执行流访问同一个资源的情况下, 会对程序结果产生一个二义性的结果, 称之为竞态条件(家里多个孩子争父母的给予的资源)
重入: 多个执行流访问同一个资源(多人吃一个苹果, 可能是结构体, 可能是全局变量...)
可重入: 多个执行流访问同一个资源, 不会对程序结果产生影响
不可重入: 多个执行流访问同一个资源, 会对程序结果产生二义性
编译器优化:
volatile: 保持变量的内存可见性
让程序在使用变量的时候, 都是从内存当中获取值