【问题标题】:When setting terminal attributes via tcsetattr(fd.....), can fd be either stdout or stdin?通过 tcsetattr(fd.....) 设置终端属性时,fd 可以是标准输出还是标准输入?
【发布时间】:2016-06-22 18:43:27
【问题描述】:

我一直在寻找man 3 tcgetattr(因为我想在程序中更改终端设置)并找到了这个。

int tcgetattr(int fd, struct termios *termios_p);

int tcsetattr(int fd, int optional_actions,
              const struct termios *termios_p);

问题:

我想知道fd 应该是什么意思? (好像是stdin,但我不明白为什么)?

背景

我的理解是终端是一起输入和输出的,因为我的理解是/dev/tty/dev/pty 一起产生stdinstdoutstderr

【问题讨论】:

  • fd 表示文件描述符
  • @HoKy22 谢谢,fd 提到的两个选项,即stdinstdout 都是文件描述符,因此不清楚,我需要问

标签: c linux terminal termios


【解决方案1】:

通过实验,我找到了以下答案:

stderr,stdout,stdin 三重中的每一个都可以通过tcsetattr(fd....) 函数用于更改终端设置。一旦更改生效,读取tcgsetattr(stdin....),tcgsetattr(stdout....),并且tcgsetattr(sterr....)struct termios.h 中返回相同的内容,可以通过memcmp(&termios_stdin,&termios_stdout,sizeof(struct termios)) == 0 进行验证

也许手册页也间接说明了这一点

tcgetattr() 获取与引用的对象关联的参数 fd 并将它们存储在由 termios_p 引用的 termios 结构中。 该函数可以从后台进程调用;然而 终端属性可能随后被前景改变 过程。

关于 fd 因此 fd 引用的对象始终是同一个终端

