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

非 Y 不嫁゛ 提交于 2019-12-10 22:37:56

问题


I have been looking int the man 3 tcgetattr (as I want to change the terminal settings in a program) and found this.

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

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

Question:

I would like to know what fd should mean? (it seems to be stdin, yet I do not understand why)?

Background

My comprehension is that terminal is input and output together, as my understanding was that a /dev/tty or /dev/pty yields stdin, stdout and stderr together.


回答1:


fd stands for file descriptor, which is a reference to an OS file object. Because it is a reference, multiple different file descriptors may refer to the same file object.

stdin, stdout, and stderr are FILE * objects -- actual pointers to stdio FILE datastructures. You can get the file descriptor that refers to the underlying OS object with the fileno function.

So there's two levels of indirection going on here. The FILE * could all refer to the same FILE, but they don't; there are 3 separate FILE objects for stdin, stdout, and stderr. These FILE objects each contain a file descriptor, normally 0, 1 and 2 (I say normally -- the OS/lib sets them up this way and they'll only change if you explicitly change them in your program). The 3 file descriptors will then normally all refer to the same underlying OS object, which is a single terminal object.

Since there's (generally) only one terminal, and all these file descriptors (usually) refer to it, it doesn't matter which fd (0, 1, or 2) you use as the first argument to tcsetaddr.

Note that it is possible for these fds to refer to different objects -- if you start your program with redirections (< or > in the shell) then one or more of them will refer to some other file object and not the terminal.




回答2:


To simplify Thomas Dickey's and Chris Dodd's answers, the typical code to select which descriptor is used to refer to the terminal is

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. */

If your application insists on having access to the controlling terminal (the terminal the user used to execute this process), if there is one, you can use the following new_terminal_descriptor() function. For simplicity, I'll embed it in an example program:

#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;
}

The wrstr() function is just a helper function, that immediately writes the specified string to the specified file descriptor, without buffering. The string contains ANSI color codes, so that if successful, it will print a light green Hello! to the terminal, even if the standard streams were closed.

If you save the above as example.c, you can compile it using e.g.

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

and run using

./example

Because the new_terminal_descriptor() uses ctermid() function to obtain the name (path) to the controlling terminal as a last resort -- this is not common, but I wanted to show here it is easy to do if you decide it is necessary --, it will print the hello message to the terminal even when all streams are redirected:

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

Finally, in case you are wondering, none of this is "special". I am not talking about the console terminal, which is the text-based console interface many Linux distributions provide as an alternative to the graphical environment, and the only local interface most Linux servers provide. All of the above uses just normal POSIX pseudoterminal interfaces, and will work just fine using e.g. xterm or any other normal terminal emulator (or Linux console), in all POSIXy systems -- Linux, Mac OS X, and the BSD variants.




回答3:


Agreeing with @chris-dodd that the file descriptors corresponding to the streams stdin, stdout and stderr usually refer to the same terminal, some points are needed for the original question:

  • the fd parameter (file descriptor) for tcgetattr and tcsetattr has to be for a terminal.
  • you could obtain this for a stream using fileno, e.g., 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) and alter the actual file descriptor.
  • while you can obtain a file descriptor for a stream, if you are doing anything interesting with the terminal attributes, that can interfere with the buffering used for streams. If you have to mix the two (file descriptor and stream), do the changes to terminal attributes before reading or writing the stream.
  • you can also get a file descriptor using an open on the terminal device. This is useful if the streams are redirected, and your application has to work with the terminal. Password prompts do this.
  • the terminal device can be read from the program tty (even if stdin, etc., are redirected).
  • programs can check a file descriptor using isatty to see if it is a terminal. If a stream is redirected to a file or pipe, it is not a terminal.

Further reading:

  • 11. General Terminal Interface (POSIX)
  • What is the difference between stdin and STDIN_FILENO?



回答4:


By way of experiment I have found myself the following answer:

Each of the triple stderr,stdout,stdin was able to be used to change the terminal settings via the tcsetattr(fd....) function. Once a change was effectuated reading tcgsetattr(stdin....),tcgsetattr(stdout....), and also tcgsetattr(sterr....) returned the same content in the struct termios.h which could be verified via memcmp(&termios_stdin,&termios_stdout,sizeof(struct termios)) == 0

also maybe the man page stated somewhat indirectly this

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 hence the object referred by fd is always the same terminal



来源:https://stackoverflow.com/questions/35873843/when-setting-terminal-attributes-via-tcsetattrfd-can-fd-be-either-stdout

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!