alarm函数允许我们设置一个计时器,它将在一个指定的时间到期。当这个计时器到期时,SIGALRM信号被产生。如果我们忽略或不捕获这个信号,它的默认行为是终止这个进程。
- #include <unistd.h>
- unsigned int alarm(unsigned int seconds);
- 返回0或上次设置的警报到现在的时间。
seconds值是将来这个信号应该产生时的时钟秒数。小心当时间发生时,信号被内核产生,但是在进程得到处理信号的控制前可以有额外的时间,因为处理器的调度延迟。
早期UNIX系统实现警告信号也可能早最多1秒被发送。POSIX.1不允许这个。
每个进程只有一个这样的警报。如果当我们调用alarm时一个之前为进程注册的闹钟还没有过期,那个闹钟还剩的秒数被返回。前一个注册的闹钟被新的值替代。
如
果前一个注册的闹钟没有过期而seconds的值为0,那么前一个闹钟被取消。然后如果进程想要终止,它可以在终止前执行任何所需的清理。如果我们要捕获
SIGALRM,那要小心在调用alarm之前安装它的信号处理器。如果在安装信号处理器前,我们先调用alarm并被发送一个SIGALRM,我们的进
程会终止。
pause函数暂停调用进程,直到一个信号被捕获。
- #include <unistd.h>
- int pause(void);
- 返回-1,errno设置为EINTR。
pause返回的唯一时机是一个信号处理器被执行并返回。在这种情况下,pause返回-1,errno设置为EINTR。
通过使用alarm和pause,我们可以把进程催眠一个指定的时间。下面的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 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函数(10.19节),但是这个简单的实现有三个问题:
1、如果调用者已经设置了一个闹钟,那个闹钟被第
一个alarm调用清除。我们可以通过查看第一个alarm的返回值来修正它。如果之前设置的闹钟剩余的秒数比这个参数小,那么我们只等到之前的闹钟到
期。如果前面设置的闹钟在我们之后,在返回前我们要重调这个闹钟以使它在将来所定的时间发生。
2、我们已经为SIGALRM修改了布署。如果我们正在为其它人写一个调用的函数,那么我们应该在被调用时保存这个布署,在完成时恢复它。我们可以通过保存signal的返回值并在return之前重置它来修正。
3、在第一次alarm调用和pause调用之间有竞争条件。在一个繁忙的系统,有可能在我们调用pause之前闹钟已经到期而信号处理器被调用。如果它发生了,调用者会永远被puase调用挂起(假定没有其它信号被捕获。)
sleep的早期实现看起来像我们的程序,但是修正了描述的第一个和第二个问题。有两种方法可以修正问题3。第一个使用setjmp,我们在下面的例子里展示。另一个是使用sigprocmask和sigsuspend,在10.19节讨论。
下面的代码使用setjmp和longjmp(10.7节)实现sleep,避免前一个例子描述的问题3。一个称为sleep2的另一个简单版本。(为了减少代码,我们不处理问题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 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 */
- }
sleep2函数避免了前面的竞争条件。即使pause没有执行,sleep2函数当SIGALRM发生时也会返回。
然而,涉及其它信号交互的sleep2有另一个诡异的问题。如果SIGALRM中断了其它一些信号处理器,当我们调用longjmp时,我们会中止其它的信号处理器。下面的代码展示了这个场景:
- static void sig_int(int);
- int
- main(void)
- {
- unsigned int unslept;
- if (signal(SIGINT, sig_int) == SIG_ERR) {
- printf("signal(SIGINT) error\n");
- exit(1);
- }
- 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 secondes
- * 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");
- }
SIGINT处理器里的循环在作者使用的系统上会超过5秒。我们简单地想它比sleep2的参数执行的长些。整型k被声明为volatile来避免一个优化编译器舍弃这个循环。运行结果如下:
$ ./a.out
^C (中断字符)
sig_int starting
sleep2 returned: 0
我们可以看到sleep2函数里的longjmp中止了其它信号处理器,sig_int,尽管它还没有完成。如果你把SVR2的sleep函数和其它信号处理器混在一起时,这将是你碰到的。
sleep1和sleep2这两个例子的目的,是展示天真地处理信号时的问题。后面几节将展示解决所有这些问题的方法,所以我们可以可靠地处理信号,而不影响其它代码。
alarm
函数的一个普遍用法,除了实现sleep,还有为一个可以阻塞的操作加上时间上限。例如,如果我们在一个可以阻塞的设备上有一个读操作(一个“慢”设
备,10.5节),我们可能想这个读操作在一段时间后到时。下面的代码做了这件事,从标准输入里读取一行并写到标准输出上:
- #include <signal.h>
- #include <stdio.h>
- #include <unistd.h>
- #define MAXLINE 4096
- static void sig_alrm(int);
- int
- main(void)
- {
- int n;
- char line[MAXLINE];
- if (signal(SIGALRM, sig_alrm) == SIG_ERR) {
- printf("signal(SIGALRM) error\n");
- exit(1);
- }
- alarm(10);
- if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) {
- printf("read error\n");
- exit(1);
- }
- alarm(0);
- write(STDOUT_FILENO, line, n);
- exit(0);
- }
- static void
- sig_alrm(int signo)
- {
- /* nothing to do, just return to interrupt the read */
- }
这段代码在UNIX应用上很普遍,但这个程序有两个问题。
1、程序有和sleep1一样的瑕疵:第一次调用alarm和
read的调用之间有竞争条件。如果内核在这两个函数之间阻塞了比闹钟期限更长的时间,read可能会永远阻塞。多数这种类型的操作使用一个长的闹钟期,
比如一分钟或更多,使它不太可能发生。尽管如此,它是一个竞争条件。
2、如果系统调用自动重启,当SIGALRM信号处理器返回时read不被中断。这种情况下,计时不做任何事。
这里,我们明确地想要一个系统调用被中断。POSIX.1没有给我们一个可移植的方法来做这件事,但是SUS的XSI扩展可以。我们将在10.4节讨论更多。
让我们重新用longjmp实现前一个例子,这种方法,我们不用担心一个慢的系统调用是否被中断:
- #include <setjmp.h>
- #include <signal.h>
- #include <unistd.h>
- #define MAXLINE 4096
- 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) {
- printf("signal(SIGALRM) error\n");
- exit(1);
- }
- if (setjmp(env_alrm) != 0) {
- printf("read timeout\n");
- exit(1);
- }
- alarm(10);
- if ((n = read(STDIN_FILENO, line, MAXLINE)) < 0) {
- printf("read error\n");
- exit(1);
- }
- alarm(0);
- write(STDOUT_FILENO, line, n);
- exit(0);
- }
- static void
- sig_alrm(int signo)
- {
- longjmp(env_alrm, 1);
- }
这个版本如期望的一样,不管系统是否重启了中断的系统调用。但是,注意我们仍然有和其它信号处理器交互的问题,和早先的代码一样。