Найти тему
Computer Science

Ostep 6 глава. Virtualization - Direct Execution перевод

Оглавление

Mechanism: Limited Direct Execution

Чтобы виртуализировать процессор, операционная система должна каким-то образом разделить физический процессор между многими заданиями, выполняемыми, казалось бы, одновременно. Основная идея проста: запустите один процесс на некоторое время, затем запустите другой и так далее. Благодаря такому распределению времени (time sharing) между процессами достигается виртуализация.

Однако при создании такого механизма виртуализации возникает несколько проблем. Во-первых, это производительность: как мы можем реализовать виртуализацию, не добавляя чрезмерных накладных расходов в систему? Во вторых, это контроль: как мы можем эффективно запускать процессы, сохраняя контроль над процессором? Контроль особенно важен для ОС, поскольку она отвечает за ресурсы; без контроля процесс может просто работать вечно и захватить машину или получить доступ к информации, доступ к которой ему не должен быть разрешен. Таким образом, получение высокой производительности при сохранении контроля является одной из центральных задач при построении операционной системы.

Проблема: как эффективно виртуализировать ЦП не теряя контроль над системой
ОС должна виртуализировать ЦП эффективным образом, сохраняя при этом контроль над системой. Для этого потребуется поддержка как аппаратного обеспечения, так и операционной системы. ОС часто использует разумную аппаратную поддержку, чтобы эффективно выполнять свою работу.

Basic Technique: Limited Direct Execution

Чтобы заставить программу работать так быстро, как можно было бы ожидать, неудивительно, что разработчики ОС придумали метод, который мы называем limited direct execution. Идея "direct execution" проста: просто запустите программу непосредственно на процессоре. Таким образом, когда ОС хочет запустить программу, она создает для нее запись в списке процессов, выделяет для нее некоторую память, загружает код программы в память (с диска), находит ее точку входа (т. е. основную процедуру или что-то подобное), переходит к ней и начинает запускать код пользователя. На рис. 6.1 показан этот базовый протокол прямого выполнения (direct execution) (пока без каких-либо ограничений), использующий обычный вызов и возврат для перехода к программе main(), а затем обратно в ядро.

Звучит просто, не так ли? Но этот подход порождает несколько проблем в нашем стремлении виртуализировать процессор. Первая простая: если мы просто запускаем программу, как ОС может убедиться в том, что программа не делает ничего, что мы не хотим, чтобы она делала, и при этом эффективно работает? Второй: когда мы запускаем процесс, как операционная система останавливает его и переключается на другой процесс, таким образом реализуя разделение времени (time sharing), необходимое для виртуализации процессора?

Отвечая на эти вопросы ниже, мы получим гораздо лучшее представление о том, что необходимо для виртуализации процессора. При разработке этих методов мы также увидим, откуда возникает “limited" часть названия; без ограничений на запуск программ ОС не будет контролировать ничего и, таким образом, будет “просто библиотекой” — очень печальное положение дел для начинающей операционной системы!

Problem № 1: Restricted Operations

Прямое выполнение (direct execution) имеет очевидное преимущество в том, что оно быстрое; программа изначально работает на аппаратном процессоре и, таким образом, выполняется так быстро, как можно было бы ожидать. Но запуск на процессоре создает проблему: Что делать, если процесс хочет выполнить какую-то ограниченную операцию, такую как выдача запроса ввода-вывода на диск или получение доступа к большему количеству системных ресурсов, таких как процессор или память?

