Содержание


Анатомия управления процессами в Linux

Создание, управление, планирование и завершение

Linux - исключительно динамичная система, решающая постоянно меняющиеся вычислительные задачи. Представление вычислительной задачи в Linux построено вокруг абстракции процесса. Процессы могут иметь как скоротечный (команда, выполненная из командной строки), так и продолжительный (сетевая служба) жизненный цикл. По этой причине очень важным является общее управление процессами и их планирование.

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

Пользовательский процесс может быть создан несколькими способами. Можно запустить какую-либо программу (что вызовет создание нового процесса) либо выполнить из кода программы системные вызовы fork или exec. Системный вызов fork создает порожденный процесс, тогда как exec замещает контекст текущего процесса новой программой. Я подробно остановлюсь на каждом из этих методов.

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

Представление процессов

В ядре Linux процесс представлен довольно большой структурой task_struct (дескриптором процесса). Помимо самой необходимой для описания процесса информации эта структура содержит массу других данных, используемых для учета и связи с другими процессами (родительскими и порожденными). Полное описание структуры task_struct выходит за рамки статьи, однако, ее фрагмент, содержащий упомянутые в статье элементы, приведен в листинге 1. Стоит заметить, что структура task_struct объявлена в файле ./linux/include/linux/sched.h.

Листинг 1. Небольшой фрагмент структуры task_struct
struct task_struct {

	volatile long state; 
	void *stack; 
	unsigned int flags;

	int prio, static_prio;

	struct list_head tasks;

	struct mm_struct *mm, *active_mm;

	pid_t pid; pid_t tgid;

	struct task_struct *real_parent;

	char comm[TASK_COMM_LEN];

	struct thread_struct thread;

	struct files_struct *files;

	...

};

В листинге 1 можно вполне ожидаемо увидеть такие данные, как состояние выполнения, стек, набор флагов, указатель на дескриптор родительского процесса, поток выполнения (их может быть несколько) и дескрипторы открытых процессом файлов. Некоторые поля мы рассмотрим подробно сейчас, к остальным же вернемся позже. Переменная state состоит из битовых флагов, отражающих состояние процесса. Большую часть времени процесс выполняется или ожидает выполнения в очереди (TASK_RUNNING), находится в состоянии приостановки (TASK_INTERRUPTIBLE), в состоянии непрерываемой приостановки (TASK_UNINTERRUPTIBLE), в состоянии останова (TASK_STOPPED) и нескольких других. Полный список флагов состояний можно найти в файле ./linux/include/linux/sched.h.

Слово flags также состоит из большого числа флагов, дающих дополнительные сведения о состоянии процесса, например, происходит ли в данный момент создание процесса (PF_STARTING), его завершение (PF_EXITING) и даже запрошено ли выделение памяти (PF_MEMALLOC). Имя исполняемого файла (не содержащее путь) находится в поле comm.

Хотя каждому процессу назначается приоритет (его значение хранится в поле static_prio), фактический приоритет процесса определяется динамически исходя из загрузки и других факторов. Чем ниже значение приоритета, тем выше его фактический приоритет.

Поле tasks представляет собой элемент связного списка. Оно содержит указатель prev (указывающий на предыдущий дескриптор процесса) и указатель next (указывающий на следующий дескриптор процесса).

Адресное пространство процесса представлено полями mm и active_mm. Поле mm содержит указатель на структуру, описывающую адресное пространство процесса, а поле active_mm указывает на такую же структуру, но относящуюся к предыдущему процессу (это сделано для ускорения переключения контекстов).

Последнее из рассматриваемых полей, thread_struct, содержит сохраненное состояние процесса. Конкретная реализация этой структуры зависит от архитектуры оборудования, на котором выполняется Linux. Ее пример можно найти в файле ./linux/include/asm-i386/processor.h. Эта структура служит для сохранения процесса при переключении контекста (сохраняется состояние аппаратных регистров, счетчика команд и т. п.).

Управление процессами

Теперь перейдем к рассмотрению управления процессами в Linux. В большинстве случаев процессы создаются динамически и описываются динамически создаваемыми дескрипторами (экземплярами структуры task_struct). Исключением является процесс init, существующий всегда и описываемый init_task - статически создаваемым дескриптором процесса, объявленным в файле ./linux/arch/i386/kernel/init_task.c.

