alarm和pause函数

alarm和pause函数

alarm函数允许我们设置一个计时器,该计时器会在未来超时到期。当计时间逾期,会产生SIGALRM信号。如果我们忽略或不捕获该信号,它的默认动作是终止进程。

#include <unistd.h>

unsigned int alarm(unsigned int seconds);

Returns: 0 or number of seconds until previously set alarm

参数seconds是一个单位为秒的时间,当这个时间到了就会产生信号。要知道当时间到了,信号是由内核产生,但是在进程获得控制该信号之前,还是有一段额外的时间,因为这是处理器调度延时。

早期的UNIX系统实现可能会通过早一秒发送信号来减少延时。POSIX.1不允许这么做。

每个处理器只能有一个闹钟。如果当我们调用alarm时,之前该处理器注册的计时器还没有逾期,那么会返回上一个计时器的剩余时间为当前调用的返回值。之前注册的计时器会被新的计时器代替。

如果之前注册的计时器还没有逾期并且如果当前alarm函数的seconds值是0,那么之前的计时器会被取消。之前计时器的剩余时间同样会做为函数返回值返回。

虽然SIGALRM的默认动作是终止进程,但多数进程仍然捕获该信号。如果进程想去终止自己,它能在结束之前执行一些清理工作。如果我们倾向于捕获SIGALRM,我们需要在调用alarm之前小心安装它的信号处理函数。如果先调用了alarm函数并发送了SIGALRM信号,而这时还没安装好信号处理函数,那么进程会终止。

pause函数挂起调用它的进程直到捕获相应信号。

#include <unistd.h>

int pause(void);

Returns: 1 with errno set to EINTR

只有当信号处理函数执行并返回时pause函数才会返回。在这种情况下pause返回1,errno设置成EINTR

例子:

使用alarm和pause,我们可以让一个进程休眠指定时间:

Figure 10.7. Simple, incomplete implementation of sleep
#include     <signal.h>
#include     <unistd.h>

static void
sig_alrm(int signo)
{
    /* nothing to do, just return to wake up the pause */
}

unsigned int
sleep1(unsigned int nsecs)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return(nsecs);
    alarm(nsecs);       /* start the timer */
    pause();            /* next caught signal wakes us up */
    return(alarm(0));   /* turn off timer, return unslept time */
}

这个函数看上去像是sleep函数,但是这个简单实现有三个问题:

  1. 如果调用者已经设置了一个alarm,那么这个计时器会被新的代替。我们能纠正这个问题,通过观察alarm的返回值。如果之前设置的计时器会先到时,那我们要等待它逾期后再设置我们的计时器。如果之前的计时器会在我们的计时器之后逾期,那我们可以先暂时记录剩下的时间,等我们的计时器逾期后,重新设置它的时间。
  2. 我们已经修改了SIGALRM的动作。如果我们正在为另一个调用写函数,那我们应该把当前的动作记录下来,然后再修改动作,等调用完毕后再回复它。我们可以通过signal的返回值来保存之前的动作。
  3. 在第一个alarmpause之间有竞争条件。在系统繁忙时,可能计时器逾期并调用信号处理函数在调用pause之前。如果发生了,调用者会被pause永久的挂起(假设其它信号不会被捕捉)。

例子:

SVR2的sleep实现使用了setjmplongjmp,用于避免竞争条件。本例是个简单的版本,被称作sleep2,如Figure 10.8所示。(为了减少长度,我们没有处理上面所说的三个问题中的1、2)

sleep2函数避免了Figure 10.7的竞争条件。即使pause永远不被执行,当SIGALRM发生时,sleep2函数都会返回。

Figure 10.8. Another (imperfect) implementation of sleep
#include   <setjmp.h>
#include   <signal.h>
#include   <unistd.h>

static jmp_buf  env_alrm;

static void
sig_alrm(int signo)
{
    longjmp(env_alrm, 1);
}

unsigned int
sleep2(unsigned int nsecs)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return(nsecs);
    if (setjmp(env_alrm) == 0) {
        alarm(nsecs);       /* start the timer */
        pause();            /* next caught signal wakes us up */
    }
    return(alarm(0));       /* turn off timer, return unslept time */
}

