Процесс может проверять использование ресурсов им самим, общее использование ресурсов его дочерними процессами либо сумму того и другого.
Системный вызов getrusage() возвращает структуру struct rusage (определенную в <sys/resource.h>), содержащую информацию о текущем использовании ресурсов.
int getrusage(int who, struct rusage * usage);
Первый параметр, who, сообщает, какой из трех счетчиков ресурсов должен быть возвращен. RUSAGE_SELF возвращает использование ресурсов текущим процессом, RUSAGE_CHILDREN — его дочерними процессами, a RUSAGE_BOTH — общее использование ресурсов текущим процессом и всеми его дочерними процессами. Второй параметр getrusage() — это указатель на struct rusage, куда помещается информация об использовании ресурсов. Хотя struct rusage и содержит относительно немного членов (список унаследован из BSD), большинство этих членов пока не используются Linux). Ниже представлено полное определение этой структуры. В табл. 10.1 описаны члены, используемые в настоящее время Linux.
#include <sys/resource.h>
struct rusage {
struct timeval ru_utime;
struct timeval ru_stime;
long intru_maxrss;
long intru_ixrss;
long intru_idrss;
long intru_isrss;
long intru_minflt;
long intru_majfit;
long intru_nswap;
long intru_inblock;
long intru_oublock;
long intru_msgsnd;
long intru_msgrcv;
long intru_nsignals;
long intru_nvcsw;
long intru_nivcsw;
};
10.3.3. Применение ограничений использования ресурсов
Чтобы помочь в предотвращении неконтролируемого снижения производительности процессами, Unix отслеживает многие ресурсы, которые может использовать процесс, и позволяет системному администратору и самим пользователям накладывать ограничения на расход ресурсов процессами.
Предусмотрены два класса доступных ограничений: жесткие и мягкие ограничения. Жесткие обычно установлены при запуске системы в RLIM_INFINITY, что означает отсутствие каких-либо ограничений. Единственное исключение из этого — RLIMIT_CORE (максимальный размер дампа памяти), который Linux инициирует нулем, чтобы предотвратить неожиданный сброс дампов ядра. Многие дистрибутивы сбрасывают этот лимит при запуске, однако, большинство технических пользователей ожидают появления дампов памяти при некоторых условиях (информацию о дампах памяти можно найти далее в главе). Мягкие ограничения — это те ограничения, которые установлены в ядре в данный момент. Любой процесс может наложить мягкое ограничение на использование ресурса на определенном уровне — равном или более низком, чем установленное жесткое ограничение.
Таблица 10.2. Ограничения ресурсов
Значение Лимит RLIMIT_AS Максимальный объем памяти, доступный процессу. Включает память для стека, глобальных переменных и динамически выделенную память. RLIMIT_CORE Максимальный размер дампа памяти, генерируемого ядром (если файл дампа получается слишком большим, он не создается). RLIMIT_CPU Общее используемое время процессора (в секундах). Более подробно об этом ограничении рассказывается при описании SIGXCPU в главе 12. RLIMIT_DATA Максимальный объем памяти данных (в байтах). Это не включает динамически выделенную память. RLIMIT_FSIZE Максимальный размер открытого файла (проверяется при записи). Более подробно об этом ограничении рассказывается при описании SIGXFSZ в главе 12. RLIMIT_MEMLOCK Максимальный объем памяти, которая может быть блокирована с помощью mlock(). Функция mlock() рассматривается в главе 13. RLIMIT_NOFILE Максимальное количество открытых файлов. RLIMIT_NPROC Максимальное количество дочерних процессов, которые может породить данный процесс. Это ограничивает только количество дочерних процессов, которые могут существовать одновременно. Это не ограничивает количества наследников дочерних процессов — каждый из них может иметь до RLIMIT_NPROC потомков. RLIMIT_RSS Максимальный объем ОЗУ, использованный в любой момент (всякое превышение этого объема используемой памяти вызывает страничную подкачку). Это также известно под названием размера резидентной части (resident set size). RLIMIT_STACK Максимальный размер памяти стека (в байтах), включая все локальные переменные.
Различные ограничения, которые могут быть установлены, перечислены в табл. 10.2 и определены в <sys/resource.h>. Системные вызовы getrlimit() и setrlimit() устанавливают и получают ограничения для отдельного ресурса.
int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);
Обе эти функции используют структуру struct rlimit, определенную следующим образом:
struct rlimit {
long int rlim_cur; /* мягкое ограничение */
long int rlim_max; /* жесткое ограничение */
};
Второй член структуры — rlim_max, указывает жесткое ограничение лимита, переданного в параметре resource, a rlim_cur — мягкое ограничение. Это те же наборы лимитов, которыми манипулируют команды ulimit и limit, одна из которых встроена в большинство командных оболочек.
10.4. Примитивы процессов
Несмотря на относительно длинную дискуссию, необходимую для описания процесса, создание и уничтожение процессов в Linux достаточно просто.
10.4.1. Создание дочерних процессов
В Linux предусмотрены два системных вызова, которые создают новые процессы: fork() и clone(). Как упоминалось ранее, clone() используется для создания потоков, и этот вызов будет кратко описан далее. А сейчас мы сосредоточимся на fork() — наиболее популярном методе создания процессов.
#include <unistd.h>
pid_t fork(void);
Этот системный вызов имеет уникальное свойство возвращать управление не один раз, а дважды: один раз в родительском процессе и другой — в дочернем. Обратите внимание, что мы не говорим "первый — в родительском" — написание кода, который делает какие-то предположения относительно предопределенного порядка — очень плохая идея.
Каждый из двух возвратов системного вызова fork() имеет разные значения. В родительский процесс этот системный вызов возвращает pid вновь созданного дочернего процесса, а в дочернем он возвращает 0.
Разница возвращаемых значений — это единственное отличие, видимое процессам. Оба имеют одинаковые образы памяти, права доступа, открытые файлы и обработчики сигналов[19]. Рассмотрим простой пример программы, порождающей дочерний процесс.
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
pid_t child;
if (!(child = fork())) {
printf("в дочернемn");
exit (0);
}
printf("в родительском - дочерний: %dn", child);
return 0;
}
10.4.2. Наблюдение за уничтожением дочерних процессов
Сбор состояний возврата дочернего процесса называется ожиданием процесса. Это можно делать четырьмя способами, хотя только один из вызовов предоставляется ядром. Остальные три метода реализованы в стандартной библиотеке С. Поскольку системный вызов ядра принимает четыре аргумента, он называется wait4().
pid_t wait4(pid_t pid, int *status, int options, struct rusage *rusage);
Первый аргумент, pid, представляет собой процесс, код возврата которого должен быть возвращен. Он может принимать ряд специальных значений.
pid < -1 Ожидать завершения любого дочернего процесса, чей pgid равен абсолютному значению pid.
pid = -1 Ожидать прерывания любого дочернего процесса.
pid = 0 Ожидать завершения дочернего из той же группы процессов, что и текущий[20].
pid > 0 Ожидать выхода процесса pid.
Второй параметр — это указатель на целое, которое устанавливается в значение, равное соду возврата того процесса, который заставляет wait4() вернуть управление (мы будем зазывать его "проверяемым" процессом). Формат возвращенного состояния довольно закрученный, и для того, чтобы сделать его осмысленным, существует набор макросов.