Запуск приложений #

Васильев Андрей Михайлович, 2024

Версии презентации

Структура потоков ввода-вывода процесса #

В момент запуска процесса ему выделяются стандартные потоки для ввода и вывода информации

  • 0, stdin — стандартный поток ввода
  • 1, stdout — стандартный поток вывода
  • 2, stderr — стандартный поток вывода сообщений об ошибках

Процесс, инициирующий запуск другого процесса, определяет на какие файловые дескрипторы будут переданы работающему процессу

Процесс во время своей работы может самостоятельно изменить направление потоков, однако эта возможность используется в основном управляющими процессами (например интерпретатором Bash)

diagram

Использование stderr #

Java #

System.out.println("Текст выведен на стандартный поток вывода")
System.err.println("Текст выведен на поток ошибок")

Python #

import sys
print("Текст на стандартный поток вывода")
print("Текст на поток ошибок", file=sys.stderr)

Си #

#include <stdio.h>
int main() {
  printf("Поток вывода\n");
  fprintf(stderr, "Поток ошибок\n");
}

Потоки ввода-вывода в эмуляторе терминала #

diagram

Эмулятор терминала общается с пользователем только по двум каналам:

  • Ввод данных с помощью клавиатуры (буфера обмена)
  • Вывод текстовых (и псевдографических) данных на экран терминала

Если специальных действий не было предпринято, тогда

  • Поток событий с клавиатуры передаётся на стандартный поток ввода
  • Содержимое потоков stdout и stderr выводится на экран в истории

С точки зрения пользователя потоки stdout и stderr выводятся как результат работы приложения, ничем не отличимые друг от друга

Если данные с потоков приходят одновременно, то возможно их пересечение, обычно происходит путём чередования строк

Создание новых процессов #

В Linux для создания нового процесса используется системный вызов:

#include <unistd.h>
pid_t fork(void); // man 2 fork
  • -1 сообщает, что задача была выполнена неуспешно
  • Процесс-родитель получает уникальный идентификатор процесса-ребёнка
  • Процесс-ребёнок получает 0 в качестве ответа

diagram

Процесс-ребёнок является точной копией процесса родителя

Наследуемые ресурсы #

Процесс-ребёнок разделяет с процессом-родителем все ресурсы, включая:

  • Список открытых файловых дискрипторов
  • Текущий срез оперативной памяти
  • Параметры переменных окружения
  • Общий программный код для исполнения

На основе кода возврата новый процесс должен решить: что ему делать дальше

  • Продолжить выполнять код оригинального процесса
  • Заменить исполняемый код на другой

Пример вызова fork #

#include <signal.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(void)
{
    pid_t pid;

    if (signal(SIGCHLD, SIG_IGN) == SIG_ERR) {
        perror("signal");
        exit(EXIT_FAILURE);
    }
    pid = fork();
    switch (pid) {
        case -1:
            perror("fork");
            exit(EXIT_FAILURE);
        case 0:
            puts("Child exiting.");
            exit(EXIT_SUCCESS);
        default:
            printf("Child is PID %jd\n", (intmax_t) pid);
            puts("Parent exiting.");
            exit(EXIT_SUCCESS);
    }
}
$ gcc info.c -o fork-test
$ ./fork-test
Child is PID 43594
Parent exiting.
Child exiting.

Группа системных вызовов exec #

Для замены исполняемого кода используются системные вызовы exec*, man 2 execve

diagram

Состояние нового процесса #

  • Все компоненты процесса, связанные с исполнением кода заменяются и инициализируются
  • Большинство ресурсов наследуется
    • Переменные окружения наследуются (если не были заменены)
    • Открытые файловые дескрипторы наследуются

Пример использования exec #

/* myecho.c */

#include <stdio.h>
#include <stdlib.h>

int
main(int argc, char *argv[])
{
    for (size_t j = 0; j < argc; j++)
        printf("argv[%zu]: %s\n", j, argv[j]);

    exit(EXIT_SUCCESS);
}
/* execve.c */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    static char *newargv[] = { NULL, "hello", "world", NULL };
    static char *newenviron[] = { NULL };

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    newargv[0] = argv[1];

    execve(argv[1], newargv, newenviron);
    perror("execve");   /* execve() returns only on error */
    exit(EXIT_FAILURE);
}
$ cc myecho.c -o myecho
$ cc execve.c -o execve
$ ./execve ./myecho
argv[0]: ./myecho
argv[1]: hello
argv[2]: world

Системный вызов freopen #

Системный вызов позволяет открыть новый файл, но вместо создания нового файлового дескриптора, будет использован один из существующих

Обычно используется для замены файловых дескрипторов стандартных потоков stdin, stderr, stdout

  • Изменим предыдущий пример по запуску внешнего приложения таким образом, чтобы стандартный поток вывода указывал на файл stdout.log
  • Изменить поток вывода необходимо до замены исполняемого кода внутри процесса
freopen("output.log", "w", stdout);

Документация доступна в man 3 fopen

Пример использования freopen #

/* execve-freopen.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main(int argc, char *argv[])
{
    static char *newargv[] = { NULL, "hello", "world", NULL };
    static char *newenviron[] = { NULL };

    if (argc != 2) {
        fprintf(stderr, "Usage: %s <file-to-exec>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    freopen("output.log", "w", stdout);

    newargv[0] = argv[1];

    execve(argv[1], newargv, newenviron);
    perror("execve");   /* execve() returns only on error */
    exit(EXIT_FAILURE);
}
$ cc execve-freopen.c -o execve-freopen.c
$ ./execve-freopen ./myecho