9月7日-8日,相约 2023 腾讯全球数字生态大会!聚焦产业未来发展新趋势! 了解详情
写点什么

Linux 信号系统

  • 2016-04-12
  • 本文字数:5911 字

    阅读完需:约 19 分钟

本文主要介绍 Linux 信号系统和如何使用 POSIX API 来响应信号。本文中的示例适用于 Linux 系统和大部分 POSIX 兼容系统。

Linux 系统中的信号

在下列情况下,我们的应用进程可能会收到系统信号:

  • 用户空间的其他进程调用了类似 kill(2) 函数
  • 进程自身调用了类似 about(3) 函数
  • 当子进程退出时,内核会向父进程发送 SIGCHLD 信号
  • 当父进程退出时,所有子进程会收到 SIGHUP 信号
  • 当用户通过键盘终端进程(ctrl+c)时,进程会收到 SIGINT 信号
  • 当进程运行出现问题时,可能会收到 SIGILL、SIGFPE、SIGSEGV 等信号
  • 当进程在调用 mmap(2) 的时候失败(可能是因为映射的文件被其他进程截短),会收到 SIGBUS 信号
  • 当使用性能调优工具时,进程可能会收到 SIGPROF。这一般是程序未能正确处理中断系统函数(如 read(2) )。
  • 当使用 write(2) 或类似数据发送函数时,如果对方已经断开连接,进程会收到 SIGPIPE 信号。

如需了解所有系统信号,参见 signal(7) 手册。

信号的默认行为

每个信号都关联一个默认的行为,当进程没有捕获并处理信号时,进程会按照默认的行为处理信号。

这些默认行为包括:

  • 结束进程。这是最通用默认行为,包括 SIGTERM、SIGQUIT、SIGPIPE、SIGUSR1、SIGUSR2 等信号。
  • 结束并执行核心转储。包括 SIGSEGV、SIGILL、SIGABRT 等信号,这一般都是因为代码中存在错误。
  • 一些信号默认会被忽略,例如 SIGCHLD。
  • 挂起进程。SIGSTOP 信号会引起进程挂起,而 SIGCOND 能够将挂起的进程继续运行。该过程常见于在控制台使用 ctrl+z 组合键。

信号处理

最传统的信号处理方式是使用 signal(2) 函数装载一个信号处理函数。但是这种方式已经被废弃,主要原因是在 UNIX 实现中,收到信号之后,会重置回默认的信号处理行为。同时,该行为是不跨平台的。因此,建议的信号处理方式是使用 sigaction(2) 函数。

sigaction(2) 函数的原型为:

int sigaction (int signum, const struct sigaction *act, struct sigaction *oldact);值得注意的是,sigaction(2) 函数不直接接受信号处理函数,而需要使用struct 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_handler:信号处理函数的函数指针,其函数原型和 signal(2) 接受的信号处理函数相同。
  • sa_sigaction:另一种信号处理函数指针,它能在处理信号时获取更多信号相关的信息。
  • sa_mask:允许设置信号处理函数执行时需要阻塞的信号。
  • sa_flags:修改信号处理函数执行时的默认行为,具体可选值请参照手册。

sigaction 使用示例:

复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
static void hdl (int sig, siginfo_t *siginfo, void *context)
{
printf ("Sending PID: %ld, UID: %ld\n",
(long)siginfo->si_pid, (long)siginfo->si_uid);
}
int main (int argc, char *argv[])
{
struct sigaction act;
memset (&act, '\0', sizeof(act));
/* 这里使用 sa_sigaction 字段,因为该字段提供了两个额外的参数,
可以获取关于接收信号的更多信息。 */
act.sa_sigaction = &hdl;
/* SA_SIGINFO 标识告诉 sigaction 函数使用 sa_sigaction 字段,而非 sa_handler 字段 */
act.sa_flags = SA_SIGINFO;
if (sigaction(SIGTERM, &act, NULL) < 0) {
perror ("sigaction");
return 1;
}
while (1)
sleep (10);
return 0;
}

