alarm函数
alarm函数允许我们设置一个在未来的某一时刻终止的定时器,当定时器终止的时候,SIGALRM信号就被发出,如果我们忽略或者不捕获这一信号的话,该信号的默认行为是终止进程.
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
Returns:0 or number of seconds until previously set alarm.
seconds的数值指定了未来信号生成所需要的秒数,当事件到达,信号就会被内核生成,虽然在进程获得控制权开始处理信号之前会因为处理器调度而多花一些额外的时间。
早期版本的UNIX系统实现可以允许信号被提前1秒中发送,但是POSIX.1并不允许这种行为。
这些alarm时钟对于每一个进程来说只能至多拥有一个,如果,当我们调用alarm的时候,之前注册的一个alarm时钟还没有终止,那么早先设置的alarm时钟剩余的描述将被alarm函数作为返回值返回。同时前一个时钟将会被新的数值替换掉。
如果之前注册的alarm时钟还没有终止,而本次调用alarm函数的参数seconds为0,那么前一次alarm定时就会被取消,前一次设置的alarm时钟剩余的秒数将被作为函数的返回值返回。
虽然SIGALRM信号的默认处理是终止进程,但是许多实用alarm时钟的进程会捕获这一信号。如果我们需要捕获信号SIGALRM,我们必须小心:必须在调用alarm之前安装信号处理函数,否则可能由于在信号处理函数安装之前先收到了SIGALRM信号而导致进程终止.
pause函数
函数pause用于挂起当前进程,直到捕获到一个信号:
#include <unistd.h>
int pause(void);
Returns:-1 with errno set to EINTR
pause返回的唯一的时间是一个信号处理函数被调用,然后该信号处理函数返回,在这种情况下,pause将返回-1,并且将errno设置为EINTR.
Example
使用函数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 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*/
}
该函数看起来与函数sleep差不多,但是这个简单的实现有三个问题:
我们已经修改了SIGALRM信号的处理函数。如果我们正在编写一个函数给其他函数调用,我们就必须在设置我们函数的时候保存之前设置的函数,并在我们的函数运行完成之后恢复信号的处理函数。我们可以通过保存signal信号的返回值并在我们函数返回之前重新设置信号的处理函数即可。 如果调用进程已经安装了一个alarm,那么之前安装的alarm将会被sleep1函数中的第一个alarm函数擦除,我们可以根据alarm函数的返回值来纠正这一错误,如果之前设置的定时器剩余的秒数小于当前准备设置的秒数,那么就不应该再设置当前的秒数了,而是应该设置之前定时器剩余的秒数;如果之前设置的定时器剩余的秒数大于我们正准备设置的秒数,那么在我们返回之前,我们应该将定时器重新以使得定时器仍然能够在其预定的时间完成。 在第一次对alarm函数调用到对pause函数的调用之间存在一个竞态条件,因为定时器可能在我们调用pause之前就已经完成了,同时在pause之前调用了信号处理函数,这样一来,调用进程就将因为对于pause函数的调用而永远处于挂起状态(假设并没有捕获到其他信号)
早期版本的sleep函数与我们的上述程序比较相似,但是解决了其中提到的第1和2个问题。对于第三个问题有两种方式改正,第一种是使用函数setjmp,该函数我们将在下一章中讲解,另一种方式是使用函数sigprocmask以及sigsuspend,我们将在10.19中进行讲述。
Example
SVR2实现中的sleep函数使用了函数setjmp以及longjmp来避免上面提到的竞态条件的出现,该函数的一个简单版本,称为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 wake us up*/
}
return (alarm(0)); /*turn off timer,return unslept time*/
}
Figure 10.8 Another (imperfect )implementation of sleep
函数sleep2避免了图10.7中的竞态条件问题,即使pause函数从来没有被执行过,当SIGALRM信号出现的时候,sleep2总是会成功返回。
然而,当我们涉及到与其他信号进行交互的时候,sleep2就会出现一个微妙的错误:如果SIGALRM中断了一些其他信号处理函数,然后当我们执行函数longjmp的时候,我们就会终止掉其他的信号处理函数,图10.9显示了这样的一个场景,在SIGINT处理函数中有一个循环,该循环保证了系统需要执行该函数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 wahtever 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所示的程序的时候,按下终端字符来中断睡眠状态的进程,我们将得到如下的输出:
os@debian:~/UnixProgram/Chapter10$ ./10_9.exe
^C
sig_int starting
sleep2 returned:0
os@debian:~/UnixProgram/Chapter10$
我们可以看出sleep2中的longjmp函数会终止其他的信号处理函数sig_int,即是其他的信号处理函数还没有完成,如果你将SVR2的sleep函数与其他的信号处理函数混合使用的话,就会遇到这样的问题,详见练习10.3
sleep1与sleep2例子的目的是为了展示天真无邪地处理信号的过程中遇到的陷阱,接下来的章节将会接收避开上述问题的方法,因此你可以可靠地处理信号,并不需要与其他的代码段有干涉。
Example
对于函数alarm的常见的使用,除了sleep函数的实现之外,就是设置可能发生阻塞的操作的时间上限,举例来说,如果我们对一个可能引起阻塞的设备执行读取操作,我们想要read函数可以超时退出,在图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*/
}
Figure10.10 Calling read with a timeout
上述代码的处理序列在UNIX应用程序中是常见的,但是该应用程序有两个问题:
- 与图10.7中的例子相同的缺陷:在对函数alarm以及函数read的调用之间出现了一个竞态条件,如果进程在这两个函数的调用之间阻塞了进程,并且阻塞的时间超过了定时器时长,那么read函数就可能永远被阻塞了,许多这种类型的操作都会使用一个很长的定时器,比如说一分钟或者更长,使得上述情况的发生变得不太可能,然而无论如何,这仍然是一个竞态条件。
- 如果系统调用是自动重启的,那么read函数在SIGALRM信号处理函数返回以后并不会被中断,在这种情况下,超时将起不到任何作用。
在这个例子中,我们想要一个慢速的系统调用被中断,我们将在10.14中看到一种可移植的方法来实现上述功能。
Example
然我们再一次使用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);
}
Figure 10.11 Calling read with a timeout, using longjmp.
这一版本能够按照预期的进行工作,无论系统是否会自动重启被中断的系统调用函数,注意,对于上述程序仍然有与其他信号发生冲突的问题。
如果我们想要对一个IO操作设置一个时间限制,我们需要使用函数longjmp,同时需要识别可能与其他信号处理函数发生的冲突。另一个选择是使用函数select或者是poll,这些内容将在14.4.1以及14.4.2中讲解。