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

时间:2016-03-08 17:20:16

标签: c linux terminal termios

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

4 个答案:

答案 0 :(得分:4)

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

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

所以这里有两个间接层次。 FILE *可以全部引用相同的FILE,但他们不会; FILEstdinstdout有3个单独的stderr个对象。这些FILE对象每个都包含一个文件描述符,通常为0,1和2(我通常说 - OS / lib以这种方式设置它们,只有在程序中明确更改它们时它们才会改变) )。然后,3个文件描述符通常都将引用相同的底层OS对象,即单个终端对象。

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

请注意,这些fd可以 来引用不同的对象 - 如果您使用重定向(<>启动您的程序shell)然后其中一个或多个将引用其他文件对象而不是终端。

答案 1 :(得分:2)

为了简化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伪终端接口,并且可以使用例如正常的POSIX伪终端接口。所有POSIXy系统中的xterm或任何其他普通终端仿真器(或Linux控制台) - Linux,Mac OS X和BSD变体。

答案 2 :(得分:1)

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

  • tcgetattrfd参数(文件描述符)和tcsetattr必须是终端
  • 您可以使用fileno获取此流,例如fileno(stdin)
  • POSIX defines constants用于将文件描述符默认分配给 stdin stdout stderr STDIN_FILENO,{ {1}}和STDOUT_FILENO。但是,可以reopen任何流(或使用dupdup2)并更改实际的文件描述符。
  • 虽然您可以获取流的文件描述符,但如果您正在对终端属性执行任何有趣的操作,则可能会干扰用于流的缓冲。如果必须混合使用两者(文件描述符和流),请在读取或写入流之前对终端属性进行更改。
  • 您还可以使用终端设备上的STDERR_FILENO获取文件描述符。如果重定向流,并且您的应用程序必须使用终端,这将非常有用。密码提示执行此操作。
  • 可以从程序tty中读取终端设备(即使重定向 stdin 等)。
  • 程序可以使用isatty检查文件描述符以查看它是否是终端。如果将流重定向到文件或管道,则它不是终端。

进一步阅读:

答案 3 :(得分:0)

通过实验,我发现自己有以下答案:

三个0stderrstdout中的每一个都可用于通过stdin功能更改终端设置。完成更改后,阅读tcsetattr(fd....)tcgsetattr(stdin....)以及tcgsetattr(stdout....)返回tcgsetattr(sterr....)中可通过struct termios.h验证的相同内容

也许这个手册页有点间接地说明了这个

  

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

memcmp(&termios_stdin,&termios_stdout,sizeof(struct termios)) == 0因此fd引用的对象始终是相同的终端