СУТЬ: КАК ВЫПОЛНЯТЬ ОГРАНИЧЕННЫЕ ОПЕРАЦИИ (RESTRICTED OPERATIONS)
Процесс должен иметь возможность выполнять операции ввода-вывода и некоторые другие ограниченные операции, но без предоставления процессу полного контроля над системой. Как ОС и аппаратное обеспечение могут работать вместе для этого?
ОТДЕЛЬНО: ПОЧЕМУ СИСТЕМНЫЕ ВЫЗОВЫ ВЫГЛЯДЯТ КАК ВЫЗОВЫ ПРОЦЕДУР
Вы можете удивиться, почему вызовы системных функций, таких как open() или read(), выглядит так же, как обычный вызов процедуры в языке C, если это выглядит как вызов процедуры, каким образом система знает, что это системный вызов, и обрабатывает его верно? Причина проста: это вызов процедуры, но внутри этого вызова процедуры скрыта знаменитая инструкция trap. Более конкретно, когда вы вызываете open () (например), вы выполняете вызов процедуры в библиотеке C. В это вызове, будь то для open() или любого другого предоставленного системного вызова, библиотека использует соглашение о вызове ядра, чтобы поместить аргументы open() в хорошо известные (well known) места (например, в стек или в определенные регистры), помещает номер системного вызова также в хорошо известное место (well known) (опять же, в стек или регистр), а затем выполняет вышеупомянутую команду trap. Код в библиотеке после вызова trap распаковывает возвращаемые значения и возвращает управление программе, которая выдала системный вызов. Таким образом, части библиотеки C, выполняющие системные вызовы, кодируются вручную в ассемблере, так как они должны тщательно следовать соглашению, чтобы правильно обрабатывать Аргументы и возвращать значения, а также выполнять специфичную для аппаратного обеспечения инструкцию trap. И теперь вы знаете, почему вам лично не нужно писать ассемблерный код для ловушки в ОС; кто-то уже написал его для вас.

Одним из подходов было бы просто позволить любому процессу делать все, что он хочет, с точки зрения ввода-вывода и других связанных с ним операций. Однако это помешало бы созданию многих видов систем. Например, если мы хотим создать файловую систему, которая проверяет разрешения перед предоставлением доступа к файлу, мы не можем просто позволить любому пользовательскому процессу совершать операции ввода/вывода на диск; если бы мы это сделали, процесс мог бы просто прочитать или записать весь диск, и таким образом все средства защиты были бы потеряны.

Таким образом, наш подход состоит в том, чтобы ввести новый режим процессора, известный как пользовательский режим; код, который работает в пользовательском режиме, ограничен в том, что он может делать. Например, при работе в пользовательском режиме процесс не может совершать запросы ввода-вывода; это приведет к тому, что процессор вызовет исключение; тогда ОС, скорее всего, убьет процесс.

В противоположность пользовательскому режиму существует режим ядра, в котором работает операционная система (или ядро). В этом режиме код, который выполняется, может делать все, что ему нравится, включая привилегированные операции, такие как запросы ввода-вывода и выполнение всех типов ограниченных инструкций.

Однако у нас все еще остается проблема: что должен делать пользовательский процесс, когда он хочет выполнить какую-то привилегированную операцию, такую как чтение с диска? Для этого практически все современные аппаратные средства предоставляют возможность пользовательским программам выполнять системный вызов. Впервые появившиеся на древних машинах, таких как Atlas, системные вызовы позволяют ядру тщательно раскрывать определенные ключевые функциональные возможности пользовательским программам, таким как доступ к файловой системе, создание и уничтожение процессов, взаимодействие с другими процессами и выделение большего объема памяти.

СОВЕТ: ИСПОЛЬЗУЙТЕ ЗАЩИЩЕННУЮ ПЕРЕДАЧУ УПРАВЛЕНИЯ
Аппаратное обеспечение помогает ОС, предоставляя различные режимы выполнения. В пользовательском режиме приложения не имеют полного доступа к аппаратным ресурсам. В режиме ядра ОС имеет доступ ко всем ресурсам машины. Также предусмотрены специальные инструкции для процедуры trap в ядро и возврата из trap (return-from-trap) обратно в пользовательский режим программ, а также инструкции, которые позволяют ОС сообщить аппаратному обеспечению, где находится trap table в памяти

Большинство операционных систем предоставляют несколько сотен вызовов (Подробнее см. Стандарт POSIX); ранние системы Unix предоставляли более сжатый набор из примерно двадцати вызовов.

Чтобы выполнить системный вызов, программа должна выполнить специальную команду trap. Эта инструкция одновременно переходит в ядро и повышает уровень привилегий до режима ядра; оказавшись в ядре, система теперь может выполнять любые необходимые привилегированные операции (если это разрешено) и, таким образом, выполнять необходимую работу для вызывающего процесса. По завершении ОС вызывает специальную инструкцию return-from-trap, которая, как и следовало ожидать, возвращается в вызывающую пользовательскую программу, одновременно снижая уровень привилегий обратно в пользовательский режим.

