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

#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回值:0或以前设置的闹钟时间的余留秒数

参数seconds的值是产生信号SIGALRM需要经过的时钟秒数。当这一时刻到达时,信号由内核产生,由于进程调度的延迟,所以进程得到控制从而能够处理该信号还需要一个时间间隔。

早期的UNIX系统实现曾提出警告,这种信号可能比预定值提前1 s发送。POSIX.1则不允许这样做。

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

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

虽然 SIGALRM 的默认动作是终止进程,但是大多数使用闹钟的进程捕捉此信号。如果此时进程要终止,则在终止之前它可以执行所需的清理操作。如果我们想捕捉 SIGALRM 信号,则必须在调用 alarm 之前安装该信号的处理程序。如果我们先调用alarm,然后在我们能够安装SIGALRM处理程序之前已接到该信号,那么进程将终止。

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

#include <unistd.h>
int pause(void);
//返回值:−1,errno设置为EINTR

只有执行了一个信号处理程序并从其返回时,pause才返回。在这种情况下,pause返回−1, errno设置为EINTR。

实例

使用alarm和pause,进程可使自己休眠一段指定的时间。图10-7中的sleep1函数看似提供了这种功能(其实这里面存在问题,我们很快就会看到)。

#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 seconds)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return(seconds);
    alarm(seconds);        /* start the timer */
    pause();            /* next caught signal wakes us up */
    return(alarm(0));    /* turn off timer, return unslept time */
}

图10-7 sleep简化而不完整的实现

程序中的sleep1函数看起来与将在10.19节中说明的sleep函数类似,但这种简单实现有以下3个问题。

  1. 如果在调用sleep1之前,调用者已设置了闹钟,则它被sleep1函数中的第一次alarm调用擦除。可用下列方法更正这一点:检查第一次调用 alarm 的返回值,如其值小于本次调用alarm的参数值,则只应等到已有的闹钟超时。如果之前设置的闹钟超时时间晚于本次设置值,则在sleep1函数返回之前,重置此闹钟,使其在之前闹钟的设定时间再次发生超时。
  2. 该程序中修改了对 SIGALRM 的配置。如果编写了一个函数供其他函数调用,则在该函数被调用时先要保存原配置,在该函数返回前再恢复原配置。更正这一点的方法是:保存signal函数的返回值,在返回前重置原配置。
  3. 在第一次调用alarm和pause之间有一个竞争条件。在一个繁忙的系统中,可能alarm在调用pause之前超时,并调用了信号处理程序。如果发生了这种情况,则在调用pause后,如果没有捕捉到其他信号,调用者将永远被挂起。

sleep的早期实现与图10-7程序类似,但更正了第1个和第2个问题。有两种方法可以更正第3个问题。第一种方法是使用setjmp,下一个实例将说明这种方法。另一种方法是使用sigprocmask和sigsuspend,10.19节将说明这种方法。

实例

SVR2中的sleep实现使用了setjmp和longjmp(见7.10节),以避免前一个实例的第3个问题中说明的竞争条件。此函数的一个简化版本称为sleep2,示于图10-8中(为了缩短实例程序的长度,程序中没有处理上面所说的第1个和第2个问题)。

#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 seconds)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
        return(seconds);
    if (setjmp(env_alrm) == 0) {
        alarm(seconds);        /* start the timer */
        pause();            /* next caught signal wakes us up */
    }
    return(alarm(0));        /* turn off timer, return unslept time */
}

图10-8 sleep的另一个不完善的实现

在此函数中,已避免了图10-7中具有的竞争条件。即使pause 从未执行,在发生SIGALRM时,sleep2函数也返回。

但是,sleep2函数中却有另一个难以察觉的问题,它涉及与其他信号的交互。如果SIGALRM中断了某个其他信号处理程序,则调用longjmp会提早终止该信号处理程序。图10-9显示了这种情况。SIGINT 处理程序中包含了for 循环语句,它在作者所用系统上的执行时间超过5s,也就是大于sleep2的参数值,这正是我们想要的。整型变量k说明为volatile,这样就阻止了优化编译程序去除循环语句。

#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");
}

图10-9 在一个捕捉其他信号的程序中调用sleep2

执行图10-9中的程序,可以通过键入中断字符来中断休眠,运行结果如下:

$ ./a.out^C          键入中断字符
sig_int starting
sleep2 returned: 0

从中可见sleep2函数所引起的longjmp使另一个信号处理程序sig_int提早终止,即使它未完成也会如此。如果将SVR2的sleep函数与其他信号处理程序一起使用,就可能碰到这种情况。见习题10.3。

sleep1 和sleep2 函数的这两个实例是告诉我们在涉及信号时需要有精细而周到的考虑。下面几节将说明解决这些问题的方法,使我们能够可靠地、在不影响其他代码段的情况下处理信号。

实例

除了用来实现sleep函数外,alarm还常用于对可能阻塞的操作设置时间上限值。例如,程序中有一个读低速设备的可能阻塞的操作(见 10.5 节),我们希望超过一定时间量后就停止执行该操作。图10-10实现了这一点,它从标准输入读一行,然后将其写到标准输出上。

#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 */
}

图10-10 带时间限制调用read

这种代码序列在很多UNIX应用程序中都能见到,但是这种程序有两个问题:

  1. 图10-10中的程序具有与图10-7 中的程序相同的问题:在第一次alarm 调用和read调用之间有一个竞争条件。如果内核在这两个函数调用之间使进程阻塞,不能占用处理机运行,而其时间长度又超过闹钟时间,则read可能永远阻塞。大多数这种类型的操作使用较长的闹钟时间,例如1分钟或更长一点,使这种问题不会发生,但无论如何这是一个竞争条件。
  2. 如果系统调用是自动重启动的,则当从SIGALRM信号处理程序返回时,read并不被中断。在这种情形下,设置时间限制不起作用。

在这里我们确实需要中断慢速系统调用。我们将在10.14节对此进行详细讨论。

AMY: Richard是在上世纪末去世的,本书最早在1992年出版,当时的计算机性能都不高,所以类似与上述问题应该算是真正的问题,但是在现在的操作系统中可能遇到这种问题的概率并不高,只供参考.

实例

让我们用 longjmp 再实现前面的实例。使用这种方法无需担心一个慢速的系统调用是否被中断,见图10-11。

#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);
}

不管系统是否重新启动被中断的系统调用,该程序都会如所预期的那样工作。但是要知道,该程序仍旧有和图10-8中的程序相同的与其他信号处理程序交互的问题。

如果要对I/O操作设置时间限制,则如上所示可以使用longjmp,当然也要清楚它可能有与其他信号处理程序交互的问题。另一种选择是使用select或poll函数,14.4.1节和14.4.2节将对它们进行说明。

results matching ""

    No results matching ""