- 你在网上买了很多件商品,再等待不同商品快递的到来。但即便快递没有到来,你也知道快递来临时,你该怎么处理快递。也就是你能“识别快递”,也就是你意识里是知道如果这时候快递员送来了你的包裹,你知道该如何处理这些包裹
- 当快递员到了你楼下,你也收到快递到来的通知,但是你正在打游戏,需5min之后才能去取快递。那么在在这5min之内,你并没有下去去取快递,但是你是知道有快递到来了。也就是取快递的行为并不是一定要立即执行,可以理解成“在合适的时候去取”。你可以暂时将包裹搁置,等到有空的时候再处理。
- 在收到通知,再到你拿到快递期间,是有一个时间窗口的,在这段时间,你并没有拿到快递,但是你知道有一个快递已经来了。本质上是你“记住了有一个快递要去取”
- 当你时间合适,顺利拿到快递之后,就要开始处理快递了。而处理快递一般方式有三种:1. 执行默认动作(幸福的打开快递,使用商品)2. 执行自定义动作(快递是零食,你要送给你你的女朋友)3. 忽略快递(快递拿上来之后,扔掉床头,继续开一把游戏)
- 快递到来的整个过程,对你来讲是异步的,你不能准确断定快递员什么时候给你打电话
什么是Linux信号?
信号是进程之间事件异步通知的一种方式,属于软中断。
信号本质是一种通知机制,用户or操作系统通过发送一定的信号,通知进程,某些事件已经发生,进程可以根据信号的种类来进行相关处理、
结合生活信号,对信号的一些小结论
先简单理解一下信号被进程保存
进程内部有相关的数据结构位图,信号每次被发送给进程时,此时信号处于未决状态,那么用来保存信号的字段表中信号编号对应的位置就会由0变1
信号处理的三种方式
默认处理
忽略
自定义捕捉
我们平常在写代码时,其实经常都有在跟信号接触,看以下代码:
#include
#include
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
由于没有退出条件,因此进程一直处于死循环,此时我们在键盘里键入:Ctrl+c 能够将进程终止,其实按下组合键后本质就是向当前在前台运行的进程发送了终止信号,进程也立马对此信号做出了反应,终止掉了进程。这就是产生信号的其中一种方式
常见信号
信号一共有61个信号,32,33,0号信号是不存在的,而1-31的信号称为普通信号,32-64为实时信号,我们只学习普通信号:
关于信号的详细信息我们可以用man手册查看:man 7 signal
每个信号的编号都是被宏定义好的:
/* Signals. */
#define SIGHUP 1 /* Hangup (POSIX). */
#define SIGINT 2 /* Interrupt (ANSI). */
#define SIGQUIT 3 /* Quit (POSIX). */
#define SIGILL 4 /* Illegal instruction (ANSI). */
#define SIGTRAP 5 /* Trace trap (POSIX). */
#define SIGABRT 6 /* Abort (ANSI). */
#define SIGIOT 6 /* IOT trap (4.2 BSD). */
#define SIGBUS 7 /* BUS error (4.2 BSD). */
#define SIGFPE 8 /* Floating-point exception (ANSI). */
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGUSR1 10 /* User-defined signal 1 (POSIX). */
#define SIGSEGV 11 /* Segmentation violation (ANSI). */
#define SIGUSR2 12 /* User-defined signal 2 (POSIX). */
#define SIGPIPE 13 /* Broken pipe (POSIX). */
#define SIGALRM 14 /* Alarm clock (POSIX). */
#define SIGTERM 15 /* Termination (ANSI). */
#define SIGSTKFLT 16 /* Stack fault. */
#define SIGCLD SIGCHLD /* Same as SIGCHLD (System V). */
#define SIGCHLD 17 /* Child status has changed (POSIX). */
#define SIGCONT 18 /* Continue (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
#define SIGTSTP 20 /* Keyboard stop (POSIX). */
#define SIGTTIN 21 /* Background read from tty (POSIX). */
#define SIGTTOU 22 /* Background write to tty (POSIX). */
#define SIGURG 23 /* Urgent condition on socket (4.2 BSD). */
#define SIGXCPU 24 /* CPU limit exceeded (4.2 BSD). */
#define SIGXFSZ 25 /* File size limit exceeded (4.2 BSD). */
#define SIGVTALRM 26 /* Virtual alarm clock (4.2 BSD). */
#define SIGPROF 27 /* Profiling alarm clock (4.2 BSD). */
#define SIGWINCH 28 /* Window size change (4.3 BSD, Sun). */
#define SIGPOLL SIGIO /* Pollable event occurred (System V). */
#define SIGIO 29 /* I/O now possible (4.2 BSD). */
#define SIGPWR 30 /* Power failure restart (System V). */
#define SIGSYS 31 /* Bad system call. */
#define SIGUNUSED 31
其中,Ctrl+c 组合键对应的就是2号信号SIGINT,它所对应的Action行为是Term行为:Terminate终止动作
注意
如何理解组合键变信号?
我们前面提到过,信号被写入进程后,进程对此信号的处理有三种方式,我们先来简单介绍一下自定义捕捉方式
介绍signal函数
功能:能够捕捉指定的信号编号为signum的信号,然后将此信号的处理方式使用我们自定义的方式handler
其中:sighandler_t是一个函数指针,以一个的函数指针作为另一个函数的参数并在函数内使用函数指针调用对应指向的函数,此函数称为回调函数,自定义捕捉时,我们实现自定义函数,并将函数作为参数传给signal,通过回调的方式,来修改对应信号的捕捉方法。返回值是自定义捕捉前的函数的地址。
通过信号捕捉的方式,验证Ctrl+c组合键是SIGINT信号
#include
#include
#include
using namespace std;
void cathSig(int signum)
{
cout<<"i catch you! SIGINT!"<
我们可以观察到,按下Ctrl-c后进程并不会像之前一样退出,而是执行了我们自己的代码!得以验证以下两个结论:
注意
我们先来比较两个信号
这两个信号都是从键盘敲组合键来向进程发送信号,其中:
他们两个的作用都能够用来终止进程,但不同的是:
2号对应的行为是Term:Terminate
3号对应的行为是Core:Core Dump
什么是Core Dump?
使用命令ulimit命令查看并修改相关资源配置
显示0时,代表核心转储不被允许产生,我们需要自行开启:
此时已显示core文件允许的最大内存为1024K,但仅仅在当前会话生效,退出会话后就失效了
验证Ctrl-\ 发送的SIGQUIT能够产生core文件的功能
先写一个死循环程序:
int main()
{
while(1)
{
printf("my pid:%d\n",getpid());
sleep(1);
}
}
运行并用Ctrl-c命令终止后发现并没有产生任何文件:
再次运行并使用Ctrl-\ 命令终止:
这次我们发现产生了core.27213文件,并且后缀就是以进程的pid来命名的,代表此文件就是此进程出现某种异常时,由os将此进程在内核中相关的核心数据转存到磁盘中
核心转储有什么用?
利用核心转储进行调试
除了SIGQUIT信号的行为是core,还有其他很多命令也有core行为,下面我们使用SIGFPE信号来验证使用核心转储调试时是否会方便
FPE:Floating point exception指的是浮点数错误,一般指除0错误
首先编写程序,并且编译时,带上-g选项,生成debug文件
int main()
{
while (1)
{
printf("my pid:%d\n", getpid());
sleep(1);
int a=100;
a/=0;
printf("run here...\n");
}
}
重新运行程序,此时也随之生成了core dump文件
启用gdb调试程序,输入core-file core.pid(core文件名)
此时我们能发现,在core dump文件的帮助下,能自动帮我们定位在哪一行代码收到了什么信号,比如这里,我们在29行除0错误处收到了8号信号,而8号信号就是对应的SIGFPE信号
core dump标记位
在进程控制一篇提到过,子进程退出时,父进程可以通过进程等待的方式,收集子进程的退出信息,以防子进程变成僵尸进程,其中进程等待可以使用wait和waitpid系统调用
其中我们也提到过status参数是一个输出型参数,其中如果子进程是被信号所杀的话,此参数的低七位会被填入终止信号的编号,而第八位则是显示是否有生成core dump文件,我们也可以通过代码来验证一下:
用子进程验证core dump标记位
int main()
{
pid_t id = fork();
if (id == 0)
{
// child
sleep(1);
int a = 100;
a /= 0;
exit(0);
}
int status = 0;
waitpid(id, &status, 0);
cout
<< "father:" << getpid()<<" "
<< "child:" << id <<" "
<< "exit sig:" << (status & 0x7F) <<" "
<< "is core:" << ((status >> 7) & 1) << " "
<<
endl;
return 0;
}
观察发现,core dump标记位为1,且也产生了子进程的core dump文件,对应的退出信号是8号信号
为什么生产环境下core dump文件默认会被关闭?
除了可以通过组合键的方式向前台运行的进程发送命令之外,我们还可以通过命令的方式向前台或者后台运行的进程发送命令
kill命令
9号信号能够直接杀死进程
kill函数
能够向对应pid的进程发送sig信号,若成功发送则返回0,失败则返回-1.
通过kill函数实现一个命令行mykill命令
static void Usage(string proc)
{
cout<<"Usage:\r\n\t"<<proc<<" signumber processid"<<endl;
}
int main(int argc,char* argv[])
{
if(argc!=3)
{
Usage(argv[0]);
exit(1);
}
int signumber=atoi(argv[1]);
int procid=atoi(argv[2]);
kill(procid,signumber);
return 0;
}
通过自己写的kill命令也能够发送信号杀死进程。
raise函数
raise函数也是可以用来发送信号的系统接口,但是其相较于kill函数它只能给自己的进程发送信号
int main()
{
while(1)
{
cout<<"start:"<<"my pid:"<<getpid()<<endl;;
sleep(1);
raise(9);
}
return 0;
}
abort函数
int main()
{
while(1)
{
cout<<"start:"<<"my pid:"<<getpid()<<endl;;
sleep(1);
abort();
}
return 0;
}
我们在进程间通信就接触过软件条件产生的信号,我们在使用”管道“进行进程间通信时,当读端进程退出时,写端仍不断在向管道内写数据,此时系统就会向管道发送SIGPIPE信号终止写端程序
验证SIGPIPE信号
编码,其中创建管道文件,让父进程做为读端读取管道文件,子进程作为写端向管道内写文件
两种方法验证:
利用父进程能够回收子进程退出信号,打印退出信号验证
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n==0);
pid_t id= fork();
if(id==0)
{
//child
close(pipefd[0]);
const char* buffer="i am child ...";
while(1)
{
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
}
//father
close(pipefd[1]);
int count=5;
char buf[128];
while(count--)
{
read(pipefd[0],buf,sizeof(buf)-1);
cout<<buf<<endl;
sleep(1);
}
cout<<"now close read..."<<endl;
close(pipefd[0]);
int status=0;
waitpid(id,&status,0);
cout<<"exit signal:"<<(status&0x7F)<<endl;
return 0;
}
通过信号捕捉直接捕捉SIGPIPE信号验证
void handler(int signum)
{
cout<<"catch signal:"<<signum<<endl;
exit(0);
}
int main()
{
int pipefd[2]={0};
int n=pipe(pipefd);
assert(n==0);
pid_t id= fork();
if(id==0)
{
//child
// signal(SIGPIPE,handler);
close(pipefd[0]);
const char* buffer="i am child ...";
while(1)
{
write(pipefd[1],buffer,strlen(buffer));
sleep(1);
}
}
//father
close(pipefd[1]);
int count=5;
char buf[128];
while(count--)
{
read(pipefd[0],buf,sizeof(buf)-1);
cout<<buf<<endl;
sleep(1);
}
cout<<"now close read..."<<endl;
close(pipefd[0]);
return 0;
}
alarm函数
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动作是终止当前进程。
返回值是0或者是以前设定的闹钟时间还余下的秒数。打个比方,某人要小睡一觉,设定闹钟为30分钟之后响,20分钟后被人吵醒了,还想多睡一会儿,于是重新设定闹钟为15分钟之后响,“以前设定的闹钟时间还余下的时间”就是10分钟。如果seconds值为0,表示取消以前设定的闹钟,函数的返回值仍然是以前设定的闹钟时间还余下的秒数。
利用alarm测试服务器算力
int main()
{
int count=0;
alarm(1);
while(1)
{
count++;
cout<<count<<endl;
}
return 0;
}
执行结果是能在1s内将变量累加到十万级别,但这并不是cpu的真正算力。
由于我们在计算过程中还加入了打印功能,这其中涉及了io操作,并且我们实在云服务器上测试的,还有网络发送的影响,因此我们想算真正的算力,需要通过一直加到信号递达时,我们通过自定义捕捉信号来查看此时累加到了多少,这样可以避免io和网络的影响。
int count=0;
void handler(int signum)
{
cout<<"catch signal: "<<signum<<" count:"<<count<<endl;
exit(1);
}
int main()
{
signal(SIGALRM,handler);
alarm(1);
while(1)
{
count++;
}
return 0;
}
此时我们发现,累加的结果已达到亿级别,可见io带上网络之后,效率是非常低的!
如何理解软件条件给系统发送信号
cpu是如何知道哪个进程出现了异常?
CPU内部,有很多寄存器,其中有一个寄存器叫做状态寄存器,其原理是跟位图一样,各种异常都会有对应的标记位,如溢出标记位等,cpu在计算完毕后,OS会自动进行检测状态标记位,如果识别到有被置为1的标记位,此时cpu的数据正好是当前进程正在运行的上下文数据,其中就有指向进程控制块task_struct 的指针,只要通过此指针提取进程pid,OS就能向进程发送相应的信号,进程便会在合适的时候进行处理。
出现了硬件异常,进程一定会退出吗?
不一定!只是进程对硬件异常的默认处理方式是退出,但是即便我们不退出,我们也无法做其他动作,因为状态寄存器会一直标记有此异常,会不断发送信号!我们通过代码来验证一下:
野指针问题时,对硬件信号进行捕捉但进程并不退出会发生什么?
void handler(int signum)
{
cout<<"receive signal:"<<signum<<endl;
sleep(1);
}
int main()
{
signal(SIGSEGV,handler);
int *p=NULL;
*p=100;
while(1);
return 0;
}
通常的,野指针或者越界问题都是需要通过地址来找到对应的值,而我们语言上所涉及的地址都是虚拟地址,是需要通过映射来转化成物理地址的,而转化地址需要用到页表和MMU(Memory Manager Unit),MMU属于硬件,我们需要知道,不仅仅是CPU才具有寄存器的,几乎任何外设都可以存在寄存器,因此MMU内部也存在状态寄存器!
MMU检测到我们所使用的野指针和越界访问是属于非法地址!因此会将状态寄存器中的段错误标记为置为1,而OS会自动进行检测,识别被置为1的标记位后,就会向此进程发送相应的SIGSEGV(段错误)—11号信号,进程收到信号后,由于我们进行了信号捕捉,并且没有进行进程的退出,因此不会执行默认处理,而此时我们的代码也存在死循环,因此进程一直都不会退出,此时CPU仍进行正常调度,调度后当前进程就会被切换,上下文数据也会被保护起来,包括状态寄存器内的异常标记位,等到下一次又调度到此进程时,上下文数据重新被恢复,因此OS检测后又会发送信号,造成了不断执行我们的自定义捕捉代码的困境
所以说即使我们不退出,我们也做不了什么,因此捕捉信号并打印错误信息后,直接退出。
引入问题
信号在内核中的示意图
解释示意图
解释引入的问题
合适的时候:阻塞标志位为0,代表此信号进程可以立即递达
需要被记录下来,未决标记为就是记录此信号尚未被处理
信号被递达后,会执行默认处理动作,若信号被捕捉,则会将用户空间的handler函数填入handler表中,handler表本质是一个函数指针数组,数组下标为信号编号,数组内容便是处理动作
进程收到信号时,进程控制块内的pending表中对应的信号会先被设置成1,表示此信号现在处于未决状态,系统会定时检查未决状态表处于未决状态的信号,若发现此时有一个未决信号的阻塞状态是0,即此信号没有被阻塞,则就执行对应的处理方法,如果是检查到是被阻塞了,则不做处理
默认处理动作
handler表中信号的处理动作有三种:
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释,比如用printf直接打印sigset_t变量是没有意义的。
接口介绍
#include
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
关于使用,我们在介绍完其他接口后一起使用。
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
第三个参数oset是输出型参数,相当于保存修改前的信号屏蔽字,如果不需要则可以设置为空
第二个参数set是将现有的信号屏蔽字传进去,并根据第一个参数how的指示具体来更改,how的方法有以下三种:我们假设现有信号屏蔽字sigset_t mask
**注意:**如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。
#include
int sigpending(sigset_t *set);
作用:获取当前调用进程的pending信号集,并填入参数set中,调用成功返回1,出错则返回0。
实验———测试信号集操作函数
在内核宏定义中,9号和19号信号是不可被阻塞的信号:
#define SIGKILL 9 /* Kill, unblockable (POSIX). */
#define SIGSTOP 19 /* Stop, unblockable (POSIX). */
我们先测试一下,将2号信号阻塞,并不断打印当前进程的pending信号集,接着向进程发送2号信号并观察现象
void handler(int signum)
{
cout<<"catch a signal: "<<signum<<endl;
}
static void showPending(sigset_t &pending)
{
for(int sig=1;sig<=31;sig++)
{
if(sigismember(&pending,sig)){
cout<<'1';
}
else{
cout<<'0';
}
}
cout<<endl;
}
int main()
{
signal(2,handler);
//定义
sigset_t obset,bset,pending;
//初始化
sigemptyset(&bset);
sigemptyset(&obset);
sigemptyset(&pending);
//添加要屏蔽的信号
sigaddset(&bset,SIGINT);
//设置到当前进程内核
int n=sigprocmask(SIG_BLOCK,&bset,&obset);
assert(n==0);
(void)n;
cout<<"阻塞2号信号成功...,pid: "<<getpid()<<endl;
//不断打印pending信号集
int count=0;
while(true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
count++;
if(count==20)
{
int n=sigprocmask(SIG_SETMASK,&obset,nullptr);
assert(n==0);
(void)n;
cout<<"解除对2号信号阻塞成功"<<endl;
}
}
return 0;
}
现象:
首先我们对2号信号做阻塞处理,在前10秒时,我们通过Ctrl-c发送2号信号,可以观察到pending信号集2号信号处标志位被标记成了有效,由‘0’ 变为了 ‘1’ 代表此信号尚未被处理,10秒过后,我们对2号信号解开阻塞,此时会先有一次处理动作,然后pending标记位也变成了无效,由‘1’ 变为了 ‘0’,此后再发送2号信号都是捕捉成功。
现在我们知道了若我们对某一个信号进行阻塞,并发送信号时,此信号的pending标记位会从无效变有效,其实就是告诉操作系统这还有个信号处于未决状态尚未处理,我们现在来测试一下若对所有的信号都进行阻塞,并发送每一个信号,会是什么样的效果。
我们先写一个脚本文件,能够一直不断给signal对应的pid从1-31号发送信号
注意,此时我们没有忽略无法被阻塞的9号和19号信号,也就是9号信号也会被发送,但是我们对他也进行了阻塞处理,我们观察是否真的无法被阻塞。
static void showPending(sigset_t &pending)
{
for(int sig=1;sig<=31;sig++)
{
if(sigismember(&pending,sig)){
cout<<'1';
}
else{
cout<<'0';
}
}
cout<<endl;
}
static void blockSig(int signum)
{
sigset_t bset;
sigemptyset(&bset);
sigaddset(&bset,signum);
int n=sigprocmask(SIG_BLOCK,&bset,nullptr);
assert(n==0);
(void)n;
}
int main()
{
//block all signal
for(int sig=1;sig<=31;++sig)
{
blockSig(sig);
}
sigset_t pending;
sigemptyset(&pending);
while(true)
{
sigpending(&pending);
showPending(pending);
sleep(1);
}
return 0;
}
我们观察到,就算9号信号被阻塞了,我们发送9号信号时,照样会将进程杀死,因此9号信号是无法被阻塞的!而9号信号之前的信号,pending表相应的位置都被置为了有效,和第一个测试一样。
接下来,我们跳过9号和19号这两个无法被阻塞的信号,再次观察
脚本:跳过9号和19号信号
结论:9号和19号信号不可阻塞!
我们在前面有初步的学习了一下信号的捕捉signal函数,他的功能是能对某一个信号的handler表进行修改,更改为用户自定义的处理动作函数,在此信号抵达时就会调用用户自定义的函数,这就叫信号捕捉。那么问题来了:内核是如何实现信号的捕捉的呢?
内核如何实现信号的捕捉?
有点复杂,没关系,我们一点一点来认识:
内核级页表
用户态和内核态
举个例子来理解,当我们自己写的代码中有诸如open这类的系统调用接口,我们首先在用户态执行其他的普通代码,执行到系统调用时,由于open系统调用内置有从用户态切换到内核态的代码,所以此时对应的状态也变成了内核态,所以此时我们有了访问内核空间的权限,内核态下就会到内核空间中找到关于open系统调用的相关数据,并通过内核级页表映射找到物理内存中内核空间的open系统调用的实现,并执行相关操作。
简单理解如何切换到内核态执行系统调用
什么时候会进行状态切换?
除了刚刚提到的调用系统调用时会从用户态切换为内核态,还有别的情况下也会进行状态切换
内核态切换为用户态:
了解完这些,我们说回信号
系统什么时候对信号进行检测?内核又是如何实现信号捕捉的?
我们可以用这样 “ 8 ” 来形象的表示状态的切换,8与直线的交点代表切换了多少次,而8中间的交点则代表从内核返回用户态时做的信号检测
为什么执行用户自定义捕捉动作时,要切回用户态?
我们在前面介绍了信号捕捉函数signal,除了signal之外,我们还可以使用sigaction函数来进行信号捕捉
#include
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
测试sigaction函数
使用sigaction函数捕捉2号信号
自定义捕捉函数可以在倒计时10s内打印本进程的pending字段
我们分别两次向进程发送2号信号,观察现象
void handler(int signum)
{
cout<<"catch a signal: "<<signum<<endl;
cout<<"catch a signal: "<<signum<<endl;
cout<<"catch a signal: "<<signum<<endl;
cout<<"catch a signal: "<<signum<<endl;
sigset_t pending;
int count=10;
while(count)
{
sigpending(&pending);
showPending(pending);
count--;
sleep(1);
}
}
int main()
{
struct sigaction act,oact;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
act.sa_handler=handler;
sigaction(2,&act,&oact);
cout<<"default action: "<<(int)(oact.sa_handler)<<endl;
while(true) sleep(1);
return 0;
}
观察上图函数的执行:
main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的 时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换 到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2
插入操作的 两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后 向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为 不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
如果一个函数符合以下条件之一则是不可重入的
作用
什么意思呢?我们通过代码来看看:
int flag=0;
void handler(int signum)
{
(void)signum;
cout<<"change flag:"<<flag;
flag=1;
cout<<"-> "<<flag<<endl;
}
int main()
{
signal(2,handler);
while(!flag);
cout<<"exit->normally"<<endl;
return 0;
}
-O3优化代码
我们在编译的时候,带上了 -O3 选项之后,就出现了上述情况,是因为此选项是在告诉编译器,编译此份代码时将其优化到最高级别的优化状态,而编译器也照做了,在其检查代码的时候,发现main函数对flag变量的使用仅仅是判断语句,没有任何语句是用来修改flag的,因此编译器就会将flag此时的值直接放入到寄存器中,这样一到判断语句时,cpu便直接在寄存器中拿flag的值来进行判断,而不再去访问内存中来检测,这样子大大提高了速度和效率,但是若flag此全局变量的值若在外部被修改,cpu也不会知道,一直用寄存器的值检测。这样就出现了死循环不退出的情况。
而volatile关键字正是为了避免这种情况,被volatile关键字修饰的变量,就算代码被优化成最高级别,cpu也照样会去内存中查看此变量的值,这就叫保证了内存的可见性。
就算被优化了也能够正常退出
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略(若父进程没有进行等待,且默认处理动作为忽略,则子进程会变成僵尸进程!),父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
测试SIGCHLD
void handler(int signum)
{
int id;
while((id=waitpid(-1,nullptr,WNOHANG))>0)
{
printf("wait child %d successfully!\n",id);
}
}
int main()
{
signal(SIGCHLD,handler);
pid_t id=fork();
if(id==0)
{
printf("child pid: %d\n",getpid());
sleep(3);
exit(1);
}
while(1)
{
printf("father are doing something!\n");
sleep(1);
}
return 0;
}
4dZqx-1707561889261)]
而volatile关键字正是为了避免这种情况,被volatile关键字修饰的变量,就算代码被优化成最高级别,cpu也照样会去内存中查看此变量的值,这就叫保证了内存的可见性。
[外链图片转存中…(img-1K8RsTCl-1707561889261)]
[外链图片转存中…(img-op7B67FO-1707561889261)]
就算被优化了也能够正常退出
进程一章讲过用wait和waitpid函数清理僵尸进程,父进程可以阻塞等待子进程结束,也可以非阻 塞地查询是否有子进程结束等待清理(也就是轮询的方式)。采用第一种方式,父进程阻塞了就不 能处理自己的工作了;采用第二种方式,父进程在处理自己的工作的同时还要记得时不时地轮询一 下,程序实现复杂。
其实,子进程在终止时会给父进程发SIGCHLD信号,该信号的默认处理动作是忽略(若父进程没有进行等待,且默认处理动作为忽略,则子进程会变成僵尸进程!),父进程可以自 定义SIGCHLD信号的处理函数,这样父进程只需专心处理自己的工作,不必关心子进程了,子进程 终止时会通知父进程,父进程在信号处理函数中调用wait清理子进程即可。
测试SIGCHLD
void handler(int signum)
{
int id;
while((id=waitpid(-1,nullptr,WNOHANG))>0)
{
printf("wait child %d successfully!\n",id);
}
}
int main()
{
signal(SIGCHLD,handler);
pid_t id=fork();
if(id==0)
{
printf("child pid: %d\n",getpid());
sleep(3);
exit(1);
}
while(1)
{
printf("father are doing something!\n");
sleep(1);
}
return 0;
}