【问题标题】:What's the proper way to safely discard stdin characters of variable length in C?在 C 中安全丢弃可变长度的标准输入字符的正确方法是什么?
【发布时间】:2015-04-04 20:34:54
【问题描述】:

我正在编写名为“Headfirst C”的 C 文本中的一些示例代码。我编写了一个演示信号处理的练习应用程序,在完成本章后,我决定尝试一下。我是一名工程师,习惯于在 LabVIEW 中工作(高度并发和直观的事件处理功能),因此我对使用警报和信号处理程序生成周期性事件很感兴趣。我的问题是这样的:

在以下示例代码中,丢弃标准输入中可变数量的用户输入的正确方法或最佳实践是什么?我编写了这个小应用程序作为演示,一个 3 秒的警报会触发一个烦人的“你好!”中断 fgets 调用的消息。然而,我注意到的是,如果用户在打字过程中被打断,当他最终按下 enter 时,输入的任何文本(是否被打断)都会被回显。我想丢弃在用户按下回车之前被中断的任何内容。

//Sample Program - Signal Handling & Alarms
//Header Includes
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>

//Function Declarations
//Handler Functions
void diediedie(int sig);
void howdy(int sig);
//Handler Register Function
int catchSignal(int signum, void(*handler)(int));

//Variable declarations
//Declare interrupted flags
static int interrupted = 0;

//Program entrypoint
int main() {

  //Register interrupt hander, catch errors
  if(catchSignal(SIGINT, diediedie) == -1) {
    fprintf(stderr, "Could not register interrupt handler");
    exit(2);
  }

  //Register alarm handler, catch errors
  if(catchSignal(SIGALRM, howdy) == -1) {
    fprintf(stderr, "Could not register alarm handler");
    exit(2);
  }

  //Create initial alarm trigger
  alarm(3);

  //Do something stupid while waiting for signals
  char name[30];
  printf("Enter your name: ");
  //Keep waiting for user input even if interrupted by alarm signal
  while(1) {
    fgets(name, 30, stdin);
    if(interrupted) {
      // reset interrupted flag
      interrupted = 0;
      // ***** ADD CODE TO DISCARD INTERRUPTED INPUT HERE ******
    }
    else {
      //echo user input and break out
      printf("Hello %s\n", name);
      break;
    }
  }

  //end program
  return 0;

}

//interrupt handler definition
void diediedie(int sig) {

  //write some stuff, exit program
  puts("Goodbye world!");
  exit(1);

}

//alarm handler definition
void howdy(int sig) {
  //set interrupted flag
  interrupted = 1;
  //write some annoying message
  puts("howdy!");
  //set another alarm trigger
  alarm(3);
  //**** COULD STDIN BE FLUSHED HERE? ****
}

//signal handler registration function definition
int catchSignal(int signum, void(*handler)(int)) {

  //register handler
  struct sigaction action;
  action.sa_handler = handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags = 0;
  return sigaction(signum, &action, NULL);

}

在警报处理程序中执行此清除的适当位置是否合适?请注意 cmets 表明我对正确代码位置的想法。

我考虑过以下几点:

while(getchar() != EOF) {}

我也想知道,当 fgets 等待用户输入并引发 SIGALRM 时会发生什么?该功能是否终止?我观察到,如果我不包括 while 循环来检查中断标志并做出适当的响应,程序将完成 fget,在屏幕上转储一些垃圾(我假设 stdin 的当前状态?)并结束程序。

感谢您的建议!

【问题讨论】:

  • 可能是tcflush(),也可能不是。
  • @JonathanLeffler 如果标准输入确实是终端,则可以使用。
  • 这条线 tcflush(fileno(stdin), TCIFLUSH); 似乎确实有效。

标签: c signals stdin alarm fgets


【解决方案1】:

在 Unix 中,信号处理程序与您的代码一起出现在带外。如果信号发生在阻塞系统调用的中间,系统调用将退出,errno 设置为 EINTR。但我相信fgets() 正在为您处理此中断并继续进行而不会将控制权交还给您。

如果您使用的是基于 Unix 的操作系统并从命令行输入输入,那么这里真正发生的事情是您正在以熟模式从终端读取数据。在按下返回之前,您的程序不会从 TTY 获取任何数据。您需要将终端设置为“原始”模式。以下是如何与您的代码集成的示例:

//Sample Program - Signal Handling & Alarms
//Header Includes
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <termios.h>

//Function Declarations
//Handler Functions
void diediedie(int sig);
void howdy(int sig);
//Handler Register Function
int catchSignal(int signum, void(*handler)(int));

//Variable declarations
//Declare interrupted sa_flags
static int interrupted = 0;

static struct termios save_termios;

