让上次的疯癫反省出信号保存与处理

上篇太颠了

我们重说

ec863d852cd2474db3852de3ebc64f66.png

懒得喷。

 73680e5713cb426999a9744d8cd7e42b.jpeg

我们先来看看昨天的问题,我问了下chat:

a89e1d8cb2bd4508a587c8396081146e.png

我安装了: 

 301190a1828d437d90d81e8c46c6ff9a.png

哎哟我说咱要不不学这个吧

知道得了

我不想改bug了

我们开启下一个话题吧:

 977935ce509145f0a214306eaa1aa1cf.png

a93ed26ff9ee46fa9888320b164fff04.png

写一份代码:

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

int Sum(int start,int end)
{
    int sum = 0;
    for(int i = start; i <= end; i++)
    {
        sum/=0;     //core
        sum+=i;
    }
}

int main()
{
    // int total = Sum(0,100);
    // std::cout << "total: " << total << std::endl;
    pid_t id = fork();
    if(id == 0)
    {
        sleep(1);
        //child
        Sum(0,100);
        exit(0);
    }

    //father
    int status = 0;
    pid_t rid = waitpid(id,&status,0);
    if(rid == id)
    {
        printf("exit code:%d,exit sig: %d,core dump: %d\n",(status>>8)&0xFF,status&0x7F,(status>>7)&0x1);
    }
    return 0;
}

 bb7065d2f0af41068fccb80e06d38830.png

core dump是0,是因为我们没有打开,这样打开:

ulimit -c 10240

 现在这样就是咯,这辈子有了:

3d67d0022c754711b03796e7a65a7ade.png

信号保存

我们把实际自行信号的处理动作称为信号递达

信号从产生到递答之间的状态,称为信号未决

进程可以选择阻塞 (Block )某个信号,阻塞一个信号,对应信号一旦产生,则永不递达,一直是未决,直到主动解除阻塞

那么一个信号如果阻塞和它有没有未决有关系吗?

肯定是无关耶

就和猎犬和switch没什么关系一样:

cd28346ca2e94ece8d94a3efd1e3ebf7.png

33ad1606b9a0462f89fa72f96064b0e9.png

sigset_t

 每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态

阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略

内核中的task_struct会维护一张判定表,维护的是判定位图

每个比特位的位置代表信号编号

比特位的内容代表信号是否收到

pending是未决信号集

handler表是一个函数指针数组,普通信号的编号就是数组的下标

一张位图和pending类型完全一样

比特位位置代表信号编号

比特位内容代表信号是否阻塞

三个表帮我们维护了两张位图+一个函数指针数组,让进程识别信号

这是示意图:

8c72f18eb0dd4ff4a0846e10ced49cf0.png

一个表阻塞一个表未决,还有一个函数指针表示处理动作

信号产生的时候,内核在task_struct中设置该信号的未决标志 

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

阻塞和忽略不同,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作

如果将上述过程描述成生活中的场景就是(暂时以送外卖这个过程举例子):

每天就在偷偷查看实验室人进度:

82c5791453e34dae84681b656c971cc7.jpeg

偷感比较重是这样的 

墨墨酱在网上买了很多件商品,在等待不同商品快递的到来

但即便快递没有到来,墨墨酱也知道快递来临时, 她该怎么处理快递

也就是她能“识别快递” 当快递员到了她家楼下,她也收到快递到来的通知,但是她正在打游戏(拜托我姐超厉害的好嘛)

5e207cbf88ee4304a73f140732444153.png

 f7702fb6c9704b56832f75bebf19ad85.png

f1a0ae05c71048808a89e6b40b841100.png

需5min之后才能去取快递。那么在这5min之内,墨墨酱并没有下去取快递,但是墨墨酱是知道有快递到来了。也就是取快递的行为并不是一定要立即执行(在合适的时候去取)

在收到通知,再到墨墨酱拿到快递期间,是有一个时间窗口的,在这段时间,墨墨酱并没有拿到快递,但是她知道有一个快递已经来了。本质上是她“记住了有一个快递要去取”

当墨墨酱打完黑吗喽时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:

1. 执行默认动作(幸福的打开快递,使用商品)

2. 执行自定义动作(快递是零食,墨墨酱要分享给励志轩吃)

3. 忽略快递(快递拿上来之后,扔在床头,继续开一把游戏)82f27db6a21348a3abf0e6a51c627b67.png

