Найти тему
IT. Как это работает?

От транзистора до фреймворка. Часть 22. Внутренности многозадачности

Оглавление

Видео: YouTube

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

Многозадачность

Давайте разберемся что это такое. Пусть компьютер выполняет программу № 1.

Логические и физическое адресные пространства
Логические и физическое адресные пространства

В памяти размещен ее исполняемый код, выделенный зеленым цветом. Желтым цветом выделен участок памяти для глобальных переменных. Синим цветом обозначим стек этой программы, растущий вниз к младшим адресам. Между стеком и глобальной областью расположено незанятое пространство. Ранее мы уже рассматривали для чего каждый сегмент памяти и как это все работает. Просто представим, что программа одна. Если совершается системный вызов, требующий некоторого времени ожидания, то мы будем наблюдать простаивание вычислительных ресурсов процессора. Пока не будет произведен выход из API функции системного вызова, новые инструкции после этого вызова выполняться не будут. Чтобы сделать вычисления более эффективными необходимо запустить другую программу, которая будет выполняться, пока первая программа заблокирована ожиданием.

Вторая программа имеет свое логическое адресное пространство от нулевого адреса до границы в 56 килобайт. Как мы помним, благодаря адресным регистрам PAR в составе контроллера памяти, вторая программа размещается по совсем другим физическим адресам. Цифрой 1 помечена первая программа, цифрой 2 — вторая. Назовем процессом программу со своими данными и стеком и добавим в это понятие контекст, то есть совокупность значений всех регистров. Как регистров общего назначения в регистровом файле, так и системных регистров в контроллере памяти. Таким образом, на компьютере функционирует операционная система и два процесса.

Дисциплина обслуживания

Как мы помним из прошлых выпусков, рассматриваемый учебный компьютер является однопроцессорным, то есть в один момент времени на нем может исполняться только один процесс. Несложно догадаться, что несколько процессов могут использовать один процессор только по очереди. Правило выбора какой процесс будет исполняться следующим называется дисциплиной обслуживания. Таких дисциплин может быть большое количество. Самая простая и довольно эффективная это, так называемая, карусель. Англоязычные публикации именуют ее как Round Robin (RR).

Дисциплина обслуживания процессов RR
Дисциплина обслуживания процессов RR

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

Реинкарнация процессной многозадачности

Теперь необходимо рассмотреть так уж ли нужна эта многозадачность? На этом рисунке программа или процесс один ожидает появление данных с твердотельного накопителя и как мы ранее рассмотрели, длится это хоть и не целую вечность, но достаточно долго. Чтобы процессор не простаивал, его занимают исполнением другой вычислительной задачи.

Ожидание окончания выполнения функции ядра
Ожидание окончания выполнения функции ядра

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

На этом рисунке приведена схема приложения, автоматизирующего работу предприятия такси.

Микросервисная архитектура приложения
Микросервисная архитектура приложения

Архитектура микросервисная, а это значит, что каждая часть является отдельным процессом. Пара процессов (Passenger WEB UI, Driver WEB UI) занимается предоставлением веб интерфейса для клиентов и водителей. Это в том случае, если они входят в приложение через веб интерфейс. Чаще всего вход осуществляется через мобильные приложения. Они имеют графический интерфейс, поэтому их запросы отправляются на так называемый шлюз API (API Gateway), это прикладной интерфейс, но в этом случае к функциям приложения, а не операционной системы, как мы рассматривали ранее. Этот шлюз перенаправляет запросы на другие процессы. Рассматриваем следующий столбец. Верхний процесс (Passenger Management) занимается работой с учетными записями клиентов, средний ( Driver Management ) занимается учетными записями водителей. Нижний процесс (Trip Management) занят построением оптимального маршрута в городе из пункта А в пункт Б. Теперь последний столбец. Верхний процесс (Billing) занимается расчетом стоимости поездки. Средний процесс (Payments) занят осуществлением платежей клиентов и оплатой труда водителей. Нижний процесс (Notification) занимается рассылкой уведомлений пассажирам и водителям. Вот если из-за какой либо ошибки процесс уведомления будет аварийно остановлен, то остальные спокойно продолжат свою работу. Даже если отсутствие уведомлений это недопустимо, то остальные могут дожидаясь перезапуска аварийного процесса заниматься полезным делом. Клиенты и водители будут все так же видеть интерфейс пользователя и не заметят никаких накладок.