В Linux существует два способа организации процессов. Первый способ основан на использовании хеш-таблицы, ключом которой является значение PID; второй способ использует кольцевой двусвязный список. Способ, использующий кольцевой список, идеален для последовательного перебора списка процессов. Поскольку кольцевой список не имеет начала и конца, за отправную точку можно принять дескриптор init_task, который существует всегда. Рассмотрим пример перебора списка текущих задач.

Список задач недоступен из пользовательского пространства, но эту проблему легко решить путем добавления нашего кода в ядро в виде модуля. В листинге 2 приведен текст простейшей программы, осуществляющей перебор списка задач и выводящей немного информации о каждой из них (name, pid и имя родительского процесса, указатель на дескриптор которого хранится в поле parent). Обратите внимание на использование модулем вызова printk для вывода сообщений. Сообщения, выведенные модулем, можно будет найти в файле /var/log/messages и вывести на консоль с помощью утилиты cat (или tail -f /var/log/messages для вывода сообщений в режиме реального времени). Функция next_task представляет собой макрос, объявленный в файле sched.h и предназначенный для облегчения перебора списка задач (возвращает указатель на дескриптор следующей задачи).

Листинг 2. Простой модуль ядра для вывода сведений о задачах (procsview.c)
#include <linux/kernel.h> 
#include <linux/module.h> 
#include <linux/sched.h>

int init_module( void ) 
{ 
  /* Выбираем отправную точку */ 
  struct task_struct *task = &init_task;

  /* Перебираем элементы списка задач, пока снова не встретим init_task */ 
  do {
    printk( KERN_INFO "*** %s [%d] parent %s\n", 
                   task->comm, task->pid, task->parent->comm );

} while ( (task = next_task(task)) != &init_task );

return 0;

}

void cleanup_module( void ) 
{ 
  return; 
}

Скомпилировать этот модуль можно с помощью make-файла, приведенного в листинге 3. После компиляции модуль можно загружать в ядро командой insmod procsview.ko и выгружать командой rmmod procsview.

