Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
LINUX_EXAM.docx
Скачиваний:
2
Добавлен:
01.07.2025
Размер:
399 Кб
Скачать
☆

2.2 Управление процессами. Выполнение программного кода. Основные понятия

Выполнение программного кода включает: – Сегмент текста (text section) – объектный код, находящийся в состоянии исполнения – Сегмент данных (data section) – глобальные переменные – Набор ресурсов (открытые файлы, ожидающие обработку сигналы) – Адресное пространство – Не менее одного потока выполнения (execution thread) • Реализацию выполнения программного кода называют процессом (process) – абстрактное понятие в системах POSIX

Поток выполнения – объект, выполняющий операции внутри процесса:

– Счётчик команд (program counter) – Стек выполнения – Набор регистров процессора

• Ядро планирует выполнение отдельных потоков, а не процессов • Раньше отдельный процесс содержал только один поток • Процесс, содержащий несколько потоков, называется многопоточным (многопоточной программой) • В Linux уникальная реализация потоков – это специальный тип процессов

Каждый процесс предусматривает наличие двух виртуальных ресурсов:

– Виртуальный процессор

• Создаёт для процесса иллюзию монопольного использования всей вычислительной системы (ВС)

• Физическим процессором совместно пользуются десятки процессов

• Эта виртуализация и помещение процессов на физический процессор называется планированием процессов – Виртуальная память

• Создаёт для процесса иллюзию того, что процесс один располагает всей памятью ВС

Потоки одного процесса: – Совместно используют одну и ту же виртуальную память (адресное пространство) – Каждый получает свой виртуальный процессор

Программа не является процессом

– Процесс – выполняющаяся программа + набор ресурсов + адресное пространство (АО)

– Несколько процессов могут выполнять одну и ту же программу

– Несколько процессов могут использовать одни и те же ресурсы (файлы, АО)

В Linux создание процесса осуществляется с помощью системного вызова fork() (fork – ветвление)

– новый процесс создаётся путём полного копирования уже существующего

• Вызвавший fork() процесс – родительский (parent) – Продолжает выполнение

• Новый процесс – дочерний (child) – Начинает выполняться с места возврата системного вызова

• Функции exec* () – создание нового адресного пространства и загрузка в неё новой программы

• Выход из программы – системный вызов exit() – Завершает процесс – Освобождает все занятые ресурсы

• Запрос о состоянии порождённых процессов – wait4() – один процесс ожидает завершение другого (wait, waitpid, wait3, wait4)

• Состояние процесса, когда он завершён, но порождающий процесс ещё не вызвал wait() или waitpid() – зомби

• Понятие задача и процесс взаимозаменяемы – Процессы в ядре называют задачами (tasks) (представление работающей программы в ядре) – Представление работающей программы в режиме пользователя – процесс.

ОПИСАТЕЛЬ ПРОЦЕССА

• Двусвязный список с информацией обо всех процессах – список задач (task list)

• Элемент списка – описатель процесса (process descriptor) – Содержит всю информацию об определённом процессе и данные, которые описывают выполняемую программу: • Открытые файлы • Адресное пространство • Ожидающие обработку сигналы • Состояние процесса и т.д.

ВЫДЕЛЕНИЕ ОПИСАТЕЛЯ:

• До 2.6 – task_struct – в конце стека ядра процессора • Местоположение описателя процесса – значение регистра указателя стека (stack pointer) (без использования дополнительных регистров).

• В серии ядер 2.6 – Память для task_struct – с помощью подсистемы выделения памяти – плиточного распределителя (slab allocator)

– Для каждой задачи введена структура thread_info • Хранится в области дна стека при стеке, растущем в сторону уменьшения значения адреса, или вершины – если в сторону увеличения • Поле task структуры является указателем на task_struct – описатель процесса.

• Ядро определяет процессы с помощью уникального значения идентификатора процесса (process identification value, PID) – Поле pid описателя процесса – PID – целое число (скрытый тип pid_t ~ int)

– Для обратной совместимости макс. 32768 • Макс. кол-во процессов • Мало для серверов • Чем меньше pid_max, тем скорее нумерация начнётся с начала – нарушение свойства «больший номер – позже запущен»

• Обычно в ядре на задачи ссылаются через указатель на описатель процесса • Макрос current – описатель процесса, выполняющегося в данный момент.

СОСТОЯНИЕ ПРОЦЕССА:

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

• Каждый процесс в одном из 5 состояний – TASK_RUNNUNG – запускаем (runnable) – TASK_INTERRUPTIBLE – приостановлен (sleeping) – заблокирован в ожидании некоторого условия, может возобновиться при получении сигнала – TASK_UNINTERRUPTIBLE – приостановлен (sleeping) – заблокирован в ожидании некоторого условия, не может возобновиться при получении сигнала – TASK_ZOMBIE – завершён, порождающий процесс ещё не вызвал wait4() – TASK_STOPED – остановлен

• Изменение состояния – set_task_state(task, state) (с установкой барьера памяти для SMP- систем) – task->state = state – set_current_state(state) или set_task_state(current, state)

КОНТЕКСТ ПРОЦЕССА

