Разработка ядра Linux - Роберт Лав
Шрифт:
Интервал:
Закладка:
Следует подчеркнуть, что сама по себе программа процессом не является; процесс — это выполняющаяся программа плюс набор соответствующих ресурсов. Конечно, может существовать два процесса, которые исполняют одну и ту же программу. В действительности может даже существовать два или больше процессов, которые совместно используют одни и те же ресурсы, такие как открытые файлы, или адресное пространство. Процесс начинает свое существование с момента создания, что впрочем не удивительно. В операционной системе Linux такое создание выполняется с помощью системного вызова fork() (буквально, ветвление или вилка), который создает новый процесс путем полного копирования уже существующего. Процесс, который вызвал системную функцию fork(), называется порождающим (родительским, parent), новый процесс именуют порожденным (дочерний, child). Родительский процесс после этого продолжает выполнение, а порожденный процесс начинает выполняться с места возврата из системного вызова. Часто после разветвления в одном из процессов желательно выполнить какую-нибудь другую программу. Семейство функций exec*() позволяет создать новое адресное пространство и загрузить в него новую программу. В современных ядрах Linux функция fork() реализована через системный вызов clone(), который будет рассмотрен в следующем разделе.
Выход из программы осуществляется с помощью системного вызова exit(). Эта функция завершает процесс и освобождает все занятые им ресурсы. Родительский процесс может запросить о состоянии порожденных им процессов с помощью системного вызова wait4()[9], который заставляет один процесс ожидать завершения другого. Когда процесс завершается, он переходит в специальное состояние зомби (zombie), которое используется для представления завершенного процесса до того момента, пока порождающий его процесс не вызовет системную функцию wait() или waitpid().
Иное название для процесса — задание или задача (task). О процессах в ядре операционной системы Linux говорят как о задачах. В этой книге оба понятия взаимозаменяемы, хотя по возможности для представления работающей программы в ядре будет использоваться термин задача, а для представления в режиме пользователя — термин процесс.
Дескриптор процесса и структура task structure
Ядро хранит информацию о всех процессах в двухсвязном списке, который называется task list[10] (список задач). Каждый элемент этого списка является дескриптором процесса и имеет тип структуры struct task_struct, которая описана в файле include/linux/sched.h. Дескриптор процесса содержит всю информацию об определенном процессе.
Структура task_struct — достаточно большая структура данных размером порядка 1,7 Кбайт на 32-разрядной машине. Однако этот размер не такой уж большой, учитывая, что в данной структуре содержится вся информация о процессе, которая необходима ядру. Дескриптор процесса содержит данные, которые описывают выполняющуюся программу, — открытые файлы, адресное пространство процесса, ожидающие на обработку сигналы, состояние процесса и многое другое (рис. 3.1).
Рис. 3.1. Дескриптор процесса и список задач
Выделение дескриптора процесса
Память для структуры task_struct выделяется с помощью подсистемы выделения памяти, которая называется слябовый распределитель (slab allocator), для возможности повторного использования объектов и раскрашивания кэша (cache coloring) (см. главу 11, "Управление памятью"). В ядрах до серии 2.6 структура task_struct хранилась в конце стека ядра каждого процесса. Это позволяет для аппаратных платформ, у которых достаточно мало регистров процессора (как, например, платформа x86), вычислять местоположение дескриптора процесса, только зная значение регистра указателя стека (stack pointer), без использования дополнительных регистров для хранения самого адреса этого местоположения. Так как теперь дескриптор процесса создается с помощью слябового распределителя, была введена новая структура thread_info, которая хранится в области дна стека (для платформ, у которых стек растет в сторону уменьшения значения адреса памяти) или в области вершины стека (для платформ, у которых стек растет в сторону увеличения значения адреса памяти)[11] (рис. 3.2.).
Рис 3.2. Дескриптор процесса и стек ядра
Структура struct thread_info для платформы x86 определена в файле <asm/thread_info.h> в следующем виде.
struct thread_info {
struct task_struct *task;
struct exec_domain *exec_domain;
unsigned long flags;
unsigned long status;
__u32 cpu;
__s32 preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
unsigned long previous_esp;
__u8 supervisor_stack[0];
};
Для каждой задачи ее структура thread_info хранится в конце стека ядра этой задачи. Элемент структуры thread_info с именем task является указателем на структуру task_struct этой задачи.
Хранение дескриптора процесса
Система идентифицирует процессы с помощью уникального значения, которое называется идентификатором процесса (process identification, PID). Идентификатор PID — это целое число, представленное с помощью скрытого типа pid_t[12] , который обычно соответствует знаковому целому— int.
Однако, для обратной совместимости со старыми версиями ОС Unix и Linux максимальное значение этого параметра по умолчанию составляет всего лишь 32768 (что соответствует типу данных short int). Ядро хранит значение данного параметра в поле pid дескриптора процесса.
Это максимальное значение является важным, потому что оно определяет максимальное количество процессов, которые одновременно могут существовать в системе. Хотя значения 32768 и достаточно для офисного компьютера, для больших серверов может потребоваться значительно больше процессов. Чем меньше это значение, тем скорее нумерация процессов будет начинаться сначала, что приводит к нарушению полезного свойства: больший номер процесса соответствует процессу, который запустился позже. Если есть желание нарушить в системе обратную совместимость со старыми приложениями, то администратор может увеличить это максимальное значение во время работы системы с помощью записи его в файл /proc/sys/kernel/pid_max.
Обычно в ядре на задачи ссылаются непосредственно с помощью указателя на их структуры task_struct. И действительно, большая часть кода ядра, работающего с процессами, работает прямо со структурами task_struct. Следовательно, очень полезной возможностью было бы быстро находить дескриптор процесса, который выполняется в данный момент, что и делается с помощью макроса current. Этот макрос должен быть отдельно реализован для всех поддерживаемых аппаратных платформ. Для одних платформ указатель на структуру task_struct процесса, выполняющегося в данный момент, хранится в регистре процессора, что обеспечивает более эффективный доступ. Для других платформ, у которых доступно меньше регистров процессора, чтобы зря не тратить регистры, используется тот факт, что структура thread_info хранится в стеке ядра. При этом вычисляется положение структуры thread_info, а вслед за этим и адрес структуры task_struct процесса.
Для платформы x86 значение параметра current вычисляется путем маскирования 13 младших бит указателя стека для получения адреса структуры thread_info. Это может быть сделано с помощью функции current_thread_info(). Соответствующий код на языке ассемблера показан ниже.
movl $-8192, %eax
andl %esp, %eax
Окончательно значение параметра current получается путем разыменования значения поля task полученной структуры thread_info:
current_thread_info()->task;
Для контраста можно сравнить такой подход с используемым на платформе PowerPC (современный процессор на основе RISC-архитектуры фирмы IBM), для которого значение переменной current хранится в регистре процессора r2. На платформе PPC такой подход можно использовать, так как, в отличие от платформы x86, здесь регистры процессора доступны в изобилии. Так как доступ к дескриптору процесса — это очень частая и важная операция, разработчики ядра для платформы PPC сочли правильным пожертвовать одним регистром для этой цели.
Состояние процесса
Поле state дескриптора процесса описывает текущее состояние процесса (рис. 3.3). Каждый процесс в системе гарантированно находится в одном из пяти различных состояний.