Обычно в ядре на задачи ссылаются непосредственно с помощью указателя на их структуры
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). Каждый процесс в системе гарантированно находится в одном из пяти различных состояний.
Рис. 3.3. Диаграмма состояний процесса
Эти состояния представляются значением одного из пяти возможных флагов, описанных ниже.
•
TASK_RUNNING
— процесс готов к выполнению (runnable). Иными словами, либо процесс выполняется в данный момент, либо находится в одной из очередей процессов, ожидающих на выполнение (эти очереди,
runqueue
, обсуждаются в главе 4. "Планирование выполнения процессов").
•
TASK_INTERRUPTIBLE
— процесс приостановлен (находится в состоянии ожидания, sleeping), т.е. заблокирован в ожидании выполнения некоторого условия. Когда это условие выполнится, ядро переведет процесс в состояние
TASK_RUNNING
. Процесс также возобновляет выполнение (wake up) преждевременно при получении им сигнала.
•
TASK_UNINTERRUPTIBLE
— аналогично
TASK_INTERRUPTIBLE
, за исключением того, что процесс не возобновляет выполнение при получении сигнала. Используется в случае, когда процесс должен ожидать беспрерывно или когда ожидается, что некоторое событие может возникать достаточно часто. Так как задача в этом состоянии не отвечает на сигналы,
Именно из-за этого появляются наводящие ужас "неубиваемые" процессы, для которых команда
ps(1)
показывает значение состояния, равное
D
. Так как процесс не отвечает на сигналы, ему нельзя послать сигнал
SIGKILL
. Более того, завершать такой процесс было бы неразумно, так как этот процесс, скорее всего, выполняет какую-либо важную операцию и может удерживать семафор.
•
TASK_ZOMBIE
—
процесс завершен, однако порождающий его процесс еще не вызвал системный вызов
wait4
. Дескриптор такого процесса должен оставаться доступным на случай, если родительскому процессу потребуется доступ к этому дескриптору. Когда родительский процесс вызывает функцию
wait4
, то такой дескриптор освобождается.
•
TASK_STOPPED
— выполнение процесса остановлено. Задача не выполняется и не имеет право выполняться. Такое может случиться, если задача получает какой-либо из сигналов
SIGSTOP
,
SIGTSTP
,
SIGTTIN
или
SIGTTOU
, а также если сигнал приходит в тот момент, когда процесс находится в состоянии отладки.
Манипулирование текущим состоянием процесса
Исполняемому коду ядра часто необходимо изменять состояние процесса. Наиболее предпочтительно для этого использовать функцию
set_task state(task, state);
/* установить задание 'task' в состояние 'state' */
которая устанавливает указанное состояние для указанной задачи. Если применимо, то эта функция также пытается применить барьер памяти (memory barrier), чтобы гарантировать доступность установленного состояния для всех процессоров (необходимо только для SMP-систем). В других случаях это эквивалентно выражению:
task->state = state;
Вызов
set_current_state(state)
является синонимом к вызову
set_task_state(current, state)
.
Контекст процесса
Одна из наиболее важных частей процесса— это исполняемый программный код. Этот код считывается из выполняемого файла (executable) и выполняется в адресном пространстве процесса. Обычно выполнение программы осуществляется в пространстве пользователя. Когда программа выполняет системный вызов (см. главу 5, "Системные вызовы") или возникает исключительная ситуация, то программа входит в пространство ядра.
С этого момента говорят, что ядро "выполняется от имени процесса" и делает это в контексте процесса. В контексте процесса макрос
current
является действительным [14] . При выходе из режима ядра процесс продолжает выполнение в пространстве пользователя, если в это время не появляется готовый к выполнению более приоритетный процесс. В таком случае активизируется планировщик, который выбирает для выполнения более приоритетный процесс.
14
Отличным от контекста процесса является контекст прерывания, описанный в главе 6, "Прерывания и обработка прерываний". В контексте прерывания система работает не от имени процесса, а выполняет обработчик прерывания. С обработчиком прерывании не связан ни один процесс, поэтому и контекст процесса отсутствует.
Системные вызовы и обработчики исключительных ситуаций являются строго определенными интерфейсами ядра. Процесс может начать выполнение в пространстве ядра только посредством одного из этих интерфейсов — любые обращения к ядру возможны только через эти интерфейсы.
Дерево семейства процессов
В операционной системе Linux существует четкая иерархия процессов. Все процессы являются потомками процесса
init
, значение идентификатора
PID
для которого равно 1. Ядро запускает процесс
init
на последнем шаге процедуры загрузки системы. Процесс
init
, в свою очередь, читает системные файлы сценариев начальной загрузки (initscripts) и выполняет другие программы, что в конце концов завершает процедуру загрузки системы.