
- •Часть 3. Подсистема управления процессами
- •Глава 7. Среда окружения процесса 2
- •Глава 8. Управление процессами 16
- •Глава 9. Взаимоотношения между процессами 49
- •Глава 10. Сигналы 62
- •Глава 11. Процессы-демоны 98
- •Глава 7. Среда окружения процесса
- •7.1. Введение
- •7.2. Функция main
- •7.3. Завершение работы процесса
- •7.3.1. Функции семейства exit
- •7.3.2. Функция atexit
- •7.4. Аргументы командной строки
- •7.5. Раскладка памяти программы на языке c
- •7.6. Разделяемые библиотеки
- •7.7. Распределение памяти
- •7.8. Список переменных окружения
- •7.9. Функции getrlimit и setrlimit
- •7.10. Выводы по главе 7
- •7.11. Упражнения по главе 7
- •Глава 8. Управление процессами
- •8.1. Введение
- •8.2. Идентификаторы процесса
- •8.3. Функция fork
- •8.4. Совместное использование файлов
- •8.5. Функция exit
- •8.6. Функции wait и waitpid
- •8.7. Гонка за ресурсами
- •8.8. Функция exec
- •8.9. Изменение идентификаторов пользователя и группы
- •8.9.1. Функции setreuid и setregid
- •8.9.2. Функции seteuid и setegid
- •8.9.3. Идентификаторы группы
- •8.10. Интерпретируемые файлы
- •8.11. Функция system
- •8.12. Идентификация пользователя
- •8.13. Временные характеристики процесса
- •8.14. Выводы по главе 8
- •8.15. Упражнения по главе 8
- •Глава 9. Взаимоотношения между процессами
- •9.1. Введение
- •9.2. Вход с терминала
- •9.2.1. Вход в систему с терминала в bsd-системах
- •9.2.2. Вход в систему с терминала в Linux
- •9.3. Вход в систему через сетевое соединение
- •9.3.1. Вход в систему через сетевое соединение в bsd-системах
- •9.3.2. Вход в систему через сетевое соединение в Linux
- •9.4. Группы процессов
- •9.5. Сессии
- •9.6. Управляющий терминал
- •9.7. Функции tcgetpgrp, tcsetpgrp и tcgetsid
- •9.8. Выводы по главе 9
- •9.9. Упражнения по главе 9
- •Глава 10. Сигналы
- •10.1. Введение
- •10.2. Концепция сигналов
- •10.3. Функция signal
- •10.3.1. Запуск программы
- •10.3.2. Создание процесса
- •10.4. Ненадежные сигналы
- •10.5. Прерванные системные вызовы
- •10.6. Реентерабельные функции
- •10.7. Надежные сигналы. Терминология и семантика
- •10.8. Функции kill и raise
- •10.9. Функции alarm и pause
- •10.10. Наборы сигналов
- •10.11. Функция sigprocmask
- •10.12. Функция sigpending
- •10.13. Функция sigaction
- •10.14. Функция sigsuspend
- •10.15. Функция abort
- •10.16. Функция system
- •10.17. Функция sleep
- •10.18. Дополнительные возможности
- •10.19. Выводы по главе 10
- •10.20. Упражнения по главе 10
- •Глава 11. Процессы-демоны
- •11.1. Введение
- •11.2. Характеристики демонов
- •11.3. Правила программирования демонов
- •11.4. Журналирование ошибок
- •11.5. Демоны в единственном экземпляре
- •11.6. Соглашения для демонов
- •11.7. Модель клиент-сервер
- •11.8. Выводы по главе 11
- •11.9. Упражнения по главе 11
8.7. Гонка за ресурсами
Мы будем называть гонкой за ресурсами состояние, которое возникает в том случае, когда несколько процессов пытаются одновременно производить некоторые действия с данными, находящимися в совместном использовании, и конечный результат зависит от порядка, в котором эти процессы выполняются. Функция fork представляет собой яркий пример потенциального источника проблем, связанных с гонкой за ресурсами, если логика выполнения программы явно или неявно зависит от того, кто первым получит управление – родительский процесс или дочерний. Вообще говоря, это невозможно заранее предсказать. Но даже если бы мы знали наверняка, какой из процессов первым получит управление, то все равно дальнейшая работа процесса зависит от степени нагрузки на систему и алгоритма планирования, заложенного в ядре.
Мы уже встречались с потенциальной ситуацией гонки за ресурсами в программе из листинга 8.5, когда второй потомок выводил идентификатор родительского процесса. Если второй потомок получит управление раньше первого, то его родительским процессом будет первый потомок. Но если сначала получит управление первый потомок и у него будет достаточно времени, чтобы успеть завершиться, то родительским процессом для второго потомка станет процесс init. Даже вызов функции sleep, который использовался в нашем примере, не может гарантировать, что процессы будут выполняться в заданном порядке. Если система сильно загружена, то даже после двухсекундной задержки второй потомок может получить управление раньше, чем первому потомку удастся завершить свою работу. Проблемы такого рода очень сложны в отладке, потому что в большинстве случаев они себя никак не проявляют.
Процесс, который желает дождаться завершения дочернего процесса, должен вызвать одну из функций семейства wait. Если же процесс хочет дождаться завершения родительского процесса, как в программе из листинга 8.5, то можно было бы воспользоваться примерно таким циклом:
while (getppid() != 1)
sleep(1);
Однако этот цикл, который называется опросом (polling), порождает еще одну проблему. Дело в том, что процесс непроизводительно расходует процессорное время, так как вызывающий процесс возобновляет работу каждую секунду, чтобы проверить истинность условия.
Чтобы не попасть в гонку за ресурсами и избежать применения опроса, необходимы средства, с помощью которых процессы могли бы синхронизировать свои действия. Для этой цели можно использовать сигналы, и один такой способ будет описан в разделе 10.16. Также можно использовать различные виды межпроцессного взаимодействия (IPC), которые будут рассмотрены в четвертой части данного пособия.
8.8. Функция exec
Мы уже говорили в разделе 8.3, что функция fork часто используется для создания нового процесса, который затем запускает другую программу с помощью одной из функций семейства exec. Когда процесс вызывает одну из функций exec, то он полностью замещается другой программой, и эта новая программа начинает выполнение собственной функции main. Идентификатор процесса при этом не изменяется, поскольку функция exec не создает нового процесса, она просто замещает текущий процесс – его сегмент кода, сегмент данных, динамическую область памяти и сегмент стека – другой программой.
Существует шесть различных функций exec, но мы обычно будем говорить просто о “функции exec”, подразумевая любую из них. Эти шесть функций завершают список примитивов Unix, предназначенных для управления процессами. С помощью функции fork можно создавать новые процессы, с помощью функций exec – запускать новые программы. Функция exit и функции семейства wait обслуживают процедуры выхода и ожидания завершения. Эти примитивы – единственное, что необходимо для управления процессами. Мы будем использовать их в последующих разделах для создания дополнительных функций, таких как popen и system.
#include <unistd.h>
int execl (const char *pathname, const char *arg0, ... /* (char *) 0 */);
int execv (const char *pathname, char *const argv[]);
int execle (const char *pathname, const char *arg0, ... /* (char *) 0, char *const envp[] */);
int execve (const char *pathname, char *const argv, char *const envp[]);
int execlp (const char *filename, const char *arg0, ... /* (char *) 0 */);
int execvp (const char *filename, char *const argv[]);
/* все шесть функций возвращают -1 в случае ошибки, не возвращают управление в случае успеха */
Одно из отличий между этими функциями заключается в том, что первые четыре принимают в качестве аргумента полный путь к файлу, а последние две – только имя файла. Аргумент filename интерпретируется следующим образом:
если аргумент filename содержит символ “/”, то он интерпретируется как полный путь к файлу;
в противном случае производится поиск исполняемого файла в каталогах, перечисленных в переменной окружения PATH.
Переменная окружения PATH содержит список каталогов, разделенных двоеточиями; они называются префиксами пути. Например, строка окружения в формате name=value
PATH=/bin:/usr/bin:/usr/local/bin:.
определяет четыре каталога, в которых будет производиться поиск исполняемых файлов. Последним в списке является текущий каталог. По причинам, связанным с безопасностью системы, никогда не следует включать текущий каталог в переменную окружения PATH.
Если функция execlp или execvp находит исполняемый файл, используя один из префиксов пути, но этот файл не является двоичным исполняемым файлом, сгенерированным редактором связей, то функция предполагает, что найденный файл является сценарием командной оболочки и пытается вызвать /bin/sh с именем файла в качестве аргумента.
Следующее различие касается передачи списка аргументов (l означает список (list), v означает вектор, или массив (vector)). Функции execl, execle и execlp требуют, чтобы каждый из аргументов командной строки новой программы был оформлен в виде отдельного аргумента. Для трех других функций (execv, execve и execvp) необходимо сформировать массив указателей на аргументы командной строки и передать адрес этого массива в качестве аргумента.
И последнее различие – передача списка переменных окружения новой программе. Две функции, execle и execve, позволяют передавать массив указателей на строки окружения. Остальные четыре функции для передачи копии среды окружения новой программе используют переменную environ вызывающего процесса. Обычно среда окружения родительского процесса передается дочерним процессам без изменения, но в некоторых случаях возникает необходимость создать особую среду окружения для дочернего процесса. Пример такого случая – программа login, которая инициализирует новую командную оболочку. Обычно программа login создает определенную среду окружения с небольшим количеством переменных и позволяет нам через файл начального запуска командной оболочки добавить свои переменные при входе в систему.
Аргументы всех шести функций семейства exec достаточно сложно запомнить. Но буквы в именах функций немного помогают в этом. Буква p означает, что функция принимает аргумент filename и использует переменную окружения PATH, чтобы найти исполняемый файл. Буква l означает, что функция принимает список аргументов, а буква v означает, что она принимает массив envp[] вместо использования текущей среды окружения. В табл. 8.6 показаны различия между этими шестью функциями.
Таблица 8.6
Различия между шестью функциями семейства exec
Функция |
pathname |
filename |
Список аргументов |
argv[] |
environ |
envp[] |
execl |
* |
|
* |
|
* |
|
execlp |
|
* |
* |
|
* |
|
execle |
* |
|
* |
|
|
* |
execv |
* |
|
|
* |
* |
|
execvp |
|
* |
|
* |
* |
|
execve |
* |
|
|
* |
|
* |
Буква в имени |
|
p |
l |
v |
|
e |
Каждая система накладывает свои ограничения на размер списка аргументов командной строки и списка переменных окружения. Этот предел задается с помощью константы ARG_MAX (раздел 2.3). Для систем, соответствующих стандарту Posix.1, его значение должно быть не менее 4096 байт.
Уже было отмечено, что идентификатор процесса не изменяется после вызова функции exec. Кроме того, программа наследует от вызывающего процесса ряд дополнительных характеристик:
идентификатор процесса и идентификатор родительского процесса;
реальный идентификатор пользователя (индивидуального и группового);
идентификаторы дополнительных групп;
идентификатор группы процессов;
идентификатор сессии;
управляющий терминал;
время, оставшееся до срабатывания таймера;
текущие рабочий и корневой каталоги;
маску режима создания файлов;
блокировки файлов;
маску сигналов процесса;
сигналы, ожидающие обработки;
ограничения на использование ресурсов;
значения tms_utime, tms_stime, tms_cutime и tms_cstime.
Судьба открытых файлов зависит от значения флага close-on-exec для каждого дескриптора. Вспомните рис. 3.1 и упоминание флага FD_CLOEXEC в разделе 3.13. Там мы говорили, что каждый открытый процессом дескриптор имеет флаг close-on-exec. Если этот флаг установлен, то дескриптор закрывается функцией exec. В противном случае дескриптор остается открытым. По умолчанию после вызова функции exec дескриптор остается открытым, если флаг close-on-exec не был специально установлен с помощью функции fcntl.
Стандарт Posix.1 требует, чтобы открытые каталоги (вспомните функцию opendir из раздела 4.21) обязательно закрывались при вызове функции exec. Обычно это обеспечивает функция opendir, которая вызывает fcntl, чтобы установить флаг close-on-exec для дескриптора, соответствующего открытому каталогу.
Обратите внимание, что реальные идентификаторы пользователя (индивидуального и группового) не изменяются при вызове функции exec, но эффективные идентификаторы могут быть изменены в зависимости от состояния битов set-user-ID и set-group-ID файла, содержащего запускаемую программу. Если бит set-user-ID установлен, то в качестве эффективного идентификатора пользователя процесса принимается идентификатор владельца программного файла. В противном случае эффективный идентификатор пользователя не изменяется, то есть он не устанавливается равным реальному идентификатору пользователя. Эффективный идентификатор группы устанавливается аналогичным образом.
В большинстве реализаций Unix только одна из этих шести функций, а именно execve, является системным вызовом. Остальные пять – обычные библиотечные функции, которые, в конечном счете, обращаются к этому системному вызову. На рис. 8.2 изображена схема взаимоотношений между шестью функциями exec.
рис. 8.2
В соответствии с этой схемой, библиотечные функции execlp и execvp обрабатывают переменную окружения PATH в поисках первого каталога, который содержит исполняемый файл с именем filename.
Пример. Программа, представленная листингом 8.8, демонстрирует работу с функциями exec.
Листинг 8.8. Пример использования функций exec
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
char *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };
int main(void)
{
pid_t pid;
if ((pid = fork()) < 0)
{
fprintf(stderr, "Ошибка вызова функции fork: %s\n", strerror(errno));
exit(1);
}
else if (pid == 0) /* задать полный путь к файлу и среду окружения */
if (execle("/home/sergeant/book/chap8/echoall", "echoall", "myarg1",
"MY ARG2", (char *)0, env_init) < 0)
{
fprintf(stderr, "Ошибка вызова функции execle: %s\n", strerror(errno));
exit(1);
}
if (waitpid(pid, NULL, 0) < 0)
{
fprintf(stderr, "Ошибка вызова функции waitpid: %s\n", strerror(errno));
exit(1);
}
if ((pid = fork()) < 0)
{
fprintf(stderr, "Ошибка вызова функции fork: %s\n", strerror(errno));
exit(1);
}
else
if (pid == 0) /* задать имя файла, наследовать среду окружения */
if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)
{
fprintf(stderr, "Ошибка вызова функции execlp: %s\n", strerror(errno));
exit(1);
}
exit(0);
}
Сначала мы вызываем функцию execle, которой необходимо передать полный путь к файлу и среду окружения. Далее вызывается функция execlp, которой передается имя файла, а среда окружения наследуется новой программой от вызывающего процесса. В данном примере обращение к функции execlp не завершится ошибкой только в том случае, если каталог /home/sergeant/book/chap8 входит в состав переменной окружения PATH. Кроме того, обратите внимание, что в качестве первого аргумента (argv[0]) командной строки новой программы мы передаем только имя файла. Некоторые командные оболочки передают в этом аргументе полный путь к файлу. Но это всего лишь соглашение, на самом деле в argv[0] можно записать любую строку. Команда login именно так и поступает, когда запускает командную оболочку. Перед ее запуском login добавляет в начало строки argv[0] дефис, тем самым сообщая командной оболочке, что она вызывается как оболочка при входе пользователя в систему. В этом случае командная оболочка производит запуск команд начальной настройки, в то время как при обычном вызове она этого не делает.
Программа echoall, дважды запускаемая программой из листинга 8.8, приведена в листинге 8.9. Это простенькая программа, которая выводит все аргументы командной строки и список переменных окружения.
Листинг 8.9. Вывод всех аргументов командной строки и переменных окружения
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
char **ptr;
extern char **environ;
for (i = 0; i < argc; i++) /* вывести все аргументы командной строки */
printf("argv[%d]: %s\n", i, argv[i]);
for (ptr = environ; *ptr != 0; ptr++) /* и все переменные окружения */
printf("%s\n", *ptr);
exit(0);
}