非缓冲标准输入读取

9

我的测试应用程序是

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

int main(int argc, char *argv[], char *envp[]) {
  int fd[2];

  if(pipe(fd) < 0) { 
    printf("Can\'t create pipe\n");
    exit(-1); 
  }

  pid_t fpid = fork();
  if (fpid == 0) {
    close(0);
    close(fd[1]);
    char *s = (char *) malloc(sizeof(char));
    while(1) if (read(fd[0], s, 1)) printf("%i\n", *s);
  }
  close(fd[0]);
  char *c = (char *) malloc(sizeof(char));
  while (1) {
    if (read(0, c, 1) > 0) write(fd[1], c, 1);
  }
  return 0;
}

我希望能在每输入一个字符后看到其字符编码。但实际上,*s 只有在控制台中出现 '\n' 后才被打印。因此,似乎 stdin(文件描述符为 0)是带缓冲的。但是 read 函数是无缓冲的,不是吗?我错在哪里了。
更新:我使用的是 Linux。
因此,解决方案是:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>

int main(int argc, char *argv[], char *envp[]) {
  int fd[2];

  if(pipe(fd) < 0) { 
    printf("Can\'t create pipe\n");
    exit(-1); 
  }

  struct termios term, term_orig;

  if(tcgetattr(0, &term_orig)) {
    printf("tcgetattr failed\n");
    exit(-1); 
  }

  term = term_orig;

  term.c_lflag &= ~ICANON;
  term.c_lflag |= ECHO;
  term.c_cc[VMIN] = 0;
  term.c_cc[VTIME] = 0;

  if (tcsetattr(0, TCSANOW, &term)) {
    printf("tcsetattr failed\n");
    exit(-1);
  }

  pid_t fpid = fork();
  if (fpid == 0) {
    close(0);
    close(fd[1]);
    char *s = (char *) malloc(sizeof(char));
    while(1) if (read(fd[0], s, 1)) printf("%i\n", *s);
  }
  close(fd[0]);
  char *c = (char *) malloc(sizeof(char));
  while (1) {
    if (read(0, c, 1) > 0) write(fd[1], c, 1);
  }
  return 0;
} 

1
请注意,这与缓冲无关。 - Šimon Tóth
代码在退出之前,应该将父进程的终端属性重置为“term_orig”吗?你可能还应该让子进程在某个时候退出——在父进程关闭后,它将不断从“read()”中获得0。然而,父进程也处于无限循环中;进程只有在被信号通知时才会结束。你真的需要一个信号处理程序,用主要信号的原始终端值调用“tcsetattr()”,这些信号可能会被处理:HUP、INT、QUIT或许、PIPE和TERM是一个不错的集合。当然,你无法对KILL或STOP做任何事情。 - Jonathan Leffler
3个回答

15
很遗憾,使用标准的ANSI C是无法实现您所需的行为的。UNIX终端I/O的默认模式是面向行的,这意味着您始终需要输入\n字符才能检索输入。您需要使用终端I/O工具,在非规范模式下编程,以便每个按键触发一个事件。在Linux/UNIX上,您可以查看<termios.h>头文件或ncurses库。

4
@Ximik,是的,他们没有使用标准 ANSI C。他们大多数使用外部库,如ncurses或termcap。 - Charles Salvia

5

在我看来,你的解决方案有点复杂。仍然不明白为什么需要管道和两个进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>

int main(int argc, char *argv[], char *envp[]) {
  struct termios term, term_orig;

  if(tcgetattr(0, &term_orig)) {
    printf("tcgetattr failed\n");
    exit(-1);
  }

  term = term_orig;

  term.c_lflag &= ~ICANON;
  term.c_lflag |= ECHO;
  term.c_cc[VMIN] = 0;
  term.c_cc[VTIME] = 0;

  if (tcsetattr(0, TCSANOW, &term)) {
    printf("tcsetattr failed\n");
    exit(-1);
  }

  char ch;
  while (1) {
    if (read(0, &ch, 1) > 0) 
      printf(" %d\n", ch);
  }
  return 0;
}

3

Unix在内核中缓冲您的tty字符,部分原因是程序不必单独处理行编辑,除非他们想要这样做。

您可以指示tty驱动程序立即提供字节。有各种库可使此比使用原始ioctl更容易一些。 您可以从termios(3)开始。


3
可惜没人花时间改进这个内核级别的行编辑器,使其真正可用... 原则上它可以和 readline 一样好用。 - R.. GitHub STOP HELPING ICE

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