【问题标题】:How to programmatically stop reading from stdin?如何以编程方式停止从标准输入读取?
【发布时间】:2010-11-15 12:22:06
【问题描述】:

fgetc() 和其他输入函数可以在文件描述符上没有数据时返回。这可以为从标准输入读取的控制台应用程序模拟,在键盘上键入 Ctrl-D(至少在 unix 上)。但是如何以编程方式进行呢?比如下面代码中如何从reader线程中的fgetc()返回(注意:忽略可能的竞态条件)?

#include <pthread.h>
#include <stdio.h>

void* reader()
{
    char read_char;
    while((read_char = fgetc(stdin)) != EOF) {
        ;
    }

    pthread_exit(NULL);
}

int main(void)
{
    pthread_t thread;
    pthread_create(&thread, NULL, reader, NULL);

    // Do something so the fgetc in the reader thread will return

    pthread_exit(NULL);
}

谢谢!

【问题讨论】:

  • 我认为您只是想close() 管道的另一端,但您的示例并没有真正意义,因为主线程不控制标准输入的另一端。
  • 我在这几周前尝试过,但我没有找到任何了解标准输入另一端的方法。还有其他方法可以实现相同的目标吗?也许我遗漏了一些东西:我只需要一种不阻塞控制台输入线程的方法。
  • 标准输入的另一端通常是终端的键盘。也许您需要的是非阻塞 I/O?从你的问题很难判断。你应该更详细地解释你想要做什么。
  • 不,我不需要非阻塞 I/O。从某种意义上说,我至少应该使用 select() 或 poll() 轮询输入超时:这比尝试关闭终端键盘设备更错误。在我看来,我试图从不同的线程访问标准输入是错误的。这可能是错误的,我应该通过将应用程序的状态保持在输入“阅读器”的范围内来避免这种情况,这样我就可以轻松跟踪何时应该处理输入。
  • 顺便说一下,相当于 UNIX 的 control-D 的 Windows 是 control-Z。

标签: c pthreads stdin eof


【解决方案1】:

当某些事件发生时,您似乎希望线程停止阻塞fgetc(stdin) 来处理该事件。如果是这种情况,您可以在 stdin 和其他一些消息管道上使用 select(),以便线程可以处理来自两者的输入:

fd_set descriptor_set
FD_ZERO(&descriptor_set); 
FD_SET(STDIN_FILENO, &descriptor_set); 
FD_SET(pipefd, &descriptor_set); 

if (select(FD_SETSIZE, &descriptor_set, NULL, NULL, NULL) < 0) 
{ 
  // select() error
} 

if (FD_ISSET(STDIN_FILENO, &descriptor_set)) {
  // read byte from stdin
  read(STDIN_FILENO, &c, 1);
}

if (FD_ISSET(pipefd, &descriptor_set)) 
  // Special event. Do something else

另外请注意,您的进程中只有一个线程应该从stdin 读取。

【讨论】:

  • 是的,这正是我的问题的答案,非常感谢!在我的实际需求中可能有点过头了,但无论如何,如果有人需要类似的模式,这就是路要走。几周前我尝试过这个解决方案,但我完全错过了 select() 跟踪多个文件描述符的能力。在我之前关于不使用 select() 或 poll() 的评论中,我反对使用超时,但您似乎完全理解了这一点。再次感谢!
【解决方案2】:

您可以使用freopen()“关闭”标准输入,或将标准输入连接到“/dev/null”(Windows 上为“NUL:”),或者您可以将标准输入连接到“/dev/zero”。

如果关闭它,每个函数调用都会失败,因为文件流无效。如果将其连接到 null 数据源,所有读取都将失败并立即返回 EOF。如果将其连接到零数据源,则每次读取都会成功并返回相应数量的零字节。

其中一种可能足以满足您的需求。如果没有,那么您可能需要更详细地说明您的实际需求。

【讨论】:

  • 前段时间我试过这个但没有成功,但似乎不便携,实际上它在 linux 上不起作用。在 fgetc() 等待数据时尝试关闭标准输入(或任何其他文件描述符)确实会阻塞。在这里讨论:lkml.indiana.edu/hypermail/linux/kernel/0106.0/0768.html 参考 select() 和 linux 开发人员的回复是:“未定义的行为”。
  • @ceztko:在一个线程中关闭标准输入,而另一个线程正在读取 - 是的,这不可靠......我一定误读了这个问题。
【解决方案3】:

使用 POSIX,您可以向其原语(例如“读取”)阻塞的线程发出信号,并且如果您设置了一个无操作信号处理程序并清除了 SA_RESTART 位,那么原语将因 EINTR 错误而失败。这是原始版本的工作版本:

#include <pthread.h>
#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>

static void SignalHandler(int signum)
{
}

void* reader(void *arg)
{
    char read_char;
    while((read_char = fgetc(stdin)) != EOF) {
        ;
    }
    printf("leaving reader\n");
    return NULL;
}