该示例中使用了三个参数版本的信号处理函数来响应 SIGTERM 信号,编译(假设源文件名为 sig.c)并执行程序,可以有以下输出:

复制代码
gcc -o sig sig.c
./sig &
kill $!

Sending PID: 16200, UID: 1000

注意,使用三参数版本信号处理函数时,必须将 sa_flags 字段设置为 SA_SIGINFO,否则信号处理函数将无法获取到正确的siginfo_t对象。

对于siginfo_t结构体, sigaction(2) 的手册中有详细介绍,其中的几个字段非常有用:

  • si_code:用于标识信号的来源,例如 kill(2) raise(3) 等通过程序调用产生的信号,该值为 SI_USER;而由内核发送的信号,该值为 SI_KERNEL。
  • 对于 SIGCHLD 信号,可以从 si_status 字段(进程退出码)、si_utime 字段(进程消耗的用户态时间)和 si_stime 字段(进程消耗的内核态时间)获取更多信息。
  • 对于 SIGILL、SIGFPE、SIGSEGV、SIGBUS 等信号,可以从 si_addr 字段获取发生错误的内存地址。

常见问题

由于信号处理函数是异步执行且无法预知执行时间,因此编码时需要特别注意异步执行产生的问题,尤其是主函数和信号处理函数之间共享的数据。

首先是编译器优化。如果一个变量在主函数中循环读取,信号处理函数中修改(例如一个退出标识),这时编译器优化可能导致信号处理函数中的修改无法让主函数感知到。例如如下代码:

复制代码
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
static int exit_flag = 0;
static void hdl (int sig)
{
exit_flag = 1;
}
int main (int argc, char *argv[])
{
struct sigaction act;
memset (&act, '\0', sizeof(act));
act.sa_handler = &hdl;
if (sigaction(SIGTERM, &act, NULL) < 0) {
perror ("sigaction");
return 1;
}
while (!exit_flag)
;
return 0;
}

如果使用 gcc O2 级别的优化,该程序会按照预期,在接收到 SIGTERM 信号时退出。但是,如果优化级别调整到 O3,向进程发送 SIGTERM 信号之后,进程还会继续运行(假设文件名为 test_sig.c):

复制代码
gcc -o test -O3 test_sig.c
./test &
killall test

这时控制台不会提示后台进程退出,使用jobs命令查看后,test 进程仍然存在:

复制代码
jinlingjie@localhost ~/data/Downloads $ ./test &
[1] 2532
jinlingjie@localhost ~/data/Downloads $ killall test
jinlingjie@localhost ~/data/Downloads $ jobs
[1]+ 运行中 ./test &

这是因为在 O3 级别的优化中,编译器发现while循环会不停读取exit_flag变量,为了加快读取速度,编译器会把该变量值直接加载到寄存器中,而不再每次从内存读取。此时信号处理函数再修改exit_flag变量,不会被更新到寄存器中,因此进程无法退出。对于这种场景,需要给共享变量增加volatile关键字,以确保进程每次读取变量时,都去内存重新获取最新的值。

上面的示例中的场景,还需要考虑对共享变量修改的原子性。在一些平台上int类型的读取或者写入可能不是原子的。信号系统提供 sig_atomic_t 对象,以确保原子的读写。

除此以外,编写信号处理函数还需要注意 _ 信号安全 _。因为信号处理函数调用的其他函数也有可能被信号中断, signal(7) 手册的 Async-signal-safe functions(异步信号安全函数)章节详细列举了所有在信号处理函数中可以安全调用的函数。

特殊信号处理

SIGCHLD 信号

如果父进程不需要获取子进程的退出状态码,也不需要等待子进程的退出,唯一的目的是清理僵尸进程。那么,父进程只需要处理 SIGCHLD 信号,并进行清理即可:

复制代码
static void sigchld_hdl (int sig)
{
/* 等待所有已经退出的子进程。
* 这里使用非阻塞的调用以防止子进程在代码其他地方被清理。 */
while (waitpid(-1, NULL, WNOHANG) > 0) {
}
}