Технические подробности многозадачности

Манипуляции с регистрами

Теперь немного технических подробностей. Каким же образом происходит смена процессов? Этот механизм сводится к сохранению содержимого всех регистров процессора.

Служебные регистры процессора
Служебные регистры процессора

То содержимое регистров, которое сохраняется, чуть позже будет восстановлено. Это похоже на заморозку мяса в морозильнике. Через пару недель его можно достать в таком же состоянии как оно было отправлено на хранение. При смене процесса никакой из них не выгружается из памяти, конечно, если ее объем позволяет. Сохраняются регистры из регистрового файла. Сохраняются адресные регистры, регистры атрибутов страниц. Занесение в регистры нового содержимого происходит с того места, куда это содержимое было сохранено ранее. Как вы уже поняли, восстанавливается содержимое регистров нового процесса. Инструкции, которые все это выполняют расположены в функции обработчике прерывания от таймера. Интервалы следования сигналов о прерывании определяют так называемый квант времени процесса. Это то время, которое процесс может работать без остановки.

Циклическое выполнение процессов
Циклическое выполнение процессов

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

Каким образом процесс может быть запущен? Если рассматривать технически, то он может быть запущен другим процессом, а уж как это случилось это совсем другой вопрос. Если пользователь захотел запустить процесс, то он воспользуется консолью или файловым менеджером.  В случае запуска по расписанию новый процесс будет запущен другим процессом и это будет некое приложение планировщик. Делают они это довольно просто.

Процедура запуска процесса
Процедура запуска процесса

Вызывается API функция операционной системы, в недрах которой хранится инструкция программного прерывания с каким-то номером. Пусть в нашем случае это будет 51. Это прерывание переключит контекст пользователя на контекст ядра, где функция обработчик выделит место в памяти для контекста нового процесса. Параметрами API функции запуска процесса являются путь к исполняемому файлу и список параметров для запускаемого процесса. Что такое путь к файлу детально рассмотрим позже.

Время жизни процесса

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

Распаковка исполняемого файла в память
Распаковка исполняемого файла в память

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

Упрощенная схема времени жизни процесса
Упрощенная схема времени жизни процесса

Возврат контекста в очередь происходит по сигналу от таймера. На самом деле такая схема уж очень проста и многое не учитывает. В частности, она не объяснит почему процесс в состоянии ожидания не должен быть отправлен на обработку. В этой очереди процессы могут быть в состоянии готовности исполняться и в состоянии блокировки в ожидании наступления каких-либо событий.

Схема времени жизни процесса с состоянием блокировки
Схема времени жизни процесса с состоянием блокировки

Благодаря такой чуть более усложненной схеме блокированный процесс может сколь угодно долго ожидать данные и не пытаться занять процессор. После окончания ожидания процесс будет поставлен в очередь как готовый к исполнению. Все эти состояния описываются в дескрипторе процесса, он же описатель процесса. Это место в памяти, на которое указывает регистр PD. Также в описателе содержится идентификатор процесса, его приоритет на случай если мы хотим использовать более эффективную дисциплину обслуживания процессов. Тот самый контекст процесса, о котором уже не раз упоминали также хранится в структуре дескриптора. Кроме того, все используемые ресурсы также отмечены в дескрипторе. О ресурсах процесса поговорим позднее.

Даже усложненная схема не идеальна, реальный жизненный цикл процесса охватывает более широкий спектр состояний на все случаи жизни. В частности, процесс может быть завершен не только из состояния выполнения. На самом деле он может быть остановлен другим процессом и не важно в каком состоянии он находится, но для учебной операционной системы такие сложности ни к чему. Рассмотрим каким образом процесс самостоятельно завершает себя. Это происходит путем вызова API функции exit().

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

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

Эффективность процессной многозадачности

Очевидно, что построение приложения как монолит без использования более мелких единиц многозадачности, а именно потоков, никакого эффекта от многоядерности не получит. Занято будет только одно ядро процессора. А это значит, что грамотный разработчик высоконагруженного приложения никуда не двинется без освоения различных библиотек, которые дадут возможность максимально эффективно использовать многоядерные процессоры. Далее, микросервисная архитектура казалось бы способна взять выгоду на все 100 процентов. Однако, и тут не все так просто. Узким горлышком будет являться коммутатор.

Схема многоядерного процессора
Схема многоядерного процессора

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

Продолжение следует...