读取当前在标准输入(stdin)中输入的所有内容。

3
我希望在10秒后读取stdin中的所有内容并停止。我目前编写的代码如下:
#include <stdio.h>
#include <stdlib.h>

int main() {
  sleep(10);
  char c;
  while (1) { // My goal is to modify this while statement to break after it has read everything.
    c = getchar();
    putchar(c);
  }
  printf("Everything has been read from stdin");
}

当在10秒内输入字母"c"时,程序应该打印出"c"(在sleep执行之后),然后打印出"Everything has been read from stdin"。

目前我已经尝试过:

  • 检查是否输入了c的文件结束符EOF -> 对于stdingetchar和类似的函数永远不会返回EOF
  • stdin上使用类似stat的函数->对stdin进行stat操作总是返回0作为大小(st_size)。

检查 c 是否为 EOF -> getchar 和类似的函数从 stdin 中不会返回 EOF。这是因为 getchar() 返回的是 int 而不是 char。将返回值强制转换为 char 会导致无法检测到 EOF。你需要将 char c; 改为 int c; - Andrew Henle
@AndrewHenle 将 char c; 改为 int c;,并将 while (1) { 改为 while ((c = getchar()) != EOF) { 对我来说并没有解决问题。 - gurkensaas
@AndrewHenle 为了澄清,我现在可以执行 echo "hello world" | ./myprogram,然后它会打印出 "hello world",接着打印出 "Everything has been read from stdin",但是这种方式读取 stdin 而不是在 sleep 期间读取用户输入并不是我的目标。 - gurkensaas
@user3121023 我知道终端通常是有缓冲区的。我的问题是,如果我取消缓冲或按下回车键,我如何知道没有更多内容可读取? - gurkensaas
@user3121023 我更倾向于使用 termios 方法。你能否在回答中提供一个示例? - gurkensaas
2个回答

1
你可以使用select函数来等待stdin上是否有可读内容,并设置一个从10秒开始的超时时间。当它检测到有内容时,你可以读取一个字符并检查错误或EOF。如果一切正常,你可以再次调用select函数,将超时时间减去已经过去的时间。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <time.h>

struct timeval tdiff(struct timeval t2, struct timeval t1)
{
    struct timeval result;

    result.tv_sec = t2.tv_sec - t1.tv_sec;
    result.tv_usec = t2.tv_usec - t1.tv_usec;
    while (result.tv_usec < 0) {
        result.tv_usec += 1000000;
        result.tv_sec--;
    }
    return result;
}

int cmptimestamp(struct timeval t1, struct timeval t2)
{
    if (t1.tv_sec > t2.tv_sec) {
        return 1;
    } else if (t1.tv_sec < t2.tv_sec) {
        return -1;
    } else if (t1.tv_usec > t2.tv_usec) {
        return 1;
    } else if (t1.tv_usec < t2.tv_usec) {
        return -1;
    } else {
        return 0;
    }
}

int main()
{
    struct timeval cur, end, delay;
    int rval, len = 0;
    fd_set fds;

    gettimeofday(&cur, NULL);
    end = cur;
    end.tv_sec += 10;
    FD_ZERO(&fds);
    FD_SET(0, &fds);

    if (fcntl(0, F_SETFL, O_NONBLOCK) == -1) {
        perror("fcntl failed"); 
        exit(1);
    }
    do {
        delay = tdiff(end, cur);
        rval = select(1, &fds, NULL, NULL, &delay);
        if (rval == -1) {
            perror("select failed");
        } else if (rval) {
            char c;
            len = read(0, &c, 1);
            if (len == -1) {
                perror("read failed");
            } else if (len > 0) {
                printf("c=%c (%d)\n", c, c);
            } else {
                printf("EOF\n");
            }   
        } else {
            printf("timeout\n");
        }
        gettimeofday(&cur, NULL);
    } while (rval > 0 && len > 0 && cmptimestamp(end,cur) > 0);

    return 0;
}

请注意,这不是在按下键时检测键,而是仅在您按下 RETURN 键或关闭标准输入后才进行检测。

这个很好用,但我有一些问题:1)如果将延迟分离成一个值,比如delayInSeconds,甚至可以接受非整数值,那么代码会是什么样子?2)非阻塞的标准输入怎么办?当我取消阻塞我的标准输入时,字符被打印出来,但程序永远不会停止/“超时”从未被打印。 - gurkensaas
我认为这是正确的方向。但是:1-您可能希望在出现错误时使用break;。2-问题中没有说明我们可以假设在10秒后,它会关闭并且我们已经到达EOF-您还可能希望使用具有O_NONBLOCK read(2)的重新打开stdin而不是getchar()以避免任何stdio缓冲效应。 - root
@root rval > 0 检查同时处理了 select 的超时和错误,而 c != 1 检查则处理了 getchar 的错误。 - dbush
不,他们不会。 - root
@root 好的,我没有考虑到可能会在标准输入上无限读取,也没有考虑到除了管道文件或终端(即套接字或其他进程)之外的流可能会停止发送数据而没有EOF。现在已更新为将stdin设置为非阻塞,并删除了内部循环,还在调用select之前添加了一个超时检查。 - dbush
显示剩余2条评论

