Linux系列
文章目录
- Linux系列
- 前言
- 一、进程对信号的捕捉
-
- [1.1 内核对信号的捕捉](#1.1 内核对信号的捕捉)
- [1.2 sigaction()函数](#1.2 sigaction()函数)
- [1.3 信号集的修改时机](#1.3 信号集的修改时机)
- 二、可重入函数
- 三、volatile关键字
- 四、SIGCHLD信号
前言
Linux系统中,信号捕捉是指进程可以通过设置信号处理函数来响应特定信号。通过信号捕捉机制,进程可以对异步事件做出及时响应,从而提高程序的健壮性和灵活性。
一、进程对信号的捕捉
图中内容及执行流程我已在Linux系列上上篇博客中介绍了,这里就不重复了。
1.1 内核对信号的捕捉
当信号的处理动作是用户自定义函数 ,在信号递达时 就调用这个函数,这称为捕捉信号 。由于信号处理函数的代码是在用户空间 的,处理过程比较复杂,举例如下:
1、用户程序注册 (对指定信号捕捉)了SIGQUIT
信号的处理函数sighandler
。
2、 当前正在执行main
函数,这时发生中断或异常切换到内核态。
3、 在中断处理完毕后要返回用户态的main函数之前 检查到有信号SIGQUIT
递达。
4、 内核决定返回用户态后 ,不是恢复main函数的上下文继续执行,而是执行sighandler
函数,sighandler
和main
函数使用不同的堆栈空间 ,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
5、 sighandler
函数返回后自动执行特殊的系统调用sigreturn
再次进入内核态。
6、 再次检测sigpending
位图,如果没有新的信号要递达,这次再返回用户态就是恢复main
函数的上下文继续执行了。
1.2 sigaction()函数
c
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
功能:捕捉指定信号,并读取和修改与指定捕捉信号相关联的处理动作。
参数
signum:指定捕捉信号的编号。
act: 输入型参数,act
若非空,则根据act
来修改信号的处理动作。
oldact:输出型参数,oldact
若非空,则获取信号原来的处理动作。
struct sigaction:系统为用户提供的结构体类型,帮助用户访问内核级结构体:
今天我们主要使用,上面两个成员对象。
- 信号处理方法,该方法需要一个整形变量,函数指针类型
act.sa_mask
所代表的是在信号处理函数执行期间需要阻塞的信号集合。也就是说,当 指定信号被捕获并且处理函数handler
开始执行时,sa_mask
里的信号会被阻塞,一直到处理函数执行完毕。
下面我们通过两个场景来认识他们:
例一
c
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signum)
{
cout<<"I catch a signal:"<<signum<<endl;
return;
}
int main()
{
struct sigaction act;
struct sigaction olact;
memset(&act,0,sizeof(act));//初始化内存空间
memset(&olact,0,sizeof(olact));
sigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法
while(true)
{
cout<<"I am process,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

可以看到这样我们,就完成了对二号进程的捕获并修改执行方法为自定义的行为。
例二
c
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;
void handler(int signum)
{
cout<<"I catch a signal:"<<signum<<endl;
sleep(10);//在执行handler方法期间,blocksig阻塞信号集中的信号被阻塞
return;
}
int main()
{
struct sigaction act;
struct sigaction olact;
memset(&act,0,sizeof(act));//初始化内存空间
memset(&olact,0,sizeof(olact));
sigset_t blocksig;
sigaddset(&blocksig,2);//将二信号添加进blocksig
act.sa_handler=handler;//将处理方法添加到act对象中
act.sa_mask=blocksig;//将想要阻塞的信号位图赋值给act
sigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法
while(true)
{
cout<<"I am process,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

从执行结构可以得到,当二号信号被捕获执行处理方法,到该方法执行结束,二号信号一直被阻塞,当解除阻塞后,二号信号再次递达。这里也可以使用SIG_IGN
(忽略信号)、SIG_DFL
(执行默认方法),来设定act.sa_handler
。测试时建议尝试其他信号,因为即使我们不手动的将二号信号添加到阻塞信号集,系统在执行二号信号时也会将它先阻塞,下面我们来详细探讨。
1.3 信号集的修改时机
当我们完成对指定信号的捕捉并执行对应处理方法时,操作系统会在执行该方法前,先将pending
位图中对应信号的标志位由1
置为0
,并将该信号添加到对应的阻塞信号集中。具体来说,在二号信号处理方法执行期间,即便进程再次收到二号信号,该信号也不会被递达。只有当上一个信号处理方法执行完毕并返回后,操作系统解除对二号信号的阻塞,新收到的二号信号才会被递达。
c
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;
void printsig()
{
sigset_t set;
sigemptyset(&set);
sigpending(&set);
for(int i=1;i<=31;i++)//依次检测信号集
{
if(sigismember(&set,i))cout<<1;
else cout<<0;
}
cout<<endl;
return ;
}
void handler(int signum)
{
int cnt=5;
while(cnt--)
{
printsig();
sleep(1);
}
cout<<"I catch a signal:"<<signum<<endl;
sleep(5);//在执行handler方法期间,blocksig阻塞信号集中的信号被阻塞
return;
}
int main()
{
struct sigaction act;
struct sigaction olact;
memset(&act,0,sizeof(act));//初始化内存空间
memset(&olact,0,sizeof(olact));
act.sa_handler=handler;//将处理方法添加到act对象中
sigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法
while(true)
{
cout<<"I am process,Pid:"<<getpid()<<endl;
sleep(1);
}
return 0;
}

从程序执行结果可以得出,当方法被执行时,操作系统会先将pending
信号集1--->0
,并将该信号阻塞,知道上次执行结束才会完成递达。
二、可重入函数
结合图中展示,分析函数调用链
在程序运行过程中,
main
函数调用insert
函数,打算向链表head
中插入节点node1
。insert
函数的插入操作分为两个步骤,当main
函数调用的insert
函数刚完成第一步时,硬件中断出现,进程被切换到内核态。在从内核态再次返回用户态之前,系统检测到有信号需要处理,于是进程转而执行sighandler
函数。在sighandler
函数中,同样调用了insert
函数,并且向同一个链表head
中插入节点node2
。sighandler
函数中的insert
操作顺利完成了两个步骤,之后从sighandler
函数返回内核态,接着再次回到用户态时,恢复上下文数据,程序从main
函数调用的insert
函数中断处继续执行,完成了剩余的第二步操作。原本main
函数和sig handler
函数先后尝试向链表中插入两个不同的节点,但最终链表中实际上仅成功插入了一个节点。
在上述执行流程中,insert函数被main和handler两条执行流重复调用,这一情况引发了结点丢失问题,并进而导致内存泄漏。像insert函数这种在被重复调用时可能出错或已经出错的函数,我们称之为不可重入函数;与之相对应的,则被称为可重入函数。
不可重入函数的特点:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
三、volatile关键字
接下来会通过这个关键字,拓展部分知识
例一
c
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;
int flag=0;
void handler(int signum)
{
cout<<"I captured a signal:"<<signum<<endl;
flag=1;
}
int main()
{
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_handler=handler;
sigaction(2,&act,nullptr);
while(!flag);
cout<<"process quit "<<endl;
return 0;
}
c
mytest:mytest.cc
g++ -o $@ $^ -std=c++11

相信这个执行结果大家都能理解,我就不对上面代码作解释了。
这里将flag设为全局变量,是因为main和sighandler是两个独立的执行流
例二
代码同上
c
mytest:mytest.cc
g++ -o $@ $^ -std=c++11 -O2

从程序执行结果可知,当将g++
编译器的优化级别设置为-O2
时,即便通过发送二号信号(SIGINT)将flag
变量修改为1
,循环仍无法终止。这一现象的根源在于:当使用-O2
这类高级优化级别编译代码时,编译器会对代码进行多维度优化以提升执行效率。针对while(!flag);
这一循环结构,编译器通过静态代码分析发现,循环体内部不存在对flag
变量的修改操作,因此推断该变量的值在循环过程中不会发生变化。
基于"内存访问速度相对较慢"这一特性,编译器为减少对内存的频繁访问,会将flag
变量的值从内存加载至CPU寄存器中缓存。此后,在循环条件判断时,CPU会直接从寄存器中读取flag
的值,而非重新从内存中获取最新数据,这就导致flag
内存不可见了。然而,信号处理机制对flag
变量的修改是直接作用于内存的,由于寄存器中的缓存值未及时刷新,导致循环条件判断始终基于寄存器中的旧值,最终造成循环无法终止的现象。
对于上面的结果我们可以,将 flag
声明为 volatile
类型,即 volatile int flag = 0;
。volatile
关键字的作用是保存flag
的内存可见性,告诉编译器,这个变量的值可能会被意外地改变,例如被硬件或者其他线程、信号处理函数等修改,因此编译器不能对其进行优化,这里就不展示了。
四、SIGCHLD信号
之前我们探讨过使用 wait
和 waitpid
函数来清理僵尸进程。在处理子进程结束的问题上,父进程有两种选择:一是进行阻塞等待,直至子进程结束;二是采用非阻塞的轮询方式,周期性地检查是否有子进程结束,以便及时清理。然而,这两种方式都存在明显的弊端。若采用阻塞等待的方式,父进程在等待期间会被阻塞,无法处理自身的任务,这会极大地降低父进程的工作效率。而采用轮询方式,虽然父进程可以在处理自身工作的同时检查子进程的状态,但这要求父进程时刻记得进行轮询操作,无疑增加了程序实现的复杂度,也容易出现疏漏。实际上,当子进程终止时,它会向父进程发送 SIGCHLD
信号。该信号的默认处理方式是被忽略,但我们可以对其进行优化。父进程可以自定义 SIGCHLD
信号的处理函数,这样一来,父进程就能够专注于自身的工作,无需时刻关注子进程的状态。当子进程终止时,会自动通知父进程,父进程只需在信号处理函数中调用 wait
函数,即可完成子进程的清理工作,既高效又便捷。 下面我们通过这样的方式实现一下:
c
#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;
void handler(int signum)
{
pid_t wid=waitpid(0,nullptr,WNOHANG);
if(wid)
cout<<"child quit success"<<endl;
return;
}
int main()
{
signal(SIGCHLD,handler);
pid_t id=fork();
if(id==0)
{
sleep(5);//模拟子进程工作
exit(0);
}
while(true)
{
cout<<"I am father process"<<endl;
sleep(1);
}
return 0;
}

从执行结果可以得出,子进程在退出时给父进程发送了SIGCHLD
信号。
当然还有一种防止僵尸进程的方法:父进程调 用sigaction
将SIGCHLD
的处理动作置为SIG_IGN
,这样fork
出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程:
c
int main()
{
struct sigaction act;
memset(&act,0,sizeof(act));
act.sa_handler=SIG_IGN;
sigaction(SIGCHLD,&act,nullptr);
pid_t id=fork();
if(id==0)
{
sleep(5);
exit(0);
}
while(true)
{
cout<<"I am father process"<<endl;
sleep(1);
}
return 0;
}
这个结果不方便展示,你自己尝试一下。
本篇就分享到这里了,如果文章的知识,或代码有错误请您联系我,不胜感激!!!