Анатомия управления процессами в Linux
Создание, управление, планирование и завершение
Linux — исключительно динамичная система, решающая постоянно меняющиеся вычислительные задачи. Представление вычислительной задачи в Linux построено вокруг абстракции процесса. Процессы могут иметь как скоротечный (команда, выполненная из командной строки), так и продолжительный (сетевая служба) жизненный цикл. По этой причине очень важным является общее управление процессами и их планирование.
В пользовательском пространстве процессы представлены идентификаторами (PID). С точки зрения пользователя PID представляет собой число, уникальным образом идентифицирующее процесс. PID остается неизменным в течение всего жизненного цикла процесса, но может быть повторно использован после его завершения, поэтому кэширование PID зачастую нежелательно.
Пользовательский процесс может быть создан несколькими способами. Можно запустить какую-либо программу (что вызовет создание нового процесса) либо выполнить из кода программы системные вызовы fork или exec . Системный вызов fork создает порожденный процесс, тогда как exec замещает контекст текущего процесса новой программой. Я подробно остановлюсь на каждом из этих методов.
Описание процессов, приведенное в этой статье, организовано следующим образом. Сначала будет рассказано о том, как процессы представлены в ядре и как оно ими управляет, затем будет дан обзор различных способов создания и планирования процессов на одном или нескольких процессорах и, в заключение, будет рассказано о том, что происходит при завершении процессов.
Представление процессов
Читайте другие статьи Тима Джонса на сайте developerWorks
В ядре Linux процесс представлен довольно большой структурой task_struct (дескриптором процесса). Помимо самой необходимой для описания процесса информации эта структура содержит массу других данных, используемых для учета и связи с другими процессами (родительскими и порожденными). Полное описание структуры task_struct выходит за рамки статьи, однако, ее фрагмент, содержащий упомянутые в статье элементы, приведен в листинге 1. Стоит заметить, что структура task_struct объявлена в файле ./linux/include/linux/sched.h.
Листинг 1. Небольшой фрагмент структуры task_struct
В листинге 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. Эта структура служит для сохранения процесса при переключении контекста (сохраняется состояние аппаратных регистров, счетчика команд и т. п.).
Управление процессами
Максимальное количество процессов
Несмотря на то, что процессы создаются динамически, их максимальное количество ограничено. В ядре это ограничение представлено символом max_threads , объявленным в файле ./linux/kernel/fork.c. Его значение можно изменять из пользовательского пространства через файл /proc/sys/kernel/threads-max файловой системы proc.
Теперь перейдем к рассмотрению управления процессами в 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)
Скомпилировать этот модуль можно с помощью make-файла, приведенного в листинге 3. После компиляции модуль можно загружать в ядро командой insmod procsview.ko и выгружать командой rmmod procsview .
Листинг 3. make-файл для сборки модуля ядра
После загрузки модуля в файле /var/log/messages появятся приведенные ниже сообщения. В их числе — информация о задаче бездействия системы (называемой swapper ) и задаче init (pid 1).
Обратите внимание на сведения о текущей задаче. В Linux имеется символ current , представляющий собой указатель на дескриптор текущего процесса (т.е. экземпляр структуры task_struct ). Если добавить в конец функции init_module строку:
будет выведено сообщение:
Заметим, что текущей задачей является insmod , поскольку функция init_module выполняется в контексте этой команды. Символ current , в действительности являющийся указателем на функцию ( get_current ), можно найти в архитектурно-зависимом заголовочном файле ./linux/include/asm-i386/current.h).
Создание процессов
Системные вызовы
Вы, вероятно, обращали внимание на закономерность в наименованиях системных вызовов. Они часто называются по шаблону sys_* и реализуют вспомогательную составляющую вызова (например, проверку возникновения ошибок и другие действия, выполняемые в пользовательском пространстве). Основная же работа вызова часто выполняется другой функцией, названной по шаблону do_* .
Рассмотрим создание пользовательского процесса шаг за шагом. Создание пользовательских процессов и процессов ядра фактически выполняется одним и тем же механизмом — вызовом 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.
Комментарии
Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.