//Program entrypoint
int main() {

  struct termios buf;
  int fd = 1;

  // This is derived from from Stevens, "Advanced Programming in the UNIX Environment"
  if (tcgetattr(fd, &save_termios) < 0) /* get the original state */
        return -1;

  buf = save_termios;

  buf.c_lflag &= ~(ECHO | ICANON | IEXTEN | ISIG);
                    /* echo off, canonical mode off, extended input
                       processing off, signal chars off */

  buf.c_iflag |= BRKINT | ICRNL;
                    /* SIGINT on BREAK, CR-toNL on */

  buf.c_cflag &= ~(CSIZE | PARENB);
                    /* clear size bits, parity checking off */

  buf.c_cflag |= CS8;
                    /* set 8 bits/char */

  buf.c_oflag &= ~(OPOST);
                    /* output processing off */

  buf.c_cc[VMIN] = 1;  /* 1 byte at a time */
  buf.c_cc[VTIME] = 0; /* no timer on input */

  if (tcsetattr(fd, TCSAFLUSH, &buf) < 0)
    return -1;


  //Register interrupt hander, catch errors
  if(catchSignal(SIGINT, diediedie) == -1) {
    fprintf(stderr, "Could not register interrupt handler");
    exit(2);
  }

  //Register alarm handler, catch errors
  if(catchSignal(SIGALRM, howdy) == -1) {
    fprintf(stderr, "Could not register alarm handler");
    exit(2);
  }

  //Create initial alarm trigger
  alarm(3);

  //Do something stupid while waiting for signals
  char name[30];
  printf("Enter your name: ");

  //Keep waiting for user input even if interrupted by alarm signal
  char nextchar = 0;
  char *p;
  p = name;
  while(nextchar != '\n') {
    nextchar = fgetc(stdin);
    if (interrupted) {
      // reset interrupted flag
      interrupted = 0;

      //Discard interrupted input by reseting 'p' to the start of the buffer
      p = name;
      *p = 0;
      continue;
    }
    if (nextchar == '\n') {
      *p = 0;
      fputc('\r', stdout);
      fputc('\n', stdout);
      break;
    }
    // You'll have to handle some characters manually to emulate what the
    // terminal does, or you could filter them out using a function like isprint() 
    // 
    if (nextchar == 127) {
      // *** handle backspace
      if (p > name) {
        p--;
      }
      // TODO: To handle this right you'll have to backup the cursor on the screen
    }  else {
      *p = nextchar;
      p++;
    }
    fputc(nextchar, stdout);

    // Handle buffer overflow
    if (p-name == sizeof(name) - 1) {
      *p = 0;
      break;
    }
  }
  // echo user input
  printf("Input is: %s\r\n", name);
  tcsetattr(1, TCSAFLUSH, &save_termios);
}

//interrupt handler definition
void diediedie(int sig) {

  //write some stuff, exit program
  puts("Goodbye world!");

  tcsetattr(1, TCSAFLUSH, &save_termios);
  exit(1);

}

//alarm handler definition
void howdy(int sig) {
  //set interrupted flag
  interrupted = 1;
  //write some annoying message
  puts("howdy!");
  //set another alarm trigger
  alarm(3);
}

// signal handler registration function definition
int catchSignal(int signum, void(*handler)(int)) {

  //register handler
  struct sigaction action;
  action.sa_handler = handler;
  sigemptyset(&action.sa_mask);
  action.sa_flags = 0;
  return sigaction(signum, &action, NULL);
}

注意,在程序退出前需要保存原来的终端设置并恢复!如果遇到麻烦,可能会破坏终端设置。从命令行使用resetstty sane 来恢复正常的终端设置。有关 termios 数据结构的更多信息,请参阅手册页。

您还可以使用 ncurses 之类的库来处理原始输入。

【讨论】:

    【解决方案2】:

    要正确执行此操作,您需要将终端置于“原始”模式,在这种模式下,每次击键都会立即返回到应用程序,而不是通过解释行编辑字符(“熟”模式,这是默认值)。

    当然,如果你不让内核处理行编辑(例如退格),那么你需要自己做,这是相当多的工作。解释退格和您感兴趣的任何其他编辑命令并不难,但在终端上保持正确的外观是一件痛苦的事。

    请参阅man termios 了解更多信息。

    【讨论】:

    • 使用 termios 我能够像这样解决它:tcflush(fileno(stdin), TCIFLUSH); 但是,我知道这仅适用于终端应用程序?我也没有改变我在终端所做的任何事情(我认为仍然是“熟”模式)所以这个解决方案有什么我没有看到的影响吗?
    • @Kin3TiX:主要问题是在负载下会出现的竞争条件。您依靠运气来保持tcflush 的操作与用户在控制台中看到的内容同步;如果tcflush 被延迟,它将删除显然在中断后键入的字符。恕我直言,刷新输入队列对您的用户来说是一件可怕的事情;如果你打算这样做,你至少应该确保结果是显而易见的。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-06
    • 1970-01-01
    • 2014-12-08
    • 1970-01-01
    • 2020-06-30
    • 1970-01-01
    • 2018-03-24
    相关资源
    最近更新 更多