这是一个简单的信号处理函数,如果需要做更多的工作,请特别注意不要使用非异步信号安全的函数。

SIGBUS 信号

前面提到过 SIGBUS 信号通常是访问被映射( mmap(2) )的内存时,无法映射到对应文件(通常是文件被截断了)。这种非正常情况下,进程的一般行为是直接退出,但是如果一定要处理 SIGBUS 信号还是可行的。这时可以通过 sigsetjmp(3) siglongjmp(3) 来跳过发生错误的地方,从而让程序继续运行。

需要特别注意的是,信号处理函数执行了 siglongjmp(3) 调用之后,代码没有继续运行下去,而是直接跳转到 sigsetjmp(3) 位置重新开始执行。如果此时代码仍然持有锁等资源,将不会释放,如果后续代码继续去竞争锁,可能会导致死锁的发生。

SIGSEGV 信号

处理 SIGSEGV(段错误)信号是可能的,但这一般是没有意义的,因为即使代码重新运行了,运行到同样的地方仍然可能发生段错误。其中一种重启程序有效的情况是通过 mmap(2) 获取到的内存有写保护,由此产生的 SIGSEGV 信号(可以通过信号处理函数中的 siginfo_t 参数获取发生原因),可能可以通过 mprotect(2) 函数来去除写保护。

如果段错误是因为栈空间不足导致的,那么这时将无法通过信号处理函数来处理 SIGSEGV 信号。因为信号处理函数同样需要分配栈空间来执行。这种情况下,可以通过 sigaltstack(2) 函数为信号处理函数定义独立的栈空间。

SIGABRT 信号

试图处理 SIGABRT 信号时,需要了解 abort(3) 函数的运行原理:该函数会先发送 SIGABRT 信号,如果该信号被忽略,或者对应的信号处理函数正常返回(没有通过 longjmp(3) 跳转),它会将信号处理函数重置为默认方式,并且重新发送 SIGABRT 信号信号,这将导致进程退出。因此,处理 SIGABRT 信号的作用可能是在进程结束前做一些最后的操作,或者使用 longjmp(3) 从新的地方开始执行。

信号和 fork()

当父进程调用 fork(2) 函数创建子进程时,子进程不会复制父进程的信号队列,即使此时父进程的信号队列非空,也会单独创建一个空的信号队列。但是,子进程会继承父进程的所有信号处理函数和信号阻塞状态。因此如果父进程已经完成对信号的设置,没有特殊情况子进程无须重新设置。

信号和线程

由于 POSIX 规范中,所有的一个进程的所有线程都有相同的进程 ID(PID),向多线程进程发送信号有两种情况:

  • 向进程发送信号(使用类似 kill(2) 这样的函数直接向进程发送信号):线程可以通过 pthread_sigmask(2) 单独设置需要阻塞的信号。因此如果有线程没有阻塞当前发送的信号,进程中的一个线程会收到该信号(但是没有特殊说明具体哪个线程会收到);如果所有的线程都阻塞了当前发送的信号,该信号会被加入进程的信号队列;如果进程没有设置当前信号的信号处理函数,并且该信号的默认行为是终止进程,那么整个进程都将被终止。
  • 向特性线程发送信号(使用 pthread_kill(2) ):线程可以通过 pthread_kill(2) 向进程中的其他线程(或者自身)发送信号,此时信号会发送到对应线程的信号队列中。同时操作系统也可能会向特性线程发送诸如 SIGSEGV 信号。如果接收信号的线程没有处理对应的信号,且该信号的默认行为是终止进程,那么该线程所在的进程都将被终止。

信号发送