1
这里有一个方案符合我对你要求的解释:
  • 该程序在10秒内读取标准输入中键入(或以其他方式输入)的任何数据(如果您设法输入2047个字符,则停止,这可能意味着输入来自文件或管道)。
  • 10秒后,它会打印出它收集到的所有内容。
  • alarm()调用设置了一个整数秒数的警报,并且当时间到达时系统会生成一个SIGALRM信号。即使没有读取任何数据,警报信号也会中断read()系统调用。
  • 接收到信号时,程序将停止而不打印任何内容。
  • 如果信号是SIGINT、SIGQUIT、SIGHUP、SIGPIPE或SIGTERM之一,则它将在不打印任何内容的情况下停止。
  • 它会更改终端设置,使输入无缓冲。这避免了它挂起。它还确保系统调用在接收到信号后不会重新启动。在Linux上可能不重要;在macOS Big Sur 11.7.1上使用signal()时,输入在警报信号后继续进行,这并不有用——使用sigaction()可以给您更好的控制。
  • 它尽最大努力确保在退出时恢复终端模式,但如果您发送不适当的信号(不是上述列表中的信号或SIGALRM),则终端将处于非规范(原始)模式。这通常会导致混乱。
  • 很容易修改程序,使其满足以下要求:
    • 终端驱动程序不会回显输入;
    • 程序在字符到达时回显字符(但要注意编辑字符);
    • 键盘不会生成信号;
    • 因此,如果它不是终端,则不会干扰标准输入终端属性。

代码

/* SO 7450-7966 */
#include <ctype.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>

#undef sigemptyset      /* MacOS has a stupid macro that triggers -Wunused-value */

static struct termios sane;

static void stty_sane(void)
{
    tcsetattr(STDIN_FILENO, TCSANOW, &sane);
}

static void stty_raw(void)
{
    tcgetattr(STDIN_FILENO, &sane);
    struct termios copy = sane;
    copy.c_lflag &= ~ICANON;
    tcsetattr(STDIN_FILENO, TCSANOW, &copy);
}

static volatile sig_atomic_t alarm_recvd = 0;

static void alarm_handler(int signum)
{
    signal(signum, SIG_IGN);
    alarm_recvd = 1;
}

static void other_handler(int signum)
{
    signal(signum, SIG_IGN);
    stty_sane();
    exit(128 + signum);
}

static int getch(void)
{
    char c;
    if (read(STDIN_FILENO, &c, 1) == 1)
        return (unsigned char)c;
    return EOF;
}

static void set_handler(int signum, void (*handler)(int signum))
{
    struct sigaction sa = { 0 };
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;    /* No SA_RESTART! */
    if (sigaction(signum, &sa, NULL) != 0)
    {
        perror("sigaction");
        exit(EXIT_FAILURE);
    }
}

