Linux系统编程 | 信号

信号的机制

进程A给进程B发送信号,进程B收到信号之前执行自己的代码,收到信号之后,不管执行到到程序的什么位置,都要暂停运行,去处理信号,处理完毕之后再继续执行原来的程序。与硬件中断类似,它是一种异步模式。但是信号是在软件层面上是实现的中断,早期通常被称为“软中断”。

每个进程收到的所有信号,都是由内核负责发送的

进程A给进程B发送信号示意图:

在这里插入图片描述


信号的状态

信号有三种状态:产生、未决和递达

信号的产生方式有一下几种情况:

1、按键产生,如Ctrl +cCtrl+zCtrl + \

2、系统调用产生,如killraiseabort

3、软件条件产生,如:定时器alarm

4、硬件异常产生,如非法访问内存(段错误)、除0(除浮点数例外)、内存对其出错(总线错误)

5、命令产生,如:kill命令

未决状态:产生和递达之间的状态。主要是由于阻塞(屏蔽)导致该状态

递达:递送并且到达进程


信号的处理方式

1、执行默认动作

2、忽略信号(丢弃不处理)

3、捕捉信号(调用用户自定义的处理函数)


信号的特质

信号的实现手段导致信号有很强的延时性,但对于用户来说,时间非常的短,不易于察觉。Linux内核的进程控制块PCB是一个结构体,task_struct,除了包含进程id,状态,工作目录,用户id,组id,文件描述符表,还包含了信号相关的信息,主要是指阻塞信号集和未决信号集


阻塞信号集和未决信号集

Linux内核的进程控制块PCB是一个结构体,这个结构体里面包含了信号相关的信息,主要是阻塞信号集和未决信号集。

1、阻塞信号集中保存的都是被当前进程阻塞的信号。若当前进程收到的是阻塞信号集中的某些信号,这些信号需要被暂时阻塞,不予处理。

2、信号产生之后由于某些原因(主要是阻塞)不能抵达,这类信号的集合称之为未决信号集。在屏蔽解除之前,信号一直处于未决状态;若是信号从阻塞信号集中解除阻塞,则该信号会被处理,并从未决信号集中去除


信号的四要素

1、 信号编号,使用kill -l命令可以查看当前系统有哪些信号,不存在信号编号为0的信号。其中1-31号信号称之为常规信号(也叫普通信号或者标准信号),34-64号信号称之为实时信号,驱动编程和硬件相关

2、信号的名称

3、信号产生的时间

4、信号的默认处理动作

  • Term: 终止进程
  • lgn: 忽略信号(默认即对该种信号忽略操作)
  • Core:终止进程,生成Core文件。(查验死亡原因,用于gdb调试)
  • Stop:停止(暂停)进程
  • Cont: 继续运行进程

几点说明:

1、SIGKILLSIGSTOP不能被捕获、不能被阻塞、不能被忽略

2、几个常用的信号: SIGINTSITQUITSIGKILLSIGSEGVSIGUSR1SIGUSR2SIGPIPESIGALRMSIGTERMSIGCHLDSIGSTOPSIGCONT


信号相关的函数

signal函数

函数作用:注册信号捕捉函数

函数原型:

typedef void(*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);

函数参数:

1、signum:信号编号

2、handler:信号处理函数


代码示例:

//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
}

int main()
{
	//注册信号处理函数
	signal(SIGINT, sighandler);

	//while(1)
	{
		sleep(10);
	}

	return 0;
}



kill函数/命令

描述:给指定的进程发送指定的信号

kill命令: kill -SIGKILL 进程PID

kill函数原型: int kill(pid_t pid ,int sig);

函数返回值值:成功返回0,失败返回-1,并设置errno

函数餐所说明:

1、sig信号参数:不推荐直接使用数字,应使用宏名,因为不同的操作系统信号编号可能不同,但是名称一致

2、pid参数

  • pid大于0:发送信号给指定的进程
  • pid=0:发送信号给与调用kill函数进程属于同一进程组的所有进程
  • pid < -1:取|pid|发送给对应进程组
  • pid = -1: 发送给进程有权限发送的系统中所有的进程