• Исполняемый программный код – Считывается из исполняемого файла (executable file) – Выполняется в адресном пространстве процесса • Обычно в пространстве пользователя – Если выполняется системный вызов или возникает исключительная ситуация – входит в пространство ядра – При выходе – работа в пространстве пользователя (только если не появляется более приоритетный процесс, иначе активизируется планировщик).

Контекст процесса включает в себя содержимое адресного пространства задачи, выделенного процессу, а также содержимое относящихся к процессу аппаратных регистров и структур данных ядра. С формальной точки зрения, контекст процесса объединяет в себе пользовательский контекст, регистровый контекст (счетчик команд, регистр состояния процесса, РОН) и системный контекст (управляющая информация, регистровый контекст предыдущего уровня контекста – для восстановления). Пользовательский контекст состоит из команд и данных процесса, стека задачи и содержимого совместно используемого пространства памяти в виртуальных адресах процесса. Процесс выполняется в рамках своего текущего контекстного уровня.

ДЕРЕВО ПРОЦЕССОВ.

• Иерархия: все процессы – потомки процесса init (PID=1) – Запуск init – на последнем шаге загрузки ядра • init читает системные сценарии инициализации (initscripts) • Выполняет другие программы для загрузки системы • Каждый процесс имеет: – Один порождающий (родительский) процесс. task_struct -> parent – Может порождать множество процессов • Процессы от одного родительского – родственные (siblings) • Список task_struct -> children • Каждый процесс имеет свой PID и PPID (Parent Process ID) • init: PPID = PID = 1

• Указатель на следующее задание в списке задач list_entry(task->tasks.next, struct task_struct, tasks); или макрос next_task(task) • Указатель на предыдущее задание в списке задач list_entry(task->tasks.prev, struct task_struct, tasks); или макрос prev_task(task)

• Цикл по всему списку задач

struct task_struct *task;

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

СОЗДАНИЕ НОВОГО ПРОЦЕССА

• В большинстве ОС – метод порождения (spawn) – Создаётся процесс в новом адресном пространстве – В него считывается исполняемый файл – Начинается исполнение процесса • В системах POSIX – операции разбиваются на две: fork() – порождается процесс-копия текущего задания с отличиями: • PID • PPID (PID родителя) = PID порождающего процесса • Ресурсы (без ожидающих на обработку сигналов) • Статистика использования ресурсов. exec*() (в Linux через системный вызов execve()) • загружает исполняемый файл в адресное пространство процесса и исполняет его • fork () + exec*() – аналог единой функции в других ОС

КОПИРОВАНИЕ ПРИ ЗАПИСИ

• В системах POSIX при fork() – дубликат всех ресурсов родительского процесса • В Linux – механизм копирования при записи (copy-on-write, COW). Откладывает или вообще предотвращает копирование. Вместо создания дубликата адресного пр-ва (АП) оба процесса могут использовать одну копию АП (м. б. > 10 Мб). При изменении данных создаётся копия. Если никогда не делается запись (exec() сразу после fork()), то копирование не проводится вовсе. fork() требует только: • Копирование таблиц страниц • Создание описателя процесса.

Важно, чтобы порождённый процесс запускался на выполнение раньше порождающего. Это позволяет избежать накладных расходов, связанных с тем, что если родительский процесс начинает выполняться первым, то он будет ожидать возможности записи в АП посредством механизма копирования при записи.

РАБОТА COPY_PROCESS

• dup_task_struct(), создаёт

– Стек ядра; – Копирование task_info, task_struct

• Не много ли процессов для пользователя?

• Начальные значения описателя

• В состояние TASK_UNINTERRUBLE

• copy_flags() – Обновляет значения поля flags (в task_struct); – Сброс права суперпользователя; – Установка PF_FORKNOEXEC (ещё не вызывана exec())

• get_pid() – получение PID

• В зависимости от флагов-аргументов clone() Копирование или сомв. использование – Открытых файлов – Информации о файловой системе – Обработчиков сигналов – АП – Пространства имён (namespace)

• Разделение оставшейся части кванта времени между родит. и дочерним процессами

• Зачистка структур данных • Возврат указателя на описатель нового процесса

РЕАЛИЗАЦИЯ ПОТОКОВ

Согласованное программирование (concurrent programming) – программирование, при к. ПО разрабатывается как набор взаимодействующих вычислительных процессов, к. могут исполнятся параллельно

Многопоточность — обеспечивает выполнение нескольких потоков в совместно используемом адресном пространстве памяти. Потоки также могут совместно использовать открытые файлы и другие ресурсы. Многопоточность используется для параллельного программирования (concurrent programming), что на многопроцессорных системах обеспечивает истинный параллелизм.

Реализация потоков в операционной системе Linux уникальна. Для ядра Linux не существует отдельной концепции потоков. В ядре Linux потоки реализованы так же, как и обычные процессы. В ОС Linux нет никакой особенной семантики для планирования выполнения потоков или каких-либо особенных структур данных для представления потоков. Поток— это просто процесс, который использует некоторые ресурсы совместно с другими процессами. Каждый поток имеет структуру task_struct и представляется для ядра обычным процессом (который совместно использует ресурсы, такие как адресное пространство, с другими процессами). Для операционной системы Linux потоки — это просто способ совместного использования ресурсов несколькими процессами.

