APUE 信号

《UNIX环境高级编程》第10章 信号 笔记

信号是软件中断。很多比较重要的应用程序都需处理信号。信号提供了一种处理异步事件的方法,例如,终端用户键入中断键,会通过信号机制停止一个程序,或及早终止管道中的下一个程序。

信号概念

每个信号都有一个名字,这些名字都以3个字符SIG开头,比如:SIGINT中断信号、SIGABRT夭折信号、SIGALRM闹钟信号。

产生信号的条件:

  • 当用户按某些终端键时,引发终端产生的信号。
  • 硬件异常产生信号:除数为0、无效的内存引用等。
  • 进程调用ki11(2)函数可将任意信号发送给另一个进程或进程组。
  • 用户可用ki11(1)命令将信号发送给其他进程
  • 当检测到某种软件条件已经发生,并应将其通知有关进程时也产生信号

信号是异步事件的经典实例。产生信号的事件对进程而言是随机出现的。进程不能简单地测试一个变量(如 errno)来判断是否发生了一个信号,而是必须告诉内核“在此信号发生时,请执行下列操作”。

在某个信号出现时,可以告诉内核按下列3种方式之一进行处理,我们称之为信号的处理或与信号相关的动作:

  • 忽略此信号,有两种信号不能被忽略,它们是 SIGKILL和 SIGSTOP。这两种信号不能被忽略的原因是:它们向内核和超级用户提供了使进程终止或停止的可靠方法。
  • 捕捉信号,通知内核在某种信号发生时,调用一个用户函数。在用户函数中,可执行用户希望对这种事件进行的处理。
  • 执行系统默认操作

函数signal

UNX系统信号机制最简单的接口是 signal函数,其用于捕获信号,并捕获以后发生什么事情。

1
2
3
4
5
#include <signal.h> 

void (*signal(int signo, void (*func)(int)))(int);

//Returns: previous disposition of signal (see following) if OK, SIG_ERR on error

signo参数是信号名。func的值是常量SIG_IGN、常量SIG_DEL或当接到此信号后要调用的函数的地址。SIG_IGN表示向内核表示忽略此信号(记住有两个信号 SIGKILI和 SIGSTOP不能忽略)。SIG_DEL表示接到此信号后的动作是系统默认动作。

当指定函数地址时,则在信号发生时,调用该函数,我们称这种处理为捕获该信号,称此函数为信号处理程序( signal handler)或信号捕捉函数 (signal-catching function)

程序启动

当执行一个程序时,所有信号的状态都是系统默认或忽略。通常所有信号都被设置为它们的默认动作,除非调用exec的进程忽略该信号。确切地讲,exec函数将原先设置为要捕捉的信号都更改为默认动作,其他信号的状态则不变(一个进程原先要捕捉的信号,当其执行一个新程序后,就不能再捕捉了,因为信号捕捉函数的地址很可能在所执行的新程序文件中已无意义)。

进程创建

当一个进程调用fork时,其子进程继承父进程的信号处理方式。因为子进程在开始时复制了父进程内存映像,所以信号捕捉函数的地址在子进程中是有意义的

不可靠信号

在早期的UNX版本中(如V7),信号是不可靠的。不可靠在这里指的是,信号可能会丢失。一个信号发生了,但进程却可能一直不知道这一点。同时,进程对信号的控制能力也很差,它能捕捉信号或忽略它。有时用户希望通知内核阻塞某个信号:不要忽略该信号,在其发生时记住它然后在进程做好了准备时再通知它。这种阻塞信号的能力当时并不具备

这些早期版本的另一个问题是:在进程不希望某种信号发生时,它不能关闭该信号。进程能做的一切就是忽略该信号。

中断的系统调用

早期UNX系统的一个特性是:如果进程在执行一个低速系统调用而阻塞期间捕捉到一个信号,则该系统调用就被中断不再继续执行。该系统调用返回出错,其erno设置为EINTR。

为了支持这种特性,将系统调用分成两类:低速系统调用和其他系统调用。低速系统调用是可能会使进程永远阻塞的一类系统调用,包括:

  • 如果某些类型文件(如读管道、终端设备和网络设备)的数据不存在,则读操作可能会使调用者永远阻塞;
  • 如果这些数据不能被相同的类型文件立即接受,则写操作可能会使调用者永远阻塞
  • 在某种条件发生之前打开某些类型文件,可能会发生阻塞(例如要打开一个终端设备需要先等待与之连接的调制解调器应答)
  • pause函数(按照定义,它使调用进程休眠直至捕捉到一个信号)和wait函数;
  • 某些ioct操作
  • 某些进程间通信函数

在这些低速系统调用中,一个值得注意的例外是与磁盘I/O有关的系统调用。虽然读、写个磁盘文件可能暂时阻塞调用者(在磁盘驱动程序将请求排入队列,然后在适当时间执行请求期间),但是除非发生硬件错误,IO操作总会很快返回,并使调用者不再处于阻塞状态。

可以用中断系统调用这种方法来处理的一个例子是:一个进程启动了读终端操作,而使用该终端设备的用户却离开该终端很长时间。在这种情况下,进程可能处于阻塞状态几个小时甚至数天,除非系统停机,否则一直如此

可重入函数

进程捕捉到信号并对其进行处理时,进程正在执行的正常指令序列就被信号处理程序临时中断,它首先执行该信号处理程序中的指令。如果从信号处理程序返回,则继续执行在捕捉到信号时进程正在执行的正常指令序列(这类似于发生硬件中断时所做的)。但在信号处理程序中,不能判断捕捉到信号时进程执行到何处。处理信号处理程序以后原来程序中有一部分会重复执行。

在信号处理函数中保证调用安全的函数(可以被安全中断的函数),这些函数是可重入的并被称之为异步信号安全的