Аппаратное обеспечение должно быть немного осторожным при выполнении trap, поскольку оно должно быть уверено, что сохранило достаточно регистров вызывающего абонента, чтобы иметь возможность правильно вернуться, когда ОС выдает команду return-from-trap. Например, на x86 процессор будет помещать счетчик программ, флаги и несколько других регистров в стек ядра (kernel stack) для каждого процесса; return-from-trap выталкивает эти значения из стека и возобновляет выполнение программы в режиме пользователя (Подробнее см. руководства Intel systems ). Другие аппаратные системы используют различные соглашения, но основные концепции схожи на разных платформах.

Есть одна важная деталь, оставленная вне этого обсуждения: как trap знает, какой код запускать внутри ОС? Очевидно, что вызывающий процесс не может указать адрес для перехода (как это было бы при вызове процедуры); это позволит программам перейти в любое место ядра, что явно является очень плохой идеей. Таким образом, ядро должно тщательно контролировать, какой код выполняется в trap.

Представьте себе, что вы переходите в код, чтобы получить доступ к файлу, но сразу после проверки прав доступа; на самом деле, вероятно, такая способность позволила бы хитрому программисту заставить ядро запускать произвольные кодовые последовательности. В общем, старайтесь избегать очень плохих идей, подобных этой.

Ядро делает это, заполняя trap table во время загрузки. Когда машина загружается, она делает это в привилегированном режиме (ядро) и, таким образом, может свободно настраивать аппаратное обеспечение машины по мере необходимости. Таким образом, одна из первых вещей, которую делает ОС, - это сообщает аппаратному обеспечению, какой код следует запускать при возникновении определенных исключительных событий. Например, какой код должен выполняться, когда происходит прерывание жесткого диска, когда происходит прерывание клавиатуры или когда программа выполняет системный вызов? ОС информирует аппаратное обеспечение о местоположении этих обработчиков ловушек (trap handlers), как правило, с помощью какой-то специальной инструкции. Как только аппаратное обеспечение получает информацию, оно запоминает местоположение этих обработчиков до следующей перезагрузки машины, и, таким образом, аппаратное обеспечение знает, что делать (т. е. к какому коду перейти), когда происходят системные вызовы и другие исключительные события.

СОВЕТ: БУДЬТЕ ОСТОРОЖНЫ С ПОЛЬЗОВАТЕЛЬСКИМ ВВОДОМ В ЗАЩИЩЕННЫХ СИСТЕМАХ
Несмотря на то, что мы приложили большие усилия для защиты ОС во время системных вызовов (добавив механизм аппаратного захвата (trapping mechanism) и обеспечив маршрутизацию всех вызовов к ОС через него), есть еще много других аспектов реализации безопасной операционной системы, которые мы должны рассмотреть. Одним из них является обработка аргументов на границе системного вызова; ОС должна проверить, что передает пользователь, и убедиться, что аргументы правильно указаны, или отклонить вызов.
Например, при системном вызове write() пользователь указывает адрес буфера в качестве источника вызова write. Если пользователь (случайно или злонамеренно) передает “плохой” адрес (например, тот, который находится внутри части адресного пространства ядра), ОС должна обнаружить это и отклонить вызов. В противном случае пользователь мог бы прочитать всю память ядра; учитывая, что память ядра (виртуальная) также обычно включает в себя всю физическую память системы, этот небольшой промах позволил бы программе прочитать память любого другого процесса в системе.
В общем, защищенная система должна относиться к вводимым пользователем данным с большим подозрением. Иной подход несомненно, приведет к легкому взлому программного обеспечения, отчаянному чувству, что мир-опасное и страшное место, и потере работы для слишком доверчивого разработчика ОС.

Чтобы указать точный системный вызов, каждому системному вызову обычно присваивается номер системного вызова (system-call number). Таким образом, код пользователя отвечает за размещение нужного номера системного вызова в регистре или в определенном месте стека; ОС, обрабатывая системный вызов внутри обработчика ловушки, проверяет этот номер, проверяет его правильность и, если это так, выполняет соответствующий код. Этот уровень косвенности служит формой защиты; код пользователя не может указать точный адрес для перехода, а скорее должен запросить определённый сервис используя его номер (system-call number).

И последнее: возможность выполнить инструкцию, чтобы сообщить аппаратному обеспечению, где находятся trap tables, - это очень мощная возможность. Таким образом, как вы могли догадаться, это также привилегированная операция. Если вы попытаетесь выполнить эту инструкцию в пользовательском режиме, аппаратное обеспечение не позволит вам, и вы, вероятно, можете догадаться, что произойдет (подсказка: adios, оскорбляющая программа). Подумайте: какие ужасные вещи вы могли бы сделать с системой, если бы могли установить свою собственную таблицу ловушек? Могли бы вы взять на себя управление машиной?

