Operating Systems: Three Easy Pieces. Part 2: Абстракция: Процесс (перевод)
Введение в операционные системы
Привет, Хабр! Хочу представить вашему вниманию серию статей-переводов одной интересной на мой взгляд литературы — OSTEP. В этом материале рассматривается достаточно глубоко работа unix-подобных операционных систем, а именно — работа с процессами, различными планировщиками, памятью и прочиими подобными компонентами, которые составляют современную ОС. Оригинал всех материалов вы можете посмотреть вот тут. Прошу учесть, что перевод выполнен непрофессионально (достаточно вольно), но надеюсь общий смысл я сохранил.
Лабораторные работы по данному предмету можно найти вот тут:
А еще можете заглядывать ко мне на канал в телеграм =)
Рассмотрим наиболее фундаментальную абстракцию, которую ОС предоставляет пользователям: процесс. Определение процесса довольно-таки просто — это работающая программа. Программа сама по себе является безжизненной вещью, располагающейся на диске — это набор инструкций и возможно каких-то статических данных, ожидающих момента запуска. Именно ОС берет эти байты и запускает их, преобразую программу во что-то полезное.
Чаще всего пользователи хотят запускать более одной программы одновременно, например вы можете запустить на вашем ноутбуке браузер, игру, медиаплеер, текстовый редактор и тому подобное. Фактически типичная система может запускать десятки и сотни процессов одновременно. Этот факт делает систему более простой в использовании, вам никогда не приходится беспокоиться о том, свободен ли CPU, вы просто запускаете программы.
Отсюда вытекает проблема: как обеспечить иллюзию множества CPU? Как ОС создать иллюзию практически бесконечного количества CPU, даже если у вас всего один физический CPU?
ОС создает эту иллюзию посредством виртуализации CPU. Запуская один процесс, затем останавливая его, запуская другой процесс и так далее, ОС может поддерживать иллюзию того, что существует множество виртуальных CPU, хотя фактически это будет один или несколько физических процессоров. Такая техника называется разделение ресурсов CPU по времени. Эта техника позволяет пользователям запускать столько одновременных процессов, сколько они пожелают. Ценою такого решения является производительность – поскольку если CPU делят несколько процессов, каждый процесс будет обрабатываться медленнее.
Для воплощения виртуализации CPU, а особенно для того чтобы делать это хорошо, ОС нуждается и в низкоуровневой и в высокоуровневой поддержке. Низкоуровневая поддержка называется механизмами — это низкоуровневые методы или протоколы, которые реализуют нужную часть функционала. Пример такого функционала — контекстное переключение, которое дает ОС возможность останавливать одну программу и запускать на процессоре другую программу. Такое разделение по времени реализовано во всех современных ОС.
На вершине этих механизмов располагается некоторая логика, заложенная в ОС, в форме “политик”. Политика — это некоторый алгоритм принятия решения операционной системой. Такие политики, например, решают, какую программу надо запускать (из списка команд) в первую очередь. Так, например, данную задачу решит политика, называющаяся планировщик (scheduling policy) и при выборе решения будет руководствоваться такими данными как: история запуска (какая программа была запущена дольше всех за последнюю минут), какую нагрузку осуществляет данный процесс (какие типы программ были запущены), метрики производительности (оптимизирована ли система для интерактивного взаимодействия или для пропускной способности) и так далее.
Абстракция: процесс
Абстракция работающей программы, выполняемая операционной системой это то, что мы называем процесс. Как уже было сказано ранее процесс – это просто работающая программа, в любой моментальный промежуток времени. Программа с помощью которой мы можем получить суммарную информацию с различных ресурсов системы, и к которым обращается или которые эта программа затрагивает в процессе своего выполнения.
Для понимания составляющих процесса нужно понимать состояния системы: что программа может считывать или изменять во время своей работы. В любой момент времени нужно понимать, какие элементы системы важны для выполнения программы.
Одним из очевидных элементов состояния системы, которые включает в себя процесс — это память. Инструкции располагаются в памяти. Данные, которые программа читает или пишет также, располагаются в памяти. Таким образом, память, которую процесс может адресовать (так называемое адресное пространство) является частью процесса.
Также частью состояния системы являются регистры. Множество инструкций направлено на то, чтобы изменить значение регистров или прочитать их значение и таким образом регистры тоже становятся важной частью работы процесса.
Следует отметить, что состояние машины формируется также из некоторых специальных регистров. Например, IP — instruction pointer — указатель на инструкцию, которую программа исполняет в текущий момент. Еще есть stack pointer и связанный с ним frame pointer, которые используются для управления: параметрами функций, локальными переменными и адресами возврата.
Наконец, программы часто обращаются к ПЗУ (постоянному запоминающему устройству). Такая информация о “I/O” (вводе-выводе) должна включать в себя список файлов, открытых процессом в данный момент.
Process API
Для того чтобы улучшить понимания работы процесса изучим примеры системных вызовов, которые должны быть включены в любой интерфейс операционной системы. Эти API в том или ином виде доступны на любой ОС.
● Create (создание): В ОС должен входить какой либо метод, позволяющий создавать новый процессы. Когда вы вводите команду в терминал или запускаете приложение через двойной клик по иконке, посылается обращение к ОС для создания нового процесса и последующего запуска указанной программы.
● Удаление: Раз есть интерфейс для создания процесса, ОС также должна предоставлять возможность для принудительного удаления процесса. Большинство программ естественно будут запущены и завершены сами по себе по мере их выполнения. В противном случае пользователь хотел бы иметь возможность убить их и таким образом интерфейс для остановки процесса будет не лишним.
● Wait (ожидание): Иногда полезно дождаться завершения процесса, поэтому обеспечиваются некоторые интерфейсы предоставляющие возможностью ожидания.
● Misc Control (разное управление): Кроме убийства и ожидания процесса еще существуют другие разнообразные контролирующие методы. Например большинство ОС предоставляют возможность заморозки процесса (остановка его выполнения на некоторый период) и последующее возобновление (продолжение исполнения)
● Status (состояние): Существуют различные интерфейсы для получения некоторой информации он статусе процесса, такие как продолжительность его работы или в каком состоянии он сейчас находится.
Создание процесса: детали
Одна из интересных вещей — как же именно программы трансформируются в процессы. Особенно, как ОС поднимает и запускает программу. Как конкретно создается процесс.
В первую очередь ОС должна загрузить код программы и статические данные в память (в адресное пространство процесса). Программы обычно располагаются на диске или твердотельном накопителе в некотором исполняемом формате. Таким образом, процесс загрузки программы и статических данных в память требует от ОС возможности прочитать эти байты с диска и расположить их где-то в памяти.
В ранних ОС процесс загрузки выполнялся нетерпеливо (eagerly), то есть это значит что код загружался в память целиком до того как программа запускалась. Современные ОС делают это лениво (lazily), то есть загружая кусочки кода или данных только тогда, когда они требуются программе во время ее выполнения.
После того как код и статические данные загружены в память ОС нужно выполнить еще несколько вещей перед тем как запустить процесс. Некоторое количество памяти должно быть выделено под стек. Программы используют стек для локальных переменных, параметров функций и адресов возврата. ОС выделяет эту память и отдает ее процессу. Стек также может выделяться с некоторыми аргументами, конкретно она заполняет параметры функции main(), например массивом argc и argv.
Операционная система может также выделять некоторое количество памяти под кучу (heap) программы. Куча используется программами для явно запрашиваемых динамически выделенных данных. Программы запрашивают это пространство, вызывая функцию malloc() и явно очищает, вызывая функцию free(). Куча нужна для таких структур данных как: связанные листы, таблицы хэшей, деревья и другие. По началу под кучу выделяется маленькое количество памяти, но со временем в процессе работы программы куча может запросить большее количество памяти, через библиотечный API вызов malloc(). Операционная система вовлечена в процесс выделения большего количества памяти для того, чтобы помочь в удовлетворении этих вызовов.
Операционная система также будет выполнять задачи инициализации, в частности те, которые относятся к вводу-выводу. Например, в системах UNIX каждый процесс по умолчанию имеет 3 открытых файловых дескриптора, для стандартного потока ввода, вывода и ошибок. Эти дескрипторы позволяют программам считывать ввод из терминала, а также выводить информацию на экран.
Таким образом, загружая код и статические данные в память, создавая и инициализируя стек, а также выполняя другую работу, относящуюся к выполнению задач ввода-вывода, ОС подготавливает площадку для выполнения процесса. В конце концов, остается последняя задача: запустить на исполнение программу через ее точку ввода, называемую функцией main(). Переходя к выполнению функции main(), ОС передает управление CPU вновь созданному процессу, таким образом, программа начинает исполняться.
Состояние процесса
Теперь, когда у нас есть некоторое понимание, что такое процесс и как он создается, перечислим состояния процесса, в которых он может находиться. В самой простой форме процесс может находиться в одном из этих состояний:
● Running. В запущенном состоянии процесс выполняется на процессоре. Это значит, что происходит выполнение инструкций.
● Ready. В состоянии готовности процесс готов запуститься, но по каким-то причинам ОС не исполняет его в заданный момент времени.
● Blocked. В заблокированном состоянии процесс выполняет какие-то операции, которые не дают ему быть готовым к исполнению до тех пор, пока не произойдет какое-либо событие. Один из обычных примеров — когда процесс инициализирует операцию IO, он становится заблокированным и таким образом какой-то другой процесс может использовать процессор.
Представить себе эти состояния можно в виде графа. Как мы можем видеть на картинке, состояние процесса может меняться между RUNNING и READY на усмотрение ОС. Когда состояние процесса меняется с READY, на RUNNING это означает, что процесс был запланирован. В обратную сторону — снят с планировки. В момент, когда процесс становится BLOCKED, например, инициализирую операцию IO, ОС будет держать его в этом состоянии до наступления некоторого события, например завершение IO. в этот момент переход в состояние READY и возможно моментально в состояние RUNNING, если так решит ОС.
Давайте взглянем на пример того, как два процесса проходят через эти состояния. Для начала представим, что оба процесса запущены, и каждый использует только CPU. В этом случае, их состояния будут выглядеть следующим образом.
В следующем примере первый процесс через некоторое время работы запрашивает IO и переходит в состояние BLOCKED, предоставляя другому процессу возможность запуска (РИС 1.4). ОС видит, что процесс 0 не использует CPU и запускает процесс 1. Во время выполнения процесса 1 — IO завершается и статус процесса 0 меняется на READY. Наконец процесс 1 завершился, а по его окончание процесс 0 запускается, исполняется и заканчивает свою работу.
Структура данных
ОС сама является программой, и также как и любая другая программа имеет некоторые ключевые структуры данных, которые отслеживают разнообразные релевантные куски информации. Для отслеживания состояния каждого процесса в ОС будет поддерживаться некоторый process list для всех процессов в состоянии READY и некоторую дополнительную информацию для отслеживания процессов, которые выполняются в текущий момент. Также, ОС должна отслеживать и заблокированные процессы. После завершения IO, ОС обязана пробудить нужный процесс и перевести его в состояние готовности к запуску.
Так, например, ОС должна сохранить состояние регистров процессора. В момент остановки процесса состояние регистров сохраняется в адресном пространстве процесса, а в момент продолжения его работы — восстановить значения регистров и таким образом продолжить выполнения этого процесса.
Кроме состояний ready, blocked, running существуют еще некоторые другие состояния. Иногда в момент создания процесс может иметь состояние INIT. Наконец процесс может быть помещен состояние FINAL, когда он уже завершился, но информация о нем еще не вычищена. В UNIX системах такое состояние называется процесс-зомби. Это состояние полезно для случаев когда родительский процесс хочет узнать код возврата потомка, например, обычно 0 сигнализирует об успешном завершении, а 1 об ошибочном, однако программисты могут делать дополнительные коды вывода, сигнализируя о разных проблемах. При завершении процесс-родитель делает последний системный вызов, например wait(), чтобы дождаться завершения работы процесса-потомка и просигнализировать ОС о том, что можно очистить любые данные, связанные с завершенным процессом.
Ключевые моменты лекции:
● Процесс — главная абстракция работающей программы в ОС. В любой момент времени процесс может быть описан по его состоянию: содержимое памяти в его адресном пространстве, содержимое регистров процессора, включая instruction pointer и stack pointer также информацией о IO, например открытыми файлами, которые считываются или записываются.
● Process API состоит из вызовов, которые программы могут делать в отношении процессов. Обычно это вызовы создания, удаления или другие.
● Процесс находится в одном из множества состояний, включая running, ready, blocked. Различные события, такие как планировка, исключение из планировки или ожидания могут переводить состояние процесса из одного в другое.
● Process list содержит информацию обо всех процессах в системе. Каждая запись в ней называется process control block, которая в реальности является структурой, которая содержит всю необходимую информацию о конкретном процессе.