Unix/Linux系统信号详解-初入信号概念

以下内容引述《Linux/Unix系统编程手册》

信号

信号是事件发生时对进程的通知机制。
有时也成为软件中断。信号与硬件中断的相似之处在于打断了程序执行的正常流程。
发往进程的诸多内核,通常都是源于内核。
引发内核为进程产生信号的各类事件如下:

  • 硬件发生异常,即硬件检测到一个错误条件并通知内核,随即再由内核发送相应信号给相关进程;
  • 用户键入了能够产生信号的终端特殊字符;
  • 发生了软件事件,比如调整了中断窗口大小,定时器到期,进程执行CPU事件超限等

针对每个信号,都定义了一个唯一的整数,从1开始顺序展开。<signal.h>以SIGxxxx形式的符号名对这些整数做了定义。

信号分为2大类,第一组用于内核向进程通知事件,构成所谓传统或者标准信号。Linux中标准信号的编号范围为1~31.
信号因某些事件而产生。信号产生后,会与稍后背传递给某一进程,而进程也会采取某些措施来响应信号。在产生和到达期间,信号处于等待状态。

信号到达后,进程视具体信号执行如下默认操作之一:

  • 忽略信号,也就是说,内核将信号丢弃,信号对进程没有产生任何影响(进程永远都不知道曾经出现过该信号);
  • 终止(杀死)进程:这有时是指进程异常终止,而不是进程因调用exit()而发生的正常终止;
  • 产生核心转储文件,同时进程终止:核心转储文件包含对进程虚拟内存的镜像,可将其加载到调试器中以检查进程终止时的状态;
  • 终止进程:暂停进程的执行;
  • 于之前暂停后再度恢复进程的执行

信号类型和默认行为

SIGABRT
进程调用abort()函数时,系统向该进程发送信号。默认情况下,该信号会终止进程,并产生核心转储文件

SIGALRM
调用alarm()或setitimer()而设置的实时定时器一旦到期,内核将产生该信号

SIGBUS
产生该信号(总线错误,bus error)即表示发生了某种内存访问错误。当使用由mmap()所创建的内存映射时,如果试图访问的地址超出了底层内存映射文件的结尾,那么将产生该错误

SIGCHLD
当父进程的某一子进程终止(或者因为调用了exit(),或者因为被信号杀死)时,(内核)将向父进程发送该信号。当父进程的某一子进程因收到信号而停止或恢复时,也可能会向父进程发送该信号

SIGCLD
与SIGCHLD信号同义

SIGCONT
将信号发送给已停止的进程,进程将会恢复运行(即在之后某个时间点重新获得调度)
当接收信号的进程当前不处于停止状态时,默认情况下将忽略该信号

SIGEMT
Unix系统通常用该信号来表示一个依赖于实现的硬件错误

SIGFPE
该信号因特定类型的算术错误而产生,比如除以0

SIGHUP
当终端断开(挂机)时,将发送该信号给终端控制进程。许多守护进程会在收到SIGHUP信号时重新进行初始化并重读配置文件。

SIGINT
当用户键入终端中断字符(通常为control-c时),终端驱动程序将发送该信号给前台进程组。该信号的默认行为是终止进程。

SIGKILL
此信号为“必杀”信号,处理器程序无法将其阻塞、忽略或者捕获

SIGQUIT
当用户在键盘上键入退出(Control-)字符时,该信号将发往前台进程组。默认情况下,该信号终止进程,并生成可用于调试的核心转储文件。
进程如果陷入无限循环,或者不再响应时,使用SIGQUIT信号,再调用gdb调试器加载刚才生成的核心转储文件,接着用backtrace命令来获取堆栈跟踪信息,就能跟踪定位到程序代码

SIGSEGV
当应用程序对内存的引用无效时,就会产生该信号

SIGSYS
如果进程发起的系统调用有误,那么将产生该信号
这意味着系统将进程执行的指令视为一个系统调用陷阱,但相关的系统调用编号却是无效的

SIGTERM
这是用来终止进程的标准信号,也是kill和killall命令所发送的默认信号。用户有时会使用kill - KILL或者kill-9显示向进程发送SIGKILL信号。然而,这种做法通常是错误的。精心设计的应用程序应当为SIGTERM信号设置处理器程序,以便于其能够预先清除临时文件和释放其他资源。发送SIGKILL信号可以杀掉某个进程,从而绕开了SIGTERM信号的处理器程序。

SIGTRAP
该程序用于实现端点调试功能以及strace(1)命令所执行的跟踪系统调用功能

SIGTSTP
这是作业控制的停止信号,当用户在键盘上输入挂起字符时(通常是Control-Z)时,将发送该信号给前台进程组,使其停止运行。

SIGTTIN
在作业控制shell下运行时,若后台进程组试图对终端进行read操作,终端驱动程序则将向该进程组发送此信号

SIGTTOU
该信号的目的与SIGTTIN信号类似,但所针对的是后台作业的终端输出。在作业控制shell下运行时,如果对终端启用了TOSTOP选项,而某一后台进程组试图对终端进行write操作,那么终端驱动程序将向该进程组发送SIGTTOU信号。该信号将停止进程。

SIGURG
系统发送给信号一个进程,表示套接字上存在带外数据

SIGURG1
该信号和SIGUSR2信号供程序员自定义使用。内核绝不会为进程产生这些信号。进程可以使用这些信号来相互通知事件的发生,或者彼此同步。