Временная шкала (со временем, увеличивающимся вниз, на рис.6.2) суммирует протокол. Мы предполагаем, что каждый процесс имеет стек ядра, в котором регистры (включая регистры общего назначения и счетчик программ) сохраняются и восстанавливаются (аппаратным обеспечением) при переходе в ядро и из него.

Протокол ограниченного прямого исполнения (LDE) состоит из двух фаз. В первом случае (во время загрузки) ядро инициализирует таблицу ловушек, и процессор запоминает ее местоположение для последующего использования. Ядро делает это с помощью привилегированной инструкции (все привилегированные инструкции выделены жирным шрифтом).

Во втором (при запуске процесса) ядро настраивает несколько вещей (например, выделение узла в списке процессов, выделение памяти) перед использованием команды return from-trap для запуска выполнения процесса; это переключает процессор в пользовательский режим и начинает выполнение процесса. Когда процесс хочет выполнить системный вызов, он переходит в режим ядра, где обрабатывает его и снова возвращает управление через return from trap процессу. Затем процесс завершает свою работу и возвращается из main(); это обычно возвращается в какой-то код-заглушку, который правильно выйдет из программы (например, вызвав системный вызов exit (), который переходит в режим ядра). На этом этапе ОС очищается, и мы закончили.

Problem № 2: Switching Between Processes

Следующая проблема с direct execution заключается в переключении между процессами. Переключение между процессами должно быть простым, не так ли? ОС должна просто решить остановить один процесс и запустить другой. Что тут такого? Но на самом деле это немного сложно: в частности, если процесс работает на процессоре, это по определению означает, что ОС не работает. Если ОС не работает, как она вообще может что-то делать? (подсказка: не может) Хотя это звучит почти философски, это реальная проблема: очевидно, что ОС не может предпринять никаких действий, если она не работает на процессоре. Таким образом, мы подходим к сути проблемы.

СУТЬ: КАК ВОССТАНОВИТЬ КОНТРОЛЬ НАД ПРОЦЕССОРОМ
Как операционная система может восстановить контроль над процессором, чтобы он мог переключаться между процессами?

Совместный Подход: Ожидание Системных Вызовов

Один из подходов, который некоторые системы использовали в прошлом (например, ранние версии операционной системы Macintosh или старая система Xerox Alto), известен как кооперативный подход. В этом стиле ОС доверяет процессам системы вести себя разумно. Предполагается, что процессы, которые работают слишком долго, периодически отказываются от процессора, чтобы ОС могла решить выполнить какую-то другую задачу.

Таким образом, вы можете спросить, как дружественный процесс отказывается от процессора в этом утопическом мире? Большинство процессов, как выясняется, довольно часто передают управление процессором ОС, совершая системные вызовы, например, чтобы открыть файл и впоследствии прочитать его, или отправить сообщение на другую машину, или создать новый процесс. Такие системы часто включают в себя явный системный вызов yield, который ничего не делает, кроме передачи управления ОС, чтобы она могла запускать другие процессы.

Приложения также передают управление ОС, когда они делают что-то незаконное. Например, если приложение совершает деление на ноль или пытается получить доступ к памяти, к которой оно не должно иметь доступа, оно сгенерирует trap для ОС. Затем ОС снова получит контроль над процессором (и, скорее всего, завершит работу нарушителя).

Таким образом, в системе в которой применяется подход cooperative sheduling ОС восстанавливает контроль над процессором, ожидая системного вызова или какой-либо незаконной операции. Вы также можете подумать: не является ли этот пассивный подход менее идеальным? Что произойдет, например, если процесс (злонамеренный или просто полный ошибок) окажется в бесконечном цикле и никогда не выполнит системный вызов? Что же тогда может сделать ОС?

Некооперативный подход: ОС берет управление на себя

Без какой-либо дополнительной помощи со стороны аппаратного обеспечения оказывается, что ОС вообще ничего не может сделать, когда процесс отказывается выполнять системные вызовы (или ошибки) и, таким образом, возвращает управление ОС. В самом деле, при кооперативном подходе единственный выход, когда процесс застревает в бесконечном цикле, - это прибегнуть к извечному решению всех проблем в компьютерных системах: перезагрузить машину. Таким образом, мы снова приходим к подзадаче нашего общего стремления получить контроль над процессором.