快递到来的整个过程,对墨墨酱来讲是异步的,因为她不能准确断定快递员什么时候给她打电话

一些小tips:

🥑 Ctrl+C 产生的信号只能发给前台进程

🥑 一个命令后面加个&可以放到后台运行,这样Shell不必等待进程 结束就可以接受新的命令,启动新的进程

🥑 Shell可以同时运行一个前台进程和任意多个后台进程,只有前台进程才能接到像Ctrl-C这种控制键产生的信号

🥑 前台进程在运行过程中用户随时可能按下 Ctrl-C 而产生一个信号,该进程的用户空间代码执行到任何地方都有可能收到 SIGINT 信号而终止,信号相对于进程的控制流程来说是异步 (Asynchronous)的

信号处理动作:

🥑 忽略此信号

🥑 执行该信号的默认处理动作

🥑 提供一个信号处理函数,要求内核在处理该信号时切换到用户态执行这个处理函数,这种方式称为捕捉 (Catch)一个信号

Core Dump

 通过终端按键可以产生信号:

SIGINT的默认处理动作是终止进程

SIGQUIT的默认处理动作是终止进程并且Core Dump

🥑

Core Dump:当一个进程要异常终止时,可以选择把进程的用户空间内存数据全部保存到磁盘上,文件名通常是core,这叫做Core Dump

概念性问题,还是要记住一些专业术语,荷叶饭好好看!

进程异常终止通常是因为有Bug,比如非法内存访问导致段错误, 事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)

一个进程允许产生多大的core文件取决于进程的Resource Limit(这个信息保存在PCB中)

默认是不允许产生core文件的, 因为core文件中可能包含用户密码等敏感信息,不安全

在开发调试阶段可以用ulimit命令改变这个限制,允许产生core文件

用ulimit命令改变Shell进程的Resource Limit,允许core文件最大为1024K:

ulimit -c 1024

ulimit命令改变了Shell进程的Resource Limit,test进程的PCB由Shell进程复制而来,所以也具有和Shell进程相 同的Resource Limit值,这样就可以产生Core Dump了捏

ca0e5c59835f4ea79cf65956c752affa.jpeg

aeded379df64403588e93feb9640ec3e.png

嘴脸:

bd1a2862a37f446b974f73c6e76b0a60.png

d0e6af4b39084fa6ae835f26c3fe2b23.png

信号处理

我们可以先自己试着搓一个位图,位图的原理大概是这样:

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

struct bits
{
    uint32_t bits[400];     //400*32毕竟是位图
};

//位图原理
//40    //我们假设得出的结果是40
//40/(sizeof(uint32_t)*8) = 1 -> bits[1]
//40%(sizeof(uint32_t)*8) = 8 -> bits[1]:8   //第八个比特位

int main()
{
    return 0;
}

11d35d4b5e1a4bbebacfa3cd5404f064.png

 4a4513764d0f4d389bb88180662f45e6.png

但是Linux提供了位图的接口:

#ifndef __sigset_t_defined
#define __sigset_t_defined 1

#include <bits/types/__sigset_t.h>

/* A set of signals to be blocked, unblocked, or waited for.  */
typedef __sigset_t sigset_t;

#endif
#ifndef ____sigset_t_defined
#define ____sigset_t_defined

#define _SIGSET_NWORDS (1024 / (8 * sizeof (unsigned long int)))
typedef struct
{
  unsigned long int __val[_SIGSET_NWORDS];
} __sigset_t;

#endif

本质上跟我们自己搓的也差不多 

信号集操作函数  

这是将位图清空的函数:

#include <signal.h>
int sigemptyset(sigset_t *set);

018d48de18054a45812fb8c8a2e29cc4.png

sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有 效信号

这是添加的函数:

int sigaddset(sigset_t *set, int signum);

 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号(包括系统支持的所有信号)

tips:在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态

初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号

 我们可以用sigset_t来获取函数sigprocmask

076b5871ceb5405faade45f288526e62.png

#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参数的可选值

bb0d6e42485d4bd0a961fe00c47c0df5.png

 oest是输出型参数,保存老的信号屏蔽字返回给用户

#include <signal.h>
int sigpending(sigset_t *set);

这个参数主要是获取当前进程的pending位图,是输出型参数

万事俱备,只差写代码,写一份炫酷的代码惊艳所有人

 我们假设先屏蔽2号信号:

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

