--信号--

本文详细介绍了Linux信号的概念,包括信号响应过程、常用信号及其术语。讲解了signal、sigaction等信号处理函数,以及信号集的管理。同时,通过实例分析展示了如何通过统一事件源来处理信号,实现主函数与信号处理的同步,确保程序的高效运行。
摘要由CSDN通过智能技术生成

前言

信号是软件中断(但不是中断,可以认为信号的响应依赖于中断)。它提供了一种处理异步事件的方法。Linux 信号可由如下条件产生:

  1. 命令行输入中断键或kill命令,例如ctrl+c中断前台进程
  2. 硬件异常,比如非法内存访问,除以0
  3. 通过函数设置产生信号(kill函数)

一、信号概念

信号都有一个名字都以SIG开头(kill -l查看),当某个信号出现时,进程可以告诉内核用下列三种方式来执行。
1、忽略信号。
2、捕捉信号。
3、执行默认动作,大多数信号的默认动作是终止该进程。

信号的响应过程

进程由于系统调用或者中断进入内核,完成相应任务返回用户空间之前,会检查有无信号到达,如果没有就返回用户态恢复上下文数据后继续执行程序;如果有信号,也会返回用户态,但这时会执行信号处理函数,执行完毕后,再次返回内核态,循环。
信号是在内核态回到用户态的路上被响应的。

常用信号

SIGALRM 调用alarm函数设置的定时器超时时,产生此信号。
SIGCHLD 在子进程终止时,会产生此信号返回给父进程。
SIGINT 当用户按中断键时,终端将产生此信号发生给前台进程。
SIGPIPE 在管道读的进程已经终止时还往管道写将产生此信号。
SIGTERM 此信号由kill命令发出的,意义是系统默认终止。

信号术语

捕捉信号:如果信号的处理动作是用户自定义的函数,在信号抵达时就调用这个函数,这个动作称为捕捉信号。

阻塞信号:不忽略该信号,在其发生时记住它,在进程做好准备后再通知它。

pending:在信号产生和递送之间,称之为pending(未决的)。如果有一个阻塞的信号,且内核对该信号不忽略,则该信号就是pending的,直到解除阻塞或者忽略该信号。调用sigpending函数能获得pending状态的信号集。

可重入函数:产生信号后,当前进程会立马调用信号处理函数handler,假设当前进程在malloc时信号到来,handler中也调用了malloc,这时候就会出错。所以规定handler中必须调用安全的函数,称为可重入函数,且这些函数会阻塞任何引起不一致的信号。

信号屏蔽字:规定了当前要阻塞递送到该进程的信号集。

二、信号函数

signal

为一个信号设置处理函数

#include <signal.h>
typedef void (*sighandler_t)(int);//不懂的可以搜一下typedef函数指针
sighandler_t signal(int signum, sighandler_t handler);

--signum 信号名
--handler 是常量SIG_IGN,就代表忽略该信号
		  是常量SIG_DFL,就代表执行默认动作
		  是函数,则在信号发生时调用该函数,也就是捕捉信号(信号处理函数)
成功返回一个函数指针,指向前一次调用signal传入的handler函数指针,失败返回SIG_ERR

sigaction

signal 函数的使用方法简单,但并不属于 POSIX 标准,在各类 UNIX 平台上的实现不尽相同,因此其用途受到了一定的限制。而POSIX 标准定义的信号处理接口是 sigaction 函数,其接口头文件及原型如下:

#include <signal.h>

int sigaction(int signum, const struct sigaction *act,struct sigaction *oldact);
--signum 信号名 
--act 指定新的处理方式
--oldact 若不为空,则输出信号之前的处理方式
成功返回0,失败返回-1并设置errno

struct sigaction {
    void (*sa_handler)(int);
    //作用等同于signal函数中的handler
    
    void (*sa_sigaction)(int, siginfo_t *, void *);
    
    sigset_t sa_mask;
    /*在处理该signum时屏蔽此信号集中的信号,设置信号掩码
    注意这是一个信号集,使用前应当sigemptyset*/
    
    int sa_flags;
    /*处理信号时的其他相关操作
    常用的有SA_RESTART:被此信号中断的系统调用会自动重启
    解释一下:如果程序在执行【处于阻塞状态的】系统调用时接收到信号,
    并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,
    并且errno被设置为EINTR。
    */
 
    void (*sa_restorer)(void);
}

信号集

信号集是一个能表示多个信号的数据类型,本质上是一个数组。数组的每个位代表了一个信号

 #include <signal.h>
int sigemptyset(sigset_t *set);
//初始化由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);
//检测信号集中是否存在signum这个信号

sigprocmask

检查或改变进程的信号掩码(信号屏蔽字)

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
-- how
	SIG_BLOCK:设置新的(Set中的)信号掩码,与原先的信号掩码相并
	SIG_UNBLOCK:为(Set中的)信号解除阻塞
	SIG_SETMASK:为该进程设置新的(Set中的)信号掩码
-- set
	新的信号屏蔽字由参数set(非空)指定
--oldset
	如果oset不为空,则把当前进程的信号屏蔽字保存到oset中
