上篇太颠了
我们重说
懒得喷。
我们先来看看昨天的问题,我问了下chat:
我安装了:
哎哟我说咱要不不学这个吧
知道得了
我不想改bug了
我们开启下一个话题吧:
写一份代码:
#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;
}
core dump是0,是因为我们没有打开,这样打开:
ulimit -c 10240
现在这样就是咯,这辈子有了:
信号保存
我们把实际自行信号的处理动作称为信号递达
信号从产生到递答之间的状态,称为信号未决
进程可以选择阻塞 (Block )某个信号,阻塞一个信号,对应信号一旦产生,则永不递达,一直是未决,直到主动解除阻塞
那么一个信号如果阻塞和它有没有未决有关系吗?
肯定是无关耶
就和猎犬和switch没什么关系一样:
sigset_t
每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略
内核中的task_struct会维护一张判定表,维护的是判定位图
每个比特位的位置代表信号编号
比特位的内容代表信号是否收到
pending是未决信号集
handler表是一个函数指针数组,普通信号的编号就是数组的下标
一张位图和pending类型完全一样
比特位位置代表信号编号
比特位内容代表信号是否阻塞
三个表帮我们维护了两张位图+一个函数指针数组,让进程识别信号
这是示意图:
一个表阻塞一个表未决,还有一个函数指针表示处理动作
信号产生的时候,内核在task_struct中设置该信号的未决标志
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作
阻塞和忽略不同,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
如果将上述过程描述成生活中的场景就是(暂时以送外卖这个过程举例子):
每天就在偷偷查看实验室人进度:
偷感比较重是这样的
墨墨酱在网上买了很多件商品,在等待不同商品快递的到来
但即便快递没有到来,墨墨酱也知道快递来临时, 她该怎么处理快递
也就是她能“识别快递” 当快递员到了她家楼下,她也收到快递到来的通知,但是她正在打游戏(拜托我姐超厉害的好嘛)
需5min之后才能去取快递。那么在这5min之内,墨墨酱并没有下去取快递,但是墨墨酱是知道有快递到来了。也就是取快递的行为并不是一定要立即执行(在合适的时候去取)
在收到通知,再到墨墨酱拿到快递期间,是有一个时间窗口的,在这段时间,墨墨酱并没有拿到快递,但是她知道有一个快递已经来了。本质上是她“记住了有一个快递要去取”
当墨墨酱打完黑吗喽时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:
1. 执行默认动作(幸福的打开快递,使用商品)
2. 执行自定义动作(快递是零食,墨墨酱要分享给励志轩吃)
3. 忽略快递(快递拿上来之后,扔在床头,继续开一把游戏)
快递到来的整个过程,对墨墨酱来讲是异步的,因为她不能准确断定快递员什么时候给她打电话
一些小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了捏
嘴脸:
信号处理
我们可以先自己试着搓一个位图,位图的原理大概是这样:
#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;
}
但是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);
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
#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参数的可选值
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;
}
信号被屏蔽,不会执行,但是会被我们打印出来:
那我们怎么解除对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;
}
解除屏蔽,一般会立即处理当前被解除的信号(如果被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;
}
你看,事实上
它早就已经被清空了
在信号递达之前就已经被清空了
信号捕捉
针对信号捕捉,我们可以这么干:
signal(2,handler); //自定义捕捉
signal(2,SIG_IGN); //忽略的一个信号
signal(2,SIG_DFL); //信号的默认处理动作
信号可能不会被立即处理,而是在合适的时候处理
合适是指从内核态返回到用户态的时候再进行处理
那么什么叫做内核态呢?
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。
由于信号处理函数的代码是在用户空间的,处理过程比较复杂
🍍 : 用户程序注册了SIGQUIT信号的处理函数sighandler,当前正在执行 main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号 SIGQUIT递达。内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler 和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复 main函数的上下文继续执行了
执行自己的代码的时候叫用户态
操作系统可否直接转过去执行用户提供的handler方法呢?
从技术角度是可以的,但是我们不能让操作系统执行(不可以利用操作系统去满足自己的一己私欲!操作系统不信任人!)
所以在执行用户提供的handler方法的时候是用户态
信号捕捉的流程可以理解成:
这是我特意从网上找到的
一个炫酷的,躺着的8
信号捕捉的过程要经历四次状态的切换,看那四个交点捏:
我们在内核态切换回用户态的时候,进行信号的检测和处理
地址空间
也是老图常谈了
在开机时,OS是第一个被加载的软件,也是在内存里的,在系统中存在用户级页表(3G)和内核级页表,而OS本身就在进程的地址空间中
假设有十个进程,用户级页表有很多,但是内核级页表只有一份
无论进程如何切换,我们总能找到OS
我们访问OS,是在我们的地址空间中进行的,和访问库函数没区别
而OS不相信任何用户
用户在访问[3,4]地址空间的时候要收到一定的约束
只能通过系统调用来访问数据
键盘输入数据过程
OS是如何得知键盘上的按键被按下了呢?
键盘有按下的时候,是会向CPU发送硬件中断的信号的,不同外设有他们自己的中断号
在内存中要初始化一个函数指针数组,很多操作系统的方法的预设都在里面
而中断号就是数组的下标
这就是信号捏!
我们学习的信号是模拟中断实现的
信号是纯软件,中断是软件+硬件
OS执行
操作系统的本质就是一个死循环(开机就在跑)+ 时钟中断,不断调度系统的任务
系统调用是函数指针表,用于系统调用处理程序作为跳转表
sys_call
我们只需要找到特定数组下标的方法,就能执行系统调用了
执行任务需要有系统调用号
进行外部中断的目的是让CPU内部寄存器形成一个中断号的数字
在CPU内部可不可以直接形成数字捏?
可以捏
这样就是不执行用户的,直接去执行OS的系统调用
但是用户无法直接跳转到[3,4]范围(OS不信任用户),在特定的条件下才能跳转过去(CPU配合)
内核态和用户态
寄存器CS里面有的比特位代表内核态(0)和用户态(3)
用户态和内核态CPU要进行识别
这是sigaction:
#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结构体
这是那个炫酷结构体:
将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;
}
可以进行信号的捕捉
给大家看一下,在我们炉石传说玩家里面
分低就是低人一等:
有人就是有神经病,不信大家看:
当前如果正在对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;
}
没错,就是这样,你现在全场领先!
就是这种无力感!
主要是sigaction里面有字段sa_mask
如果还想处理2号(OS对2号自动屏蔽),同时对其他信号也进行屏蔽
之前说过9号信号不能被捕捉,但是它可不可以被屏蔽呢?
理想很丰满,现实很骨感,要是能让它屏蔽,岂不是把所有进程的信号都屏蔽,它就成金刚不坏之身了?
所以肯定是不可以的
信号的三个阶段:产生和发送、保存、处理
可重入函数
进程在时间片到了的时候也是要被剥离下来的
insert函数被重新进入,简称是被重入了,如果一个函数被main函数和信号捕捉函数同时进入,那么这种情况就是被重入了
如果问题是因为被重入产生的,那么这个函数就是不可重入函数
大部分的函数都是不可重入函数
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续往下执行,先前做第一步之后被打断,现在继续做完第二步
结果:main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中 ,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数
如果一个函数符合以下条件之一则是不可重入的:
调用了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;
}
编译器是会对我们的代码进行一定程度的优化的
而我们的代码,在main函数内部,没有对gflag进行修改
而这些运算最终是要让CPU去执行的,CPU会进行算术运算和逻辑运算
是CPU对gflag做不断检测,但是gflag在内存中啊,CPU怎样对它做不断检测呢?
首先是物理内存中的gflag加载到CPU中
然后CPU对其进行逻辑运算并且判断,然后可以去执行其他代码
但是编译器会对其进行优化
比如这是g++编译器对代码优化的不同级别:
默认是O0,是没有优化
我们如果带上O1的选项,刚刚那段代码还是会继续执行,但是不会直接结束了
为什么会出现这样的情况呢?
因为我们的代码里并没有对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函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻塞地查询是否有子进程结束等待清理(轮询)
如果采用第一种方式,父进程阻塞了就不能处理自己的工作了
如果采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
什么?我打灿灿?
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略,父进程可以自定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
但是这个信号默认是被Ign的(忽视)
嘻嘻
我们要证明一下这个东西
#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;
}
让父进程捕捉一下这个信号:
这样就可以实现父进程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系统上都可用
这就是信号阶段的所有内容啦
我要玩多线程