文章目录
一.引例:
以生活角度为例:红绿灯,绿灯亮了过马路;早上的闹钟响了,就得起床了。
结论:1.信号在生活当中,随时可以产生。
2.认识这个信号。(认识红绿灯)
3.知道信号产生后,如何处理信号。(知道红灯停绿灯行)
4.信号产生后,可能在做别的更重要的事情,把到来的信号暂不处理,在合适的时候处理。(点外卖后,打王者推高地了,外卖小哥给你打电话让拿外卖)
综上所述:信号的产生和我(进程)是异步的。
二.进程信号
信号:Linux系统提供的一种向指定进程发送特定事件的方式。
查看所有的信号:
kill -l
把1号到31号信号叫做普通信号;34到64号信号叫做实时信号。
所有的普通信号都是大写的,就是一个宏,前面的数字就是对应的宏值。
信号产生是异步的:信号的产生和进程的运行是互不干扰的(进程在运行的时候,可以直接用9号信号终止进程)
信号处理的常见方式
收到一个信号,处理信号的方式一共有三种:
1.默认动作 --》终止自己,暂停,忽略…
2.忽略动作 --》 信号收到,但不处理
3.自定义处理(信号捕捉)
默认动作
查看31个信号的默认动作:
man 7 signal
Action就是是默认的处理动作:Core和Term就是直接终止进程
自定义处理
如果不想执行上述的默认动作,让进程执行我们自己设计的信号处理方式,就是自定义处理。
用到函数调用:signal
对信号的自定义捕捉,我们只要捕捉一次,后续一直有效
先看现象:对2号信号进行捕捉
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
//对信号的自定义捕捉,我们只要捕捉一次,后续一直有效
signal(2,handler);
while(true)
{
std::cout << "hello Linux,pid: " << getpid() <<std::endl;
sleep(1);
}
return 0;
}
发现发送二号信号,会被捕捉。发送三号信号,会终止进程。
如果一直不产生2号信号,handler方法一直不会被调用。
也可以对其它更多的信号进程捕捉:
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(2,handler);
signal(3,handler);
signal(4,handler);
while(true)
{
std::cout << "hello Linux,pid: " << getpid() <<std::endl;
sleep(1);
}
return 0;
}
最后kill -9终止进程
2号信号默认做什么动作?
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
//signal(2,handler);
while(true)
{
std::cout << "hello Linux,pid: " << getpid() <<std::endl;
sleep(1);
}
return 0;
}
2号终止了进程。就是ctrl+C
如何理解信号的发送与保存?
进程是struct结构体,信号是其中的成员变量,以位图的形式来保存。
类似成员变量:unit32_t signals;
0000 0000 0000 0000 0000 0000 0000 0000 没有0号信号,有一位不用
发送信号:修改指定进程PCB中的信号指定位图,由0->1
且信号为内核数据结构对象,只有操作系统能够修改内核结构中的值。
三.信号产生
以三个阶段来讲述信号:
信号的产生:
1.通过kill命令,向指定的进程发送指定的信号。
2.键盘可以产生信号(ctrl+C)
ctrl+\是3号信号,也是可以终止进程的。
3.系统调用
kill
kill函数
返回值:成功返回0;失败返回-1
用一下kill
myprocess.cc
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(2,handler);
while(true)
{
std::cout << "hello Linux,pid: " << getpid() <<std::endl;
sleep(1);
}
return 0;
}
testsig.cc
#include <iostream>
#include <signal.h>
#include <sys/types.h>
#include <unistd.h>
// ./mykill 信号 pid
int main(int argc,char* argv[])
{
if(argc!=3)
{
std::cerr << "Usage: " << argv[0] << " signum pid" << std::endl;
return 1;
}
pid_t pid = std::stoi(argv[2]) ;
int signum = std::stoi(argv[1]);
kill(pid,signum);
return 0;
}
raise
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(2, handler);
while (true)
{
raise(2);
std::cout << "hello Linux,pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
每隔1秒调用一次2号信号
abort
本质是给指定进程发送6号信号SIGABRT
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
signal(6, handler);
while (true)
{
abort();
std::cout << "hello Linux,pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
会发现信号被捕捉后依旧会把进程终止。
问题:如果把所有的信号都捕捉,进程如何被终止?
操作系统想到了这个问题,所有有几个信号是不能被捕捉的。
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
}
int main()
{
for (int i = 1; i <= 31; i++)
signal(i, handler);
while (true)
{
std::cout << "hello Linux,pid: " << getpid() << std::endl;
sleep(1);
}
return 0;
}
9号信号不能被自定义捕捉,除此之外还有19号信号也不能被捕捉。
综上所述有3种信号产生的方式,但真正发送信号的只能是操作系统。因为只有OS能修改PCB中信号的位图。
4.软件条件
管道:读端关闭,写端一直写就会触发13号SIGPIPE信号。
alarm
本质就是发送14号SIGALRM信号。
int main()
{
int cnt = 0;
alarm(1); //设定1秒之后的闹钟 --》SIGALRM
while(true)
{
std::cout << "cnt: " << cnt++ << std::endl;
}
return 0;
}
自定义捕捉:
void handler(int sig)
{
std::cout << "get a sig: " << sig << std::endl;
exit(1); //防止信息刷新太快,直接退出
}
int main()
{
int cnt = 0;
signal(SIGALRM, handler);
alarm(1); //设定1秒之后的闹钟 --》SIGALRM
while(true)
{
std::cout << "cnt: " << cnt++ << std::endl;
}
return 0;
}
只最后打印cnt
int cnt = 0;
void handler(int sig)
{
std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
exit(1);
}
int main()
{
signal(SIGALRM, handler);
alarm(1); // 设定1秒之后的闹钟 --》SIGALRM
while (true)
{
// std::cout << "cnt: " << cnt++ << std::endl;
cnt++;
}
return 0;
}
会发现cnt的值比上一段的多很多,所以IO是一个很慢的过程。
台式机或笔记本中有一个纽扣电池,维持着时间。就算电脑关机了,时间也不会停止。
闹钟可以同时在系统中存在多个,操作系统就需要管理闹钟:先组织,再描述。
类似于:
struct alarm
{
time_t expired;//未来超时的时间
pid_t pid;
......
}
组织最常见的方法是最大堆、最小堆。超时了pop堆顶。
返回值
先定一个5秒的闹钟,睡眠2秒,后定一个0秒的闹钟
int main()
{
signal(SIGALRM, handler);
alarm(5);
sleep(2);
int n = alarm(0);
std::cout << "n: " << n << std::endl;
return 0;
}
发现返回值为3,alarm(0)是取消闹钟,返回值是上一个闹钟的剩余的时间。
设置一次闹钟,默认会触发一次。
int cnt = 0;
void handler(int sig)
{
std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
std::cout << "cnt: " << cnt++ << std::endl;
sleep(1);
}
return 0;
}
发现就捕捉了一次14号信号
如果想要闹钟一直触发:
int cnt = 0;
void handler(int sig)
{
alarm(1);
std::cout << "cnt: " << cnt << " get a sig: " << sig << std::endl;
}
int main()
{
signal(SIGALRM, handler);
alarm(1);
while (true)
{
std::cout << "cnt: " << cnt++ << std::endl;
sleep(1);
}
return 0;
}
如果系统允许设置多个闹钟,就需要有标识符。
5.异常
例子:除零
int main()
{
while (true)
{
std::cout << "hello bit,pid: " << getpid() << std::endl;
int a = 10;
a /= 0;
}
return 0;
}
野指针:
int main()
{
while (true)
{
std::cout << "hello bit,pid: " << getpid() << std::endl;
int *p = nullptr;
*p = 100;
}
return 0;
}
问题:程序为什么会崩溃?
答:程序非法访问,导致操作系统给进程发送信号。
Floating point exception就是收到了8号SIGFPE信号。
void handler(int sig)
{
std::cout << " get a sig: " << sig << std::endl;
}
int main()
{
signal(SIGFPE,handler);
while (true)
{
std::cout << "hello bit,pid: " << getpid() << std::endl;
int a = 10;
a /= 0;
}
return 0;
}
发现捕捉到了8号信号,且会不断的捕捉8号信号。
野指针(Segmentation fault)是11号SIGSEGV信号
问题:崩溃了为什么会退出?可以不退出吗?
答:崩溃了默认是终止进程;可以不退出(捕捉异常),推荐终止进程。
问题:为什么是操作系统给进程发信号?
答:CPU的计算是来源于用户的,有些运算是正确的、有些运算是错误的。CPU不仅要在软件上有健壮性、硬件上也要有健壮性。那么CPU如何得知运算是正常还是异常的?以除零为例,CPU中有一个状态寄存器eflag,eflag中有一个溢出标记位,大小为1个比特位,未溢出为0、溢出为1。10/0会变为加法运算,累加到一定程度会溢出,溢出就由0->1,CPU就会标记运算出错了。操作系统是软硬件资源的管理者!所以OS要随时处理这种硬件问题-》向目标进程发送信号。
问题:为什么上一段代码会不断捕捉8号信号?
答:寄存器只有一套,但寄存器里面的数据是属于每一个进程的
默认是终止进程,但自定义处理是捕捉并打印信号,进程要调度,调度就要切换轮询,就意味着要保存与恢复数据,eflag也会被保存,进程又不退,eflag恢复到CPU中的寄存器中,恢复了错误的数据,就一直在触发调度,就死循环了。
问题:为什么推荐终止进程?
答:终止进程就是释放进程的上下文数据,包括溢出标志位数据或者其它异常数据,相当于把异常删掉了。
野指针的原理也一样:
MMU是一个硬件,在CPU中。CR3寄存器保存的是页表的起始地址。MMU和CR3一起工作让虚拟地址转化为物理地址。
CR2是页故障线性地址寄存器:如果给予的虚拟地址是错误的,会放入CR2中,然后OS要发送11号SIGSEGV信号,终止进程,释放数据。
综上所述:所有信号的产生,都要经过OS之手。
Core和Term的区别:
区别:
Term:异常终止。
Core:异常终止,但是他会帮我们形成一个debug文件。
在云服务器上这个debug文件是默认是关闭的
查看对资源的限制
ulimit -a
默认是0,不允许在云服务器上生成文件
打开的指令
ulimit -c 10240
ubuntu20.04的版本还需要
sudo bash -c "echo core > /proc/sys/kernel/core_pattern"
为什么要有Core?
我们在发开的时候,程序容易挂掉。就得调试代码,发现哪里引起程序崩溃,比较慢。
以如下代码为例:除零错误
#include <iostream>
int Sum(int start, int end)
{
int sum = 0;
for (int i = start; i <= end; i++)
{
sum /= 0;
sum += i;
}
return sum;
}
int main()
{
int total = Sum(0, 100);
std::cout << "total: " << total << std::endl;
return 0;
}
以调试的方式打开,输入core-file core就能知道在哪一行报错
综上所述:core是协助我们进行debug的文件,方便事后调试
退出信号中有core dump文件是否形成的标志。
做一下验证:
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int Sum(int start, int end)
{
int sum = 0;
for (int i = start; i <= end; i++)
{
sum /= 0;
sum += i;
}
return sum;
}
int main()
{
pid_t id = fork();
if (id == 0)
{
// 子进程;
sleep(1);
Sum(0, 100);
exit(0);
}
// 父进程
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);
}
}
首先把core file size关闭
发现core dump标记位为0,且没有生成core文件
打开
发现core dump标记位为1,且生成了core文件
四.信号保存
实际执行信号的处理动作称为信号递达。
信号从产生到递达之间的状态称为信号未决。
进程可以选择阻塞某个信号:阻塞一个信号,对应的信号一旦产生,永不递达,一直未决,直到主动解除阻塞关系。
保存信号靠task_struct维护的三张表:block、pending、handler
内核会维护pending表,被称为:未决信号集
操作系统给每个task_struct都维护了一个handler表,内容是一个信号该如何处理。
block表是阻塞信号集(信号屏蔽字),也是一张位图
每一个进程都有这三张表,横着看:
1号信号没有被block,也没有递达。
2号信号被block,所以不能被递达。
4号信号没有block,递达了,所以handler
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
综上所述:在操作系统中维护的 两张位图+一张函数指针数组 == 让进程识别信号
位图的基本原理
struct bits
{
uint32_t bits[400]; //位图的类型一般是uint32_t
};
比如:要访问第40个比特位
40/(sizeof(uint32_t)*8) = 1 -> bit[1]
40%(sizeof(uint32_t)*8) = 8 -> bit[1]的第8个比特位就是第40个比特位
sigset_t
Linux给用户提供了一种用户级的数据类型:sigset_t
该类型是只在Linux下有效,C语言下没效果的一张位图。
每一个信号都有两个位图表,所以未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态:在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
sigset_t的定义:
因为是操作系统提供的,所以操作系统也提供了一些接口:
#include <signal.h>
int sigemptyset(sigset_t *set); //把位图全部清空
int sigfillset(sigset_t *set); //把整个位图全部制1
int sigaddset (sigset_t *set, int signo); //把signo设置进set中,例如把5的信号位制1
int sigdelset(sigset_t *set, int signo); //例5号信号是0不动,为1制0
int sigismember(const sigset_t *set, int signo); //判断signal是否在set中
sigprocmask
获取或设置block表的用函数调用sigprocmask
返回值:若成功为0,若出错为-1
若how为SIG_SETMASK,则set直接覆盖原来的block表。
若how为SIG_BLOCK,则添加阻塞的信号。
若how为SIG_UNBLOCK,则解除阻塞的信号:
sigpending
获取当前进程的pendind表。
返回值:0表示成功;-1表示失败
实验:若先把2号信号屏蔽了,后发送2号信号,2号信号不会被递达,获取2号信号被屏蔽的pending表,就可以肉眼看见2号信号被屏蔽的效果,然后把二号信号的屏蔽解除。
代码:
#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
void PrintPending(sigset_t &pending)
{
std::cout << "cur process[" << getpid() << "] pending: ";
for (int signal = 31; signal > 0; signal--)
{
if (sigismember(&pending, signal)) // 判断信号是否在pending表中
{
std::cout << 1;
}
else
{
std::cout << 0;
}
}
std::cout << "\n";
}
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
}
int main()
{
// 0.捕捉二号信号
signal(2,handler);
// 1.屏蔽二号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT);
// 修改当前内核的block表
sigprocmask(SIG_BLOCK, &block_set, &old_set);
int cnt = 15;
while (true)
{
// 2.获取当前的pending信号集
sigset_t pending;
sigpending(&pending);
// 3.打印pending信号集
PrintPending(pending);
cnt--;
// 4.解除对2号的屏蔽
if(cnt==0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK,&old_set,&block_set);
}
sleep(1);
}
return 0;
}
现象:
综上所述:解除屏蔽,一般会立即处理当前被解除的信号(被pending的信号)。
问题:pending位图对应的信号被清零,是在递达之前?还是递达之后?
在捕捉的时获得pending表就可以知道
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-----------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-----------------------" << std::endl;
}
发现在捕捉中就已经被清空了,所以在递达之前pending位图被清空
五.信号捕捉
signal(signal,handler); //自定义捕捉
signal(signal,SIG_IGN); //忽略一个信号
signal(signal,SIG_DFL); //信号的默认处理动作
源码就是强转,把数字强转成函数指针类型。因为handler表是函数指针数组
信号可能不会被理解处理,而是在合适的时候处理。其中合适的时候是指从内核态返回到用户态的时候处理
内核态和用户态
内核态:处于操作系统的状态,就叫做内核态。
用户态:执行自己的代码,访问自己的数据,就叫做用户态。
进入内核态最明显的是使用系统调用
问题:在处理自定义信号捕捉时,操作系统能不能直接使用用户提供的handler方法呢?
答:从技术角度是可以。但不能用这个技术,handler方法是用户提供的,handler中写了什么代码操作系统是不知道的,万一写了rm *等命令,利用了操作系统的高权限,绕过了权限的制度。所以必须用用户的身份执行。
通常函数调用是以压栈的方式,A函数调用B函数,把B压入A上面。但handler函数和main函数没有直接调用的关系,所以不能直接跳转。
总结:信号的捕捉过程要经历4次状态的切换。在内核态切换会用户态的时候,进行信号的检测和处理:
再谈地址空间
第一次谈地址空间:进程的地址空间
第二次谈地址空间:动静态库的加载
再上两次的基础上进一步谈地址空间:
问题:有函数调用就有函数地址,那操作系统的函数地址在哪里?如何找到getpid()函数?
以32为的机器为例:
地址空间中[0,3]GB是给用户使用,[3,4]GB是给操作系统使用。
开机的时候,操作系统最先加载到内存中的软件。
内核级页表的工作是将内核地址空间与操作系统的内存空间进行映射,意味着操作系统本身就在进程的地址空间中。
若存在多个进程,都用一个内核级页表
综上:无论进程如何切换,我们总能找到操作系统。
我们访问操作系统,其实还是在访问我们的地址空间中进行,跟访问库函数没区别。
用户不能直接访问操作系统:[3,4]GB是操作系统规定的,那么CPU一定也知道,CPU不让跳转。
谈谈键盘输入数据的过程
操作系统怎么知道键盘按下了?
CPU有很多针脚,当键盘按下的时候,会向CPU发送硬件中断(给CPU的针脚发送特定的信号),每一个硬件都有自己的中断号。CPU发现针脚上有高电频,就会把对应发送高电频的中断号放到寄存器中,变为寄存器的数据,就有了从硬件转为软件的动作。
在电脑上,第一款软件是操作系统,操作系统在开机加载的时候会形成一张函数指针数组,其中会有很多方法被预先设置,比如读磁盘、网卡…其中每一种设备对应的方法都有自己的中断号,中断号就是对应的函数数组下标。CPU收到中断数据了,就把所有任务暂停下来,去操作系统的这张函数指针表索引,自动读取键盘的方法,把数据读到内存里。该函数指针数组叫做中断向量表。
综上:操作系统不关心键盘中有无数据,键盘有数据会通过中断给CPU,CPU在执行函数指针数组的方法读到内存中,操作系统就读到了数据。
操作系统是如何运行的
从开机之后,操作系统一直不关机,说明操作系统是一个死循环。
有一个名为时钟的硬件,可能会在CPU内部,它会不断的给CPU发送中断,CPU会把时钟的中断号记录下来,该中断号在中断表中是调度的方法,CPU也不一定要调度,要先检测当前进程的时间片:如果时间片没到,什么都不做;如果时间片到了,就执行切换。
看一个最初始的操作系统源码:
最开始的动作就是做初始化
一旦死循环,就暂停,等待收到信号
操作系统一开始就注册了一些系统中断的方法:以sched_init()为例
其中就有用汇编写的时钟中断程序的预备工作
综上所述:操作系统是靠死循环+时钟中断运行的。
动漫版的:时钟是如何让CPU运行的
如何理解操作系统调用
操作系统给我们提供了一张sys_call_table表(函数指针数组)。比如我们平时用到fork,在系统中是sys_fork()
我们只要找到特定数组下标的方法,就能执行系统调用了。其中数组下标叫做系统调用号
以执行fork()为例:
在中断向量表中的0x80存的是系统调用的方法,调用fork()时先把2放入eax寄存器中,CPU自动形成一个数字0x80写入中断寄存器,CPU拿着eax中的2去操作系统内部找系统调用表中找到fork()。
由外部形成的中断叫做外部中断,内部形成的中断叫做陷阱或缺陷。
源码:
通过上述内容,讲解以下两个问题:
1.OS不信任任何用户,如何做到让用户无法跳转到[3,4]GB的地址空间范围?
2.如何在特定的条件下,跳转到[3,4]GB的地址空间范围?
跳转需要一个硬件cs的配合,在CPU中,全称为code semgment,表示的是代码范围,要想执行内核的代码就指向内核代码的地址,cs中有两个比特位:若为0,则为可以执行操作系统的代码;若为3,只能执行用户级的代码。再所谓的用户态还是内核态,就是这个比特位是0还是3。由内核态转换为用户态就是由0->3。
sigaction
原理是更改handler表
先用:
#include <iostream>
#include <unistd.h>
#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;
}
现象:
两个特点:1.如果当前正在对n号信号进行处理,默认n号信号会被自动屏蔽。
2.对n号信号处理完成的时候,会自动解除对n号信号的屏蔽。
由两个特点可以得出:同一个信号不能被连续处理。
验证:
#include <iostream>
#include <unistd.h>
#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;
}
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(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没有被捕捉
验证信号处理结束后会自动解除屏蔽
void handler(int signum)
{
std::cout << "get a sig: " << signum << std::endl;
while(true)
{
sigset_t pending;
sigpending(&pending);
Print(pending);
sleep(3);
break;
}
}
六.可重入函数
链表在插入时,捕捉到了自定义处理的信号,调用handler函数其中也有插入函数,再次调用函数,head指向node2,但最后回到第一个插入函数的时候head指向node1,node2的数据就丢失了,造成内存泄漏。其中第二次调用insert函数的动作叫做insert函数被重入。因为出了问题,就叫做不可重入函数。
我们学到的大部分函数都是不可重入的。
综上:函数可重入和不可重入描述的是函数的特点。
七.volatile
先看现象:
#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;
return 0;
}
现象:收到2号信号,gflag==1就退出
其中的过程:CPU会负责算数运算 + 逻辑运算
我们的编译器会有各种优化:
man c++
没有优化:
gcc main.cc -O0
没有任何变化
优化等级为1:
g++ main.cc -O1
会发现终止不了进程,只管捕捉
先把gflag的值直接优化到寄存器里,以后不去内存里拿值。在内存中对信号做修改,跟寄存器没有关系,就一直循环捕捉2号信号,进程不退出。
给全局变量加个volatile就可以解决
volatile int gflag = 0;
在信号的场景里可能会出现以上问题。
八.SIGCHLD信号
子进程退出时,会给信号发送17号SIGCHLD信号,且该信号的动作时忽略,所以我们平常看不到
证明:
#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)
{
//child
std::cout << "I am child process,pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
// father
sleep(100);
return 0;
}
确实发送了17号信号
这样以后就不用强行等待子进程了,什么时候发送信号什么时候说明退出:
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.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;
}
}
void DoOtherThing()
{
std::cout << "DoOtherThing~" << std::endl;
}
int main()
{
signal(SIGCHLD,notice);
pid_t id = fork();
if(id == 0)
{
//child
std::cout << "I am child process,pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
// father
while(true)
{
DoOtherThing();
sleep(1);
}
return 0;
}
问题就来了:如果一共有10个子进程退出,同时退出,上述的代码因为pending位图的原因,不能记录多次信号的产生,所以只能等待一个子进程。
改进:
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 DoOtherThing()
{
std::cout << "DoOtherThing~" << std::endl;
}
int main()
{
signal(SIGCHLD, notice);
for (int i = 0; i < 10; i++)
{
pid_t id = fork();
if (id == 0)
{
// child
std::cout << "I am child process,pid: " << getpid() << std::endl;
sleep(3);
exit(1);
}
}
// father
while (true)
{
DoOtherThing();
sleep(1);
}
return 0;
}