信号 ——《Linux高性能服务器编程》第10章——读书笔记

本文深入介绍了Linux信号的概念,包括信号作为信息载体的特性,信号的状态(递达、未决、阻塞)及其转换。详细阐述了信号集(阻塞信号集和未决信号集)的管理以及如何通过sigaction和signal函数处理信号。此外,还讲解了几个与网络编程相关的信号,如SIGHUP、SIGPIPE和SIGURG。最后,提到了alarm函数用于设置超时处理。通过对信号的全面理解,有助于提升Linux系统编程的能力。
摘要由CSDN通过智能技术生成

1、概述

在这里插入图片描述

2、信号的一些概念

2.1 什么是信号?

信号是信息的载体,他能在进程间传递一个信息,这个信息极其简单,而且只有满足特定条件才能被发送。

A给B发送信号,B收到信号之前执行自己的代码,收到信号后,不管执行到什么位置,都要暂停运行,去处理信号,处理完毕再继续执行,这叫做中断。

学过嵌入式的应该都知道中断,单片机里的中断主要由时钟(硬件)触发并实现,也叫时钟中断。但是信号中的中断不太一样,这里的中断是由软件方法实现的,也称为软中断,这种实现方法导致信号由很强的延时性。但对于人来说,这个延时非常短,基本察觉不到。

上面说是A给B发送信号,其实A是请求Linux内核产生信号并发给B,而B处理信号,其实也是B请求内核进行处理。所以严禁来说,A不能给B发信号。

2.2 信号的状态

递达: 执行信号的处理动作叫做信号抵达
未决: 信号从产生到递达之间的状态,称为信号未决
阻塞: 信号产生时,保持未决状态,当进程解除对此信号的阻塞,才会执行抵达动作
(阻塞和忽略不同,忽略是一种信号处理动作,而阻塞的信号还没到处理那一步呢)

2.3 两个信号集

阻塞信号集(信号屏蔽字): 是当前进程要阻塞的信号的集合(信号可以还没有产生)
未决信号集: 当前进程中还处于未处理状态的信号的集合(信号必须存在)。未决的信号又称为被挂起的信号

这两个信号集使用set存储在内核的PCB中。下面详细说明这两个信号集的联系。

(下面描述的例子适用于任何信号,为方便描述,我们用SIGINT信号举例)
只要产生一个SIGINT信号(信号编号为2),未决信号集中对应的2号编号的位置上值为1,表示目前处于未决状态;在这个信号要被处理之前,需要查看阻塞信号集中的编号为2的位置上是否为1:
———如果为1,表示SIGINT信号被当前进程阻塞了,所以暂不处理,未决信号集上的该位置只能还保持1,当解除阻塞后,该信号被处理
———如果为0,表示SIGINT信号没被当前进程阻塞,需要进行处理,完成处理后未决信号集上该位置0,表示已经处理了。如果是这种情况,信号的产生到处理一帆风顺,其未决信号集由1到0的改变是极快的。

综上,未决信号集上的状态位一定程度上由阻塞信号集上的状态位决定,不过前提是产生了该信号,未决信号集的状态位只有在信号存在时才有意义。

2.4 处理信号

执行默认处理
忽略: 丢弃,注意:这不是不处理,忽略动作也会使未决信号集的状态位置0
捕捉: 调用用户处理函数

所有信号的默认处理动作共有五种:

Term	结束进程
Ign		忽略信号(如子进程死亡触发 SIGCHLD 信号,父进程收到该信号进行忽略处理,然后继续运行)
Core	结束进程并生成核心传输文件(用于检查进程死亡原因,用于 gdb 调试)
Stop	暂停进程
Cont	继续进程

2.5 信号4要素

每个信号都包含下面四个属性:
——信号名
——信号编号
——信号默认处理方式
——信号所对应的事件

3、Linux信号概述

3.1 发送信号

使用kill给其他进程发送信号,其定义如下:

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

该函数吧信号sig发送到进程pid

参数说明:
  • pid可能的取值如下:
    在这里插入图片描述

  • 对于sig
    Linux定义的信号值都大于0,如果sig取值为0,kill不发送任何信号,但这种情况调用kill可用来检测目标进程或进程组是否存在,因为检查工作在信号发送前执行。

    不过这种检测方式是不靠谱的,一方面由于进程PID的回绕,可能导致被检测的PID不是我们期望的进程的PID;另一方面,这种检测方法不是原子操作。

返回值:

成功返回0,失败返回-1并设置errnoerrno的可能值如下:
在这里插入图片描述

3.2 处理信号

目标信号收到信号时,要定义接收函数处理他。其原型如下:

#include<signal.h>
typedef void (*__sighandler_t)(int);

这是个自定义的函数指针,只含一个整型参数,该参数指明信号类型。信号处理函数应该是可重入的,否则容易引发竞态条件。在信号处理函数中严禁定义不安全的函数。