СУТЬ: КАК ПОЛУЧИТЬ КОНТРОЛЬ БЕЗ СОТРУДНИЧЕСТВА
Как ОС может получить контроль над процессором, даже если процессы не сотрудничают? Что может сделать ОС, чтобы гарантировать, что мошеннический процесс не захватит машину?

Ответ оказывается простым и был открыт многими людьми, строившими компьютерные системы много лет назад: прерывание таймера. Устройство таймера может быть запрограммировано так, чтобы вызывать прерывание каждые столько-то миллисекунд; когда прерывание вызывается, текущий запущенный процесс останавливается, и запускается предварительно настроенный обработчик прерываний в ОС. В этот момент ОС восстановила контроль над процессором и, таким образом, может делать все, что ей заблагорассудится: остановить текущий процесс и запустить другой.

Как мы уже обсуждали ранее с системными вызовами, ОС должна информировать аппаратное обеспечение о том, какой код должен выполняться, когда происходит прерывание таймера; таким образом, во время загрузки ОС делает именно это. Во-вторых, также во время загрузки ОС должна запустить таймер, что, конечно же, является привилегированной операцией. После запуска таймера ОС может чувствовать себя в безопасности в том, что управление в конечном итоге будет возвращено ей, и, таким образом, ОС может свободно запускать пользовательские программы. Таймер также может быть отключен (также привилегированная операция), что мы обсудим позже, когда более подробно разберемся в параллелизме.

Обратите внимание, что аппаратное обеспечение несет определенную ответственность при возникновении прерывания, в частности, за сохранение состояния программы, которая была запущена в момент прерывания, чтобы последующая инструкция return-from-trap могла правильно возобновить работу запущенной программы. Этот набор действий очень похож на поведение аппаратного обеспечения во время вызова trap и перехода в режим ядра, при этом различные регистры, таким образом, сохраняются (например, в стек ядра) и, таким образом, легко восстанавливаются инструкцией return-from-trap.

Сохранение и восстановление контекста

Теперь, когда ОС восстановила контроль, будь то совместно с помощью системного вызова или более решительно с помощью прерывания таймера, необходимо принять решение: продолжать ли запускать текущий процесс или переключиться на другой. Это решение принимается частью операционной системы, известной как планировщик; мы подробно обсудим политику планирования в следующих нескольких главах.

СОВЕТ: РАБОТА С НЕПРАВИЛЬНЫМ ПОВЕДЕНИЕМ ПРИЛОЖЕНИЯ
Операционным системам часто приходится иметь дело с неправильными процессами, которые либо по замыслу (злонамеренность), либо случайно (ошибки) пытаются сделать что-то, чего делать не следует. В современных системах способ, которым ОС пытается справиться с такими преступлениями, заключается в том, чтобы просто уничтожить нарушителя. Один удар-и ты выбыл! Возможно, жестоко, но что еще должна делать ОС, когда вы пытаетесь незаконно получить доступ к памяти или выполнить незаконную инструкцию?

Если принято решение о переключении, ОС затем выполняет низкоуровневый фрагмент кода, который мы называем контекстным переключателем(context switch). Переключение контекста концептуально просто: все, что ОС должна сделать, это сохранить несколько значений регистров для текущего процесса (например, в свой стек ядра) и восстановить несколько для будущего процесса (из своего стека ядра). Поступая таким образом, операционная система гарантирует, что, когда инструкция return-from-trap исполнится, вместо того чтобы вернуться к процессу, который был запущен, система возобновляет выполнение другого процесса.

Чтобы сохранить контекст текущего запущенного процесса, ОС выполнит некоторый низкоуровневый ассемблерный код для сохранения регистров общего назначения, PC и указателя стека ядра текущего запущенного процесса, а затем восстановит указанные регистры, PC и переключится на стек ядра для предстоящего выполнения процесса. Переключая стеки, ядро вводит вызов switch code в контексте одного процесса (того, который был прерван) и возвращается в контексте другого (того, который скоро будет выполнен). Когда ОС, наконец, выполняет команду return-from trap, процесс, который скоро будет выполнен, становится текущим процессом. И таким образом переключение контекста завершено.

