如何在C/Linux中的计时器到期后执行代码post阻塞操作?

How to execute code post blocking operation after expiry of timer in C/Linux?

我有一种情况需要在阻塞操作之前启动到期计时器。如果控制在预定的时间间隔内没有从阻塞操作中出来,我需要继续前进并恢复其他活动。该程序不应在计时器到期时终止。相反,我必须执行阻塞操作之后的代码。在下面的代码片段中,当计时器到期时,我需要去 B 点而不是退出。工作环境为ubuntu18.04,代码为c。我探索了 setitimer 函数调用以及与信号处理程序相关联的函数调用。但是控制并没有跳过阻塞操作并继续执行后续代码。

  Program to create a semaphore and setting the semaphore for use by another program.

  int main(int argc,char *argv[])
  {
    key_t SemKey = 0x12345 ;

    int SemId = -1;
    int NumOfSems = 2 ;
    int Flag ;
    int RetStat ;

    char choice = 'n' ;

    struct
    {
            int val;
            struct semid_ds *buf;
            ushort array[2] ;
    } Arg ;


    Flag = IPC_CREAT | IPC_EXCL | 0666 ;

    SemId = semget(SemKey,NumOfSems,Flag ) ;
    if ( SemId == -1 )
    {
            perror("semget");
            exit(EXIT_FAILURE);
    }

    Arg.array[0] = 0 ;
    Arg.array[1] = 0 ;

    RetStat = semctl(SemId, 0, SETALL, Arg.array);

    if (RetStat == -1)
    {
            perror("semctl");
            exit(EXIT_FAILURE);
    }

    printf("\nPress y or Y to continue ahead.\n");
    scanf("%c",&choice);

    if ( ( choice != 'y') && ( choice != 'Y'))
            exit(EXIT_FAILURE);

    if ( ( choice != 'y') && ( choice != 'Y'))
            exit(EXIT_FAILURE);


    RetStat = semctl( SemId,0, SETVAL, 1 );
    if( RetStat < 0 )
    {
            perror( "SET SEMOP Failure: " );
            exit(EXIT_FAILURE);
    }

    return EXIT_SUCCESS ;
  }

  Program that waits for the semaphore set by the above program.

  int DriverModule(int );

  int main(int argc,char *argv[])
  {
    key_t SemKey = 0x12345 ;
    int SemId ;
    u_int Flag;

    Flag = 0666 ;

    SemId = semget( SemKey, 0, Flag );
    if ( SemId == -1 )
    {
            perror("semget");
            exit(EXIT_FAILURE);
    }

    DriverModule(SemId) ;

    return EXIT_SUCCESS ;
  }

  #define MAX_ITERATIONS 100

  struct itimerval timer;

  int WaitForSemaphore(int ,unsigned short ) ;

  void timer_handler (int signum)
  {
    static int count = 0;
    printf ("timer expired %d times\n", ++count);

    if ( count >= MAX_ITERATIONS ) // Stop the timer
    {
            printf("\n\nForcing Time Out Termination.....\n\n");

            timer.it_value.tv_sec = 0;
            timer.it_value.tv_usec = 0;
            timer.it_interval.tv_sec = 0;
            timer.it_interval.tv_usec = 0;

            setitimer (ITIMER_VIRTUAL, &timer, NULL);

            return ;
    }

   }

  int DriverModule (int SemId)
  {
    struct sigaction sa;

    /* Install timer_handler as the signal handler for SIGVTALRM. */
    memset (&sa, 0, sizeof (sa));

    sa.sa_flags = SA_SIGINFO;
    sa.sa_handler = &timer_handler;
    sigaction (SIGVTALRM, &sa, NULL);

    /* Configure the timer to expire after 250 msec... */
    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = 250000;
    /* ... and every 500 msec after that. */
    timer.it_interval.tv_sec = 0;
    timer.it_interval.tv_usec = 500000;

    /* Start a virtual timer. It counts down whenever this process is  
     executing. */
    setitimer (ITIMER_VIRTUAL, &timer, NULL);

    printf("\nBefore calling wait for semaphore.......\n");

    // Waiting for sempahore
    if( !WaitForSemaphore( SemId, 0) )
    {
            printf("\nUnable to get sempahore.\n");
            return 0 ;
    }

    printf("\nAfter calling after calling wait for semaphoe module.........\n");

    return 1 ;
  }

  int WaitForSemaphore(int SemId,unsigned short SemNum )
  {
    struct sembuf SemBuf;

    int  RetStat;
    unsigned int NoOfSemOps;

    SemBuf.sem_num = SemNum;
    SemBuf.sem_op = -1;
    SemBuf.sem_flg = 0;

    NoOfSemOps = 1;

    RetStat = semop( SemId, &SemBuf, NoOfSemOps );
    if( RetStat < 0 )
    {
            if( errno == EINTR )
            {
                    WaitForSemaphore( SemId, SemNum );
            }
            else
            {
                    perror( "Wait SEMOP Failure: " );
                    return 0 ;
            }
    }

    return 1 ;

   }

