信号和睡眠无法正常工作

Signals and sleep not working properly

我有一项大学作业要完成,它几乎完成了,大部分工作正常,只有一个方面不工作,我不太确定如何解决它.. objetivo 是让问题等待 2 个 ctrl+C 并关闭。但是如果他抓住第一个 ctrl+C 并通过超过 3 秒,程序必须忘记它并再次等待另一个 2 个 ctrl+C。我就是这样做的:

/*Problem 2. Write a program that sleeps forever until the user interrupts it twice with a Ctrl-C, and
then exits. Once the first interrupt is received, tell the user: “Interrupt again to exit.”. The first
interrupt should be forgotten 3 seconds after it has occurred. Additionally, the program should block
the SIGQUIT signal, and ignore the SIGTSTP signal. The program should start by printing “Interrupt
twice with Ctrl-C to quit.” on the screen.*/

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>

//handler to catch the first ctrl_c and ask user to do it another time(no reference to time limit)
void ctrl_c(int sig){
    signal(sig, SIG_IGN);  
    printf("\nInterrupt again to exit.\n");
}

//handler for second ctrl_c. If called, program will end
void second_catch(int sig){
   if(sig == SIGINT){
       printf("\n");
       exit(0);
   }
}

//handler to always ignore ctrl_z
void ctrl_z(int sig){
   signal(sig, SIG_IGN);
}

int main(){
    //blocking SIQUIT (Ctrl+\) using series of command to change the mask value of SIGQUIT
    sigset_t sg;
    sigemptyset (&sg);
    sigaddset(&sg, SIGQUIT);
    sigprocmask(SIG_BLOCK, &sg, NULL);

    //installing handler to ignore SIGTSTP (Ctrl+Z)
    signal(SIGTSTP, ctrl_z);

    //two part SIGINT handling
    printf("\nInterrupt twice with Ctrl+C to quit.\n");
    signal(SIGINT, ctrl_c); //first handler install

    do{ //cycle for second hanler install and 3 second timer
        if(sleep(3) == 0){
           main(); //if second_catch handler is not called within 3 seconds,       program will restart
        }
        else {
           signal(SIGINT, second_catch); //upon call, program will end
        }   
    }while(1);

    return 0;
}

发生的事情是它在循环中 3 秒后不断重置。但是我只想在单击 ctrl+c 3 秒后重置 1 次。 我必须改变什么?

您的方法不太可能产生有效的程序。

首先,使用信号处理程序,只要捕获到 SIGINT 信号,它只设置一个全局变量(volatile sig_atomic_t 类型)。不要尝试从信号处理程序打印任何内容,因为标准 I/O 不是异步信号安全的。

其次,使用sigaction()安装信号处理器。使用零标志。换句话说,在安装处理程序时不要使用 SA_RESTART 标志。这样,当信号传递给您的处理程序时,它将中断大多数系统调用(包括睡眠)。 (函数将 return -1 与 errno == EINTR。)

这样,在您的 main() 安装信号处理程序后,您可以让它打印指令,并进入循环。

在循环中,清除中断标志,休眠几秒。多久都没关系。如果在睡眠完成后没有设置中断标志,continue(在循环开始时)。

否则,你知道用户按下了Ctrl+C。所以,清除中断标志,再睡三秒钟。如果标志在睡眠完成后设置,您知道用户提供了另一个 Ctrl+C,您可以跳出循环。否则,您只需再次继续循环。


从技术上讲,这里存在竞争条件,因为用户可能会连续按两次 Ctrl+C,速度足够快,因此main() 只能看到一个。

不幸的是,增量(flag++)不是原子的;编译器或硬件实际上可能会执行 temp = flag; temp = temp + 1; flag = temp; 并且信号可能会在第三步之前传递,导致信号处理程序和 main() 看到 flag.[=80= 的不同值]

一种解决方法是使用 C11 原子(如果体系结构和 C 库在 <stdatomic.h> 中提供它们,并定义了宏 ATOMIC_INT_LOCK_FREE):volatile atomic_int flag; 用于标志, __atomic_add_fetch(&flag, 1, __ATOMIC_SEQ_CST) 递增,__atomic_sub_fetch(&flag, 1, __ATOMIC_SEQ_CST) 递减。