СОВЕТ: ИСПОЛЬЗУЙТЕ ПРЕРЫВАНИЕ ТАЙМЕРА, ЧТОБЫ ВОССТАНОВИТЬ КОНТРОЛЬ
Добавление прерывания таймера дает ОС возможность снова работать на процессоре, даже если процессы действуют некооперативно. Таким образом, эта аппаратная функция имеет важное значение для того, чтобы помочь ОС поддерживать контроль над машиной.
Ранее мы отмечали, что единственным решением для бесконечных циклов (и подобных поведений) при кооперативном упреждении является перезагрузка машины. Хотя вы можете насмехаться над этим взломом, исследователи показали, что перезагрузка (или вообще запуск какой-то части программного обеспечения) может быть чрезвычайно полезным инструментом для создания надежных систем. В частности, перезагрузка полезна, потому что она возвращает программное обеспечение в известное и, вероятно, более проверенное состояние. Перезагрузка также восстанавливает устаревшие или утекшие ресурсы (например, память), с которыми в противном случае может быть трудно справиться. Наконец, перезагрузку легко автоматизировать. По всем этим причинам в крупномасштабных кластерных интернет-сервисах для программного обеспечения системного управления нередко приходится периодически перезагружать наборы машин, чтобы сбросить их и таким образом получить перечисленные выше преимущества. Таким образом, в следующий раз, когда вы перезагрузитесь, вы не просто выполняете какой-то уродливый хак. Скорее, вы используете проверенный временем подход к улучшению поведения компьютерной системы.

График всего процесса показан на рис. 6.3. В этом примере процесс А выполняется, а затем прерывается прерыванием таймера. Аппаратное обеспечение сохраняет свои регистры (в свой стек ядра) и входит в ядро (переключаясь в режим ядра). В обработчике прерываний таймера ОС решает переключиться с запущенного процесса А на процесс В. В этот момент он вызывает процедуру switch (), которая тщательно сохраняет текущие значения регистров (в структуру процесса A), восстанавливает регистры процесса B (из его записи структуры процесса), а затем переключает контексты, в частности, изменяя указатель стека, чтобы использовать стек ядра B (а не A). Наконец, ОС выполняет return-from-trap, который восстанавливает регистры B и запускает его.

рис. 6.3
рис. 6.3

Обратите внимание, что существует два типа сохранения/восстановления регистров, которые происходят во время выполнения этого протокола. Первый - это когда происходит прерывание таймера; в этом случае пользовательские регистры запущенного процесса неявно сохраняются аппаратным обеспечением, используя стек ядра этого процесса. Во-вторых, когда ОС решает переключиться с A на B; в этом случае регистры ядра явно сохраняются программным обеспечением (то есть ОС), но на этот раз в память в структуре процесса. Последний шаг переводит систему из режима работы "режим ядра из процесса А" в режим работы "режим ядра из процесса В".

Чтобы дать вам лучшее представление о том, как осуществляется такой переключатель, на рис.6.4 показан код контекстного переключателя для xv6. Посмотрите, сможете ли вы разобраться в этом (для этого вам нужно немного знать x86, а также немного xv6). Структура контекста старого и нового процессов находятся в old и new соответственно.

рис. 6.4
рис. 6.4

Беспокоитесь О Параллелизме?

Некоторые из вас, как внимательные и вдумчивые читатели, возможно, сейчас думают: "Хм... Что происходит, когда во время системного вызова происходит прерывание таймера?” или “что происходит, когда вы обрабатываете одно прерывание, а другое происходит? Разве это не становится трудным для обработки в ядре?” Хорошие вопросы — у нас действительно есть еще какая-то надежда на вас!

Ответ-да, ОС действительно должна быть обеспокоена тем, что произойдет, если во время обработки прерывания или ловушки произойдет другое прерывание. Собственно, это и есть тема всей второй части этой книги, посвященной параллелизму; мы отложим подробное обсуждение до тех пор.

Чтобы подогреть ваш аппетит, мы просто набросаем некоторые основы того, как ОС справляется с этими сложными ситуациями. Одна простая вещь, которую может сделать ОС, - это отключить прерывания во время обработки прерываний; это гарантирует, что при обработке одного прерывания ни одно другое не будет доставлено в процессор. Конечно, ОС должна быть осторожна при этом; отключение прерываний слишком долго может привести к потере прерываний, что (с технической точки зрения) плохо.

Операционные системы также разработали ряд сложных схем блокировки для защиты параллельного доступа к внутренним структурам данных. Это позволяет одновременно выполнять несколько действий в ядре, что особенно полезно для мультипроцессоров. Однако, как мы увидим в следующей части этой книги о параллелизме, такая блокировка может быть сложной и приводить к множеству интересных и труднодоступных ошибок.