好的,主要问题是使用 ITIMER_VIRTUAL。当进程为 运行 时,此 [类型] 计数器 递减。如果进程正在执行系统调用,则它 不是 运行。

因此,我们必须改用 ITIMER_REAL。而且,如果我们这样做,它会生成一个 SIGALRM 信号 [并且 而不是 一个 SIGVTALRM 信号。

进行这些更改后,出现了另一个问题。

定时器必须被禁用它正在保护的系统调用之后(例如semop)。否则,unexpired 计时器(即 semop 确实 not 超时)可能会中断 another/subsequent/unrelated 系统调用(例如 readwrite、等等)。

因此,在下面的代码中,我们需要(例如):

timer_set(250000);
semop(...);
timer_set(0);

另外,请注意,可以从信号处理程序[安全]执行有限 系统调用[and/or 库函数调用]。值得注意的是,printf 不能 不能 用于信号处理程序。

而且,即使进行“信号安全”系统调用也需要信号处理程序 [在进入时] 保存并在 [退出时] 恢复 errno 的原始值。

否则,基线代码(例如 [中断] semop 之后)将看不到正确的 errno 值(例如 EINTR),而是 errno 值对于信号处理程序选择执行的任何系统调用。


我不得不对代码进行相当多的重构才能制作出合理的测试程序。

我将这两个程序组合成一个测试程序,以便为正常情况和超时情况生成适当的单元测试,由于计时问题和竞争条件,这对于它们作为单独的程序来说很难。

我还增强了调试打印输出。

无论如何,这是代码:

// create a semaphore and setting the semaphore for use by another program.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <signal.h>
#include <unistd.h>
#include <stdarg.h>
#include <time.h>

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/time.h>
#include <sys/wait.h>

int opt_n;
int opt_y;

#define MAX_ITERATIONS 100

int itype;
struct itimerval timer;
const char *pgmtail;

int WaitForSemaphore(int, unsigned short);
int DriverModule(int);

typedef long long tsc_t;

tsc_t tvzero;

tsc_t
tscget(void)
{
    struct timespec ts;
    tsc_t tsc;

    clock_gettime(CLOCK_MONOTONIC,&ts);

    tsc = ts.tv_sec;
    tsc *= 1000000000;
    tsc += ts.tv_nsec;

    tsc -= tvzero;

    return tsc;
}

double
tscsec(tsc_t tsc)
{
    double sec;

    sec = tsc;
    sec /= 1e9;

    return sec;
}

void
dbgprt(const char *fmt,...)
{
    va_list ap;
    char buf[1000];
    char *bp = buf;

    bp += sprintf(bp,"%.9f %s ",tscsec(tscget()),pgmtail);

    va_start(ap,fmt);
    bp += vsprintf(bp,fmt,ap);
    va_end(ap);

    fputs(buf,stdout);
    fflush(stdout);
}

int
pgma(void)
{
    key_t SemKey = 0x12345;

    pgmtail = "pgma";

    int SemId = -1;
    int NumOfSems = 2;
    int Flag = 0;
    int RetStat;

#if 0
    char choice = 'n';
#endif

#if 0
    struct {
        int val;
        struct semid_ds *buf;
        ushort array[2];
    } Arg;
#endif

    Flag |= IPC_CREAT;
    //Flag |= IPC_EXCL;
    Flag |= 0666;

    SemId = semget(SemKey, NumOfSems, Flag);
    if (SemId == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

#if 0
    Arg.array[0] = 0;
    Arg.array[1] = 0;

    RetStat = semctl(SemId, 0, SETALL, Arg.array);

    if (RetStat == -1) {
        perror("semctl");
        exit(EXIT_FAILURE);
    }
#endif

    for (int phase = 0;  phase <= opt_y;  ++phase) {
        int setval = phase;
        dbgprt("pgma: SET phase=%d\n",phase);

        RetStat = semctl(SemId, 0, SETVAL, setval);
        if (RetStat < 0) {
            perror("SET SEMOP Failure: ");
            exit(EXIT_FAILURE);
        }

        dbgprt("USLEEP/BEF\n");
        usleep(1000 * 1000);
        dbgprt("USLEEP/AFT\n");
    }

    return EXIT_SUCCESS;
}

// Program that waits for the semaphore set by the above program.

int
pgmb(void)
{
    key_t SemKey = 0x12345;
    int SemId;

    pgmtail = "pgmb";

// NOTE/BUG: we must add IPC_CREAT if we start pgmb first
    u_int Flag = 0;
    Flag |= 0666;
#if 1
    Flag |= IPC_CREAT;
#endif
    SemId = semget(SemKey, 2, Flag);

    if (SemId == -1) {
        perror("semget");
        exit(EXIT_FAILURE);
    }

    DriverModule(SemId);

    dbgprt("pgmb: complete\n");

    return EXIT_SUCCESS;
}

char *
exmsg(int status)
{
    static char buf[1000];
    char *bp = buf;

    bp += sprintf(bp,"status=[%8.8X ",status);

    do {
        if (WIFEXITED(status)) {
            bp += sprintf(bp,"exited status=%d", WEXITSTATUS(status));
            break;
        }

        if (WIFSIGNALED(status)) {
            bp += sprintf(bp,"killed by signal %d", WTERMSIG(status));
            break;
        }

        if (WIFSTOPPED(status)) {
            bp += sprintf(bp,"stopped by signal %d", WSTOPSIG(status));
            break;
        }

        bp += sprintf(bp,"continued");
    } while (0);

    bp += sprintf(bp,"]");

    return buf;
}

void
testit(int newval)
{
    int code;
    pid_t pid;

    opt_y = newval;

    for (int phase = 1;  phase <= 2;  ++phase) {
        // start the receiver
        pid = fork();
        if (pid != 0) {
            dbgprt("testit: pid=%d\n",pid);
            continue;
        }

        switch (phase) {
        case 1:
            code = pgma();
            break;
        case 2:
            code = pgmb();
            break;
        default:
            code = 99;
            break;
        }

        exit(code);
    }

    while (1) {
        int status;

        pid = wait(&status);
        if (pid <= 0)
            break;

        dbgprt("main: pid %d reaped -- %s\n",pid,exmsg(status));
    }
}

int
main(int argc,char **argv)
{
    //pid_t pid;

    --argc;
    ++argv;

    pgmtail = "main";

    tvzero = tscget();

    for (;  argc > 0;  --argc, ++argv) {
        char *cp = *argv;
        if (*cp != '-')
            break;

        switch (cp[1]) {
        case 'n':
            opt_n = ! opt_n;
            break;
        }
    }

    opt_y = ! opt_n;
    dbgprt("pgma: opt_y=%d\n",opt_y);

    //testit(0);
    testit(1);

    return 0;
}

void
timer_set(int usec)
{

    dbgprt("timer_set: SET usec=%d\n",usec);

    timer.it_value.tv_sec = 0;
    timer.it_value.tv_usec = usec;

    /* ... and every 500 msec after that. */
// NOTE/BUG: timer must _not_ be rearmed -- can abort unrelated syscalls
    timer.it_interval.tv_sec = 0;
#if 0
    timer.it_interval.tv_usec = 500000;
#else
    timer.it_interval.tv_usec = 0;
#endif

    setitimer(itype, &timer, NULL);
}

void
timer_handler(int signum)
{
    static int count = 0;

// NOTE/BUG: printf may _not_ be called from a signal handler
    //dbgprt("timer expired %d times\n", ++count);

    // Stop the timer
// NOTE/BUG: disabling now done elsewhere
    if (count >= MAX_ITERATIONS) {
        //dbgprt("Forcing Time Out Termination.....\n");

        timer.it_value.tv_sec = 0;
        timer.it_value.tv_usec = 0;
        timer.it_interval.tv_sec = 0;
        timer.it_interval.tv_usec = 0;

#if 0
        setitimer(ITIMER_VIRTUAL, &timer, NULL);
#else
        setitimer(ITIMER_REAL, &timer, NULL);
#endif

        return;
    }
}

int
DriverModule(int SemId)
{
    struct sigaction sa;
    sigset_t set;
    int signo;

#if 0
    itype = ITIMER_VIRTUAL;
    signo = SIGVTALRM;
#else
    itype = ITIMER_REAL;
    signo = SIGALRM;
#endif

    /* Install timer_handler as the signal handler for SIGVTALRM. */
    memset(&sa, 0, sizeof(sa));

    sa.sa_flags = SA_SIGINFO;
    sa.sa_handler = &timer_handler;
    sigaction(signo, &sa, NULL);

    sigemptyset(&set);
    sigaddset(&set,signo);
    sigprocmask(SIG_UNBLOCK,&set,NULL);

    /* Configure the timer to expire after 250 msec... */

    // Start virtual timer. It counts down whenever this process is executing.
#if 0
    setitimer(itype, &timer, NULL);
#else
    timer_set(250000);
#endif

    dbgprt("Before calling wait for semaphore.......\n");

    // Waiting for sempahore
    if (! WaitForSemaphore(SemId, 0)) {
        dbgprt("Unable to get sempahore.\n");
        return 0;
    }

    dbgprt("After calling after calling wait for semaphoe module.........\n");

    return 1;
}

int
WaitForSemaphore(int SemId, unsigned short SemNum)
{
    struct sembuf SemBuf;

    int RetStat;
    unsigned int NoOfSemOps;

    while (1) {
        SemBuf.sem_num = SemNum;
        SemBuf.sem_op = -1;
        SemBuf.sem_flg = 0;

        NoOfSemOps = 1;

        dbgprt("WaitFor: SEMOP\n");

        RetStat = semop(SemId, &SemBuf, NoOfSemOps);
        if (RetStat >= 0) {
            dbgprt("WaitFor: OKAY\n");
            break;
        }

        if (errno == EINTR) {
            dbgprt("WaitFor: EINTR\n");

            // do stuff here ...

            // rearm timer
            timer_set(250000);
            continue;
        }

        perror("Wait SEMOP Failure: ");
        exit(1);
    }

// NOTE/BUG: _must_ always disable timer to prevent other syscalls from being
// interrupted
#if 1
    timer_set(0);
#endif

    return 1;
}

这是重构程序的一些示例输出:

0.000000332 main pgma: opt_y=1
0.000182324 main testit: pid=849558
0.000267203 main testit: pid=849559
0.000830005 pgma pgma: SET phase=0
0.000847541 pgmb timer_set: SET usec=250000
0.000882037 pgma USLEEP/BEF
0.000891077 pgmb Before calling wait for semaphore.......
0.000895977 pgmb WaitFor: SEMOP
0.250932859 pgmb WaitFor: EINTR
0.250950128 pgmb timer_set: SET usec=250000
0.250956676 pgmb WaitFor: SEMOP
0.500996272 pgmb WaitFor: EINTR
0.501014687 pgmb timer_set: SET usec=250000
0.501021903 pgmb WaitFor: SEMOP
0.751066428 pgmb WaitFor: EINTR
0.751089504 pgmb timer_set: SET usec=250000
0.751097693 pgmb WaitFor: SEMOP
1.000970921 pgma USLEEP/AFT
1.000987303 pgma pgma: SET phase=1
1.001001916 pgma USLEEP/BEF
1.001046071 pgmb WaitFor: OKAY
1.001055982 pgmb timer_set: SET usec=0
1.001062505 pgmb After calling after calling wait for semaphoe module.........
1.001066632 pgmb pgmb: complete
1.001210687 main main: pid 849559 reaped -- status=[00000000 exited status=0]
2.001078396 pgma USLEEP/AFT
2.001269995 main main: pid 849558 reaped -- status=[00000000 exited status=0]

更新:

  1. Why re-arming the timer is required inside WaitForSemaphore?

通过注释掉信号处理程序中的 printf 调用 [由于信号安全问题我们必须这样做],作为副作用,count 的增量无效。

起初,我只是注释掉 printf,没有意识到副作用“破坏”了处理程序的解除代码。过了一会儿,我意识到我做了什么并且想要效果(即让处理程序“什么都不做”)。

因此,整个信号处理程序 [有效地] 除了允许信号被拦截 [并导致挂起的系统调用接收 EINTR 错误之外什么都不做——这正是我们真正想要的]。 =56=]

我忽略了在评论中明确说明这一点[并用 #if 0 注释掉所有代码]。

例如,由于我最初的困惑,最好不要将active/necessary代码[count++]放在调试代码中( printf)。所以,printf(...,count++); 会更好:printf(...); count++;

这是一种体系结构选择,可以让基线代码 [non-handler 代码] 控制 arm/disarm。这是因为基线 不知道 是否知道处理程序是否做了任何事情,因此无论如何它都必须明确地解除武装 [以防止 timer/signal 在等待循环代码].

一般来说,[在内核中]有一个周期性的定时器中断肯定是有用的,并且通过类比,作为一个周期性信号有点有用,这对于这里只是唤醒一个 [卡住]系统调用。

  1. Why usleep is required inside pgma module?

这只是为了强制 pgmb 查看 两个 超时调用 semop 的条件和 testing/validation 的正常调用目的。在 normal/production 代码中,pgma 不需要睡眠。

开始时,pgma 向信号量发送一个 0 值。然后它会休眠 1 秒(1000 毫秒)。

pgmb 启动时,它会看到此 0 值 [并在 ~250 毫秒后超时]。 pgmb 将循环大约 4 次,直到 pgma 醒来并发布一个 1 值 [这将正常完成 pgmbsemop]。 =56=]

所以,现在,我们已经 pgmb 体验 failed/timeout 案例 normal/successful个案例。

这就是一组[好的]单元测试应该做的:测试目标代码[pgmbsemop的等待循环]对于所有可能的modes/states.

在这段代码中,我们通过查看调试输出看到了这一点。对于一组真正的单元测试,我们需要额外的代码以编程方式对此进行检查。