第10章——信号

信号是由用户、系统或进程发送给目标进程的信息,以通知目标进程某个状态的改变或者系统异常。
Linux的信号可由如下条件产生:

  • 用户可以通过输入特殊的终端字符给前台进程发送信号,如Ctrl + C发送中断信号。
  • 系统异常。比如浮点异常和非法内存段访问
  • 系统状态变化。比如alarm定时器到期引起的SIGALRM信号
  • 运行kill命令或者调用kill函数
    服务器应该及时处理或忽略常见信号,以避免异常终止。

Linux信号概述

发送信号

#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid, int sig);

该函数将信号sig发送给目标进程,目标进程由pid指定
在这里插入图片描述
sig是信号值,如果设置为0,则不发送任何信号。但将其设置为0可以检测目标进程或进程组是否存在,因为会在信号发送前执行检查操作。但是这种方法并不可靠:①进程的PID回绕,导致被检测的PID不是期望的PID。②这种检测方法不是原子性的

执行成功返回0,失败返回-1,常见的errno值:

  • EINVAL 无效信号
  • EPERM 该进程没有权限发送信号给其他进程
  • ESRCH 目标进程或进程组不存在

信号处理方式

#include<signal.h>
// 函数原型
typedef void (*__sighandler_t) (int); 

#include<bits/signum.h>
#define SIG_DFL ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)

信号处理函数只有一个带整形参数来指示信号类型。信号处理函数应该是可重入的,以避免竞态条件,所以在信号处理函数中禁用不安全代码。

除了用户自定义信号处理函数,还有两个宏(如上所示)SIG_IGN表示忽略该信号,SIG_DFL表示使用信号的默认处理方式——结束进程(Term)、忽略信号(Ign),结束进程并生成核心转储文件(Core),暂停进程(Stop),继续进程(Cont)

Linux信号

定义在bits/signum.h中,常用的信号:SIGHUP, SIGPIPE, SIGURG, SIGALRM, SIGCHLD
在这里插入图片描述

在这里插入图片描述

中断系统调用

在这里插入图片描述

信号函数

signal

signal系统调用可以为一个信号设置处理函数

#include<signal.h>
_sighandler_t signal (int sig, _sighandler_t _handler);

·sig参数指定要捕获的信号类型,_handler参数是_sighandler_t 类型的函数指针,用于指定信号sig的处理函数。
signal调用成功时返回一个函数指针,是前一次调用signal函数时传入的函数指针或者是信号sig对应的默认处理函数指针SIG_DEF(如果是第一次调用signal的话)。
调用失败则返回SIG_ERR

sigaction

设置信号处理函数更健壮的接口是如下的系统调用

#include<signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oact);

sig参数指定信号类型,act参数指定新的信号处理方式,oact输出信号之前的处理方式。

struct sigaction{
#ifdef __USE_POSIX199309
	union   
    {
   // 指定信号处理函数
   __sighandler_t sa_handler;
   
   void (*sa_sigaction) (int, siginfo_t *, void *);
    }
   __sigaction_handler;
# define sa_handler __sigaction_handler.sa_handler
# define sa_sigaction      __sigaction_handler.sa_sigaction
#else
   __sighandler_t sa_handler;
#endif

   // 设置进程的信号掩码,以指定哪些信号不能发送给本进程
   __sigset_t sa_mask;

   // 设置程序接收到信号时的行为
   int sa_flags;

   /* Restore handler.     */
   // 不再使用
   void (*sa_restorer) (void);
};

在这里插入图片描述

Example

void sig_urg(int sig){
	// 为了保证可重入性,保存errno
	int save_err = errno;
	// 对信号的处理
	return save_err;
}

// 绑定注册信号
void addsig(int sig, void (*sig_handler)(int)){
	struct sigaction sa;
	memset(&sa, '\0', sizeof(sa));
	sa.sa_handler = sig_handler;
	sa.sa_flags |= SA_RESTART;
	sigfillset(&sa.sa_mask);
	assert(sigaction(sig, &sa, NULL) != -1);
}

int main(){
	// 将信号SIGURG绑定到信号处理函数sig_urg上
	addsig(SIGURG, sig_urg);
}

void sig_handler(int sig){
	// 为了保证可重入性,保存当前errno
	int save_err = errno;
	int msg = sig;
	// 将信号写入管道,以通知主循环
	send(pipefd[1], (char*)&msg, 1, 0);
	errno = save_err;
}

// 设置信号处理函数
void addsig(int sig){
	struct sigaction sa;
	memset(&sa, '\0', sizeof(sa));
	sa.sa_handler = sig_handler;
	sa.sa_flags |= SA_RESTART;
	sigfillset(&sa.sa_mask);
	assert(sigaction(sig, &sa, NULL) != -1);
}

