[2022.7.31][10 信号]使用alarm函数实现sleep,使用alarm函数实现对阻塞操作设置超时

  • Post author:
  • Post category:其他


1 相关函数介绍

1.1 函数kill和raise

kill将信号发送给进程。raise函数则允许进程向自身发送信号。

#include <signal.h>

// 成功,返回0;失败,返回-1
int kill(pid_t pid, int signo);
int raise(int signo);

调用raise(signo)等价于调用kill(getpid(), signo)

kill函数的pid参数分为以下4种情况:

(1)pid>0

将信号发送给进程ID为pid的进程

(2)pid=0

将信号发送给与发送进程属于同一进程组的所有进程(发送进程具有权限向这些进程发送信号)

(3)pid<0

将信号给其进程组ID等于pid绝对值,且发送进程具有权限向其发送信号的所有进程

(4)pid==-1

将信号发送给发送进程有权限向它们发送信号的所有进程

进程将信号发送给其他进程需要权限。超级用户可以将信号发送给任一进程。对于非超级用户,规则是发送者的实际用户ID或有效用户ID必须等于接收者的实际用户ID或有效用户ID。

1.2 函数alarm和pause

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

#include <unistd.h>

//返回值:0或之前设置的闹钟时间的余留值
unsigned int alarm(unsigned int seconds);

每个进程只能有一个闹钟时间。如果在调用alarm时,之前已为该进程注册的闹钟时间还没有超时,则该闹钟时间的余留值作为本次alarm函数调用的返回值。以前注册的闹钟时间则被新值代替。如果有之前注册的尚未超过的闹钟时间,且本次调用的seconds值为0,那么其余留值仍作为alarm函数返回,然后取消以前的闹钟时间。

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

#include <unistd.h>

//返回值:-1,errno设置为EINTR
int pause(void);

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

2 使用alarm函数实现sleep

使用alarm和pause,进程可以使自己休眠一段指定的时间。

#include    <signal.h>
#include    <unistd.h>
#include    <stdio.h>

static void sig_alrm(int signo)
{
    /* nothing to do, just return to wake up the pause */
    printf("sig_alrm !!! \n");
}

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

int main(void) {
    unsigned int unslept;
    
    unslept = sleep1(5);
    printf("unslept:%d \n", unslept);
    return 0;
}

测试结果如下:

xxx$ ./sleep1
alarm start !!!
sig_alrm !!!
alarm done !!!
unslept:0

实际上,sleep1函数有竞争关系:alarm在调用pause之前超时。如果发生这种情况,则在调用pause后将不会再捕捉到信号,调用者将永远被挂起。

可以使用setjmp和longjmp来避免如上的竞争条件:

#include    <setjmp.h>
#include    <signal.h>
#include    <unistd.h>
#include    <stdio.h>

static jmp_buf    env_alrm;

static void sig_alrm(int signo)
{
    printf("sig_alrm!!! \n");
    longjmp(env_alrm, 1);
}

unsigned int sleep2(unsigned int seconds)
{
    if (signal(SIGALRM, sig_alrm) == SIG_ERR)
            return(seconds);
    if (setjmp(env_alrm) == 0) {
        printf("alarm start!!! \n");
        alarm(seconds);        /* start the timer */
        pause();            /* next caught signal wakes us up */
        printf("after pause 1 !!! \n");
    }
    // longjmp to here!
    printf("after pause 2 !!! alarm done!!! \n");
    return(alarm(0));        /* turn off timer, return unslept time */
}

int main(void) {
    unsigned int unslept;
    
    unslept = sleep2(5);
    printf("unslept:%d \n", unslept);
    return 0;
}

测试结果如下:

xxx$ ./sleep2
alarm start!!!
sig_alrm!!!
after pause 2 !!! alarm done!!!
unslept:0

sleep2函数也有问题,涉及到与其他信号的交互。例如如下伪代码:

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)
{
    //耗时操作>5s
}

如上程序若SIGINT的信号处理程序先触发,且是耗时操作>5s。则等sleep2(5) 5s超时后,sig_int程序会被突然打断,引起异常。

3 使用alarm函数实现对阻塞操作设置超时

alarm函数还常用于对可能阻塞的操作设置时间上限值。

如下程序,从标准输入读一行,然后将其写到标准输出上。

#include        <setjmp.h>
#include        <signal.h>
#include        <unistd.h>
#include        <stdio.h>
#include        <stdlib.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");
    if (setjmp(env_alrm) != 0) {
                // longjmp here!!!
                printf("read timeout \n");
                goto out1;
    }

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

    write(STDOUT_FILENO, line, n);
    exit(0);
out1:
        printf("out1 exit(1) >>> \n");
        exit(1);
}

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

测试结果如下:

xxx$ ./read2
read timeout
out1 exit(1) >>>
xxx$ ./read2
safasdf
safasdf