Linux进程信号
文章目录
信号入门
什么是linux信号?
- 信号是进程之间事件异步通知的一种方式,属于软中断。
我们输入命令,在Shell下启动一个进程迎来循环打印一个字符串。
int main(){while (1){printf("i am a process,i am waiting signal!\n");sleep(1);}return 0;}
我们可以使用kill -2 命令终止该进程。
信号处理的常见方式
- 执行该信号的默认处理动作。
- 捕捉信号,提供一个信号处理函数吗,让操作系统在处理该信号时切换到用户自定义的处理函数。
- 忽略该信号。
查看系统定义的信号列表
我们可以通过kill -l命令查看linux中定义的信号列表,其中,1 - 31号信号为普通信号,34 - 64号信号为实时信号。
当然,我们可以使用 man 7 signal 查看各个信号的默认处理行为。
[yzh@yzh test1]$ kill -l
产生信号
通过终端按键产生信号
当我们执行以下死循环程序,我们可以通过CTRL + C来终止该进程。
int main(){while (1){printf("i am a process,i am waiting signal!\n");sleep(1);}return 0;}
信号时如何被进程保存?
如果一个进程接受到该信号,那么该信号是保存在该进程的PCB(进程控制块)中的信号位图字段中。
其中,信号的位置代表普通信号的编号,比特位0 or 1 代表信号是否被保存。以上图示中代表进程保存了3号信号。
信号发送的本质?
当一个进程收到信号,本质上修改PCB(进程控制块)中指定的位图结构,进而完成发送信号的过程。
使用signal函数捕捉信号
sighandler_t signal(int signum, sighandler_t handler);
参数:
- signal:代表需要捕捉的信号
- handler: 操作系统捕捉信号后可能执行的自定义函数。
注意:
signal函数仅仅是修改进程特定信号的后续处理动作,不是直接调用对应的处理动作。
void catchSig( int signum ){ cout << "进程捕捉到了一个信号,正在处理中: " << signum << " pid: " << getpid() << endl;}int main(){ signal(SIGINT,catchSig); //捕捉2号信号 while( true ) { cout << "我是一个进程,我正在运行... Pid: " << getpid() << endl; sleep(1); } return 0;}
- 一般情况下,运行程序后,当我们使用使用键盘输入CTRL+C命令后,操作系统解释该组合键为2号信号,并默认查找进程列表,找到前台运行的进程,写入2号信号到PCB中对应的位图结构中,进而终止该进程。
- 然而,此时如果我们使用signal函数捕捉该2号信号,OS会直接去处理用户自定义的函数方法,导致无法处理该进程。
注意:
- Ctrl+C产生的信号只能发送给前台进程。在一个命令后面加个&就可以将其放到后台运行,这样Shell就不必等待进程结束就可以接收新的命令,启动新的进程。
- Shell可以同时运行一个前台进程和任意多个后台进程,但是只有前台进程才能接到像Ctrl+C这种控制键产生的信号。
- 前台进程在运行过程中,用户随时可能按下Ctrl+C而产生一个信号,也就是说该进程的用户空间代码执行到任何地方都可能收到SIGINT信号而终止,所以信号相对于进程的控制流程来说是异步的。
核心转储
在云服务器中,核心转储默认是关闭的,当时我们可以使用 ulimit -a 命令查看当前资源配置情况。
如果第一行中core文件的大小为0,代表该云服务器的核心转储是关闭的。
我们可以使用 ulimit - c 命令来设置核心转储文件的大小。
运行.signal可执行程序,使用 kill -8 PID 命令终止目标进程即可在当前路径下生成对应的core文件。
core dump标记位
我们知道大,对于waipid函数,status可以用于获取子进程的退出状态。其中status不能简单的以一个整形判断,status的比特位便代表着一些退出信息。
- 如果进程是正常终止的,那么status的次低8位就表示进程的退出状态,返回退出码。
- 如果进程被信号所杀,那么status的低7位表示终止信号,而第8位比特位是core dump标志,如果为该比特位为1,就代表进程终止时是否进行了核心转储。
以下代码中,当子进程出现野指针问题,OS便会想子进程发送SIGFPE信号终止子进程,并且在core dump标志设置为1,留下core dump文件记录相关进程信息。
int main(){if (fork() == 0){//子进程cout << " 子进程正在运行 " << endl;int *p = NULL;*p = 100;exit(0);}//父进程int status = 0;waitpid(-1, &status, 0);cout << " coreDump:%d ",(status >> 7) & 1 << endl;return 0;}
结果如下:
利用核心转储进行调试
当进程出现异常的时候,OS会将当前进程在内存中的相关核心数据,转存在磁盘当中,也就是core文件,所以,我们可以利用core文件进行调试。
void catchSig( int signum ){ cout << "进程捕捉到了一个信号,正在处理中: " << signum << endl;}int main(){ signal(SIGINT,catchSig); //回调函数。 while( true ) { cout << "我是一个进程,我正在运行... Pid: " << getpid() << endl; int a = 100;a /= 0; //除0错误 sleep(1); } return 0;}
首先,我们可以使用 gdb 可执行文件命令文件进行调试,然后再通过core file 命令加载core文件,既可以判断该进程在终止时收到了8号信号,并且定位到了程序产生错误时的一段代码。
注意:
- 进程异常终止通常是因为有Bug,比如非法内存访问导致段错误,事后可以用调试器检查core文件以查清错误原因,这叫做Post-mortem Debug(事后调试)。
如果普通信号全部被捕捉,如何终止进程?
我们可以通过以下代码,将1 ~ 31号信号全部捕捉,如果收到其中信号,OS按理来说会执行我们所自定义的函数。
void handler(int signal){printf("get a signal:%d pid:%d ", signal,getpid());printf("\n");}int main(){int sign;for (sign = 1; sign <= 31; sign++){signal(sign, handler);}while (1){sleep(1);}return 0;}
当我们使用一系列kill命令终止目标进程,发现OS接收到信号之后执行用户定义的自定义函数而无法终止进程。但是,如果使用 kill -9 命令,即时9号命令被捕捉,OS还是执行系统默认处理动作,终止目标进程。
所以有些信号无法被捕捉,例如Linux中的9号信号,因为如果所有信号都可以被捕捉的话,那么操作系统也将无法被终止。
调用系统函数向进程发送信号
在Shell中,实际上kill命令也是在调用了kill函数实现的。
kill函数可以给一个指定的进程发送指定的信号。
返回值: 调用成功返回0,失败返回-1。
int kill(pid_t pid, int sig);
所以,我们可以通过kill函数实现一个简单的kill命令。
static void Usage( char* proc){cout << "USage %s : " << proc << endl;}int main(int argc,char* argv[] ){if( argc != 3 ){Usage(argv[0]);exit(1);}pid_t pid = atoi(argv[1]);int signal = atoi(argv[2]);kill(pid,signal);return 0;}
结果如下:
模拟实现的”kill“命令将sleep进程成功终止。
raise
raise函数可以给当前进程发送指定的信号(自己给自己发送6号信号 )。
返回值: 成功返回0,失败返回-1。
int raise(int sig);
当我们使用signal将6号信号捕捉,调用abort函数时。
static void hander( int number ){cout << " get a signal " << number << endl;}int main(){ signal( 6,hander ); while( true ) {cout << "我开始运行了" << endl;sleep(1);abort(); // = raise(6) = kill(pid,6) } return 0; }
结果如下:
即使我们捕捉了6号信号,进程执行了我们自定义的函数,但是进程依旧被终止了。
注意:
abort的作用是使当前进程收到信号而异常终止,生成core dump文件,exit是让当前进程正常终止。
如何理解调用系统接口?
用户调用系统接口 --> OS执行OS对应的系统调用代码 --> OS提取参数,或者设置特定的数值 --> OS向目标进程写信号–> 修改对应进程PCB中位图的标记位–进程后续处理信号–> 执行对应的处理方法。
由软件条件产生信号
SIGPIPE
调用一个alarm函数可以设定一个闹钟,也就是告诉操作系统在seconds秒之后给当前进程发送SIGALRM信号,该信号的默认处理动作为终止当前进程。
unsigned int alarm(unsigned int seconds);
返回值:
- 如果调用alarm函数前,进程已经设置了闹钟,则返回上一个·设定闹钟时间还剩下的描述。
- 如果在调用alarm函数前,进程没有设置过闹钟,则返回0。
我们通过alarm闹钟设置1秒时count计算的总次数。
int main(){ alarm(1); int count = 0; while( true ) { cout <<" count: " << count++ << endl; } return 0; }
结果如下:
我们发现在通过vscode远程连接云服务器一秒钟计累加70163次。可是,实际上云服务器1秒钟累加次数远远大于该值,那么现在的结果远小于实际结果的原因是什么?
- 因为,Linux云服务器中,该进程每一次累加count,则每一次都需要加count打印在屏幕上。即1秒内,该进程不但对count多次累加,还执行了大量的IO操作,这耗费了大量的是将
- 并且,云服务器将count累加后还需要通过网络操作将数据传输过来,这也需要耗费时间。
怎么单纯计算云服务器计算能力?
我们在通过ALARM函数设定1秒闹钟后,通过signal函数捕捉SIGALRM信号,即在收到SIGALRM信号之后进程将执行catchSig即获取1秒内count的累加值。
int count = 0;void catchSig( int signum ){ cout << "final count " << count << endl; }int main(){ alarm(1); signal( SIGALRM,catchSig ); while( true ) { count++; } return 0; }
结果如下:
此时云服务器中count累加大概为5亿次,并且alarm闹钟在触发一次就被移除了。
那么,如果我们想周期性1秒内计算count累加值,如何处理?
我们可以在自定义函中又设置一次alarm闹钟,进程执行完该函数后,又累加count,1秒后便再次触发闹钟执行该自定义函数,周期性不断循环。
long long int count = 0;void catchSig( int signum ){ cout << "final count " << count << endl; alarm(1);}int main(){ alarm(1); signal( SIGALRM,catchSig ); while( true ) { count++; } return 0; }
结果如下:
//定时器待定。
如何理解软件条件给进程发送信号
- 就如闹钟一样,当超过闹钟所设定的时间,OS便会识别到ALARM闹钟触发,然后构建信号,获取该进程的PID,发送给目标进程,终止该进程。如果周期性设置闹钟,那么根据”先描述,后组织“,将ALARM相关属性写入一个结构体中,并将多个闹钟结构体由队列组织起来,知道队列中的某一个闹钟触发被OS识别。
硬件异常产生信号
当我们对SIGFPE(8号信号)进行捕捉,当遇到除零错误时,进程执行自定义函数。
void handler( int signum ){ sleep(1); std::cout << "获得了一个2号信号: " << signum << std::endl; // exit(0); //应该及时退出。} int main( ){ signal(SIGFPE,handler); int a = 100; a /= 0; while(1) sleep(1); return 0;}
结果如下:
可是,进程为什么会循环执行handler自定义函数呢?
我们先来理解除0错误。
- 在OS中,CPU用来分析计算的,在它的内部含有状态寄存器,其中有对应的状态标记位(类似于位图),状态标记为中还有溢出表记位,OS会自动计算结果完毕的检测。如果溢出标记位为1(说明硬件错误),OS便会识别到溢出问题。OS只要找到当前进程的task_struct提取PID,并将所识别的硬件错误包装成8号信号发送给目标进程,进程会在何时的时候被终止。
- 并且,一旦出现硬件异常,进程不一定会选择退出(一般为默认退出),但是我们即便不退出,也做不了什么。
综合以上结论,进程之所以循环执行自定义函数,因为寄存器中的异常一直没有被解决,当当前进程执行时切换到其他进程时,当前进程的上下文是保存在寄存器中的。当寄存器又重新切到当前进程,又识别到异常错误,操作系统就会不断重复得发送8号信号。所以一般有该异常并捕捉之后,一般需要及时退出。
如何理解野指针或者越界问题?
- 当我们要访问一个变量时,一般通过页表的映射关系将虚拟地址转换为物理地址后进行访问。
- 但是,实际上在转换过程中,本质上是通过 页表 + MMU(Memory Unit,硬件)实现的。需要进行虚拟地址到物理地址的映射时,我们先将页表的左侧的虚拟地址导给MMU,然后MMU会计算出对应的物理地址,进程再通过这个物理地址进行相应的访问。
- 所以,在 野指针,越界的情况下,MMU由虚拟地址转换到物理地址就会出现问题,此时OS转换成信号发送给对应进程,随后在合适的时候目标进程会被OS终止。
综合以上情况,无论是硬件问题还是软件问题擦还是产生信号,所有信号都有它的来源,但最终都是由OS所识别,解释,并发送的。
阻塞信号
阻塞信号相关常见概念
- 信号从产生到递达之间的状态成为信号未决。
- 进程实际执行信号的处理动作称为信号递达。
- 进程可以选择阻塞(Block)某个信号
- 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
- 阻塞和忽略是不同的,只要信号被阻塞就不会被递达,而忽略是阻塞的之后的一种可选动作。
信号在内核中的表示
- 每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作,其中,0表示无,1表示有。当信号产生时,内核在进程控制块中设置该信号的未决标志(0设置为1),直到信号递达才清除(1设置为0)。在上图的例子中,信号SIGHUP信号既未产生也未阻塞,所以当信号发生时,直接执行OS默认处理操作。
- SIGINT信号产生过,但是被屏蔽,所以暂时还不可以递达。虽然它的处理动作为忽略,但是在没有解除阻塞之前,不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。
- SIGQUIT信号未产生过,但是一旦产生SIGQUIT信号即将被阻塞。它的处理动作为是用户自定义函数sighandler。
sigset_t
- 从上图来看,每个信号只有一个bit的未决标志,非0即1,并不记录该信号产生了多少次,阻塞标志也是这样表示的。
- 因此,未决和阻塞标志可以使用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的”有效“或者”无效“状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。并且阻塞信号集也叫做当前进程的信号屏蔽字。
信号操作函数
sigset_t类型对于每种信号用于一个bit表示”有效“或者”无效“状态,至于在这个类型内部如何存储这些bit则依赖于系统的实现,从使用者的并不关心。使用者只能调用以下函数来操作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);
-
函数sigempty初始化set所指向的信号集,使其中所有的信号的对应bit位清零,表示该信号集不包含任何有效信号。
-
函数sigfilset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号的有效信号包括系统所支持的所有信号。
-
注意,在使用sigset_ t类型的变量之前,一定要调 用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
-
sigismember是一个布尔函数,用于判断一个信号集的有效符号是否包含某种有效信号,若包含则返回1,不包含则返回0,出错返回-1。这四个函数都是成功返回0,出错返回-1。
sigprocmask
调用函数sigprocmask可以读取或更改进程的信号屏蔽子(阻塞信号集)。
#include int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
- 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset函数传出。
- 如果set是非空指针,则更改进程的信号屏蔽字没参数how只是如何修改。
- 如果oset和set都是非空指针,则先将原来的信号 屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。
假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。
how | explantion |
---|---|
SIG_BLOCK | set包含了我们所希望添加到当前信号屏蔽字的信号,相当于mask = mask |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask = mask & ~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask =set |
sigpending
sigpending函数用于读取进程的未决信号集,并通过set参数传出。
该函数调用成功返回0,出错返回-1。
int sigpending(sigset_t *set);
实践代码:
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(){ //定义信号集对象;sigset_t bset,obset; sigset_t pending; //用于保存信号集 //2.初始化sigemptyset( &bset );sigemptyset( &obset );sigemptyset( &pending );//3.添加要屏蔽的信号----2号信号 sigaddset(&bset,2); //其实只是在栈上作了修改。//4.设置set到内核中对应的进程内。(默认情况下进程不会对任何信号进行block) int n = sigprocmask( SIG_BLOCK,&bset,&obset ); assert( n == 0 ); (void)n; cout << "block 2 号信号成功 ...get: pid "<< getpid() << endl; while( true ){//5.1获取当前进程的pending信号集。sigpending( &pending);//5.2显示pending信号集。showPending( pending );、sleep(1); // signal( 2,handler ); } return 0;}
我们运行该可执行程序,如果该进程没有收到2号信号,那么penging信号集2号比特位就为0,当我们使用kill命令向该进程发送2号信号时,由于该进程的2号信号被屏蔽,所以该进程便一直处于pending(未决)状态。
如果我们想看到2号信号递达之后pending表的变化情况,我们设置20秒之后,将自动解除该进程的2号信号屏蔽状态,此时2号信号便会立即递达。并且我们再对2号进程进行捕捉。当处理2号信号时,进程会转而执行用户所写的自定义函数。
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 handler( int signum ){ cout << "捕捉 信号: " << signum << endl;}int main(){ signal(2,handler);//定义信号集对象;sigset_t bset,obset; sigset_t pending; //2.初始化sigemptyset( &bset );sigemptyset( &obset );sigemptyset( &pending );//3.添加要屏蔽的信号 sigaddset(&bset,2); //其实只是在栈上作了修改。//4.设置set到内核中对应的进程内。(默认情况下进程不会对任何信号进行block) int n = sigprocmask( SIG_BLOCK,&bset,&obset ); assert( n == 0 ); (void)n; cout << "block 2 号信号成功 ...get: pid "<< getpid() << endl;//重复打印当前进程的pending信号集。int count = 0; while( true ){//5.1获取当前进程的pending信号集。sigpending( &pending);//5.2显示pending信号集中没有被递达的信号。showPending( pending );sleep(1);++count;if( count == 20 ){sigprocmask(SIG_SETMASK,&obset,nullptr); //恢复该进程的屏蔽字。cout << "开始解除对2号信号的屏蔽" << endl;}} return 0; }
我们可以看到,当进程收到2号信号后,一段时间内该进程处于阻塞状态,如果解除对2号信号的屏蔽,此时2号信号便会立刻递达,转而执行我们的自定义方法。pending表中对应的比特位也由1变成了0。
信号捕捉
进一步了解地址空间
每一个进程都含有进程地址空间,进程地址空间由用户地址空间和内核地址空间构成。
-
用户地址空间:用户缩写的代码和数据存储于用户空间,通过用户级页表与物理内存建立映射关系。
-
内核地址空间:操作系统的代码数据存储与OS内核空间,通过内核级页表与物理内存建立映射关系。
在每个进程的进程地址空间中,用户空间是属于当前进程的,每个进程看到的代码和数据是完全不同的,但内核空间所存放的都是操作系统的代码和数据,所有进程看到的都是相同的。
内核态和用户态
- 内核态: 内核态指的是OS执行操作系统代码和数据的状态,具备非常高的优先级。
- 用户态: 用户态指的是OS执行用户代码和数据的状态,是一个受管控的状态。
内核态和用户态是如何进行转换的?
用户态与内核态之间互相主要为3种情况:
- 调用系统函数
- 当前进程时间片到了导致进程切换
- 进程发生异常,中断等
用户凭什么执行OS的代码?
CPU的寄存器有2套,一套可见,一套不可见。
其中便有一套CR3寄存器 ---- 表示当前CPU的执行权限, 其中有一个位图结构,例如 1 表示内核态,3表示用户态。
例如:当我们调用open系统函数时,有一行汇编指令 int 80 将用户态改为内核态,此时,因为CPU中保存了改进程的内核地址空间和用户地址空间及其对应的页表地址,进程便能通过CPU找到内核地址空间和内核级页表来执行OS代码和数据。
内核如何实现信号的捕捉
内核如何捕捉信号?
当我们执行主控制流程大的时候,某条命令可能因为某些情况进入内核(转换为内核态),当内核处理完异常情况前准备返回用户态时,就必须查看pengding表。
如果OS查看 pending表对应比特位由0变1,再查看block位图对应的比特位,如果为0,则执行OS默认终止进程操作不用转换为用户态,如果为1,则直接忽略该信号动作,并且转换为用户,从主控制流程中上次被中断的地方继续向下执行。
如果待处理信号为自定义捕捉时,。信号处理完毕之后调用系统sigreturn再次转换为内核态,再将pending表中对应的比特位清除,最后再返回用户态从上次被中断的地方继续向下执行。
当捕捉信号动作为自定义函数时,内核态能不能直接处理用户地址空间的代码?
可以,因为内核态是一种权限非常高的状态,但是OS不能这样设计。
因为OS如果用户在用户地址空间中写了一些非法操作,比如 rm命令删除内核数据时,这是非常危险的,进程并不能确保用户所写的代码是否安全合法,所以基础南横不能以内核态的状态执行与用户的代码。
我们可以利用以下例图巧记:
该图形与直线有几个交点就说明有该进程有几次状态切换,箭头的方向代表进程状态的切换方向,两个椭圆的交点(红色)表示内核信号检测,也就是pending表的检测。
sigaction
sigaction函数可以读取并修改与指定信号相关联的处理动作。
返回值:
调用成功则返回0,出错则返回- 1。
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
参数:
signo:表示指定信号的编号
act: 如果act指针非空,那么根据act修改该信号的处理动作。
oact: 如果oact指针非空,那么根据oact传出该信号原来的处理动作。
其中,参数act和oldact都是结构体变量,结构体定义如下:
struct sigaction {void(*sa_handler)(int);void(*sa_sigaction)(int, siginfo_t *, void *); //不作解释sigset_t sa_mask;int sa_flags; //一般赋为0void(*sa_restorer)(void); //不做解释};
sa_handler
- 如果sa_handler = SIG_IGN,表示忽略信号
- 如果sa_handler = SIG_DFL,表示执行系统默认动作
- 如果sa_handler = 自定义函数指针, 表示使用自定义函数捕捉该信号。
注意:
用户所写的自定义函数,返回值必须为void,参数为int,我们可以通过参数来获取当前被捕捉信号的编号。
sa_mask
- 当某个信号的处理函数被调用,内核将自动将当前信号加入进程的信号屏蔽字中,当信号处理函数返回时便自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理函数调用结束为止。
- 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
例如:我们使用sigaction函数捕捉2号信号,并自动屏蔽3,4,5号信号,在进程处理自定义函数时打印信号集。
void showPending( sigset_t* pending ){ for( int sig = 1; sig <= 31; ++sig ) { if( sigismember( pending ,sig)) cout << " 1 "; else cout << " 0 "; } cout << endl;}void handler( int num ){ cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; cout << " 捕捉2号信号成功 pid:" << getpid() << endl; sigset_t pending; int c = 20; while( true ) { sigpending(&pending); showPending(&pending); c--; if( !c ) break; sleep(1); }}int main( ){ //内核数据,用户栈定义的。 struct sigaction act,oact; act.sa_flags = 0; sigemptyset(&act.sa_mask); act.sa_handler = handler; sigaddset(&act.sa_mask,3); sigaddset(&act.sa_mask,4); sigaddset(&act.sa_mask,5); sigaction(2,&act,&oact); while( true ) sleep(1); //设置进当前调用进程的pcb当中。 return 0;}
当进程捕捉2号信号时,自定义函数还有一段时间持续在返回。此时,我们通过kill命令向目标进程发送3,4,5号信号,此时便可以通过pending发现3,4,5号信号在该时间段内已经被屏蔽了。但是,一段时间后,因为自动恢复原来的信号屏蔽字,3,4,5信号之一开始递达终止进程。
可重入函数
如果我们在main函数中调用insert函数将结点1插入链表,但是insert函数共分为两步。当进程插入函数执行到第一步的时候(并没有执行完),
因为某些中断,异常到了等原因切换到内核态,再次返回时由检查pending表发现还有信号处理,所以进程执行sighandler函数。
当进程执行完sighandler函数,也是将node2结点头插到链表中。
但是,当进程执行完自定义函数并返回为用户态执行mian函数中insert调用异常时继续向下执行,则继续执行insert函数中的第一,二步执行完毕,完成头插。
但是,最终只有node1完成了头插,而node2因为结点地址丢失而找不到了进而无法处理,导致了内存泄露。
像这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称之为重入,insert访问一个全局链表,有可能因为重入而导致错乱,像这样的函数称为: 不可重入函数,反之的,如果一个函数只访问自己的局部变量或参数,则称为:可重入函数。
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标志I/O库函数,因为标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
volatile
volatile作用: 保持内存的可见性,告知编译器,该被关键字修饰的变量,不允许被优化,对该变量的任何操作,都必须在真实的内存中进行操作。
以下代码中,我们在标准情况下,将2号信号进行捕捉后执行handler函数时让全局变量flag由0变1,当执行完毕后继续执行main函数中的代码此时应该跳出循环并打印flag值。
#include #include int flag = 0;void handler(int signo){cout << "change flag: " << flag; flag = 1; cout << " -> " << flag << endl;}int main(){signal(2, handler);while (!flag);cout << " 进程正常退出后 " << flag << endl; return 0;}
结果如下:
但是在gcc中,也有对应的优化机制,当我们使用最高优化级别”O3“进行编译,并运行该可执行程序时.
此时,尽管在handler函数中全局变量flag由0变成了1,但是在主函数中while循环依旧符合条件。
因为,在gcc优化之前,每一次访问全局变量flag都会从物理内存读取flag到寄存器中检测。可是编译器优化过后,编译器认为main函数中的全局变量flag并没有被修改。所以编译器在第一次加载flag时,便读取到寄存器edx中,而在handler修改flag时只是在内存中修改,之后,进程执行到mian函数中的flag那一行代码,便会从寄存器中检测。
所以,对全局变量以volatile关键字修饰,让编译器对其保持对内存的可见性。
#include #include volatile int flag = 0;void handler(int signo){cout << "change flag: " << flag; flag = 1; cout << " -> " << flag << endl;}int main(){signal(2, handler);while (!flag);cout << " 进程正常退出后 " << flag << endl;return 0;}
注意:
OS不关心执行代码,编译器优化在编译期间便把代码优化编译了,CPU按照编译后的代码执行。
SIGCHLD信号
用户为了避免僵尸进程,父进程需要使用wait和waitpid来处理僵尸进程,但是有两种处理方式:
- 父进程以阻塞的方式等待子进程结束
- 父进程在处理工作的同时还必须轮询一下是否有子进程退出,程序实现复杂。
父进程fork出子进程,子进程调用exit(0)终止,父进程自定 义SIGCHLD信号的处理函数,在其中调用waitpid以非阻塞的方式获得子进程的退出状态并打印。
void handler( int signum ){ pid_t id = 0; cout << ": 子进程退出" << signum << " father " << getpid() << endl; while( id = waitpid(-1,NULL,WNOHANG) > 0 ) { cout << "wait child success " << endl; }}int main(){ signal( SIGCHLD,handler ); if( fork() == 0 ) { cout << "child pid: " << getpid() << endl; sleep(1); exit(0); } while( true ) { sleep(3); cout << " 正在执行父进程 " << endl; }}
结果如下:
由于OS并不知道有多少个子进程退出,所以我们需要采用while循环调用waitpid函数以非阻塞方式清理僵尸进程,如果以阻塞方式等待,那么父进程再下一轮查询子进程状态时该子进程恰好不退出的话,那么父进程就会阻塞等待,无法继续执行下面的代码。
如果我们不想等待子进程,并且还想让子进程退出之后自动释放僵尸进程,我们可以通过signal函数默认对子进程忽略。
int main(){signal(SIGCHLD,SIG_IGN); if( fork() == 0 ) { cout << "child pid: " << getpid() << endl; sleep(5); exit(0); } while( true) { cout << "parent 正在运行 "<< endl; sleep(1); }}
这种忽略的含义就是用户告诉OS退出并默认清理僵尸进程。
来源地址:https://blog.csdn.net/m0_63300413/article/details/132453677
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341