大多数函数是不可重入的,因为
(a)已知它们使用静态数据结构;
(b)它们调用mal1oc或free;
(c)它们是标准I/O函数。标准IO库的很多实现都以不可重入方式使用全局数据结构

可靠信号术语与语义

我们需要先定义一些在讨论信号时会用到的术语。首先,当造成信号的事件发生时,为进程产生一个信号(或向一个进程发送一个信号)。事件可以是硬件异常(如除以0)、软件条件(如a1arm定时器超时)、终端产生的信号或调用ki11函数。当一个信号产生时,内核通常在进程表中以某种形式设置一个标志

当对信号采取了这种动作时,我们说向进程递送了一个信号。在信号产生(generation)和递送(delivery)之间的时间间隔内,称信号是未决的(pending)

进程可以选用“阻塞信号递送”。如果为进程产生了一个阻塞的信号,而且对该信号的动作是系统默认动作或捕捉该信号,则为该进程将此信号保持为未决状态,直到该进程对此信号解除了阻塞,或者将对此信号的动作更改为忽略。内核在递送一个原来被阻塞的信号给进程时(而不是在产生该信号时),才决定对它的处理方式。于是进程在信号递送给它之前仍可改变对该信号的动作。进程调用sigpending函数来判定哪些信号是设置为阻塞并处于未决状态的。

每个进程都有一个信号屏蔽字( signal mask.),它规定了当前要阻塞递送到该进程的信号集。对于每种可能的信号,该屏蔽字中都有一位与之对应。对于某种信号,若其对应位已设置,则它当前是被阻塞的。进程可以调用 sigprocmask来检测和更改其当前信号屏蔽字。

函数kill和raise

ki11函数将信号发送给进程或进程组。 ralse函数则允许进程向自身发送信号。

1
2
3
4
5
#include <signal.h> 
int kill(pid_t pid, int signo);
int raise(int signo);

// Both return: 0 if OK, −1 on error

进程将信号发送给其他进程需要权限。超级用户可将信号发送给任一进程。对于非超级用户,其基本规则是发送者的实际用户DD或有效用户ID必须等于接收者的实际用户ID或有效用户ID。

函数alarm和pause

使用alarm函数可以设置一个定时器(闹钟时间),在将来的某个时刻该定时器会超时。当定时器超时时,产生 SIGALRM信号。如果忽略或不捕捉此信号,则其默认动作是终止调用该a1arm函数的进程

1
2
3
4
#include <unistd.h> 
unsigned int alarm(unsigned int seconds);

// Returns: 0 or number of seconds until previously set alarm

每个进程只能有一个闹钟时间。如果在调用a1arm时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次a1arm函数调用的值返回。以前注册的闹钟时间则被新值代替。

如果有以前注册的尚未超过的闹钟时间,而且本次调用的 seconds值是0,则取消以前的闹钟时间,其余留值仍作为a1arm函数的返回值

pause函数使调用进程挂起直至捕捉到一个信号

1
2
3
4
#include <unistd.h> 
int pause(void);

// Returns: −1 with errno set to EINTR

只有执行了一个信号处理程序并返回时,pause才返回

信号集

我们需要有一个能表示多个信号—信号集(signal set)的数据类型。我们将在sigprocmask类函数中使用这种数据类型,以便告诉内核不允许发生该信号集中的信号。不同的信号的编号可能超过一个整型量所包含的位数,所以一般而言,不能用整型量中的一位代表一种信号,也就是不能用一个整型量表示信号集。POSIX 定义数据类型sigset_t以包含一个信号集,并且定义了下列5个处理信号集的函数。

1
2
3
4
5
6
7
#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);

// All four return: 0 if OK, −1 on error int sigismember(const sigset_t *set, int signo); Returns: 1 if true, 0 if false, −1 on error

函数sigprocmask

调用函数sigprocmask可以检测或更改,或同时进行检测和更改进程的信号屏蔽字。

1
2
3
4
#include <signal.h> 
int sigprocmask(int how, const sigset_t *restrict set, sigset_t *restrict oset);

// Returns: 0 if OK, −1 on error

如果oset为是非空指针,则进程当前的信号屏蔽字通过oset返回
其次,若ser是一个非空指针,则参数how指示如何修改当前信号屏蔽字。主要的操作有并集、交集、替代

在调用sigprocmask后如果有任何未决的、不再阻塞的信号,则在sigprocmask返回前至少将其中之一递送给该进程

函数sigpending

sigpending函数返回一信号集,对于调用进程而言,其中的各信号是阻塞不能递送的,因而也一定是当前未决的。该信号集通过set参数返回

1
2
3
4
5
#include <signal.h>

int sigpending(sigset_t *set);

// Returns: 0 if OK, −1 on error

函数sigaction

sigaction函数的功能是检查或修改(或检查并修改)与指定信号相关联的处理动作。此函数取代了UNX早期版本使用的 signal函数。

1
2
3
4
5
#include <signal.h>

int sigaction(int signo, const struct sigaction *restrict act, struct sigaction *restrict oact);

// Returns: 0 if OK, −1 on error

参数signo是要检测或修改其具体动作的信号编号。若act指针非空,则要修改其动作。如果oact指针非空,则系统经由oact指针返回该信号的上一个动作。

函数abort

abort函数的功能是使程序异常终止,此函数将 SIGABRT信号发送给调用进程(进程不应忽略此信号)。

1
2
3
4
#include <stdlib.h> 
void abort(void);

// This function never returns
  • 本文作者: Sixzeroo
  • 本文链接: https://www.liuin.cn/2018/04/18/APUE-信号/
  • 发布时间: 2018年4月18日 - 21时04分
  • 最后更新: 2019年1月6日 - 14时01分
  • 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 许可协议。转载请注明出处!