
- •Часть 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.4. Совместное использование файлов
Когда при запуске программы из листинга 8.1 мы перенаправляем стандартный поток вывода родительского процесса в файл, стандартный поток вывода дочернего процесса также оказывается перенаправленным. Действительно, одна из особенностей функции fork заключается в том, что она передает дочернему процессу дубликаты всех дескрипторов, открытых в родительском процессе. Мы говорим “дубликаты”, потому что они представляют собой копии дескрипторов, как если бы для каждого из них была вызвана функция dup. Родительский и дочерний процессы совместно используют одни и те же записи в таблице файлов для каждого из открытых дескрипторов (вспомните рис. 3.3).
Представим себе процесс, который открыл три файла: стандартного ввода, стандартного вывода и стандартного вывода сообщений об ошибках. По возвращении из функции fork мы получим распределение дескрипторов, показанное на рис. 8.1.
рис. 8.1
Важно заметить, что и родительский, и дочерний процессы совместно используют текущую позицию файла. Рассмотрим процесс, который запустил дочерний процесс и ожидает его завершения. Допустим, что оба процесса в ходе своей работы производят запись в стандартный поток вывода. Если стандартный поток вывода родительского процесса будет перенаправлен в файл (например, командной оболочкой), то текущая позиция файла, установленная родительским процессом, неизбежно будет изменена дочерним процессом, когда он выполнит запись в стандартный поток вывода. В этом случае дочерний процесс может записывать данные в стандартный поток вывода, пока родительский процесс ожидает его завершения. По окончании работы дочернего процесса родительский процесс сможет продолжить запись в стандартный поток вывода, зная, что его данные будут записаны после тех, что записал дочерний процесс. Если бы текущая позиция файла различалась у родительского и дочернего процессов, то подобного эффекта достичь было бы гораздо сложнее, и это потребовало бы дополнительных усилий со стороны родительского процесса.
Если и родительский, и дочерний процессы пишут в один и тот же дескриптор без какой-либо синхронизации, например, когда родительский процесс не ожидает завершения дочернего процесса, то данные их вывода будут перемешаны (если этот дескриптор был открыт до вызова функции fork). Хотя это и возможно согласно рис. 8.1, тем не менее, такой режим работы не является нормальным.
Существуют два стандартных способа обслуживания дескрипторов после вызова функции fork:
Родительский процесс ожидает, когда потомок завершит свою работу. В этом случае родительскому процессу ничего не нужно делать со своими дескрипторами. Когда потомок завершится, текущая позиция файла любого из разделяемых дескрипторов, которые использовались им для чтения или записи, изменится надлежащим образом.
И родительский, и дочерний процессы продолжают работу независимо друг от друга. В этом случае после вызова функции fork родительский процесс закрывает дескрипторы, которые ему больше не потребуются, дочерний процесс делает то же самое. Таким образом, они более не будут совместно использовать одни и те же дескрипторы. Этот сценарий часто используется в сетевых серверах.
Помимо дескрипторов открытых файлов есть много других характеристик родительского процесса, которые наследуются дочерним:
реальный идентификатор пользователя, реальный идентификатор группы, эффективный идентификатор пользователя, эффективный идентификатор группы;
идентификаторы дополнительных групп;
идентификатор группы процессов;
идентификатор сессии;
управляющий терминал;
флаги set-user-ID и set-group-ID;
текущий рабочий каталог;
корневой каталог;
маска режима создания файлов;
маска сигналов и их диспозиция;
флаги close-on-exec для открытых дескрипторов;
среда окружения;
присоединенные сегменты разделяемой памяти;
отображения в память;
ограничения на ресурсы.
Существуют следующие отличия между родительским и дочерним процессами:
функция fork возвращает различные значения;
различные идентификаторы процессов;
различные идентификаторы родительских процессов: идентификатор родительского процесса в потомке соответствует идентификатору процесса в родительском процессе, идентификатор родительского процесса в родительском процессе остается без изменений;
значения tms_utime, tms_stime, tms_cutime, tms_cstime в дочернем процессе устанавливаются равными 0;
блокировки файлов, установленные в родительском процессе, не наследуются;
таймеры, ожидающие срабатывания, в дочернем процессе сбрасываются;
набор сигналов, ожидающих обработки, в дочернем процессе очищается.
Многие из этих характеристик еще не обсуждались; мы поговорим о них в следующих главах.
Ошибка вызова функции fork происходит обычно в двух случаях:
Когда в системе слишком много работающих процессов, что обычно свидетельствует о неполадках.
Когда общее количество процессов превысило максимальное значение для заданного реального идентификатора пользователя; при этом максимальное количество одновременно работающих процессов на один реальный идентификатор пользователя определяется константой CHILD_MAX (раздел 2.3).
Функция fork используется в двух случаях:
Когда процесс хочет продублировать себя, чтобы родительский и дочерний процессы могли выполнять различные участки программы одновременно. Это обычно используется в сетевых серверах. Родительский процесс ожидает запроса на обслуживание от клиента, при его получении он вызывает fork и передает обслуживание запроса дочернему процессу, после чего возвращается к ожиданию следующего запроса.
Когда процесс хочет запустить другую программу. Это обычно используется в командных оболочках. В этом случае дочерний процесс вызывает функцию exec (которую мы рассмотрим в разделе 8.10), как только функция fork вернет управление.
Некоторые операционные системы объединяют вызов fork и следующий за ним вызов exec в одну операцию, которая называется spawn. Unix разделяет эти две операции по той простой причине, что достаточно часто вызов функции fork не сопровождается вызовом функции exec. Кроме того, такое разделение позволяет дочернему процессу между вызовами fork и exec изменить некоторые характеристики процесса, например, перенаправить ввод-вывод, идентификатор пользователя, диспозицию сигналов и т. д. Многочисленные примеры, иллюстрирующие это, мы увидим в главе 15.