В настоящее время, когда каждый пользователь имеет достаточно мощный персональный компьютер, акценты в развитии ОС снова изменились. Теперь большое значение приобретает развитие сетевых, многозадачных ОС. В сущности, теперь пользователь имеет возможность установить на отдельном персональном компьютере многозадачную ОС и разрабатывать приложения, совмещающие вы-полнение нескольких процессов. Каждый процесс, в свою очередь, может состоять из нескольких потоков, выполняемых в адресном пространстве процесса.
Первые операционные системы, реализованные на персональных компьютерах, сильно уступали в концептуальном плане и по своим реальным возможностям системам с разделением времени, давно реализованным в mainframe- компьютерах. В Win 16, например, тоже существует понятие многозадачности. Реализовано оно следующим образом: обработав очередное сообщение, приложение передает управление операционной системе, которая может передать управление другому приложению. Такой вид многозадачности, при котором операционная система передает управление от одного приложения другому не в любой момент времени, а только когда текущее приложение отдает управление системе, получил, как было упомянуто, название кооперативной многозадачности (cooperative multi-tasking).
Если при таком подходе обработка сообщения затягивается, то пользователь увидит реакцию системы только после завершения обработки текущим приложением • текущего сообщения. Обычно при выполнении длительных операций программист изменяет форму курсора (песочные часы), вызвав API-функцию BeginWaitCursor. Иногда, если это предусмотрел разработчик программы, в таких случаях застрявшее приложение даже вызывает функцию PeekMessage, сообщая системе, что она может обработать очередное сообщение, а текущее приложение способно и подождать. Но главная неприятность при таком подходе заключается в том, что в случае бесконечного цикла, вызванного ошибкой в программе, ОС не имеет шансов получить управление и также зависнет. Пользователю придется перезагружать систему.
В Windows начиная с Windows 95 реализован принципиально другой вид многозадачности, в котором операционная система действительно контролирует и управляет процессами, потоками и их переключением. Способность операционной системы прервать выполняемый поток практически в любой момент времени и передать управление другому ожидающему потоку определяется термином preemptive multitasking — преимущественная, или вытесняющая, многозадачность. Реализация ее выглядит так: все существующие в данный момент потоки, часть из которых может принадлежать одному и тому же процессу, претендуют на процессорное время и, с точки зрения пользователя должны выполняться одновременно. Для создания этой иллюзии система через определенные промежутки времени забирает управление, анализирует свою очередь сообщений, распределяет сообщения по другим очередям в пространстве процессов и, если считает нужным, переключает потоки (рис. 12.5).
Реализация вытесняющей многозадачности в Windows 2000 дает не только возможность плавного переключения задач, но и устойчивость среды к зависаниям, так как ни одно приложение не может получить неограниченные права на процессорное время и другие ресурсы. Так система создает эффект одновременного выполнения нескольких приложений.
Если компьютер имеет несколько процессоров, то системы Windows NT/2000 могут действительно совмещать выполнение нескольких приложений. Если процессор один, то совмещение остается иллюзией. Когда заканчивается квант времени, отведенный текущей программе, система ее прерывает, сохраняет контекст и отдает управление другой программе, которая ждет своей очереди.
Величина кванта времени (time slice)зависит от ОС и типа процессора, в Windows NT она в среднем равна 20 мс. Следует отметить, что добиться действительно одновременного выполнения потоков можно только на машине с несколькими процессорами и только под управлением Windows NT/2000, ядра которых поддерживают распределение потоков между процессорами и процессорного времени между потоками на каждом процессоре. Windows 95 работает только с одним процессором. Даже если у компьютера несколько процессоров, под управлением Windows 95 задействован лишь один из них, а остальные простаивают.
Источник: osdev.fandom.com
15.Основные типы организации многозадачности и типы многозадачных ОС
Многозадачность— свойство операционной системы обеспечивать возможность параллельной (или псевдопараллельной) обработки нескольких процессов. Истинная многозадачность операционной системы возможна только в распределённых вычислительных системах.
Многозадачные системы позволяют запустить одновременно несколько программ, которые будут работать параллельно.
Существует 2 типа многозадачности:
— Процессная многозадачность (основанная на процессах — одновременно выполняющихся программах). Здесь программа — наименьший элемент кода, которым может управлять планировщик операционной системы. Более известна большинству пользователей (работа в текстовом редакторе и прослушивание музыки).
— Поточная многозадачность (основанная на потоках). Наименьший элемент управляемого кода — поток (одна программа может выполнять 2 и более задачи одновременно).
Многопоточность — специализированная форма многозадачности.
По числу одновременно выполняемых задач ОС могут быть разделены на два класса:
— однозадачные (MS-DOS, MSX);
— многозадачные (ОС ЕС, OS/2, UNIX, Windows 95).
Однозадачные ОС в основном выполняют функцию представления пользователю виртуальной машины, делая более простым и удобным процесс взаимодействия пользователя с компьютером. Однозадачные ОС включают средства управления периферийными устройствами, средства управления файлами[1], средства общения с пользователем.
Многозадачные ОС, кроме вышеперечисленных функций, управляют разделением совместно используемых ресурсов, таких как процессор, оперативная память, файлы и внешние устройства.
Поддержка многопользовательского режима. По числу одновременно работающих пользователей ОС делятся на:
— однопользовательские (MS-DOS, Windows3.x, ранние версии OS/2);
— многопользовательские (UNIX, Windows NT).
Главным отличием многопользовательских систем от однопользовательских является наличие средств защиты информации каждого пользователя от несанкционированного доступа других пользователей. Следует заметить, что не всякая многозадачная система является многопользовательской, и не всякая однопользовательская ОС является однозадачной.
Многопроцессорная обработка. Другим важным свойством ОС является отсутствие или наличие в ней средств поддержки многопроцессорной обработки. В наши дни становится общепринятым введение в ОС функций поддержки многопроцессорной обработки данных. Такие функции имеются в операционных системах Solaris 2.x фирмы Sun, Oрen Server 3.x компании Santa Crus Oрerations, OS/2 фирмы IBM, Windows NT фирмы Microsoft и NetWare 4.1 фирмы Novell.
Многопроцессорные ОС могут классифицироваться по способу организации вычислительного процесса в системе с многопроцессорной архитектурой: асимметричные ОС и симметричные ОС. Асимметричная ОС целиком выполняется только на одном из процессоров системы, распределяя прикладные задачи по остальным процессорам. Симметричная ОС полностью децентрализована и использует весь пул процессоров, разделяя их между системными и прикладными задачами.
Многозадачные ОС подразделяются на три типа в соответствии с использованными при их разработке критериями эффективности:
— системы пакетной обработки (например, OC EC),
— системы разделения времени (UNIX, VMS),
— системы реального времени (QNX, RT/11).
Источник: studfile.net
Algo
Две системы на одном ПК Windows 10 и Windows 11
Как установить 2 операционные системы на диске, компьютере или ноутбуке (Windows 10 и Windows 7) 💻
В каждый конкретный момент времени процессор способен выполнять код только одного процесса (о многоядерных процессорах мы поговорим чуть позднее). За переключение процессов/потоков в современной многозадачной ОС отвечает специальный модуль ядра, который называется «планировщик» (scheduler).
Следуя определенным правилам он формирует очередь из ожидающих процессов/потоков и при окончании time slice одного процесса выбирает следующий процесс из очереди, выделяет ему определенный time slice и запускает его на исполнение. Для переключения процессов, надо:
- остановить процесс
- сохранить где-то его текущее состояние
- загрузить состояние следующего процесса
- начать исполнение следующего процесса с той точки, где он был недавно остановлен
Состояние процесса в конкретный момент времени называется «контекстом» (context). Это состояние описывается значениями регистров и счетчиков процессора в данный момент времени. Если эти все значения сохранить, а потом позже их загрузить, то процесс даже не поймет и не заметит, что его останавливали на какое-то время, он продолжит работать так, словно никакой остановки и не было. Этот процесс называется «переключением контекста» (context switch).
Переключаясь между процессами через определенные очень короткие промежутки времени, сохраняя и восстанавливая контекст каждого процесса, операционная система создает иллюзию у пользователя, что несколько процессов работают параллельно и одновременно, хотя на самом деле в каждый конкретный момент времени работает только один процесс. Так собственно и работает многозадачность в многозадачной операционной системе на однопроцессорной машине.
В Windows и Solaris потоки поддерживаются по-другому, поэтому речь о них идти не будет. В Linux, если наше приложение — многопоточное, каждый поток для операционной системы является отдельным особым процессом. Переключение между потоками приложения для Linux выглядит точно также, как переключение между обычными процессами с одной существенной разницей.
Context switch между потоками одного приложения выполняется быстрее, чем context switch между двумя независимыми процессами. Это связано с тем, что все потоки одного приложения имеют доступ ко всей памяти данного процесса, следовательно при переключении контекста не надо менять контекст памяти, а достаточно только поменять контекст стека. На это тратится намного меньше времени. Так что, если в очереди на исполнение процессы в планировщике стоят так:
proc1:thread1
proc1:thread2
proc2:thread7
proc1:thread6
то переключение контекста proc1:thread1 -> proc1:thread2 осуществится быстрее, чем переключение proc2:thread7 -> proc1:thread6.
Переключение контекста на многоядерных и многопроцессорных машинах
Ушло в прошлое время одноядерных процессоров. Сейчас все процессоры выпускаются в многоядерном варианте. По сути в одном корпусе процессора вы получается сразу несколько CPU, которые могут выполнять код одновременно независимо друг от друга. Если одноядерному процессору надо было быстро переключаться между процессорами для создания иллюзии их параллельного одновременного исполнения, то на многоядерном процессоре в каждый конкретный момент времени может исполняться несколько процессов одновременно, параллельно, по-настоящему без всяких иллюзий.
Машины серверного класса имеют два и более процессоров для обеспечения надежности. Причем каждый процессор еще и является многоядерным. Например, 2х-процессорная машина с 2-мя 16 ядерными процессорами способна одновременно, параллельно выполнять 32 процесса/потока. А если на процессорах включена функция гиперпоточности — то 64 процесса/потока!
20 лет назад для достижения такого результата нам бы понадобилась машина с 64 процессорами класса Pentium. И 20 лет назад такая машина считалась бы суперкомпьютером, стоила бы миллионы американских долларов и доступна была бы только оборонке или богатому научному учреждению.
Планировщик
Пришествие многоядерных процессоров и многопроцессоных машин потребовало внесения изменений и в алгоритмы планировщика операционной системы. Теперь планировщику нужно планировать исполнение процессов так, чтобы одни ядра процессора не простаивали, в то время как другие — работают со 100% загрузкой.
С самой первой версии Linux 1991 года вплоть до версии 2.4 планировщик был прост, понятен и тривиален, но плохо масштабировался при большом количестве процессов и при работе на многопроцессорных машинах. В версии ядра 2.5 была предпринята попытка создания нового планировщика под названием O(1), который работал намного лучше, повторял в чем-то работу планировщиков других UNIX-систем, но все равно имел свои недостатки. В версии ядра 2.6 были предприняты попытки написать еще один планировщик, который бы работал лучше, чем O(1). Этот эксперимент завершился в версии ядра 2.6.23, когда был представлен планировщик Completely Fair Scheduler (или сокращенно CFS).
Перенос потока с одного ядра на другое
Планировщик может перебрасывать процесс/поток перед его новым запуском с одного ядра на другой, или даже с одного процессора на другой, если в машине несколько многоядерных процессоров.
Например, дана двухпроцессорная машина с двумя 16 ядерными процессорами. Нумерация ядер в Linux будет выглядеть так CPU0, … CPU7 — ядра первого процессора, CPU8 … CPU15 — ядра второго процессора. Скажем, наш главный процесс работает изначально на CPU0 (т.е. первом ядре первого процессора). Это процесс порождает 6 потоков.
Обычно операционная система пытается каждый поток запустить на отдельном ядре, т.е. все 7 потоков (родительский и 6 порожденных) займут ядра с CPU0 до CPU6. Но потом планировщик будет останавливать эти потоки, чтобы дать на каждом ядре исполнится коду других потоков. Если мы посмотрим на наше приложение скажем через 15 минут, мы заметим, что наши потоки «разбрелись» по всем доступным ядрам процессора: родительский оказался на CPU5, а какой-то из порожденных потоков — на CPU15 (т.е. на последнем ядре второго процессора).
Как это скажется на производительности?
Ивалидация кэша
Каждое ядро процессора оснащено кэшем первого (L1) и второго уровней (L2), а сам процессор имеет общий кэш третьего уровня (L3), которым пользуются все ядра данного процессора. В кэше хранится копия данных, недавно извлеченная из памяти. Если процесс был остановлен планировщиком и потом запущен на другом ядре, кэши этого ядра не будут иметь этой копии данных, так как на этом ядре они еще не извлекались. А данные в кэше, оставшиеся от предыдущего потока, ему не нужны.
Первым делом поток обратится к кэшам за данными, не найдет их так и произойдет событие, которое называется cache miss процессов остановится, полезет в кэш L1, потом в кэш L2, потом в кэш L3, потом в память, извлечет из нее данные, поместит их в кэш, и только тогда продолжит исполнение процесса. На это требуется определенное время. Это все приведет к небольшой задержке. На графиках производительности это все будет выглядеть как jitter.
Если потом поток будет снова перенесен планировщиком на новое ядро, вся история повторится, это снова приведет к задержке. Так как переключение процессов на современных компьютерах происходит каждые 20-50 миллисекунд, мы будем каждые 20-50 миллисекунд получать задержку только по той причине, что планировщик запускает наш процесс всякий раз на каком-то другом ядре. Хорошо, если данные, с которыми предстоит процессу работать окажутся в кэше хотя бы третьего уровня (L3), тогда задержка будет несколько десятков наносекнуд, а если в памяти? А если в той странице памяти, которая отсутствует в физической памяти и должна быть прочитана с диска (page fault)?
Система предсказания переходов
Подсистема предсказания переходов (branch prediction) все еще может хранить информацию о предыдущих исполнениях этого потока. Значит, когда этот же поток снова начинает исполняться на этом же ядре, процессору не придется собирать эту информацию с нуля.
Affinity и Isolation
В полном идеале хорошо было бы, чтобы каждый конкретный поток не делил ядро с другим потоком, т.е. количество активных потоков было бы равно количеству выделенных для процесса ядер. Как минимум было бы хорошо, чтобы каждый поток работал на своем ядре и не переносился планировщиком на другое ядро. Для решения этой проблемы есть несколько инструментов.
Affinity
Первый инструмент называется affinity. Дословно — «приклеивание» процесса определенному ядру. Об этом приеме можно найти достаточно подробное описание в Википедии. Благодаря affinity планировщик точно знает, что данный процесс после «пробуждения» должен работать только на этом ядре или ядрах, и ни на каких других.
Например, вот как функция affinity выглядит в Windows 10. Обратите внимание, что так мы управляем affinity процесса, но не потоков этого процесса:
На Linux привязки процесса к процессору можно добиться с помощью утилиты taskset. Более тонкая привязка какого-то потока к определенному ядру уже осуществляется:
- программно через JNI/C++: например, как это сделано в OpenHFT/ThreadAffinity
- с помощью утилит isocpu и cgroups.
Первый вариант предпочтительнее, так как при перезапуске процесса, не требуется выстраивать affinity вручную. С помощью конфигурационного файла можно определить affinity каждого потока и при запуске эти настройки автоматически «разбросают» потоки по ядрам.
Isolation
Второй инструмент — изоляция ядер (isolation). Мало приклеить процесс к определенному ядру, надо еще сообщить планировщику, чтобы он не запускал на этом ядре другие процессы, которые могут «загрязнить» кэши процесссора своими данными. На Linux изоляция процессора под определенную задачу осуществляется с помощью утилиты isolcpus.
На двухпроцессорной машине можно отвести все ядра одного процессора под low-latency приложение, а все сопутствующие сервисы и приложения операционной системы — запускать на втором процессоре. Так с помощью affinity и изоляции в полном распоряжении вашего low-latency приложения окажется 16 ядер одного процессора. Если в вашем приложении 16 потоков, то каждый поток беспрерывно будет исполняться на своем ядре и не будет приводить к переключению контекстов.
Paging и Page Fault
Процесс в память загружается не целиком, в памяти процесса в конкретный момент находится только несколько сегментов кода по 4 килобайта каждый. Если ваш код в процессе исполнения делает переход на часть кода, которого нет в этих страницах, происходит так называемый «page fault», исполнение программы приостанавливается, ядро дозагружает нужную страницу с диска и потом исполнение продолжается дальше.
Swapping
Если все работающие процессы не помещаются в памяти, операционная система запускает механизм свопа: останавливает неактивные процессы и выгружает из целиком на диск. В случае если процесс снова надо вернуть в память и выполнить его, происходит обратная операция: весь процесс загружается в память и продолжает свое исполнение как если бы он там все время находился. Выгрузка процесса на диск и загрузка его с диска в память — очень дорогие операции. Всякий, кто когда-либо работал, например с Windows, на маленьком объеме памяти помнит, как при переключении между задачами система буквально замирала, а жесткий диск неистово дёргался.
Paging и Swapping схожи друг с другом, но это разные функции.
Как это применить к производительности
Из вышесказанного можно сделать следующие выводы:
- уменьшить количество работающих процессов, отключить все ненужные процессы и понизить приоритет тех, которые отключить нельзя. В идеале low-latency приложение должно работать на машине одно, и не конкурировать с другими low-latency процессами за ресурсы.
- не допускать никакого свопа процессов, особенно нашего low-latency процесса — у операционной системы всегда должно быть достаточно свободной памяти для работы в течение всего операционного дня
- использовать оптимизированный планировщик операционной системы, который более эффективно планирует исполнение процессов, так как поставляемый по умолчанию — слишком общ. Разные планировщики по-разному определяют приоритеты процессов и по разному определяют, какой процесс должен исполняться следующим
- увеличить размеры страниц памяти, чтобы процесс загружал в память большие куски себя и не лез на диск за недостающими сегментами
- выделить для low-latency процесса определенное количество ядер и запретить другим процессам использовать эти ядра (process isolation)
- не позволять потокам процесса переходить с одного ядра на другой (affinity)
- писать на диск или в сеть только в самом крайнем случае, лучше всего делегировать это одному потоку, который исполняется на отдельном ядре и не мешает другим критическим потокам исполняться
Ссылки
Источник: algodma.wordpress.com