什么是信号
比如红绿灯、闹钟、倒计时、鸡叫等。
-
红绿灯:红灯亮的时候,会有匹配的动作。你为什么会有这个动作?因为曾有人有事“培养”过你。所以信号没有产生,我们也知道如何处理它。
-
我们能够认识并处理一个信号 —— 我们是能够识别这个信号的
进程就是我,信号就是一个数字,进程在没有收到信号的时候,其实它早就已经能够知道一个信号该怎么被处理了。也就是能够认识并处理一个信号。程序员在设计程序的时候,早就已经设计了对信号的识别能力。
-
因为信号可能随时产生,所以在信号产生前,我正在做优先级更高的事情,我们可能不能立马处理这个信号!要在后续合适的时候进行处理。
信号产生<———时间窗口————>信号处理
在这个过程中信号需要被保存起来。
同理:进程收到信号的时候,如果没有立马处理这个信号,需要进程有记录信号的能力!—— 信号保存
-
信号的产生对于进程来说是异步的。
kill -l
#查看linux中所有的信号
1-31普通信号,34-64实时信号
普通信号只保存有无产生
-
进程如何记录对应的信号?记录在哪里?
先描述,再组织。
使用位图结构管理信号。
task_struct 内部必定要存在一个位图结构,用int表示
-
所谓的发送信号,本质其实是写入信号,直接修改特定进程的信号位图中的特定的比特位,0→1。
比特位的位置——信号的编号
比特位的内容——是否收到该信号
-
task_struct 数据内核结构,只能由OS进行修改 —— 无论画面有多少种信号产生的方式,最终都必须让OS来完成最后的发送过程。
-
信号产生之后,不是立即处理的,是在合适的时候。
处理信号的方式:
- 默认动作
- 用户自定义捕捉动作
- 忽略信号
大部分进程的信号的默认动作都是中止进程,那么为什么要分那么多种类呢?
为了进一步确认进程是因为什么原因退出的,信号这么多类也可以表征进程异常时是因为什么原因异常的。
信号处理,可以不是立即处理的,而是”合适“的时候。
信号可以被立即处理吗?
如果一个信号之前被block了,当他被解除block的时候,对应的信号会被立即递达!
为什么大部分情况下信号不是立即处理的呢?
信号的产生是异步的,当前进程可能正在做更重要的事情!
什么时候是合适的时候呢?
当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的加测与处理!
信号的产生
1. 用户输入命令,在Shell下启动一个前台进程。
用户按ctrl+c,这个键盘输入产生一个键盘中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出。
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while(true)
{
cout << "我是一个进程,PID:" << getpid() << endl;
sleep(1);
}
return 0;
}
前台进程,无法运行其他程序,可使用ctrl+c中断。
后台进程,不影响其他程序的运行,无法使用ctrl+c中断,必须使用信号中断。
捕捉信号
signal()
#include <signal.h>
typedef void (*sighandler_t)(int); //函数指针
sighandler_t signal(int signum, sighandler_t handler);
//int signum 信号编号
#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;
//自定义方法
//signo: 特定信号被发送给当前进程的时候,执行handler方法的时候,要自动填充对应的信号给handler方法
//甚至可以给所有信号设置同一个处理函数
void handler(int signo)
{
cout << " signal num:" << signo<< endl;
}
int main()
{
//1. 2号信号,继承的默认处理动作是终止进程
//2. signal 可以进行对指定的信号设定自定义处理动作
//3. signal(2,handler);调用完这个函数的时候,handler方法被调用了吗
//没有,只是更改了2号信号的处理动作,并没有调用handler函数
//4. 那么handler方法何时被调用?2号信号产生的时候!
//5. 默认我们对2号信号的处理动作:终止进程,我们用signal(2,handler), 我们在执行用户动作的自定义捕捉
signal(2,handler);
while(true)
{
cout << "我是一个进程,PID:" << getpid() << endl;
sleep(1);
}
return 0;
}
不再中断,运行自己定义的函数
man 7 signal
9号信号,管理员信号,不可被自定义
平时在输入的时候,计算机怎么知道我们从键盘输入了数据呢?
键盘是通过硬件中断的方式通知系统 键盘已经被按下了。
2.通过系统调用向进程发信号
(1)kill
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
//pid 向哪个进程发信号
//sig 信号编号
.PHONY:all
all:mykill loop
mykill:mykill.cpp
g++ -o $@ $^ -std=c++11
loop:loop.cpp
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f mykill loop
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void usage(char* proc)
{
std::cout << "\\n Usage:\\n\\t" << proc << " 进程编号 信号编号\\n" << std::endl;
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
exit(2);
}
int pid = std::stoi(argv[1]);
int signo = std::stoi(argv[2]);
int n = kill(pid, signo);
if(n != 0)
{
std::cerr<< errno << " : " << strerror(errno);
}
return 0;
}
//loop.cpp
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
while (true)
{
cout << "我是一个进程,我的pid是:" << getpid() << endl;
sleep(1);
}
}
(2)raise
#include <signal.h>
int raise(int sig);
//哪个进程调用就给谁发sig
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl;
}
int main(int argc, char *argv[])
{
signal(2, handler);
while (true)
{
raise(2);
sleep(1);
}
return 0;
}
(3)abort
#include <stdlib.h>
void abort(void);
//给调用abort的进程发送指定的信号6
即便被自定义捕捉了,也要退出进程
3. 由软件条件产生信号
13)SIGPIPE,管道将读端fd关闭,这时写端就没意义了,OS就会将进程直接终止。向管道写入必须满足写入条件,也就是不关闭读端fd,这就是软件条件中的一种。
14)SIGALRM
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//设定闹钟,告诉内核在seconds秒后为当前进程发送SIGALRM信号,该信号默认处理动作是终止该进程
//返回值为0或之前设定的闹钟时间余下的秒数
//如果seconds值设为0,则表示取消之前设定的闹钟,返回值依然是之前设定的闹钟时间还余下的秒数
//闹钟是一次性的
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl;
alarm(1); //自己给自己设置闹钟,称为自举
}
int main(int argc, char *argv[])
{
signal(14,handler);
alarm(1);
while(true)
{
sleep(1);
}
return 0;
}
使用alarm可以计算CPU一秒计算量级
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int count = 0;
void handler(int signo)
{
std::cout << "get a signal: " << signo << "\\ncount: " << count << std::endl;
}
int main(int argc, char *argv[])
{
signal(14,handler);
alarm(1);
while(true)
{
count++;
}
return 0;
}
如果每次计算都进行打印可以发现和直接计算最后打印差上千倍,可见IO的速度是很慢的。
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
int main(int argc, char *argv[])
{
alarm(1);
while(true)
{
std::cout << "count: " << count++ << std::endl;
}
return 0;
}
4. 硬件异常产生信号
CPU
CPU中有一个状态寄存器,会检测本次计算是否有溢出问题,除0会导致溢出,OS检测到状态寄存器中记录了溢出错误时会向进程发送终止信号(8)SIGFPE),所以除0的本质就是出发硬件异常(CPU)
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
int a = 10;
a /= 0;
cout << "——除零错误——" << endl;
return 0;
}
OS会向进程循环发送信号
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "get a signal: " << signo << endl;
}
int main()
{
signal(SIGFPE, handler);
int a = 10;
a /= 0;
cout << "——除零错误——" << endl;
return 0;
}
MMU
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
int *a = nullptr;
*a = 10;
cout << "——野指针——"
return 0;
}
页表中不止有虚拟地址和物理地址,还有权限(rwx)。
查页表的动作由MMU完成,
虚拟地址到物理地址的转换采用软硬件结合。
*a = 10;
第一步并不是写入,而是首先进行虚拟到物理地址的转换。
没有映射,MMU硬件报错。
有映射但是没有权限,MMU硬件报错。
然后OS向进程发送信号11)SIGSEGV终止进程。
OS向进程循环发送11号信号,因为硬件报错并没有被修复
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
void handler(int signo)
{
cout << "get a signal: " << signo << endl;
}
int main()
{
signal(SIGSEGV, handler);
int *a = nullptr;
*a = 10;
return 0;
}
总结
信号的产生:
- 键盘
- 系统调用
- 软件条件
- 硬件异常
信号都需借助OS的手向目标进程发送信号,向目标进程的PCB信号位图。
根据上面的测试可以发现,进程信号的默认动作Term和Core都是终止进程,那么他们有什么区别呢?
Linux系统级别提供了一种能力,可以将一个进程在异常的时候,OS可以将该进程在异常时,核心代码部分进行核心转储,将内存中进程的相关数据,全部dump到磁盘中,一般会在当前进程的运行目录下,形成core.pid(核心转储文件)这样的二进制文件。
核心转储文件在云服务器上看不见,因为云服务器默认时关闭这个功能的。
ulimit -a #查看系统对应资源的上限
核心转储文件默认大小被设置为0了所以功能没有打开。
ulimit -c size
设置一个大小即可打开
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
//signal(SIGSEGV, handler);
cout << "——————野指针——————" << endl;
int *a = nullptr;
*a = 10;
return 0;
}
如果给进程手动发送信号呢?
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <signal.h>
using namespace std;
int main()
{
while (true)
{
cout << "我是一个进程,我的PID:" << getpid() << endl;
sleep(1);
}
return 0;
}
Term:终止,没有多余的其他动作
Core:先进行核心转储,再进行终止
核心转储有什么用?
方便程序出现异常后进行调试
核心转储为什么一般都是被关闭的?
云服务器一般是开发环境、测试环境、生产环境为一体的。默认是生产环境
在公司上万个服务器,如果核心转储功能是打开的,在深夜大家都睡着了,如果某个服务出了一个段错误,服务器的OS会尝试重启这个服务,但是这个服务的问题并没有被解决,所以会不断的产生核心转储文件,直到占满服务器的存储器。
进程状态里面有一个core dump标志,他是什么呢?
core dump标志表示是否发生core dump
#include <iostream>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
using namespace std;
int main()
{
pid_t id = fork();
if (id == 0)
{
cout << "——————野指针——————" << endl;
int *a = nullptr;
*a = 10;
}
int status = 0;
int a = waitpid(id, &status, 0);
cout << "child exit code: " << ((status > 8) & 0xFF) << endl;
cout << "child exit signal: " << (status & 0x7F) << endl;
cout << "core dump: " << ((status > 7) & 0x01) << endl;
return 0;
}
异常和中断
产生源不相同,异常是由CPU产生的,而中断是由硬件设备产生的。 中断是异步的,这意味着中断可能随时到来;而异常是CPU产生的,所以,它是时钟同步的。 当处理中断时,处于中断上下文中;处理异常时,处于进程上下文中。
阻塞信号
- 信号其他相关常见概念
实际执行信号的处理动作称为信号递达(Delivery) 信号从产生到递达之间的状态,称为信号未决(Pending)。 进程可以选择阻塞 (Block )某个信号。 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
信号在内核中的表示
pending表:位图结构,比特位的位置表示哪个信号,比特位的内容,代表是否收到该信号。
block表:位图结构,比特位的位置,表示哪个信号,比特位的内容代表对应的信号是否该被阻塞。
handler表:函数指针数组——void (*sighandler_t)(int);
。该数组的下标,表示信号编号,数组的特定下标内容,表示该信号的递达任务。
SIG_DFL(0): 默认动作
SIG_IGN(1): 忽略
信号集
sigset_t 属于OS的应用层一种数据类型,位图结构。
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set); //位图结构全置1
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo); //判断信号是否在信号集中
系统调用
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
//how :如何更改,三个选项SIG_BLOCK SIG_UNBLOCK SIG_SETMASK
//oset: 输出型参数,用于备份
sigpending
读取pending信号集
#include <signal.h>
int sigpending(sigset_t *set);
测试代码
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t& pending)
{
cout << "当前进程的Pending信号集: ";
for(int i = 1;i <= 31;i++)
{
if(sigismember(&pending,i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
int main()
{
//设置对2号信号的阻塞
sigset_t set,oset;
//初始化
sigemptyset(&set);
sigemptyset(&oset);
//2号信号添加进set
sigaddset(&set, SIGINT);
//更改阻塞信号集
sigprocmask(SIG_BLOCK, &set, &oset);
//循环获取、打印pending信号集
while(true)
{
sigset_t pending;
sigemptyset(&pending);
//读取pending信号集
int n = sigpending(&pending);
assert(n == 0);
(void) n;
showPending(pending);
sleep(1);
}
}
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t &pending)
{
cout << "当前进程的Pending信号集: ";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
int main()
{
// 设置对2号信号的阻塞
sigset_t set, oset;
// 初始化
sigemptyset(&set);
sigemptyset(&oset);
// 2号信号添加进set
sigaddset(&set, SIGINT);
// 更改阻塞信号集
sigprocmask(SIG_BLOCK, &set, &oset);
int count = 0;
// 循环获取、打印pending信号集
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 读取pending信号集
int n = sigpending(&pending);
assert(n == 0);
(void)n;
showPending(pending);
sleep(1);
// 十秒后取消对2号信号的屏蔽
if (count++ == 10)
{
cout << "已取消该进程对2号信号的屏蔽!" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
}
#include <iostream>
#include <cassert>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t &pending)
{
cout << "当前进程的Pending信号集: ";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
void handler(int signo)
{
cout << "捕捉到信号: " << signo << endl;
}
int main()
{
//自定义捕捉动作
signal(SIGINT, handler);
// 设置对2号信号的阻塞
sigset_t set, oset;
// 初始化
sigemptyset(&set);
sigemptyset(&oset);
// 2号信号添加进set
sigaddset(&set, SIGINT);
// 更改阻塞信号集
sigprocmask(SIG_BLOCK, &set, &oset);
int count = 0;
// 循环获取、打印pending信号集
while (true)
{
sigset_t pending;
sigemptyset(&pending);
// 读取pending信号集
int n = sigpending(&pending);
assert(n == 0);
(void)n;
showPending(pending);
sleep(1);
// 十秒后取消对2号信号的屏蔽
if (count++ == 10)
{
cout << "已取消该进程对2号信号的屏蔽!" << endl;
sigprocmask(SIG_SETMASK, &oset, nullptr);
}
}
}
信号的处理
信号处理,可以不是立即处理的,而是”合适“的时候。
信号可以被立即处理吗?
如果一个信号之前被block了,当他被解除block的时候,对应的信号会被立即递达!
为什么大部分情况下信号不是立即处理的呢?
信号的产生是异步的,当前进程可能正在做更重要的事情!
什么时候是合适的时候呢?
当进程从内核态切换回用户态的时候,进程会在OS的指导下,进行信号的检测与处理!
用户态:执行用户写的代码的时候,进程所处的状态。
内核态:执行OS的代码的时候,进程所处的状态。
进程怎么样会执行OS的代码?
1、 进程时间片到了,需要切换,就要执行进程切换逻辑。 2、 系统调用
再看地址空间
-
所有的进程[0,3]GB是不同的,每一个进程都要有自己的用户级页表
-
所有的进程[3,4]GB是一样的,每一个进程都可以看到同一张内核级页表,所有进程都可以通过统一的窗口,看到同一个OS!
-
OS运行的本质:其实都是在进程的地址空间内运行的!!
无论进程如何切换,[3,4]GB不变,看到OS的内容,与进程切换无关。
-
所以,所谓的系统调用的本质:其实就如同调用.so中的方法,在自己的地址空间中进行函数跳转并返回即可!
那么我们不就可以通过虚拟地址任意访问OS的数据和代码了吗?
不想看到这样的现象,所以有了用户态和内核态的概念。进程在地址空间中访问自己的代码[0,3]GB,此时的状态就是用户态。一旦进程要访问[3,4]GB的内容时,OS就会对进程的身份进行检测,如果不是内核态,CPU就会拒绝执行这部分代码,OS检测到硬件异常非法访问,向进程发送信号,终止进程。
CPU中有一个CR3寄存器,寄存器中有对应的比特位,如果对应的比特位为
- 3:表征正在运行的进程执行级别是用户态。
- 0:表征正在运行的进程执行级别是内核态。
谁来更改这些?
用户无法直接更改,所以,OS提供的所有系统调用,内部在正式执行调用逻辑的时候,会去修改执行级别!
-
所以进程是如何被调度的?
进程被调度,就是时间片到了,然后将进程对应的上下文等进行保存并切换,选择合适的进程,这些工作由系统函数schedule()来做
OS的本质是什么?
- OS是软件,本质是一个死循环。
- OS时钟硬件,每隔很短的时间向OS发送时钟中断,OS就要执行对应大的中断处理方法——检测当前进程的时间片,时间片到了OS就让进程调用schedule()函数。
信号捕捉
下图为信号捕捉的全过程:
过程简图:
本文在前面介绍了一个捕捉信号的方法,第二个方法sigaction()
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
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_flags设为0,sa_sigaction是实时信号的处理函数。
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t &pending)
{
cout << "当前进程的Pending信号集: ";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
void handler(int signo)
{
cout << "捕捉到信号: " << signo << endl;
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_flags = 0;
act.sa_handler = handler;
sigaction(SIGINT, &act, &oldact);
while (true)
{
sleep(1);
}
}
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t &pending)
{
cout << "当前进程的Pending信号集: ";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
void handler(int signo)
{
cout << "捕捉到信号: " << signo << endl;
int count = 10;
while(--count)
{
sigset_t pending;
sigemptyset(&pending);
int n = sigpending(&pending);
assert(n == 0);
(void) n;
showPending(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_flags = 0;
act.sa_handler = handler;
sigaction(SIGINT, &act, &oldact);
while (true)
{
sleep(1);
}
}
#include <iostream>
#include <cassert>
#include <cstring>
#include <unistd.h>
#include <signal.h>
using namespace std;
void showPending(const sigset_t &pending)
{
cout << "当前进程的Pending信号集: ";
for (int i = 1; i <= 31; i++)
{
if (sigismember(&pending, i))
cout << 1;
else
cout << 0;
}
cout << endl;
}
void handler(int signo)
{
cout << "捕捉到信号: " << signo << endl;
int count = 30;
while(--count)
{
sigset_t pending;
sigemptyset(&pending);
int n = sigpending(&pending);
assert(n == 0);
(void) n;
showPending(pending);
sleep(1);
}
}
int main()
{
struct sigaction act, oldact;
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
act.sa_flags = 0;
act.sa_handler = handler;
//阻塞3、4号信号
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);
sigaddset(&act.sa_mask,4);
sigaction(SIGINT, &act, &oldact);
while (true)
{
sleep(1);
}
}
可重入函数
- 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关键字
#makefile
test:test.cpp
g++ -o $@ $^
.PHONY:clean
clean:
rm -f test
#include <iostream>
#include <signal.h>
using namespace std;
int flag = 0;
void handler(int signo)
{
cout << "change flag 0 to 1" << endl;
cout << "flag = " << flag;
flag = 1;
cout << " -> flag = " << flag << endl;
}
int main()
{
signal(2, handler);
while(!flag);
cout << "程序正常退出" << endl;
return 0;
}
发现进程接受到信号后flag可以正常被修改,进程正常退出
但是如果在编译语句中加上优化呢
#makefile
test:test.cpp
**g++ -o $@ $^ -O1**
.PHONY:clean
clean:
rm -f test
优化情况下,键入 CTRL-C ,2号信号被捕捉,执行自定义动作,修改 flag=1 ,但是 while 条件依旧满足,进程继续运行!但是很明显flag肯定已经被修改了,但是为何循环依旧执行?很明显, while 循环检查的flag,并不是内存中最新的flag,这就存在了数据二异性的问题。 while 检测的flag其实已经因为优化,被放在了CPU寄存器当中。
解决方法就是使用volatile关键字,使flag不要让CPU优化。
#include <iostream>
#include <signal.h>
using namespace std;
volatile int flag = 0;
void handler(int signo)
{
cout << "change flag 0 to 1" << endl;
cout << "flag = " << flag;
flag = 1;
cout << " -> flag = " << flag << endl;
}
int main()
{
signal(2, handler);
while(!flag);
cout << "程序正常退出" << endl;
return 0;
}
volatile关键字保持内存的可见性,告知编译器,被该关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
如何理解编译器的优化?
- 编译器的本质是更改代码
- CPU其实很笨,用户喂给他什么代码,它就执行什么代码。
子进程退出了,父进程如何得知的呢?
父进程阻塞式等待&&非阻塞 ——都需要父进程主动检测
子进程在退出的时候,会向父进程发送信号SIGCHLD,而父进程对这个信号的默认动作是SIG_DFL,就是什么也不做
由于UNIX 的历史原因,要想不产生僵尸进程还有另外一种办法:父进程调 用sigaction将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不 会产生僵尸进程,也不会通知父进程。只在linux下有效。
signal(17,SIG_IGN);
这里的手动设置的IGN和之前默认动作中的IGN是不一样的