Linux 信号 2019.1.8(信号的实现机制,信号状态,阻塞信号集和未决信号集,raise,abort,alarm,setitimer,信号捕捉,利用SIGCHLD回收子进程)

本文详细解析了信号处理机制,包括信号的特点、实现机制、产生方式及信号捕捉等核心概念。探讨了不同信号的默认处理行为,信号集操作,以及如何通过sigaction和signal函数进行信号捕捉,防止进程意外终止。
摘要由CSDN通过智能技术生成

学习目标

 

推荐使用sigaction,因为不同平台是signal可能不一样

 

信号的特点

简单,不能携带大量信息,满足特定条件才会发生。信号也叫软中断,有可能会有延迟

 

信号的实现机制

信号实际上是由内核发送,内核来处理收到的信号。收到信号的进程,必须对信号做出处理(忽略,捕获,默认动作都行)

 

产生信号

 

信号状态

  • 产生
  • 递达——信号到达并且处理完
  • 未决——信号被阻塞

 

信号的默认处理方式

  • 忽略
  • 默认动作
  • 捕获

 

信号的四要素

  • 编号
  • 事件
  • 名称
  • 默认处理动作

默认处理动作有五个(linux 输入  man 7 signal 即可查看),如图所示:

注意:9号,19号信号,不能捕捉,不能忽略,不能阻塞。

 

 

 

阻塞信号集和未决信号集

处理了就把二进制位设置为0未处理就设置为1.

阻塞信号集的作用是影响未决信号集,相当于给他挡了一堵墙。

如果我们把2号信号设置成阻塞(即在阻塞信号集的对应位置设为1),那么来一个2号信号,则未信号集的对应值置为1,什么时候阻塞信号集中的对应位置变成0了,什么时候未决信号集才能去处理之前被阻塞的那个信号

 

 

信号的产生

 

系统api产生信号

 

kill函数

int kill(pid_t pid, int sig);
  • pid > 0 ,要发送进程ID
  • pid = 0, 代表当前调用进程中内所有进程
  • pid = -1, 代表有权限发送的所有进场
  • pid < 0,代表 -pid 对应的组内所有进程
  • sig 对应的信号

 

练习,让3号子进程杀死父进程

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/stat.h>
#include<signal.h>

int main(){
	int i;
	for(i=0; i<5; i++){
		pid_t pid=fork();
		if(pid==0){
			break;
		}
	}
	//我们使3号子进程杀死父进程
	if(i==2){
		printf("I will kill my father after 5 second!\n");
		sleep(5);
		kill(getppid(),SIGKILL);
	}
	if(i==5){
		while(1){
			printf("I am father, very happy!\n");
			sleep(1);
		}
	}
	return 0;
}

运行结果

 

再来一个父进程把3号子进程杀死

#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<signal.h>

int main(){
	int i=0;
	pid_t pid3, pid;
	for(i=0; i<5; i++){
		pid=fork();
		if(pid==0) break;
		if(i==2) pid3=pid;
	}
	if(i<5){
		while(1){
			printf("I am child, pid=%d, ppid=%d\n", getpid(), getppid());
			sleep(3);
		}
	}
	else if(i==5){
		printf("I am father,pid=%d, I will kill xiao san pid3=%d\n", getpid(), pid3);
		sleep(5);
		kill(pid3, SIGKILL);
		while(1) sleep(1);
	}
	return 0;
}

运行结果:pid15537不见了

 

 

raise——给自己发送信号

#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>

int main(){
	printf("I will die!\n");
	sleep(2);
	raise(SIGKILL);//kill(getpid(), sig)
	return 0;
}

运行结果:

实际上调用的是kill

 

abort——直接给自己发送异常信号,直接退出

#include<stdio.h>
#include<sys/types.h>
#include<signal.h>
#include<unistd.h>
#include<stdlib.h>

int main(){
	printf("I will die!\n");
	sleep(2);
	abort();//kill(getpid(), sig)
	return 0;
}

运行结果:

 

 

 

时钟信号

 

alarm

  • 定时给自己发送SIGALRM
  • 几秒后发送信号
  • 返回值——上次闹钟剩余的秒数
  • 特别的,如果传入参数秒为0,代表取消闹钟
#include<stdio.h>
#include<unistd.h>

int main(){
	alarm(6);
	while(1){
		printf("lai da wo!\n");
		sleep(1);
	}
	return 0;
}

运行结果

 

我们来看alarm函数的返回值

#include<stdio.h>
#include<unistd.h>

