Preventing Glitches in Signal Processing
By Danny Kalev, C++ Pro
Signals are similar to hardware interrupts. They cause a process to
branch from the current execution flow, perform a specific action, and
resume execution from the point of interruption. In the following
sections, I will dissect the ANSI library and
demonstrate how to use its interfaces. We will then proceed to the
By default,
certain signals cause a process to terminate. For example, an attempt
to access memory that the process doesn't own fires a SIGSEGV
("segmentation fault") signal, which in turn aborts the process. Yet
for many applications, this is undesirable. Debuggers, emulators, and
transaction processing systems must handle such signals and continue to
run.
By default, certain signals cause a process to terminate, no questions asked. How can this be prevented?
Install handlers for incoming signals and catch them when they occur.
Step 1: Setting a Handler
A signal is an integer value that the kernel delivers to a process.
When a process receives a signal, it can react in one of the following
manners:
Ignore the signal;
Let the kernel perform the associated with that signal;
Catch the signal, i.e., have the kernel transfer control to a
signal handler and resume the program from the place of interruption
once the handler returns.
A signal handler is a function that the kernel automatically invokes
when a signal occurs. The signal() function registers a handler for a
given signal:
typedef void (*handler)(void);
void * signal(int signum, handler);
The first
argument is the signal's code. The second argument is an address of a
user-defined function to be called when the signal signum occurs.
Instead of
a function's address, the second argument may take two special values:
SIG_IGN and SIG_DFL. SIG_IGN indicates that the signal should be
ignored (note however that the SIGKILL and SIGSTOP signals cannot be
blocked, caught, or ignored); SIG_DFL instructs the kernel to perform
the default action when the signal is raised.
Step 2: Signaling
There are three ways to signal a process:
A process explicitly sends a signal to itself by calling raise();
A signal is sent from another process, say by using the system call or a ;
A signal is sent from the kernel. For instance, when the process
has attempted to access memory it doesn't own or during system shutdown.
Step 3: Raising and Handling a Signal
The following program registers a SIGTERM handler. It then raises a SIGTERM signal thereby causing the handler to run:
#include
#include
using namespace std;
void term(int sig)
{
//..necessary cleanup operations before terminating
cout << "handling signal no." <}
int main()
{
signal(SIGTERM, term); // register a SIGTERM handler
raise(SIGTERM); // will cause term() to run
}
ANSI Limitations
What happens when a process that is already running a handler for a
SIGx signal receives another SIGx signal? One solution is to let the
kernel interrupt the process and run the handler once more. To allow
this, the handler must be re-entrant. However, designing re-entrant handlers is too complicated.
The ANSI C solution to the recurring signal problem is to reset the handler to SIG_DFL before executing the user-defined handler. This is problematic, though.
When two signals occur quickly, the kernel runs the handler for the
first signal and performs the default action for the second one, which
might terminate the process.
Several alternative signal frameworks have evolved in the past
three decades, each of which offers a different solution to the
recurring signal problem. The is the most mature and portable among them.
POSIX Signals
The POSIX signal functions operate on sets of signals packed in a sigset_t datatype:
int sigemptyset(sigset_t * pset); Clears all the signals in pset.
int sigfillset(sigset_t * pset); Fills pset with all available signals.
int sigaddset(sigset_t * pset, int signum); Adds signum to pset.
int sigdelset(sigset_t * pset, int signum); Removes signum from pset.
int sigismember(const sigset_t * pset, int signum); Returns a nonzero value if signum is included in pset, 0 otherwise.
Sigaction() registers a handler for a specific signal:
int sigaction (int signum, struct sigaction * act, struct sigaction *prev);
The sigaction struct describes the kernel's handling of signum:
struct sigaction { sighanlder_t sa_hanlder; sigset_t sa_mask; // list of signals to block unsigned long sa_flags; // blocking mode void (*sa_restorer)(void); // never used };
sa_hanlder holds an address of a function that takes int and returns no
value. It can also take one of two special values: SIG_DFL and SIG_IGN.
Additional Features
The POSIX API offers various services not present in the ANSI library.
These include the ability to block incoming signals and retrieve
currently pending signals.
Blocking Signals
The sigprocmask() blocks and unblocks signals:
int sigprocmask(int mode, const sigset_t* newmask, sigset_t * oldmask);
mode takes one of the following values:
SIG_BLOCK--add the signals in newmask to the current signal mask
SIG_UNBLOCK--remove the signals in newmask from the current signal mask
SIG_SETMASK--block only the signals in newmask
Retrieving Pending Signals Blocked signals wait until the
process is ready to receive them. Such signals are said to be pending
and can be retrieved by calling sigpending():
int sigpending(sigset_t * pset);
Danny Kalev
is a system analyst and software engineer with 13 years of experience,
specializing in C++ and object-oriented analysis and design. He is a
member of the ANSI C++ standardization committee and the author of
ANSI/ISO C++ Professional Programmer's Handbook (Que, 1999, ISBN:
0789720221). See the DevX review here. He can be reached at .
|
摘要:本文将剖析 ANSI 库并示范如何使用其接口。进而讨论
POSIX 信号处理 API。 |
信号处理类似硬件中断。它们促使某个进程从当前的执行控制流程中跳出,以实现特定的行为,待特定处理完成后,再恢复到中断点继续执行。本文将剖析
ANSI 库并示范如何使用其接口。然后,本文将进而讨论 POSIX 信号处理 API。默认情况下,某些信号导致进程终止。例如,试图存取进程不拥有的内存将触发 SIGSEGV
(“段故障”)信号,这时该信号会终止进程的执行。许多应用程序都有这个问题,这是我们不希望看到的。调试,仿真和事务处理系统必须处理这样的信号以便让进程继续执行。那么我们如何防止这种发生呢?
答案是安装一个处理器处理进来的信号并在发生时捕获它们。
第一步:设置信号处理例程
信号是内核传给某个进程的一个整数。当进程接收到信号,它便以以下方式之一响应:
- 忽略该信号;
- 让内核完成与该信号关联的默认操作;
- 捕获该信号,即让内核将控制传给信号处理例程,等信号处理例程执行完毕,然后又从中断的地方恢复程序的执行。
所谓信号处理例程是一个函数,当某个信号发生时,内核会自动调用该函数。signal()函数为给定的信号注册一个处理例程:
typedef void (*handler)(void);
void * signal(int signum, handler);
第一个参数是信号编码。第二个参数用户定义的函数地址,当信号 signum 产生时,handler 所指向的函数被调用。
除了函数地址之外,第二个参数也可以是两个特殊的值:SIG_IGN 和 SIG_DFL。SIG_IGN 表示该信号应被忽略(注意:SIGKILL
和 SIGSTOP 在无论如何都是不能被阻塞、捕获或忽略的);SIG_DFL 指示内核该信号产生时完成默认行为。
第二步:发送信号
向某个进程发送信号有三种方式:
- 进程通过系统调用函数 raise() 显式地发送信号给自己;
- 信号从另一个进程发送,比方说通过 kill() 系统调用或者 Perl 脚本;
- 信号从内核发送。例如,当进程试图存取不属于自己的内存,或在系统关闭期间存取内存时;
第三步:产生和处理信号
下面的程序先注册 SIGTERM 处理例程,然后产生一个 SIGTERM 信号,从而导致该处理例程运行:
#include
#include
using namespace std;
void term(int sig)
{
//..necessary cleanup operations before terminating
cout << "handling signal no." <}
int main()
{
signal(SIGTERM, term); // register a SIGTERM handler
raise(SIGTERM); // will cause term() to run
}
ANSI 的局限
当进入就绪状态的某个进程准备运行一个 SIGx 信号处理例程时又接收到另一个 SIGx
信号,这时会发生什么情况呢?一个方法是让内核中断该进程并再次运行该信号处理例程。为此,这个处理例程必须是可重入的(re-entrant)。但是,设计可重入的处理例程决非易事。ANSI
C 解决重现信号(recurring signals)问题的方法是在执行用户定义的处理例程前,将处理例程重置为 STG_DFL。这样做是有问题的。
当两个信号快速产生时,内核运行第一个信号的处理例程,而对第二个信号则进行默认处理,这样有可能终止该进程。
在过去的三十年中出现了几种另外的信号处理框架,每一种框架对重现信号的处理问题提供了不同的解决方法。POSIX 信号 API 是其中最为成熟的和可移植的一个。
POSIX 信号
POSIX 信号处理函数操作一组打包在 sigset_t 数据类型中信号:
- int sigemptyset(sigset_t * pset); 清除 pset 中的所有信号。
- int sigfillset(sigset_t * pset); 用可获得的信号填充 pset。
- int sigaddset(sigset_t * pset, int signum); 将 signum 添加到 pset。
- int sigdelset(sigset_t * pset, int signum); 从 pset 中删除 signum。
- int sigismember(const sigset_t * pset, int signum); 如果 signum 包含在
pset 中,则返回非零,否则返回 0。
Sigaction() 为特定的信号注册处理例程:
int sigaction(int signum, struct sigaction * act, struct sigaction *prev);
sigaction 结构描述内核处理 signum 的信息:
struct sigaction
{
sighanlder_t sa_hanlder;
sigset_t sa_mask; // 阻塞信号的清单
unsigned long sa_flags; // 阻塞模式
void (*sa_restorer)(void); // 未使用
};
sa_hanlder 保存函数的地址,该函数带一个整型参数,没有返回值。它还可以是两个特别值之一:SIG_DFL 和 SIG_IGN。
额外特性
POSIX API 提供多种 ANSI 库中所没有的服务。其中包括阻塞进入的信号并获取当前未决信号。
阻塞信号
sigprocmask() 阻塞和取消阻塞信号:
int sigprocmask(int mode, const sigset_t* newmask,sigset_t * oldmask);
mode 可取以下值之一:
SIG_BLOCK —— 将 newmask 中的信号添加到当前的信号挡板中。
SIG_UNBLOCK —— 从当前的信号挡板中删除 newmask 信号。
SIG_SETMASK —— 仅阻塞 newmask 中的信号。
获取未决信号
阻塞的信号处于等待状态,直到进程就绪接收它们。这样的信号被称为未决信号,可以通过调用 sigpending() 来获取。
int sigpending(sigset_t * pset);
作者简介
Danny Kalev 是一名通过认证的系统分析师,专攻 C++ 和形式语言理论的软件工程师。1997 年到 2000
年期间,他是 C++ 标准委员会成员。最近他以优异成绩完成了他在普通语言学研究方面的硕士论文。
业余时间他喜欢听古典音乐,阅读维多利亚时期的文学作品,研究 Hittite、Basque 和 Irish Gaelic
这样的自然语言。其它兴趣包括考古和地理。Danny 时常到一些 C++ 论坛并定期为不同的 C++
网站和杂志撰写文章。他还在教育机构讲授程序设计语言和应用语言课程。
阅读(723) | 评论(0) | 转发(0) |