《Linux高性能服务器编程》第十章 信号

Github代码地址

第 10 章 信号

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

  • 对于前台进程,用户可以通过输入特殊的终端字符来给它发信号。比如输入 Ctrl + C 通常会给进程发送一个中断信号
  • 系统异常。比如浮点异常和非法内存段访问
  • 系统状态变化。比如 alarm 定时器到期将引起 SIGALRM 信号
  • 运行 kill 命令或调用 kill 函数

服务器必须能处理(或至少忽略)一些常见的信号,以免异常终止

10.1 Linux 信号概述

10.1.1 发送信号

Linux 下,一个进程给其他进程发送信号的 APIkill 函数。其定义如下:

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

该函数把信号 sig 发送给目标进程;目标进程由 pid 参数指定,其可能的取值及含义如下

pid 参数含义
pid > 0信号发送给 PID 为 pid 的进程
pid = 0信号发送给本地进程组内的其他进程
pid = -1信号发送给除 init 进程外的所有进程,但发送者需要拥有对目标进程发送信号的权限
pid < -1信号发送给组 ID 为 -pid 的进程族中的所有成员

Linux 定义的信号值都大于 0 .如果 sig 取值为 0, 则 kill 函数不发送任何信号。

10.1.2 信号处理方式

目标进程在收到信号时,需要定义一个接收函数来处理,原型如下:

#include <bits/signum.h>
#define SIG_DEF ((__sighandler_t) 0)
#define SIG_IGN ((__sighandler_t) 1)
  • SIG_IGN 表示忽略目标的信号

  • SIG_DEF 表示使用信号的默认处理方式。

    信号的默认处理方式有如下几种:

    • 结束进程 ( Term )
    • 忽略信号 ( Ign )
    • 结束进程并生成核心转储文件 ( Core )
    • 暂停进程 ( Stop )
    • 继续进程 ( Cont )

10.1.3 Linux 信号

Linux 的可用信号都定义在 bits/signum.h 头文件中。

重点的几个信号

信号起源默认行为含义
SIGHUPPOSIXTerm控制终端挂起
SIGPIPEPOSIXTerm往读端被关闭的管道或者 socket 连接中写数据
SIGURG4.2 BSDIgnsocket 连接上接收到紧急数据
SIGALRMPOSIXTerm由 alarm 或 setitimer 设置的实时闹钟超时引起
SIGCHLDPOSIXIgn子进程状态发生变化 ( 退出或者暂停 )

10.1.4 中断系统调用

如果程序在执行处于阻塞状态的系统调用时接收到信号,并且我们为该信号设置了信号处理函数,则默认情况下系统调用将被中断,并且 errno 被色织为 EINTR。我们可以使用 sigaction 函数为信号设置 SA_RESTART 标志以自动重启被该信号中断的系统调用

对于默认行为时暂停进程的信号,如果我们没有为他们设置信号处理函数,则也可以中断某些系统调用( connect、epoll_wait )。

10.2 信号函数

10.2.1 signal 系统调用

要为一个信号设置处理函数,可以使用下面的 signal 系统调用:

#include <signal.h>
_sighandler_t signal ( int sig, _sighandler_t _handler )
  • sig 指出要捕获的信号类型。
  • _handler _sighandler_t 类型的函数指针,用于指定信号 sig 的处理函数

signal 函数成功时返回一个函数指针,和第二个参数类型一样。这个返回值是前一次调用 signal 函数时传入的函数指针,或者时信号 sig 对应的默认处理函数指针 SIG_DEF ( 如果时第一次调用 signal 的话 )

signal 系统调用出错时返回 SIG_ERR

10.2.2 sigaction 系统调用

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

#inclide <signal.h>
int sigaction( int sig, const struct sigaction* act, struct sigaction* oact );
  • sig 指出要捕获的信号类型
  • act 指定新的信号处理方式
  • oact 输出信号先前的处理方式 ( 如果不为 NULL )

sigaction 结构体描述了信号处理的细节,定义如下:

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;
    void (*sa_restorer) (void);
};
  • sa_hander 成员指定信号处理函数

  • sa_mask 成员设置进程的信号掩码,以指定哪些喜好不能发送给本进程

    sa_mask 是信号集 sigset_t 类型,该类型指定一组信号。

10.3 信号集

10.3.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;

Linux 提供了如下一组函数来设置、修改、删除和查询信号集

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

