Второе издание
Скачать 3.09 Mb.
|
Функции mmap() и do_mmap():unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) Эта функция выполняет отображение на память содержимого файла f i l e на- чиная с позиции в файле offset; размер отображаемого участка равен len байт. Значения параметров f i l e и offset могут быть нулевыми, в этом случае отображе- ние не будет резервироваться (сохраняться) в файле. Такое отображение называется анонимным (anonymous mapping). Если указан файл и смещение, то отображение назы- вается отображением, файла в память (file-backed mapping). Параметр addr указывает (точнее, всего лишь подсказывает), откуда начинать по- иск свободного интервала адресов. Параметр p r o t указывает права доступа для страниц памяти в данной области. Возможные значение флагов зависят от аппаратной платформы и описаны в файле приведенные в табл. 14.2. Параметр flags позволяет указать все остальные флаги области VMA. Эти флаги также определены в Таблица 14.2. Флаги защиты страниц памяти 1 Адресное пространство процесса 325 Флаг Влияние на страницы памяти в созданном интервале адресов PROT_READ PROT_WRITE PROT_EXEC PROT_NONE Соответствует флагу VM_READ Соответствует флагу VM_WRITE Соответствует флагу VM_EXEC К страницам памяти нет доступа Таблица 14.3. Флаги защиты страниц памяти MAP_SHARED MAP_PRIVATE MAP_FIXED MAP_ANONYMOUS MAP_GROWSDOWN MAP_DENYWRIIE MAP_EXECUTABLE MAP_LOCKED MAP_NORESERVE MAP_POPULATE MAP_NONBLOCK Системный вызов mmap() Возможности функции do_mmap() экспортируются в пространство пользователя с помощью системного вызова mmap() , который определен следующим образом. void * mmар2 (void *start, size_t length, int prot, int flags, int fd, off_t pgoff) Этот системный вызов имеет имя mmap2() ,т.е. второй вариант функции mmap(). Первоначальный вариант mmap() требовал в качестве последнего параметра смеще- ние в байтах, а текущий вариант, mmap2(), — смещение в единицах размера страни- цы памяти. Это позволяет отображать файлы большего размера с большим значе- нием смещения. Первоначальный вариант функции mmap() , который соответствует стандарту POSTX, доступен через библиотеку функций языка С, как функция mmap() , но в ядре уже не реализован. Новый вариант библиотечной функции называется mmap2() . Обе эти библиотечные функции используют системный вызов mmap2 (), При этом библиотечная функция mmap() переводит значение смещения из байтов в количество страниц памяти. 326 Глава 14 Флаг Влияние на созданный интервал адресов Если какой-либо из параметров имеет недопустимое значение, то функция do_mmap() возвращает отрицательное число. В протипном случае создастся не- обходимый интервал адресов. Если это возможно, то этот интервал объединяется с соседней областью памяти. Если это невозможно, то создается новая структура vm_area_struct, которая выделяется в слябовом кэше vm_area_cachep. После этого новая область памяти добавляется в связанный список и красно-черное дерево областей памяти адресного пространства с помощью функции vma_link() . Затем обновляется значение поля total_vm в дескрипторе памяти. В конце концов, функ- ция возвращает начальный адрес вновь созданного интервала адресов. Отображение может быть совместно используемым Отображение не может быть совместно используемым Создаваемый интервал адресов должен начинаться с указанного адреса addr Отображение является анонимным, а не отображением файла Соответствует флагу VM_GROWSDOWN Соответствует флагу VM DENYWRITE Соответствует флагу VM_EXECUTABLE Соответствует флагу VM_LOCKED Нет необходимости резервировать память для отображения Предварительно заполнить (prefault) таблицы страниц Не блокировать при операциях ввода-вывода Функции munmap() и do_munmap(): удаление интервала адресов Функция do_manmap() удаляет интервал адресов из указанного адресного про- странства процесса. Эта функция объявлена в файле int do_munmap(struct mm_struct *mm, unsigned long start, size t_len) Первый параметр указывает адресное пространство, из которого удаляется ин- тервал адресов, начинающийся с адреса s t a r t и имеющий длину len байт. В случае успеха возвращается нуль, а в случае ошибки — отрицательное значение. Системный вызов munmap() Системный вызов munmap () экспортируется в адресное пространство пользова- теля, чтобы иметь возможность удалять интервалы адресов из адресного простран- ства. Эта функция является комплиментарной к системному вызову mmap () и имеет следующий прототип. int munmap(void *start, size_t length) Данный системный вызов реализован в виде очень простой интерфейсной обо- лочки (wrapper) функции do_munmap (). asmlinkage long sys_munmap(unsigned long addr, size_t len) { int ret; struct mm_struct *mm; mm = current->mm; down_write(&mm->mmap_sem); ret = do_munmap(mm, addr, len); p_write(&mm->mmap_sem); return ret; } Таблицы страниц Хотя пользовательские программы и работают с виртуальной памятью, которая отображается на физические адреса, процессоры работают непосредственно с эти- ми физическими адресами. Следовательно, когда приложение обращается к адресу виртуальной памяти, этот адрес должен быть конвертирован в физический адрес, чтобы процессор смог выполнить запрос. Соответствующий поиск выполняется с помощью таблиц страниц. Таблицы страниц работают путем разбиения виртуально- го адреса на части. Каждая часть используется в качестве индекса (номера) записи в таблице. Таблица содержит или указатель на другую таблицу, или указатель на соот- ветствующую страницу физической памяти. Адресное пространство процесса 327 В операционной системе Linux таблицы страниц состоят из трех уровней 3 Несколько уровней позволяют эффективно поддерживать неравномерно заполнен- ные адресные пространства даже для 64-разрядных машин. Если бы таблицы стра- ниц были выполнены в виде одного статического массива, то их размер, даже для 32-разрядных аппаратных платформ, был бы чрезвычайно большим. В операцион- ной системе Linux трехуровневые таблицы страниц используются даже для тех ап- паратных платформ, которые аппаратно не поддерживают трехуровневых таблиц (например, для некоторых аппаратных платформ поддерживается только два уровня или аппаратно реализовано хеширование). Три уровня соответствуют своего рода "наибольшему общему знаменателю". Для аппаратных платформ с менее сложной ре- ализацией работа с таблицами страниц в ядре при необходимости может быть упро- щена с помощью оптимизаций компилятора. Таблица страниц самого верхнего уровня называется глобальным каталогом стра- ниц (page global directory, PGD). Таблица PGD представляет собой массив элементов типа pgd_t. Для большинства аппаратных платформ тип pgd_t соответствует типу unsigned long. Записи в таблице PGD содержат указатели на каталоги страниц бо- лее низкого уровня, PMD. Каталоги страниц второго уровня еще называются каталогами страниц; среднего уровня (page middle directory, PMD). Каждый каталог PMD— это массив элементов типа prad_t. Записи таблиц PMD укалывают на таблицы РТЕ (page table entry, запись таблицы страниц). Таблицы страниц последнего уровня называются просто таблицами страниц и со- держат элементы типа pte_t. Записи таблиц страниц указывают на страницы памяти. Для большинства аппаратных платформ поиск в таблицах страниц выполняется аппаратным обеспечением (по крайней мере частично). При нормальной работе аппаратное обеспечение берет на себя большую часть ответственности по исполь- зованию таблиц страниц. Однако для этого ядро должно все настроить так, чтобы аппаратное обеспечение могло нормально работать. На рис. 14.1 показана диаграмма того, как происходит перевод виртуального адреса в физический с помощью табли- цы страниц. Рис. 14.1. Таблицы страниц 3 Начиная с ядра версии 2.6.11 таблицы страниц в ОС Linux для 64-разрядных аппаратных платформ стали 4-уровневыми, что позволяет в полном объеме использовать все виртуальное адресное про- странство. Для 32-разрядных аппаратных платформ осталось 3 уровня, как и раньше. — Примеч. ред. 328 Глава 14 Структура mm_struct Страница физической памяти Структура page PGD PMD РТЕ Каждый процесс имеет свои таблицы страниц (разумеется, потоки эти таблицы используют совместно). Поле pgd дескриптора памяти указывает на глобальный ка- талог страниц. Манипуляции с таблицами и прохождение по ним требуют захвата блокировки page_table_lock, которая также находится в соответствующем де- скрипторе памяти. Структуры данных, связанные с таблицами страниц, сильно зависят от аппарат- ной платформы и определены в файле Поскольку практически каждое обращение к страницам виртуальной памяти тре- бует определения соответствующего адреса физической памяти, производительность операций с таблицами страниц является очень критичной. Поиск всех этих адресов в памяти должен всегда выполняться очень быстро. Чтобы посодействовать этому, большинство процессоров имеют буфер быстрого преобразования адреса (translation loo- kaside buffer, или TLB), который работает, как аппаратный кэш отображения вирту- альных адресов на физические. При обращении к виртуальному адресу процессор вначале проверяет, не кэшировано ли это отображение в TLB. Если обращение в кэш было удачным, то сразу же возвращается физический адрес. В противном случае поиск физического адреса выполняется с помощью таблиц страниц. Несмотря на это, управление таблицами страниц все же остается критичной и развивающейся частью ядра. Изменения в ядре 2.6 включают выделение частей таблиц страниц не в области верхней памяти. В будущем, вероятно, появится воз- можность совместного использования таблиц страниц с копированием при записи. В такой схеме таблицы страниц будут совместно использоваться родительским и по- рожденным процессами даже после выполнения вызова fork(). Если же родитель- ский или порожденный процесс изменит некоторую запись таблицы страниц, то будет создана копия этой записи, и эти процессы больше не будут совместно исполь- зовать данную запись. Совместное использование таблиц страниц позволит устра- нить затраты, связанные с копированием таблиц страниц при вызове fork(). Заключение В этой главе была рассмотрена абстракция виртуальной памяти, которая предо- ставляется каждому процессу. Было рассказано, как ядро представляет адресное пространство процесса (с помощью структуры s t r u c t mm_struct) и каким обра- зом ядро представляет области памяти внутри этого адресного пространства ( s t r - uct vrn_area_struct). Также рассказывалось о том, как ядро создает (с помощью функции mmap()) и удаляет (с помощью функции munmap()) области памяти. Б кон- це были рассмотрены таблицы страниц. Так как операционная система Linux — это система с виртуальной памятью, то все эти понятия очень важны для понимания работы системы и используемой модели процессов. В следующей главе рассматривается страничный кэш - общий кэш данных, кото- рый используется для выполнения страничных операций ввода-вывода и обратной записи страниц. Оставайтесь с нами! Адресное пространство процесса 329 15 Страничный кэш и обратная запись страниц В ядре операционной системы Linux реализован один главный дисковый кэш, который называется страничным (page cache). Назначение этого кэша— мини- мизировать количество дисковых операций ввода-вывода путем хранения в памяти тех данных, для обращения к которым необходимо выполнять дисковые операции, Эта глава посвящена рассмотрению страничного кэша. Кэширование дисковых данных важно по двум причинам. Во-первых, доступ к диску выполняется значительно медленнее, чем доступ к памяти. Доступ к данным в памяти выполняется значительно быстрее, чем к данным на диске. Во-вторых, если к некоторым данным осуществлялся доступ, то с достаточно большой вероятностью к этим же данным в ближайшем будущем потребуется обратиться снова. Принцип, согласно которому операции обращения к некоторым данным имеют тенденцию группироваться друг с другом во времени, называется сосредоточенностью во време- ни (temporal locality). Сосредоточенность во времени гарантирует, что если данные кэшируются при первом доступе к ним, то существует большая вероятность удачного обращения в кэш к этим данным в ближайшем будущем. Страничный кэш состоит из физических страниц, которые находятся в оператив- ной памяти. Каждая страница памяти в кэше соответствует нескольким дисковым блокам. Когда ядро начинает некоторую операцию страничного ввода-вывода (дис- ковые, обычно файловые, операции ввода-вывода, которые выполняются порциями, равными размеру страницы памяти), то оно вначале проверяет, нет ли соответству- ющих данных в страничном кэше. Если эти данные есть в кэше, то ядро может не обращаться к диску и использовать данные прямо из страничного кэша. Отдельные дисковые блоки также могут быть привязаны к страничному кэшу с помощью буферов блочного ввода-вывода. Вспомните из материала главы 13, "Уровень блочного ввода-вывода", что буфер — это представление в памяти одного физического дискового блока. Буферы играют роль дескрипторов, которые отобра- жают страницы памяти на дисковые блоки. Поэтому страничный кэш также позво- ляет сократить количество обращений к диску при выполнении операций блочного ввода-вывода как за счет кэширования, так и за счет буферизации операций блочно- го ввода-вывода для выполнения в будущем. Такой тип кэширования часто называют "буферным кэшем", хотя на самом деле это не отдельный кэш, а часть страничного кэша. Рассмотрим те типы операций и данных, которые связаны со страничным кэшем. Страничный кэш в основном пополняется при выполнении страничных операций ввода-вывода, таких как read() и w r i t e ( ) . Страничные операции ввода-вывода вы- полняются с целыми страницами памяти, в которых хранятся данные, что соответ- ствует операциям с более, чем одним дисковым блоком. В страничном кэше данные файлов хранятся порциями. Размер одной порции равен одной странице памяти. Операции блочного ввода-вывода работают в каждый отдельный момент времени с одним дисковым блоком. Часто встречающаяся операция блочного ввода-вывода — это чтение и запись файловых индексов. Ядро предоставляет функцию bread() , ко- торая выполняет низкоуровневое чтение одного блока с диска. С помощью буферов дисковые блоки отображаются на связанные с ними страницы памяти и благодаря этому сохраняются в страничном кэше. Например, при первом открытии в текстовом редакторе дискового файла с ис- ходным кодом, данные считываются с диска и записываются в память. При редакти- ровании файла считывается вес больше данных в страницы памяти. Когда этот файл позже начинают компилировать, то ядро может считывать соответствующие страни- цы памяти из дискового кэша. Нет необходимости снова считывать данные с диска. Поскольку пользователи склонны к тому, чтобы периодически работать с одними и теми же файлами, страничный кэш уменьшает необходимость выполнения большого количества дисковых операций. Страничный кэш Как следует из названия, страничный кэш (page cache) — это кэш страниц; памяти. Соответствующие страницы памяти получаются в результате чтения и записи обыч- ных файлов на файловых системах, специальных файлов блочных устройств и фай- лов, отображаемых в память. Таким образом, в страничном кэше содержатся стра- ницы памяти, полностью заполненные данными из файлов, к которым только что производился доступ. Перед выполнением операции страничного ввода-вывода, как, например, read() 1 , ядро проверяет, есть ли те данные, которые нужно считать, в страничном кэше. Если данные находятся в кэше, то ядро может быстро возвратить требуемую страницу памяти. Объект address_space Физическая страница памяти может содержать данные из нескольких несмежных физических дисковых блоков 2 1 Как было показано в главе 12 ," Виртуальная файловая система", операции страничного ввода-вы- вода непосредственно выполняются не системными вызовами read() и write(), а специфичными для файловых систем методами file->f_op->read() и file>f_op->wriie(). 2 Например, размер страницы физической памяти для аппаратной платформы х86 равен 4 Кбайт, в то время как размер дискового блока для большинства устройств и файловых систем равен 512 байт. Следовательно, в одной странице памяти может храниться 8 блоков. Блоки не обязатель- но должны быть смежными, так как один файл может быть физически "разбросанным" по диску. 332 Глава 15 Проверка наличия определенных данных в страничном кэше может быть затруд- нена, если смежные блоки принадлежат совершенно разным страницам памяти. Невозможно проиндексировать данные в страничном кэше, используя только имя устройства и номер блока, что было бы наиболее простым решением. Более того, страничный кэш ядра Linux является хранилищем данных доста- точно общего характера в отношении того, какие страницы памяти в нем могут кэшироваться. Первоначально страничный кэш был предложен в операционной системе System V (SVR 4) для кэширования только данных из файловых систем. Следовательно, для управления страничным кэшем операционной системы SVR 4 использовался эквивалент файлового объекта, который назывался s t r u c t vnode. Кэш операционной системы Linux разрабатывался с целью кэширования любых объ- ектов, основанных на страницах памяти, что включает множество типов файлов и отображений в память. Для получения необходимой общности в страничном кэше операционной систе- мы Linux используется структура address_space (адресное пространство), которая позволяет идентифицировать страницы памяти, находящиеся в кэше. Эта структура определена в файле Поле i_mmap — это дерепо поиска по приоритетам для всех совместно использу- емых и частных отображений. Дерево поиска по приоритетам— это хитрая смесь базисных и частично упорядоченных бинарных деревьев 3 Всего в адресном пространстве nrpages страниц памяти. 3 Реализация ядра основана на базисном дереве поиска по приоритетам, предложенном в работе Edward M. McCreight, опубликованной в журнале SIAM Journal of Computing, May 1985, vol. 14. № 2 , P. 257-276. Страничный кэш и обратная запись страниц 333 struct address_space { struct inode *host; struct radix_tree_root page_tree; spinlock_ t tree_lock; unsigned int i_mmap_wrltable; struct prio_tree_root i_mmap; struct list_head i_mmap_nonlinear; spinlock_t i_mmap_lock; atomic_t truncate_counl; unsigned long nrpages; pgoff_t writeback_index; struct address_space_operations *a_ops; unsigned long flags; struct backing_dev_info *backing_dev_info; spinlock_t private_lock; struct list_head private_list; struct address_spacs *assoc_mapping; }; /* файловый индекс, которому принадлежит объект */ /* базисное дерево всех страниц */ /* блокировка для защиты поля page_tree */ /* количество областей памяти с флагом VM_SHARED */ /* список всех отображений */ /* список областей памяти с флагом VM_NONLINEAR */ /* Блокировка поля i_mmap */ /* счетчик запросов truncate */ /* общее количество страниц */ /* смещения начала обратной записи */ /* таблица операций */ /* маска gfp_mask и флаги ошибок */ /* информация упреждающего чтения */ /* блокировка для частных отображений */ /* список частных отображений */ /* соответствующие буферы */ Объект a d d r e s s s p a c e связан с некоторым другим объектом ядра, обычно с файловым индексом. Если это так, то поле host указывает на соответствующий фай- ловый индекс. Если значение поля host равно NULL, то соответствующий объект не является файловым индексом; например, объект address_space может быть связан с процессом подкачки страниц (swapper). Поле a_ops указывает на таблицу операгций с адресным пространством так же, как и в случае объектов подсистемы VFS. Таблица операций представлена с помо- щью структуры s t r u c t address_space_operations, которая определена в файле struct address_space_operations { int (*writepage) (struct page *, struct writeback_control * ) ; int (*readpage) (struct file *, struct page * ) ; int (*sync_page) (struct page * ) ; int (*writepages) (struct address_space *, struct writeback_control * ) ; int (*set_page_dirty) (struct page * ) ; int (*readpages) (struct file *, struct address_space *, struct list_head *, unsigned); int (*prepare_write) (struct file *, struct page *, unsigned, unsigned); int (*commit_write) (struct file *, struct page *, unsigned, unsigned); sector_t (*bmap)(struct address_space *, sector_t); int (*invalidatepage) (struct page *, unsigned long); int (*releasepage) (struct page *, int); int (*direct_IO) (int, struct kiocb *, const struct iovec *, loff_t, unsigned long); }; Методы r e a d _ p a g e и w r i t e _ p a g e являются наиболее важными. Рассмотрим шаги, которые выполняются при страничной операции чтения. Методу чтения в качестве параметров передается пара значений: объект address_ s p a c e и смещение. Эти значения используются следующим образом для поиска не- обходимых данных в страничном кэше. page = find_get_page(mapping, index); где параметр mapping — это заданное адресное пространство, a index - заданная по- зиция в файле. Если в кэше нет необходимой страницы памяти, то новая страница памяти вы- деляется и добавляется в кэш следующим образом. struct page *cached_page; int error; cached_page = page_cache_alloc_cold (mapping); if (!cached_page) /* ошибка выделения памяти */ error = add_to_page_cache_lru (cached_page, mapping, index, GFP_KERNEL); if (error) /* ошибка добавления страницы памяти в страничный кэш */ 334 Глава 15 Наконец, необходимые данные могут быть считаны с диска, добавлены в стра- ничный кэш и возвращены пользователю. Это делается следующим образом. error = mapping->a_ops->readpage(file, page); Операции записи несколько отличаются. Для отображаемых в память файлов при изменении страницы памяти система управления виртуальной памятью просто вызывает следующую функцию. SetPageDirty(page); Ядро выполняет запись этой страницы памяти позже с помощью вызова метода w r i t e p a g e (). Операции записи для файлов, открытых обычным образом (без ото- бражения в память), выполняются более сложным путем. В основном, общая опе- рация записи, которая реализована в файле mm/filemap.с, включает следующие шаги. page = __grab_cache_page(mapping, index, &cached_page, &lru_pvec); status a_ops->prepare_write(file, page, offset, offset+bytes); page_fault = filemap_copy_from_user(page, offset, buf, bytes); status = a_ops->commit_write(file, page, offset, offset+bytes}; Выполняется поиск необходимой страницы памяти в кэше. Если такая страница в кэше не найдена, то создается соответствующий элемент кэша. Затем вызывает- ся метод p r e p a r e _ w r i t e (), чтобы подготовить запрос на запись. После этого дан- ные копируются из пространства пользователя в буфер памяти в пространстве ядра. И наконец данные записываются на диск с помощью функции comnit_write (). Поскольку все описанные шаги выполняются при всех операциях страничного ввода-вывода, то все операции страничного ввода-вывода выполняются только че- рез страничный каш. Ядро пытается выполнить все запросы чтения из страничного кэша. Если этого сделать не удается, то страница считывается с диска и добавляется в страничный кэш. Для операций записи страничный кэш выполняет роль "старто- вой площадки". Следовательно, все записанные страницы также добавляются в стра- ничный кэш. Базисное дерево Так как ядро должно проверять наличие страниц в страничном кэше перед тем, как запускать любую операцию страничного ввода-вывода, то этот поиск должен вы- полняться быстро. В противном случае затраты на поиск могут свести на нет все выгоды кэширования (по крайней мере, в случае незначительного количества удач- ных обращений в кэш, эти затраты времени будут сводить на нет все преимущества считывания данных из памяти по сравнению со считыванием напрямую с диска). Как было показано в предыдущем разделе, поиск в страничном кэше выполняется на основании информации объекта a d d r e s s _ s p a c e и значения смещения. Каждый объект a d d r e s s _ s p a c e имеет свое уникальное базисное дерево (radix tree), кото- рое хранится в поле p a g e t r e e . Базисное дерево - это один из типов бинарных де- ревьев. Базисное дерево позволяет выполнять очень быстрый поиск необходимой страницы только на основании значения смещения в файле. Функции поиска в стра- ничном кэше, такие как find_get_page () и r a d i x _ t r e e _ l o o k u p (), выполняют по- иск с использованием заданного дерева и заданного объекта. Страничный кэш и обратная запись страниц 335 Основной код для работы с базисными деревьями находится в файле l i b / r a d i x - t r e e . с . Для использования базисных деревьев необходимо подключить за- головочный файл Старая хеш-таблица страниц Для ядер до серии 2.6 поиск в страничном кэше не выполнялся с помощью базис- ных деревьев. Вместо этого поддерживалась глобальная хеш-таблица всех страниц памяти в системе. Специальная хеш-функция возвращала двухсвязный список значе- ний, связанных с одним значением ключа. Если нужная страница находится в кэше, то один из элементов этого списка соответствует этой нужной странице. Если стра- ница в кэше отсутствует, то хеш-функция возвращает значение NULL. Использование глобальной хеш-таблицы приводило к четырем основным проб- лемам. • Хеш-таблица защищалась одной глобальной блокировкой. Количество кон- фликтов при захвате этой блокировки было достаточно большим даже для не очень больших машин. В результате страдала производительность. • Размер хеш-таблицы был большим, потому что в ней содержалась информация обо всех страницах памяти в страничном кэше, в то время как важными явля- ются лишь страницы, связанные с одним конкретным файлом. • Производительность в случае неудачного обращения в кэш (когда искомая страница памяти не находится в кэше) падала из-за необходимости просматри- вать все элементы списка, связанного с заданным ключом. • Хеш-таблица требовала больше памяти, чем другие возможные решения. Применение в ядрах серии 2.6 страничного кэша на основании базисных дере- вьев позволило решить эти проблемы. |