向进程发送信号的方式可以有:

  • 通过键盘交互:一些键盘的组合键,可以向控制台正在执行的进程发送信号。
    • CTRL+C:发送 SIGINT 信号,该信号默认行为是终止进程。
    • CTRL+\:发送 SIGQUIT 信号,该信好默认行为是终止进程并核心转储。
    • CTRL+Z:发送 SIGSTOP 信号,该信号默认行为是挂起进程。
  • kill(2) kill(2) 函数接受两个参数,一个是信号发送的进程 ID,一个是需要发送的信号。其中的进程 ID 有一些特殊的约定。
    • 0:如果 PID 为 0, 信号发送的目标是当前进程组的所有进程。
    • -1:如果 PID 为 -1,信号发送的目标是所有(有权限发送信号)的进程。
    • < -1:如果 PID 小于 -1, 信号发送的目标是进程 ID 为 -PID 的进程组。
  • 向进程自身发送信号:进程可以通过调用 raise(3)、abort(3) 等函数向自身发送信号。
    • raise(3):可以向进程发送指定信号,需要注意的是,在多线程环境中,只会向当前线程发送信号。
    • abort(3):向当前进程发送 SIGABRT 信号,前文已经提到过,该函数会重置信号处理函数,因此无需关心进程是否已经处理了 SIGABRT 信号。
  • sigqueue(2) :该函数和 kill(2) 函数类似,但是多了一个sigval参数。因此调用者可以向信号处理函数传递一个整数或者一个指针。信号处理函数可以通过siginfo_t参数获取该参数。

信号阻塞

有些时候,我们需要阻塞信号,防止信号打断当前程序的执行,而不是捕获和处理信号。传统的 signal(2) 函数可以通过将信号处理函数设置为SIG_IGN来实现阻塞的功能。但是该方式已经废弃,建议使用 sigprocmask(2) 函数来实现信号阻塞功能,因为它提供了更多的参数,可以适用于复杂场景。

一个简单的示例:

复制代码
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static int got_signal = 0;
static void hdl (int sig)
{
got_signal = 1;
}
int main (int argc, char *argv[])
{
sigset_t mask;
sigset_t orig_mask;
struct sigaction act;
memset (&act, 0, sizeof(act));
act.sa_handler = hdl;
if (sigaction(SIGTERM, &act, 0)) {
perror ("sigaction");
return 1;
}
sigemptyset (&mask);
sigaddset (&mask, SIGTERM);
if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) {
perror ("sigprocmask");
return 1;
}
sleep (10);
if (sigprocmask(SIG_SETMASK, &orig_mask, NULL) < 0) {
perror ("sigprocmask");
return 1;
}
sleep (1);
if (got_signal)
puts ("Got signal");
return 0;
}

上述示例展示了通过 sigprocmask(2) 函数来阻塞 SIGTERM 信号 10 秒,此时如果进程接收到了 SIGTERM 信号,会被加入到进程的信号队列中。解除对 SIGTERM 信号的阻塞,此时如果之前的信号队列中有 SIGTERM 信号,或者新收到了 SIGTERM 信号,就会执行对应的信号处理函数。

阻塞信号使用的一个场景就是防止信号的竞争。一些函数(如 select(2) poll(2) )会阻塞当前函数执行,这时在异常的情况下,这些函数会期望通过信号来中断当前的阻塞操作。但是,如果此时程序还设置了其他信号处理函数,这时信号可能会被设置的信号处理函数消费,导致阻塞操作的函数仍然执行,无法中断。

遇到这种情况,就需要使用 sigprocmask(2) 配合支持重置sigmask的阻塞函数(如 pselect(2) poll(2) ),大致的示例代码片段如下:

复制代码
sigemptyset (&mask);
sigaddset (&mask, SIGTERM);
if (sigprocmask(SIG_BLOCK, &mask, &orig_mask) < 0) {
perror ("sigprocmask");
return 1;
}
while (!exit_request) {
/* 如果在这里接收到信号,信号会被阻塞,
* 直到取消阻塞(下面 pselect 实现)
*/
FD_ZERO (&fds);
FD_SET (lfd, &fds);
res = pselect (lfd + 1, &fds, NULL, NULL, NULL, &orig_mask);
/* 下面继续文件描述符操作 */
}

后记

本文对 Linux/UNIX 信号系统、信号的处理、发送、阻塞等做了简单的介绍。但是整个信号系统非常复杂,还有很多没有提到的内容,期待和大家继续交流。


