上面已经说明,更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞。使用这种技术可以保护不希望由信号中断的代码临界区。如果希望对一个信号解除阻塞,然后pause以等待以前被阻塞的信号发生,则又将如何呢?假定信号是SIGINT,实现这一点的一种不正确的方法是:
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/* block SIGINT and save current signal mask */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) <0)
err_sys("SIG_BLOCK error");
/* critical region of code */
/* restore signal mask, which unblocks SIGINT */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/* window is open */
pause(); /* wait for signal to occur continue processing */
如果在信号阻塞时,产生了信号,那么该信号的传递就被推迟直到对它解除了阻塞。
对应用程序而言,该信号好像发生在解除对SIGINT
的阻塞和pause
之间(取决于内核如何实现信号)。如果发生了这种情况,或者如果在解除阻塞时刻和 pause
之间确实发生了信号,那么就会产生问题。因为可能不会再见到该信号,所以从这种意义上讲,在此时间窗口中发生的信号丢失了,这样就使得pause永远阻塞。这是早期的不可靠信号机制的另一个问题。
为了纠正此问题,需要在一个原子操作中先恢复信号屏蔽字,然后使进程休眠。这种功能是由sigsuspend
函数所提供的。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
//返回值:−1,并将errno设置为EINTR
进程的信号屏蔽字设置为由sigmask
指向的值。在捕捉到一个信号或发生了一个会终止该进程的信号之前,该进程被挂起。如果捕捉到一个信号而且从该信号处理程序返回,则sigsuspend
返回,并且该进程的信号屏蔽字设置为调用sigsuspend
之前的值。
注意,此函数没有成功返回值。如果它返回到调用者,则总是返回−1,并将 errno
设置为EINTR
(表示一个被中断的系统调用)。
实例
图10-22显示了保护代码临界区,使其不被特定信号中断的正确方法。
#include "apue.h"
static void sig_int(int);
int
main(void)
{
sigset_t newmask, oldmask, waitmask;
pr_mask("program start: ");
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/*
* Block SIGINT and save current signal mask.
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/*
* Critical region of code.
*/
pr_mask("in critical region: ");
/*
* Pause, allowing all signals except SIGUSR1.
*/
if (sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend: ");
/*
* Reset signal mask which unblocks SIGINT.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/*
* And continue processing ...
*/
pr_mask("program exit: ");
exit(0);
}
static void
sig_int(int signo)
{
pr_mask("\nin sig_int: ");
}
图10-22 保护临界区不被信号中断
注意,当sigsuspend
返回时,它将信号屏蔽字设置为调用它之前的值。在本例中,SIGINT
信号将被阻塞。因此将信号屏蔽恢复为之前保存的值(oldmask)。
运行图10-22中的程序得到下面的输出:
$ ./a.out
program start:
in critical region: SIGINT
^C 键入中断字符
in sig_int: SIGINT SIGUSR1
after return from sigsuspend: SIGINT
program exit:
在调用sigsuspend
时,将SIGUSRI
信号加到了进程信号屏蔽字中,所以当运行该信号处理程序时,我们得知信号屏蔽字已经改变了。从中可见,在 sigsuspend
返回时,它将信号屏蔽字恢复为调用它之前的值。
实例
sigsuspend
的另一种应用是等待一个信号处理程序设置一个全局变量。图10-23中的程序用于捕捉中断信号和退出信号,但是希望仅当捕捉到退出信号时,才唤醒主例程。
#include "apue.h"
volatile sig_atomic_t quitflag; /* set nonzero by signal handler */
static void
sig_int(int signo) /* one signal handler for SIGINT and SIGQUIT */
{
if (signo == SIGINT)
printf("\ninterrupt\n");
else if (signo == SIGQUIT)
quitflag = 1; /* set flag for main loop */
}
int
main(void)
{
sigset_t newmask, oldmask, zeromask;
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
if (signal(SIGQUIT, sig_int) == SIG_ERR)
err_sys("signal(SIGQUIT) error");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGQUIT);
/*
* Block SIGQUIT and save current signal mask.
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
while (quitflag == 0)
sigsuspend(&zeromask);
/*
* SIGQUIT has been caught and is now blocked; do whatever.
*/
quitflag = 0;
/*
* Reset signal mask which unblocks SIGQUIT.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
exit(0);
}
图10-23 用sigsuspend等待一个全局变量被设置
此程序的样本输出是:
$ ./a.out
^C 键入中断字符interrupt
^C 再次键入中断字符interrupt
^C 再一次interrupt
^\ $ 用退出符终止
考虑到支持ISO C的非POSIX系统与POSIX系统两者之间的可移植性,在一个信号处理程序中唯一应当做的是为
sig_atomic_t
类型的变量赋一个值。POSIX.1规定得更多一些,它详细说明了在一个信号处理程序中可以安全地调用的函数列表(见图10-4),但是如果这样来编写代码,则它们可能不会正确地在非POSIX系统上运行。
实例
可以用信号实现父、子进程之间的同步,这是信号应用的另一个实例。图 10-24 给出了 8.9节中提到的5个例程的实现,它们是TELL_WAIT
、TELL_PARENT
、TELL_CHILD
、WAIT_PARENT
和WAIT_CHILD
。
AMY : 这里原著的TELL_WAIT没有加下划线,错字。
#include "apue.h"
static volatile sig_atomic_t sigflag; /* set nonzero by sig handler */
static sigset_t newmask, oldmask, zeromask;
static void
sig_usr(int signo) /* one signal handler for SIGUSR1 and SIGUSR2 */
{
sigflag = 1;
}
void
TELL_WAIT(void)
{
if (signal(SIGUSR1, sig_usr) == SIG_ERR)
err_sys("signal(SIGUSR1) error");
if (signal(SIGUSR2, sig_usr) == SIG_ERR)
err_sys("signal(SIGUSR2) error");
sigemptyset(&zeromask);
sigemptyset(&newmask);
sigaddset(&newmask, SIGUSR1);
sigaddset(&newmask, SIGUSR2);
/* Block SIGUSR1 and SIGUSR2, and save current signal mask */
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
}
void
TELL_PARENT(pid_t pid)
{
kill(pid, SIGUSR2); /* tell parent we're done */
}
void
WAIT_PARENT(void)
{
while (sigflag == 0)
sigsuspend(&zeromask); /* and wait for parent */
sigflag = 0;
/* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
}
void
TELL_CHILD(pid_t pid)
{
kill(pid, SIGUSR1); /* tell child we're done */
}
void
WAIT_CHILD(void)
{
while (sigflag == 0)
sigsuspend(&zeromask); /* and wait for child */
sigflag = 0;
/* Reset signal mask to original value */
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
}
图10-24 父子进程可用来实现同步的例程
其中使用了两个用户定义的信号:SIGUSR1
由父进程发送给子进程,SIGUSR2
由子进程发送给父进程。图15-7显示了使用管道的这5个函数的另一种实现。
如果在等待信号发生时希望去休眠,则使用sigsuspend
函数是非常适当的(正如在前面两个例子中所示),但是如果在等待信号期间希望调用其他系统函数,那么将会怎样呢?遗憾的是,在单线程环境下对此问题没有妥善的解决方法。如果可以使用多线程,则可专门安排一个线程处理信号(见12.8节中的讨论)。
如果不使用线程,那么我们能尽力做到最好的是,当信号发生时,在信号捕捉程序中对一个全局变量置1。例如,若我们捕捉SIGINT
和SIGALRM
这两种信号,并用signal_intr
函数设置这两个信号的处理程序,使得它们中断任一被阻塞的慢速系统调用。当进程阻塞在调用read函数等待慢速设备输入时,很可能发生这两种信号(如果设置闹钟以阻止永远等待输入,那么对于SIGALRM
信号,这种情况尤其会发生)。处理这种问题的代码类似于下面所示:
if (intr_flag) /* flag set by our SIGINT handler*/
handle_intr();
if (alrm_flag) /* flag set by our SIGALRM handler */
handle_alrm();
/* signals occurring in here are lost */
while (read( ... ) < 0) {
if (errno == EINTR) {
if (alrm_flag)
handle_alrm();
else if (intr_flag)
handle_intr();
} else {
/* some other error */
}
} else if (n == 0) {
/* end of file */
} else {
/* process input */
}
AMY : 原书的花括号就是上面这样,看的我一脸懵碧,虽然是伪代码但是意思表达的还算清楚,凑合看吧,应该把while小括号里的read挪出来不是在里面,类似while true : n = read if n <0: else if n==0: else:这种形式
在调用read之前测试各全局标志,如果read返回一个中断的系统调用错误,则再次进行测试。如果在前两个if语句和后随的read 调用之间捕捉到两个信号中的任意一个,则问题就发生了。正如代码中的注释所指出的,在此处发生的信号丢失了。调用信号处理程序,它们设置了相应的全局变量,但是read决不会返回(除非某些数据已准备好可读)。
我们希望实现下列操作步骤。
- 阻塞
SIGINT
和SIGALRM
。 - 测试两个全局变量以判别是否发生了一个信号,如果已发生则对此进行处理。
- 调用
read
(或任何其他系统函数)并解除对这两个信号的阻塞,这两个操作应当是一个原子操作。仅当第(3)步是pause
操作时,sigsuspend
函数才能帮助我们。