进程组的概念:每个进程都属于一个进程组,进程组是一个或者多个进程的集合,他们相互关联,共同完成一个实体任务,每个进程组都有一个进程组组长,默认进程组ID与进程组长ID相同。


代码示例:

//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
}

int main()
{
	//注册信号处理函数
	signal(SIGINT, sighandler);


	while(1)
	{
		sleep(1);
		kill(getpid(), SIGINT);
	}

	return 0;
}


raise函数

函数说明:给当前进程发送指定型号(自己给自己发)

函数原型:int raise(int sig);

函数返回值: 成功返回0,失败返回非0值

函数拓展:raise(signo)与函数kill(getpid(),signo)的功能相同


abort函数

函数描述:给自己发送异常终止信号 6) SIGABRT,并产生core文件

函数原型: void abort(void);

函数拓展: abort()与kill(getpid(),SIGABRT)的功能相同


代码示例:

//raise和abort函数测-
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
}

int main()
{
	//注册信号处理函数
	signal(SIGINT, sighandler);

	//给当前进程发送SIGINT信号
	raise(SIGINT);

	//给当前进程发送SIGABRT
	abort();

	while(1)
	{
		sleep(10);
	}

	return 0;
}


alarm函数

函数原型: unsigned int alarm(unsigned int seconds);

函数描述:设置定时器(闹钟)。在指定seconds后,内核会给当前进程发送14) SIGALRM信号。进程收到该信号,默认动作终止。每个进程都有且只有唯一的要给定时器。

函数返回值: 返回0或者剩余的秒数,无失败。

图解示意:

在这里插入图片描述

常用操作: 取消定时器alarm(0),返回旧闹钟余下的秒数

说明:

1、alarm使用的是自然定时法,与进程的状态无关,就绪,运行,挂起(阻塞、暂停)、终止、僵尸……,无论进程处于何种状态,alarm都计数

2、使用time命令查看程序执行的时间。程序运行的瓶颈在于I/O,优化程序,首先优化I/O

3、实际执行时间 = 系统时间 + 用户时间 + 损耗时间。其中损耗时间主要来自于文件的、I/O操作,I/O操作会有用户区到内核区的切换,切换的次数越多越耗时


代码示例:

//signal函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
}

int main()
{
	//注册信号处理函数
	signal(SIGINT, sighandler);
	signal(SIGALRM, sighandler);

	int n = alarm(5);
	printf("first: n==[%d]\n", n);

	sleep(2);
	n = alarm(5);
	//n = alarm(0); //取消时钟
	printf("second: n==[%d]\n", n);

	while(1)
	{
		sleep(10);
	}

	return 0;
}


setitimer函数

函数原型:

int setitimer(int which,const struct itimerval * new_value,struct itimerval * old_value);

函数描述:

设置定时器(闹钟),可代替alarm函数,精度微秒us,可以实现周期定时

函数返回值:成功返回0,失败返回-1,并设置errno

函数参数说明:

1、which:指定定时方式

  • 自然定时:ITIMER_REAL---->14) SIGALRM 计算自然时间
  • 虚拟空间计时(用户空间):ITIMER_VIRTUAL---->26) SIGVTALRM只计算进程占用cpu的时间
  • 运行时计时(用户+内核): ITIMER_PROF----> 27) SIGPROF计算占用cpu及执行系统调用的时间

2、new_value: struct itimerval,负责设定timeout时间

  • itimerval.it_value: 设定第一次执行function所延迟的秒数
  • itimerval.it_interval:设定以后每几秒执行function

结构体struct itimerval和结构体struct timeval说明

struct itimerval{
	struct timerval it_interval;  //闹钟出发周期
	struct timerval it_value;    //闹钟出发周期
}

struct timerval{
	long tv_sec;    //秒
	long tv_usec;   //微秒
}

3、old_value:存放旧的timeout值,一般指定为NULL


信号集相关

未决信号集和阻塞信号集的关系

