Файл Mmap, чем открыть
Обновлено: 22.11.2024
Когда я спрашиваю своих коллег, почему mmap работает быстрее, чем системные вызовы, ответ неизбежно звучит так: «накладные расходы на системные вызовы»: стоимость пересечения границы между пользовательским пространством и ядром. Оказывается, эти накладные расходы имеют больше нюансов, чем я думал, поэтому давайте заглянем внутрь, чтобы понять разницу в производительности.
Предыстория (пропустите, если вы разбираетесь в ОС):
Системные вызовы. Системный вызов — это специальная функция, позволяющая выполнять перекрестную защиту доменов. Когда программа выполняется в пользовательском режиме (непривилегированный домен защиты), ей не разрешается делать то, что разрешено коду, выполняющемуся в режиме ядра (привилегированный домен защиты). Например, программа, работающая в пользовательском пространстве, обычно не может читать файлы без помощи ядра. Когда пользовательская программа запрашивает службу у операционной системы, система защищает себя от вредоносных программ или программ с ошибками с помощью системных вызовов. Системный вызов выполняет специальную аппаратную инструкцию, часто называемую «trap», которая передает управление ядру. Затем ядро может решить, будет ли оно выполнять запрос.
Хотя эта защита очень полезна, она имеет свою цену. Когда мы переходим из пользовательского пространства в ядро, нам приходится сохранять аппаратные регистры, потому что они могут понадобиться ядру. Кроме того, поскольку напрямую разыменовывать указатели пользовательского уровня небезопасно (а что, если они нулевые — это приведет к сбою ядра!), данные, на которые ссылаются эти указатели, должны быть скопированы в ядро.
Когда мы возвращаемся из системного вызова, мы должны повторить последовательность в обратном порядке: скопировать все данные, запрошенные пользователем (поскольку мы не можем просто передать указатели пользовательских программ в память ядра), восстановить регистры и перейти в пользовательский режим.
Ошибки страницы. Операционная система и оборудование вместе преобразуют адреса, записанные в исполняемом файле вашей программы (они называются виртуальными адресами), в адреса в фактической физической памяти (физические адреса). ). Для компилятора было бы довольно неудобно генерировать физические адреса напрямую, потому что он не знает, на какой машине вы можете запустить свою программу, сколько у нее памяти и какие другие программы могут использовать физическую память во время выполнения вашей программы. Отсюда необходимость в этой трансляции виртуального адреса в физический. Переводы или сопоставления настраиваются в таблице страниц вашей программы. Когда ваша программа начинает работать, ни одно из этих сопоставлений не настроено. Таким образом, когда ваша программа пытается получить доступ к виртуальному адресу, она генерирует ошибку страницы, которая сигнализирует ядру о необходимости настройки сопоставления. Ядро уведомляется о том, что ему необходимо обработать отказ страницы через ловушку, таким образом, это немного похоже на системный вызов. Разница в том, что системный вызов является явным, а ошибка страницы — неявной.
Кэш буфера. Буферный кеш — это часть памяти ядра, которая используется для хранения фрагментов файлов, к которым недавно обращались (эти фрагменты называются блоками или страницами). Когда пользовательская программа запрашивает чтение файла, страница из файла (обычно) сначала помещается в буферный кеш. Затем данные копируются из буферного кэша в предоставленный пользователем буфер во время возврата из системного вызова.
Мап. Mmap означает файлы отображенные в память. Это способ чтения и записи файлов без вызова системных вызовов. Операционная система резервирует часть виртуальных адресов программы для непосредственного «сопоставления» части в файле. Таким образом, если программа считывает данные из этой части адресного пространства, она получит данные, находящиеся в соответствующей части файла. Если эта часть файла окажется в буферном кеше, виртуальные адреса сопоставленного фрагмента будут просто сопоставлены с физическими адресами соответствующих страниц буферного кеша при первом доступе, и никакие системные вызовы или другие ловушки не будут вызываться. позже. Если данные файла не находятся в буферном кеше, доступ к отображаемой области приведет к ошибке страницы, что побудит ядро перейти к получению соответствующих данных с диска.
Почему mmap должен быть быстрее
Начнем с формулировки гипотезы. Почему мы ожидаем, что mmap будет быстрее? Есть две очевидные причины. Во-первых, он не требует явного пересечения защитных доменов, хотя неявное пересечение по-прежнему происходит, когда возникают ошибки страниц. Тем не менее, если к заданному диапазону в файле обращаются более одного раза, есть вероятность, что мы не столкнемся с ошибками страницы после первого доступа. Однако в моих экспериментах этого не происходило, поэтому я ожидал, что каждый раз, когда буду читать новый блок файла, будет срабатывать ошибка страницы.
Во-вторых, если приложение написано таким образом, что оно может обращаться к данным непосредственно в отображаемой области, нам не нужно выполнять копирование памяти. Однако в своих экспериментах мне было интересно измерить сценарий, в котором приложение имеет отдельный целевой буфер для данных, которые оно считывает.Таким образом, даже несмотря на то, что файл сопоставлен, приложение все равно скопирует данные из сопоставленной области в целевой буфер.
Поэтому в моей экспериментальной среде я ожидал, что mmap будет немного быстрее, чем системные вызовы, потому что я думал, что код для обработки ошибок страниц будет немного более упорядоченным, чем код для системных вызовов.
Эксперимент
Я организовал свой эксперимент следующим образом. Я создаю файл размером 4 ГБ, а затем читаю его последовательно или случайно, используя размер блока 4 КБ, 8 КБ или 16 КБ. Я прочитал файл с помощью системного вызова чтения или mmap. В случае mmap я не просто обращаюсь к отображаемой области напрямую, но копирую данные из отображаемой области в отдельный «целевой» буфер (см. сообщение в блоге, описывающее мой целевой вариант использования понять, почему я так делаю). Таким образом, в обоих экспериментах мы копируем данные из кэша буфера ядра в буфер назначения пользователя, но в случае mmap мы делаем это с помощью ошибок страниц, а в случае системных вызовов мы делаем это с помощью чтение системного вызова.
Я запускаю эти тесты с использованием либо холодного буферного кеша, что означает, что файл там не кэшируется, либо теплого буферного кеша, что означает, что файл находится в память ядра. Носитель данных — это твердотельный накопитель, который вы можете ожидать от типичного сервера. Все операции чтения выполняются в одном потоке. Исходный код моего теста находится здесь.
Результаты
На следующих диаграммах показана производительность теста чтения для последовательного/теплого, последовательного/холодного, случайного/теплого и случайные/холодные запуски.
Файл с отображением памяти содержит содержимое файла в виртуальной памяти. Это сопоставление между файлом и областью памяти позволяет приложению, включая несколько процессов, изменять файл, читая и записывая непосредственно в память. Вы можете использовать управляемый код для доступа к файлам, отображаемым в памяти, так же, как собственные функции Windows обращаются к файлам, отображаемым в памяти, как описано в разделе Управление файлами, отображаемыми в памяти.
Существует два типа отображаемых в память файлов:
Сохраняемые файлы, отображенные в памяти
Сохраняемые файлы — это отображаемые в память файлы, связанные с исходным файлом на диске. Когда последний процесс закончил работу с файлом, данные сохраняются в исходный файл на диске. Эти отображаемые в память файлы подходят для работы с очень большими исходными файлами.
Не сохраняемые файлы с отображением в памяти
Несохраняемые файлы — это отображаемые в памяти файлы, не связанные с файлом на диске. Когда последний процесс завершает работу с файлом, данные теряются, а файл утилизируется сборщиком мусора. Эти файлы подходят для создания общей памяти для межпроцессного взаимодействия (IPC).
Процессы, представления и управление памятью
Файлы, отображаемые в память, могут совместно использоваться несколькими процессами. Процессы могут сопоставляться с одним и тем же отображаемым в память файлом, используя общее имя, которое назначается процессом, создавшим файл.
Для работы с отображенным в память файлом необходимо создать представление всего отображаемого в памяти файла или его части. Вы также можете создать несколько представлений для одной и той же части отображаемого в память файла, тем самым создавая параллельную память. Чтобы два представления оставались параллельными, они должны быть созданы из одного и того же файла с отображением в памяти.
Несколько представлений также могут быть необходимы, если размер файла превышает размер логической памяти приложения, доступной для сопоставления памяти (2 ГБ на 32-разрядном компьютере).
Существует два типа представлений: представление потокового доступа и представление произвольного доступа. Используйте представления потокового доступа для последовательного доступа к файлу; это рекомендуется для несохраняемых файлов и IPC. Представления произвольного доступа предпочтительны для работы с постоянными файлами.
Доступ к файлам, отображаемым в памяти, осуществляется через диспетчер памяти операционной системы, поэтому файл автоматически разбивается на несколько страниц и используется по мере необходимости. Вам не нужно самостоятельно управлять памятью.
На следующем рисунке показано, как несколько процессов могут одновременно иметь несколько перекрывающихся представлений одного и того же файла, отображаемого в память.
На следующем изображении показаны несколько и перекрывающиеся представления файла с отображением в памяти:
Программирование с помощью файлов, отображенных в памяти
В следующей таблице приведены рекомендации по использованию отображаемых в памяти файловых объектов и их членов.
Читайте также: