Главная страница
Навигация по странице:

  • Выделение дескриптора процесса

  • Хранение дескриптора процесса

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

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

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

  • Копирование при записи

  • Второе издание


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница6 из 53
    1   2   3   4   5   6   7   8   9   ...   53
    Дескриптор процесса и структура task structure
    Ядро хранит информацию о всех процессах в двухсвязном списке, который на- зывается task list
    3
    (список задач). Каждый элемент этого списка является дескрипто-
    ром процесса и имеет тип структуры s t r u c t task_struct, которая описана в файле i n c l u d e / l i n u x / s c h e d . h . Дескриптор процесса содержит всю информацию об определенном процессе.
    2
    В ядре реализован системный вызов w a i t 4 ( ) . В операционной системе Linux через библиотеку функций языка С доступны функции w a i t ( ) , w a i t p i d ( ) , w a i t 3 ( ) и w a i t 4 ( ) . Все эти функции возвращают информацию о состоянии завершившегося процесса, хотя в несколько разной семан- тике.
    3
    Иногда в литературе по построению операционных систем этот список называется t a s k a r r a y
    (массив задач). Поскольку в ядре Linux используется связанный список, а не статический массив,
    его называют task l i s t .
    i
    46 Глава 3

    Структура t a s k _ s t r u c t — достаточно большая структура данных размером по- рядка 1,7 Кбайт на 32-разрядной машине. Однако этот размер не такой уж большой,
    учитывая, что в данной структуре содержится вся информация о процессе, которая необходима ядру. Дескриптор процесса содержит данные, которые описывают вы- полняющуюся программу, — открытые файлы, адресное пространство процесса, ожи- дающие на обработку сигналы, состояние процесса и многое другое (рис. 3.1).
    Выделение дескриптора процесса
    Память для структуры task_struct выделяется с помощью подсистемы выделе- ния памяти, которая называется слябовый распределитель (slab allocator), для возмож- ности повторного использования объектов и раскрашивания кэша (cache coloring)
    (см. главу 11, "Управление памятью"). В ядрах до серии 2.6 структура t a s k _ s t r u c t хранилась в конце стека ядра каждого процесса. Это позволяет для аппаратных плат- форм, у которых достаточно мало регистров процессора (как, например, платформа х86), вычислять местоположение дескриптора процесса, только зная значение ре- гистра указателя стека (stack pointer), без использования дополнительных регистров для хранения самого адреса этого местоположения. Так как теперь дескриптор про- цесса создается с помощью слябового распределителя, была введена новая структура thread_info, которая хранится в области дна стека (для платформ, у которых стек растет в сторону уменьшения значения адреса памяти) или в области вершины стека
    (для платформ, у которых стек растет в сторону увеличения значения адреса памя- ти)
    4
    (рис. 3.2.).
    struct task_struct struct task_struct struct task struct
    Дескриптор процесса unsigned long state;
    int prio;
    unsigned long policy;
    struct task_struct *parent;
    struct list_head tasks;
    pid_t pid;
    Список задач (task list)
    Рис. З.1. Дескриптор процесса и список задач
    4
    Причиной создания структуры thread_info было не только наличие аппаратных платформ, обед- ненных регистрами процессора, но и то, что положение этой структуры позволяет достаточно просто рассчитыпать смешения адресов для значений ее нолей при использовании языка ассем- блера.
    Управление процессами
    47
    struct task struct

    Стек ядра процесса
    current_thread_infо()
    Начало стека
    Структура struct thread_infо
    Наибольшее значение адреса памяти
    Указатель стека
    Наименьшее значение адреса
    Структура thread_infо содержит указатель на дескриптор процесса
    Структура struct task_struct процесса
    Рис 3.2. Дескриптор процесса и стек ядра
    Структура struct thread_info для платформы х86 определена в файле thread_info.h> в следующем виде.
    struct thread_info {
    struct task_struct *task;
    struct exec_domain *exec_domain;
    unsigned long flags;
    unsigned long status;
    u32 cpu;
    __s32 preempt_count;
    mm_segment_t addr_limit;
    struct restart_block restart_block;
    unsigned long previous_esp;
    __u8 supervisorytack[0];
    };
    Для каждой задачи ее структура thread_info хранится в конце стека ядра этой задачи. Элемент структуры thread_info с именем t a s k является указателем на структуру task_struct этой задачи.
    Хранение дескриптора процесса
    Система идентифицирует процессы с помощью уникального значения, которое называется идентификатором процесса (process identification, PID). Идентификатор PID —
    это целое число, представленное с помощью скрытого типа pid_t
    5
    , который обыч- но соответствует знаковому целому— int.
    5
    Скрытый тип (opaque type) — это тип данных, физическое представление которого неизвестно или не существенно.
    48
    Глава 3

    Однако, для обратной совместимости со старыми версиями ОС Unix и Linux мак- симальное значение этого параметра по умолчанию составляет всего лишь 32768
    (что соответствует типу данных short int). Ядро хранит значение данного парамет- ра в поле pid дескриптора процесса.
    Это максимальное значение является важным, потому что оно определяет мак- симальное количество процессов, которые одновременно могут существовать в си- стеме. Хотя значения 32768 и достаточно для офисного компьютера, для больших серверов может потребоваться значительно больше процессов. Чем меньше это зна- чение, тем скорее нумерация процессов будет начинаться сначала, что приводит к нарушению полезного свойства: больший номер процесса соответствует процессу,
    который запустился позже. Если есть желание нарушить в системе обратную совме- стимость со старыми приложениями, то администратор может увеличить это макси- мальное значение во время работы системы с помощью записи его в файл /ргос/
    sys/kernel/pid_max.
    Обычно в ядре на задачи ссылаются непосредственно с помощью указателя на их структуры t a s k _ s t r u c t . И действительно, большая часть кода ядра, работаю- щего с процессами, работает прямо со структурами task_struct. Следовательно,
    очень полезной возможностью было бы быстро находить дескриптор процесса, ко- торый выполняется в данный момент, что и делается с помощью макроса current.
    Этот макрос должен быть отдельно реализован для всех поддерживаемых аппарат- ных платформ. Для одних платформ указатель на структуру t a s k _ s t r u c t процес- са, выполняющегося в данный момент, хранится в регистре процессора, что обе- спечивает более эффективный доступ. Для других платформ, у которых доступно меньше регистров процессора, чтобы зря не тратить регистры, используется тот факт, что структура thread_infо хранится в стеке ядра. При этом вычисляется по- ложение структуры thread_info, а вслед за этим и адрес структуры task_struct процесса.
    Для платформы х86 значение параметра current вычисляется путем маскирования
    13 младших бит указателя стека для получения адреса структуры thread_infо. Это мо- жет быть сделано с помощью функции current_thread_info (). Соответствующий код на языке ассемблера показан ниже.
    movl $-8192, %eax andl %esp, %eax
    Окончательно значение параметра c u r r e n t получается путем разыменования значения поля task полученной структуры thread_info:
    current_thread_info()->task;
    Для контраста можно сравнить такой подход с используемым на платформе
    PowerPC (современный процессор на основе RISC-архитектуры фирмы IBM), для которого значение переменной c u r r e n t хранится в регистре процессора r2. На платформе РРС такой подход можно использовать, так как, в отличие от платформы х8б, здесь регистры процессора доступны в изобилии. Так как доступ к дескриптору процесса — это очень частая и важная операция, разработчики ядра для платформы
    РРС сочли правильным пожертвовать одним регистром для этой цели.
    Управление процессами 49

    Состояние процесса
    Поле s t a t e дескриптора процесса описывает текущее состояние процесса
    (рис. 3-3). Каждый процесс в системе гарантированно находится в одном из пяти различных состояний.
    Существующий процесс вызывает функцию fork()
    и создает новый процесс
    TASK_ZOMBIE
    (процесс завершен)
    Планировщик отправляет задачу на выполнение: функция schedule ()
    вызывает функцию concext_switch ()
    Задача завершается через do exit()
    TASK_RUNNING
    (готов, но пока не выполняется)
    TASK_RUNNING
    (выполняется]
    Задача вытесняется более приоритетной задачей
    TASK_INTЕRRUPTIBLE
    TASK_UNINTERRUPTTВLE
    (задача ожидает)
    Событие произошло, задача возобновляет выполнение и помещается обратно в очередь готовых к выполнению задач
    Задача находится в приостановленном состоянии в очереди ожиданий на определенное событие
    Рис. 3.3. Диаграмма состояний процесса
    Эти состояния представляются значением одного из пяти возможных флагов,
    описанных ниже.
    • TASK_RUNNING— процесс готов к выполнению (runnable). Иными словами,
    либо процесс выполняется в данный момент, либо находится в одной из оче- редей процессов, ожидающих на выполнение (эти очереди, runqueue, обсуж- даются в главе 4. "Планирование выполнения процессов").
    • TASK_INTERRUPTIBLE — процесс приостановлен (находится в состоянии ожидания, sleeping), т.е. заблокирован в ожидании выполнения некоторого
    50
    Глава 3
    Задача разветвляется
    условия. Когда это условие выполнится, ядро переведет процесс в состояние
    TASK__RUNNING. Процесс также возобновляет выполнение (wake up) преждевре- менно при получении им сигнала.
    • TASK_UNNTERRUPTIBLE - аналогично TASK_INTERRUPTIBLE, за исключени- ем того, что процесс не возобновляет выполнение при получении сигнала.
    Используется в случае, когда процесс должен ожидать беспрерывно или когда ожидается, что некоторое событие может возникать достаточно часто. Так как задача в этом состоянии не отвечает на сигналы, TASK_UNINTERRUPTIBLE ис- пользуется менее часто, чем TASK_INTERRUPTIBLE
    6
    • TASK_ZOMBIE — процесс завершен, однако порождающий его процесс еще не вызвал системный вызов wait4 ( ) . Дескриптор такого процесса должен оста- ваться доступным на случай, если родительскому процессу потребуется доступ к этому дескриптору. Когда родительский процесс вызывает функцию wait4 (),
    то такой дескриптор освобождается.
    • TASK_STOPPED — выполнение процесса остановлено. Задача не выполняется и не имеет право выполняться. Такое может случиться, если задача получает ка- кой-либо из сигналов SIGSTOP, SIGTSTP, SIGTTIN или SIGTTOU, а также если сигнал приходит в тот момент, когда процесс находится в состоянии отладки.
    Манипулирование текущим состоянием процесса
    Исполняемому коду ядра часто необходимо изменять состояние процесса.
    Наиболее предпочтительно для Этого использовать функцию set_task state(task, state);
    /* установить задание 'task' в состояние 'state' */
    которая устанавливает указанное состояние для указанной задачи. Если применимо,
    то эта функция также пытается применить барьер памяти (memory barrier), чтобы га- рантировать доступность установленного состояния для всех процессоров (необхо- димо только для SMP-систем). В других случаях это эквивалентно выражению:
    task->state = state;
    Вызов s e t c u r r e n t s t a t e ( s t a t e ) является синонимом к вызову set_task_
    s t a t e ( c u r r e n t , s t a t e ) .
    Контекст процесса
    Одна из наиболее важных частей процесса— это исполняемый программный код. Этот код считывается из выполняемого файла (executable) и выполняется в адрес- ном пространстве процесса. Обычно выполнение программы осуществляется в про-
    странстве пользователя. Когда программа выполняет системный вызов (см. главу 5,
    "Системные вызовы") или возникает исключительная ситуация, то программа вхо- дит в пространство ядра.
    6
    Именно из-за этого появляются наводящие ужас "неубиваемые" процессы, для которых команда ps (1) показывает значение состояния, равное D, Так как процесс не отвечает на сигналы, ему нель- зя послать сигнал SIGKILL. Более того, завершать такой процесс было бы неразумно, так как этот процесс, скорее всего, выполняет какую-либо важную операцию и может удерживать семафор.
    Управление процессами 51

    С этого момента говорят, что ядро "выполняется от имени процесса" и делает это в контексте процесса. В контексте процесса макрос current является действитель- ным
    1
    . При выходе из режима ядра процесс продолжает выполнение в пространстве пользователя, если в это время не появляется готовый к выполнению более приори- тетный процесс. В таком случае активизируется планировщик, который выбирает для выполнения более приоритетный процесс.
    Системные вызовы и обработчики исключительных ситуаций являются строго определенными интерфейсами ядра. Процесс может начать выполнение в простран- стве ядра только посредством одного из этих интерфейсов — любые обращения к ядру возможны только через эти интерфейсы.
    Дерево семейства процессов
    В операционной системе Linux существует четкая иерархия процессов. Все про- цессы являются потомками процесса i n i t , значение идентификатора PID для кото- рого равно 1. Ядро запускает процесс i n i t на последнем шаге процедуры загрузки системы. Процесс i n i t , в свою очередь, читает системные файлы сценариев началь-
    ной загрузки (initscripts) и выполняет другие программы, что в конце концов заверша- ет процедуру загрузки системы.
    Каждый процесс в системе имеет всего один порождающий процесс. Кроме того, каждый процесс может иметь один или более порожденных процессов.
    Процессы, которые порождены одним и тем же родительским процессом, назы- ваются родственными (siblings). Информация о взаимосвязи между процессами хра- нится в дескрипторе процесса. Каждая структура task_struct содержит указатель на структуру t a s k _ s t r u c 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 теперь указывает на один из процессов,
    порожденных текущим процессом */
    }
    Дескриптор процесса i n i t — это статически выделенная структура данных с име- нем i n i t t a s k . Хороший пример использования связей между всеми процессами —
    это приведенный ниже код, который всегда выполняется успешно.
    1
    Отличным от контекста процесса является контекст прерывания, описанный в главе 6, "Прерыва- ния и обработка прерываний". В контексте прерывания система работает не от имени процесса,
    а выполняет обработчик прерывания. С обработчиком прерывании не связан ни один процесс,
    поэтому и контекст процесса отсутствует.
    52 Глава 3
    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) позволяет выполнить цикл по всему списку задач. На каждом шаге цикла переменная t a s k указывает на следующую за- дачу из списка:
    struct task_struct *task;
    for_each_process(task) {
    /* просто печатается имя команды и идентификатор PID
    для каждой задачи */
    printk("%s[%d]\n", task->comm, task->pid);
    }
    Следует заметить, что организация цикла по всем задачам системы, в которой выполняется много процессов, может быть достаточно дорогостоящей операцией.
    Для применения такого кода должны быть веские причины (и отсутствовать другие альтернативы).
    Создание нового процесса
    В операционной системе Unix создание процессов происходит уникальным об- разом. В большинстве операционных систем для создания процессов используется метод порождения процессов (spawn). При этом создается новый процесс в новом адресном пространстве, в которое считывается исполняемый файл, и после этого начинается исполнение процесса. В ОС Unix используется другой подход, а именно разбиение указанных выше операций на две функции: fork () и exec ()
    8 8
    Под e x e c ( ) будем понимать любую функцию из семейства e x e c * ( ) . В ядре реализован системный вызов e x e c v e ( ) , на основе которого реализованы библиотечные функции e x e c l p ( ) , execle(),
    e x e c v ( ) и execvp().
    Управление процессами 53

    В начале с помощью функции f o r k ( ) создается порожденный процесс, который является копией текущего задания. Порожденный процесс отличается от родитель- ского только значением идентификатора PID (который является уникальным в си- стеме), значением параметра PPID (идентификатор PID родительского процесса,
    который устанавливается в значение PID порождающего процесса), некоторыми ре- сурсами, такими как ожидающие на обработку сигналы (которые не наследуются), а также статистикой использования ресурсов - Вторая функция — exec () — загружает исполняемый файл в адресное пространство процесса и начинает исполнять его.
    Комбинация функций f o r k () и exec () аналогична той одной функции создания процесса, которую предоставляет большинство операционных систем.
    Копирование при записи
    Традиционно при выполнении функции f o r k ( ) делался дубликат всех ресурсов родительского процесса и передавался порожденному. Такой подход достаточно наивный и неэффективный. В операционной системе Linux вызов fork () реализо- ван с использованием механизма копирования при записи (copy-on-write) страниц памяти.
    Технология копирования при записи (copy-on-write, COW) позволяет отложить или вообще предотвратить копирование данных. Вместо создания дубликата адресного пространства процесса родительский и порожденный процессы могут совместно ис- пользовать одну и ту же копию адресного пространства. Однако при этом данные помечаются особым образом, и если вдруг один из процессов начинает изменять данные, то создается дубликат данных, и каждый процесс получает уникальную ко- пию данных. Следовательно, дубликаты ресурсов создаются только тогда, когда в эти ресурсы осуществляется запись, а до того момента они используются совместно в режиме только для чтения (read-only). Такая техника позволяет задержать копи- рование каждой страницы памяти до того момента, пока в эту страницу памяти не будет осуществляться запись. В случае, если в страницы памяти никогда не делается запись, как, например, при вызове функции exec () сразу после вызова fork (), то эти страницы никогда и не копируются. Единственные накладные расходы, которые вносит вызов функции fork ( ) , — это копирование таблиц страниц родительского процесса и создание дескриптора порожденного процесса. Данная оптимизация пре- дотвращает ненужное копирование большого количества данных (размер адресного пространства часто может быть более 10 Мбайт), так как процесс после разветвле- ния в большинстве случаев сразу же начинает выполнять новый исполняемый образ.
    Эта оптимизация очень важна, потому чти идеология операционной системы Unix предусматривает быстрое выполнение процессов.
    1   2   3   4   5   6   7   8   9   ...   53


    написать администратору сайта