int main(){
	int ret=0;
	ret=alarm(6);
	printf("ret=%d\n", ret);
	sleep(2);
	ret=alarm(5);
	printf("ret=%d\n", ret);
	while(1){
		printf("lai da wo!\n");
		sleep(1);
	}
	return 0;
}

运行结果

 

 

setitimer函数——周期性的发送信号

 

结构体——结构体中嵌套了两个结构体

 

函数原型

int settime(int which, const struct itimerval *new_value, struct itimerval *old_value);)
  • which
  1. ITIMER_REAL 自然定时法 SIGALRM
  2. ITIMER_VIRTUAL 计算进程执行时间 SIGVTALRM
  3. ITIMER_PROF 程序执行时间+调度时间 ITIMER_VIRTUAL
  • new_value 要设置的闹钟时间
  • old_value 原闹钟时间

 

setitimer实现alarm功能

#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>

int main(){
	//setitimer是一个结构体中嵌套了两个结构体
	//两个结构体分别代表:周期性的时间设置,下次的闹钟时间
	//每个结构体里。,第一个参数是秒,第二个参数是微妙
	//这里定义的意思就是:三秒之后发送SIGALRM信号
	struct itimerval myit={{0,0},{3,0}};
	setitimer(ITIMER_REAL, &myit, NULL);

	while(1){
		printf("Who can kill me!\n");
		sleep(1);
	}
	return 0;
}

运行结果

 

 

测试周期性发送信号功能

周期性发送SIGALRM信号杀死进程,进程利用catch_sig函数来捕获SIGALRM信号

#include<stdio.h>
#include<sys/time.h>
#include<unistd.h>
#include<signal.h>

void catch_sig(int num){
	printf("cat %d sig\n", num);
}
int main(){
	signal(SIGALRM, catch_sig);
	//第一次等待五秒,之后每隔三秒
	struct itimerval myit={{3,0},{5,0}};
	setitimer(ITIMER_REAL, &myit, NULL);

	while(1){
		printf("Who can kill me!\n");
		sleep(1);
	}
	return 0;
}

运行结果

 

 

setitimer实现alarm

#include<stdio.h>
#include<unistd.h>
#include<sys/time.h>

unsigned int myalarm(unsigned int seconds){
	struct itimerval oldit,myit={{0,0},{0,0}};
	//second秒之后,给我发送一个SIGALRM信号
	myit.it_value.tv_sec=seconds;
	//old_value 原闹钟时间
	setitimer(ITIMER_REAL, &myit, &oldit);
	//打印秒和微秒
	printf("tv_sec=%ld, tv_microsec=%ld\n",
		oldit.it_value.tv_sec, oldit.it_value.tv_usec);
	return oldit.it_value.tv_sec;

}

int main(){
	int ret=0;
	ret=myalarm(5);
	printf("ret=%d\n", ret);
	sleep(3);
	ret=myalarm(3);
	printf("ret=%d\n", ret);
	while(1){
		printf("lai da wo!\n");
		sleep(1);
	}
	return 0;
}

 

 

 

信号集的操作函数

输入可查看信号集出来函数的man手册

man sigemptyset
  •  int sigemptyset(sigset_t *set);——清空信号集
  • int sigfillset(sigset_t *set);——填充信号集
  •  int sigaddset(sigset_t *set, int signum);——添加某个信号到信号集
  • int sigdelset(sigset_t *set, int signum);——从集合中删除某个信号
  • int sigismember(const sigset_t *set, int signum——判断是否为集合里的成员——返回 1 代表集合中
  •  int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);——用来设置或者解除阻塞信号集
  • int sigpending(sigset_t *set);——获取未决信号集

 

输入命令,查看另一个信号集函数——sigprocmask——用来设置或者解除阻塞信号

man sigprocmask

  • how
  1. SIG_BLOCK 设置阻塞
  2. SIG_UNBLOCK 解除阻塞
  3. SIG_SETMASK 设置set为新的阻塞信号集
  • set 传入的信号集
  • oldset 旧信号集,传出

 

 

输入命令,查看另一个信号集函数——sigpending——用来获取未决信号集

man sigpending

  • set 传出参数,当前的未决信号集

 

 

把所有常规信号的未决信号集打到屏幕上

将2号信号(ctrl+c)放在阻塞信号集上,这样该信号在未决信号集上会进行保留

#include<stdio.h>
#include<unistd.h>
#include<signal.h>