int main(int argc, const char * argv[])
{
    struct sigaction action;
    memset(&action, 0, sizeof(action));  // SA_RESTART bit not set
    action.sa_handler = SignalHandler;
    sigaction(SIGUSR1, &action, NULL);

    pthread_t thread;
    pthread_create(&thread, NULL, reader, NULL);

    sleep(1); // time to start reader thread
    // Do something so the fgetc in the reader thread will return
    pthread_kill(thread, SIGUSR1);
    sleep(1); // time to exit reader thread; could join it if set up

    return 0;
}

【讨论】:

  • 我还没有测试过,但我猜你的代码可以工作。不过,我认为您的方法有点粗略:您向线程发出信号,并且根据 reader() 函数的复杂性,您可能会导致不必要的退出点,因为大代码区域的副作用(内核原语)你无法控制。我认为标记答案中建议的方法更细粒度,并允许在预期的退出点精确抢占线程。另请参阅我对完整示例的回答。
  • @ceztko:是的,我的方法更简单,但针对性更差。我不清楚有一个明确的答案:你可以将它与 fscanf 等更高级别的函数一起使用吗?
  • 显然你可以将fscanf与管道一起使用,还有一些caveats
【解决方案4】:

Alexandre 发布了正确的解决方案。他的回答正好回应了我提出的问题。它遵循基于他的提示的简单自编译代码:

#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/select.h>

static int pipe_fds[2];

void* user_interaction()
{
    char read_char;
    
    fd_set descriptor_set;
    FD_ZERO(&descriptor_set); 
    FD_SET(STDIN_FILENO, &descriptor_set); 
    FD_SET(pipe_fds[0], &descriptor_set);

    while(1)
    {
        if (select(FD_SETSIZE, &descriptor_set, NULL, NULL, NULL) < 0) {
            // select() error
        }

        if (FD_ISSET(STDIN_FILENO, &descriptor_set)) {
            // read byte from stdin
            read(STDIN_FILENO, &read_char, 1);
            // Re-set the selected file descriptor so it can
            // be signaled again
            FD_SET(STDIN_FILENO, &descriptor_set);
        }

        if (FD_ISSET(pipe_fds[0], &descriptor_set))
            // Special event. break
            break;
    }
    
    pthread_exit(NULL);
}

int main(void)
{
    pipe(pipe_fds);
    
    pthread_t thread;
    pthread_create(&thread, NULL, user_interaction, NULL);
    
    // Before closing write pipe endpoint you are supposed
    // to do something useful
    sleep(5);

    close(pipe_fds[1]);
    
    pthread_join(thread, NULL);
    
    pthread_exit(NULL);
}

【讨论】:

    【解决方案5】:

    我尝试使用 this answer 中的代码以稍微修改的形式:

    void *user_interaction()
    {
        char ch;
        int rv;
        fd_set set;
    
        FD_ZERO(&set);
        FD_SET(STDIN_FILENO, &set);
        FD_SET(pipe_fds[0], &set);
    
        while (1)
        {
            rv = select(pipe_fds[0] + 1, &set, NULL, NULL, NULL);
            if (rv < 0)
            {
                printf(">>> select(): error occurred, %d\n", rv);
                break;
            }
            
            if (FD_ISSET(pipe_fds[0], &set))
            {
                printf(">>> pipe_fds[0]: is ready\n");
                break;
            }
    
            if (FD_ISSET(STDIN_FILENO, &set))
            {
                read(STDIN_FILENO, &ch, 1);
                write(STDOUT_FILENO, &ch, 1);
            }
        }
    
        pthread_exit(NULL);
    }
    

    但没有得到预期的行为。如下执行时:
    $ echo -n 1 | ./a.out

    我的终端在无限循环中使用1 进行渲染,并且select() 从未报告管道准备好(即即使在主线程中close()ing 之后)。

    通过一些实验,我认为您需要将FD_ZERO/FD_SET 移动到循环内,以使select() 按需要工作:

    void *user_interaction()
    {
        char ch;
        int rv;
        fd_set set;
    
        while (1)
        {
            FD_ZERO(&set);
            FD_SET(STDIN_FILENO, &set);
            FD_SET(pipe_fds[0], &set);
    
            rv = select(pipe_fds[0] + 1, &set, NULL, NULL, NULL);
            if (rv < 0)
            {
                printf(">>> select(): error occurred, %d\n", rv);
                break;
            }
            
            if (FD_ISSET(pipe_fds[0], &set))
            {
                printf(">>> pipe_fds[0]: is ready\n");
                break;
            }
    
            if (FD_ISSET(STDIN_FILENO, &set))
            {
                read(STDIN_FILENO, &ch, 1);
                write(STDOUT_FILENO, &ch, 1);
            }
        }
    
        pthread_exit(NULL);
    }
    

    【讨论】:

    • 通过删除不起作用的代码并只保留好的部分来简化您的答案。您也可以评论原始答案并指出错误所在。因为 FD_ZERO 不是绝对必要的,但重新设置信号描述符是必要的。我将修复用户数据描述符的答案(另一个描述符用于控制/退出程序)。
    • 想发表评论,但我在这里的“声誉”很低...... :( 很高兴你喜欢这些改进!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-10-08
    • 2011-11-15
    • 1970-01-01
    • 2018-05-05
    • 1970-01-01
    相关资源
    最近更新 更多