8. Как мой компьютер управляет процессами, чтобы они не нарушали работу друг друга?

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

Каждый процесс в вашем зоопарке нуждается в своей собственной области памяти, где будет храниться выполняемый код этой программы, переменные и результаты вычислений. Её можно представить как набор сегментов кода (code segment) (содержащих инструкции процессору для выполнения программы), из которых можно только читать; и сегментов данных (data segment) (хранилище переменных и данных процесса), в них можно осуществлять и запись и чтение. Сегменты данных действительно уникальны для каждого процесса, но если два процесса используют один и тот же программный код, Unix для эффективности позволяет им использовать единый сегмент кода.

8.1. Виртуальная память: простое объяснение

Эффективность важна, потому что память — дорогостоящий ресурс. Иногда вам может её не хватать, чтобы полностью разместить все запущенные на машине программы, особенно если вы используете большие программы, например, X-сервер. Чтобы обойти это, Unix использует технику, получившую название виртуальная память (virtual memory). Она не пытается держать весь код и данные процесса в памяти. Вместо этого она хранит только относительно небольшие рабочие наборы (working set); полностью код и данные процессов находятся в специальной области подкачки (swap space) на вашем жёстком диске.

Стоит отметить, что в прошлом, «Иногда», употреблённое в предыдущем абзаце, было «Почти всегда» — обычно размер памяти был меньше, чем размер выполняемых программ, так что подкачка с диска использовалась часто. В наши дни память гораздо менее дорогостоящая и даже машины нижней ценовой категории располагают ею в достаточном объёме. На современных однопользовательских машинах с объёмом памяти 64 Мб и выше, можно запускать X-сервер и делать типичную работу без обращений к области подкачки, после того как программы загружены в память.

8.2. Виртуальная память: разъяснение деталей

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

Существует пять типов памяти: регистры процессора, внутренний (on-chip) кэш процессора, внешний кэш (off-chip), основная память и жёсткий диск. Причина, благодаря которой они все существуют, проста: скорость стоит денег. Я перечислил эти типы памяти в порядке возрастания времени доступа (скорости работы) и в порядке убывания стоимости. Регистровая память быстрейшая и наиболее дорогая, произвольный доступ к ней может выполняться миллиард раз в секунду, в то время как память на жёстком диске самая медленная и дешёвая, и при произвольном доступе достигается скорость около 100 обращений в секунду.

Вот список скоростей начала 21 века, отражающий типичную настольную машину. В то время как скорости и объёмы меняются, а цены становятся ниже, пропорции между видами памяти остаются достаточно постоянными и очерчивают образ иерархии памяти.

Жёсткий диск

Размер: 13000 Мб Скорость доступа: 100 Кб/сек

Основная память

Размер: 256 Мб Скорость доступа: 100 Мб/сек

Внешний кэш

Размер: 512 Кб Скорость доступа: 250 Мб/сек

Внутренний кэш

Размер: 32 Кб Скорость доступа: 500 Мб/сек

Регистры процессора

Размер: 28 байт Скорость доступа: 1000 Мб/сек

Мы не можем построить всю систему с использованием только самой быстрой памяти. Это будет очень дорого — даже если это не так, быстрая память непостоянна, а именно, она теряет весь свой блеск и преимущества, когда мы выключаем компьютер. Поэтому компьютеры имеют жёсткие диски или другие устройства постоянного хранения данных, на которых те и содержатся после выключения питания. И здесь мы имеем огромное несоответствие между скоростью процессоров и жёстких дисков. Три средних уровня иерархии памяти: внутренний кэш (internal cache), внешний кэш (external cache) и основная память существуют, чтобы заполнить эту брешь.

Linux и другие Unix имеют функциональное средство, называемое виртуальной памятью (virtual memory), которое позволяет операционной системе работать с намного большим объёмом основной памяти, чем фактически имеется в наличии. Ваша настоящая физическая память работает как набор окон или кэшей, ведущих к гораздо большему пространству «виртуальной» памяти, большая часть которой на самом деле расположена в специальной области подкачки (swap area) на диске. Скрывая механизм реализации этой иллюзии от взгляда программ пользователя, ОС перемещает блоки данных (называемых «страницами», «pages») между памятью и диском. Конечным результатом этих ухищрений является то, что ваша виртуальная память намного больше, но не намного медленнее, чем реальная память.

Насколько виртуальная память медленней физической, зависит от того, насколько хорошо алгоритмы своппинга операционной системы соответствуют способу использования памяти вашими программами. Здесь есть благоприятный момент в том что операции чтения и записи памяти близки по времени, это ведёт к созданию группы блоков (кластеров (cluster)) в пространстве памяти. Эта тенденция называется локальность (locality), или, более формально, локальные ссылки (locality of reference) — и это хорошая штука. Если ссылки на адреса в памяти прыгают по виртуальному пространству беспорядочно, то будет осуществляться чтение и запись диска для каждой новой ссылки, и ваша виртуальная память будет такой же медленной, как и диск. Но из-за того, что программы преимущественно демонстрируют локальность ссылок, ваша операционная система может делать сравнительно малое количество подкачек относительно числа ссылок.

Опытным путём было установлено, что наиболее эффективный метод для широкого класса моделей использования памяти очень прост; он называется LRU или «least recently used» (наименее востребованные) алгоритм. Система виртуальной памяти по мере надобности извлекает блоки данных с диска в свои рабочие наборы (working set). Когда физической памяти становится недостаточно для рабочих наборов, она сбрасывает те из них, которые были наименее востребованы. Все Unix и большинство других операционных систем, использующих виртуальную память, используют незначительно изменённый LRU.

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

Как физическая память выступает в роли окон или кэшей для области подкачки на диске, так и внешний кэш работает как окна в основную память. Внешний кэш быстрее (250 Мб/сек, конечно, быстрее 100 Мб/сек) и меньше. Аппаратный контроллер памяти использует алгоритм LRU во внешнем кэше применительно к блокам данных, расположенных в основной памяти. По историческим причинам единица своппинга в кэше называется строка (line), а не страница.

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

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

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

8.3. Модуль управления памятью

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

Эта таблица используется для передачи команд специализированной части аппаратного обеспечения, называемой MMU или memory management unit (модуль управления памятью). Современные процессоры имеют встроенные в них MMU. MMU используют специальную способность включать ограждение вокруг областей памяти, так что ссылка, выходящая за допустимые пределы, будет отклонена и вызовется специальное прерывание.

Если вы когда либо видели сообщение Unix, которое гласило «Segmentation fault», «core dumped» или что-то подобное, то именно это и произошло; выполняемая программа попыталась обратиться к памяти вне своего сегмента, что вызвало неизбежное прерывание. Это указывает на ошибку в программном коде; core dump оставляет после себя информацию, помогающую программисту диагностировать и найти её.

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