int main(){
	sigset_t pend, sigproc;
	//设置阻塞信号,等待按键产生信号
	//先清空
	sigemptyset(&sigproc);
	//将2号信号(ctrl+c)放在集合里
	sigaddset(&sigproc, SIGINT);
	//设置阻塞信号集
	sigprocmask(SIG_BLOCK, &sigproc, NULL);

	//循环取未决信号集中的信号
	while(1){
		//循环读取未决信号,打印
		sigpending(&pend);
		for(int i=0; i<32; i++){
			//存在信号集中
			if(sigismember(&pend, i)==1){
				printf("1");
			}else{
				printf("0");
			}
		}
		printf("\n");
		sleep(1);
	}

	return 0;
}

 

 

 

 

信号捕捉

 

防止进程意外死亡

输入查看函数原型

man signal
  • typedef void (*sighandler_t)(int);
  • sighandler_t signal(int signum, sighandler_t handler);
  1. signum 要捕捉的信号
  2. handler 要执行的捕捉函数指针,函数应该声明 void func(int);//函数名可变

 

输入查看函数原型

man sigaction
  • int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
  1. signum 捕捉的信号
  2. act 传入的动作
  3. oldact 原动作

 

捕捉我们定时给自己发送的14号自杀信号

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/time.h>

//定义捕捉函数
void catch_sig(int num){
	printf("catch %d sig\n", num);
}

int main(){
	//注册捕捉函数
	struct sigaction act;
	//说明为你使用的是sigaction结构体中的第一个捕捉函数
	act.sa_flags=0;
	//那个捕捉函数的函数指针指向我们上面自己写的捕捉函数catch_sig
	act.sa_handler=catch_sig;
	//清空信号集
	sigemptyset(&act.sa_mask);
	sigaction(SIGALRM, &act, NULL);

	//setitimer 5秒之后每隔3秒来一次信号
	struct itimerval myit={{3,0},{5,0}};
	setitimer(ITIMER_REAL, &myit, NULL);
	while(1){
		printf("Who can kill me!\n");
		sleep(1);
	}	
	return 0;
}

 

 

信号捕捉的特性

 

 

 

内核实现信号捕捉过程

 

 

 

利用SIGCHLD回收子进程

子进程在暂停或者退出的时候会发送SIGCHLD信号,我们可以通过捕捉SIGCHLD信号来回收子进程

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>

void catch_sig(int num){
	pid_t pid=waitpid(-1, NULL, WNOHANG);
	if(pid>0){
		printf("wait child %d ok\n", pid);
	}
}

int main(){
	int i=0;
	pid_t pid;
	for(i=0; i<10; i++){
		pid=fork();
		if(pid==0){
			break;
		}
	}
	if(i==10){
		//father
		struct sigaction act;
		act.sa_flags=0;
		sigemptyset(&act.sa_mask);
		act.sa_handler=catch_sig;
		sigaction(SIGCHLD, &act, NULL);
		while(1){
			sleep(1);
		}
	}else if(i<10){
		printf("I am %d child, pid=%d\n", i, getpid());
		sleep(i);
	}
	return 0;
}

上面的程序是有问题的,如果我十个孩子不是依次死去,而是一起死,就会出现僵尸进程,因为信号不排队的

 

改进一下上面的代码,让他不出僵尸进程。我们即将信号收集函数catch_sig里面用一个while循环来循环处理pid收集到的各个SIGCHLD信号。

#include<stdio.h>
#include<unistd.h>
#include<signal.h>
#include<sys/wait.h>

void catch_sig(int num){
	pid_t pid;
	//此时,如果多个子进程一起死,pid对获得多个信号
	while((pid=waitpid(-1, NULL, WNOHANG))>0) {
		if(pid>0){
			printf("wait child %d ok\n", pid);
		}
	}	
}

int main(){
	int i=0;
	pid_t pid;
	for(i=0; i<10; i++){
		pid=fork();
		if(pid==0){
			break;
		}
	}
	if(i==10){
		//father
		struct sigaction act;
		act.sa_flags=0;
		sigemptyset(&act.sa_mask);
		act.sa_handler=catch_sig;
		sigaction(SIGCHLD, &act, NULL);
		while(1){
			sleep(1);
		}
	}else if(i<10){
		printf("I am %d child, pid=%d\n", i, getpid());
	}
	return 0;
}

 

如果注册sigaction之前,子进程就死了,那么还是会产生僵尸进程。如何避免?