Резюме

Мы описали некоторые ключевые низкоуровневые механизмы для реализации виртуализации ЦП, набор методов, которые мы все вместе называем ограниченным прямым исполнением. Основная идея проста: просто запустите программу, которую вы хотите запустить на процессоре, но сначала убедитесь, что настроили аппаратное обеспечение так, чтобы ограничить то, что процесс может сделать без помощи ОС.

СКОЛЬКО ВРЕМЕНИ ЗАНИМАЕТ ПЕРЕКЛЮЧЕНИЕ КОНТЕКСТА
У вас может возникнуть естественный вопрос: сколько времени занимает что-то вроде переключения контекста? Или даже системный вызов? Для тех из вас, кому интересно, есть инструмент под названием lmbench, который измеряет именно эти вещи, а также несколько других показателей производительности, которые могут иметь отношение к делу. Результаты со временем значительно улучшились. Например, в 1996 году под управлением Linux 1.3.37 на 200-МГц процессоре P6 системные вызовы занимали примерно 4 микросекунды, а переключение контекста-примерно 6 микросекунд. Современные системы работают почти на порядок лучше, с субмикросекундными результатами на системах с 2-или 3-ГГц процессорами. Следует отметить, что не все действия операционной системы отслеживают производительность процессора. Как заметил Оустерхаут, многие операции ОС требуют большого объема памяти, и пропускная способность памяти со временем не улучшилась так резко, как скорость процессора [O90]. Таким образом, в зависимости от вашей рабочей нагрузки покупка новейшего и самого мощного процессора может не ускорить работу вашей ОС так сильно, как вы могли бы надеяться.

Этот общий подход применяется и в реальной жизни. Например, те из вас, у кого есть дети или, по крайней мере, слышали о детях, могут быть знакомы с концепцией детской защиты комнаты: запирание шкафов, содержащих опасные вещи, и прикрытие электрических розеток. Когда комната таким образом подготовлена, вы можете позволить своему ребенку свободно бродить, зная, что самые опасные аспекты комнаты были ограничены.

Аналогичным образом ОС “проверяет " процессор, сначала (во время загрузки) настраивая обработчики ловушек (trap handlers) и запуская таймер прерывания, а затем только запуская процессы в ограниченном режиме. Таким образом, ОС может чувствовать себя вполне уверенной в том, что процессы могут работать эффективно, только требуя вмешательства ОС для выполнения привилегированных операций или когда они монополизировали процессор слишком долго и поэтому должны быть отключены.

Таким образом, у нас есть основные механизмы для виртуализации процессора. Но главный вопрос остается без ответа: какой процесс мы должны запустить в данный момент? Именно на этот вопрос должен ответить планировщик, а значит, и следующая тема нашего исследования.

КЛЮЧЕВЫЕ ТЕРМИНЫ ВИРТУАЛИЗАЦИИ ЦП (МЕХАНИЗМЫ)
* Процессор должен поддерживать по крайней мере два режима выполнения: режим ограниченного пользователя и привилегированный (не ограниченный) режим ядра.
* Типичные пользовательские приложения работают в пользовательском режиме и используют системный вызов для захвата ядра для запроса служб операционной системы.
* Инструкция trap тщательно сохраняет состояние регистра, изменяет состояние оборудования в режим ядра и переходит в ОС к заранее заданному месту назначения: trap table.
• Когда ОС завершает обслуживание системного вызова, она возвращается к пользовательской программе с помощью другой специальной команды возврата из ловушки (return-from-trap), которая уменьшает привилегии и возвращает управление команде после ловушки, которая прыгнула в ОС.
* trap tables должны быть настроены ОС во время загрузки и нужно быть уверенным в том, что они не могут быть легко изменены пользовательскими программами. Все это является частью ограниченного протокола прямого выполнения, который запускает программы эффективно, но без потери контроля ОС.
* После запуска программы ОС должна использовать аппаратные механизмы, гарантирующие, что пользовательская программа не будет работать вечно, а именно прерывание таймера. Этот подход является некооперативным подходом к планированию ЦП.
* Иногда ОС во время прерывания таймера или системного вызова может захотеть переключиться с запуска текущего процесса на другой, низкоуровневый метод, известный как переключение контекста.