文章目录
异常
控制流的概念:
EFC(异常控制流):现代系统通过控制流发生突变来对突发情况进行处理。
突发性情况:硬件定时器定期产生信号;进程向磁盘请求数据,进程会进入休眠状态,直到被通知数据已就绪;子进程终止,父进程回收信号。
异常是异常控制流当中的一种常见的形式,他是一部分通过硬件,一部分通过操作系统实现的,下面会叙述异常的产生信号的四种方式。
异常的处理:
系统当中为每一种异常都提供了唯一的非负整数的异常号,在我们的系统启动就会被分配和初始化一张异常表的跳转表,这个表结构在后续也会讲述,这张表可以通过一些系统调用来修改signal,异常的处理实际上就是一个执行跳转代码的过程,与普通函数的最大的不同在于想要执行对应的异常标的代码需要在内核模式下去执行,当然如果是自定义的代码会跳转为用户层执行。
信号
生活当中存在信号,例如红绿灯,日常生活当中的我们会遵循红灯停,绿灯行的这一行为,是因为我们提前被注册了看到这一信号对应的处理方法。
而人就被类比成进程,进程在遇到信号的时候也会有对应的动作。
普通信号 1 ~ 31,实时信号34 ~ 64.
研究信号,无异于处理三个环节:
信号产生时,信号等待中,信号处理中。
异常产生信号
通过终端产生信号
我们日常在命令行结束前台进程的时候所用的ctrl+c的组合键,实际上就是往前台进程发生2号信号,为了验证这个说法,我们先了解一个接口。
signal
捕捉一个信号,并执行我们传入的函数,signal实际就是一种回调函数。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
测试中断产生信号代码:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void func(int no)
{
printf("捕获信号: %d\n", no);
}
int main()
{
signal(2, func);
while (1)
{
printf("I am a process!\n");
sleep(1);
}
return 0;
}
结果:ctrl+c确实往前台进程发送2号信号,而20号信号没有捕获,当向前台进程发送20号信号的时候,进程收到来自终端的停止信号,等到SIGCONT才会继续运行。
ps: 前台进程只有一个,后台进程有多个,当进程放到后台时,终端按键产生信号就杀不掉前台进程了。fg+[2]编号可以将后台进程放到前台进程运行。
通过系统调用向进程发送信号
一个系统调用:kill函数,命令行当中的kill指令也是依靠kill的系统调用是西安的。
kill函数可以给一个指定的进程发送指定的信号。raise函数可以给当前进程发送指定的信号(自己给自己发信号)
#include <signal.h>
int kill(pid_t pid, int signo);
测试kill函数:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void func(int no)
{
printf("捕获信号: %d\n", no);
}
int main()
{
signal(2, func);
int count = 5;
while (1)
{
if (count-- == 0)
{
//count为0给自己发送2号信号
kill(getpid(), 2);
}
printf("I am a process!\n");
sleep(1);
}
return 0;
}
结果:kill函数给自己发送2号信号后,被signal捕捉了。
如何理解命令行当中的kill:
类似下面的实现方式,再将当前路径配置到系统的默认路径下就可以实现kill一样的调用方法了。同理sleep。
两个普通函数:
raise函数
int raise(int signo);
这两个函数都是成功返回0,错误返回-1。
对raise函数的测试:
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
void func(int no)
{
printf("捕获信号: %d\n", no);
}
int main()
{
signal(2, func);
int count = 5;
while (1)
{
if (count-- == 0)
{
//count为0给自己发送2号信号
raise(2);
}
printf("I am a process!\n");
sleep(1);
}
return 0;
}
结果:也同样是接受到了自己发送的2号信号。
abort函数
使当前进程接收到6号信号而异常终止。
#include <stdlib.h>
void abort(void);
就像exit函数一样,abort函数总是会成功的,所以没有返回值
测试abort
#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<stdlib.h>
void func(int no)
{
printf("捕获信号: %d\n", no);
}
int main()
{
//注册所有普通信号
for (int i = 1; i < 32; ++i)
signal(i, func);
int count = 5;
while (1)
{
if (count-- == 0)
{
//count为0给自己发送6号信号
abort();
}
printf("I am a process!\n");
sleep(1);
}
return 0;
}
结果:signal6号信号,当abort后依旧会程序退出,这点和之前的信号有些不同。
了解三个信号
19号信号SIGSTOP,暂停指定进程,进程状态变为T, 该信号无法被捕捉。 类比游戏暂停,游戏本身就是多个进程的协作,发送该信号相当于让进程暂停。
[ljh@VM-0-11-centos 3.7]$ ./test
I am a process,my pid: 23532
I am a process,my pid: 23532
I am a process,my pid: 23532
[7]+ Stopped ./test
[ljh@VM-0-11-centos 3.7]$ jobs
[1]+ Stopped ./test
查看进程状态为Stopped
18号 信号SIGCONT,启动指定进程
9号信号,最强势的命令,能杀掉大部分进程(除僵尸进程)。
软件条件产生信号
1.譬如先前的管道中,读端关闭,写端还在写,操作系统就会将写端的进程发送信号。
2.以及一条函数:alarm,能够在时间到了之后给自己发送一条信号。
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
调用alarm函数可以设定一个闹钟,也就是告诉内核在seconds秒之后给当前进程发SIGALRM信号, 该信号的默认处理动
作是终止当前进程。
测试alarm
#include<stdio.h>
#include<unistd.h>
int main()
{
alarm(2);
while(1)
{
printf("I am a running process: %d\n",getpid());
sleep(1);
}
return 0;
}
结果:
[ljh@VM-0-11-centos 3.10]$ ./signal
I am a running process: 27963
I am a running process: 27963
Alarm clock
硬件条件产生异常
大家曾经都遇到过这些错误,除零错误,指针越界。
除零错误:int a = 1/0;
假设运行到这行代码,CPU的运算部件会产生异常,CPU此时知道是自己调度的进程出现了问题,操作系统就可以发送SIGFPE信号给该进程
指针越界:指针越界更常见,对常量区的修改,访问数组越界,这都是指针越界。
当发生上述过程的时候页表转换MMU硬件会识别,操作系统检测到异常便将SIGSEGV发送给对应的进程。
信号的保存
信号的处理并不是立即处理(后面讲),那么未处理的信号存在哪里合适呢?
要存储信号,得知道用何种数据结构来存储,普通信号为1-31位,实时信号在34-64(这里讲述普通信号)。其实用一个size_t/int,都可以很好的讲这普通信号的31个比特位进行保存。保存在哪里呢?保存在进程控制块当中。
内核的位图时表示
task_struct当中的字段:
因为进程在处理一些工作的时候是不可中断的,处理信号需要从用户到内核态,切换上下文。
有了上面的位图,我们就知道位置为0表示这个位置没有信号,有则表示有信号,但有一张表还仅仅不够,实际上还有一张阻塞位图,阻塞表当中置1表示这个信号被阻塞了。并且还有一张handler数组,数组当中存放的是函数指针,这个数组就是函数指针数组。
进程相关的重要概念
实际执行信号的处理动作称为信号递达(Delivery)
信号从产生到递达之间的状态,称为信号未决(Pending)。
进程可以选择阻塞 (Block ) 某个信号。(与IO的阻塞没有关系!!!)允许某些信号不会被递达。直到解除阻塞才可以递达。9号信号无法阻塞。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.
三张表结构:
pending位图:比特位的位置代表信号的编号,比特位的内容(0 或者 1)代表是否收到该信号。OS发送信号本质上是修改task_struct pending位图的内容。
handler数组:用信号的编号作为数组的索引,找到信号对应的处理方式,然后指向对应方法。
block位图:比特位的位置代表信号的编号,比特位的内容(0 或 1)代表是否阻塞该信号。即使没有收到该信号,照样可以阻塞信号!!
如下图当中,1号信号没有被阻塞,也没有在未决信号集,倘若有1号信号来,他会执行默认行为;2号信号则是被阻塞了,并且此时处于未决(即信号来了还没处理),不过他的信号默认处理方式此时是忽略,即使更改信号屏蔽字,它的处理方式是忽略,也没有现象发生;3号信号是被阻塞了,但是该进程未接收到3号信号,所以他的pending表的对应位是0,当它的信号屏蔽字被更改,并且有信号递达时,他的处理方式是用户自定义的sighander。
若是暂停,或者杀掉信号的之指令不需要返回系统调用了,直接走退出进程的那一套或者在内核态暂停即可。忽略的话直接pending由1置0,就可以返回系统调用了。
理解为什么自定义要回到用户态去执行代码:
内核的权限高,理论上执行用户层代码是可以的,但是,倘若代码是一段恶意的程序,别人自定义的handler函数若是以exec*系列替换成
阻塞 vs 忽略
阻塞的信号可以没有来,也可以在未决的位图,阻塞的信号可以再通过更改阻塞信号集后递达。而忽略是信号递达后的一种处理方式,被忽略的信号一定不在信号屏蔽字(阻塞信号集)当中。
小例子:今天别人抢劫了你,你无法报警,那么就是被阻塞了,当你从抢匪逃脱出来,发送信号给朋友,朋友没有接受,这个时候就是信号被忽略了。
信号的递达
从下图可以知道关于常见信号的一些默认处理方式,信号递达前,我们可以通过signal函数更改信号的默认行为。
早在之前我们学习过父进程需要等待子进程退出拿到对应的返回值,这个过程中虽然waitpid有非阻塞的方式进行轮询检测,但是这样还是会造成父进程效率上的损失,那么有没有更好的方法呢?
观察上图,结合之前所学知识,子进程在退出的时候会给父进程发送SIGCHLD,这个信号的默认动作是忽略,通过更改这个动作实际上就可以让父进程接收到这个信号的时候再去waitpid将子进程的退出状态获取了,而父进程和这个信号的到来就是异步的了。
注意:信号是不会排队的!!
sigaction
提供了比signal更多的选择,兼顾了实时信号。用于信号的捕捉。
man手册struct sigactioon的字段。其中的sa_sigaction和sa_restorer是用于实时信号的。而sa_handler用于定义捕捉的方法。sa_flags通常设置成0,执行我们的默认动作,sa_mask可以用于阻塞你想阻塞的其他信号,希望屏蔽一些其他信号时加入(递达时对应的信号会被加入block,这个字段是可以设置其他信号是否需要屏蔽的。)Linux不允许在信号的处理流程当中处理同一个信号,会暂时把对应信号block,回到用户层的系统调用之前取消block。即默认情况下,一个信号在处理某个信号,该信号会被短暂的block,直到处理完毕,即将返回用户层代码时解除阻塞。
测试sigaction的使用
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
printf("signo:%d\n",signo);
}
int main()
{
struct sigaction sg;
struct sigaction oldsg;
sigemptyset(&sg.sa_mask);
sg.sa_flags = 0;
sg.sa_restorer = nullptr;
sg.sa_handler = handler;
sigaction(2,&sg,&oldsg);
while(1)
{
printf("process is running!\n");
sleep(1);
}
return 0;
}
结果:
[ljh@VM-0-11-centos 3.12]$ ./test
process is running!
process is running!
^Csigno:2
process is running!
process is running!
process is running!
^Csigno:2
process is running!
process is running!
^Z
[1]+ Stopped ./test
测试屏蔽其他信号。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
void handler(int signo)
{
printf("signo:%d\n", signo);
sleep(10);
}
int main()
{
struct sigaction sg;
struct sigaction oldsg;
sigemptyset(&sg.sa_mask);
sg.sa_flags = 0;
sg.sa_restorer = nullptr;
sg.sa_handler = handler;
//设置处理时默认屏蔽3号信号
sigaddset(&sg.sa_mask, 3);
//将sigaction结构体拷贝入内核
sigaction(2, &sg, &oldsg);
while (1)
{
printf("process is running!\n");
sleep(1);
}
return 0;
}
结果: 信号在pending表当中,并且被阻塞了,信号下次来的时候对信号进行处理(3号信号,退出进程)。并且发送2号信号的时候,2号信号也是被屏蔽的。
信号的处理
信号的处理是在用户切换到内核态的时候才会完成的。打个比方,进程执行代码,而此时一个SIGINT信号发过来,而进程正好调用到write命令,那么进程就会由于系统调用这种陷阱而嵌入内核层,在内核层处理完异常,系统调用是一种异常,进程就会检测是否有未决信号,通常是将最低位的未决信号处理,倘若是内核的代码,那么就在内核当中执行完再回到应用层,而若是用户自定义的代码,那么要回到用户模式下执行完才会回到内核当中。
为什么自定义的处理方法在用户层执行?
内核层的权限很大,此时自定义的代码若存在问题,会对操作系统造成很大的伤害,但是倘若此时切换成用户态执行,若出现问题,系统会有各种机制将问题捕获处理,处理不了就终止进程,有效的保护了操作系统。所以内核当中有一个内存指针指向用户层的代码和数据,而不是拷贝用户层的代码和数据在内核执行。
小总结:所以自定义处理一次信号需要4次的cpu上下文切换,而默认和忽略是两次。
为什么要在内核层才去信号处理?
用户层无法对进程的属性做出修改,需要更大的权限。
操作位图
sigset_t的理解
sigset_t是系统提供给用户的数据类型,未决和阻塞都可以用相同的数据类型来存储,他被称为信号集。而阻塞信号集也称作信号屏蔽字,屏蔽在这里要理解为阻塞而不是忽略。
由于不同的平台对sigset_t的定义不相同,虽然都是位图,但是定义的方法可以用数组,有整形变量,用结构体,所以我们要调用对应的接口来完成。
未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号
的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。
那么我们有没有对应的系统调用接口去操作内核当中的位图呢?
#include <signal.h>
//用来对用户层定义的sigset进行初始化,即每个位置置成0
int sigemptyset(sigset_t *set);
//和上面类似,只不过是位置是置成1
int sigfillset(sigset_t *set);
//对用户层的sigset添加signo信号
int sigaddset (sigset_t *set, int signo);
//对用户层的sigset删除signo信号
int sigdelset(sigset_t *set, int signo);
//对一个set结构,判断对应signo是否在,在返回1,不在0
int sigismember(const sigset_t *set, int signo);
系统调用
sigprocmask:对进程block信号屏蔽字操作函数。
将用户层定义的sigset结构与进程的同步,oset可以取出先前进程的信号集。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
how:有如下几种从左
sigpending:通过输出型参数set拿出未决信号集当中
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。
代码:屏蔽2号信号,获取pending表,再取消屏蔽2号信号,因为我们想发现进程退出的各种表结构,所以我们需要signal改变2号信号的方法,观察现象。sigset_t* newblock = nullptr;这样子建立newblock是不正确的
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
void PrintSet(sigset_t* set)
{
for(int i = 1;i<=31 ;++i)
{
//检测1~31号信号是否在
if(sigismember(set,i))
printf("1");
else printf("0");
}
printf("\n");
}
void handler(int signo)
{
printf("signo:%d\n",signo);
}
int main()
{
//将2号信号阻塞,然后获取他的pending信号集,然后再解除阻塞,打印pending信号集
signal(2,handler);
sigset_t* oldblock = (sigset_t*)malloc(sizeof(sigset_t));
sigset_t* newblock = (sigset_t*)malloc(sizeof(sigset_t));
//sigset_t* newblock = nullptr;
sigemptyset(oldblock);
sigemptyset(newblock);
sigaddset(oldblock,2);
sigprocmask(SIG_BLOCK,oldblock,newblock);
printf("block table is:\n");
PrintSet(oldblock);
sigset_t pending;
int count = 5;
//先检测打印01....
//5s后则00.....
printf("pending table is:\n");
while(count--)
{
if(count == 0)
{
//原先的信号集有newblock保存
sigprocmask(SIG_SETMASK,newblock,oldblock);
printf("pending is :\n");
}
sigpending(&pending);
PrintSet(&pending);
sleep(1);
}
printf("block is :\n");
PrintSet(newblock);
}
结果:
block table is:
0100000000000000000000000000000
pending table is:
0000000000000000000000000000000
0000000000000000000000000000000
^C0100000000000000000000000000000
0100000000000000000000000000000
signo:2
pending is :
0000000000000000000000000000000
block is :
0000000000000000000000000000000
SIGCHLD
之前进程等待要么阻塞等待,要么轮询等待,对于父进程来说都效率不高的表现,Linux下,子进程退出会给父进程发送SIGCHLD信号,父进程若不关心这个进程的退出状态,同时不希望出现僵尸进程,则可以signal把这个信号设置为忽略!
倘若同时有若干个SIGCHLD发给父进程,父进程由于处理时会将到来的信号放到pending信号集,而若pending信号集当中有信号,新来的信号就会被丢弃,这点是十分重要滴。
小测试,通过父进程更改信号处理动作实现。
等待的时候用WNOHANG,避免等待进程失败的时候被阻塞。
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
void handler(int signo)
{
printf("father recv:%d\n",signo);
//一次尽可能接受多的僵尸进程,并且设置第三个关键字,不然等最后一个不存在的进程的时候会阻塞住。
while(waitpid(-1,NULL,WNOHANG) >0)
;
}
int main()
{
pid_t id = fork();
signal(SIGCHLD,handler);
if(id == 0)
{
//child
//子进程就打印五次就退出
int count = 5;
while(count--)
{
printf("child running!\n");
sleep(1);
}
printf("child quit!\n");
exit(0);
}
//father 如何知道是否应该退出了?
while(1)
{
sleep(1);
}
return 0;
}
不等待进程
显示的设置signal将SIGCHLD设置为SIG_IGN,不会产生僵尸进程,但是只能在Linux下保证有效。,并且不会导致sleep函数停止,即父进程不会被中断。
结论:是否需要wait子进程取决于我们是否需要子进程的退出码,若不需要,我们要保证不产生僵尸进程,则可以用上述方法来解决。即从内存泄露的角度可以没必要等。但关心子进程的退出码必须等。
coredump的理解
当我们用ulimit -c 1024给core文件分配内存的时候,在我们test.c出现一些错误的时候core文件可以帮助我们定位到对应的出错位置。
Floating point exception (core dumped)
此时在当前文件夹会出现一个core.4301的文件。
gdb test后core-file core.4301就可以帮助我们定位到哪里出错。
9,19号信号无法被阻塞
通过下面代码验证,倘若运行后程序没有退出则表示9号信号可以被阻塞,反之。
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
void PrintSet(sigset_t* set)
{
for(int i = 1;i<=31 ;++i)
{
//检测1~31号信号是否在
if(sigismember(set,i))
printf("1");
else printf("0");
}
printf("\n");
}
void handler(int signo)
{
printf("signo:%d\n",signo);
}
int main()
{
sigset_t* set = (sigset_t*) malloc(sizeof(sigset_t));
sigset_t* tmpset = (sigset_t*) malloc(sizeof(sigset_t));
sigemptyset(set);
sigaddset(set,9);
printf("block table:\n");
PrintSet(set);
sigprocmask(SIG_BLOCK,set,tmpset);
//查看一下block信号集
printf("block table:\n");
PrintSet(set);
printf("pid is :%d\n",getpid());
//给自己发送9号信号
kill(getpid(),9);
//死循环
while(1)
{
sleep(1);
}
return 0;
}
结果:9号信号无法被阻塞,同理可以自己试一下19号信号
sleep
sleep是有返回值的,它可以被提前唤醒,信号来了时候,sleep就会暂停,返回值就是剩余的时间。
实时信号
信号及信号来源
sigqueue只能发送给一个进程,不能发送给一个进程组,并且用sigqueue发送非实时信号也不会加入队列当中。
SYNOPSIS
#include <signal.h>
int sigqueue(pid_t pid, int sig, const union sigval value);
31~64为普通信号,实时信号不丢失,实际内核当中有一个sigqueue来保存。
系统调用也是sigqueue,用链表来维护,他的处理方式可以理解为就是在sigqueue当中查找有多少个信号,若相同信号出现多次,则处理完后不修改pending信号集,直到全部执行完pending信号集的对应位置置0。
理解signal:
signal(2,handller)的时候,实际上就是找到当前进程,通过2号对应的编号找到2号的处理方法,将handler设置进去。
[ljh@VM-0-11-centos mytest]$ ./test
block table:
0000000010000000000000000000000
block table:
0000000010000000000000000000000
pid is :29543
Killed
信号的递达
进程从内核态返回内核态的时候,就会尝试对信号检测与捕捉执行。
内核态和用户态实际就是一个cpu寄存器的比特位不同而区分的。
访问用户空间就是用户态,访问内核空间就是内核态。
系统在调用系统调用会进行身份切换。
操作系统如何识别自己是用户态还是内核态?
cpu当中有状态寄存器(cr3),它的比特位设置为不同的值表示所处的状态,并且每个进程都有自己的用户级页表,但是内核级页表只有一份,只需要维护一份内核级页表,每个进程的上下文通过使用用户页表映射到不同进程的代码和数据,权限提升就可以访问到同一张内核页表,所以大家访问的内核数据都是一样的。系统还可以根据使用的页表的属性来判断是用户态还是内核态。
程序切换的时候,剥离进程时,实际就是用户态变成内核态,执行操作系统的代码,把自己的上下文保存,看到操作系统,就能执行操作系统代码,同时看到用户数据,把Cpu数据保存到用户空间上。进程的切换实际就是在进程的上下文完成的。
上下文:
上下文是程序正确执行运行所需要的状态组成的。这个状态包括存放在程序当中的代码和数据,它的栈,通用目的寄存器的内容,程序计数器,环境变量以及打开文件描述符的集合。
cpu上下文切换 vs 进程上下文切换
当从用户态到内核态之间的切换时,若不涉及进程切换,此时是CPU的上下文切换,即对应的cr寄存器的若干比特位的设置。
而进程切换肯定是需要进入内核态,由操作系统来进行进程切换的,所以这个过程会用到cpu上下文切换,并且还需要保存当前进程的变量,所以这里我认为进程上下文切换一定包含cpu上下文切换,而cpu上下文切换不一定会涉及进程上下文切换。
系统调用的上下文切换,由于可能不涉及进程切换,所以上下文指的是cpu寄存器的一些信息。
中断:scanf,cin,卡在输入,程序运行并且阻塞着,键盘的按键被按下,触发时被操作系统识别,对应的数据放在操作系统的缓冲当中,进程才能去读取,按键盘的时候,硬件方便的中断,通过芯片完成和8259cpu交互,传递到cpu的针脚,从外设拷贝到内核当中。
INT80陷入内核的编号,系统调用的底层原理。
系统调用的时候陷入内核,执行内核代码。
区分用户和内核态
用户态和内核态的权限级别不同,决定能看到的资源是不一样的,但是有个特例,内核不能执行用户的代码。
跟进程上下文不同,中断上下文切换并不涉及到进程的用户态。 所以,即便中断过程打断了一个正处在用户态的进程,也不需要保存和恢复这个进程的虚拟内存、全局变量等用户态资源。中断上下文,其实只包括内核态中断服务程序执行所必需的状态,包括 CPU 寄存器、内核堆栈、硬件中断参数等
系统调用过程通常称为特权模式切换,因为不需要进程切换。
大佬博客链接直达:
下面的这张图当中kernel对应的就是内核,而用户的则是用户空间,进程的字段可以指向用户空间的代码和数据,但是要执行用户层的代码和数据的时候,需要进行上下文的切换,此时设置CPU寄存器上的比特位即可。
这里也能很好的看出为什么让用户层指向代码和数据,因为执行期间也必须在用户层执行。
可重入函数
一个函数能否在被多个执行流调用并且不调用就是可重入的,否则就是不可重入得到。我们接触的大多都是不可重入函数。
如链表的插入逻辑:
由于一次信号的用户自定义方法,导致node2节点最终没能插入到链表结构当中(即遍历head找不到这个节点),这种现象。
fujin
在信号这里:单执行流也有可能会碰到函数的重入。
可重入函数的特征:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile易变关键字
保持内存的可见性。
man gcc我们可以看到 -O系列的调整优化级别。
#include<stdio.h>
#include<signal.h>
#include<unistd.h>
int flag = 0;
void handler(int signo)
{
flag = 1;
printf("set flag = 1\n");
}
int main()
{
signal(2,handler);
while(!flag)
{
}
printf("proc end\n");
return 0;
}
结果: 编译器优化级别高时,会将i定义为register变量,在while检测的时候发现上下文没有被更改,则直接将寄存器当中的0放入while,而信号处理设置为1是放到内存当中,所以会出现这种现象。
^Cset flag = 1
^Cset flag = 1
^Cset flag = 1
^Cset flag = 1
^Cset flag = 1
ps:这种问题通常比较隐藏,解决方法在i前加volatile即可。
即声明该变量是容易改变的,不要优化这个变量。观测结果循环直接退出!
总结
@_@