Мультизадачность, мультипрограммирование, мультипроцессирование, мультипотоковость... Вся эта терминология может сконфузить даже опытного пользователя. Попытаемся систематизировать эти понятия на функциональном уровне, не вдаваясь в детали, доступные только профессиональным программистам.
Прежде чем перейти к обсуждению темы, приведем необходимые для понимания определения. Одним из основных является задача. Это базовый программный блок, управляемый ОС. Каждая операционная система может определять задачу по-разному – это зависит от того, как она спроектирована. Таким образом, задача – это концепция ОС, строящаяся на комбинации выполняемой программы и информации об используемых ею системных ресурсах. Всякий раз, когда программа ставится на исполнение, ОС создает для нее новую задачу, которая служит своеобразным контейнером для программы: она присваивает ей номер и прикрепляет контекст. Если программа в процессе своего выполнения вызывает какие-нибудь утилиты, то они, в свою очередь, могут рассматриваться ОС как задачи (подзадачи).
Актуальное выполнение программы называется процессом. Задача и процесс по существу описывают одну и ту же сущность, и, хотя они имеют разную терминологическую историю, используются как синонимы.
Операционная система выделяет и закрепляет за процессом его собственные ресурсы. В типичном случае это:
- образ исполняемого программного кода;
- память (реальная или/и виртуальная), содержащая исполняемый код и специфические для процесса данные;
- дескрипторы ресурсов, которые выделяются процессу, например дескрипторы файлов (UNIX) или сокеты, описатели файлов (file handles), устройств (device handles) и окна (Microsoft Windows);
- атрибуты безопасности, такие как владелец процесса и набор прав доступа;
- состояние процессора (контекст), включающее содержимое регистров, адреса физической памяти, указатели команд, флаги, источники и приемники данных и т. п.
ОС размещает большинство подобной информации об активном процессе в структурах данных, называемых блоки управления процессом (Process Control Blocks, PCB).
С одной программой может быть ассоциировано несколько процессов. Например, открытие двух-трех окон в одной программе в типичном случае приводит к выполнению более чем одного процесса.
Процесс может порождать множество дочерних подпроцессов, являющихся репликой родительского и способных разделять некоторые его ресурсы.
Ну и, наконец, последнее в нашем списке определений – это поток, или нить (thread). Так называют часть программы, которая может выполняться независимо от других частей. Программа либо состоит из одного потока, либо разветвляется на несколько. Поток и процессы могут отличаться от одной ОС к другой, но, в общем, способ, посредством которого создается и разделяет ресурсы поток, разнится от такового для процесса. Потоки содержатся внутри процесса, и разные потоки в одном процессе разделяют некоторые ресурсы, тогда как разные процессы – нет.
Уточним вышесказанное. Традиционные процессы отличаются от потоков тем, что они, как правило, независимы, включают значительный объем данных о своем состоянии, имеют отдельные адресные пространства и взаимодействуют только посредством системного механизма межпроцессных сообщений. С другой стороны, множество потоков в типичном случае разделяют информацию о состоянии с процессом-хозяином и прямо разделяют память и другие ресурсы. Отметим, что потоки не могут иметь собственных ресурсов за исключением стека, копии регистров (включая указатель команд) и локальной памяти (если это предусмотрено).
Ну, а теперь приставим префикс «мульти» к приведенным выше определениям, учитывая, что мультизадачность (multitasking) и мультипроцессность (multiprocess) рассматриваются как синонимы, и посмотрим, что получится.
Мультизадачность
Исторически технологии мультизадачности начали создаваться для однопроцессорных систем. В 60-х годах прошлого столетия машинное время стоило дорого, а периферия работала медленно. Для того чтобы процессор не простаивал во время операций ввода-вывода, был предложен мультипрограммный режим (multiprogramming). Мультипрограммирование позволяет нескольким программам находиться одновременно в отдельных областях памяти. С этой целью были разработаны ОС с организацией памяти разделами. Если программам выделяются разделы, размеры которых фиксированы, то такой режим назывался мультипрограммированием с фиксированным числом задач (Multiprogramming with a Fixed number of Tasks, MFT). Он, в частности, был реализован в качестве промежуточного шага в одной из конфигураций известной OS/360 для линейки мэйнфреймов IBM System/360. В 1967 г. MFT заменил режим мультипрограммирования с переменным числом задач (MVT), в котором разделы памяти определялись динамически.
|
Рис. 1. Парадигма мультизадачности |
Режим мультипрограммирования работал следующим образом. Несколько различных программ в пакете загружались в память, и первая начинала выполняться. При обращении программы к периферии ее контекст сохранялся, и ресурсы процессора предоставлялись второй. Это продолжалось до тех пор, пока не заканчивались все задачи.
Но при этом не было никаких гарантий, что программы будут выполняться своевременно. Ведь, скажем, какой-нибудь итерационный процесс может часами не обращаться к периферии. Однако в те времена это не создавало каких-либо проблем, поскольку программист не ожидал завершения задачи за терминалом. Он, к примеру, приносил свою колоду карт утром и приходил за результатом вечером. Когда же пакетный режим сменился интерактивным, мультипрограммирование также потребовало замены. Был разработан режим выполнения нескольких задач с разделением по времени, получивший название мультизадачный.
В ранних версиях мультизадачные системы выполняли «родственные» приложения, которые по истечении определяемого ими самими отрезка времени добровольно передавали управление друг другу. Такой режим получил название кооперативной мультизадачности. Она была реализована, к примеру, в MAC OS (до MAC OS X), Novell NetWare 3.11, Windows 9x для 16-разрядных приложений. Недостаток ее заключался в том, что эффективность зависела от «благородства» приложений, а плохо разработанная программа могла привести к зависанию всей системы.
В итоге был предложен режим вытесняющей многозадачности. В нем каждая задача получает свой квант времени, по истечении которого контекст переключается и управление передается другой задаче. Очередность их выполнения может регулироваться присвоением приоритетов.
На рис. 1 схематично представлена парадигма многозадачности. Данные выполняющихся процессов разделены по умолчанию: каждый из них имеет собственный стек для локальных переменных и собственную область памяти, к примеру, для объектов и других элементов данных. Многие операционные системы позволяют программисту определить область памяти, в которой объекты данных будут разделяться всеми процессами.
Мультипотоковость
С появлением мультизадачности программисты стали реализовывать приложения как набор кооперативных процессов, например, один процесс собирает данные, второй – обрабатывает их, третий записывает результаты на диск.
|
Рис. 2. Парадигма мультипотоковости |
Идея потоков родилась из осознания того факта, что наиболее эффективный способ обмена данными для кооперативных процессов заключается в разделении всего пространства памяти, выделенного задаче. Таким образом, потоки можно рассматривать как процессы, работающие с общим контекстом памяти.
Безусловно, мультипотоковость должна быть поддержана соответствующей архитектурой микропроцессора. Доступный обзор мультипотоковых архитектур приведен в статье Михаила Кузьминского «Многонитевая архитектура микропроцессоров» («Открытые системы», № 1, 2002). В ней, в частности, описываются три типа распараллеливания на уровне потоков.
При грубозернистом (coarse grained) распараллеливании микропроцессор должен поддерживать не менее двух аппаратных контекстов потоков (регистры общего назначения, указатели команд, слово состояния программы и т. п.). В каждый момент времени выполняются команды только одного из них (контекст которого активен) до тех пор, пока не произойдет прерывание, вызванное, к примеру, отсутствием необходимых данных в кэш-памяти. По получении прерывания процессор переключает контекст и начинает выполнение соответствующего потока.
Тонкозернистый (fine grained) тип распараллеливания предусматривает поддержку процессором N контекстов потоков, а команды каждого потока ставятся на выполнение на каждом N-ом такте.
Наиболее эффективной является архитектура с одновременным выполнением потоков (Simultaneous Multi-Threading, SMT). В таких CPU на каждом новом такте на выполнение в какое-либо функциональное исполняющее устройство может направляться команда любого потока. Иногда для данного режима можно встретить английское обозначение hyper-threading.
Может показаться, что поддержка SMT требует существенного расширения аппаратных средств по сравнению с таковыми для многопотоковости, выполняемой на однопотоковом процессоре. К счастью, это не так. К примеру, Intel сообщила, что для использования технологии Hyper-Threading в процессорах Xeon потребовалось увеличить площадь кристалла всего на 5%. Приведем некоторые детали этой реализации.
Intel Xeon может параллельно выполнять не менее двух потоков на двух логических процессорах. Для того чтобы ОС и пользователь видели оба CPU, необходимо управлять двумя различными и независимыми контекстами двух потоков. Это делается посредством разделения микроархитектурных ресурсов процессора на три типа: дублированные (replicated), раздельные (partitioned) и разделяемые (shared).
К первым принадлежат логика переименования регистров, указатель команд (по одному на поток с возможностью выбора любого на каждом такте), буфер предыстории трансляции команд (Instruction Translation Look-aside Buffer, нужен для осуществления процедуры удаления команд, выполненных вне очереди), стек адресов возврата (для их предсказания из подпрограмм) и различные другие архитектурные регистры. Раздельные ресурсы включают буфер восстановления последовательности (Reorder Buffer, структура, управляющая вычислениями с изменением последовательности, отслеживая состояние команд, которые были переданы на исполнение, но были удалены), буфера загрузки/хранения, различные очереди (такие как очереди планируемых заданий, микроопераций и т. п.). Разделяемыми являются кэш-память уровней L1, L2 и L3, регистры микроархитектуры и исполняющие устройства ядра.
В качестве иллюстрации к парадигме мультипотоковости приведем рис. 2, на котором представлена многопотоковая программа, работающая внутри единого экземпляра виртуальной машины Java. Каждый поток здесь отделен, так что локальные переменные в методах, которые выполняются потоком, разделены для разных потоков. Эти локальные данные являются полностью частными (private), т. е. не существует способа, посредством которого один процесс мог бы обратиться к локальным данным другого. Если же два процесса выполняют один и тот же метод, каждый получает отдельную копию его локальных переменных. Это полностью аналогично запуску двух копий текстового редактора: каждый процесс будет иметь отдельную копию локальных переменных.
Объекты и другие переменные экземпляра, с другой стороны, могут разделяться потоками в Java-программе, и механизм разделения этих объектов потоками более простой, чем для процессов в большинстве ОС.
Описанные выше технологии позволили повысить производительность ряда приложений. Однако наибольший эффект удается получить при использовании многопроцессорных систем.
Мультипроцессирование
Мультипроцессирование (multiprocessing) означает наличие нескольких процессоров, одновременно работающих с общей памятью. Кроме массового параллелизма, который мы не будем затрагивать, различают симметричное (SMP) и асимметричное (ASMP) мультипроцессирование.
В SMP-системах простейшей конфигурации все процессоры работают с одной ОС и разделяют общую память. Такие системы иногда называют Shared Memory Processing. Для связи CPU с памятью в типичном случае используется два метода: общая шина или коммутационная матрица (crossbar). Производительность систем, базированных на шине, сдерживается ее пропускной способностью, поэтому они плохо масштабируются – обычно до 8 процессоров. Возможности при применении коммутационной матрицы значительно лучше. Здесь системы масштабируются до 64 CPU.
Для преодоления этих ограничений была предложена конфигурация с распределенной разделяемой памятью (Non-Uniform Memory Access, NUMA). В этой модели взаимосвязи память физически распределяется на аппаратном уровне (каждому процессору отводится свой банк памяти), но для пользователя система все еще предоставляет ее единый образ. Естественно, доступ процессора к «своей» памяти будет быстрее, чем к памяти соседа. Правда, здесь возникает проблема согласования кэш-памяти процессоров, что требует дополнительной аппаратной логики (cache-coherent NUMA, ccNUMA).
Архитектура ccNUMA предоставляет удобство единого образа памяти и масштабируемость на уровне систем с массовым параллелизмом. Так, компанией SGI были продемонстрированы SMP-системы, поддерживающие 1024 процессора.
В ASMP-системах каждый процессор выполняет свою собственную задачу, а всей системой управляет один – главный. Планировщик направляет потоки на соответствующие процессоры. Например, CPU-1 может управлять дисковыми операциями записи/чтения, CPU-2 – заниматься обработкой видео и т. п. Распределение потоков в ASMP-системах проще, и они лучше масштабируются. Однако систем, поддерживающих эту архитектуру, мало. Для полноты картины можно вспомнить VAX 11, PDP-10 и OS-360.
Очевидно, что в данной публикации затронута даже не надводная часть айсберга. Но все же есть надежда, что представленного материала достаточно, чтобы разобраться в теме «в принципе».
Ready, set, buy! Посібник для початківців - як придбати Copilot для Microsoft 365