进程信号(信号产生、注册、注销、处理),信号阻塞和volatile关键字

进程信号

信号:信号是一个软件中断;
作用:操作系统通过信号告诉进程发生了某个事件,打断进程当前操作,去处理这个事情
操作系统中的信号:通过 kill -l 命令可以查看系统中的信号种类(62种)

1~31好信号:从Uinux借而来,每个信号都有具体对应的系统事件;(非可靠信号,有可能丢失信号);
34~64号信号:后期补充的,因为没有具体对应的事件,因此命名比较草率;(可靠信号,不会丢失信号);

信号的生命周期:1、产生;2、在进程中注册;3、在进程中注销;4、信号处理;

信号产生

硬件:ctrl+c / ctrl+z /ctrl + |
软件:kill -signum pid命令 kill默认发送15号进程 /kill(int pid, int signum) / raise(int signum) / abort( ) /alarm(int seconds)
kill杀死一个进程的原理:向进程发送一个信号,信号有对应的事件,进程放下手头工作去处理这个事件,然而事件的处理结果就是让进程退出。fg的功能是将一个暂停的后台程序调到前台运行

  • kill(进程id,信号值)
  • kill(getpid(),SIGHUP); // 给指定进程发送指定的信号
  • raise(SIGTERM); //给进程自己发送指定的信号
  • abort( ); //给自己发送SIGABRT信号通常用于异常通知
  • alarm(3); //3秒之后给进程自己发送SIGALRM信号(相当于定时器)

信号在进程中注册

如何让进程知道自己收集了某个信号;pcb -> struct sigpending -> struct sigset_t
sigset_t这个结构体中只有一个数据成员;这个数组用于实现一个位图 - - - 称之为未决信号集合(收到了但是没有被处理的信号集合);
给一个进程发送一个信号,就会将这个位图中对应位置为1,表示进程当前收到了这个信号;
信号的注册其实不仅会修改位图,还会为信号组织一个sigqueue节点添加到pcb的sigqueue链表中;

  • 1~31号非可靠信号注册:若信号注册的时候位图为0,则互创建一个sigqueue节点并修改位图为1,但是若位图为1,则什么也不做;
  • 34~64号可靠信号的注册:不管位图当前是否为0,都会创建一个节点,添加到链表中,并修改位图;

信号在进程的注销

为了保证一个信号只会被处理一次,因此先注销再处理;在pcb中删除当前信号信息;将pending位图置为0;删除信号节点;
非可靠信号注销:因为非可靠信号只会有一个节点,因此删除节点后,位图直接置为0;
可靠信号注销:因为可靠信号有可能注册多次,有多个节点,因此删除节点后,需要判断是否还有相同节点,若没有才会将位图置为0;

为什么要注销?(先注销后处理)
答:因为有这个信号需要处理,并且取出了这个信号的信息,因此注销之后就立即去处理;这样保证一个信号只会被处理一次。

信号的处理

信号表示一个事件的到来,处理事件就是完成功能,(在C语言中完成一个功能的最小模块为- - -函数)。
其实每一个信号都对应有自己的事件处理函数,信号到来,去处理这个事件就是去执行这个处理函数;执行完毕事件就处理完了。

信号的处理方式:

  1. 默认处理方式:操作系统中原定义好的每个信号的处理方式;
  2. 忽略处理方式:处理方式就是忽略,什么也不做;
  3. 自定义处理方式:自己定义一个回调函数,使用这个函数替换内核中默认的处理函数;信号到来就会调用我们定义的函数了。
    在这里插入图片描述
    typedef void(*sighandler_t)(int signo); - - - - 定义了一个名称为sighandler_t的函数指针类型。
    sighandler_t signal(int signum,sighandler_t handler);
    handler :SIG_DFL- - - 默认处理方式 / SIG_IGN - - - 忽略处理方式 / 用户自己定义的一个没有返回值,有个int型参数的函数地址。

自定义信号的捕捉流程

1、主控流程因为中断/异常/系统调用切换到内核态运行;
2、在返回用户态之前处理信号;
3、返回用户态运行自定义回调函数;
4、返回内核态;
5、返回用户态主控流程;

在这里插入图片描述
默认处理方式调用的函数与忽略处理方式调用的函数都是系统中已经实现的 - - -内核中直接处理。
自定义信号处理方式- - - -用户自己写一个事件处理函数;

信号阻塞

并不是不接收信号。信号依然可以注册,只是标识哪些信号暂时不处理。
在pcb中有一个位图,位图叫block位图 - - - 阻塞信号集合,这个集合中的信号如果来了(添加到pending位图中)则暂时不处理。
在这里插入图片描述

如何阻塞一个信号?

所有信号中,有两个信号比较特殊:SIGKILL -9 / SIGSTOP -19,这两个信号不可被阻塞,不可被忽略,不可被自定义。

int sigprocmask(int how, sigset_t *set, sigset_t *old);

how