信号集

信号集函数

Linux使用sigset_t表示一组信号

#include<bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigined loong int)));
typedef struct{
	unsigned long int __val(_SIGSET_NWORDS);
} __sigset_t;

sigset_t实际上是一个长整型数组,数组的每个元素的每个位表示一个信号。这种定义方式和文件描述符fd_set相似。 操作信号集的函数:

#include<signal.h>
int sigemptyset(sigset_t* _set); // 清空信号集
int sigfillset(sigset_t* _set); // 在信号集中设置所有信号
int sigaddset(sigset_t* _set, int _signo); // 将信号添加到信号集中
int sigdelset(sigset_t* _set, int _signo); // 在信号集中删除信号
int int sigismember(sigset_t* _set, int _signo); // 检测是否在信号集中

进程信号掩码

我们可以利用sigaction结构体的sa_mask字段来设置进程的信号掩码,如下函数可以用于设置或查看进程的信号掩码。

#include<signal.h>
int sigprocmask(int _how, _const sigset_t* _set, sigset_t* _oset);

_set参数指定新的信号掩码,_oset输出保存以前的旧的信号掩码(与set_nonblocking里面return oldopt一个意思),_how参数指定设置进程信号掩码的方式
在这里插入图片描述

被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能再被接收。如果向进程发送一个被屏蔽的信号,则操作系统将该信号设置为进程的一个被挂起的信号。如果我们取消对该信号的屏蔽,它就可以被立即接收到。该函数可以查看被挂起的信号集
int sigpending(sigset_t* set);
set参数保存被挂起的信号集。
进程多次接收到同一个被挂起的信号,其函数也只能反映一次。

统一事件源

信号是异步事件,信号处理函数和程序的主循环是两条不同的执行路线。
信号在处理期间,系统不会再触发它。所以在执行信号处理函数时,应该尽快执行完毕,以确保该信号不会被屏蔽。

一般的解决方案:统一处理信号

将信号的主要处理逻辑放在程序的主循环里,当信号处理函数被触发时,他只是通知主循环程序接收到的信号的值,主循环再根据信号值处理相关的逻辑。
信号处理函数可以通过管道将信号传递给主循环,主循环通过IO复用来监听管道的读端上是否有读就绪事件。

关键函数:

void addfd(int epollfd, int fd){
	epoll_event event;
	event.data.fd = fd;
	event.events = EPOLLIN | EPOLLET;
	epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
	setnonblocking(fd);
}

// 信号处理函数
void sig_handler(int sig){
	// 为了保证可重入性,保存当前errno
	int save_err = errno;
	int msg = sig;
	// 将信号写入管道,以通知主循环
	send(pipefd[1], (char*)&msg, 1, 0);
	errno = save_err;
}

// 设置信号处理函数
void addsig(int sig){
	struct sigaction sa;
	memset(&sa, '\0', sizeof(sa));
	sa.sa_handler = sig_handler;
	sa.sa_flags |= SA_RESTART;
	sigfillset(&sa.sa_mask);
	assert(sigaction(sig, &sa, NULL) != -1);
}

// 创建管道,注册pipefd[0]上的可读事件
{
	ret = socketpair(AF_UNIX, SOCK_STREAM, 0, pipefd);
	assert(ret != -1);
	setnonblocking(pipefd[1]);
	// 注册读事件
	addfd(epollfd, pipefd[0]);
}

// 接收并处理信号
{
epoll_event events[MAX_EVENT_NUM];
int number = epoll_wait(epollfd, events, MAX_EVENT_NUM, -1);
for(int i = 0; i < number; i++){
	int fd = events[i].data.fd;
	if(fd == pipefd[0] && events[i].events & EPOLLIN){
		int sig;
		char signals[1024];
		ret = recv(fd, signals, sizeof(signals), 0);
		if(ret <= 0){
			// 处理
		}else{
			// 接收到了一个或多个信号,一个信号1B
			for(int j = 0; j < ret; j++){
				switch(signals[j]){
					case SIGCHLD:
					//
					case SIGHUP:
					//
					case SIGTERM:
					//
					case SIGINT:
					//
				}
			}
		}
	}
}
}

网络编程中常用的信号

SIGHUP

当挂起进程的控制终端时,SIGHUP信号将被触发。
对于没有控制终端的网络后台程序,常利用其强制服务器重新读取配置文件。

SIGPIPE

往一个读端关闭的管道或者socket连接中写数据将引发SIGPIPE信号。程序接收到SIGPIPE的默认行为是结束程序,所以程序员一般需要捕获并处理该信号,或者忽略其。引起SIGPIPE信号的写操作将设置errno为EPIPE

SIGURG

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值