信号量 , 进程信号( 产生, 注册, 注销, 捕捉, 自定义信号处理方式, 处理, 阻塞, 重入 )

1. 信号量

信号量:
    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, 也就是意味着同一时刻只有一个进程可以访问到临界资源.
	

2. 进程信号:

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信号, 在几秒后执行这个信号

注意:

  • Ctrl+C 产生的信号只能发给前台进程。一个命令后面加个&可以放到后台运行,这样Shell不必等待进程, 结束就可以接受新的命令,启动新的进程。
  • Shell可以同时运行一个前台进程和任意多个后台进程 , 只有前台进程才能接到像 Ctrl+C 这种控制键产生的信号。
  • 前台进程在运行过程中用户随时可能按下 Ctrl+C 而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止 , 所以信号相对于进程的控制流程来说是异步的

3. 信号的原理

  • 信号注册和注销

信号量 , 进程信号( 产生, 注册, 注销, 捕捉, 自定义信号处理方式, 处理, 阻塞, 重入 )_第1张图片

       信号注册
            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参数表示说: 哪一个信号触发了内核调用该自定义处理函数, 传递给自定义信号那个触发他的那个信号的值(序号)

信号量 , 进程信号( 产生, 注册, 注销, 捕捉, 自定义信号处理方式, 处理, 阻塞, 重入 )_第2张图片
理解自定义信号处理方式
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结构体
   
  1. 可重入&不可重入

    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: 保持变量的内存可见性
	让程序在使用变量的时候, 都是从内存当中获取值

你可能感兴趣的:(linux知识点,linux)