Главная » Ядро Linux » Хранение дескриптора процесса

0

Система идентифицирует процессы с помощью уникального значения, которое называетс я идентификатором процесса (process identification,  PID).  Идентификато р PID — это целое число, представленное с помощью скрытого типа pid_t5 , который обычно соответствует знаковому целому— int.

5 Скрыты й тип (opaqu e type) —  это ти п данных, физическо е представлени е которог о  неизвестно или не существенно.

Однако, для обратной совместимости со старыми версиями ОС Unix и Linux максимальное значение этого  параметра по умолчанию составляет всего  лишь  32768 (что соответствует типу данных  shor t   int) . Ядро хранит значение данного параметра в поле pi d дескриптора процесса.

Это  максимальное значение является важным, потому  что оно  определяет максимальное количество процессов, которые одновременно могут существовать в системе.  Хотя значения 32768 и достаточно для  офисного компьютера, для  больших серверов  может потребоваться значительно больше  процессов. Чем меньше это значение, тем скорее  нумерация процессов будет начинаться сначала, что приводит к нарушению полезного свойства: больший номер  процесса соответствует процессу, который запустился позже.   Если  есть желание нарушить в системе  обратную  совместимость со старыми приложениями, то администратор может увеличить это максимальное значение во время  работы  системы с помощью записи его в файл  /ргос/ sys/kernel/pid_max.

Обычно в ядре  на задачи  ссылаются непосредственно с помощью указателя на их структуры  task_struct .  И  действительно, большая часть  кода  ядра, работающего  с процессами, работает  прямо  со структурами task_struct . Следовательно, очень полезной возможностью было  бы быстро  находить  дескриптор процесса, который  выполняется в данный момент, что и делается  с помощью макроса current . Этот  макрос  должен  быть отдельно  реализован для  всех поддерживаемых аппаратных  платформ. Для  одних  платформ указатель  на  структуру  task_struc t  процесса, выполняющегося в данный момент, хранится в регистре  процессора, что обеспечивает более  эффективный доступ.  Для  других  платформ, у которых  доступно меньше регистров процессора, чтобы  зря  не  тратить  регистры, используется тот факт, что структура  thread_inf о хранится в стеке  ядра.  При  этом  вычисляется положение структуры  thread_info , а вслед за этим  и адрес  структуры  task_struc t процесса.

Для платформы х86 значение параметра current  вычисляется путем маскирования

13 младших  бит указателя стека для получения адреса структуры  thread_infо. Это может быть сделано  с помощью функции current_thread_inf o (). Соответствующий код на языке  ассемблера показан ниже.

movl $-8192, %eax andl %esp, %eax

Окончательно значение параметра curren t получается путем  разыменования значения поля  tas k полученной структуры  thread_info :

current_thread_info()->task;

Для  контраста можно  сравнить такой  подход  с используемым на  платформе PowerPC (современный процессор на основе  RISC-архитектуры фирмы IBM), для которого значение переменной curren t хранится в регистре  процессора r2.  На платформе РРС  такой  подход можно  использовать, так как, в отличие  от платформы х8б, здесь регистры процессора доступны в изобилии. Так как доступ к дескриптору процесса — это очень  частая  и важная операция, разработчики ядра для платформы РРС сочли  правильным пожертвовать одним  регистром для этой цели.

Состояние процесса

Поле   stat e  дескриптора  процесса  описывает текущее   состояние  процесса (рис.  3-3).  Каждый процесс  в системе  гарантированно находится  в одном  из пяти различных состояний.

Существующий процесс вызывает функцию  fork( )

и создает новый процесс

Планировщик отправляет задачу на выполнение: функция schedule () вызывает функцию concext_switch ()

TASK_ZOMBIE (процесс  завершен)

Задача

разветвляется

Задача завершается через do exit()

TASK_RUNNING (готов, но пока не выполняется)

TASK_RUNNING

(выполняется]

Задача вытесняется более приоритетнойзадачей

Событие произошло, задача возобновляет выполнение

и помещается обратно в очередь готовых к выполнению задач

TASK_INTЕRRUPTIBLE TASK_UNINTERRUPTTВLE

(задача ожидает)

Задача находится

в приостановленном состоянии в очереди ожиданий на определенное событие

Рис. 3.3. Диаграмма состояний процесса

Эти  состояния представляются значением одного  из пяти  возможных флагов, описанных ниже.

• TASK_RUNNING— процесс  готов к выполнению (runnable). Иными словами, либо  процесс  выполняется в данный момент, либо  находится  в  одной  из очередей  процессов, ожидающих на выполнение (эти  очереди,  runqueue, обсуждаются  в главе 4. "Планирование выполнения процессов").

• TASK_INTERRUPTIBLE  — процесс  приостановлен (находится в состоянии ожидания, sleeping), т.е. заблокирован в ожидании выполнения  некоторого

условия. Когда  это  условие   выполнится, ядро  переведет процесс  в состояние TASK    RUNNING.  Процесс также  возобновляет выполнение (wake up)  преждевременно при  получении им  сигнала.

• TASK_UNNTERRUPTIBLE аналогично TASK_INTERRUPTIBLE, за  исключением  того,  что  процесс  не возобновляет выполнение  при  получении сигнала. Используется в случае,  когда  процесс должен  ожидать  беспрерывно или  когда ожидается, что некоторое событие может  возникать достаточно часто.  Так  как задача  в этом  состоянии не  отвечает  на  сигналы,  TASK_UNINTERRUPTIBLE  используется менее  часто, чем  TASK_INTERRUPTIBLE6.