感谢魏星对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2016-04-12 17:235509

评论

发布
暂无评论
发现更多内容

抓包分析RST信号

蓝胖子的编程梦

TCP Wireshark tcpdump RST 报文 Connection reset

分享几款 Mac 上非常好用的的免费软件

搞大屏的小北

数据可视化 数据库工具 截图软件 视屏转 gif 视频号下载

科兴未来|2023年扬中高层次人才创新创业大赛

科兴未来News

教培行业的“智能GPT私教”?WorkPlusAI助理帮助教培机构实现十倍人效!

WorkPlus

精耕丝路,智胜全球 | 新华三助力中企跑好“出海”赛道

新消费日报

DevEco创建项目时的错误解决

路北路陈

6 月 优质更文活动

揭秘Spring依赖注入和SpEL表达式

华为云开发者联盟

开发 华为云 华为云开发者联盟 企业号 6 月 PK 榜

Navicat Premium将关系和实体添加到概念模型的方法

背包客

macos MySQL 数据库 Mac 软件 Navicat Premium

Sentinel熔断降级的规则及实现原理

互联网架构师小马

Java sentinel 熔断降级

相约未名湖畔,百度商业AI技术创新大赛携手北大学子共探AI发展

百度Geek说

人工智能 百度 企业号 6 月 PK 榜

数据分析:电子商务需要关注的重要指标有哪些?

搞大屏的小北

电子商务 销售指标

“敏捷教练进阶课程”7月22-23日 ·A-CSM认证在线周末班【提前报名特惠】CST导师亲授

ShineScrum捷行

敏捷教练

参与赢大奖!阿里云机器学习平台PAI助力开发者激发AIGC潜能

阿里云大数据AI技术

阿里云 AIGC

3 个技巧,让你像技术专家一样解决编码问题

LigaAI

程序人生 技术专家 技术人成长 问题分析及解决 企业号 6 月 PK 榜

MySQL 8.0.29 instant DDL 数据腐化问题分析

GreatSQL

greatsql greatsql社区

TCMalloc 技术细节详解

KaiwuDB

KaiwuDB TCMalloc

Web网页端IM产品RainbowChat-Web的v5.0版已发布

JackJiang

网络编程 即时通讯 IM

【零售电商系列】走进亚马逊之自建仓储&物流

小诚信驿站

6 月 优质更文活动

智慧生活垃圾焚烧发电厂Web3D可视化平台

2D3D前端可视化开发

物联网 数字孪生 三维可视化 工业组态 智慧垃圾焚烧发电厂

“数字创新产品课程”7月29-30日 · CSPO认证周末班【提前报名特惠】CST导师亲授

ShineScrum捷行

Java代码性能测试实战之ContiPerf

javalover123

单元测试 性能测试 压测 JUnit Java'

当GaussDB遇上了毕昇编译器

华为云开发者联盟

数据库 后端 华为云 华为云开发者联盟 企业号 6 月 PK 榜

电路板电镀中4种特殊的电镀方法

华秋PCB

经验 电路板 焊接 PCB板 电镀

源生创新 云享未来|GOTC全球开源技术峰会华为云云原生精彩时刻

华为云开发者联盟

云原生 后端 华为云 华为云开发者联盟 企业号 6 月 PK 榜

AIGC时代,设计软件应该做什么?丨AIGC X 企业服务

ToB行业头条

科兴未来|2023”福地句才”海外人才创业大赛

科兴未来News

对线面试官-线程池(四)

派大星

Java 面试题

从分布式到微服务解密“架构”原理与实战笔记

小小怪下士

Java 程序员 分布式 微服务

Win服务器图床配置

路北路陈

6 月 优质更文活动

NFTScan | 06.05~06.11 NFT 市场热点汇总

NFT Research

NFT 热点

今年LED显示屏市场趋势

Dylan

商业 广告 娱乐 数字化 LED显示屏

  • 扫码添加小助手
    领取最新资料包
Linux信号系统_Linux_金灵杰_InfoQ精选文章