Linux glibc, gcc, стандартные потоки. Где создаются стандартные потоки?
Где код который создаёт стандартные потоки ввода, вывода, ошибки для нового процесса? Или всё процессы просто наследуют стандартные потоки? Тогда где код создающий потоки для первого процесса?
man 3 stdio
При запуске программы предопределяются три текстовых потока, которые не следует
открывать явно: стандартный ввод (standard input) (для чтения условного ввода),
стандартный вывод (standard output) (для записи условного вывода) и стандартный поток
ошибок (standard error) (для вывода диагностики). Сокращённые названия потоков: stdin,
stdout и stderr.
Никто не дал ответа на вопрос. Когда вы делаете что-то подобное.
grep '.' >&- <(echo Y)
Оболочка Bash закрывает дескриптор который содержит поле _fileno структуры FILE для stdout - поток стандартного вывода, для нового процесса после fork и перед execve(grep). Утилита grep использует функции stdio для вывода результата поиска. В итоге функция вывода stdio передают системному вызову write дескриптор которого нету. Системный вызов возвращает ошибку и утилита выводит причину ошибки на экран. Я хотел чтоб мне объяснили где создаются структуры стандартных потоков, при старте процесса. Или они просто приезжают с процессом он же копируется.
Ответы (3 шт):
Сейчас обычно shell из которого мы запускаем свои команды сам запускается из эмулятора терминала (например, у меня это /usr/lib/gnome-terminal/gnome-terminal-server).
Именно он создает псевдотерминал к которому цепляет дескрипторы 0, 1 и 2 (stdin, stdout, stderr) процесса, в котором потом запускает shell (у меня это bash).
Таким образом, код, который вас интересует, находится в эмуляторе терминала.
Если говорить о библиотечных (в libc) вызовах, то запуск процесса под псевдотерминалом может быть сделана в forkpty, а для переключения потоков перед exec-ом используется, например, dup3
Код, создающий потоки для переменных stdin, stdout, stderr находится в файле glibc/libio/stdio.c:
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdin = (FILE *) &_IO_2_1_stdin_;
FILE *stdout = (FILE *) &_IO_2_1_stdout_;
Переменные _IO_2_1_... определены в файле glibc/libio/stdfiles.c как обёртки типа FILE вокруг файловых дескрипторов 0,1,2.
Судя по вашему вопросу, вас интересует, откуда берутся файловые дескрипторы с номерами 0, 1 и 2. Ответ - из процесса предка. Процессы в Linux в большинстве своём порождаются парой системных вызовов fork/clone + execve. Процессы, порождаемые fork, наследуют таблицу открытых файлов предка. Системный вызов execve так же не меняет таблицу открытых файлов (за исключением файлов, открытых с флагом O_CLOEXEC).
Где код который создаёт стандартные потоки ввода, вывода, ошибки для нового процесса?
На ваш вопрос нет одного ответа. Всё зависит от того, как именно был порождён процесс. Например, если он был запущен из bash, то этот код находится в файле execute_cmd.c
Если же вы сами порождаете процесс, то этот код находится в вашей программе. Вот маленький пример. Он запускает процесс с именем child.exe и перенаправленными потоками: вход child.exe читает из файла с именем input, стандартный вывод пишет в файл out, а ошибки в файл err. Файловые дескрипторы в этом примере открываются в порождённом процессе между fork и execv.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <string.h>
#include <sys/wait.h>
#include <stdint.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
pid_t cpid, w;
int wstatus;
cpid = fork();
if (cpid == -1)
{
perror("fork");
exit(EXIT_FAILURE);
}
if (cpid == 0)
{ /* Code executed by child */
int fd0=-1, fd1=-1, fd2=-1;
FILE *ch_log;
int ret = 0;
ch_log = fopen("./child.log", "w");
if (ch_log == NULL) {
perror("Failed to open log");
exit(-1);
}
fprintf(ch_log, "Hello\n");
close(0);
close(1);
close(2);
fd0 = open("./input", O_RDONLY);
if (fd0 < 0) {
fprintf(ch_log, "Failed to open input: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened input: %d\n", fd0);
}
fd1 = open("./out", O_RDWR|O_CREAT|O_TRUNC);
if (fd1 < 0) {
fprintf(ch_log, "Failed to open out: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened out: %d\n", fd1);
}
fd2 = open("./err", O_RDWR|O_CREAT|O_TRUNC);
if (fd2 < 0) {
fprintf(ch_log, "Failed to open err: %s\n", strerror(errno));
ret = -1;
goto close_log;
} else {
fprintf(ch_log, "Opened err: %d\n", fd2);
}
close_log:
fclose(ch_log);
if (ret < 0) {
if (fd0 >= 0) { close(fd0); }
if (fd1 >= 0) { close(fd1); }
if (fd2 >= 0) { close(fd2); }
exit(-1);
}
execl("./child.exe", (char*)NULL);
exit(ret);
}
else
{ /* Code executed by parent */
do
{
w = waitpid(cpid, &wstatus, WUNTRACED | WCONTINUED);
if (w == -1)
{
perror("waitpid");
exit(EXIT_FAILURE);
}
if (WIFEXITED(wstatus))
{
printf("exited, status=%d\n", WEXITSTATUS(wstatus));
}
else if (WIFSIGNALED(wstatus))
{
printf("killed by signal %d\n", WTERMSIG(wstatus));
}
else if (WIFSTOPPED(wstatus))
{
printf("stopped by signal %d\n", WSTOPSIG(wstatus));
}
else if (WIFCONTINUED(wstatus))
{
printf("continued\n");
}
} while (!WIFEXITED(wstatus) && !WIFSIGNALED(wstatus));
exit(EXIT_SUCCESS);
}
}
ВДОГОНКУ
Как убедиться, что stdout инициализируется статически.
Для простоты не будем связываться с динамической линковкой, посмотрим на статически слинкованный файл. Испытания будем проводить на файле some.c
#include <stdio.h>
int main() {
FILE z = *stdout;
printf("stdout == &_IO_2_1_stdout_: %d\n", stdout == &_IO_2_1_stdout_);
printf("_flags: %o\n", z._flags);
printf("_IO_read_ptr: %p\n", z._IO_read_ptr);
printf("_IO_read_end: %p\n", z._IO_read_end);
printf("_IO_read_base: %p\n", z._IO_read_base);
printf("_IO_write_base: %p\n", z._IO_write_base);
printf("_IO_write_ptr: %p\n", z._IO_write_ptr);
printf("_IO_write_end: %p\n", z._IO_write_end);
printf("_IO_buf_base: %p\n", z._IO_buf_base);
printf("_IO_buf_end: %p\n", z._IO_buf_end);
printf("_IO_save_base: %p\n", z._IO_save_base);
printf("_IO_backup_base: %p\n", z._IO_backup_base);
printf("_IO_save_end: %p\n", z._IO_save_end);
printf("_markers: %p\n", z._markers);
printf("_chain: %p\n", z._chain);
printf("_fileno: %d\n", z._fileno);
printf("_flags2: %o\n", z._flags2);
}
Компилируем: gcc -static some.c
Получаем: ll a.out => 845152 Feb 22 22:03 a.out*
Запускаем:
$ ./a.out
stdout == &_IO_2_1_stdout_: 1
_flags: 37353220204
_IO_read_ptr: (nil)
_IO_read_end: (nil)
_IO_read_base: (nil)
_IO_write_base: (nil)
_IO_write_ptr: (nil)
_IO_write_end: (nil)
_IO_buf_base: (nil)
_IO_buf_end: (nil)
_IO_save_base: (nil)
_IO_backup_base: (nil)
_IO_save_end: (nil)
_markers: (nil)
_chain: 0x6b9580
_fileno: 1
_flags2: 0
Первая строка подтверждает, что stdout == &_IO_2_1_stdout_
Последующие строки печатают часть полей объекта типа FILE, на который указывает значение символа stdout
Следующий шаг: смотрим таблицу символов получившегося файла:
$ objdump -x a.out | grep stdout
00000000006b97a0 g O .data 0000000000000008 stdout
00000000006b9360 g O .data 00000000000000e0 _IO_2_1_stdout_
00000000006b97a0 g O .data 0000000000000008 .hidden _IO_stdout
Символы stdout и _IO_stdout занимают 8 байтов по смещению 6b97a0 от начала файла, в секции .data
Символ _IO_2_1_stdout_ находится по смещению 6b9360 тоже в секции .data.
Далее: смотрим, как эти символы используются в коде приложения.
$ objdump --disassemble a.out | grep -E '(<[^>]+>:|stdout)' | grep stdout -B1
0000000000400b6d <main>:
400b87: 48 8b 05 12 8c 2b 00 mov 0x2b8c12(%rip),%rax # 6b97a0 <_IO_stdout>
400caa: 48 8b 15 ef 8a 2b 00 mov 0x2b8aef(%rip),%rdx # 6b97a0 <_IO_stdout>
400cb1: 48 8d 05 a8 86 2b 00 lea 0x2b86a8(%rip),%rax # 6b9360 <_IO_2_1_stdout_>
--
000000000040f960 <_IO_printf>:
40f9d9: 48 8b 3d c0 9d 2a 00 mov 0x2a9dc0(%rip),%rdi # 6b97a0 <_IO_stdout>
--
000000000040ff60 <_IO_new_fclose>:
40fff4: 48 39 1d a5 97 2a 00 cmp %rbx,0x2a97a5(%rip) # 6b97a0 <_IO_stdout>
--
00000000004106d0 <_IO_wfile_underflow>:
41083c: 48 8b 2d 5d 8f 2a 00 mov 0x2a8f5d(%rip),%rbp # 6b97a0 <_IO_stdout>
41089f: 48 8b 3d fa 8e 2a 00 mov 0x2a8efa(%rip),%rdi # 6b97a0 <_IO_stdout>
410cbf: 48 8b 3d da 8a 2a 00 mov 0x2a8ada(%rip),%rdi # 6b97a0 <_IO_stdout>
--
0000000000412900 <_IO_new_file_underflow>:
41294a: 4c 8b 2d 4f 6e 2a 00 mov 0x2a6e4f(%rip),%r13 # 6b97a0 <_IO_stdout>
412ae4: 48 8b 3d b5 6c 2a 00 mov 0x2a6cb5(%rip),%rdi # 6b97a0 <_IO_stdout>
412b75: 48 8b 3d 24 6c 2a 00 mov 0x2a6c24(%rip),%rdi # 6b97a0 <_IO_stdout>
Символ stdout используется в функциях main, _IO_printf, _IO_new_fclose, _IO_wfile_underflow, _IO_new_file_underflow. Если поглядеть в дизассемблированный код, то можно глазками убедиться, что этот символ используется только для чтения. Так как приложение слинковано статически, то нет никакого другого кода, в котором переменные stdout и _IO_2_1_stdout_ могли бы инициализироваться. То есть это данные, статически инциализированные компилятором.
Финальный шаг: подтверждение того, что stdout - это статический указатель на _IO_2_1_stdout_
Извлечём объектный файл stdio.o из библиотеки libc.a: $ ar x /usr/lib/x86_64-linux-gnu/libc.a stdio.o
Теперь посмотрим таблицу символов и релокации этого объектного файла:
$ objdump -t -r stdio.o
stdio.o: file format elf64-x86-64
SYMBOL TABLE:
0000000000000000 l d .text 0000000000000000 .text
0000000000000000 l d .data 0000000000000000 .data
0000000000000000 l d .bss 0000000000000000 .bss
0000000000000000 l d .data.rel 0000000000000000 .data.rel
0000000000000000 l d .note.GNU-stack 0000000000000000 .note.GNU-stack
0000000000000000 g O .data.rel 0000000000000008 stderr
0000000000000000 *UND* 0000000000000000 _IO_2_1_stderr_
0000000000000000 g O .data.rel 0000000000000008 .hidden _IO_stderr
0000000000000008 g O .data.rel 0000000000000008 stdout
0000000000000000 *UND* 0000000000000000 _IO_2_1_stdout_
0000000000000008 g O .data.rel 0000000000000008 .hidden _IO_stdout
0000000000000010 g O .data.rel 0000000000000008 stdin
0000000000000000 *UND* 0000000000000000 _IO_2_1_stdin_
0000000000000010 g O .data.rel 0000000000000008 .hidden _IO_stdin
RELOCATION RECORDS FOR [.data.rel]:
OFFSET TYPE VALUE
0000000000000000 R_X86_64_64 _IO_2_1_stderr_
0000000000000008 R_X86_64_64 _IO_2_1_stdout_
0000000000000010 R_X86_64_64 _IO_2_1_stdin_
Символ stdout занимает 8 байт по смещению 8 в секции .data.rel, а таблица релокации предписывает записать в эту секцию по смещению 8 адрес символа _IO_2_1_stdout_.
Итого. Когда линкер собирает исполнимый файл, в котором упоминается stdout, он берёт этот символ из объектного файла libc.a/stdio.o и в соответствии с таблицей релокации подставляет в значение этого символа смещение символа _IO_2_1_stdout_ из объектного файла libc.a/stdfiles.o, то есть инициализирует stdout как указатель на _IO_2_1_stdout_
Потоки stdin/stdout/stderr создают терминалы: программы getty, sshd и эмуляторы терминалов.
Самый простой случай - терминал без иксов.
Потоки создаются в программе agetty вот в этой строчке
https://github.com/karelzak/util-linux/blob/master/term-utils/agetty.c#L1059 смотрите строчки:
Открывается терминал /dev/ttyX как файл
if (open(buf, O_RDWR|O_NOCTTY|O_NONBLOCK, 0) != 0)
Вывод делается из ввода
if (dup(STDIN_FILENO) != 1 || dup(STDIN_FILENO) != 2)
Далее они копируются в программу login, потом из неё в программу bash, а далее их получает Ваша программа.
Предопределяются - не значит создаются. В этом случае они устанавливаются родительским процессом.
Дескриптор это почти поток. Далее процитирую
Код, создающий потоки для переменных stdin, stdout, stderr находится в файле glibc/libio/stdio.c:
FILE *stdin = (FILE *) &IO_2_1_stdin; FILE *stdin = (FILE *) &IO_2_1_stdin; FILE *stdout = (FILE *) &IO_2_1_stdout; Переменные IO_2_1... определены в файле glibc/libio/stdfiles.c как обёртки типа FILE вокруг файловых дескрипторов 0,1,2.