• Некоторые ОС имеют явные средства поддержки потоков в ядре: – Microsoft Windows, Oracle Solaris. – Потоки называются легковесными процессами. – процессами с быстрым переключением контекста.

СОЗДАНИЕ ПОТОКОВ В ЯДРЕ

Потоки создаются так же, как и обычные задания, за исключением того, что в системный вызов clone() передаются флаги с указанием, какие ресурсы должны использоваться совместно:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

– Результат – как при вызове fork(), но общие:

• АП • Ресурсы файловой системы (ФС) • Описатели файлов • Обработчики сигналов

ФЛАГИ:

• CLONE_FILES – общие открытые файлы • CLONE_FS – общая информация о ФС • CLONE_IDLETASK – PID = 0 (холостые idle задачи) • CLONE_NEWNS – новое пространство имён • CLONE_PARENT – наследуется родитель • CLONE_PTRACE – трассировать и порождённый процесс • CLONE_SETTLS – создать новую область локальных данных потока (thread local storage) • CLONE_SIGHAND – общие обработчики сигналов • CLONE_THREAD – оба процессов принадлежат одной группе потоков • CLONE_VFORK – vfork(): род. процесс приостанавливается, пока порождённый его не возобновит • CLONE_UNTRACED – запретить родителю использовать CLONE_PTRACE для порождённого процесса • CLONE_STOP – запустить процесс в состоянии TASK_STOPED • CLONE_VM – общее АП

ПОТОКИ ЯДРА

Часто в ядре полезно выполнить некоторые операции в фоновом режиме. В ядре такая возможность реализована с помощью потоков пространства ядра (kernel thread) — обычных процессов, которые выполняются исключительно в пространстве ядра. Не имеют адресного пространства (значение указателя mm для них равно NULL). Планируются и вытесняются, как обычные задачи. В действительности поток в пространстве ядра может быть создан только другим потоком, работающим в пространстве ядра.

int kernel_thread(int (*fn)(void*), void* arg, unsigned long flags);

Новая задача создается с помощью обычного системного вызова clone() с соответствующими значениями флагов, указанными в параметре flags. При возврате из системного вызова родительский поток режима ядра завершается и возвращает указатель на структуру task_struct порожденного процесса. Порожденный процесс выполняет функцию, адрес которой указан в параметре fn, в качестве аргумента этой функции передается параметр arg. Для указания обычных флагов потоков пространства ядра существует флаг CLONE_KERNEL, который объединяет в себе флаги CLONE_FS, CLONE_FILES и CLONE_SIGHAND, так как большинство потоков пространства ядра должны указывать эти флаги в параметре flags.

ЗАВЕРШЕНИЕ ПРОЦЕССА

Когда процесс завершается, ядро должно освободить ресурсы, занятые процессом, и оповестить процесс, который является родительским для завершившегося, о том, что его порожденный процесс, к сожалению, "умер". Обычно уничтожение процесса происходит тогда, когда процесс вызывает системный вызов exit() явно или неявно при выходе из главной функции программы (компилятор языка С помещает вызов функции exit() после возврата из функции main()). Процесс также может быть завершен непроизвольно. Это происходит, когда процесс получает сигнал или возникает исключительная ситуация, которую процесс не может обработать или проигнорировать. Независимо от того, каким образом процесс завершается, основную массу работы выполняет функция do_exit(), а именно указанные далее операции.

• Устанавливается флаг PF_EXITING в поле flags структуры task struct.

• Вызывается функция del_timer_sync(), чтобы удалить все таймеры ядра.

• Если включена возможность учета системных ресурсов, занятых процессами, то вызывается функция acct_process() для записи информации об учете ресурсов.

• Вызывается функция __exit_mm() для освобождения структуры mm_struct, занятой процессом.

• Вызывается функция exit_sem(). Если процесс находится в очереди ожидания на освобождение семафора подсистемы IPC, то в этой функции процесс удаляется из этой очереди.

• Вызываются функции __exit_files(), __exit_fs(), exit_namespace() и exit_signals() для уменьшения счетчика ссылок на объекты, которые отвечают файловым дескрипторам, данным по файловой системе, пространству имен и обработчикам сигналов соответственно.

• Устанавливается код завершения задания, который хранится в поле exit_code структуры task struct.

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

• Вызывается функция schedule() для переключения на новый процесс.

После возврата из функции do_exit() дескриптор завершенного процесса все еще существует в системе, но процесс находится в состоянии TASK_ZOMBIE и не может выполняться. Как уже рассказывалось выше, это позволяет системе получить информацию о порожденном процессе после его завершения. Следовательно, завершение процесса и удаление его дескриптора происходят в разные моменты времени. После того как родительский процесс получил информацию о завершенном порожденном процессе, структура task_struct порожденного процесса освобождается. Когда приходит время окончательно освободить дескриптор процесса, вызывается функция release_task() (декремент счетчика ссылок, освобождение страниц памяти со структурой thread_info и кэша со структурой task_struct).

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]