• TASK_ZOMBIE — процесс завершен, однако порождающий его процесс еще  не вызвал  системный вызов  wait 4 () . Дескриптор такого  процесса должен  оставаться  доступным на  случай,  если  родительскому  процессу потребуется  доступ к этому дескриптору. Когда  родительский процесс вызывает функцию  wait 4 () , то  такой  дескриптор освобождается.

• TASK_STOPPED — выполнение процесса остановлено. Задача  не выполняется и не  имеет  право   выполняться. Такое  может  случиться, если  задача  получает  какой-либо из сигналов SIGSTOP, SIGTSTP, SIGTTIN или  SIGTTOU, а также если сигнал приходит в тот  момент, когда  процесс находится в состоянии  отладки.

Манипулирование текущим состоянием процесса

Исполняемому  коду ядра часто необходимо  изменять  состояние  процесса. Наиболее предпочтительно для Этого использовать функцию

set_task state(task, state);

/* установить задание ‘task’ в состояние ‘state’ */

которая устанавливает указанное состояние для указанной задачи. Если применимо, то эта функция также пытается применить барьер памяти (memory barrier), чтобы гарантировать доступность установленного состояния для всех  процессоров (необходимо только для SMP-систем). В других случаях это  эквивалентно выражению:

task->state = state;

Вызов  se t  curren t  stat e   (state )   является синонимом  к вызову set_task _

state(current ,  state) .

Контекст процесса

Одна  из  наиболее важных   частей   процесса—  это  исполняемый  программный код.  Этот  код  считывается из  выполняемого  файла (executable)  и выполняется в адресном  пространстве процесса. Обычно выполнение  программы осуществляется в пространстве  пользователя.  Когда  программа выполняет системный вызов  (см.  главу 5, "Системные  вызовы") или  возникает  исключительная  ситуация,  то  программа входит  в  пространство ядра.

6 Именно из-за  этого  появляются наводящие ужас "неубиваемые" процессы , для которы х команда ps (1)  показывает значение состояния, равное D, Так как процесс не отвечает  на сигналы, ему нельзя послать  сигнал  SIGKILL. Более  того, завершать такой  процесс было  бы неразумно, так как  этот процесс, скорее  всего, выполняет какую-либо важную  операцию и может удерживать  семафор.

С этого  момента  говорят,  что ядро  "выполняется от имени  процесса"  и делает это в контексте процесса. В контексте процесса макрос curren t является действительным1. При  выходе  из режима  ядра  процесс  продолжает выполнение в пространстве пользователя, если в это время  не появляется готовый  к выполнению более приоритетный  процесс. В таком  случае  активизируется планировщик,  который выбирает для выполнения более приоритетный процесс.

Системные вызовы  и обработчики исключительных ситуаций являются строго определенными интерфейсами ядра. Процесс может начать выполнение в пространстве ядра только  посредством одного  из этих интерфейсов — любые  обращения к ядру возможны только  через  эти  интерфейсы.

Дерево семейства процессов

В операционной системе  Linux  существует  четкая  иерархия процессов. Все процессы  являются потомками процесса init , значение идентификатора PID  для которого  равно  1. Ядро  запускает  процесс   ini t на последнем шаге  процедуры  загрузки системы. Процесс init , в свою очередь,  читает системные файлы сценариев начальной загрузки (initscripts) и выполняет другие программы, что в конце  концов завершает процедуру  загрузки  системы.

Каждый процесс   в  системе  имеет  всего  один  порождающий процесс. Кроме того,  каждый   процесс   может  иметь  один  или  более  порожденных  процессов. Процессы, которые  порождены одним  и тем же родительским процессом, называются  родственными  (siblings).  Информация о взаимосвязи между процессами хранится  в дескрипторе процесса.  Каждая  структура  task_struc t  содержит  указатель на  структуру  task_struc t  родительского процесса,  который  называется parent , эта  структура  также  имеет  список   порожденных процессов,  который называется children . Следовательно, если  известен  текущий   процесс   (current) , то для  него можно  определить дескриптор родительского процесса с помощью выражения:

struct task_struct *task = current->parent;

Аналогично можно  выполнить цикл  по  процессам, порожденным от текущего процесса, с помощью кода:

struct task_struct *task;

struct list_head *list;

list_for_each (list, scurrent->children) {

task = list_entry(list, struct task_struct, sibling);

/* переменная task теперь указывает на один из процессов,

порожденных текущим процессом */

}

Дескриптор процесса ini t — это статически выделенная структура данных  с именем  inittask . Хороший пример  использования связей  между всеми  процессами — это приведенный ниже  код,  который всегда выполняется успешно.

1 Отличным от контекста процесса  является  контекст прерывания, описанный в главе 6,  "Прерывания  и обработка  прерываний" .  В контексте прерывани я система  работает  не  от имени  процесса, а  выполняе т обработчи к  прерывания . С  обработчиком прерывании  не  связан  ни  один  процесс , поэтому  и  контекст процесса  отсутствует.

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_tas k (task )  (получить предыдущую задачу). Наконец, макрос for_each_proces s (task )  позволяет выполнить цикл по всему списку задач. На каждом шаге цикла переменная tas k указывает на следующую задачу из списка:

struct task_struct *task;

for_each_process(task) {

/* просто печатается имя команды и идентификатор PID

для каждой задачи */

printk("%s[%d]\n",  task->comm, task->pid);

}

Следует  заметить, что  организация цикла по  всем  задачам   системы,  в  которой выполняется много   процессов, может  быть  достаточно дорогостоящей  операцией. Для  применения такого  кода  должны быть  веские  причины (и  отсутствовать другие альтернативы).

Источник: Лав,  Роберт. Разработка ядра  Linux, 2-е  издание. : Пер.  с англ.  — М.  : ООО  «И.Д.  Вильяме» 2006. — 448 с. : ил. — Парал. тит. англ.

По теме:

  • Комментарии