阻塞信号集是当前进程要阻塞的信号的集合,未决信号集是当前进程中还处于未决状态的信号的结合,这两个集合存储在内核的PCB中。

示例:以SIGINT为例说明未决信号集和阻塞信号集的关系:

当进程收到一个SIGINT信号(信号编号为2),首先这个信号会保存在未决信号集合中,此时对应的2号编号的这个位置上置为1,表示处于未决状态;在这个信号需要被处理之前首先要在阻塞信号集中的编号为2的位置上去检查该值是否为1;

  • 如果为1,表示SIGINT信号被当前进程阻塞了,这个信号暂时不被处理,所以未决信号集上该位置上的值保持为1,表示该信号处于未决状态
  • 如果为0,表示SIGINT信号没有被当前进程阻塞,这个信号需要被处理,内核会对SIGINT信号进行处理(执行默认动作,忽略或者执行用户自定义的信号处理函数),并将未决信号集中编号为2的位置上1变为0,表示该信号已经处理了,这个时间非常短,用户感知不到

当SIGINT信号从阻塞信号集中解除阻塞之后,该信号就会被处理。

图解:

在这里插入图片描述


信号集相关函数

由于信号集属于内核的一块区域,用户不能直接操作内核空间,为此,内核提供了一些信号集相关的接口函数,使用这些函数用户就可以完成对信号集相关的操作。

信号集是一个能表示多个信号的数据类型,sigset_tset即一个信号集。既然是一个集合,就需要对集合进行添加、删除等操作。

结构体sigset_t的相关定义:

typedef _sigset_t sigset_t;

#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))

typedef struct
{
	unsigned long int _val[_SIGSET_NWORDS];
}_sigset_t;

具体相关函数:

1、int sigemptyset(sigset_t * set);

函数说明:将某个信号集清零

函数返回值:成功返回0,失败返回-1并设置errno

2、int sigfillset(sigset_t * set);

函数说明:将某个信号集置1

函数返回值:成功返回0,失败返回-1并设置errno

3、int sigaddset(sigset_t *set,int signum);

函数说明:将某个信号加入到信号集中

函数返回值:成功返回0,失败返回-1并设置errno

4、int sigdelset(sigset_t * set, int signum);

函数说明:将某个信号从信号集中删除

函数返回值:成功返回0,失败返回-1并设置errno

5、int sigismember(const sigset_t * set, int signum);

函数说明:判断某个信号是否在信号集中

函数返回值:成功返回0,失败返回-1并设置errno

6、sigprocmask函数

函数说明:用来屏蔽信号、解除屏蔽信号都是使用这个函数。其本质,读取或者修改进程控制块中的阻塞信号集

注意:屏蔽信号只是将信号处理延后执行(延至解除屏蔽);而忽略信号表示信号丢弃处理

函数原型: int sigprocmask(int how, const sigset_t * set,sigset_t * oldset);

函数返回值:成功返回0,失败返回-1并设置errno

函数参数说明:

a. how参数取值:假设当前信号屏蔽字为mask

  • SIG_BLOCK:当how设置为此值时,set表示需要屏蔽的信号,相当于mask = mask | set
  • SIG_UNBLOCK:当how设置为此值时,set表示需要解除屏蔽的信号。相当于mask = mask &~ set
  • SIG_SETMASK:当how设置为此,set表示用于替代原始屏蔽集的新的屏蔽集。相当于mask = set,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达。

b.set:传入参数,是一个自定义的信号集。由参数how来指示如何修改当前信号的屏蔽字

c.oldset:传出参数,保存旧的信号屏蔽字

7、sigpending函数

函数原型:int sigpending(sigset_t * set);

函数说明:读取当前进程的未决信号集

函数参数:set传出参数

函数返回值: 成功返回0,失败返回-1并设置errno


代码示例:

//信号集相关函数测试
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
}

