Роберт Лав - Разработка ядра Linux
Состояние процесса
Поле state дескриптора процесса описывает текущее состояние процесса (рис. 3.3). Каждый процесс в системе гарантированно находится в одном из пяти различных состояний.
Рис. 3.3. Диаграмма состояний процесса
Эти состояния представляются значением одного из пяти возможных флагов, описанных ниже.
• TASK_RUNNING — процесс готов к выполнению (runnable). Иными словами, либо процесс выполняется в данный момент, либо находится в одной из очередей процессов, ожидающих на выполнение (эти очереди, runqueue, обсуждаются в главе 4. "Планирование выполнения процессов").
• TASK_INTERRUPTIBLE — процесс приостановлен (находится в состоянии ожидания, sleeping), т.е. заблокирован в ожидании выполнения некоторого условия. Когда это условие выполнится, ядро переведет процесс в состояние TASK_RUNNING. Процесс также возобновляет выполнение (wake up) преждевременно при получении им сигнала.
• TASK_UNINTERRUPTIBLE — аналогично TASK_INTERRUPTIBLE, за исключением того, что процесс не возобновляет выполнение при получении сигнала. Используется в случае, когда процесс должен ожидать беспрерывно или когда ожидается, что некоторое событие может возникать достаточно часто. Так как задача в этом состоянии не отвечает на сигналы, TASK_UNINTERRUPTIBLE используется менее часто, чем TASK_INTERRUPTIBLE[13].
• 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]. При выходе из режима ядра процесс продолжает выполнение в пространстве пользователя, если в это время не появляется готовый к выполнению более приоритетный процесс. В таком случае активизируется планировщик, который выбирает для выполнения более приоритетный процесс.
Системные вызовы и обработчики исключительных ситуаций являются строго определенными интерфейсами ядра. Процесс может начать выполнение в пространстве ядра только посредством одного из этих интерфейсов — любые обращения к ядру возможны только через эти интерфейсы.
Дерево семейства процессов
В операционной системе Linux существует четкая иерархия процессов. Все процессы являются потомками процесса init, значение идентификатора PID для которого равно 1. Ядро запускает процесс init на последнем шаге процедуры загрузки системы. Процесс init, в свою очередь, читает системные файлы сценариев начальной загрузки (initscripts) и выполняет другие программы, что в конце концов завершает процедуру загрузки системы.
Каждый процесс в системе имеет всего один порождающий процесс. Кроме того, каждый процесс может иметь один или более порожденных процессов. Процессы, которые порождены одним и тем же родительским процессом, называются родственными (siblings). Информация о взаимосвязи между процессами хранится в дескрипторе процесса. Каждая структура task_struct содержит указатель на структуру task_struct родительского процесса, который называется parent, эта структура также имеет список порожденных процессов, который называется children. Следовательно, если известен текущий процесс (current), то для него можно определить дескриптор родительского процесса с помощью выражения:
struct task_struct *task = current->parent;
Аналогично можно выполнить цикл по процессам, порожденным от текущего процесса, с помощью кода:
struct task_struct *task;
struct list_head *list;
list_for_each(list, ¤t->children) {
task = list_entry(list, struct task_struct, sibling);
/* переменная task теперь указывает на один из процессов,
порожденных текущим процессом */
}
Дескриптор процесса init — это статически выделенная структура данных с именем init_task. Хороший пример использования связей между всеми процессами — это приведенный ниже код, который всегда выполняется успешно.
struct task_struct *task;
for (task = current; task != $init_task; task = task->parent)
;
/* переменная task теперь указывает на процесс init */
Конечно, проходя по иерархии процессов, можно перейти от одного процесса системы к другому. Иногда, однако, желательно выполнить цикл по всем процессам системы. Такая задача решается очень просто, так как список задач — это двухсвязный список. Для того чтобы получить указатель на следующее задание из этого списка, имея действительный указатель на дескриптор какого-либо процесса, можно использовать показанный ниже код:
list_entry(task->tasks.next, struct task_struct, tasks);
Получение указателя на предыдущее задание работает аналогично.
list_entry(task->tasks.prev, struct task_struct, tasks);
Дна указанных выше выражения доступны также в виде макросов next_task(task) (получить следующую задачу), prev_task(task) (получить предыдущую задачу). Наконец, макрос for_each_process(task) позволяет выполнить цикл по всему списку задач. На каждом шаге цикла переменная task указывает на следующую задачу из списка:
struct task_struct *task;
for_each_process(task) {
/* просто печатается имя команды и идентификатор PID
для каждой задачи */
printk("%s[%d]n", task->comm, task->pid);
}
Следует заметить, что организация цикла по всем задачам системы, в которой выполняется много процессов, может быть достаточно дорогостоящей операцией. Для применения такого кода должны быть веские причины (и отсутствовать другие альтернативы).
Создание нового процесса
В операционной системе Unix создание процессов происходит уникальным образом. В большинстве операционных систем для создания процессов используется метод порождения процессов (spawn). При этом создается новый процесс в новом адресном пространстве, в которое считывается исполняемый файл, и после этого начинается исполнение процесса. В ОС Unix используется другой подход, а именно разбиение указанных выше операций на две функции: fork() и exec()[15].
В начале с помощью функции fork() создается порожденный процесс, который является копией текущего задания. Порожденный процесс отличается от родительского только значением идентификатора PID (который является уникальным в системе), значением параметра PPID (идентификатор PID родительского процесса, который устанавливается в значение PID порождающего процесса), некоторыми ресурсами, такими как ожидающие на обработку сигналы (которые не наследуются), а также статистикой использования ресурсов. Вторая функция — exec() — загружает исполняемый файл в адресное пространство процесса и начинает исполнять его. Комбинация функций fork() и exec() аналогична той одной функции создания процесса, которую предоставляет большинство операционных систем.