10.3.2 进程信号掩码

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

#include <signal.h>
int sigprocmask( int _how, _const sigset_t* _set, sigset_t* _oset )
  • _set 指定新的信号掩码
  • _oset 输出原来的信号掩码

如果 set 不为 NULL, 则 _how 指定进程信号的掩码方式,可选值如表

_how 参数含义
SIG_BLOCK新的进程信号掩码是其当前值和 _set 指定信号集的并集
SIG_UNBLOCK新的进程信号掩码是其当前值和 ~_set 信号集的交集,因此 _set 指定的信号集将不屏蔽
SIG_SETMASK直接将进程信号掩码设置为 _set

如果 setNULL, 则进程信号掩码不变,此时我们仍然可以利用 _oset 参数来获得进程当前的信号掩码

10.3.3 被挂起的信号

设置进程信号掩码后,被屏蔽的信号将不能被进程接收。否则操作系统将该信号设置为进程的一个被挂起的信号。如果取消对被挂起信号的屏蔽,则能立即被进程接收到。如下函数可以获得进程当前被挂起的信号集

#include <signal.h>
int sigpending( sigset_t* set );
  • set 保存被挂起的信号集。

在多进程、多线程环境中,要以进程、线程为单位来处理信号和信号掩码。不能设想新创建的进程、线程具有和父进程、主线程完全相同的信号特征。比如 fork 调用产生的子进程能继承父进程的信号掩码,但具有一个空的挂起信号集。

10.4 统一事件源

信号是一种异步事件,信号处理函数和程序的主循环是两条不同的执行路线。为了信号不被屏蔽太久,信号处理函数需要尽快执行完成(为了避免一些竞态条件,信号在处理期间,系统不会再次触发它)

竞态条件(Race Condition):计算的正确性取决于多个线程的交替执行时序时,就会发生竞态条件。

最常见的竞态条件为:

先检测后执行。

执行依赖于检测的结果,而检测结果依赖于多个线程的执行时序,而多个线程的执行时序通常情况下是不固定、不可判断的,从而导致执行结果出现各种问题。

一种典型的解决方案是:把信号的主要处理逻辑放到程序的主循环中,当信号处理函数被触发时,它只是简单地通知主循环程序接收到信号,并把信号值传递给主循环,主循环再根据接收到地信号值执行目标信号对应的逻辑代码。信号处理函数通常使用管道读出该信号值。那么主循环怎么知道管道上何时有数据可读?-- 使用 I/O 复用系统调用来监听管道的读端文件描述符上的可读事件。

event.cpp

10.5 网络编程相关信号

10.5.1 SIGHUP

该信号在挂起进程的控制终端时被触发。而对于没有控制终端的网络后台程序,它们通常利用 SIGHUP 信号来强制服务器重读配置文件。

xinetd 程序在收到该信号后调用 hard_reconfig 函数。若某个正在运行的子服务的配置文件被修改以停止服务,则 xinetd 主进程将给该子服务进程发送 SIGTERM 信号以结束它。

10.5.2 SIGPIPE

默认情况下,往一个读端关闭的管道或 socket 连接中写数据将引发 SIGPIPE 信号。我们需要捕获并处理该信号,或者至少忽略它,因为程序接收到 SIGPIPE 信号的默认行为时结束进程。引起 SIGPIPE 信号的写操作将设置 errnoEPIPE

我们可以使用 send 函数的 MSG_NOSIGNAL 标志来禁止写操作触发 SIGPIPE 信号。在这种情况下,我们应使用 send 函数反馈的 errno 值来判断管道或者 socket 连接的读端是否已经关闭

我们也可以用 I/O 复用系统调用来检测管道和 socket 连接的读端是否关闭。以 poll 为例,当管道的读端关闭时,写端文件描述符上的 POLLHUP 事件将被触发;当 socket 连接被对方关闭时,socket 上的 POLLRDHUP 事件将被触发

10.5.3 SIGURG

Linux 环境下,内核通知应用程序带外数据到达主要有两种方法:

  • I/O 复用计数, select 等系统调用在接收到带外数据时将返回,并向应用程序报告 socket 上的异常事件

    • select.c 第九章
  • 使用 SIGURG 信号

    SIGURG.cpp

    可以通过第五章的 client.c 程序发送数据查看如何处理

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Artintel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值