子进程死之前,如果main函数还没注册sigaction,就把SIGCHLD信号屏蔽即可。屏蔽信号的工作,应该在创建子进程之前就开始去做,如此可以避免极端情况下出现差错。

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <signal.h>

void catch_sig(int num)
{
    pid_t wpid ;
    while( (wpid = waitpid(-1,NULL,WNOHANG)) > 0   ){
        printf("wait child %d ok\n",wpid);
    }
    
}

int main()
{
    int i =0;
    pid_t pid ;

    //在创建子进程之前屏蔽SIGCHLD信号
    sigset_t myset,oldset;
    sigemptyset(&myset);
    sigaddset(&myset,SIGCHLD);
    //oldset 保留现场,设置了SIGCHLD到阻塞信号集
    sigprocmask(SIG_BLOCK,&myset,&oldset);

    for(i = 0 ; i < 10 ; i ++){
        pid =fork();
        if(pid == 0 ){
            break;
        }
    }

    if(i == 10 ){
        //parent 
		//模拟注册晚于子进程死亡
        sleep(2);
        struct sigaction act ;
        act.sa_flags  = 0;
        sigemptyset(&act.sa_mask);
        act.sa_handler = catch_sig;

        sigaction(SIGCHLD,&act,NULL);
        
        //解除屏蔽现场
        sigprocmask(SIG_SETMASK,&oldset,NULL);

        while(1){
            sleep(1);
        }
    }else if(i < 10){
        printf("I am %d child,pid = %d\n",i,getpid());
    }

    return 0;
}

 

 

作业:利用SIGUSR1和SIGUSR2在父子进程之间进行消息传递,实现父子进程交替报数(间隔1s)

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include <signal.h>
#include<sys/types.h>
pid_t pid;
int count =0;
int flag = 0;

void cat_sig_father(int num)
{
    sleep(1);
	//父进程将自己的flag改成1
    flag = 1;
    printf("%d\n",count);
    count +=2;
    //    kill(pid,SIGUSR2);
}
void cat_sig_child(int num)
{
    sleep(1);
	//子进程中的flag变成1
    flag = 1;
    printf("%d\n",count);
	//因为交替数数,所以cout+2
    count +=2;
    //    kill(getppid(),SIGUSR1);
}

int main(int argc,char *argv[]){
    pid = fork();
    if(pid == 0) {
        //son
        count=1;
		//捕捉SIGUSR2信号,捕捉到之后,执行函数cat_sig_child
        signal(SIGUSR2,cat_sig_child);
		//获取父进程id
        pid_t ppid = getppid();

        while(1){
			//flag的值为1时,向父进程发送SIGUSR1信号
            if(flag == 1){
                kill(ppid,SIGUSR1);
                flag = 0;
            }
        }
    }
    else{
		//父进程睡了10微秒
        usleep(10);
		//count的值父子进程不共享
        count=2;
		//注册SIGUSR1
        signal(SIGUSR1,cat_sig_father);
		//给子进程发送SIGUSR2信号
        kill(pid,SIGUSR2);
        //printf("begin ...\n");
        while(1){
            if(flag == 1){
				//给子进程发送SIGUSR2信号
                kill(pid,SIGUSR2);
                flag = 0;
            }
        }

    }
    return 0;
}

 

将上面函数cat_sig_fathercat_sig_child中的sleep()都注释掉之后,会发生卡死的情况。

因此,当系统负荷严重的时候,信号极不稳定

 

产生卡死的原因:

当子进程执行完kill函数之后,此时发生中断,flag=0这句还没执行

       while(1){
			//flag的值为1时,向父进程发送SIGUSR1信号
            if(flag == 1){
                kill(ppid,SIGUSR1);
                flag = 0;
            }
        }

此时父进程收到了信号,执行cat_sig_father函数,将自己的flag改成1,然后发送信号SIGUER2给子进程,父进程的flag变为0。

此时获取了cpu的子进程收到了信号,收到信号要优先处理信号,于是去执行上面的signal函数

        //捕捉SIGUSR2信号,捕捉到之后,执行函数cat_sig_child
        signal(SIGUSR2,cat_sig_child);
		//获取父进程id
        pid_t ppid = getppid();

        while(1){
			//flag的值为1时,向父进程发送SIGUSR1信号
            if(flag == 1){
                kill(ppid,SIGUSR1);
                flag = 0;
            }
        }

而此时子进程的flag值为0,进程无法进入while循环,也就无法向父进程发送信号,此时父子进程的flag的值都为0,因此就卡死了。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值