Листинг 3. make-файл для сборки модуля ядра
obj-m += procsview.o
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default: 
          $(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

После загрузки модуля в файле /var/log/messages появятся приведенные ниже сообщения. В их числе - информация о задаче бездействия системы (называемой swapper) и задаче init (pid 1).

Nov 12 22:19:51 mtj-desktop kernel: [8503.873310] *** swapper [0] parent swapper 
Nov 12 22:19:51 mtj-desktop kernel: [8503.904182] *** init [1] parent swapper 
Nov 12 22:19:51 mtj-desktop kernel: [8503.904215] *** kthreadd [2] parent swapper 
Nov 12 22:19:51 mtj-desktop kernel: [8503.904233] *** migration/0 [3] parent kthreadd ...

Обратите внимание на сведения о текущей задаче. В Linux имеется символ current, представляющий собой указатель на дескриптор текущего процесса (т.е. экземпляр структуры task_struct). Если добавить в конец функции init_module строку:

printk( KERN_INFO, "Current task is %s [%d], current->comm, current->pid );

будет выведено сообщение:

Nov 12 22:48:45 mtj-desktop kernel: [10233.323662] Current task is insmod [6538]

Заметим, что текущей задачей является insmod, поскольку функция init_module выполняется в контексте этой команды. Символ current, в действительности являющийся указателем на функцию (get_current), можно найти в архитектурно-зависимом заголовочном файле ./linux/include/asm-i386/current.h).

Создание процессов

Рассмотрим создание пользовательского процесса шаг за шагом. Создание пользовательских процессов и процессов ядра фактически выполняется одним и тем же механизмом - вызовом do_fork. При создании потока ядра оно вызывает функцию kernel_thread (см. файл ./linux/arch/i386/kernel/process.c), выполняющую предварительную работу и затем вызывающую do_fork.

Аналогичные действия выполняются при создании пользовательского процесса. Программа осуществляет вызов fork, который, в свою очередь, обращается к системному вызову sys_fork (см. файл ./linux/arch/i386/kernel/process.c). Связь этих функций показана на диаграмме, приведенной на рисунке 1.

Рисунок 1. Иерархия вызовов при создании процесса
Иерархия вызовов при создании процесса
Иерархия вызовов при создании процесса

Из рисунка 1 видно, что функция do_fork выполняет основную работу по созданию процесса. Она объявлена в файле ./linux/kernel/fork.c (наряду с другой функцией создания процессов - copy_process).

Функция do_fork начинает создание процесса с обращения к вызову alloc_pidmap, возвращающему новый PID. Затем do_fork проверяет, не находится ли родительский процесс в состоянии трассировки отладчиком. Если это так, то в параметре clone_flags функции do_fork устанавливается флаг CLONE_PTRACE. Далее функция do_fork вызывает функцию copy_process, передавая ей флаги, стек, регистры, дескриптор родительского процесса и ранее полученный новый PID.

Функция copy_process создает новый процесс путем копирования родительского процесса. Она выполняет все необходимые действия кроме запуска процесса, который происходит позже. Сначала copy_process проверяет, допустимо ли сочетание флагов CLONE. Если это не так, она возвращает код ошибки EINVAL. Затем она запрашивает Модуль безопасности Linux (LSM), разрешено ли текущей задаче породить новую задачу. Узнать больше об LSM в контексте Security-Enhanced Linux (SELinux) можно из материалов, приведенных в разделе Статьи.

Далее происходит вызов функции dup_task_struct (объявлена в файле ./linux/kernel/fork.c), создающей новый экземпляр структуры task_struct и копирующей в него различные дескрипторы, относящиеся к текущему процессу. После создания стека нового потока производится инициализация полей структуры task_struct, описывающих состояние процесса, после чего происходит возврат в copy_process. Затем в copy_process производится проверка лимитов и полномочий, выполняются вспомогательные действия, включающие инициализацию различных полей структуры task_struct. Потом происходит вызов функций, выполняющих копирование составляющих процесса: таблицы дескрипторов открытых файлов (copy_files), таблицы сигналов и обработчиков сигналов (copy_signal и copy_sighand), адресного пространства процесса (copy_mm) и структуры thread_info (copy_thread).

Затем только что созданная задача назначается процессору из числа разрешенных для ее выполнения (cpus_allowed). После того как новый процесс унаследует приоритет родительского процесса, выполняется еще небольшое число вспомогательных действий и происходит возврат в do_fork. Теперь новый процесс существует, но пока не выполняется. Функция do_fork решает эту проблему вызовом wake_up_new_task. Эта функция, определенная в файле ./linux/kernel/sched.c, инициализирует служебные данные планировщика, помещает новый процесс в очередь выполнения и инициирует его выполнение. После этого функция do_fork завершает создание процесса и свою работу, возвращая значение PID.

Планирование выполнения процесса

Выполнение процесса Linux потенциально может планироваться планировщиком задач. Выходя за рамки этой статьи, отмечу, что для каждого значения приоритета в планировщике задач Linux имеется список указателей на экземпляры структуры task_struct. Задачи запускаются функцией schedule (объявленной в файле ./linux/kernel/sched.c), определяющей наилучший процесс для выполнения исходя из загрузки системы и информации о ранее выполнявшихся процессах. Узнать больше о планировщике задач Linux версии 2.6 можно из материалов, приведенных в разделе Статьи.

Завершение процесса

Завершение процесса может быть вызвано несколькими событиями, от нормального завершения до завершения по сигналу или по вызову функции exit. Однако независимо от события, послужившего причиной завершения процесса, эта работа выполняется вызовом функции ядра do_exit (объявленной в ./linux/kernel/exit.c). Связь этой функции с различными вариантами завершения процесса показана на диаграмме, приведенной на рисунке 2.

Рисунок 2. Иерархия вызовов при завершении процесса
Иерархия вызовов при завершении процесса
Иерархия вызовов при завершении процесса

Функция do_exit предназначена для удаления из системы всех ресурсов завершаемого процесса (всех ресурсов, не являющихся общими). Первым шагом процедуры завершения процесса является установка флага PF_EXITING. Другие компоненты ядра анализируют значение этого флага, чтобы избежать выполнения действий с завершающимся процессом. Освобождение ресурсов, занятых процессом в течение его жизненного цикла, выполняется вызовами соответствующих функций, от exit_mm (освобождающей занятые страницы памяти) до exit_keys (удаляющей криптографические ключи потока, сеанса и процесса). Функция do_exit выполняет различные вспомогательные операции по завершению процесса и затем вызывает функцию exit_notify, рассылающую сигналы, оповещающие о завершении процесса (например, родительский процесс о завершении порожденного процесса). После этого статус процесса устанавливается в PF_DEAD и вызывается функция schedule, начинающая выполнение другого процесса. Обратите внимание на то, что если требуется отправка сигнала родительскому процессу (или процесс находится в режиме трассировки), задача не исчезнет из системы. Если отправка сигнала не требуется, вызов функции release_task освободит память, используемую процессом.

Двигаясь дальше

Развитие Linux продолжается, и в области управления процессами также будут появляться новые решения и производиться оптимизации. Оставаясь верным принципам UNIX, Linux продолжает осваивать новые рубежи. Новые архитектуры процессоров, симметричная многопроцессорная обработка (SMP) и виртуализация задают направления дальнейшего развития этой части ядра. Примером может служить новый O(1)-планировщик, появившийся в Linux версии 2.6, обеспечивающий масштабируемость систем, выполняющих большое количество задач. Другой пример - обновленная потоковая модель, использующая библиотеку потоков POSIX (Native POSIX Thread Library, NPTL) и обеспечивающая более эффективную работу потоков, нежели предшествующая модель LinuxThreads. Узнать больше о новых решениях и направлениях развития можно из материалов, приведенных в разделе Статьи.


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи Anatomy of Linux process management (EN) (developerWorks, декабрь 2008 г.).
  • Одним из самых передовых решений в составе ядра версии 2.6 является O(1)-планировщик. Он позволяет Linux выполнять очень большое количество процессов, избегая обычных в таких случаях непроизводительных затрат. Больше узнать о планировщике ядра версии 2.6 можно из статьи "Планировщик задач Linux" (developerWorks, июнь 2006 г.).
  • Великолепный обзор управления памятью в Linux дан в книге Мела Гормана (Mel Gorman) Understanding the Linux Virtual Memory Manager (EN) (Prentice Hall, 2004), доступной также в формате PDF. В книге приведено подробное, но доступно изложенное описание управления памятью в Linux, включающее главу, посвященную адресному пространству процесса.
  • Хорошее введение в управление процессами дано в книге Performance Tuning for Linux: An Introduction to Kernels (EN) (Prentice Hall, 2005). Глава из этой книги доступна на сайте IBM Press.
  • В Linux реализован интересный подход к организации системных вызовов, использующий переходы между пользовательским пространством и ядром (разными адресными пространствами). Об организации системных вызовов в Linux можно узнать подробнее из статьи "Kernel command using Linux system calls" (EN) (developerWorks, март 2007 г.).
  • В этой статье были приведены примеры проверки ядром параметров безопасности процессов, выполняющих системные вызовы. Базовый интерфейс ядра и среды безопасности носит название Linux Security Module. Чтобы изучить Linux Security Module в контексте SELinux, читайте статью "Анатомия SELinux" (developerWorks, апрель 2008).
  • Спецификация Portable Operating System Interface (POSIX) standard for threads (EN) определяет стандартный интерфейс программирования приложений (API) для создания потоков и управления ими. Существуют реализации POSIX для Linux, Sun Solaris и даже для операционных систем, не основанных на UNIX.
  • Native POSIX Thread Library (EN) представляет собой эффективную реализацию POSIX-потоков в ядре Linux. Эта технология появилась в ядре версии 2.6. Предшествующая реализация называлась LinuxThreads (EN).
  • Обзор удобной альтернативы состояниям процесса TASK_UNINTERRUPTIBLE и TASK_INTERRUPTIBLE приведен в статье "TASK_KILLABLE: Новое состояние процесса в Linux" (developerWorks, сентябрь 2008 г.).
  • В разделе сайта developerWorks, посвященном Linux, можно найти другие материалы для Linux-разработчиков (включая новичков) и просмотреть наши самые популярные статьи и руководства (EN).
  • Ознакомьтесь со всеми полезными советами и руководствами по Linux на сайте developerWorks.
static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Linux
ArticleID=387584
ArticleTitle=Анатомия управления процессами в Linux
publish-date=05052009