static void dump_string(const char *tag, const char *buffer)
{
    printf("\n%s [", tag);
    int c;
    while ((c = (unsigned char)*buffer++) != '\0')
    {
        if (isprint(c) || isspace(c))
            putchar(c);
        else
            printf("\\x%.2X", c);
    }
    printf("]\n");
}

int main(void)
{
    char buffer[2048];

    stty_raw();
    atexit(stty_sane);
    set_handler(SIGALRM, alarm_handler);
    set_handler(SIGHUP, other_handler);
    set_handler(SIGINT, other_handler);
    set_handler(SIGQUIT, other_handler);
    set_handler(SIGPIPE, other_handler);
    set_handler(SIGTERM, other_handler);
    alarm(10);

    size_t i = 0;
    int c;
    while (i < sizeof(buffer) - 1 && !alarm_recvd && (c = getch()) != EOF)
    {
        if (c == sane.c_cc[VEOF])
            break;
        if (c == sane.c_cc[VERASE])
        {
            if (i > 0)
                i--;
        }
        else
            buffer[i++] = c;
    }
    buffer[i] = '\0';

    dump_string("Data", buffer);
    return 0;
}

编译:

gcc -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -fno-common tensec53.c -o tensec53 

没有错误(或警告,但警告会被转换为错误)。
分析
  • #undef行移除了sigemptyset()的任何宏定义,使编译器调用实际函数。C标准要求这样做(§7.1.4 ¶1)。在macOS上,该宏是#define sigemptyset(set) (*(set) = 0, 0),GCC会抱怨,“逗号表达式的右操作数没有效果”。解决这个警告的另一种方法是测试sigemptyset()的返回值,但这可能比宏更愚蠢。(是的,我对此感到不满!)
  • sane变量记录程序启动时终端属性的值——通过在stty_raw()中调用tcgetattr()来设置它。代码确保在激活任何将调用sttr_sane()的代码之前设置sane
  • stty_sane()函数将终端属性重置为程序启动时有效的正常状态。它被atexit()和信号处理程序使用。
  • stty_raw()函数获取原始终端属性,复制它们,修改副本以关闭规范化处理(有关详细信息,请参见规范化与非规范化终端输入),并设置修订后的终端属性。
  • 标准C表示,在信号处理函数中除了设置volatile sig_atomic_t变量、调用带有信号编号的signal()或调用一个退出函数之外,你不能做太多事情。POSIX更加宽容——有关详细信息,请参见如何避免在信号处理程序中使用printf()
  • 有两个信号处理程序,一个用于SIGALRM,另一个用于捕获的其他信号。
  • alarm_handler()忽略进一步的闹钟信号并记录它被调用的情况。
  • other_handler()忽略相同类型的进一步信号,将终端属性重置为正常状态,并使用用于报告程序被信号终止的状态退出(请参见POSIX shell 命令的退出状态)。
  • getch()函数从标准输入读取单个字符,将失败映射到EOF。强制转换确保返回值像getchar()一样为正数。
  • set_handler()函数使用sigaction()设置信号处理。在信号处理程序中使用signal()有点懒,但足够了。它确保SA_RESTART位未设置,因此当信号中断系统调用时,它会返回一个错误而不是继续执行。
  • dump_string()函数将带有任何非打印字符(除空格字符外)的字符串写出为十六进制转义。
  • main()函数设置终端,在退出时确保终端状态被重置(atexit()和使用other_handler参数的set_handler()调用

    POSIX 函数和头文件:


这段代码可以在我的SOQ(Stack Overflow Questions)GitHub存储库中的src/so-7450-7966子目录下的文件tensec53.c中找到。 - Jonathan Leffler
我想在分析中加入一点内容:警报信号的存在是解除getch()read()调用阻塞的原因。我还会设置MIN和/或TIME,而不是假设默认终端特殊字符。 - root
@root - 谢谢;我强调了alarm()的作用,并添加了有关使用timer_settime()等进行亚秒定时的信息(或者,使用过时的函数setitimer())。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接