目录
信号的产生方式
程序的崩溃
通过键盘产生
进程异常产生
系统调用产生
软件条件产生
信号产生中
函数介绍
sigset_t(信号集)
sigprocmask函数
sigpending函数
信号处理
用户态和内核态的理解
处理信号的过程
信号捕捉
sigaction函数
volatile关键字
SIGCHLD信号
信号和信号量就相当于老婆和老婆饼的关系,其实没有实质联系。
1、信号具有识别信号并处理信号的能力,远远早于信号的产生
2、进程收到某种信号的时候,并不算是立即处理,而是在合适的时候
3、进程收到信号之后,有时需要先保存起来,以供合适的时候处理
4、信号的本质是数据,信号的发送就是向进程的task_struct内写入信号数据
5、无论任何信号的发送,本质都是在底层通过OS发送的
进程收到信号的处理方案有以下几种
1、默认动作---------->一部分是终止自己,暂停等
2、忽略动作---------->是一种信号处理的方式,只不过动作就是什么都不干
3、(信号捕捉)自定义动作-->使用signal方法修改信号的处理动作。默认动作->自定义动作
该函数用于修改指定信号的默认行为。
在阐述信号的产生方式之前,我们需要对程序的崩溃重塑以下系统层面的认识。
ps:浮点异常的情况中的计算会加载到cpu中运算,其中除数为0,引发cpu错误。野指针访问的情况,我们需要知道页表映射虚拟地址和物理地址。但是这里的p是一个null指针,没有虚拟地址,何谈映射物理地址,引发错误。
程序上的错误,通常会体现在硬件或者其他软件上边。
OS作为硬件的管理者,当发现硬件错误的时候,就会寻找引发错误的进程,对其发送信号,终止进程。便引起了进程的崩溃。
写入一个死循环
键盘输入ctrl+c,终止进程
验证该操作是由异常引起的。编写以下代码
void handler(int signo){
switch(signo){
case 2:
printf("hello Linux,get a signal:%d\n", signo);
break;
case 3:
printf("hello world,get a signal:%d\n", signo);
break;
case 9:
printf("hello ...,get a signal:%d\n", signo);
break;
default:
printf("hello signal:%d\n", signo);
break;
}
exit(1);
}
int main(){
//捕捉所有信号
int cnt = 1;
for( ; cnt < 31; cnt++){
signal(cnt, handler);//将cnt号信号的默认动作修改成自定义动作
}
while(1){
printf("hello linux!\n");
sleep(1);
}
return 0;
}
执行结果:
执行下边的代码,具有浮点溢出错误
void handler(int signo){
switch(signo){
case 2:
printf("hello Linux,get a signal:%d\n", signo);
break;
case 3:
printf("hello world,get a signal:%d\n", signo);
break;
case 9:
printf("hello ...,get a signal:%d\n", signo);
break;
default:
printf("hello signal:%d\n", signo);
break;
}
exit(1);
}
int main(){
//捕捉所有信号
int cnt = 1;
for( ; cnt < 31; cnt++){
signal(cnt, handler);//将cnt号信号的默认动作修改成自定义动作
}
int a = 10;
a /= 0;//浮点异常
while(1){
printf("hello linux!\n");
sleep(1);
}
return 0;
}
执行结果
当我们想找到具体崩溃在哪一行的时候。需要用到进程等待的知识。
在Linux中,当一个进程退出的时候,它的退出码和退出信号都会被设置(正常情况)。
当进程异常的时候,进程的退出信号会被设置,表明当前进程退出的原因。
如果必要,OS会设置退出信息中的core dump标志位,并将进程在内存中的数据转存到磁盘中,方便后期调试。
默认情况下,云服务器上的core dump服务是被关掉的。
使用ulimit -a可以查看系统资源。
我们需要使用ulimit -c + 大小命令将服务其打开。
然后再运行程序。如下:
再使用gdb调试
ps:并不是所有信号都会引发core dump标志位的重置。
这里的kill就是一个向进程发送信号的接口。
alarm接口设置seconds秒后向进程发送SIGALRM信号。
执行以下代码
#include
#include
#include
#include
#include
#include
int count = 0;
void HandlerAlarm(int signo){
printf("hello: %d\n", count);
exit(1);
}
int main(){//统计一下1s的时间,服务器能对count递增多少
signal(SIGALRM, HandlerAlarm);
alarm(1);//没有设置alarm信号的捕捉动作(即没有自定义),执行默认动作
while(1){
count++;
//printf("hello: %d\n",count++);//这里比较慢,因为有IO
}
return 0;
}
执行结果
总结:信号产生的方式有4种。1、键盘产生2、进程异常产生3、系统调用产生4、软件条件产生。这四种方式最终都是通过OS向目标进程发送的信号。
进程的task_struct中存放着进程的各种属性,其中采用位图结构来标识进程是否收到信号。OS向进程发送信号也就能理解成本质是向进程的task_struct中的信号位图中对应比特位写为1,即完成信号的发送。
实际执行信号的处理动作称为信号抵达:可分为自定义捕捉,忽略和默认三种情况。
信号从产生到抵达之间的状态称为信号未决:本质是这个信号被暂存再task_struct信号为途中。
进程可以选择阻塞某个信号:本质是OS允许进程暂时屏蔽指定的信号。
进程抵达中的忽略与阻塞是完全不同的概念。忽略只是进程抵达的一种方式,而阻塞是没有抵达,是一种独立的状态。忽略和阻塞是不同阶段的概念。
task_struct中存在指向了存放这三张表结构的变量,如下图
其中block位图用于表示进程是否阻塞。pending位图用于是否有信号写入。handler函数指针数组用于进程执行何种动作(其中存放的是动作函数指针,每个信号的编号就是其数组下标)。
这三张表必须横向同时看,也就是说识别一个信号是一个三元组。比如:
第一行表示该进程未被阻塞,但是并未写入0号信号,执行SIG_DFL(默认)动作。
第二行表示该进程被阻塞,且写入了1号信号,执行SIG_IGN(忽略)动作。
第三行表示该进程被阻塞,但是未写入2号信号,执行自定义动作。
每一行的执行逻辑,可参考下边伪代码
int IsHandler(int signo){
if(block & signo){//该信号被block
· · · · ·
return 0;
}
else{//未被block
if(signo & pending){//未被block,且已被写入
handler_arr[signo](signo);
return 0;
}
}
}
从上图来看,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次,阻塞标志也是这样表示的。 因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号 的“有效”或“无效”状态,在阻塞信号集中“有效”和“无效”的含义是该信号是否被阻塞,而在未决信号集中“有 效”和“无效”的含义是该信号是否处于未决状态。 阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask),这里的“屏蔽”应该理解为阻塞而不是忽略。
sigset_t类型对于每种信号用一个比特位表示“有效”或“无效”状态,至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t变量,而不应该对它的内部数据做任何解释。
#include
int sigemptyset(sigset_t *set);//初始化信号集,置0
int sigfillset(sigset_t *set);//初始化信号集,置1
int sigaddset (sigset_t *set, int signo);//将信号集的某一信号置为1
int sigdelset(sigset_t *set, int signo);//将信号集的某一信号置为0
int sigismember(const sigset_t *set, int signo);//判断某一信号在信号集中是否为1
ps:
函数sigemptyset初始化set所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含任何有效信号。
函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置位,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用sigset_ t类型的变量之前,一定要调用sigemptyset或sigfillset做初始化,使信号集处于确定的 状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号
sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后 根据set和how参数更改信号屏蔽字。
sigpending函数可以读取当前进程的未决信号集,通过set参数传出。
现在我们使用sigprocmask函数和sigpending函数完成一个函数。思路是,
1、对2号信号写入信号集
2、将该信号集使用sigprocmask函数将2号信号block住
3、我们键盘输入ctrl+c,产生2号信号,发送至该进程。
4、使用sigpending函数输出pending位图。
ps:因为我们将2号进程block住了,该信号并不能抵达。我们再向其发送2号信号,信号被block住了并不会影响信号的写入。因此我们再发送2号进程之前pending位图应该全为0,发送2号进程之后会看见pending位图发送由0至1的变化。
void show_pending(sigset_t* set){
int i = 1;
for(i; i<= 31; i++){
if(sigismember(set, i)){
printf("1");
}
else{
printf("0");
}
}
printf("\n");
}
void handler(int signo ){
printf("%d号信号被抵达了,已经处理完成!\n", signo);
}
int main(){
signal(2, handler);//捕捉2号信号,完成自定义动作
sigset_t iset, oset;
//对集合做清空
sigemptyset(&iset);
sigemptyset(&oset);
sigaddset(&iset, 2);//将2号信号写入iset集合
//1、设置当前进程的屏蔽字
//2、获取当前进程老的屏蔽字
sigprocmask(SIG_SETMASK, &iset, &oset);
int count = 0;
sigset_t pending;
while(1){
sigemptyset(&pending);
sigpending(&pending);
show_pending(&pending);
sleep(1);
count++;
if(count == 10){
sigprocmask(SIG_SETMASK, &oset, NULL);
//2号信号执行自定义动作
printf("恢复2号信号,可以被抵达了\n");
}
}
return 0;
}
运行结果如下
进程从内核态返回到用户态的时候,进行信号检测和处理。
关于进程如何切换,我们都能找到同一个OS,因为使用同一张内核页表。
关于系统调用:就是进程的身份转化为内核态,然后根据内核页表找到系统函数,进行执行。
大部分情况下,我们的OS都是可以在进程的上下文中直接运行。
ps:用户态的时候执行用户代码,在调用系统接口或者进程切换的时候切换到内核态,查看进程的三张表。(信号捕捉过程)当需要执行自定义动作的时候,切回用户态执行自定义动作,自定动作完成之后,再切到内核态执行sys_sigreturn()函数,最后再切回用户态中用户代码的下一行,继续执行。当执行默认动作或者忽略动作的时候,直接可以再内核中完成,完成之后切回用户态中的用户代码的下一行,继续执行。
sigaction函数可以读取和修改与指定信号相关联的处理动作。
signo 是指定信号的编号。若act指针非空,则根据act修改该信号的处理动作。若oact指针非 空,则通过oact传出该信号原来的处理动作。act和oact指向sigaction结构体
当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,则用sa_mask字段说明这些需要额外屏蔽的信号,当信号处理函数返回时自动恢复原来的信号屏蔽字。
示例
#include
#include
#include
#include
void handler(int signo){
while(1){
printf("get a signo: %d\n", signo);
sleep(1);
}
}
int main(){
struct sigaction act;
memset(&act, 0, sizeof(act));
act.sa_handler = handler;//执行自定义动作
//act.sa_handler = SIG_IGN;
//act.sa_handler = SIG_DFL;
sigemptyset(&act.sa_mask);
sigaddset(&act.sa_mask, 3);
sigaction(2, &act, NULL);//本质是修改当前进程的handler函数指针数组特定内容
while(1){
printf("hello linux!\n");
sleep(1);
}
return 0;
}
上边的代码代码对2号信号执行自定义动作,并将3号信号添加到了block位图中,因此无论我们输入ctrl+c(向该进程发送2号信号)还是ctrl+\(向该进程发送3号信号)都无法终止进程,只能发送9号管理员信号终止。执行结果如下:
#include
#include
int flag = 0;
void handler(int signo){
flag = 1;
printf("change flag 0 to 1\n");
}
int main(){
signal(2, handler);
while(!flag);
printf("这个进程是正常退出的!\n");
return 0;
}
我们使用gcc -O3的方式优化上述代码。
无法出现我们预期的结果。原因如下图。
ps:我们使用gcc优化的方式,全局变量flag的值会直接缓存在cpu中的寄存器里,当flag值发生变化的时候,cpu也不会从内存中再次读取flag,而是使用缓存的flag值。
volatile的作用则是:告诉编译器,不要对该变量做任何优化,必须贯穿式的读取内存,不要读取中间缓冲区寄存器中的数据。
在flag变量前加上volatile关键字的运行结果。
子进程在终止时会给父进程发送SIGCHLD信号,该信号的默认处理动作是忽略。验证如下:
#include
#include
#include
#include
void handler(int signo){
printf("get a signal: %d, pid: %d\n", signo, getpid());
}
int main(){
signal(SIGCHLD, handler);
//signal(SIGCHLD, SIG_IGN);//显式设置忽略SIGCHLD信号,当进程退出后,自动释放僵尸进程
pid_t id = fork();
if(id == 0){
int cnt = 5;
while(cnt){
printf("I am child: %d\n", getpid());
sleep(1);
cnt--;
}
exit(0);
}
while(1);
return 0;
}
运行结果