Linux glibc, gcc, стандартные потоки. Где создаются стандартные потоки?

Рейтинг: 3Ответов: 3Опубликовано: 19.02.2023

Где код который создаёт стандартные потоки ввода, вывода, ошибки для нового процесса? Или всё процессы просто наследуют стандартные потоки? Тогда где код создающий потоки для первого процесса?

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 дескриптор которого нету. Системный вызов возвращает ошибку и утилита выводит причину ошибки на экран. Я хотел чтоб мне объяснили где создаются структуры стандартных потоков, при старте процесса. Или они просто приезжают с процессом он же копируется.

Ответы

▲ 5

Код, создающий потоки для переменных 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_

▲ 0

Сейчас обычно shell из которого мы запускаем свои команды сам запускается из эмулятора терминала (например, у меня это /usr/lib/gnome-terminal/gnome-terminal-server).

Именно он создает псевдотерминал к которому цепляет дескрипторы 0, 1 и 2 (stdin, stdout, stderr) процесса, в котором потом запускает shell (у меня это bash).

Таким образом, код, который вас интересует, находится в эмуляторе терминала.

Если говорить о библиотечных (в libc) вызовах, то запуск процесса под псевдотерминалом может быть сделана в forkpty, а для переключения потоков перед exec-ом используется, например, dup3

▲ 0

Потоки 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.