int main()
{
    //屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,SIGINT);   //还没修改内核block表
    
    //设置进入进程的block中
    sigprocmask(SIG_BLOCK,&block_set,&old_set);     //修改当前内核的block表,对2号信号进行屏蔽

    return 0;
}

然后再进行相应的打印:

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "current process[" << getpid() << "]" << " pending: ";
    for(int signo = 31; signo >=1;signo--)
    {
        if(sigismember(&pending, signo))
        {
            //判定signo是否在集合里
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

int main()
{
    //屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,SIGINT);   //还没修改内核block表
    
    //设置进入进程的block中
    sigprocmask(SIG_BLOCK,&block_set,&old_set);     //修改当前内核的block表,对2号信号进行屏蔽

    while (true)
    {
        //获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);

        //打印pending信号类
        PrintPending(pending);
        sleep(1);
    }
    
    return 0;
}

信号被屏蔽,不会执行,但是会被我们打印出来:

 4a11b06f45f646d9a9f77005b9fdcc8b.png

 那我们怎么解除对2号信号的屏蔽呢?

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "current process[" << getpid() << "]" << " pending: ";
    for(int signo = 31; signo >=1;signo--)
    {
        if(sigismember(&pending, signo))
        {
            //判定signo是否在集合里
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

void handler(int signo)
{
    std::cout << signo << "号信号被递达" << std::endl;
}

int main()
{
    //捕捉2号信号
    signal(2,handler);
    //屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,SIGINT);   //还没修改内核block表
    
    //设置进入进程的block中
    sigprocmask(SIG_BLOCK,&block_set,&old_set);     //修改当前内核的block表,对2号信号进行屏蔽

    int cnt = 10;
    while (true)
    {
        //获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);

        //打印pending信号类
        PrintPending(pending);
        sleep(1);

        //解除对2号信号的屏蔽
        cnt--;
        if(cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
    }
    
    return 0;
}

1525172795da49daa85bfeabc76e9b17.png

 解除屏蔽,一般会立即处理当前被解除的信号(如果被pending)

pending位图对应的信号也要被清0(是递达之前还是递达之后?怎么验证这个)

很简单,获取一下就好了

#include<iostream>
#include<unistd.h>
#include<cstdio>
#include<sys/types.h>
#include<sys/wait.h>

void PrintPending(sigset_t &pending)
{
    std::cout << "current process[" << getpid() << "]" << " pending: ";
    for(int signo = 31; signo >=1;signo--)
    {
        if(sigismember(&pending, signo))
        {
            //判定signo是否在集合里
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << "\n";
}

void handler(int signo)
{
    std::cout << signo << "号信号被递达" << std::endl;
    std::cout << "--------------------------------" << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "--------------------------------" << std::endl;
}

int main()
{
    //捕捉2号信号
    signal(2,handler);
    //屏蔽2号信号
    sigset_t block_set,old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);
    sigaddset(&block_set,SIGINT);   //还没修改内核block表
    
    //设置进入进程的block中
    sigprocmask(SIG_BLOCK,&block_set,&old_set);     //修改当前内核的block表,对2号信号进行屏蔽

    int cnt = 10;
    while (true)
    {
        //获取当前进程的pending信号集
        sigset_t pending;
        sigpending(&pending);

        //打印pending信号类
        PrintPending(pending);
        sleep(1);

        //解除对2号信号的屏蔽
        cnt--;
        if(cnt == 0)
        {
            std::cout << "解除对2号信号的屏蔽" << std::endl;
            sigprocmask(SIG_SETMASK, &old_set, &block_set);
        }
    }
    
    return 0;
}

 你看,事实上

bcf0702a50894466badb8b1be63b062e.png

它早就已经被清空了 

在信号递达之前就已经被清空了

c9f3b8c7b6634c37ac59536d867f17d3.png

信号捕捉

针对信号捕捉,我们可以这么干:

signal(2,handler);      //自定义捕捉
signal(2,SIG_IGN);      //忽略的一个信号
signal(2,SIG_DFL);      //信号的默认处理动作

 信号可能不会被立即处理,而是在合适的时候处理

合适是指从内核态返回到用户态的时候再进行处理

那么什么叫做内核态呢?

772233145d5b45f2b029c2c7f38b68d8.png

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂

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

执行自己的代码的时候叫用户态

操作系统可否直接转过去执行用户提供的handler方法呢?

从技术角度是可以的,但是我们不能让操作系统执行(不可以利用操作系统去满足自己的一己私欲!操作系统不信任人!)

所以在执行用户提供的handler方法的时候是用户态

信号捕捉的流程可以理解成:

dd6c8adba867472ba3dbb00771387e3c.jpeg

这是我特意从网上找到的

一个炫酷的,躺着的8

 信号捕捉的过程要经历四次状态的切换,看那四个交点捏:

0f0b590550534c36931ba4f671f79733.png

我们在内核态切换回用户态的时候,进行信号的检测和处理

地址空间

b52bacfe7ab8424db40786147bd95473.png

也是老图常谈了

 在开机时,OS是第一个被加载的软件,也是在内存里的,在系统中存在用户级页表(3G)和内核级页表,而OS本身就在进程的地址空间中

假设有十个进程,用户级页表有很多,但是内核级页表只有一份

无论进程如何切换,我们总能找到OS

我们访问OS,是在我们的地址空间中进行的,和访问库函数没区别

而OS不相信任何用户

用户在访问[3,4]地址空间的时候要收到一定的约束

只能通过系统调用来访问数据

键盘输入数据过程

OS是如何得知键盘上的按键被按下了呢?

键盘有按下的时候,是会向CPU发送硬件中断的信号的,不同外设有他们自己的中断号

在内存中要初始化一个函数指针数组,很多操作系统的方法的预设都在里面

而中断号就是数组的下标

这就是信号捏!

我们学习的信号是模拟中断实现的

信号是纯软件,中断是软件+硬件

OS执行

操作系统的本质就是一个死循环(开机就在跑)+ 时钟中断,不断调度系统的任务

系统调用是函数指针表,用于系统调用处理程序作为跳转表

sys_call

我们只需要找到特定数组下标的方法,就能执行系统调用了

执行任务需要有系统调用号

进行外部中断的目的是让CPU内部寄存器形成一个中断号的数字

 

在CPU内部可不可以直接形成数字捏?

可以捏

这样就是不执行用户的,直接去执行OS的系统调用

但是用户无法直接跳转到[3,4]范围(OS不信任用户),在特定的条件下才能跳转过去(CPU配合)

内核态和用户态

寄存器CS里面有的比特位代表内核态(0)和用户态(3)

用户态和内核态CPU要进行识别

23f70ddaddf1477e9a99f8aa44ae4558.png

这是sigaction:

66090c0b6c55475c8dae1f91c37ea296.png

#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact); 

sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。signo是指定信号的编号。

若act指针非空,则根据act修改该信号的处理动作。若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体

 这是那个炫酷结构体:

07cc12e90edb487ca6183955bfc7461b.png

将sa_handler赋值为常数SIG_IGN传给sigaction表示忽略信号,赋值为常数SIG_DFL表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为void,可以带一个int参数,通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。这是一个回调函数,不是被main函数调用,而是被系统所调用。 

🍍当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags字段包含一些选项,sa_sigaction是实时信号的处理函数

 sigaction是对特定信号实现自定义捕捉

#include<iostream>
#include<signal.h>

void handler(int signum)
{
    std::cout << "get a sig" << signum << std::endl;
    exit(1);
}


int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    
    //进行自定义捕捉
    sigaction(2,&act,&oact);

    while (true)
    {
        std::cout << "I am a process,pid: " << getpid() << std::endl;
        sleep(1);
    }
    
    return 0;
}

 e3ba3decf6ff4056b7be75626ecbb2f5.png

可以进行信号的捕捉

 7f13c6c9e0ae42ae91eb4a7d06c1b0bb.png

 给大家看一下,在我们炉石传说玩家里面

分低就是低人一等:

a137288deadf4953af43fbc51c5141dd.png

有人就是有神经病,不信大家看:

f9b4f86e229e4647b260ac80793e9be9.png

d55c6dff7d4544c7bd2701ef49469a26.png

 

当前如果正在对2号信号进程处理,默认2号信号会被自动屏蔽,对2号信号处理完成的时候,会自动解除对2号信号的屏蔽:

#include<iostream>
#include<signal.h>

//当前如果正在对2号信号进程处理,默认2号信号会被自动屏蔽
//对2号信号处理完成的时候,会自动解除对2号信号的屏蔽

void handler(int signum)
{
    std::cout << "get a sig" << signum << std::endl;
    sleep(100);
    exit(1);
}


int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    
    //进行自定义捕捉
    sigaction(2,&act,&oact);

    while (true)
    {
        std::cout << "I am a process,pid: " << getpid() << std::endl;
        sleep(1);
    }
    
    return 0;
}

这个时候Ctrl+C是获取到信号,但是还是没有进行处理,所以会一直sleep在那里

那么如何显式的看到信号被屏蔽呢?

#include<iostream>
#include<signal.h>

void Print(sigset_t &pending)
{
    for(int sig = 31;sig > 0; sig--)
    {
        if(sigismember(&pending, sig))
        {
            std::cout << 1;
        }
        else
        {
            std::cout << 0;
        }
    }
    std::cout << std::endl;
}

//当前如果正在对2号信号进程处理,默认2号信号会被自动屏蔽
//对2号信号处理完成的时候,会自动解除对2号信号的屏蔽

void handler(int signum)
{
    std::cout << "get a sig" << signum << std::endl;
    while (true)
    {
        sigset_t pending;
        sigpending(&pending);

        Print(pending);
        sleep(1);
    }
    
    exit(1);
}


int main()
{
    struct sigaction act,oact;
    act.sa_handler = handler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    
    //进行自定义捕捉
    sigaction(2,&act,&oact);

    while (true)
    {
        std::cout << "I am a process,pid: " << getpid() << std::endl;
        sleep(1);
    }
    
    return 0;
}

没错,就是这样,你现在全场领先!

 5f8bd57b8d2842e59ede2c05fc1adee8.png

就是这种无力感!

 主要是sigaction里面有字段sa_mask

如果还想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽

之前说过9号信号不能被捕捉,但是它可不可以被屏蔽呢?

理想很丰满,现实很骨感,要是能让它屏蔽,岂不是把所有进程的信号都屏蔽,它就成金刚不坏之身了?

所以肯定是不可以的

信号的三个阶段:产生和发送、保存、处理

可重入函数

dd561bddfa8049e582a649c4d645ad04.png

进程在时间片到了的时候也是要被剥离下来的 

 insert函数被重新进入,简称是被重入了,如果一个函数被main函数和信号捕捉函数同时进入,那么这种情况就是被重入了

如果问题是因为被重入产生的,那么这个函数就是不可重入函数

大部分的函数都是不可重入函数

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

848da80ed3cc49d7840ae401e9f86f2e.png

结果:main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中 ,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数

c6895396e4d84e28909f26d4f16f942a.png

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

调用了malloc或free,因为malloc也是用全局链表来管理堆的

调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构

volatile

这是C阶段的一个关键字(真的假的?)

我是真的没有印象啊

没事,我们先暂且写一段代码

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

int gflag = 0;

void changedata(int signo)
{
    std::cout << "get a signo:" << signo << ",change gflag 0 -> 1" << std::endl;
    gflag = 1;
}

int main()
{
    signal(2,changedata);
    while (!gflag);
    std::cout << "process quit normal" << std::endl;
}

 984235c2f7154095b4a7cbeef0b67bd0.png

 编译器是会对我们的代码进行一定程度的优化的

而我们的代码,在main函数内部,没有对gflag进行修改

而这些运算最终是要让CPU去执行的,CPU会进行算术运算和逻辑运算

是CPU对gflag做不断检测,但是gflag在内存中啊,CPU怎样对它做不断检测呢?

首先是物理内存中的gflag加载到CPU中

然后CPU对其进行逻辑运算并且判断,然后可以去执行其他代码

但是编译器会对其进行优化11354d9cb36c4426b1bd0346fd4300ca.png

比如这是g++编译器对代码优化的不同级别:

1bf832819f3149dea4701b2c47a07c0c.png

默认是O0,是没有优化

 我们如果带上O1的选项,刚刚那段代码还是会继续执行,但是不会直接结束了

为什么会出现这样的情况呢?

75bfbd2f424248b79b4d355a511aec42.png

因为我们的代码里并没有对gflag进行修改,所以编译器开始发力

认为我们没必要每一次判断都从内存中拿数据,把数据直接放到寄存器里,这就会可能导致,对内存做的修改CPU看不到了(寄存器隐藏了内存中的真实值,编译器过度优化导致的问题)

我们需要让编译器保持内存的可见性,为了使编译器做到这一点,C语言就给我们提供了一个关键字:volatile

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

volatile int gflag = 0;

void changedata(int signo)
{
    std::cout << "get a signo:" << signo << ",change gflag 0 -> 1" << std::endl;
    gflag = 1;
}

int main()
{
    signal(2,changedata);
    while (!gflag);
    std::cout << "process quit normal" << std::endl;

}

这样修改后不论编译器怎样优化,都能看到我们修改的值了 (保持内存可见性)

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

SIGCHLD信号

紫禁城退出时是静悄悄退出么?(你爱的静悄悄)

并不是。会给父进程发送信号--SIGCHLD

当我们用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(轮询)

如果采用第一种方式,父进程阻塞了就不能处理自己的工作了

如果采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。

什么?我打灿灿?

274adf33683f4a6f86479c8391d41f62.png

其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。

但是这个信号默认是被Ign的(忽视)

嘻嘻

eb6c7313db1e49a19d1387b6b11b0a70.png

我们要证明一下这个东西

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

void notice(int signo)
{
    std::cout << "get a signal: " << signo << "pid:" << getpid() << std::endl;
}

int main()
{
    signal(SIGCHLD,notice);
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "I am child process" << std::endl;
        sleep(3);
        exit(1);
    }

    //father
    sleep(100);
    return 0;
}

让父进程捕捉一下这个信号:

 b5edc960feb14cdc9b617e74ff2f1901.png

这样就可以实现父进程fork出子进程,子进程调用exit(1)终止,父进程自定义SIGCHLD信号的处理函数, 在其中调用wait获得子进程的退出状态并打印了

#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>
#include<sys/types.h>

void notice(int signo)
{
    std::cout << "get a signal: " << signo << "pid:" << getpid() << std::endl;
    pid_t rid = waitpid(-1,nullptr,0);
    if(rid > 0)
    {
        std::cout << "wait child success ,rid: " << rid << std::endl;
    }
}

int main()
{
    signal(SIGCHLD,notice);
    pid_t id = fork();
    if(id == 0)
    {
        std::cout << "I am child process" << std::endl;
        sleep(3);
        exit(1);
    }

    //father
    sleep(100);
    return 0;
}

上面的代码还是太保守了,存在很多问题:

如果一共十个紫禁城,他们要同时退出怎么办?

 改一下自定义的函数就好了:

void notice(int signo)
{
    std::cout << "get a signal: " << signo << "pid:" << getpid() << std::endl;
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, 0);
        if (rid > 0)
        {
            std::cout << "wait child success ,rid: " << rid << std::endl;
        }
        else if(rid < 0)
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
    }
}

那如果一共有十个紫禁城,五个退出,五个永远不退呢?

 让我们做一个炫酷的假设

假设励志轩是兽医专业的一名大学生,荷叶饭也是柚专的一名大学生,她们两个是好朋友,有一天,荷叶饭开始向励志轩借钱,说自己没饭吃了,励志轩欣然应下,荷叶饭每个月都会向励志轩借钱,但是没还过,淑华说的好,有再一再二没有再三再四,但是荷叶饭第三四个月依然在借,而进程也是一样,我们是站在上帝视角知道还有几个进程不退,但就跟借钱一样,励志轩又怎么会知道还有几个月荷叶饭会向他借钱呢?那程序又怎么知道还有几个紫禁城不退呢?所以还需要进行检测,还有紫禁城没退出就阻塞了(没办法干其他事),所以我们需要进行非阻塞等待:

void notice(int signo)
{
    std::cout << "get a signal: " << signo << "pid:" << getpid() << std::endl;
    while (true)
    {
        pid_t rid = waitpid(-1, nullptr, WNOHANG);      //非阻塞方式
        if (rid > 0)
        {
            std::cout << "wait child success ,rid: " << rid << std::endl;
        }
        else if(rid < 0)
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
        else
        {
            std::cout << "wait child success done " << std::endl;
            break;
        }
    }
}

由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>

int main()
{
    signal(SIGCHLD, SIG_IGN);       //收到设置对SIGCHLD进行忽略
    pid_t id = fork();
    if(id == 0)
    {
        int cnt = 5;
        while (cnt)
        {
            std::cout << "chlid running" << std::endl;
            cnt--;
            sleep(1);
        }
        exit(1);
    }
    //father
    while (true)
    {
        std::cout << "chlid running" << std::endl;
        sleep(1);
    }
    exit(1);
    return 0;
}

系统默认的忽略动作和用户用sigaction函数自定义的忽略通常是没有区别的,但这是一个特例

两个忽略的含义不一样

此方法对于Linux可用,但不保证在其它UNIX系统上都可用

这就是信号阶段的所有内容啦

我要玩多线程

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值