Операционные системы
Аппаратное обеспечение компьютера – это интересно, это высокие технологии и т.д. Однако, без соответствующего программного обеспечения, это не более, чем груда дорогостоящего железа (точнее меди, аллюминия, а так же кремния и пластика).
По большому счету, ПО может работать с железом напрямую. В каком-то смысле это даже проще. Так часто делают, например, при разработке встраиваемых систем (скажем, микроволновок). Однако, любое ПО общего назначения должно бы выполняться на различном оборудовании одного класса, и тут работа с железом напрямую превращается в ад. Тут нам на помощь приходит операционная система.
С точки зрения программиста, основная задача операционной системы – предоставить универсальный интерфейс для работы с типовым оборудованием, такими как ПЗУ, ОЗУ, устройства ввода-вывода.
Задачи, которые фактически выполняет операционная система:
- Управление временем ЦП
- Управление памятью
- Управление периферийными устройствами
- Управление вводом-выводом
Так же ОС предоставляет API (интерфейс программирования приложений) для доступа к этим функциям.
Операционные системы можно разделить на 4 типа:
- Реального времени
- Однозадачные
- Многозадачные
- Многопользовательские
- ОС реального времени
- это ОС, используемые, как правило, для научного и индустриального оборудования. Основной смысл ОС реального времени в том, что одна и та же операция на одном и том же оборудовании завершается за строго определенное время. В некотором смысле, такая ОС ближе всего к “голому железу”.
- Однозадачные ОС
- Как подразумевает называние, это ОС, в которой активна одно пользовательское приложение. Примерами могут служить, скажем, первые ОС для смартфонов, такие как Symbian первых версий, или Palm OS, или ОС для первых электронных книг.
- Многозадачные ОС
- Позволяют одному пользователю работать с несколькими приложениями одновременно. К примерам относятся Windows 9x, или ранние версии Mac OS.
- Многопользовательские ОС
- Позволяют многим пользователям работать одновременно. Примерами могут служить различные UNIX-подобные ОС.
Замечу, что когда я говорю о “работать одновременно”, я не обязательно имею ввиду удаленно. В свое время я собирал машину с несколькими мониторами, мышами и клавиатурами на одном системном блоке, причем за каждым набором мог работать отдельный пользователь.
Основное отличие многозадачных от многопользовательских систем заключено в деталях реализации.
Ядро ОС
Ядро ОС – это центральная часть современных операционных систем. По сути, это программа, изолирующая аппаратную часть от всей остальной программной части. На x86-подобных процессорах, ядро ОС переводит процессор в защищенный режим и выполняется в нулевом кольце привилегий, и обеспечивает управление аппаратными ресурсами и запуск приложений в других кольцах привелегий.
Существуют различные подходы к архитектуре ядра. Два наиболее полярных (sic!) это:
- Монолитное ядро
- все ядро исполняется в едином адресном пространстве. Это значительно ускоряет выполнение операций уровня ядра, однако ошибки в компонентах ядра могут приводить к краху всей системы или порче данных.
- Микроядерная архитектура
- ядро системы обеспечивает механизмы межпроцессового взаимодействия и низкоуровневый доступ к оборудованию. Основная работа выполняется в процессах, работающих с более низкими привилегиями (на пользовательском уровне). Это позволяет изолировать подсистемы друг от друга, и крах, скажем, сетевой подсистемы приведет к отказу сети, а не всей ОС в целом.
Так же существуют гибридные подходы, например, модульные ядра. Большинство существующих на данный момент систем используют именно гибридные подходы.
Процессы и потоки
Программный код представляет из себя набор инструкций для процессора и ОС. Однако сам по себе он инертен и ничего не делает, пока ОС не запустит процесс выполнения данного программного кода.
Стандарт ISO 9000:2000 определяет процесс как совокупность взаимосвязанных и взаимодействующих действий, преобразующих входящие данные в исходящие. По сути, это исполняемый экземпляр программы.
Часто в понятие процесс включается не только, собственно процесс исполнения программы, но и ресурсы, этой программой используемые, а именно:
- Образ исполняемого машинного кода, связанного с программой
- Память, включающая исполняемый код, процессо-специфичные данные (ввод и вывод), стек вызовов, и т.н. “кучу”.
- Дескрипторы ресурсов, выделенных процессу операционной системой.
- Атрибуты безопасности, такие как владелец и разрешения.
- Состояние процессора (контекст), такое как значение регистров, адресация физической памяти, и т.п.
ОС хранит информацию о процессе в структурах, называемых Process Control Block (PCB, блок управления процессом).
Так же, в зависимости от ОС, процесс может иметь несколько потоков выполнения. Их можно себе представлять как “под-процессы”, индивидуальные задачи внутри процесса, каждая из которых имеет собственный контекст исполнения, но общую память и дескрипторы ресурсов.
В большинстве реализаций, поток исполнения является ресурсом процесса, но при этом имеет собственный блок управления, называемый Thread Control Block (как правило, несколько урезанный по сравнению с PCB).
Управление временем ЦП
Управление временем ЦП сводится к двум взаимосвязанным задачам:
- Обеспечение каждого процесса временем ЦП для нормальной работы
- Максимально эффективное использование времени процессора
Кроме процессов приложений, система обычно выполняет множество сервисных процессов, или демонов, как их иногда называют. Демоны могут выполнять самые разные функции, например, управлять сетью, или обеспечивать связь между процессами. Основное отличие от процессов приложений в том, что демоны не взаимодействуют непосредственно с пользователем, работающим с системой, а выполняются в фоновом режиме.
Исполнение процесса наиболее просто описывается в однозадачной системе:
- Процесс создается операционной системой
- Процесс выполняется
- Процесс завершается
Исполнение процесса в таком случае может быть приостановлено только для обработки прерываний.
Очевидная проблема однозадачного подхода – простой ЦП в случаях, когда активный процесс, скажем, ожидает ввода пользователя или устройства. Для более эффективного использования процессроного времени используется многозадачность.
Существует несколько подходов к реализации многозадачности, в частности:
- Кооперативная
- Вытесняющая
- Гибридная
Кооперативная многозадачность полагается на то, что текущий исполняемый процесс сообщит ОС о том, что готов освободить время ЦП для работы других процессов. До этого момента переключения процессов не происходит. В зависимости от ОС, от процесса так же может требоваться сохранить свой контекст выполнения до передачи управления ОС, и восстановить его, когда управление будет передано обратно. Очевидный минус такого подхода состоит в том, что программная ошибка (либо просто неаккуратно написанная программа) легко может блокировать работу всей ОС. Однако, этот подход может быть очень удобен при разработке приложений реального времени, когда критическая задача не должна быть прервана никакой другой.
Прямо противоположный подход – это вытесняющая многозадачность. В данном случае ОС полностью берет на себя задачу управления временем выполнения процессов, создавая с точки зрения программного интерфейса иллюзию однозадачной системы. Наиболее распространенный вариант реализации вытесняющей многозадачности состоит в регулярной генерации прерывания, переключающего систему в контекст ядра. Ядро системы сохраняет текущий контекст выполнения, загружает новый контекст выполнения и передает управление другому процессу. При этом, выполнение любой программы может быть прервано в любой момент. Этот подход приобрел известную популярность, поскольку, во-первых, сильно упрощает написание программ, поддерживающих многозадачность, а во-вторых гарантирует продолжение работы системы даже в случае программных ошибок, приводящих к “зависаниям”.
Различные гибридные подходы позволяют части процессов выполняться в кооперативном режиме, в то время как для всех остальных применяется вытесняющая многозадачность. Такие подходы чаще всего используются в многозадачных ОС, требующих режима реального времени для отдельных задач.
Для описания работы процесса в многозадачной системе, воспользуемся диаграммой состояний.
Различные реализации могут иметь различное число состояний процесса, однако сейчас наиболее распространенным является вариант пяти состояний, приведенных на рисунке:
ОС начинает выполнение процесса с составления PCB и загрузки образа исполняемого кода. Затем ОС переводит процесс в состояние ожидания. При очередной генерации прерывания переключения задачи, ОС загружает содержимое регистров и т.п. из PCB и переводит процесс в состояние выполнения. При следующем прерывании переключения задачи, будет загружен PCB другого процесса и т.д. Если процесс ожидает ввода пользователя1 или устройства, то он переключается в состояние “блокирован”, и сообщает ОС условия “разблокирования”. Когда эти условия выполняются, ОС автоматически переключает процесс в состояние ожидания. Блокированному процессу время не выделяется. При завершении, процесс устанавливает состояние “уничтожен”, и ОС при очередном переключении в контекст ядра, освобождает ресурсы, ссылки на которые есть в PCB и удаляет сам PCB.
Отдельно следует сказать о файле подкачки, или свопе. В целях экономии оперативной памяти, ОС может переносить память, выделенную процессу из ОЗУ в ПЗУ. Это относится только к процессам, которые находятся в ожидании или заблокированны. Тогда при очередном переключении на процесс, ОС должна загрузить память процесса в ОЗУ (возможно выгрузив память другого процесса в ПЗУ), затем загрузить PCB и только после этого передать управление самому процессу. Как не сложно догадаться, это не самая быстра операция. В современных ОС, память процесса может быть выгружена в своп по частям, что позволяет выносить в своп только редко используемые области памяти.
Существуют разные подходы к выделению времени процессам. Большинство современных ОС используют приоритезацию процессов, и выделяют время в первую очередь тем процессам, которые имеют более высокий приоритет. Особенно интересным становится вопрос выделения времени в многопроцессорных системах. Так, в частности, может быть испольвзован симметричный и асимметричный подходы.
В симметричном подходе, каждый процессор используестя по очереди всеми процессами: при очередном переключении процесса, ОС просто загружает процесс на следующий процессор.
При ассиметричном, процессы “привязываются” к процессорам, иногда эксклюзивно, иногда нет. В некоторых случаях, отдельный процессор выделяется под нужды ОС. Это нередко приводит к измеримому ускорению работы, поскольку процессор кэширует содержимое оперативной памяти, и при постоянном переключении процессов, кэш оказывается неактуальным, требуя обращения к основной оперативной памяти2.
С другой стороны, если какой-то из процессов, привязанных к процессору, требует много процессорного времени, это может плохо сказываться на работе других процессов, привязанных к этому процессору, в то время как при симметричном подходе, ОС автоматически сбалансирует распределение процессорного времени.
Большинство современных ОС используют гибридный подход: используется симметричное распределение времени с возможностью, при необходимости, привязки конкретных процессов к конкретным процессорам.
Есть старая шутка, что с точки зрения программиста, пользователь – это устройство, в ответ на любой запрос возвращающее случайный набор символов.↩︎
Это представляет гораздо меньшую проблему на системах с одним многоядерным процессором, поскольку часто ядра используют общий кэш (по крайней мере, на последнем уровне).↩︎