int main()
{

	//注册SIGINT和SIGQUIT的信号处理函数
	signal(SIGINT, sighandler);
	signal(SIGQUIT, sighandler);

	//定义sigset_t类型的变量
	sigset_t pending, mask, oldmask;

	//初始化
	sigemptyset(&pending);
	sigemptyset(&mask);
	sigemptyset(&oldmask);

	//将SIGINT和SIGQUIT加入到阻塞信号集中
	sigaddset(&mask, SIGINT);
	sigaddset(&mask, SIGQUIT);

	//将mask中的SIGINT和SIGQUIT信号加入到阻塞信号集中
	//sigprocmask(SIG_BLOCK, &mask, NULL);
	sigprocmask(SIG_BLOCK, &mask, &oldmask);

	int i = 1;
	int k = 1;
	while(1)
	{
		//获取未决信号集
		sigpending(&pending);	

		for(i=1; i<32; i++)
		{
			//判断某个信号是否在集合中
			if(sigismember(&pending, i)==1)	
			{
				printf("1");
			}
			else
			{
				printf("0");	
			}
		}
		printf("\n");

		if(k++%10==0)
		{
			//从阻塞信号集中解除对SIGINT和SIGQUIT的阻塞
			//sigprocmask(SIG_UNBLOCK, &mask, NULL);	
			sigprocmask(SIG_SETMASK, &oldmask, NULL);	
		}
		else
		{
			sigprocmask(SIG_BLOCK, &mask, NULL);	
		}

		sleep(1);
	}

	return 0;
}



信号捕捉函数

1、signal函数:注册一个信号捕捉函数

2、sigaction函数:

函数说明:注册一个信号处理函数

函数原型:intsigaction(int signum,const struct sigaction * act,struct sigaction * oldact);

函数参数:

  • signum:捕捉的信号
  • act:传入的参数,新的处理方式
  • oldact:传出参数,旧的处理方式

结构体struct sigaction说明:

struct sigaction{

	void (*sa_handler)(int);  //信号处理函数
	void (*sa_sigaction)(int,siginfo_t*,void*);  //信号处理函数
	sigset_t sa_mask;  //信号处理函数执行期间需要阻塞的信号
	int sa_flags; //通常为0,表示使用默认标识
	void (*sa_resorer)(void);
}

总结:

1、sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可以赋值为SIG_IGN表示忽略或者SIG_DFL表示执行默认动作

2、sa_mask:用来指定在信号处理函数执行期间需要被屏蔽的信号,特别是当某个信号被处理时,把自身会被自动放入进程的信号掩码,因此在信号处理函数执行期间这个信号不会再次发生。注意:仅在处理函数被调用期间生效,是临时性设置。

3、sa_flags:通常设置为0,使用默认属性

4、sa_restorer:已经不再使用


代码示例:

//sigaction函数测试---注册信号处理函数
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>

//信号处理函数
void sighandler(int signo)
{
	printf("signo==[%d]\n", signo);
	sleep(4);
}

int main()
{
	//注册信号处理函数
	struct sigaction act;
	act.sa_handler = sighandler;
	sigemptyset(&act.sa_mask);  //在信号处理函数执行期间, 不阻塞任何信号
	sigaddset(&act.sa_mask, SIGQUIT);
	act.sa_flags = 0;
	sigaction(SIGINT, &act, NULL);

	
	signal(SIGQUIT, sighandler);	

	while(1)
	{
		sleep(10);
	}

	return 0;
}

知识点补充

信号不支持排队

1、在A信号处理函数执行期间,A信号是被阻塞的,如果该信号产生了多次,在A信号处理函数结束之后,该A信号只会被处理一次

2、在A信号处理函数执行期间,如果阻塞了B信号,若B信号产生了多次,当A信号处理函数结束之后,B信号只会被处理一次

内核实现信号捕捉的过程

如果信号的处理函数是用户自定义的函数,在信号递达的时候就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间,处理过程比较复杂,举例说明:

1、用户注册了SIGQUIT信号的处理函数sighandler

2、当前正在执行main函数,这时候发生中断或者异常切换到内核态

3、在终端处理完毕之后要返回到用户态的main函数之前检查到有信号SIGQUIT递达

