Operating Systems: Three Easy Pieces. Part 1: Intro (перевод)
Привет, Хабр! Хочу представить вашему вниманию серию статей-переводов одной интересный на мой взгляд литературы — OSTEP. В этом материале рассматривается достаточно глубоко работа unix-подобных операционных систем, а именно — работа с процессами, различными планировщиками, памятью и прочиними подобными компонентами, которые составляют современную ОС. Оригинал всех материалов вы можете посмотреть вот тут. Прошу учесть, что перевод выполнен непрофессионально (достаточно вольно), но надеюсь общий смысл я сохранил.
Лабораторные работы по данному предмету можно найти вот тут:
А еще можете заглядывать ко мне на канал в телеграм =)
Работа программы
Что же происходит когда работает какая-либо программа? Запущенная программ выполняет одну простую вещь — она исполняет инструкции. Каждую секунду миллионы и даже возможно миллиарды инструкций извлекаются процессором из оперативной памяти, в свою очередь он декодирует их (например, распознает к какому типу, принадлежат эти инструкции) и исполняет. Это могут быть сложение двух чисел, доступ к памяти, проверка условия, переход к функции и так далее. После окончания выполнения одной инструкции процессор переходит к выполнению другой. И так инструкция за инструкцией, они исполняются до тех пор, пока программа не завершится.
Данный пример естественно рассмотрен упрощенно — на самом деле для ускорения работы процессора современное железо позволяет исполнять инструкции вне очереди, просчитывать возможные результаты, выполнять инструкции одновременно и тому подобные ухищрения.
Фон-Неймановская модель вычисления
Описанная нами упрощенная форма работы похожа на Фон-Неймановскую модель вычислений. Фон-Нейман это один из пионеров компьютерных систем, также он один из авторов теории игр. Во время работы программы происходит еще куча других событий, работает множество других процессов и сторонней логики, основная цель которых — упрощение запуска, работы и обслуживания системы.
Существует набор программного обеспечения, который ответственен за простоту запуска программ (или даже позволяющий запускать несколько программ одновременно), он позволяет программам разделять одну и ту же память, а так же взаимодействовать с различными устройствами. Такой набор ПО (программного обеспечения) по сути и называют операционной системой и в его задачи входило отслеживание того чтобы система работала корректно и эффективно, а также обеспечение простоты управления этой системой.
Операционная система
Операционная система, сокращенно ОС — комплекс взаимосвязанных программ, предназначенных для управления ресурсами компьютера и организации взаимодействия пользователя с компьютером.
ОС добивается своей эффективности в первую очередь, через самую главную технику — технику виртуализации. ОС взаимодействует с физическим ресурсом (процессором, памятью, диском и тому подобные) и трансформирует его в более общую, с большими возможностями и более простую для использования форму самого себя. Поэтому для общего понимания, можно очень грубо сравнить операционную систему с виртуальной машиной.
Для того чтобы позволять пользователям давать команды операционной системе и таким образом использовать возможности виртуальной машины (такие как: запуск программы, выделение памяти, доступ к файлу и так далее), операционная система предоставляет некоторый интерфейс, называемый API (application programming interface) и к которому можно делать вызовы (call). Типичная операционная система дает возможность сделать сотни системных вызовов.
И наконец, поскольку виртуализация позволяет множеству программ работать (таким образом, совместно использовать CPU), и одновременно получать доступ к их инструкциям и данным (тем самым разделяя память), а также получать доступ к дискам (таким образом, совместно использовать устройства ввода-вывода), операционную систему еще называют менеджером ресурсов. Каждый процессор, диск и память это ресурс системы и таким образом одной из ролей операционной системы становится задача по управлению этими ресурсами, делая это эффективно, честно или, наоборот, в зависимости от задачи, для которой эта операционная система разработана.
Виртуализация CPU
Рассмотрим следующую программу:
(https://www.youtube.com/watch? v=zDwT5fUcki4&feature=youtu.be)
Она не выполняет каких-то особых действий, по сути всё что она делает — вызывает функцию spin(), задача которой циклическая проверка времени и возврат, после того как прошла одна секунда. Таким образом, она повторяет бесконечно строку, которую пользователь передал в качестве аргумента.
Запустим эту программу, и передадим ей аргументом символ «А». Результат получается не особо интересный — система просто выполняет программу, которая периодически выводит на экран символ «А».
Теперь попробуем вариант, когда запущено множество экземпляров одной и той же программы, но выводящие разные буквы, чтобы было понятнее. В этом случае, результат получится несколько иной. Не смотря на то, что у нас один процессор, программа выполняется одновременно. Как же так получается? А получается что операционная система, не без помощи возможностей оборудования, создает иллюзию. Иллюзию того, что в системе есть несколько виртуальных процессоров, превращая один физический процессор в теоретически бесконечное количество и тем самым позволяя, программам на вид выполняться одновременно. Такую иллюзию и называют Виртуализацией CPU.
Подобная картина порождает много вопросов, например, если несколько программ желают запуститься одновременно, то какая именно будет запущена? За этот вопрос отвечают «политики» ОС. Политики используются во многих местах ОС и отвечают на подобные вопросы, а так же являются базовыми механизмами, которые ОС воплощает. Отсюда и роль ОС как ресурсного менеджера.
Виртуализация памяти
Теперь давайте рассмотрим память. Физическая модель памяти в современных системах представляется как массив байт. Для чтения из памяти нужно указать адрес ячейки, чтобы получить к ней доступ. Чтобы записать или обновить данные нужно также указать данные и адрес ячейки, куда их записать.
Обращения к памяти происходит постоянно в процессе работы программы. Программа хранит в памяти всю ее структуру данных, и обращается к ней, выполняя различные инструкции. Инструкции между тем тоже хранятся в памяти, поэтому обращение к ней происходит также на каждый запрос к следующей инструкции.
Вызов malloc ()
Рассмотрим следующую программу, которая выделяет область памяти, используя вызов malloc () (https://youtu.be/jnlKRnoT1m0):
Программа делает несколько вещей. Во-первых, выделяет некоторый объем памяти (строка 7), затем выводит адрес выделенной ячейки (строка 9), записывает ноль в первый слот выделенной памяти. Далее программа входит в цикл, в котором инкрементирует значение, записанное в памяти по адресу в переменной «p». Также она выводит идентификатор процесса самого себя. Идентификатор процесса уникален для каждого запущенного процесса. Запустив же несколько копий, мы наткнемся на интересный результат: В первом случае, если не сделать ничего и просто запустить несколько копий, то адреса будут разными. Но это же не попадает под нашу теорию! Верно, поскольку в современных дистрибутивах включена по умолчанию функция рандомизации памяти. Если ее отключить, получим ожидаемый результат — адреса памяти у двух одновременно работающих программ будут совпадать.
В итоге получается, что две независимые программы работают со своими собственными приватными адресными пространствами, которые в свою очередь отображаются операционной системой в физической памяти. Поэтому использование адресов памяти в пределах одной программы никак не будет затрагивать другие и каждой программе кажется, что у нее собственный кусок физической памяти, целиком отданный в ее распоряжение. Реальность, однако, такова, что физическая память — разделяемый ресурс, управление которым осуществляет операционная система.
Согласованность
Еще одна из важных тем в рамках операционных систем — согласованность. Этот термин используется, когда речь идет о проблемах в системе, которые могут возникать при работе со многими вещами одновременно в пределах одной программы. Проблемы согласованности возникают даже в самой операционной системе. В предыдущих примерах с виртуализацией памяти и процессора, мы поняли, что ОС управляет многими вещами одновременно — запускает первый процесс, затем второй и так далее. Как оказалось, такое поведение может привести к некоторым проблемам. Так, например современные многопоточные программы испытывают такие трудности.
Рассмотрим следующую программу:
Программа в главной функции создает два потока, используя вызов Pthread_create (). В данном примере о потоке можно думать как о функции, запущенной в одном пространстве памяти рядом с другими функциями, причем количество запущенных одновременно функций явно более одной. В данном примере каждый поток стартует и выполняет функцию worker () которая в свою очередь просто инкрементирует переменную,.
Запустим эту программу с аргументом 1000. Как вы уже могли догадаться, результатом должно стать 2000, поскольку каждый поток инкрементировал переменную 1000 раз. Однако все не так просто. Попробуем запустить программу с числом повторений на порядок больше.
Подавая на вход число, например, 100000 мы ожидаем увидеть на выходе число 200000. Однако, запустив число 100000 несколько раз, мы не только не увидим правильный ответ, но и получим разные неправильные ответы. Разгадка кроется в том, что для увеличения числа требуется три операции — извлечение числа из памяти, инкрементация и затем запись числа обратно. Поскольку все эти инструкции не осуществляются атомарно (все одновременно), такие странные вещи могут происходить. Эта проблема называется в программировании race condition — состояние гонки. Когда неизвестные силы в неизвестный момент могут повлиять на выполнение каких-либо ваших операций.