SIGURG2
同SIGURT1

SIGVTALRM
调用setitimer()设置的虚拟定时器刚一到期,内核就会产生该信号。虚拟定时器记录的是进程在用户态所使用的CPU时间

SIGWINCH
窗口环境中,当终端窗口尺寸发生变化时,要么是由于用户手动调整了大小,要么是因为程序调用ioctl()对大小做了调整,会向前台进程组发送该信号

改变信号处理

UNIX系统提供两种方法来改变信号处理:signal()和sigaction()

signal

signal函数时设置信号处置的原始API,所提供的接口比sigaction接口简单。
signal的行为在不同UNIX实现间存在差异

#include <signal.h>
void ( *signal(int sig, void (*handler)(int)) )(int);

参数sig标志希望修改处置的信号编号,参数二handler标志信号抵达时所调用函数的地址。

signal的返回值是之前的信号处置,像handler参数一样,这是一枚指针,所指向的是带有一个整形参数且无返回值的函数。

void (*oldHandler)(int);
oldHandler = signal(SIGINT, newHandler);
if(oldHandler == SIG_ERR)
	errExit("signal");
if(signal(SIGINT, oldHandler) == SIG_ERR)
	errExit("signal");

使用signal,将无法在不改变信号处置的同时,还能获取当前的信号处置。

信号处理器

#include <signal.h>
#include "tlpi_hdr.h"
static void sigHandler(int sig)
{
	printf("Ouch!\n");
}
int main(int argc, char* argv[])
{
	int j;
	if(signal(SIGINT, sigHandler) == SIG_ERR)
		errExit("signal");
	for(j=0; ; j++) {
		printf("%d\n", j);
		sleep(3);
	}
}	

发送信号

kill

一个进程能够使用kill系统调用向另一个进程发送信号

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

raise

有时,进程需要向自身发送信号

#include <signal.h>
int raise(int sig);

在单线程程序中,调用raise相当于对kill的如下调用

kill(getpid(), sig);
pthread_kill(pthread_self(), sig);

当进程使用raise向自身发送信号时,信号将立即传递

killpg

#include <signal.h>
int killpg(pid_t pgrp, int sig);

等同于

kill(-pgrp, sig);

显示信号描述

每个信号都有一串与之相关的可打印说明。这些描述位于数组sys_siglist中。例如,可以使用sys_siglist[SIGPIPE]来获取对SIGPIPE信号的描述,推荐使用strsignal函数

#define _BSD_SOURCE
#include <signal.h>
extern const char* const sys_siglist[];
#define _GNU_SOURCE
#include <string.h>
char* strsignal(int sig);

strsignal函数较之于直接饮用sys_siglist数组的另一优势是对本地设置敏感。

信号集

多个信号可使用一个称之为信号集的数据结构来表示,其系统数据类型为sigset_t
sigset_t数据类型在Linux中是一个位掩码
sigemptyset函数初始化一个未包含任何成员的信号集,sigfillset函数则初始化一个信号机,使其包含所有信号

#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);

必须使用sigemptyset或者sigfillset来初始化信号集,因为C语言不会对自动变量进行初始化,并且,借助将静态变量初始化为0的机制来表示空信号集的做法在可移植性上存在维问题,因为有可能使用位掩码之外的结构来实现信号集。
信号集初始化后,可以分别使用sigaddset和sigdelset函数向一个集合中添加或者移除单个信号

#include <signal.h>
int sigaddset(sigset_t *set, int sig);
int sigdelset(sigset_t *set, int sig);
// 测试是否在信号集中
// yes -- 1 , no -- 0
int sigismember(const sigset_t* set, int sig);

信号掩码(阻塞)

内核会为每个进程维护一个信号掩码,即一组信号,并将阻塞其针对该进程的传递,如果信号将遭阻塞的信号发送给某进程,那么对该信号的传递将延后,直至从进程信号掩码移除该信号,从而解除阻塞为止。

改变信号处理

sigaction

sigaction系统调用时设置信号处理的另一选择。虽然sigaction的用法比之signal更为复杂,但是作为回报,也更具有灵活性。尤其是,sigaction允许在获取信号处置的同时无需将其改变,并且,还可设置各种属性对调用信号处理器程序时的行为施以更加精准的控制。

#include <signal.g>
int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact);
struct sigaction {
	void (*sa_handler)(int);	// address of handler
	sigset_t sa_mask;			// signals blocks during handler invocation
	int sa_flag;				// flags controlling handler invocation
	void (*sa_handler)(void);   // Not for application use
}

等待信号

调用pause将暂停进程的执行,直至信号处理器函数中断该调用为止(或者直至处理信号终止进程为止)

#include <signal.h>
int pause(void);

总结

  1. 信号是事件的通知机制,由内核、另一个进程或者进程自身发送给进程;
  2. 信号传递通常是异步行为,这意味着信号中断进程执行的位置是不可预测的;
  3. 默认情况下,要么忽略信号,要么终止进程,要么停止一个正在运行的进程,要么重启一个已停止的进程
  4. 每个进程都具有一个信号掩码,代表当前传递遭到阻塞的一组信号。使用sigprocmask可从信号掩码中添加或者移除信号
  5. 如果接收到的信号当前遭到阻塞,那么该信号将保持等待状态,直至解除对其阻塞。系统不会对标准信号进行排队处理,意即信号标记为等待状态只会发生一次;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值