该函数有一个不容易被发现的问题——当需要和其它信号互动的时候。如果SIGALRM中断了其它信号处理函数,当我们调用longjmp时,我们忽略了它其信号处理函数。

Figure 10.9. Calling sleep2 from a program that catches other signals
#include "apue.h"

unsigned int        sleep2(unsigned int);
static void         sig_int(int);

int
main(void)
{
    unsigned int        unslept;

    if (signal(SIGINT, sig_int) == SIG_ERR)
        err_sys("signal(SIGINT) error");
    unslept = sleep2(5);
    printf("sleep2 returned: %u\n", unslept);
    exit(0);
}

static void
sig_int(int signo)
{
    int            i, j;
    volatile int   k;

    /*
     * Tune these loops to run for more than 5 seconds
     * on whatever system this test program is run.
     */
    printf("\nsig_int starting\n");
    for (i = 0; i < 300000; i++)
        for (j = 0; j < 4000; j++)
            k += i * j;
    printf("sig_int finished\n");
}

Figure 10.9显示了这个方案。在SIGINT处理函数的循环中已经写了,它的执行在某些系统上要很久。我们只是简单的想让它执行的时间比sleeep2时间长一些。整型k被声明为volatile为了防止编译器优化循环中的k。执行Figure 10.9中的程序显示如下:

    $ ./a.out

    ^?                      we type the interrupt character
    sig_int starting
    sleep2 returned: 0

sleep1和sleep2这两个函数的目的是显示使用信号的隐患。下面的章节将会围绕所有这些问题,如何可靠的处理信号并不添加多余的代码。

例子:

alarmsleep函数的通常用法是,放一个计时器去限制某些操作产生阻塞。例如:如果我们read操作一个设备(该设备可能产生阻塞,它是一个“慢”速设备),在read一段时间后超时。Figure 10.10显示了从标准输入读一行,然后写到标准输出。

该程序是在UNIX下的程序,但是这个程序有两个问题。

  1. 该程序有像Figure10.7一样的瑕疵:在第一次调用alarm和调用read之间有竞争条件。如果内核在这两个函数间阻塞进程的时间超过计时器的周期,read可能永久的被阻塞 。多数这种操作使用一个较长的计时器周期,但无论怎样这都是一个竞争条件。
  2. 如果系统调用自动重启,当SIGALRM信号处理函数返回时,read不会被中断。在本例中,超时不会做任何时事。
Figure 10.10. Calling read with a timeout
#include "apue.h"

static void sig_alrm(int);

int
main(void)
{
    int     n;
    char    line[MAXLINE];

    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        err_sys("signal(SIGALRM) error");

    alarm(10);
    if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
        err_sys("read error");
    alarm(0);

    write(STDOUT_FILENO, line, n);
    exit(0);
}

static void
sig_alrm(int signo)
{
    /* nothing to do, just return to interrupt the read */
}

例子:

使用longjmp重写上面的例子。使用这种方法不用担心是否有一个慢系统调用被中断。

这个本版会如希望的那样工作,无论系统是否重新启动被中断的系统调用。实际上,这个版本仍然存在和其它信号交互的问题。

Figure 10.11. Calling read with a timeout, using longjmp
#include "apue.h"
#include <setjmp.h>

static void       sig_alrm(int);
static jmp_buf    env_alrm;

int
main(void)
{
    int     n;
    char    line[MAXLINE];

    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        err_sys("signal(SIGALRM) error");
    if (setjmp(env_alrm) != 0)
        err_quit("read timeout");

    alarm(10);
    if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0)
        err_sys("read error");
    alarm(0);

    write(STDOUT_FILENO, line, n);
    exit(0);
}

static void
sig_alrm(int signo)
{
    longjmp(env_alrm, 1);
}

如果我们想设置一个计时器来限置I/O操作,我们需要使用longjmp,就像上面显示的那样,实际使用过程中它很有可能和其它信号处理函数的交互。另一个选择是使用selectpoll函数。

发表回复