另一种方法是使用 POSIX semaphore. The signal handler can increment it (using sem_post()) safely. In main(), you can use sem_timedwait() 在有限的时间内等待信号,然后 sem_trywait() 将其递减。

第三种方法是使用 sigtimedwait() 超时捕获 main() 中的信号,无需任何信号处理程序。我相信最后一个是最健壮和最容易实现的,所以这就是我在实际应用程序中使用的。


事实证明,还有另一种方法可以实现这一点,即在三秒内响应连续两次 Ctrl+C 按下,没有留下任何讨厌的角落案例。

这不完全是对 OP 的要求,因此不是他们练习的有效答案,但否则这将是一个很好的方法。

这个想法是使用 alarm() 和一个 SIGALRM 处理程序,以及两个 sig_atomic_t 标志:一个计算 Ctrl+C 次按键,以及在三秒内有两次按键时标记的情况。

不幸的是,在这种情况下不能使用 sleep() -- 您必须使用 nanosleep() 代替 --,如 sleep()alarm()SIGALRM 信号处理可能会相互干扰。

本质上,我们使用

#define INTR_SECONDS 3

static volatile sig_atomic_t  done = 0;
static volatile sig_atomic_t  interrupted = 0;

static void handle_sigalrm(int signum)
{
    if (interrupted > 1)
        done = 1;
    interrupted = 0;
}

static void handle_sigint(int signum)
{
    interrupted++;
    if (interrupted > 1) {
        done = 1;
        alarm(1);
    } else
        alarm(INTR_SECONDS);
}

handle_sigalrm() 作为 SIGALRM 处理程序安装,其信号掩码中带有 SIGINThandle_sigint() 作为 SIGINT 处理程序安装,其信号掩码中带有 SIGALRM。这样两个信号处理器就可以互相阻塞,不会被对方打断。

收到第一个 SIGINT 时,将启动警报。如果这是第二个(或第三个等)SIGINT 没有干预 SIGALRM,我们还设置完成标志,并准备在一秒钟内发生警报,​​以确保我们捕捉到状态变化最多一秒。

收到SIGALRM时,中断计数清零。如果是两个或更多,完成标志也被设置。

main()中,我们只检查doneinterrupted,从不修改它们。这避免了我担心的极端情况。

在最坏的情况下,延迟一秒退出,如果第二个Ctrl+C在我们检查后交付,但就在我们睡觉之前。 handle_sigint() 中的 alarm(1) 就是针对这种情况。

main 中的循环就是

while (!done) {

    while (!done && !interrupted)
        nanosleep(&naptime, NULL);

    if (done)
        break;

    printf("Ctrl+C again to quit!\n");
    fflush(stdout);

    while (interrupted == 1 && !done)
        nanosleep(&naptime, NULL);
}

第一个内部循环仅在自上次 SIGINT 以来超过三秒时才休眠(或者我们从未收到过)。它会被 SIGINTSIGALRM 打断,所以持续时间无关紧要。

if (done) break; 案例只是避免打印任何东西,如果用户有闪电般的手并且非常快地键入 Ctrl+C 两次.

第二个内部循环仅在我们等待第二个Ctrl+C时休眠。它也会被两个信号打断,所以这里的持续时间也无关紧要。但是请注意,我们确实希望首先检查 interrupted,以确保我们可靠地捕获所有更改。 (如果我们先检查 done,我们可能会在检查 interrupted 之前被打断,理论上,done 变为非零并且 interrupt 变为零并且然后同时变为 1。但是,如果我们首先检查 interrupted,并且它是 1,则任何额外的中断都将设置 done,我们将捕获它。所以,interrupted == 1 && done == 0是这里正确顺序的正确检查。)

如上所述,为 nanosleep() 指定的持续时间实际上并不重要,因为它无论如何都会被信号传递打断。十秒左右应该没问题,

struct timespec naptime = { .tv_sec = 10, .tv_nsec = 0L };

如果讲师推荐了 POSIX.1 函数(sigaction()nanosleep()),这将是一个非常有趣的练习。