a.你为什么能认识红绿灯或者闹钟?曾经有人教育过我们,大脑里记住的
b.现在没有闹钟的时候,你知不知道闹钟响了之后,该怎么办?知道
很多事情需要经过这三个问题:是什么?为什么?怎么办?
a,b交代了:是什么?为什么?怎么办?
c.操作系统相当于社会,进程相当于人,进程要能够识别非常多的信号
总结:人能够识别信号
进程就是你,操作系统就是快递员,信号就是快递
用户输入命令,在Shell下启动一个前台进程。用户按下 Ctrl-C ,这个键盘输入产生一个硬件中断,被OS获取,解释成信号,发送给目标前台进程,前台进程因为收到信号,进而引起进程退出
#include <stdio.h>
int main()
{
while(1){
printf("I am a process, I am waiting signal!\n");
sleep(1);
}
}
我们ctrl+c,发现进程终止了:
注意
每个信号都有一个编号和一个宏定义名称,这些宏定义可以在signal.h中找到,例如其中有定 义 #define SIGINT 2,1号到31号信号称为普通信号,剩下的是实时信号,我们不讨论实时信号。
这些信号各自在什么条件下产生,默认的处理动作是什么,在signal(7)中都有详细说明: man 7 signal:
信号的生命周期:信号产生时->信号识别中->信号处理中
我们写一个死循环的程序:
#include<iostream>
#include<sys/types.h>
#include<unistd.h>
int main()
{
while(true)
{
std::cout<<"i am a process: "<<getpid()<<std::endl;
sleep(1);
}
}
Makefile的编写
CC=g++
LDFLAGS=-std=c++11
Src=sig.c
Bin=mysig
$(Bin):$(Src)
$(CC) -o $@ $^ $(LDFLAGS)
.PHONY:clean
clean:
rm -f $(Bin)
我们写了一个程序,让他死循环,然后让他运行起来变成进程,我们ctrl+c实际上是将2号信号发送给进程:
也可以命令行kill命令给进程发送信号,kill命令其实底层调用了kill接口函数:
kill -SIGINT 24994
底层本质这两种是一样的,对于相当一部分信号而言,当进程收到的时候默认的处理动作就是终止当前进程
信号19暂停进程,18继续进程:
发送18信号发现进程并没有终止:
终止进程
void abort(void);
int raise(int sig);
raise函数可以给当前进程发送指定的信号(自己给自己发信号)。
int kill(pid_t pid,int sig);
发送一个信号给一个进程
kill也是一个接口,kill命令就是调用kill函数实现的。kill函数可以给一个指定的进程发送指定的信号。
#include<cstdlib>
//kill 9 112233
int main(int argc,char *argv[])
{
if(argc != 3)
{
cerr<<"Usage: "<<argv[0]<<"pid signum"<<endl;
exit(1);
}
kill(atoi(argc[2]),atoi(argv[1]));
}
可以看到成功杀掉了进程15430
软件条件是某种条件设定,当条件触发时,OS会向进程发信号,设定闹钟,到达该时间点,OS给该进程发送信号
比如我们想计算1秒能够打印多少次count:
#include<iostream>
using namespace std;
#include<unistd.h>
int main()
{
alarm(1);//1秒之后会给目标进程发送SIGALRM,注意:设定不会有明显的现象,1秒之后触发
int count = 0;
while(1)
{
cout<<count++<<endl;
}
}
1秒之后会给目标进程发送SIGALRM,注意:设定不会有明显的现象,1秒之后触发
我们可以通过信号捕捉的方式看一下alarm发送的是什么信号:
#include<iostream>
using namespace std;
#include<unistd.h>
void handler(int signo)
{
cout<<"get a signo: "<<signo<<endl;
sleep(3);
}
int main()
{
for(int i = 1;i<32;i++)
{
signal(i,handler);//信号捕捉
}
sleep(3);
alarm(1);//1秒之后会给目标进程发送SIGALRM,注意:设定不会有明显的现象,1秒之后触发
int count = 0;
while(1)
{
//cout<<count++<<endl;//因为有输入输出,所有效率低
count++;//不带IO进行累加
}
}
可以看到14信号是SIGALRM。
我们发现一秒才打印了count一共60000多次,计算机计算速度那么快为什么才这么少呢?是因为有输入输出,而且我们这是云服务器,还需要网络将数据发送给云服务器,还通过网络将数据结果返回来,所以效率低,我们将count设置成全局变量,不带IO进行累加,在信号捕捉里看一下一秒count能够加到多少:
#include<unistd.h>
int count = 0;
void handler(int signo)
{
cout<<"get a signo: "<<signo<<endl;
cout<<"count is "<<count<<endl;
sleep(3);
}
int main()
{
for(int i = 1;i<32;i++)
{
signal(i,handler);//信号捕捉
}
sleep(3);
alarm(1);//1秒之后会给目标进程发送SIGALRM,注意:设定不会有明显的现象,1秒之后触发
//int count = 0;
while(1)
{
//cout<<count++<<endl;//因为有输入输出,所有效率低
count++;//不带IO进行累加
}
return 0;
}
可以发现不进行IO进行累加,count都加到了2亿
说到硬件异常产生信号,我们要先说一个东西core dump标志位,这个标志是否打开核心转储,这个标志位在进程等待那里提到过,父进程获取子进程退出状态时就有这个信息。
查看Linux中内核中一些东西的大小:
ulimit -a
我们看到core file size是0,说明此时的核心转储是关闭的,那么怎么打开?
ulimit -c 1024
将核心转储的大小设置成1024:
我们在写程序时,故意写个除0错误,进程会收到8号信号然后被终止掉,当进程收到8号信号时,目录下会有core文件,core dumped叫做核心转储:OS将进程运行时的核心数据dump到磁盘上,方便用户进行调试使用,一般而言线上环境核心转储是被关闭的(比如云服务器),为什么呢?
一般在公司里,如果服务器出问题了,一般不是立即找bug,而是尽快的将服务再弄起来,在下一次出问题之前将bug找出来解决,线上环境如果将核心转储打开,服务器一直出问题,就会生成一堆的core文件,时间一长服务器内存可能已经被占满了,服务器可能登录都是问题了
为什么要核心转储?方便用户进行调试使用,比如我们在程序中写出一个除0操作,进入gdb调试,将core文件倒到gdb,gdb可以直接定位到出错的行数:
core-file core.29792
程序异常(野指针(11号信号),除0(8号信号))这些都是硬件异常产生信号的范围,站在语言的角度,就叫程序崩溃,站在系统的角度,就叫做进程收到了信号,core dump标志位,如果发生了核心转储,为1,没有发送核心转储,为0
Makefile的编写
CC=g++
LDFLAGS=-std-c++11
Src=mytest.cc
Target=mytest
$(Target):$(Src)
$(CC) -o $@ $^ $(LDFLAGS)
.PHONY:clean
clean:
rm -f $(Target)
检测core dump标志位,子进程退出,父进程获取子进程的退出状态waitpid:
#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
using namespace std;
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int count = 5;
while(count)
{
cout<<"I am child: "<<getpid()<<"count:"<<count<<endl;
count--;
sleep(1);
}
int arr[5] = 0;
for(int i = 0;i<50;i++)
{
arr[i] = i;//越界
cout<<arr[i]<<endl;
}
int *p;
*p = 100;
int a = 10;
int b = 0;
a/=b;//除0
exit(0);
}
//parent
int status = 0;
pid_t ret = waitpid(id,&status,0);
id(ret == id)
{
cout<<"wait success!"<<endl;
cout<<"exit code!"<<(status>>8)&0xff<<endl;
cout<<"exit signal!"<<(status & 0x7F)<<endl;
cout<<"core dump!"<<(status>>7)&1<<endl;
}
return 0;
}
我们发现core dump是0,然后我们将核心转储打开:ulimit -c 1024,允许core dump核心转储
一个进程要有core dump标志位为1:
需要操作系统打开核心转储,而且还要出现相关的错误(除0,越界等等)
当你的进程触发错误的时候,比如说除0,野指针越界的时候,也会由操作系统识别到,然后给目标进程发送信号,来达到终止进程的目的。
如何理解自己曾经遇到的各自程序崩溃的现象?
本质上就是操作系统识别到错误,然后给目标进程发送信号,来达到终止进程的目的
OS是如何具备识别异常的能力?
OS是软硬件的管理者!软硬件好的时候OS清楚,软硬件坏的时候也能够知道
基本上所有的报错都有对应的软硬件:
除0报错:CPU->状态寄存器,出现错误是状态寄存器会发生变化
越界/野指针:内存和页表MMU
出现错误后OS会知道谁干的,比如如果CPU执行指令出错了,这是谁的指令,然后将该进程终止
上面所说的所有信号产生,最终都要有OS来进行执行,为什么?
OS是进程的管理者
信号的处理是否是立即处理的?在合适的时候
信号如果不是被立即处理,那么信号是否需要暂时被进程记录下来?记录在哪里最合适呢?
一个进程在没有收到信号的时候,能否能知道,自己应该对合法信号作何处理呢?
如何理解OS向进程发送信号?能否描述一下完整的发送处理过程?
为什么不是立即处理的而是在合适的时候?
信号的产生是在进程的运行的任何时间点都可以产生的,有可能进程正在做更重要的事情,信号的产生和进程的运行是:异步的
信号的本质:因为信号不是立即处理的,所以信号一定要先被保存起来
在哪里保存?如何保存?谁发的,如何发?
在进程的PCB,进程控制块task_struct
如何保存?
对进程而言,关心的是"是否有信号"+"信号是谁"的问题,就跟外卖员给你送外卖,你关心的是外卖到了没和是什么外卖,用什么结构来保存呢?位图!unsigned int signals;比特位的位置代表的是是谁,比特位的内容(0或者1)代表的是是否收到信号
谁发的,如何发?
发送信号的本质就相当于写对应进程的task_struct信号位图,因为OS是进程的管理者,对进程数据做修改,OS是有能力和义务的!信号是OS发送的,通过修改对应进程的信号位图(0->!),完成信号的发送!信号的产生都是直接或者间接通过OS发送给进程
#include<iostream>
#include<sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl;
}
int main
{
signal(2, handler);//自定义方式捕捉信号
while(true)
{
std::cout<< "l am a process: " << getpid0 << std::endl;
sleep(1);
}
return 0;
}
可以看到捕捉到了2信号
#include<iostream>
#include<sys/types.h>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "get a signal: " << signo << std: : endl ;
}
int main
{
for(int i=0;i < 32; i++)
{
signal(i, handler);
}
while(true)
{
std::cout<< "l am a process: " << getpid0 << std: : endl;
sleep(1);
}
return 0;
}
可以对大部分信号进行自定义捕捉,但是个别无法自定义捕捉,比如9号信号,更不能忽略,9号信号没有办法捕捉和忽略
所以我们写了自定义捕捉程序时,如果程序终止不了,可以使用kill -9选项进行终止程序
/usr/include/bits/signum.h
在这个路径下可以看到信号的信息说明:
信号的发送,信号的识别,信号的处理上面已经讲解了,信号发送给进程后,进程不一定会立即处理,需要保存信号,那么具体是怎么保存的呢?我们从内核的角度来讲解信号的保存
信号抵达的方式有:默认,忽略,自定义捕捉,进程可以允许某些信号不会被递达(阻塞),此时这些信号是阻塞信号,保持在未决状态,直到解除阻塞(方可递达),被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:
阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作
前面我们说过进程在接受到信号后,可能不是立即处理信号,而是先将信号保存起来,是在进程控制块中保存的:
每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。
在上图的例子中:
如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?
Linux是这样实现的:
常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
pending位图:比特位的位置代表信号的编号,比特位的内容(0 or 1)代表是否收到信号,OS发送信号本质是修改task_struct pending位图的内容
handler数组:用信号的编号,作为数组的索引,找到该信号对应的信号处理方式,然后指向对应的方法(递达)
block位图:比特位的位置代表信号的编号,比特位的内容(0 or 1) 代表是否阻塞该信号
注意:
如果没有收到对应的信号,照样可以阻塞特定信号,阻塞更准确的理解成一种"状态",检测信号是否会被递达,是否被阻塞,都是OS的任务,信号的自定义捕捉方式是用户提供的!是在用户的权限下对应的方法
信号集用来描述信号的集合,每个信号占用一位。从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集
这个类型可以表示每个信号的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有效”和“无效”的含义是该信号是否处于未决状态。下面将详细介绍信号集的各种操作。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
我们写程序创建sigset_t变量,本质上是在栈上开辟的空间创建的他,是在用户空间,我们设置进程属性还需要系统调用接口
int main()
{
sigset_t set;//在栈上开辟空间,用户空间,设置进程属性(OS),系统调用接口
return 0;
}
sigset_t类型对于每种信号用一个bit表示“有效”或“无效”状态,至于这个类型内部如何存储这些bit则依赖于系统
实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做
任何解释,比如用printf直接打印sigset_t变量是没有意义的
这些操作函数只是在用户空间上的,修改的是用户空间的变量
#include <signal.h>
int sigemptyset(sigset_t *set);
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有
效信号。
#include <signal.h>
int sigfillset(sigset_t *set);
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系
统支持的所有信号。
注意:
在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
#include <signal.h>
int sigaddset (sigset_t *set, int signo);
指定位置设置为1(添加信号)
#include <signal.h>
int sigdelset(sigset_t *set, int signo);
指定位置设置为0(删除信号)
#include <signal.h>
int sigismember(const sigset_t *set, int signo);
判断特定信号是否已经被设置
这四个函数都是成功返回0,出错返回-1。sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含
某种 信号,若包含则返回1,不包含则返回0,出错返回-1。
这些都只是语言层面的操作函数,我们需要设置进程属性的话就需要系统调用接口:
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
set是输入型参数,oset是输出型参数:
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信
号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后
根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值:
how | 说明 |
---|---|
SIG_BLOCK | set包含了我们希望添加到当前信号屏蔽字的信号,相当于mask=mask|set |
SIG_UNBLOCK | set包含了我们希望从当前信号屏蔽字中解除阻塞的信号,相当于mask=mask&~set |
SIG_SETMASK | 设置当前信号屏蔽字为set所指向的值,相当于mask=set |
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。调用成功则返回0,出错则返回-1。 下面用刚学的几个函数写个程序。程
序如下:
程序的步骤:
#include<signal.h>
#include<iostream>
using namespace std;
void show_pending(sigset_t *pending)
{
for(int i = 1;i <= 31;i++)
{
if(sigismember(pending,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
sigset_t in,out;//定义变量表示阻塞信号,in阻塞信号集:out是旧的阻塞信号集(输出型参数)
sigemptyset(&in);//初始化阻塞信号集所有位为0
sigemptyset(&out);//初始化未决信号集所有位为0
sigaddset(&in,2);//设置2号信号被block,user stack,用户栈上设置,不影响内核
sigprocmask(SIG_SETMASK,&in,&out);//在内核中完成2号block
sigset_t pending;
while(1)
{
sigpending(&pending);//读取当前进程的未决信号集
sleep(1);//每隔一秒打印一下当前进程的未决信号集
show_pending(&pending);
}
return 0;
}
首先要打印0000000000000000000000000000000,因为此时2号信号是阻塞状态,我们还没有发送2号状态,所以刚开始未决信号集打印的是0000000000000000000000000000000,然后当我们发送2号信号时打印的是0100000000000000000000000000000,因为2号是被设置为阻塞,只能处于未决,不能递达:
接下来我们再添加一些步骤:
第五步需要考虑一下2号信号的默认处理
void show_pending(sigset_t *pending)
{
for(int i = 1;i <= 31;i++)
{
if(sigismember(pending,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
int main()
{
signal(2,handler);
sigset_t in,out;
sigemptyset(&in);
sigemptyset(&out);
sigaddset(in,2);//设置2号信号被block,user stack,用户栈上设置,不影响内核
sigprocmask(SIG_SETMASK,&in,&out);//在内核中完成2号block
int count = 0;
sigset_t pending;
while(1)
{
sigpending(&pending);
sleep(1);
show_pending(&pending);//每隔一秒打印一下当前进程的未决信号集
if(count == 20)
{
//20秒之后会恢复2号信号
sigprocmask(SIG_SETMASK,&out,&in);//恢复之后,2号信号立马递达,并且执行默认动作
//恢复之后in又变成了老的阻塞信号集
cout<<"old: ";
show_pending(&in);//打印in信号集 010000000000...
cout<<"new: ";
show_pending(&out); //打印out信号集 0000000000...
}
count++;
}
return 0;
}
我们分析一下程序:首先每隔一秒打印一下当前进程的未决信号集00000…,然后我们发送2号信号,此时未决信号集发生变化打印01000000…,然后20秒之后,将2号信号恢复,恢复之后in又变成了老的阻塞信号集,out是新的阻塞信号集,打印in信号集即0100000…,打印out信号集00000…
看打印结果:
我们发现20秒之后程序直接退出了,并没有打印old和in,当count等于20时,恢复了2号信号,2号信号是终止进程,恢复了2号信号,2号信号完成递达,我们没有自定义捕获信号,所以默认处理,即终止了进程。
我们写了自定义捕获信号后,就不会终止进程,会继续打印in信号集和out信号集了:
void show_pending(sigset_t *pending)
{
for(int i = 1;i <= 31;i++)
{
if(sigismember(pending,i))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<endl;
}
//自定义处理方式:捕获:
void handler(int signo)
{
std::cout << "get a signal: " << signo << std::endl ;
}
int main()
{
signal(2,handler);
sigset_t in,out;
sigemptyset(&in);
sigemptyset(&out);
sigaddset(in,2);//设置2号信号被block,user stack,用户栈上设置,不影响内核
sigprocmask(SIG_SETMASK,&in,&out);//在内核中完成2号block
int count = 0;
sigset_t pending;
while(1)
{
sigpending(&pending);
sleep(1);
show_pending(&pending);//每隔一秒打印一下当前进程的未决信号集
if(count == 20)
{
//20秒之后会恢复2号信号
signal(2,handler);//自定义捕获2号信号
sigprocmask(SIG_SETMASK,&out,&in);//恢复之后,2号信号立马递达,并且执行默认动作
//恢复之后in又变成了老的阻塞信号集
cout<<"old: ";
show_pending(&in);//打印in信号集 010000000000...
cout<<"new: ";
show_pending(&out); //打印out信号集 0000000000...
}
count++;
}
return 0;
}
什么时候能够处理信号呢?合适的时候,指的是进程从内核态切换到用户态时,尝试进行信号检测与捕捉执行
如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下: 用户程序注册了SIGQUIT信号的处理函数sighandler。 当前正在执行main函数,这时发生中断或异常切换到内核态。 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。 内核决定返回用户态后不是恢复main函数的上下文继续执行,而是执行sighandler数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是 两个独立的控制流程。 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。 如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。
什么时候进行信号的递达?
普通用户可以操作用户空间,可以进行管理,用户访问用户空间是用户态,访问内核时是内核态,进程从内核态返回用户态时,尝试进行信号检测与捕捉执行
进程地址空间中有3G的用户空间和1G的内核空间:
我们在执行程序时如果访问的是用户的代码,则所处的状态是用户态,当通过系统调用访问内核数据时,用户是不能访问内核的,所以系统会自动进行身份切换:usr->kernel,那么OS怎么知道当前所处的状态呢?CPU中会存在一个权限相关的寄存器数据或者看你使用的是哪个种类的页表来标识所处的状态,如果状态变成内核态肯定用的是内核态的页表
我们知道每个用户进程都有自己的用户级页表!但是OS只有一份,所有我们只需要维护一份内核级页表,内核页表是进程所共享的,不同进程通过内核级页表映射的代码和数据看到的是一样的代码和数据:
如何触发内核和用户之间的切换?
中断,调用系统接口等等
用户态和内核态的权限级别不同,决定了能看到的资源是不一样的,内核态的级别更高,但是并不代表它可以随意访问用户态,前面我们说信号被捕捉的时间点,内核态返回用户态的时候,下面我们来说明一下整个信号捕捉的过程:
CPU执行用户层的代码,用户层的代码可能有系统调用,调用系统调用去执行,这里的系统调用是函数,OS提供。并且有代码,也需要被执行,那么应该以什么态执行呢?普通人不能执行OS的代码,故要以内核态允许,当调用完系统调用接口时,执行完系统调用后需要返回,返回时会检测信号与捕捉执行,如果有信号在返回时需要做信号处理,当函数指针数组里面的处理方法是默认和忽略是2简单。麻烦的是自定义捕捉,如果是自定义捕捉,那么就要返回去执行信号捕捉方法,那么执行信号捕捉方法的状态是什么状态?
理论上内核态是绝对可以执行用户态的代码,但实际上并非如此,不能以内核的身份去执行用户层的代码,OS不相信任何人写的代码,一旦自定义捕捉信号是恶意程序的话,要是以内核态执行,那么是最高权限去执行的,这样就危险了,所以必须得从内核态切换回用户态去执行用户态的代码,执行完不能直接返回系统调用那里,需要再返回内核态经过特殊系统调用sys_sigreturn()返回用户态调用系统调用的地方。
一图理解信号捕捉过程:
与横线的交点数就是内核态和用户态之间的切换次数,我们发现有四个交点,箭头的指向说明是由谁切换向谁
对应处理信号捕捉的函数除了signal,还有sigaction:
信号捕捉,第一个参数是你想捕捉哪个信号,第二个是你想怎么处理,
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
sigaction函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回0,出错则返回- 1。
第一个参数:signo是指定信号的编号。
第二个参数:若act指针非空,则根据act修改该信号的处理动作。
第三个参数:若oact指针非空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体
这个结构体里面我们只需要关心第一个成员,第二个成员和第三个成员
sa_flag我们基本设置为0
sa_mask的解释:
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
默认情况下,当你正在处理某个信号的时候,当前信号会被短暂的block,直到当前信号处理完毕,设置自定义捕捉时相当于是在那个函数指针数组中修改数组中的元素,下面我们来写程序验证一个信号的捕捉过程:
Makefile的编写
CC=g++
Src=myproc.cc
Target=myproc
$(Target):$(Src)
$(CC) -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f Target
验证一个信号的捕捉过程(sigaction):
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void handler(int signo)
{
cout<<"get a signo: "<<signo<<endl;
sleep(10);
exit(10);
}
int main()
{
struct sigaction act,oact;
act.sa_headler = headler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);//sa_mask都置为0
sigaddset(&act.sa_mask,3);//将3号信号添加到sa_mask信号集
sigaction(SIGINT,&act,&oact);//信号捕捉
while(1)
{
cout<<"process is running...\n"<<endl;
sleep(1);
}
return 0;
}
写出这个代码的意思是,我们先让进程运行起来,然后我们设置sigaction参数,我们主要演示sa_mask的作用,我们将3号信号添加到sa_mask信号集,我们的程序需要出现的画面是:先一秒打印一次process is running…,当我们发送2号信号,会进去自定义捕捉,打印get a signo: 2,然后睡眠10秒,在这10秒期间我们发送2号信号和3号信号,发现没反应,是因为它们在阻塞状态。
到此我们信号发送时,信号保存中,信号处理时,信号的这三个过程已经全部讲完。
下面说一下普通信号的一个区别:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void show(int signo)
{
int i = 0;
while(i<10)
{
cout<<"get a signo: "<<signo<<endl;
i++;
sleep(1);
}
}
void handler(int signo)
{
show(signo);
}
int main()
{
struct sigaction act,oact;
act.sa_headler = headler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);//将3号信号添加
sigaction(SIGINT,&act,&oact);
while(1)
{
cout<<"process is running...\n"<<endl;
sleep(1);
}
return 0;
}
上面的代码当我们发送2号信号时,会进入自定义信号捕捉函数,然后会调用show函数,show函数里面10秒进行打印10次,那么有个问题,当我们在这10秒期间再次发送2号信号呢?
我们刚开始发送2号信号开始了打印,当我们在这10秒当中有发送2号信号,会等第一次发送的2号信号打印完然后再次打印后面发的2号信号。
当我们在main函数当中调用show函数,会发生什么现象呢?我们来看一下:
#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;
void show(int signo)
{
int i = 0;
while(i<10)
{
cout<<"get a signo: "<<signo<<endl;
i++;
sleep(1);
}
}
void handler(int signo)
{
show(signo);
}
int main()
{
struct sigaction act,oact;
act.sa_headler = headler;
act.sa_flags = 0;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask,3);//将3号信号添加
sigaction(SIGINT,&act,&oact);
show(9999);//调用show函数
while(1)
{
cout<<"process is running...\n"<<endl;
sleep(1);
}
return 0;
}
我们发现刚开始我们调用了show函数再进行打印9999,在打印的10秒期间我们发送2号信号,发现开始打印2了,打印完2,再将剩余的9999打印完。我们发现一个函数被多个执行流同时进入访问。
main执行流和信号捕捉执行流同时进入了,函数被多个执行流同时进入的情况,叫做重入
不可重入函数(重入会引起问题)和可重入函数(重入不会引起问题),如果函数里面都是涉及局部变量,那大概率是可重入的,如果是全局变量,那么大概率是不可重入的,大部分的函数都是不可重入的!
volatile关键字作用:
保持内存的可见性,易变关键字
我们来看下面这个代码:
int flag = 0;
void handler(int signo)
{
flag = 1;
cout<<"handler signo: "<<signo<<",set flag == 1"<<endl;
}
int main()
{
signal(2,handler);
while(!flag);
cout<<"process end..."<<endl;
return 0;
}
我们通过发送2号信号来讲flag改为1,这样死循环就能够退出了:
然后换成C语言:
int flag = 0;
void handler(int signo)
{
flag = 1;
//cout<<"handler signo: "<<signo<<",set flag == 1"<<endl;
printf("handler signo: %d,set flag == 1\n",signo);
}
int main()
{
signal(2,handler);
while(!flag);
//cout<<"process end..."<<endl;
printf("process end...\n");
return 0;
}
看gcc可不可以,gcc:
我们发现gcc也是可以的
此时没有问题是因为优化级别为默认O0
优化级别高一点时就会有疑惑,比如我们设置成O1
我们发现我们设置成O1时,还是在死循环,咦?我们明明把flag改成1了,为什么还在死循环呢?
是因为main执行流没有修改flag,当main执行流中没有修改flag的操作时,就会进行优化:会将flag优化为寄存器变量,该寄存器变量的值为0,while循环会去寄存器ebx当中查看flag的值,而在信号捕获处理函数中是将内存中的值改为了1,而main执行流在寄存器中直接ebx当中查看falg的值。所以while循环并没有出去。
一旦用volatile关键字修饰,就告诉编译器,这个变量我们可能还要修改呢,不要放在寄存器里,此时编译器就不会优化到寄存器,找flag的值就需要内存中的flag,main执行流就会将内存中的flag拿到CPU中去判断,此时就跳出了循环:
我们都知道父进程创建子进程,父进程得知道子进程退出了,如果想要知道子进程的退出状态,有两种方式:
void handler(int signo)
{
cout<<"father process .."<<getpid()<<" " << getppid() << " count:" << "signo"<<signo<<endl;
}
int main()
{
signal(SIGCHLD,handler);//
if(fork())
{
//child
int count = 5;
while(count)
{
cout<<"child process .."<<getpid()<<" " << getppid() << " count:" << count<<endl;
sleep(1);
count--;
}
cout<<"child quit...!"<<endl;
exit(0);
}
//> 0 parent
sleep(10);
return 0;
}
五秒之后子进程退出,而父进程此时正在sleep,此时父进程被信号提前唤醒,在这里子进程给父进程发送SIGCHLD信号,父进程被唤醒处理该信号,处理完该信号之后父进程不会继续剩余时间的睡眠,而是执行下面的工作,sleep函数的返回值是睡眠的剩余时间,我们打印一下sleep的返回值:
通过理解SIGCHLD信号,我们可以给父进程可以写个信号处理函数来等待子进程的退出,这个信号处理函数里面等待子进程的退出(waitpid)获取子进程的状态。
事实上,要想不产生僵尸进程还有另外一种办法:父进程调用signal函数将SIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程。系统默认的忽略动作和用户用signal函数自定义的忽略通常是没有区别的,但这是一个特例。此方法对于Linux可用,但不保证在其它UNIX系统上都可用。
#include<iostream>
using namespace std;
#include<unistd.h>
#include<signal.h>
int main()
{
signal(SIGCHLD,SIG_IGN);
if(fork() == 0)
{
//child
int count = 5;
while(count)
{
cout<<"child process .."<<getpid()<<" " << getppid() << " count:" << count<<endl;
sleep(1);
count--;
}
cout<<"child quit...!"<<endl;
exit(0);
}
//> 0 parent
int ret = sleep(10);
cout<<ret<<end;
return 0;
}
可以看到ret为0,父进程没有被唤醒,忽略了此信号,并且子进程不会变成僵尸进程,会自动清理掉
我们如何决定是否需要wait子进程?
站在Z进程的内存泄露角度,可以等待也可以不等待,signal(SIGCHLD,SIG_IGN),有可能需要获取子进程的退出码,如果父进程不关心子进程的退出码,可以不进行wait,有可能关心,那么父进程必须wait
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/attemptendeavor/article/details/123885945
内容来源于网络,如有侵权,请联系作者删除!