除了自定义信号处理函数,bits/signum.h头文件中定义了信号的两种其他处理方式:SIG_IGNSIG_DFL

#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	继续进程

3.3 Linux 信号

Linux 的可用信号都定义在 bits/signum.h头文件中,其中包括标准信号 和 POSIX实时信号。我们仅讨论标准信号,如下所示:
在这里插入图片描述
在这里插入图片描述

3.4 中断系统调用

某些信号可以在特定情况下中断系统调用。比如:程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用被中断,并且errno被设置为EINTR。我们可以使用sigaction函数(后面讲)为信号设置SA_RESTART标志,从而自动重启被该信号中断的系统调用。

对于默认行为是暂停进程的信号(比如SIGSTOPSIGTTIN),如果我们没有为他们设置信号处理函数,则它们也可以中断某些系统调用(比如:connectepoll_wait)。这是Linux独有的,POSIX没有规定这种行为。

4、信号函数

4.1 signal系统调用

signal能够为一个信号注册处理函数:

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

sig参数是想要注册的信号,_handler参数是函数指针,他指向该信号的处理函数。

signal函数的行为是:给某个信号注册一个处理函数。真正捕捉信号的是Linux内核,内核捕捉到了该信号以后,调用我们注册的函数。所以,我们实际上不会主动调用_handler函数,这种 定义了的,但代码里没调用过的函数成为回调函数。

返回值说明

signal函数成功返回函数指针,他指向上一次给该信号注册的处理函数(也就是上一次调用signal时传入的_handler参数)。如果是第一次调用signal,则返回信号对应的默认处理函数指针SIG_DEF

signal调用失败返回SIG_ERR,并设置errno

4.2 sigaction系统调用

sigaction是注册信号处理函数的更健壮的接口:

#include<signal.h>
int sigaction(int sig, const struct sigaction* act, struct sigaction* oldact);
参数说明:
  • sig仍然是想要注册的信号
  • act参数是结构体指针,指向的结构体描述了该信号的处理方式
  • oldact也是结构体指针,指向该信号先前的处理方式(如果不为NULL的话)。

sigaction结构体定义如下:

struct sigaction
{
	void 	 (*sa_handler)(int);	老朋友,指向信号处理函数的指针
	void 	 (*sa_sigaction)(int, siginfo_t*, void*);	这个参数不用管
	sigset_t sa_mask;
	int 	 sa_flags;
	void 	 (*sa_restorer)(void);	该参数已被废弃,不用管
};

sa_mask 是一个信号集,该信号集指定当前信号被处理期间所阻塞的信号集。也就是当你处理该信号处理了一半时,有其他的信号来了,是去套娃处理其他的信号呢,还是先阻塞其他信号,继续处理该信号。
注:该阻塞信号集仅在处理函数执行期间生效。

sa_flags 设置程序收到信号时的行为,其值如下所示:
在这里插入图片描述
如果传入sa_flags的值为0,表示使用默认行为。默认行为是:当来了一个该信号,那么使用处理函数处理它,处理的途中,又来了一个该信号,那么不管前面的sa_mask里面有没有该信号,该信号都将被阻塞。直到第一个来的该信号处理完毕。

返回值说明:

成功返回0,失败返回-1并设置errno

5、信号集

5.1 信号集

前面经常出现sigset_t类型的对象,他就是信号集,其定义如下:

#include<bits/sigset.h>
#define _SIGSET_NWORDS (1024 / (8 * sizeof(unsigned long int)))
typedef struct
{
	unsigned long int __val[_SIGSET_nwords];
}sigset_t;

有定义可知,sigset_t实际上是一个长整型数组,数组每个元素的每一位表示一个信号。这种定义方式可以类比文件描述符集fd_set

5.2 操作信号集的函数

Linux提供如下函数置1,清0,或查询信号集:

#include<signal.h>
int sigemptyset(sigset_t* set);						将 set 的所有位全部清0,成功返回0,失败返回-1
int sigfillset(sigset_t* set);						将 set 的所有位全部置1,成功返回0,失败返回-1
int sigaddset(sigset_t* set, int signum);			将 set 的 signum 置1,即加入 signum,成功返回0,失败返回-1
int sigdelset(sigset_t* set, int signum);			将 set 的 signum 清0,即删除 signum,成功返回0,失败返回-1
int sigismember(const sigset_t* set, int signum);	查询 set 中的是否存在 signum,存在返回1,不存在返回0,错误返回-1

但是这些函数操作的仅仅是我们自己定义出来的信号集,和PCB中的阻塞信号集、未决信号集完全没有关系。不过,未决信号集由阻塞信号集来决定。那么问题就变成了:我们怎么用自己的信号集去影响阻塞信号集?

即答:使用如下函数:

#include<signal.h>
int sigprocmask(int how, const sigset_t* set, sigset_t* oldset);

