16、sigsuspend函数
我们已经知道如何改变一个进程的signal mask来阻塞或者取消阻塞被选择的信号。我们可以使用这个技术来保护代码的关键区域,防止它被信号打断。如果我们想要取消阻塞一个信号然后pause,并等待之前被阻塞的信号发生,这会怎样?假设信号是SIGINT,正确的做法是:
sigset_t newmask, oldmask;
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/*阻塞SIGINT信号,然后保存当前的signal mask*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/*代码的关键区域*/
/*重新设置signal mask,这样会取消对SIGINT的阻塞*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/* 容易发生问题的时间窗口在这里出现了!!!(也就是重新设置signal mask和suspend这两步之间易发生问题) */
pause(); /* wait for signal to occur */
/*后续的处理*/
当信号在被阻塞的时候被发送给进程,这个信号的发送将会被延迟,直到信号被取消阻塞。对于应用程序来说,这个看起来就好象信号是在取消阻塞和调用pause之间发生的(取决于内核如何执行信号相关的处理).如果信号真的是在取消对它的阻塞和pause调用之间发生了,那么我们就会有一个问题。然后出现在上述容易发生问题的时间窗口中的这个信号,都会丢失,我们无法再看到这个信号了,这样pause就会被永远地阻塞在了那里。这个问题,也是早期非可靠信号机制中的另一种问题。
为了修正这个问题,我们需要一个方法,可以把重新设置信号,以及将进程设置到sleep状态这两步操作变成一个单一的原子性质的操作。通过sigsuspend函数就可以实现这个目的。
#include <signal.h>
int sigsuspend(const sigset_t *sigmask);
返回:返回1(其值实际为-1),并且将errno设置成EINTR.
进程的signal mask被设置成参数sigmask指向的值,然后进程被suspend,直到信号被捕获,或者直到一个信号发生导致进程被终止。如果一个信号被捕获,并且如果信号处理函数返回了,那么sigsuspend会返回,然后进程的signal mask被设置成调用sigsuspend之前的值。
注意,这个sigsuspend没有成功的返回值,如果它从调用中返回了,那么它一定会返回一个1,并且同时将errno设置成EINTR(表征一个被打断的系统调用)。
举例:
static void sig_int(int signo)
{
pr_mask("\nin sig_int: ");
}
int main(void)
{
sigset_t newmask, oldmask, waitmask;
pr_mask("program start: ");/*打印字符串以及signal mask.*/
if (signal(SIGINT, sig_int) == SIG_ERR)
err_sys("signal(SIGINT) error");
sigemptyset(&waitmask);
sigaddset(&waitmask, SIGUSR1);
sigemptyset(&newmask);
sigaddset(&newmask, SIGINT);
/*
* 阻塞SIGINT 信号并且保存当前的signal mask.
*/
if (sigprocmask(SIG_BLOCK, &newmask, &oldmask) < 0)
err_sys("SIG_BLOCK error");
/*
* 关键代码区域.
*/
pr_mask("in critical region: ");/*打印字符串以及signal mask.*/
/*
* 暂停并允许除了SIGUSR1之外所有的信号.
* 这里就将重新设置signal mask和suspend合并为一个原子操作了,没有时间窗口问题了。
*/
if (sigsuspend(&waitmask) != -1)
err_sys("sigsuspend error");
pr_mask("after return from sigsuspend: ");
/*
* 恢复SIGINT信号的阻塞.
*/
if (sigprocmask(SIG_SETMASK, &oldmask, NULL) < 0)
err_sys("SIG_SETMASK error");
/*
* 继续处理...
*/
pr_mask("program exit: ");
exit(0);
}
上面的例子,给出了保护关键代码区域不被指定的信号所打扰的正确的方法。
需要注意的是,当sigsuspend返回的时候,它会把signal mask设置成这个调用之前的signal mask.在这个例子中,SIGINT信号将会被阻塞。所以我们后来将signal mask重新设置为之前我们保存的值(oldmask).
运行上述代码程序,输出大致如下:
$ ./a.out
program start:
in critical region: SIGINT
^? 键入中断信号字符.
in sig_int: SIGINT SIGUSR1
after return from sigsuspend: SIGINT
program exit:
在调用sigsuspend的时候,我们把SIGUSR1添加到现有的signal mask中了,所以当信号函数运行的时候,我们可以看到signal mask实际上被改变了。在sigsuspend返回的时候,我们可以看到signal mask 被恢复到调用之前的值了。
第二个例子省略了,主要思想就是,虽然前面posix列出了许多可重入的函数,但是为了让非posix的系统尽可能的好用,我们在信号处理函数中只设置标记,而不是调用什么系统调用。
第三个例子涉及到父子进程同步,有待仔细考虑其中的问题…
译者注
原文参考
17、abort函数
在前面我们提到过abort函数会导致程序非正常地终止,
#include <stdlib.h>
void abort(void);
这个函数不会返回。
这个函数会发送SIGABRT信号给调用者.(进程不应该忽略这个信号.)ISOC要求调用abort将会通过raise(SIGABRT)给主机环境发送一个非成功的标记。
ISO C要求,如果信号被捕获,并且信号处理函数返回了,那么abort也不会返回到它的调用者处。如果信号被捕获了,只能通过调用exit,_exit,_Exit,longjmp,或者siglongjmp让信号处理函数不返回。POSIX.1也指定abort会覆盖进程对信号的阻塞和忽略(???)。
让进程捕获SIGABRT的目的是让它在进程终止之前可以进行它想要进行的清理工作。如果进程没有从信号处理函数中终止,POSIX.1已经说了,当信号处理函数返回的时候,abort会终止进程。
这个函数的ISO C标准,让具体实现取决定是否刷新输出流,或者删除临时文件。POSIX.1更进一步要求,如果调用abort终止了进程,那么对进程打开的I/O流的影响就像进程在终止之前为每一个流调用了fclose一样。
早期版本的System V会在abort函数中产生SIGIOT信号。此外,进程有可能会忽略或者捕获这个信号,并且从信号处理函数中返回,这时候,abort函数会返回到它的调用之处。
4.3BSD会产生SIGILL信号。在做这个之前,4.3BSD函数会取消信号的阻塞,并且重置信号处理动作为SIG_DFL(终止进程并产生core文件)。这个会阻止一个进程忽略或者捕获信号。
历史上,对abort的实现对标准I/O流的处理有所不同。对于保守点的编程以及更好的可移植的角度来说,如果我们想要标准I/O流被刷新,我们需要在调用abort之前来做它们。
由于大多数UNIX 系统在创建了tmpfile之后立即对tmpfile执行unlink操作,ISO C会警告这样的文件,但是不会考虑我们。
举例
void abort(void) /* POSIX风格的abort函数 */
{
sigset_t mask;
struct sigaction action;
/*
* 调用者不能忽略SIGABRT,如果忽略则设置成默认的.
*/
sigaction(SIGABRT, NULL, &action);
if (action.sa_handler == SIG_IGN) {
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL);
}
if (action.sa_handler == SIG_DFL)
fflush(NULL); /* 刷新所有打开的标准输入输出流 */
/*
* 要确保调用者没有对SIGABRT进行阻塞.
*/
sigfillset(&mask);
sigdelset(&mask, SIGABRT); /* signal mask中只有 SIGABRT 被排除在外 */
sigprocmask(SIG_SETMASK, &mask, NULL);
kill(getpid(), SIGABRT); /* 发送信号 */
/*
* 如果我们到达了这里,说明进程已经捕获到了 SIGABRT 信号并且返回。
*/
fflush(NULL); /* 刷新所有打开的标准输入输出流 */
action.sa_handler = SIG_DFL;
sigaction(SIGABRT, &action, NULL); /* 设置信号为默认的处理 */
sigprocmask(SIG_SETMASK, &mask, NULL); /* just in case ... */
kill(getpid(), SIGABRT); /* and one more time */
exit(1); /* this should never be executed ... */
}
例子给出了一个根据POSIX.1指定的abort函数的实现。
我们首先查看是否有默认的动作发生,如果有,那么我们会刷新所有打开的标准I/O流。这一点和fclose所有打开的流的效果不是一样的,因为前者是只刷新流并没有关闭它们,但是当进程终止的时候,系统会关闭所有打开的文件。如果进程捕获到了信号并且返回了,那么我们再次刷新流,因为期间进程可能又会产生一些输出的。如果进程捕获到了信号,并且调用了_exit或者_Exit,只有这种情况我们不这样做。这时候,内存中任何没有被刷新的标准I/O缓存都会被丢弃。我们假设调用者在不想要刷新缓存的时候这样做。
根据前面所说过的,如果调用kill给caller发送一个信号,并且如果信号没有被阻塞,那么这个信号会在kill返回的时候被发送给进程。我们阻塞了除SIGABRT之外的所有信号,所以我们知道,如果调用的kill返回了,那么进程就捕获到了信号,并且信号处理函数已经返回了。
网友评论