【讨论】:

    【解决方案2】:

    fd 代表文件描述符,它是对 OS 文件对象的引用。因为是引用,所以多个不同的文件描述符可能引用同一个文件对象。

    stdinstdoutstderrFILE * 对象 -- 指向 stdio FILE 数据结构的实际指针。您可以使用fileno 函数获取引用底层操作系统对象的文件描述符。

    所以这里有两个间接级别。 FILE * 都可以引用同一个FILE,但它们不是; stdinstdoutstderr 有 3 个单独的 FILE 对象。这些FILE 对象每个都包含一个文件描述符,通常为 0、1 和 2(我说的是正常的——OS/lib 以这种方式设置它们,只有在您在程序中显式更改它们时它们才会更改)。这 3 个文件描述符通常都会引用同一个底层 OS 对象,这是一个单一的终端对象。

    由于(通常)只有一个终端,并且所有这些文件描述符(通常)都引用它,因此您将哪个 fd(0、1 或 2)用作tcsetaddr 的第一个参数并不重要。

    请注意,这些fds 有可能引用不同的对象——如果您使用重定向启动程序(<> 在 shell 中)然后一个或它们中的更多将引用其他文件对象而不是终端。

    【讨论】:

      【解决方案3】:

      同意@chris-dodd,stdinstdoutstderr流对应的文件描述符通常指的是同一个终端,原始问题需要一些要点:

      • tcgetattrtcsetattrfd 参数(文件描述符)必须用于终端
      • 您可以使用fileno 为流获取此信息,例如fileno(stdin)
      • POSIX defines constants 用于将文件描述符默认分配给 stdinstdoutstderr,如 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO。但是,可以reopen 任何流(或使用dupdup2)并更改实际的文件描述符。
      • 虽然您可以获得流的文件描述符,但如果您对终端属性做任何有趣的事情,可能会干扰用于流的缓冲。如果您必须混合使用这两者(文件描述符和流),请在读取或写入流之前对终端属性进行更改
      • 您还可以在终端设备上使用open 获取文件描述符。如果流被重定向,并且您的应用程序必须与终端一起使用,这将很有用。密码提示执行此操作。
      • 可以从程序tty读取终端设备(即使stdin等被重定向)。
      • 程序可以使用isatty 检查一个文件描述符,看它是否是一个终端。如果流被重定向到文件或管道,则它不是终端。

      进一步阅读:

      【讨论】:

        【解决方案4】:

        为了简化Thomas Dickey'sChris Dodd's 的答案,选择使用哪个描述符来引用终端的典型代码是

        int ttyfd;
        
        /* Check standard error, output, and input, in that order. */
        if (isatty(fileno(stderr)))
            ttyfd = fileno(stderr);
        else
        if (isatty(fileno(stdout)))
            ttyfd = fileno(stdout);
        else
        if (isatty(fileno(stdin)))
            ttyfd = fileno(stdin);
        else
            ttyfd = -1; /* No terminal; redirecting to/from files. */
        

        如果您的应用程序坚持访问控制终端(用户用来执行此过程的终端),如果有,您可以使用以下new_terminal_descriptor() 函数。为简单起见,我将其嵌入到示例程序中:

        #define  _POSIX_C_SOURCE 200809L
        #include <stdlib.h>
        #include <unistd.h>
        #include <string.h>
        #include <termios.h>
        #include <sys/types.h>
        #include <sys/stat.h>
        #include <fcntl.h>
        #include <stdio.h>
        #include <errno.h>
        
        int new_terminal_descriptor(void)
        {
            /* Technically, the size of this buffer should be
             *  MAX( L_ctermid + 1, sysconf(_SC_TTY_NAME_MAX) )
             * but 256 is a safe size in practice. */
            char buffer[256], *path;
            int  fd;
        
            if (isatty(fileno(stderr)))
                if (!ttyname_r(fileno(stderr), buffer, sizeof buffer)) {
                    do {
                        fd = open(path, O_RDWR | O_NOCTTY);
                    } while (fd == -1 && errno == EINTR);
                    if (fd != -1)
                        return fd;
                }
        
            if (isatty(fileno(stdout)))
                if (!ttyname_r(fileno(stdout), buffer, sizeof buffer)) {
                    do {
                        fd = open(path, O_RDWR | O_NOCTTY);
                    } while (fd == -1 && errno == EINTR);
                    if (fd != -1)
                        return fd;
                }
        
            if (isatty(fileno(stdin)))
                if (!ttyname_r(fileno(stdin), buffer, sizeof buffer)) {
                    do {
                        fd = open(path, O_RDWR | O_NOCTTY);
                    } while (fd == -1 && errno == EINTR);
                    if (fd != -1)
                        return fd;
                }
        
            buffer[0] = '\0';
            path = ctermid(buffer);
            if (path && *path) {
                do {
                    fd = open(path, O_RDWR | O_NOCTTY);
                } while (fd == -1 && errno == EINTR);
                if (fd != -1)
                    return fd;
            }
        
            /* No terminal. */
            errno = ENOTTY;
            return -1;
        }
        
        static void wrstr(const int fd, const char *const msg)
        {
            const char       *p = msg;
            const char *const q = msg + ((msg) ? strlen(msg) : 0);    
            while (p < q) {
                ssize_t n = write(fd, p, (size_t)(q - p));
                if (n > (ssize_t)0)
                    p += n;
                else
                if (n != (ssize_t)-1)
                    return;
                else
                if (errno != EINTR)
                    return;
            }
        }
        
        int main(void)
        {
            int ttyfd;
        
            ttyfd = new_terminal_descriptor();
            if (ttyfd == -1)
                return EXIT_FAILURE;
        
            /* Let's close the standard streams,
             * just to show we're not using them
             * for anything anymore. */
            fclose(stdin);
            fclose(stdout);
            fclose(stderr);
        
            /* Print a hello message directly to the terminal. */
            wrstr(ttyfd, "\033[1;32mHello!\033[0m\n");
        
            return EXIT_SUCCESS;
        }
        

        wrstr() 函数只是一个辅助函数,它立即将指定的字符串写入指定的文件描述符,而不进行缓冲。该字符串包含 ANSI 颜色代码,因此如果成功,它将向终端打印浅绿色 Hello!,即使标准流已关闭。

        如果将上面的内容保存为example.c,则可以使用例如编译它

        gcc -Wall -Wextra -O2 example.c -o example
        

        并使用运行

        ./example
        

        因为new_terminal_descriptor() 使用ctermid() 函数来获取控制终端的名称(路径)作为最后的手段——这并不常见,但我想在这里展示如果你决定它很容易做到是必要的——即使所有流都被重定向,它也会将 hello 消息打印到终端:

        ./example </dev/null >/dev/null 2>/dev/null
        

        最后,如果您想知道,这些都不是“特别的”。我不是在谈论 控制台终端,它是许多 Linux 发行版作为图形环境的替代品提供的基于文本的控制台界面,也是大多数 Linux 服务器提供的唯一本地界面。以上所有都只使用普通的 POSIX 伪终端接口,并且使用例如可以正常工作。 xterm 或任何其他普通终端仿真器(或 Linux 控制台),适用于所有 POSIXy 系统——Linux、Mac OS X 和 BSD 变体。

        【讨论】:

        • 很高兴阅读您的信息丰富的答案及其代码,谢谢。 wrstr() 及其write() 不会导致系统缓冲的原因是我不明白的。我的看法是,这确实完全取决于该终端设备的设置,即在struct termios.c_lflag 中设置ICANON 标志应该make input "available line by line."。我错过了什么,你的好代码的哪一部分确实设置了这个?
        • @humanityANDpeace:你没有错过任何东西,我只是用词很糟糕。我指的是默认情况下标准 C I/O 为 stdout 所做的缓冲。本质上,wrstr() 只是立即将字符串交给内核——就像fputs(str, stdout); fflush(stdout); 那样。
        猜你喜欢
        • 1970-01-01
        • 2023-03-15
        • 2016-04-27
        • 2019-01-31
        • 2022-07-25
        • 1970-01-01
        • 2018-12-05
        • 2015-01-02
        相关资源
        最近更新 更多