Запуск приложений #
Васильев Андрей Михайлович, 2024
Версии презентации
Структура потоков ввода-вывода процесса #
В момент запуска процесса ему выделяются стандартные потоки для ввода и вывода информации
0
,stdin
— стандартный поток ввода1
,stdout
— стандартный поток вывода2
,stderr
— стандартный поток вывода сообщений об ошибках
Процесс, инициирующий запуск другого процесса, определяет на какие файловые дескрипторы будут переданы работающему процессу
Процесс во время своей работы может самостоятельно изменить направление потоков, однако эта возможность используется в основном управляющими процессами (например интерпретатором Bash)
Использование 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");
}
Потоки ввода-вывода в эмуляторе терминала #
Эмулятор терминала общается с пользователем только по двум каналам:
- Ввод данных с помощью клавиатуры (буфера обмена)
- Вывод текстовых (и псевдографических) данных на экран терминала
Если специальных действий не было предпринято, тогда
- Поток событий с клавиатуры передаётся на стандартный поток ввода
- Содержимое потоков
stdout
иstderr
выводится на экран в истории
С точки зрения пользователя потоки stdout
и stderr
выводятся как результат работы приложения, ничем не отличимые друг от друга
Если данные с потоков приходят одновременно, то возможно их пересечение, обычно происходит путём чередования строк
Создание новых процессов #
В Linux для создания нового процесса используется системный вызов:
#include <unistd.h>
pid_t fork(void); // man 2 fork
- -1 сообщает, что задача была выполнена неуспешно
- Процесс-родитель получает уникальный идентификатор процесса-ребёнка
- Процесс-ребёнок получает 0 в качестве ответа
Процесс-ребёнок является точной копией процесса родителя
Наследуемые ресурсы #
Процесс-ребёнок разделяет с процессом-родителем все ресурсы, включая:
- Список открытых файловых дискрипторов
- Текущий срез оперативной памяти
- Параметры переменных окружения
- Общий программный код для исполнения
На основе кода возврата новый процесс должен решить: что ему делать дальше
- Продолжить выполнять код оригинального процесса
- Заменить исполняемый код на другой
Пример вызова 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
Состояние нового процесса #
- Все компоненты процесса, связанные с исполнением кода заменяются и инициализируются
- Большинство ресурсов наследуется
- Переменные окружения наследуются (если не были заменены)
- Открытые файловые дескрипторы наследуются
Пример использования 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