SIG_BLOCK - - -将set集合中的信号添加到内核中的block阻塞信号集合中,使用old保存原来的阻塞信息以便于还原- - -(阻塞set集合中的信号);
SIG_UNBLOCK - - - 将set集合中的信号从内核中的block阻塞信号集合中移除- - - (对set集合中的信号解除阻塞);
SIG_SETMASK - - - 将内核中的block阻塞信号集合内容设置为set集合中的信息 - - -(阻塞set集合中的信号);
SIG_BLOCK - - -set | block;
SIG_UNBLOCK - - - set & block;
SIG_SETMASK - - - block = set;

0.将一些信号的处理函数自定义;1.将所有的信号都给阻塞;2.在解除阻塞之前,给进程发送信号;3.解除阻塞,查看信号的处理情况。

int sigemptyset(sigset_t *set); //清空set信号集合 - - - 使用一个变量时的初始化过程
int sigaddset(sigset_t *set, int signum); //向set集合中添加指定的信号
int sigdelset(sigset_t *set,int signum); //从set集合中移除指定的信号
int sigismember(const sigset_t *set,int signum); //判断指定信号是否在set集合中
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <signal.h>

void sigcb(int signo)
{
    printf("recv a signal:%d\n", signo);
}
int main()
{
    signal(SIGINT, sigcb);
    signal(SIGRTMIN+4, sigcb);

    sigset_t set;
    sigemptyset(&set);//清空集合,防止未知数据造成影响
    sigfillset(&set);//向集合中添加所有信号

    sigprocmask(SIG_BLOCK, &set, NULL);//阻塞set集合中的所有信号

    printf("press enter coninue\n");
    getchar();//等待一个回车,如果不按回车就一直卡在这里

    sigprocmask(SIG_UNBLOCK, &set, NULL);//解除set集合中的信号阻塞

    while(1) {
        sleep(1);
    }
    return 0;
}

僵尸进程:子进程退出后会向父进程发送SIGCHLD信号通知父进程,子进程的状态改变;但是因为SIGCHLD信号默认 处理方式是忽略;因此之前的程序中若不进行进程等待则不通知子进程退出。
如果进行进程等待,而且不想让父进程阻塞,就可以自定义SIGCHLD信号的处理方式;
在自定义回调函数中调用waipid,处理僵尸进程,父进程就不用一直等待;
但是SIGCHLD信号是一个非可靠信号,如果有多个子进程同时退出,有可能造成信号丢失。

while(waitpid(-1,NULL,WNOHANG)>0);//非阻塞循环在一个回调中将所有的僵尸进程全部处理
waitpid(int pid,int *status, int options)
options:WNOHANG- - -将waipid设置为非阻塞,没有子进程退出则立即报错返回;(0 - - - 默认阻塞等待子进程退出);
返回值:>0(退出的子进程pid);==0(有子进程但是没有退出); <0 (出错了,比如当前没有子进程);

关键字volatile

用于修饰一个变量,保持变量的内存可见性(cpu在处理的时候每次都重新从内存获取数据),防止编译器过度优化。
cpu处理一个数据的过程时从内存中将数据加载到寄存器上进行处理;
gcc编译器在编译的程序的时候,如果使用了代码优化 -Olevel选项,发现某个变量使用频率非常高,为了提高效率,则直接将变量值设置为某个寄存器的值,以后访问的时候直接从寄存器访问,则减少了内存访问的过程,提高效率。(但是这种优化有时候会造成代码的逻辑混乱)。
因此使用volatile关键字修饰变量,让cpu无论如何每次都重新到内存中获取数据。

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

volatile long long  a = 1;

void sigcb(int no)
{
    a=0;
    printf("a=%d\n", a);
}
int main()
{
    signal(SIGINT, sigcb);
    while(a) {
    }
    printf("exited a=%d\n", a);
    return 0;
}

函数的可重入和不可重入

函数的重入:在多个执行流程中,同时进入一个函数运行。
函数可重入:函数重入之后,不会造成数据二义或者逻辑混乱;
函数不可重入:函数重入之后,有可能会造成数据二义或者逻辑混乱。
在这里插入图片描述

#include <stdio.h>
#include <unistd.h>
#include <signal.h>

int a = 1, b = 1;

int test() {
    a++;
    sleep(3);
    b++;
    return a+b;
}

void sigcb(int no) {
    printf("signal sum:%d\n", test());
}
int main()
{
    signal(SIGINT, sigcb);
    printf("main sum:%d\n", test());
    return 0;
}

函数是否可重入的判断基准:这个函数中是否对全局变量进行了非原子的操作,若有则不可重入。
操作的原子性:操作一次完成,中间不会被打断;
原子操作:操作要么一次完成,要么就不操作;
一个函数如果根本没有操作全局数据,则肯定是可重入的,因为每个函数调用的时候都有独立的函数栈。
一个函数若对全局数据进行操作,但是操作是原子性的,则也是可重入的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章