4、内核决定返回用户态之后不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandler和main函数使用不同的堆栈空间,它们之间不存在调用和被调用关系,是两个独立的控制流程

5、sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态

6、如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了


图解示例:

在这里插入图片描述


SIGCHLD信号

产生SIGHLD信号的条件

1、子进程结束的时候

2、子进程收到SIGSTOP信号

3、当子进程停止时,收到SIGCONT信号

SIGCHLD信号的作用

子进程退出后,内核会给它的父进程发送SIGCHLD信号,父进程收到这个信号以后可以对子进程进行回收。

使用SIGCHLD信号完成对子进程的回收可以避免父进程阻塞等待而不能执行其他操作,只有当父进程收到SIGCHLD信号之后才去调用信号捕捉函数完成对子进程的回收,未收到SIGCHLD信号之前可以处理其他操作。

使用SIGCHLD信号完成对子进程的回收

代码示例:父进程创建三个子进程,然后父进程捕获SIGCHLD信号完成对子进程的回收

注意点:

1、有可能还未完成信号处理函数的注册三个子进程都退出了

解决办法:可以在fork之前先将SIGCHLD信号阻塞,当完成信号处理函数的注册后在解除阻塞

2、当SIGCHLD信号函数处理期间,SIGCHLD信号若再次产生是被阻塞的,而且若产生了多次,则该信号只会被处理一次,这样可能会产生僵尸进程

解决办法:可以信号处理函数里面使用while(1)循环回收,这样就有可能出现捕获一次SIGCHLD信号但是回收了多个子进程的情况,从而可以避免僵尸进程的产生

代码实现:

//父进程使用SICCHLD信号完成对子进程的回收
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void waitchild(int signo)
{
	pid_t wpid;

	//回收子进程
	while(1)
	{
		wpid = waitpid(-1, NULL, WNOHANG);
		if(wpid>0)
		{
			printf("child is quit, wpid==[%d]\n", wpid);
		}
		else if(wpid==0)
		{
			printf("child is living, wpid==[%d]\n", wpid);
			break;
		}
		else if(wpid==-1)
		{
			printf("no child is living, wpid==[%d]\n", wpid);
			break;
		}
	}
}

int main()
{
	int i = 0;
	int n = 3;

	//将SIGCHLD信号阻塞
	sigset_t mask;
	sigemptyset(&mask);
	sigaddset(&mask, SIGCHLD);
	sigprocmask(SIG_BLOCK, &mask, NULL);

	for(i=0; i<n; i++)	
	{
		//fork子进程
		pid_t pid = fork();
		if(pid<0) //fork失败的情况
		{
			perror("fork error");
			return -1;
		}
		else if(pid>0) //父进程
		{
			printf("father: fpid==[%d], cpid==[%d]\n", getpid(), pid);
			sleep(1);
		}
		else if(pid==0) //子进程
		{
			printf("child: fpid==[%d], cpid==[%d]\n", getppid(), getpid());
			break;
		}
	}

	//父进程
	if(i==3)
	{
		printf("[%d]:father: fpid==[%d]\n", i, getpid());
		//signal(SIGCHLD, waitchild);
		//注册信号处理函数
		struct sigaction act;
		act.sa_handler = waitchild;
		sigemptyset(&act.sa_mask);
		act.sa_flags = 0;
		sleep(5);
		sigaction(SIGCHLD, &act, NULL);

		//解除对SIGCHLD信号的阻塞
		sigprocmask(SIG_UNBLOCK, &mask, NULL);

		while(1)
		{
			sleep(1);
		}
	}

	//第1个子进程
	if(i==0)
	{
		printf("[%d]:child: cpid==[%d]\n", i, getpid());
		//sleep(1);
	}

	//第2个子进程
	if(i==1)
	{
		printf("[%d]:child: cpid==[%d]\n", i, getpid());
		sleep(1);
	}

	//第3个子进程
	if(i==2)
	{
		printf("[%d]:child: cpid==[%d]\n", i, getpid());
		sleep(1);
	}

	return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值