问题
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 fd
s 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
andSTDERR_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