在 C 中安全丢弃可变长度的标准输入字符的正确方法是什么?
What's the proper way to safely discard stdin characters of variable length in C?
我正在编写一些名为 "Headfirst C" 的 C 文本示例代码。我编写了一个演示信号处理的练习应用程序,并在完成本章后决定尝试一下。我是一名工程师,曾经在 LabVIEW(高度并发和直观的事件处理功能)中工作,所以我对使用警报和信号处理程序来生成周期性事件很感兴趣。我的问题是:
在以下示例代码中,丢弃位于 stdin 中的可变数量的用户输入的正确方法或最佳实践是什么?我已经将这个小应用程序编写为演示,3 秒警报会触发烦人的 "howdy!" 消息来中断 fgets 调用。然而,我注意到的是,如果用户在打字过程中被打断,当他最终按下回车键时,任何输入的文本(无论是否被打断)都会被回显。我想丢弃在用户按下回车键之前被打断的任何东西。
//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);
}
警报处理程序中是否适合执行此清除操作?请注意表明我对正确代码位置的想法的注释。
我考虑了以下几点:
while(getchar() != EOF) {}
我也想知道,当 fgets 等待用户输入并引发 SIGALRM 时会发生什么?该功能是否已终止?我观察到,如果我不包括 while 循环来检查中断标志并做出适当响应,程序将完成 fgets,在屏幕上转储一些垃圾(我假设 stdin 的当前状态?)并结束程序。
感谢任何建议!
要做到这一点,您需要将终端置于 "raw" 模式,在该模式下,每次击键都会立即返回给应用程序,而不是通过行编辑字符的解释进行累积("cooked"模式,这是默认的)。
当然,如果你不让内核处理行编辑(例如退格键),那么你需要自己做,这是相当多的工作。解释退格键和您感兴趣的任何其他编辑命令并不难,但在终端上保持正确的外观是一件痛苦的事。
有关更多信息,请参阅 man termios
。
在 Unix 中,信号处理程序出现在您的代码的带外。如果信号发生在阻塞系统调用的中间,系统调用将退出,errno
设置为 EINTR。但我相信 fgets()
正在为您处理此中断并在没有 return 控制您的情况下继续进行。
如果您使用的是基于 Unix 的 OS 并从命令行键入输入,那么这里真正发生的是您正在以熟化模式从终端读取数据。在按下 return 之前,您的程序不会从 TTY 获取任何数据。您需要将终端设置为 'raw' 模式。以下是如何将其与您的代码集成的示例:
//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);
}
注意需要保存原来的终端设置,在程序退出前恢复!如果遇到麻烦,您可能会破坏终端设置。从命令行使用 reset
或 stty sane
来恢复正常的终端设置。有关 termios 数据结构的更多信息,请参阅手册页。
您还可以使用 ncurses 等库来处理原始输入。
我正在编写一些名为 "Headfirst C" 的 C 文本示例代码。我编写了一个演示信号处理的练习应用程序,并在完成本章后决定尝试一下。我是一名工程师,曾经在 LabVIEW(高度并发和直观的事件处理功能)中工作,所以我对使用警报和信号处理程序来生成周期性事件很感兴趣。我的问题是:
在以下示例代码中,丢弃位于 stdin 中的可变数量的用户输入的正确方法或最佳实践是什么?我已经将这个小应用程序编写为演示,3 秒警报会触发烦人的 "howdy!" 消息来中断 fgets 调用。然而,我注意到的是,如果用户在打字过程中被打断,当他最终按下回车键时,任何输入的文本(无论是否被打断)都会被回显。我想丢弃在用户按下回车键之前被打断的任何东西。
//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);
}
警报处理程序中是否适合执行此清除操作?请注意表明我对正确代码位置的想法的注释。
我考虑了以下几点:
while(getchar() != EOF) {}
我也想知道,当 fgets 等待用户输入并引发 SIGALRM 时会发生什么?该功能是否已终止?我观察到,如果我不包括 while 循环来检查中断标志并做出适当响应,程序将完成 fgets,在屏幕上转储一些垃圾(我假设 stdin 的当前状态?)并结束程序。
感谢任何建议!
要做到这一点,您需要将终端置于 "raw" 模式,在该模式下,每次击键都会立即返回给应用程序,而不是通过行编辑字符的解释进行累积("cooked"模式,这是默认的)。
当然,如果你不让内核处理行编辑(例如退格键),那么你需要自己做,这是相当多的工作。解释退格键和您感兴趣的任何其他编辑命令并不难,但在终端上保持正确的外观是一件痛苦的事。
有关更多信息,请参阅 man termios
。
在 Unix 中,信号处理程序出现在您的代码的带外。如果信号发生在阻塞系统调用的中间,系统调用将退出,errno
设置为 EINTR。但我相信 fgets()
正在为您处理此中断并在没有 return 控制您的情况下继续进行。
如果您使用的是基于 Unix 的 OS 并从命令行键入输入,那么这里真正发生的是您正在以熟化模式从终端读取数据。在按下 return 之前,您的程序不会从 TTY 获取任何数据。您需要将终端设置为 'raw' 模式。以下是如何将其与您的代码集成的示例:
//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);
}
注意需要保存原来的终端设置,在程序退出前恢复!如果遇到麻烦,您可能会破坏终端设置。从命令行使用 reset
或 stty sane
来恢复正常的终端设置。有关 termios 数据结构的更多信息,请参阅手册页。
您还可以使用 ncurses 等库来处理原始输入。