文章目录
进程信号
信号产生
在Linux系统中信号分为2类:普通信号和实时信号。可以使用kill -l
命令查看系统中所有的信号。
其中1-31号信号为普通信号,34-64号信号为实时信号,没有32和33号信号。
实时信号(了解)
常用的信号是普通信号,实时信号主要应用于实时操作系统,Linux操作系统是支持实时的,但一般采用分时。
分时操作系统:CPU基于时间片轮转进行进程调度的OS
实时操作系统:不基于时间片轮转,用于特殊场合,例如军工。
信号的特点
- 信号的产生相对于进程来说是异步的,进程调度与信号产生没有必然联系
- 进程接收到信号可能不是立即处理,对于信号的处理可能具有延后性
通过终端按键产生信号
通过键盘上的组合键可以产生信号,键盘是通过硬件中断的方式进行工作的,键盘上的组合键可以被OS识别并解释。若OS识别到用户按下了ctrl + c,会将其解释为信号,写给正在运行的目标前台进程。
通过终端按键只能够向正在运行的前台进程发送信号,无法给后台进程发送信号。
系统调用产生信号
kill
系统调用函数可以向指定进程发送指定的信号,函数调用成功返回0,失败返回-1.
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int signum);
demo:使用kill函数模拟实现kill命令
int main(int argc,char* argv[]){
if(argc!=3){
cout<<"命令行参数有误"<<endl;
abort();
}
kill(atoi(argv[2]),atoi(argv[1]));
return 0;
}
raise
系统调用函数可以让进程向自己发送指定信号,相当于kill(getpid(),int signum)
.raise函数调用成功返回0,失败返回-1.
#include<signal.h>
int raise(int signum)
int main(){
raise(SIGQUIT);//进程向自身发送3号信号SIGQUIT,导致进程core dump
return 0;
}
abort
系统调用函数可以让进程向自身发送SIGABRT信号,进程对于SIGABRT信号的默认处理方式是Core
#include<stdlib.h>
void abort();
abort函数没有返回值,调用总是成功。
由软件条件产生信号
进程在运行过程中不符合某种软件条件时OS会向进程发送信号,例如管道的读端关闭,写端依旧在向管道中写入内容,当写端将管道写满之后,OS会向写端进程发送SIGPIPE信号(13),进程对于SIGPIPE信号的默认处理方式是Term(终止进程)
int main(){
int pipefd[2]={0};
pipe(pipefd);
if(!fork()){
close(pipefd[0]);
while(1){
write(pipefd[1],"123456",6);
}
exit(0);
}
close(pipefd[0]);
close(pipefd[1]);
int status;
wait(&status);
cout<<"子进程的退出信号为:"<<(status&0x7f)<<endl;//13号,SIGPIPE
cout<<"子进程的core dump标记位为:"<<((status>>7)&1)<<endl;
return 0;
}
设置系统闹钟产生信号也属于软件条件产生信号,可以使用系统调用alarm
设置一个闹钟,让内核在若干秒之后向进程发送SIGALRM(14)信号(进程对于SIGALRM信号的默认处理方式是Term)
#include<unistd.h>
unsigned int alarm(unsigned int seconds);
demo:使用alarm函数测试当前机器的算力
int g_val=0;
int main(){
auto f=[](int signum){
cout<<"收到"<<signum<<"号信号"<<endl;
cout<<g_val<<endl;
exit(0);
};
sighandler_t catchf=f;
signal(SIGALRM,catchf);
alarm(1);
while(1){
g_val++;
}
return 0;
}
alarm设定闹钟的特点:alarm设定一个闹钟,在若干秒后,OS会向进程发送SIGALRM(14)号信号,闹钟生效,闹钟一旦触发,会立刻自动解除。
demo:利用alarm的特点实现定时器功能
u_int64_t g_val=0;
int cnt=10;
int main(){
auto f=[](int signum){
cout<<"收到"<<signum<<"号信号"<<endl;
cout<<g_val<<endl;
if(cnt--==0){
exit(0);
}
alarm(1);
};
sighandler_t catchf=f;
signal(SIGALRM,catchf);
alarm(1);
while(1){
g_val++;
}
return 0;
}
软件条件产生信号的原理
由软件条件产生信号在系统层面上的理解:OS识别到某种软件条件被触发(例如alarm设置的闹钟到期)或不满足某种软件条件(例如管道失去了读端)之后,OS会构建信号,写入到指定进程。
OS是进程的管理者,OS有能力识别进程是否触发某些软件条件或不满足某些软件条件,进而给进程发送信号。
SIGPIPE(13)信号的产生原理:
- 进程之间通过管道通信,先描述,在组织,在内核中一定存在管理管道的内核数据结构,这个数据结构中记录了管道对应哪些读端进程和写端进程,记录了读端进程与写端进程的pid。
- 当管道的读端进程关闭时,OS检测到该数据结构管理的管道失去了读端进程,便会向写端进程发送SIGPIPE(13)信号。这个检测工作是由内核完成的,用户不必关心其具体过程。
alarm设置定时闹钟产生信号的原理:
- 通过alarm设置的闹钟本质上也是一种内核数据结构,在同一时刻,OS中可能会有很多进程通过alarm设置了闹钟,OS需要使用特定的数据结构将这些闹钟组织起来(例如链表),并定期检测内核中的过期闹钟,向创建该闹钟的进程发送SIGALRM(14)信号。
- OS检测系统的过期闹钟是由内核完成的,用户也不必关系其具体细节。
硬件异常产生信号
demo:除0错误和越界野指针问题
int a=10;
a/=0;//除0错误,SIGFPE(8),默认处理方式core dump
int* p=reinterpret_cast<int*>(0x00112233);
*p=10;//野指针,段错误,SIGSEGV(11),默认处理方式core dump
int arr[5]={1,2,3,4,5};
arr[10]=10;//数组越界写,段错误,SIGSEGV(11)
若捕捉因为硬件异常产生的信号,可能会导致死循环。
int main(){
auto f=[](int signum){
cout<<"捕捉到"<<signum<<"号信号"<<endl;
sleep(1);
};
sighandler_t catchf=f;
signal(8,catchf);
int a=10;
a/=0;//除0错误,SIGFPE(8),默认处理方式core dump
return 0;
}
对于除0错误导致硬件异常产生信号的理解:
- a/=0指令的计算过程是由CPU执行的,CPU中有一套完整的寄存器,其中有一个寄存器称为状态寄存器,状态寄存器中存在状态标记位和溢出标记位,用来记录此次CPU的计算是否发生了除0和溢出。若发生了,那么状态标记位和溢出标记位会被设置,这个操作是由CPU硬件完成的,与OS无关。
- 在CPU每一次计算完毕之后,OS会对CPU状态寄存器中的状态标记位和溢出标记位做检测,若检测到状态标记位或溢出标记位被设置,那么OS认为CPU在计算进程的某些数据时发生异常,此时OS会找到当前正在被CPU调度的进程,向其发送SIGFPE(8)信号。如果用户没有自定义捕捉,那么进程默认会core dump。
- 在CPU寄存器中的数据属于进程的上下文数据,CPU状态寄存器中的状态标记位和溢出标记位数据也不例外。在分时操作系统中,每一次进程被CPU调度,都要恢复自己的上下文数据,由于CPU状态寄存器中的异常数据一直没有被解决,每一次CPU在调度该进程时,OS对CPU做检测,得到的状态标记位和溢出标记位永远都是1,OS便会向进程继续发送SIGFPE(8)信号。
越界野指针问题导致硬件异常产生信号的理解:
- 野指针的解引用和数组的越界访问,都是通过进程地址空间的虚拟地址映射到物理地址来完成的。
- 将进程地址空间的虚拟地址转化为物理地址需要借助页表(内核数据结构)和MMU(内存管理单元)完成,MMU属于硬件,在MMU中也有寄存器,当用户代码出现野指针或越界访问时,MMU在进行虚拟地址到物理地址的转化会报错,导致MMU寄存器中的某些数据出现异常,当OS检测到MMU中的数据出现异常时(检测工作由内核完成,用户无需关心具体细节),会向进程发送SIGSEGV(11)信号。
硬件异常产生信号的理解
硬件异常产生信号的本质是硬件中的数据出现异常。因为发生某些错误导致硬件中的一些数据出现异常被OS识别到,然后OS向进程发送特定的信号。
若进程不终止,那么由硬件异常产生的信号问题就是长期性的,只要硬件中的异常数据没有被解决,信号问题就一直存在。一般而言,出现了硬件异常,OS向进程写入相应的信号后,进程会退出,若用户自定义捕捉了信号,进程没有退出,那么硬件异常导致的信号问题一直存在。
信号产生的本质
不论是通过终端按键产生信号、软件条件产生信号、系统调用产生信号,还是硬件异常产生信号,本质上都是通过特定的方式驱使内核向进程写入信号。
内核向进程发送信号,实际上是更改了进程PCB(task_struct)中的未决信号集(pending),将对应的比特位由0置为1,当信号递达之后,pending中的对应比特位在由1置为0,因此,向进程发送信号也可称为向进程写入信号。
信号保存
递达、未决、阻塞
递达(Delivery):信号递达指的是进程收到信号,并且处理(忽略、捕捉、默认处理)了信号
未决(pending):信号未决指的是进程收到了信号,但是暂时没有时间没有处理,信号在没有递达之前处于未决状态
阻塞(block):被阻塞的信号会一直处于未决状态,不会被递达,除非阻塞被解除
信号被阻塞与信号被忽略:
- 信号被阻塞指的是信号没有递达,一直处于未决状态
- 信号被忽略表示信号已经递达了,并且进程对信号进行了处理,处理方式是忽略
保存信号的内核数据结构
在进程的task_struct内部含有与信号保存相关的3个内核数据结构,分别是阻塞信号集(又称信号屏蔽字,block)、未决信号集(pending),handler。其中信号屏蔽字用来记录那些信号被设置为阻塞,未决信号集用来表示进程当前有哪些信号处于未决状态,未决信号集(pending)和阻塞信号集(block)都是内核中的位图结构,采用比特位来保存信号信息。
task_struct中的handler用来记录进程对于信号的处理方式。
获取pending与设置block
OS提供了系统调用可以让用户设置进程的信号屏蔽字、获取进程的未决信号集。
sigpending
#include<signal.h>
int sigpending(sigset_t* set);
用户可以通过sigpending获取进程的未决信号集,sigpending函数的参数set属于输出型参数,内核会将进程pending内容写入到用户传入的set,供用户读取。
sigpending函数调用成功返回0,失败返回-1,sigpending函数的参数类型sigset_t是OS提供的一种数据类型,称为信号集,用户可以通过该类型,使用特定接口得到进程的block和pending信息。sigset_t使用位图结构,通过位操作可以设置进程的阻塞信号集,得到进程的未决信号集。
OS提供了修改sigset_t类型对象的接口,用户不应该直接对信号集进行操作(而非不能),应当使用系统调用接口进行操作,操作信号集常用的接口:
#include<unistd.h>
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);//检测信号集中是否存在特定信号
demo:
int main(){
auto&& ShowSigset=[](const sigset_t& s){
//打印信号集
for(int i=31;i>=1;i--){
if(sigismember(&s,i)){
cout<<'1';
}else{
cout<<'0';
}
}
cout<<endl;
};
sigset_t s;
ShowSigset(s);//打印没有初始化的信号集
sigemptyset(&s);
ShowSigset(s);//打印被清空的信号集
sigfillset(&s);
ShowSigset(s);//打印添加了所有信号的信号集
for(int i=10;i<=20;i++){
sigdelset(&s,i);//去除信号集中的10~20号信号
}
ShowSigset(s);
for(int i=10;i<=15;i++){
sigaddset(&s,i);//向信号集中添加10~15号信号
}
ShowSigset(s);
return 0;
}
/*
0000000000000000000000000000000
0000000000000000000000000000000
1111111111111111111111111111111
1111111111100000000000111111111
1111111111100000111111111111111
*/
sigprocmask
#include<signal.h>
int sigprocmask(int how,const sigset_t* set,sigset* oldset);
sigprocmask可以修改进程的信号屏蔽字,同时可以获取进程原来的信号屏蔽字。
参数:
- how,how参数选择调用sigprocmask函数的哪种功能,对应的选项有3个。
- SIG_BLOCK,向进程的阻塞信号集中添加set信号集所包含的信号
- SIG_UNBLOCK,将set信号集中包含的信号从进程的阻塞信号信号集移除
- SIG_SETMASK,设置进程阻塞信号集的内容为set信号集
- set,set参数用于修改阻塞信号集
- oldset,oldset参数可以得到进程原来的阻塞信号集
demo:将所有信号加入阻塞信号集,是否所有信号都不会被递达?
int main(){
sigset_t s;
sigfillset(&s);
sigprocmask(SIG_BLOCK,&s,nullptr);
for(int i=1;i<=31;i++){
raise(i);
printf("%d号信号成功被阻塞\n",i);
}
sleep(10);
exit(0);
}
/*
1号信号成功被阻塞
2号信号成功被阻塞
3号信号成功被阻塞
4号信号成功被阻塞
5号信号成功被阻塞
6号信号成功被阻塞
7号信号成功被阻塞
8号信号成功被阻塞
Killed
*/
OS为了防止恶意进程采用阻塞信号的方式一直占用系统资源,设置了某些信号不能被屏蔽,也不能自定义捕捉,例如SIGKILL(9)和SIGSTOP(19)。其中SIGKILL(9)属于管理员信号,是终止恶意进程的最后一道屏障,因为OS将其设计为不能被屏蔽,不能被捕捉;SIGSTOP(19)是暂停进程的信号,也无法屏蔽,无法捕捉。
对于block,OS提供了获取block的接口(通过sigprocmask的oldset参数可获取)和修改block的接口,对于pending,OS只提供了获取pending的接口,没有提供设置pending的接口,产生信号应当是通过某种手段驱使内核向进程写信号,而非直接修改进程的pending位图。
信号处理
信号的常见处理方式
忽略
进程在收到信号以后什么都不做,内核直接将pending对应的位由1置0.
执行默认处理方式
OS的设计者在设计进程的时候,给每一个进程设定了对于信号默认的处理方式,这个属性是进程天然就有的,可以通过man 7 signal
进行查看,例如对于SIGHUP(1)信号,默认处理方式是Term(终止进程),对于SIGABRT(6)信号,默认处理方式是Core,Core也是终止进程,与Term的区别在于以Core的方式终止进程,可能会产生core文件,发生核心转储。
一般而言,在服务器上,默认核心转储功能处于关闭状态;在虚拟机中,核心转储功能处于开启状态,可以使用ulimit -a
查看相关信息,若核心转储功能处于关闭状态,可以使用命令ulimit -c 4096
开启核心转储功能,4096是指定生成core文件的大小为4096Bytes。
demo:创建一个子进程,让其以Core的方式终止,通过wait/waitpid拿到子进程的core dump标志。
int main(){
if(fork()==0){
abort();//子进程向自己发送SIGABRT(6)信号,默认处理方式是Core
}
int status;
wait(&status);
cout<<"子进程的退出信号为:"<<(status&0x7f)<<endl;
cout<<"子进程的core dump标记是否被设置:"<<(((status>>7)&1)?"是":"否")<<endl;
cout<<"子进程的退出码为:"<<((status>>8)&0xff)<<endl;
return 0;
}
当子进程以Core的方式终止,若系统的核心转储功能处于开启状态,那么OS会将子进程在内存中的核心数据转存到磁盘,生成一个core文件,文件后缀为子进程的pid,core文件可以gdb调试,帮助定位子进程产生信号的位置,可以在gdb中使用命令core-file core.???
进行调试。
在一般的生成环境中系统的core dump选项默认处于关闭状态,主要是为了避免进程因为Core退出产生的core文件占用磁盘空间。
进程对于信号的默认处理方式除了Term和Core之外,还有Lgn(对于SIGCHLD(17)信号,父进程的默认处理方式是忽略)、Stop(对于SIGSTOP(19)信号,进程的默认处理方式是暂停当前进程)、Cont(对于SIGCONT(18)信号,进程的默认处理方式是继续执行)。
捕捉信号
使用系统调用signal
可以实现对信号的捕捉,用户可以通过signal注册一个函数,用来处理特定的信号,这样进程在收到该信号时,会由内核态切换到用户态,调用注册的函数。
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
signal函数调用成功会返回之前的信号处理函数,若之前对于信号采取的方式是默认处理,那么signal函数返回SIG_DFL(#define SIG_DFL ((sighandler_t)0)
);若signal函数调用失败,返回SIG_ERR(#define SIG_ERR ((sighandler_t)-1)
)。
demo:是否所有的信号都能被捕捉?
int main(){
auto f=[](int signum){
cout<<"进程收到了"<<signum<<"号信号"<<endl;
};
sighandler_t catchf=f;
for(int i=1;i<=31;i++){
signal(i,catchf);//将所有信号都捕捉
}
for(int i=1;i<=31;i++){
raise(i);//向自己发送信号
}
return 0;
}
SIGKILL(9)和SIGSTOP(19)信号不能被捕捉,SIGKILL(9)和SIGSTOP(19)既不能被捕捉,也不能被设置为阻塞。
信号的处理时机
进程在由内核态返回到用户态时会对信号进行处理。原因:进程对信号进行处理的本质是将进程的未决信号集对应的比特位由1置为0,然后调用handler表中相应的处理方法,进程的未决信号集和handler表都是属于内核数据结构,必须在进程处于内核态时才能进行修改和访问,OS的设计者没有选择在进程刚进入内核时对信号进行处理,而是选择了在进程由内核态返回用户态的时候进行处理。
内核态与用户态
用户态是一个受管控的状态,权限相对有限,内核态时OS执行自己代码的一个状态,内核态具备非常高的优先级和权限。
进程进入内核态的方式:
- 进行系统调用
- 进程因为缺陷、陷阱、异常而进入内核态
- 进程被调度或者切换,进程被调度或者切换是通过OS执行进程切换的代码实现的(例如OS执行switch process函数进行进程切换),本质上还是在进行系统调用。在进行进程切换的时候,需要执行OS的进程切换代码将进程在寄存器中的上下文数据保存在PCB中;在进程重新被调度时,需要执行OS与进程调度相关的代码,将进程PCB中的上下文数据恢复到寄存器中。
从根本上来说,进程想要由用户态进入内核态,都是通过系统调用实现的,OS为了系统安全,需要确保用户想要进入内核只能是通过系统调用,不能通过其它手段。系统调用函数中内嵌了int 80汇编指令(80号中断),int 80汇编指令可以让用户陷入内核,将代码的执行权限交给OS。
进程地址空间的内核空间
在进程地址空间中,[0GB,3GB]是用户空间,[3GB,4GB]是内核空间,由用户态进入内核态,就是从进程地址空间的用户空间切换到内核空间,执行OS的代码,在由用户空间切换到内核空间时,需要更改CPU寄存器中的数据。
一般CPU的寄存器有2套,一套是可见寄存器,给用户用的,一套是不可见寄存器,CPU自己用的,在不可见寄存器中,存在一个CR3寄存器,CR3寄存器中的数据用来表示当前CPU的执行权限是内核态还是用户态,当进程由用户空间切换到内核空间想要执行OS的代码时,OS会修改CPU的执行权限为内核态,这样才能执行OS的代码。若用户通过非法手段想要进入内核空间,OS是不会修改CPU的执行权限的,CPU的执行权限依然是用户态,无法执行OS的代码;只有用户是通过系统调用的方式进入内核空间,OS才会修改CPU的执行权限。
进程从用户态进入内核态:从进程地址空间的用户空间切换到内核空间,OS修改CPU的执行权限为内核态,进程调用OS的代码
进程从内核态切回用户态:进程执行OS的代码完毕,从内核空间返回用户空间,OS修改CPU的执行权限为用户态,进程在用户空间继续执行用户的后序代码
每一个进程都有用户地址空间和内核地址空间,进程的用户地址空间映射到物理内存的不同区域,内核地址空间映射到物理内存的同一区域,因为OS只有一份。
判断进程处于用户态还是内核态的依据是CPU的执行权限是内核态还是用户态,进程处于内核态还是用户态是与硬件相关的。
OS存在于每一个进程的进程地址空间上下文中,每一个进程通过自己[3GB,4GB]的内核空间+内核级页表(内核级页表只有一份,所有进程共享)就可以访问OS的代码和数据。实际上进程进行系统调用和使用动态库的代码没有本质区别,只不过CPU的执行权限不一样,使用的页表不一样。
捕捉信号
信号捕捉,并没有创建新的进程或线程。
demo:在信号捕捉函数中打印线程id和进程的pid
int main()
{
cout << "进程的pid为" << getpid() << endl;
cout << "线程id为" << pthread_self() << endl;
auto &&f = [](int signum) -> void
{
cout << "收到" << signum << "号信号" << endl;
cout << "进程的pid为" << getpid() << endl;
cout << "线程id为" << pthread_self() << endl;
};
struct sigaction act;
act.sa_handler = f;
sigaction(SIGINT, &act, nullptr);
sleep(5);
return 0;
}
/*
进程的pid为3046
线程id为140573100148544
^C收到2号信号
进程的pid为3046
线程id为140573100148544
*/
捕捉信号的流程
时钟中断的概念:时钟中断属于硬件层面上的概念,CPU每隔一段时间会向OS发送时钟中断,告诉OS进程的时间片到了,OS要执行进程调度的代码,调度下一个进程。
内核态与用户态的切换是通过软硬件结合的方式完成的,在硬件上CPU有寄存器,在软件上,进程有进程地址空间,软硬件结合完成内核态到用户态的切换。
信号的处理流程:
- 用户进程因为系统调用或者CPU调度的原因进入内核
- OS的调度代码执行完毕或系统调用代码执行完毕,从内核态返回到用户态之前,进行信号检测
- 若检测到未决信号集中有信号,会调用handler表中的信号处理方法
- 若handler表中的信号处理方法为忽略,则将未决信号集对应的位置由1置为0,然后直接返回到用户态,执行用户代码
- 若handler表中的信号处理方法为默认,则将未决信号集对应的位置由1置为0,执行OS的默认处理方法(一般是终止进程)
- 若handler表中的处理方式是用户自定义捕捉,那么进程会先从内核态返回到用户态,执行用户的捕捉函数(注意OS可以在内核态调用用户的捕捉函数,但是OS不会在内核态调用用户的捕捉函数,因为内核态的优先级和权限过高,若用户的捕捉函数中存在非法操作,会影响整个OS的安全性),在用户态将捕捉函数执行完毕以后,进程会自动调用sigreturn系统函数再次进入内核,修改进程的pending信号集,将对应比特位由1置为0。然后调用sys_sigreturn函数返回到用户,继续执行用户代码
信号检测的流程可以用下图表示:
- 进程因为系统调用或者CPU执行进程的调度代码,进程进入内核态
- 进程从内核态返回用户态时对信号做检测,需要执行用户自定义的回调函数,并且将pending位图对应的比特位置为0
- 进程从内核返回用户,执行信号的回调函数
- 执行回调函数完毕,再次进入内核
- 准备从内核返回用户
- 从内核返回用户完成,继续执行用户代码。第5步和第6步之间没有信号检测的过程,是直接返回到用户的过程
整个过程一共有2次进入内核,2次从内核回到用户。
sigaction函数
进行信号捕捉可以使用signal函数
#include<signal.h>
typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
若使用signal函数对SIGINT(2)信号进行了捕捉,当进程收到2号信号时,会调用自定义的捕捉函数,在调用捕捉函数期间,进程会自动阻塞2号信号。
demo:进程正在处理SIGINT(2)信号时,再次收到SIGINT(2)信号
int main()
{
auto catchfunc = [](int signum) -> void
{
cout << "进程收到" << signum << "号信号" << endl;
sigset_t sigset;
sigemptyset(&sigset);
int cnt = 5;
while (cnt--)
{
sigpending(&sigset);
for (int i = 31; i >= 1; i--)
{
cout << (sigismember(&sigset, i) ? '1' : '0');
}
cout << endl;
raise(SIGINT); // 进程再次向自己发送2号信号,此时2号信号被屏蔽
sleep(1);
}
};
signal(SIGINT, catchfunc);
sleep(5);
return 0;
}
进程在由内核态返回到用户态,会对信号做检测,从内核态切换到用户态执行回调函数之前,会先将未决信号集的对应比特位置为0。
进程若正在某一个信号时,该信号会被自动设置为阻塞。
#include<signal.h>
int sigaction(int signum,const struct sigaction* act/*输入型参数*/,struct sigaction* oldact/*输出型参数*/);
struct sigaction{
void(*sa_handler)(int);//信号捕捉函数
sigset_t sa_mask;
//...............
};
sigset_t sa_mask
:在调用信号处理函数时,进程会自动屏蔽当前信号,如果还希望进程自动屏蔽一些别的信号,那么需要使用sa_mask字段额外说明。
demo:使用sigaction函数,让进程在处理SIGINT(2)信号的时候,除了自动屏蔽SIGINT(2)信号之外,还屏蔽SIGQUIT(3)信号。
int main()
{
auto f = [](int signum) -> void
{
cout << "收到" << signum << "号信号" << endl;
sigset_t sigset;
sigemptyset(&sigset);
while (true)
{
sigpending(&sigset);
for (int i = 31; i >= 1; i--)
{
cout << sigismember(&sigset, i) ? '1' : '0';
}
cout << endl;
raise(SIGINT);
sleep(1);
}
};
struct sigaction act, oact;
act.sa_flags = 0;
act.sa_handler = f; // 设置回调函数
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3); // 进程在处理某一个信号时,额外屏蔽3号信号
sigaction(SIGINT, &act, &oact);
// 打印进程原来的sa_mask
for (int i = 31; i >= 1; i--)
{
cout << sigismember(&oact.sa_mask, i) ? '1' : '0';
}
cout << endl<<"------------------------------"<<endl;
sleep(5);
return 0;
}
可重入函数的概念
可重入与不可重入是函数的一种特性,可重入函数。指的是多个执行流同时调用这个函数,不会出现问题,这样的函数叫做可重入函数,一般的函数都是不可重入的函数。大多数函数都是不可重入函数
volatile关键字
远程拷贝数据的命令scp
scp code.tgz slowstep@192.144.141:/home/
volatile关键是保持内存可见性的一个关键字。
int main()
{
const int x = 10;
*const_cast<int *>(&x) = 20;
cout << x << endl;//10,g++编译器认为x被const修饰,不会被改变,因此读取x的值直接在寄存器读取
return 0;
}
用volatile修饰变量表示每一次在读取变量的时候都在内存中读取。
int main()
{
volatile const int x = 10;
*const_cast<int *>(&x) = 20;
cout << x << endl;//20
return 0;
}
在信号捕捉的场景下也有类似情况
int flag = 1;
void catchfun(int signum)
{
cout << "save " << signum << " signal" << endl;
cout << "has changed g_val" << (flag = 0) << endl;
}
int main()
{
struct sigaction act;
act.sa_handler = catchfun;
sigaction(SIGINT, &act, nullptr);
while (flag);
cout << "flag=" << flag << endl;
return 0;
}
当采用g++编译器,使用-O3进行代码优化时,while循环每一次判断都会从寄存器读取flag的值,因为编译器在编译代码的时候认为main函数中没有修改falg的操作,所以即使在内存中修改了falg的值为0,while循环依旧从寄存器读取,不会退出。
加了volatile关键字之后,while循环每一次判断都会从内存中读取flag的值。
volatile int flag = 1;
void catchfun(int signum)
{
cout << "save " << signum << " signal" << endl;
cout << "has changed g_val" << (flag = 0) << endl;
}
int main()
{
struct sigaction act;
act.sa_handler = catchfun;
sigaction(SIGINT, &act, nullptr);
while (flag);
cout << "flag=" << flag << endl;
return 0;
}
volatile关键字在编译器进行编译的时候就已经起效果了,但是在运行的时候才真正发挥作用。
SIGCHLD信号
当子进程被暂停或终止,子进程会主动向父进程发送SIGCHLD(17)信号,父进程的默认处理方式是忽略。
demo:创建10个子进程,利用SIGCHLD信号的机制进行非阻塞回收子进程。
int main()
{
auto &&f = [](int signum) -> void
{
cout << "收到" << signum << "信号" << endl;
// 由于不知道是哪一个子进程发过来的SIGCHLD(17)信号,可能是多个子进程同时发送,因此需要循环检测回收
pid_t id = 0;
while ((id = waitpid(-1, nullptr, WNOHANG)) > 0)
{
cout << "成功回收pid为" << id << "的子进程" << endl;
}
};
struct sigaction act;
act.sa_handler = f;
sigaction(SIGCHLD, &act, nullptr);
for (int i = 0; i < 10; i++)
{
if (fork() == 0)
{
exit(0);
}
}
sleep(3);
return 0;
}
如果父进程即不想等待子进程,并且还想让子进程退出之后,自动释放僵尸进程,可以手动设置对SIGCHLD(17)忽略。
#define SIG_DFL ((__sighandler_t)0)
#define SIG_IGN ((__sighandler_t)1)
signal(SIGCHLD,SIG_IGN);
struct sigaction act;
act.sa_handler=SIG_IGN;
sigaction(SIGCHLD,&act,nullptr);
SIGCHLD(17)的默认处理方式是忽略,这个忽略指的是OS级别的忽略,表示OS不管用户创建的子进程,子进程的回收工作应该由父进程完成
使用signal(SIGCHLD,SIG_IGN)表示用户告诉OS,用户忽略对于创建的子进程的回收工作,要求OS自动回收用户创建的子进程。
int main()
{
struct sigaction act;
act.sa_handler = SIG_IGN;
sigaction(SIGCHLD, &act, nullptr);
for (int i = 0; i < 10; i++)
{
if (fork() == 0)
{
cout << "子进程pid为" << getpid() << endl;
exit(0);
}
}
sleep(20);
return 0;
}