成功返回0,失败返回-1并设置errno

sigprocmask(SIG_UNBLOCK,&set,&saveset);
//对set里的信号屏蔽字解阻塞,把之前信号屏蔽字的状态保存到saveset

sigprocmask(SIG_BLOCK,&set,&oldset);
//对set里的信号屏蔽字加阻塞,把之前的状态保存到oldset中

sigpromask(SIG_SETMASK,&oldset,NULL);
//恢复了上一次的状态,也就是第一次调用sigpromask后的状态

sigpromask(SIG_SETMASK,&saveset,NULL);
//恢复到最开始的状态,第一次调用sigpromask之前的状态

sigpending

返回由pending信号构成的pengind集

#include <signal.h>

int sigpending(sigset_t *set);
set--用于保存返回的信号集
成功返回0,失败返回-1并设置errno

sigsuspend

进程的信号屏蔽字替换为由参数mask指向的信号集。在捕捉到一个信号或发生一个会终止该进程的信号前,该进程会被阻塞。可以简单看作是解除信号屏蔽字的阻塞和pause的原子操作。

#include <signal.h>
int sigsuspend(const sigset_t *mask);
/*进程的信号屏蔽字替换为由参数mask指向的信号集。
此函数无成功返回值,返回-1,并将errno设置为EINTR。*/

三、实例分析——统一事件源

信号是异步事件,信号处理函数handler和主函数是两条不一样的执行路线,为了不让主函数等待很久,所以加快执行的速度。

解决方法:把信号的主要处理逻辑放到程序的主循环中,当调用handler时,通过管道把信号值传递给主函数,主函数根据信号值执行目标信号对应的逻辑代码。

handler往管道的写端写信号值,主函数则从管道的读端读出该信号值。那么主函数怎么知道管道上何时有数据可读呢?这很简单,我们只需要使用I/O复用来监听管道的读端文件描述符上的可读事件。这样,信号事件就能和其他I/O事件一样被处理,称为统一事件源
下面给出伪码

#include <头文件>
//主函数
int main(int argc,char *argv[])
{
	1建立监听套接字
	
	int listenfd=socket(PF_INET,SOCK_STREAM,0);
	struct sockaddr_in serv_addr;
	serv_addr.sin_family...
	serv_addr.sin_port....
	serv_addr.sin_addr...
	bind(listenfd,(sockaddr*)serv_addr,sizeof(serv_addr));
	listen(listenfd,listenfd_size);
	
	2创建管道--用socketpair来创建全双工的通信方式
	//使用socketpair函数能够创建一对套节字进行进程间通信
	/*
	#include <sys/types.h>          
    #include <sys/socket.h>
	int socketpair(int domain, int type, int protocol, int sv[2]);
	
	domain:协议族
	type:协议
	protocol:表示类型
	sv[2]:套节字柄对,该两个句柄作用相同,均能进行读写双向操作
	成功返回0,-失败返回-1并设置errno
	*/
	socketpair(PF_INET,SOCK_STREAM,0,pipefd);
	//pipefd[0]、pipefd[1]都能进行双向读写操作
	
	SetNonBlock(pipefd[1]);/*对此描述符实现非阻塞,调用fcntl函数*/
	
	3将listenfd和管道的读端fd加入监听
	
	addfd(epfd,listenfd);
	addfd(epfd,pipefd[0]);
	/*
	int epfd=epoll_create(1);
	struct epoll_event ev;
	ev.fd=fd;
	ev.events|=EPOLLIN;
	epoll_ctl();
	SetNonBlock(fd);//设置下非阻塞
	*/
	
	4为信号设置处理方式
	addsig(SIGINT);
	/*
	addsig(int signum){
	struct sigaction sa;
	memset(&sa,'\0',sizeof(sa));
	sa.sa_handler=handler;
	sa.sa_flags|=SA_RESTRT;
	sigfillset(sa.sa_mask);
	sigaction(signum,&sa,NULL);
	}
	*/
	
	/*信号处理函数
	void handler(int sig)
	{
		int signal=sig;
		//将信号值写入管道
		send(pipefd[1],&signal,sizeof(signal),0);
	}
	*/
	while(1)
	{
		5监听
		struct epoll_event events[MAX_EVENTS];
		int nread=epoll_wait(epfd,events,MAX_EVENTS,-1);
		
		for(int i=0;i<nread;i++)
		{
			int fd=events[i];
			if(fd==listenfd)
			{
				/*处理新连接请求*/
				int clntsock=accept(...);
				/*加入监听*/
				addfd(epfd,clntsock);
			}
			else if(fd==pipefd[0])//管道有信号传来,处理该信号
			{
				char signals[size]
				int read_len=recv(pipefd[0],msg,sizeof(msg),0);//接收信号
				//信号值占一个字节,通过for循环遍历,用switch case来处理每个信号
				for(int i=0;i<read_len;i++)
				{
					switch(signals[i])
					{
						case SIGINT
						...
						break;
						case ...
						...
						break;
					}
				}
			}
		}
	}
	close(listenfd);
	close(pipefd[0]);
	close(pipefd[1]);
	exit(0);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值