В этом документе представлен UBC (Unified Buffer Cache - объединённый буферный кэш) - проект объединения кэшей файловой системы и данных файлов виртуальной памяти, увеличивающий производительность системы. Здесь обсуждаются как традиционные интерфейсы кэширования BSD, так и новые интерфейсы UBC, особое внимание уделяется принятым проектным решениям. Мы также обсудим архитектуру сходных решений из других операционных систем, уделяя особое внимание практическим сторонам этих отличий. Этот проект всё ещё находится в стадии разработки и по завершении войдёт в будущий выпуск NetBSD.
Современные операционные системы позволяют получать доступ к данным файловой системы двумя способами: через отображение в память и через системные вызовы подсистемы ввода-вывода read() и write(). В традиционных операционных системах типа UNIX запросы к отображению в памяти обрабатываются подсистемой виртуальной памяти, а системные вызовы обрабатываются подсистемой ввода-вывода. Традиционно эти две подсистемы разрабатывались раздельно и поэтому не глубоко интегрированы друг с другом. Например, в операционной системе NetBSD [1] подсистема виртуальной памяти UVM [2] и подсистема ввода-вывода оснащены собственными механизмами кэширования данных, которые работают почти независимо друг от друга. Недостаток интеграции ведёт к снижению совокупной производительности и гибкости системы. Для достижения хорошей производительности важно чтобы подсистемы виртуальной памяти и ввода-вывода были тесно интегрированы. Эту интеграцию реализует UBC.
Для понимания вносимых UBC улучшений важно сначала разобраться, как обстояло дело до появления UBC. Для начала поясним несколько терминов:
"буферный кэш":
Область памяти, предназначенная для кэширования данных файловой системы. Она выделяется во время запуска системы и управляется процедурами специального назначения.
Эта память организуется в "буферы", которые не имеют фиксированного размера и отображаются в виртуальное адресное пространство ядра. Буферы содержат данные файлов, пока данные остаются неизменными.
"страничный кэш":
Часть доступной памяти системы, которая используется для кэширования данных файлов и управляется системой виртуальной памяти.
Объём страничного кэша может меняться от 0% до 100% объёма физической памяти, не задействованного для других нужд.
"vnode":
Абстракция ядра, соответствующая файлу.
Большинство действий над vnode'ами и связанными с ними данными производится при помощи VOP'ов (сокращение от Vnode OPerations - операции с vnode'ами).
Главные интерфейсы для доступа к данным файлов - это:
read() и write():
Системный вызов read() при необходимости считывает данные с диска в кэш ядра и копирует данные из кэша ядра в адресное пространство приложения. Системный вызов write() перемещает данные в обратном направлении, копируя их из адресного пространства приложения в кэш ядра и в конечном итоге записывая данные из кэша на диск. Для сохранения данных в ядре эти интерфейсы могут быть реализованы как с использованием буферного, так и страничного кэша.
mmap():
Системный вызов mmap() даёт приложению прямой отображаемый через память доступ к данным в страничном кэше ядра. Данные файла читаются в страничный кэш по мере необходимости, как только процесс попытается получить доступ к отображению, созданному с помощью mmap(), и как только произойдёт ошибка доступа к странице.
В NetBSD без UBC системные вызовы read() и write() реализованы с использованием буферного кэша. Системный вызов read() читает данные файла в буферный кэш и затем копирует их в приложение. Однако системный вызов mmap() использует для хранения данных страничный кэш, т.к. память буферного кэша не управляется системой виртуальной памяти и не может быть отображена на адресное пространство приложения. Поэтому данные файла из буферного кэша копируются в страничный кэш, чтобы использовать их для исправления ошибок доступа к страницам отображения приложения. Для записи на диск изменённых данных страничного кэша новая версия данных копируется обратно в буферный кэш и из него записывается на диск. На рисунке 1 показан поток данных между диском и приложением при наличии традиционного буферного кэша.
Такое двойное кэширование данных значительно снижает эффективность системы. Наличие двух копий данных означает, что используется в два раза больше памяти и что приложениям доступно меньше памяти. Копирование данных туда и обратно между буферным и страничным кэшами создаёт нагрузку на центральный процессор, вымывает данные из его кэшей и в общем отрицательно влияет на производительность. Наличие двух копий данных также создаёт риск несоответствия копий, что может привести к трудноуловимым проблемам в приложениях.
Т.к. буферный кэш имеет постоянный объём, он плохо подходит как для большого объёма данных, потому что может оказаться слишком мал (что будет приводить к частым промахам), так и для небольшого объёма данных, для которого может оказаться слишком велик (из-за чего память будет недоступной для других нужд).
Данные буферного кэша отображаются в виртуальное адресное пространство ядра. Т.к. современное оборудование может иметь больше оперативной памяти, чем имеется в виртуальном адресном пространстве ядра, возникает искусственное ограничение.
Для решения этих проблем во многих операционных системах способ использования страничного и буферного кэшей был изменён. В каждой системе использовались собственные решения, поэтому мы сначала опишем UBC, а затем рассмотрим решения из нескольких других популярных операционных систем.
UBC - это новая подсистема, которая решает проблему двух кэшей. В UBC данные файла хранятся в страничном кэше и для системных вызовов read()/write() и для системного вызова mmap(). Данные файла читаются сразу в страничный кэш без прохождения через буферный кэш. Для этого вводятся два новых VOP'а, которые возвращают страницы с требуемыми данными из страничного кэша, при необходимости запрашивая драйвер устройства прочитать данные с диска. Т.к. страницы из страничного кэша должны принадлежать отображению в памяти, введён новый механизм для создания временных отображений, чтобы системные вызовы read() и write() могли скопировать данные файла в адресное пространство приложения. На рисунке 2 показаны изменения потока данных с появлением UBC.
UBC вводит новые интерфейсы:
VOP_GETPAGES(), VOP_PUTPAGES()
Эти два VOP'а позволяют файловым системам запрашивать диапазон страниц из системы виртуальной памяти для чтения в память с диска или для записи из памяти обратно на диск. VOP_GETPAGES() должен выделить страницы из системы виртуальной памяти для данных, которые ещё не помещены в кэш и инициировать операции ввода-вывода устройства для чтения всех дисковых блоков, содержащих данные этих страниц. VOP_PUTPAGES() должен инициировать операции ввода-вывода устройства для записи грязных страниц обратно на диск.
ubc_alloc(), ubc_release()
Эти функции выделяют и освобождают временные отображения данных файла в страничный кэш. Они являются эквивалентами функций буферного кэша getblk() и brelse() [3] для страничного кэша. Эти временные отображения не являются невыгружаемыми, но они кэшируются для ускорения повторяющегося доступа к одному и тому же файлу. Для центральных процессоров с виртуально-адресуемым кэшем данных важно тщательно выбирать для этих временных отображений виртуальные адреса, чтобы отображения файлов в адресные пространства пользовательских процессов и ядра могли существовать без возникновения проблем согласованности кэша центрального процессора. Если приложение создаёт невыровненные отображения файла, то проблемы всё же возможны. Но если приложение позволит операционной системе выбрать адрес отображения, то все отображения всегда будут выровнены.
ubc_pager
Это средство подкачки UVM, обрабатывающее ошибки доступа к страницам отображения, созданного при помощи ubc_alloc(). (Средство подкачки UVM - это абстракция для исправлении ошибок доступа к страницам и управления данными виртуальной памяти. Обратитесь к документации по UVM [2] за более подробной информацией о средствах подкачки.) Поскольку в данном случае есть только одна причина возникновения ошибок доступа к страницам, фактически ubc_pager просто вызывает новую операцию VOP_GETPAGES() для получения страниц, которые требуются для исправления ошибки.
Кроме этих новых интерфейсов было внесено несколько изменений в существующую архитектуру UVM для её полировки.
Ранее в UVM структуры vnode и uvm_object не были взаимозаменямыми. Несколько их полей дублировались и поддерживались независимо в каждой из них. Эти дублирующиеся поля были объединены. При первом использовании структуры vnode в качестве структуры uvm_object пока ещё нужна дополнительная инициализация, но в конечном итоге она будет удалена.
Ранее UVM поддерживал в структурах uvm_object только 32-битные смещения, поэтому в страничный кэш можно было сохранить данные только первых 4 гигабайта файла. Это не было большой проблемой, потому что существует не так много программ, желающих получать доступ к смещениям после 4 гигабайт при помощи mmap(). Но теперь системные вызовы read() и write() для доступа к данным тоже используют интерфейсы страничного кэша. Чтобы позволить обращаться к файлам по смещениям больше 4 гигабайт, в uvm_object была реализована поддержка 64-битных смещений.
Решаемая UBC проблема существует долгое время, с тех пор как в конце 1980'х появился доступ к файлам через отображения в памяти. В большинстве операционных систем типа UNIX эта проблема так или иначе была решена, способы решения значительно отличаются.
Впервые к этой проблеме обратились разарботчики операционной системы SunOS [4, 5]. По большому счёту в UBC копируются её архитектурные решения. Основные отличия архитектур кэша SunOS и системы UBC проистекают из разницы между системами виртуальной памяти SunOS и UVM. Но т.к. абстракции средств подкачки UVM и драйверов сегментов SunOS похожи, их влияние на архитектуру довольно мало.
Когда два года назад впервые началась работа над UBC, в качестве прототипа рассматривалась так же операционная система FreeBSD [6], где эта проблема тоже уже решена. В FreeBSD для доступа к данным файла продолжают использоваться интерфейсы буферного кэша, но данные хранятся в страничном кэше, а не в отдельной области памяти. В результате те же физические страницы можно получить из файла и через интерфейс буферного кэша и через интерфейс страничного кэша. Такой подход выгоден тем, что не нужно переделывать имеющиеся реализации файловых систем. Связующий код трансляции между интерфейсами так же сложен, как связующий код из SunOS, но в нём не была решена проблема взаимоблокировки (когда приложение одновременно вызывает write() и изменяет этот же файл через отображение в памяти), поэтому предпочтение было отдано решению из SunOS.
Подход Linux [7] (версии 2.3.44 - последней версии на момент написания этого документа) также очень похож на подход SunOS. Данные файла сохраняются только в страничном кэше. Временные отображения страничного кэша для системных вызовов read() и write() не требуются, т.к. Linux всегда отображает всю физическую память в виртуальное адресное пространство ядра. Одна из интересных особенностей заключается в том, что при сохранении страниц диска в кэше Linux добавляет номера блочных устройств в виде списка структур buffer_head. При изменении страница записывается обратно на диск, запросы на выполнение операций ввода-вывода можно отправить сразу драйверу устройства, без чтения дополнительных блоков для определения места, в которое должны быть записаны данные страницы.
Последней рассмотренной системой была HP-UX, в которой проблема кэширования данных файловой системы решена совершенно иначе. HP-UX продолжает сохранять данные файла и в буферном кэше и в страничном кэше, хотя и избегает дополнительного копирования данных, имевшегося в NetBSD до UBC, при чтении данных с диска прямо в страничный кэш. Такое решение обоснуется тем, что к большинству файлов обращаются либо через системные вызовы read()/write(), либо через системный вызов mmap(), но не через оба сразу. Пока оба механизма хорошо работают поотдельности, нет необходимости перепроектировать HP-UX для устранения проблемы согласованности. Некоторые попытки избежать рассогласованности между кэшами предпринимались, но использование блокировки не позволяет достичь максимальной эффективности.
Есть и другие операционные системы, в которых реализован объединённый кэш (например, в Compaq Tru64 UNIX и в IBM AIX), но мы не смогли найти информацию об архитекутре этих операционных систем для сравнения.
Т.к. работа над UBC ещё не завершена, точная оценка производительности была бы преждевременной. Однако мы провели несколько простых сравнений достигнутого состояния. Для тестов использовался компьютер с 333-мегагерцовым процессором Pentium II, 64 мегабайтами оперативной памяти и IDE-диском объёмом 12 гигабайт. Для оценки скорости последовательного чтения и записи использовалась последовательность команд dd. Мы создали файл размером 1 гигабайт (что значительно больше физического объёма оперативной памяти, доступной для кэширования). Затем мы перезаписали этот файл, чтобы оценить скорость, с которой данные, изменённые с помощью системного вызова write(), попадут на диск без издержек на выделение блоков файла. Затем мы снова прочитали весь файл, чтобы оценить насколько быстро файловая система может извлечь данные с диска. Наконец, мы несколько раз прочитали первые 50 мегабайт файла (которые должны полностью уместиться в физической памяти), чтобы определить ускорение доступа к данным в кэше. Результаты тестов сведены в таблице 1.
Время выполнения теста (в секундах) | ||||||
---|---|---|---|---|---|---|
Ввод | Вывод | Размер | NetBSD | NetBSD | FreeBSD | Linux |
1.4.2 | с UBC | 3.4 | 2.2.12-20smp | |||
устройство | /dev/null | 1 Гб | 72.8 | 72.7 | 279.3 | 254.6 |
/dev/zero | новый файл | 1 Гб | 83.8 | 193.0 | 194.3 | 163.9 |
/dev/zero | перезапись файла | 1 Гб | 79.4 | 186.6 | 192.2 | 167.3 |
нерезидентный файл | /dev/null | 1 Гб | 72.7 | 86.7 | 279.3 | 254.5 |
нерезидентный файл | /dev/null | 50 Мб | 3.6 | 4.3 | 13.7 | 12.8 |
резидентный файл | /dev/null | 50 Мб | 3.6 | 0.8 | 4.1 | 11.5 |
см. выше | /dev/null | 50 Мб | 3.6 | 0.8 | 0.7 | 4.5 |
см. выше | /dev/null | 50 Мб | 3.6 | 0.8 | 0.7 | 0.8 |
см. выше | /dev/null | 50 Мб | 3.6 | 0.8 | 0.7 | 0.8 |
Большая разница в результатах первых четырёх тестов на трёх операционных системах без UBC возникает из-за отличий в производительности их дисковых драйверов IDE. Все операционные системы, за исключением NetBSD с UBC, во время тестов выполняли последовательное буферизованное чтение большого файла с устройства с одинаковой скоростью, поэтому всё что мы можем в действительности сказать из этого, что другая архитектура кэширования не добавляет сколь-нибудь значительных издержек. При чтении система UBC не достигает скорости устройства, поэтому тут есть возможности для улучшений. Нужен дополнительный анализ, чтобы выяснить причины замедления.
Очевидно UBC требует улучшения производительности записи. Частично проблема вызвана тем, что UVM при нехватке памяти недостаточно эффективно сбрасывает изменённые страницы, а частично тем, что код файловой системы в настоящее время не переупорядочивает асинхронные операции записи на диск в непрерывную последовательность. Мы сосредоточились на производительности чтения, поэтому плохая производительность записи не была неожиданной.
Наибольший интерес представляют тесты, в которых 5 раз читаются одни и те же 50 мегабайт файла. Из них явно видна выгода от увеличения памяти, доступной для кэшировани в системе UBC по сравнению с NetBSD. В NetBSD 1.4.2 все пять чтений происходят со скоростью устройства, в то время как во всех других системах через несколько запусков достигнута скорость чтении из памяти. У нас нет объяснения, почему FreeBSD и Linux не выполняют второе чтение 50 мегабайт со скоростью памяти и почему Linux не делает этого даже с третьей попытки.
В этом документе представлена система UBC, улучшающая кэширование файловой системы и виртуальной памяти для NetBSD. Эта система содержит много улучшений по сравнению с прежней архитектурой NetBSD:
Как только работа будет завершена, она станет частью будущего релиза NetBSD. В настоящее время исходный код находится в ветке "chs-ubc2" дерева CVS NetBSD и может быть получен анонимно. За подробностями обратитесь по ссылке https://www.netbsd.org/Sites/net.html.
Идёт активный процесс разработки, предстоит выполнить ещё больше работы! Запланирована следующая работа:
Хотим поблагодалить всех, кто помог с обзором черновиков этого документа. Отдельное спасибо Чаку Кранору!
Проект NetBSD.
Операционная система NetBSD.
Более подробную информацию см. по ссылке https://www.netbsd.org.
C. Cranor and G. Parulkar / Чарльз Кранор, Гурудатта Парулкар.
The UVM Virtual Memory System / Система виртуальной памяти UVM
In Proceedings of the 1999 USENIX Technical Conference, June 1999.
Marice J. Bach.
The Design of the UNIX Operating SystemПП1
.Prentice Hall, February 1987.
J. Moran, R. Gingell and W. Shannon.
Virtual Memory Architecture in SunOS.
In Proceedings of USENIX Summer Conference, pages 81-94. USENIX, June 1987.
J. Moran.
SunOS Virtual Memory Implementation.
In Proceedings of the Spring 1988 European UNIX Users Group Conference, April 1988.
Проект FreeBSD.
Операционная система FreeBSD.
Более подробную информацию см. по ссылке https://www.freebsd.org.
Л. Торвальдс и другие.
Операционная система Linux.
Более подробную информацию см. по ссылке https://www.linux.org.
Автор перевода на русский язык: Владимир Ступин