该函数能供通过我们自己的信号集去改变阻塞信号集,既能用于阻塞信号,也能解除阻塞。

参数说明:

set
set就是我们自己的信号集,信号集中哪位为1,就表示想要更改阻塞信号集中的哪个信号的值。至于具体改成0还是1,要看第一个参数how

how
该参数仅有三种取值:

SIG_BLOCK
SIG_UNBLOCK
SIG_SETMASK

第一个宏表示,把我们的信号集里面的信号设置为阻塞,即阻塞信号集里的相应位置1。
第二个宏表示,把我们的信号集里面的信号解除阻塞,即阻塞信号集里的响应为清0。
第三个宏表示,直接用我们的信号集 覆盖阻塞信号集。一般不推荐用这个。

oldset
是个传出参数,保存旧的阻塞信号集(如果不为NULL的话)。

返回值说明:

成功返回0,失败返回-1并设置errno

5.3 读取未决(挂起)信号集

既然未决信号集由阻塞信号集决定,那么我们也就没有必要改变未决信号集。那么总有办法读到未决信号集里的内容吧!

还真有:

int sigpending(sigset_t* set);

该函数很简单,就是把未决信号集读到传出参数set中。成功返回0,失败返回-1并设置errno
在这里插入图片描述

在这里插入图片描述

6、统一事件源

统一事件源: 信号事件和其他I/O事件都使用相同的方式(epoll实现的多路复用)处理,就是统一事件源。

信号是一种异步事件:信号处理函数和程序的主函数是两条不同的执行路线。很显然,信号处理函数需要尽快执行完毕,从而保证后来到达的信号不被阻塞太久。

一种解决办法就是,把信号的主要处理逻辑放到程序的主循环中,而当信号处理函数被触发时,他只是简单的把接收到的信号值传递给主循环,主循环根据接收到的信号值执行相应逻辑代码。

信号处理函数通常使用管道将信号传递给主循环:信号处理函数把信号值写入管道,主循环从管道读出信号值。

那么主循环如何知道管道上何时有数据?使用I/O复用系统监听管道读端的可读事件即可。这样,信号事件就能像其他I/O事件一样被处理,即统一事件源。

这是一个服务器程序,它使用统一事件源的方式同时处理I/O和信号

7、网络编程相关信号

下面探讨三个和网络编程模切相关的信号。

7.1 SIGHUP

对于与终端脱离关系的守护进程(没有控制终端的后台进程),这个信号用于通知该进程重新读取配置文件。

比如,xinetd超级服务程序。
当xinetd程序在接收到SIGHUP信号之后调用hard_reconfig函数,它将循环读取/etc/xinetd.d/目录下的每个子配置文件(这些文件是xinetd服务的子服务的配置文件),并检测其变化。如果某个正在运行的子服务的配置文件被修改以停止服务,则xinetd主进程讲给该子服务进程发送SIGTERM信号来结束它。如果某个子服务的配置文件被修改以开启服务,则xinetd将创建新的socket并将其绑定到该服务对应的端口上。

7.2 SIGPIPE

默认情况,往读端关闭的管道或socket连接中写数据将引发SIGPIPE信号。程序接收到该信号的默认行为是结束进程,但我们通常不会想因为区区错误的写操作而导致进程结束,所以我们需要在代码中捕获并处理该信号,或至少忽略他。

还有一种办法是:使用send函数的MSG_NOSIGNAL标志来禁止写操作触发SIGPIPE信号。这时,需要使用send 函数反馈的errno值来判断管道或socket连接的读端是否已经关闭,向这样的读端调用写操作会设置errnoEPIPE

还有一种办法:利用I/O复用系统调用检测管道和socket链接的读端是否已关闭。以poll为例,当管道的读端关闭时,写段文件描述符上的POLLHUP事件将被触发;当socket连接被对方关闭时,socket上的POLLRDHUP事件被触发。

7.3 SIGURG

内核使用SIGURG信号通知应用程序带外数据到达。内核通知带外数据到达的另一种方法是是I/O复用技术,对于select来说是异常事件,对于pollepoll来说分别是:POLLPRIEPOLLPRI

这是一个服务器程序,使用SIGURG信号接收带外数据。

8、信号相关函数

8.1 alarm函数

#inlcude<unistd.h>
unsigned int alarm(unsigned int seconds);

每隔seconds秒给当前进程发送一个SIGALRM信号。

返回上一次调用alarm定时剩余的秒数。没有失败。
例如:

alarm(5);	第一次调用返回0
刚过了2秒又调用
alarm(5);	这次调用返回3,因为上次调用还差3秒就到5
alarm(0);	这意思是取消闹钟

无论进程处于什么状态(就绪、运行、阻塞、暂停、终止、僵尸),alarm都会计时。

9、实例程序

9.1 定时器 与 信号相结合实现超时处理

这是一个服务器程序,使用定时器链表处理非活动socket连接,本程序中是直接将其关闭。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值