通过tcsetattr(fd.....)设置终端属性时,fd可以是stdout还是stdin?

When setting terminal attributes via tcsetattr(fd.....), can fd be either stdout or stdin?

我一直在寻找 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 一起。

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

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

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

tcgetattr() gets the parameters associated with the object referred by fd and stores them in the termios structure referenced by termios_p. This function may be invoked from a background process; however, the terminal attributes may be subsequently changed by a foreground process.

about fd 因此 fd 引用的对象总是同一个终端

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

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

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

因为(通常)只有一个终端,并且所有这些文件描述符(通常)都引用它,所以使用哪个 fd(0、1 或 2)作为 [= 的第一个参数并不重要24=].

请注意, 这些 fd 可能引用不同的对象 -- 如果您使用重定向(<> 在 shell) 中,那么其中一个或多个将引用其他文件对象而不是终端。

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

  • fd tcgetattr and tcsetattrfd 参数( 文件描述符 )必须是 终端
  • 您可以使用 fileno 为流获取此信息,例如 fileno(stdin)
  • POSIX defines constants for the default assignment of file descriptors to stdin, stdout and stderr as STDIN_FILENO, STDOUT_FILENO and STDERR_FILENO. However, it is possible to reopen any of the streams (or use dup or dup2) 并更改实际的文件描述符。
  • 虽然您可以获得流的文件描述符,但如果您正在对终端属性做任何有趣的事情,这可能会干扰用于流的缓冲。如果您必须混合使用两者(文件描述符和流),请在读取或写入流之前更改终端属性
  • 您还可以在终端设备上使用 open 获取文件描述符。如果流被重定向,并且您的应用程序必须与终端一起工作,这将很有用。密码提示执行此操作。
  • 可以从程序中读取终端设备tty(即使stdin等被重定向)
  • 程序可以使用 isatty 检查 文件描述符 以查看它是否是终端。如果流被重定向到文件或管道,则它不是终端。

进一步阅读:

为了简化 and 答案,select 用于引用终端的描述符的典型代码是

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] = '[=11=]';
    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, "3[1;32mHello!3[0m\n");

    return EXIT_SUCCESS;
}

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

如果你把上面的保存为example.c,你可以使用例如

编译它
gcc -Wall -Wextra -O2 example.c -o example

和运行使用

./example

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

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

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