netlib.narod.ru | < Назад | Оглавление | Далее > |
В этой главе уже было несколько раз упомянута возможность отсутствия страницы в оперативной памяти — и действительно, если бы все нужные страницы всегда находились в оперативной памяти, виртуальная память вряд ли бы была нужна. Но в этой главе еще не рассматривалось подробно, что происходит, когда страница не находится в оперативной памяти. Когда процесс пытается обратиться к странице, которая не находится в оперативной памяти, модуль MMU активизирует ситуацию отсутствия страницы, которую ядро пытается разрешить. Ситуации отсутствия страницы возникают также, когда процесс нарушает защиту уровня страницы, например, пытается писать в области памяти, предназначенной только для чтения.
Поскольку ситуации отсутствия страницы возникают при любом недопустимом доступе к памяти, тот же механизм применяется и для поддержки страничного обмена по требованию. Страничным обменом по требованию называют чтение страниц с диска только тогда, когда к ним обращаются, то есть по требованию — еще один пример повышения эффективности за счет лени.
В частности, страничный обмен по требованию применяется для создания исполняемых модулей, которые загружаются в память по требованию. Для этого при первой загрузке программы в физическую память считывается только небольшая часть образа исполняемого модуля, а затем для получения других страниц исполняемого модуля ядро применяет страничный обмен по требованию (например, когда процесс первый раз переходит к подпрограмме). Если не считать патологических ситуаций, это всегда быстрее считывания сразу всей программы: диск работает медленно, и вся программа может никогда не потребоваться. И действительно, обычно вся программа и не нужна, поскольку в любом отдельно взятом сеансе работы основная часть средств большой программы не используется (фактически, это также справедливо и для большинства небольших и средних программ). Этим области применения исполняемых модулей со страничным обменом по требованию не исчерпываются: если все хорошо обдумать, можно понять, что страничный обмен по требованию может также применяться и для модулей ядра, но эти компоненты слишком важны.
6980: do_page_fault — это функция ядра, вызываемая при возникновении ситуаций отсутствия страницы (это установлено в строке 363). Когда возникает ситуация отсутствия страницы, процессор настраивает регистры процесса так, чтобы после разрешения этой ситуации процесс продолжил сою работу с команды, которая активизировала эту ситуацию. Таким образом, попытка доступа к памяти, вызвавшая нарушение, автоматически повторяется после того, как ядро обеспечивает возможность ее выполнения. Иначе, если этот отказ действительно вызван ошибкой и не может быть устранен, ядро сообщает об этом процессу-нарушителю. Если ситуация отсутствия страницы возникла в самом ядре, то применяется аналогичная, но не идентичная стратегия, как будет показано ниже.
6992: Регистр управления 2 (CR2) — это регистр процессора Intel, содержащий линейный адрес, который вызвал ситуацию отсутствия страницы. Этот адрес считывается непосредственно из указанного регистра в локальную адресную переменную.
7004: Функция find_vma (строка 33460) возвращает первую область VMA, диапазон адресов которой заканчивается после этого адреса. Как известно, одно это не гарантирует наличия данного адреса в области VMA: мы знаем, что адрес расположен перед концом области VMA, но он может также находиться перед началом VMA. Поэтому выполняется и эта проверка. Если адрес успешно проходит проверку, то есть находится в пределах VMA, управление передается вперед на метку good_area (строка 7023); вскоре будет дано ее описание.
7005: Если значение, возвращенное из функции find_vma, представляет собой NULL, то адрес находится после всех областей VMA данного процесса, другими словами, за пределами всей памяти, относящейся к данному процессу.
7009: И начало, и конец VMA расположены строго после адреса; следовательно, адрес находится перед этой областью VMA. Но еще не все потеряно. Если область VMA входит в число объектов такого типа, которые возрастают в сторону младших адресов, другими словами, если она является стеком, этот стек может просто вырасти в сторону младших адресов, чтобы захватить и этот адрес.
7011: Выполняется проверка бита 2 кода ошибки error_code, полученного от процессора. Этот бит устанавливается, если ситуация отсутствия страницы возникает в режиме пользователя, а не в режиме супервизора (ядра). В режиме пользователя функция do_page_fault проверяет, не находится ли данный адрес в пределах области стека, отведенной для процесса, в соответствии с установкой регистра ESP. (Это может произойти, если, например, код выходит за пределы массива, размещенного в стеке.) В режиме супервизора (ядра) последняя проверка не выполняется и просто предполагается, что ядро действует правильно.
7019: Выполняется расширение области VMA для включения нового адреса с использованием функции expand_stack (строка 15480), если это возможно. В случае успеха член vm_start объекта VMA будет скорректирован для включения адреса.
7023: Достижение метки good_area означает, что область VMA включает данный адрес, поскольку он либо уже находился в ней, либо для его включения был расширен стек.
Так или иначе, теперь можно проверить два самых младших бита кода ошибки error_code, которые включают дополнительную информацию о том, почему возникла ситуация отсутствия страницы. Бит 0 — это бит присутствия/защиты: если он равен 0, то страница просто не находилась в памяти; если он равен 1, то страница присутствовала в памяти, но попытка доступа противоречила установкам битов защиты уровня страницы. Бит 1 — это бит чтения/записи: 0 обозначает чтение, 1 — запись.
7025: Этот переключатель методически прорабатывает четыре возможности, представленные этими двумя битами:
7047: Выполняется запрос к функции handle_mm_fault (которая описана ниже), чтобы она выполнила попытку сделать эту страницу присутствующей в памяти. Если эта попытка терпит неудачу, выдается ошибка SIGBUS.
7062: Код очистки для большинства функций ядра не заслуживает особого внимания. Функция do_page_fault составляет исключение; мы рассмотрим ее код очистки более подробно. Эта метка, bad_area, достигается в любом из следующих обстоятельств:
7066: Если код пользователя допустил любую из перечисленных выше ошибок, в него отправляется ужасающее сообщение SIGSEGV — нарушение сегментации. (Обратите внимание, что здесь термин «сегментация» применяется по традиции, а не по смыслу: когда речь идет о процессоре, технически — это нарушение страничного обмена, а не обязательно нарушение сегментации.) Этот сигнал обычно приводит к уничтожению процесса, как описано в главе 6.
7075: В процессоре Pentium компании Intel (и в некоторых из его разновидностей) есть так называемая ошибка f00f, которая позволяет любому процессу остановить работу процессора, выполнив неверную команду 0xf00fc7c8. Здесь реализован обходной маневр, предложенный компанией Intel.
Часть таблицы дескрипторов прерываний IDT (interrupt descriptor table) (см. главу 6) была заранее отмечена как предназначенная только для чтения, поскольку это заставляет процессор активизировать ситуацию отсутствия страницы вместо останова процессора в случае выполнения недопустимой команды. Здесь do_page_fault проверяет, не находился ли адрес, который привел к возникновению ситуации отсутствия страницы, в том месте IDT, которое связано с выполнением этой недопустимой команды. Если это так и есть, процессор пытается обслуживать прерывание «Invalid Opcode» — ошибка в процессоре не позволяет выполнить это действие правильно, но данный код исправляет ситуацию, непосредственно вызывая функцию do_invalid_op. В ином случае, процессор никогда бы не пытался писать в IDT (то есть писать в ней, если она предназначена только для чтения), поэтому при неудачном завершении проверки в строке 7080 недопустимая команда так и не будет выполнена.
7086: Метка no_context будет достигнута в одном из следующих случаев:
Любая из этих ситуаций — это проблема ядра (часто вызываемая драйвером), а не ситуация отсутствия страницы, активизированная каким-то кодом пользователя. Если ядро (или драйвер) подготовлено к этой возможности путем предварительной настройки кода обработки ошибок, этот код будет найден и будет выполнен переход к нему с помощью некоторых уловок, которые не входят в тематику данной книги.
7097: Иначе, ядро пыталось обратиться к недопустимой странице и функция do_page_fault не знала, что с этим делать. Эта попытка все еще могла быть преднамеренной. Код запуска ядра проверяет, правильно ли работает защита записи MMU; ошибка, возникающая при такой проверке, не является настоящей ошибкой, и функция do_page_fault может просто вернуть управление.
7109: Ядро обратилось к недопустимой странице и функция do_page_fault не смогла устранить проблему. Функция do_page_fault выводит некоторую информацию с описанием проблемы и в строке 7129 прекращает работу самого ядра. Это приводит к останову всей системы, поэтому, естественно, к такому действию нельзя относиться легкомысленно. Однако, если дело дошло до этой точки, ядру уже ничем не поможешь.
7134: Последняя рассматриваемая метка — do_sigbus, которая достигается, только если функция handle_mm_fault не смогла обработать ситуацию. Этот случай относительно прост; в основном он сводится к отправке ошибки SIGBUS задаче-нарушителю и обратному переходу к метке no_context, если это произошло в режиме ядра.
32725: Вызывающая программа обнаружила необходимость обеспечить доступ к странице. Это — та страница, которая содержит адрес, и этот адрес принадлежит к VMA. Сама функция handle_mm_fault довольно проста, но лишь потому, что она построена на других макрокомандах и функциях, которые выполняют всю черную работу. Мы последовательно рассмотрим эти функции низкого уровня после описания данной функции.
32732: Выполняет поиск соответствующих входов каталога страниц и промежуточного каталога страниц (это платформа х86, поэтому, как описано выше, эти два каталога фактически совпадают).
32735: Получает или распределяет (если это возможно) таблицу страниц для данного входа промежуточного каталога страниц.
32737: Вызывает функцию handle_pte_fault для считывания страницы во вход таблицы страниц; если это выполняется успешно, вызывается также функция update_mmu_cache для обновления кэша MMU. Если управление достигает этой точки, это значит, что все прошло хорошо, и функция handle_mm_fault может вернуть ненулевое значение (чаще всего 1) в качестве подтверждения успеха. Если любой этап на этом пути оканчивается неудачей, управление переходит на строку 32744 и функция возвращает 0 в качестве обозначения неудачи.
11284: Эта макрокоманда просто делит адрес на PGDIR_SHIFT (это значение установлено директивой #define равным 22 в строке 11052), округляет в меньшую сторону и использует результат (верхние 10 битов перед сдвигом) в качестве индекса массива pgd, размещенного в объекте struct mm_struct. Поэтому это значение представляет собой вход каталога страниц, в котором расположен адрес соответствующей таблицы страниц.
Это эквивалентно выражению
&((mm)->pgd[(address) >> PGDIR_SHIFT]);
хотя, возможно, более эффективно.
11454: Поскольку промежуточные каталоги страниц на платформе х86 не предусмотрены, эта функция предельно проста: она лишь возвращает переданный ей указатель pgd, приведя его к иному типу. На других платформах она должна выполнять больше работы, по аналогии с pte_alloc.
11422: Функция pte_alloc получает два параметра: указатель на вход промежуточного каталога страниц, где находится желаемый адрес, и сам адрес. Извращенную логику этой функции будет проще объяснить, если мы приступим к ее описанию не сразу, поэтому пройдем по следующим номерам строк.
11425: Преобразует адрес в смещение в PMD почти непостижимым способом.
Эта строка заслуживает подробного описания. Для начала напомним, что каждый вход в PMD представляет собой указатель, а указатели в архитектуре х86 занимают 4 байта (здесь мы находимся в части кода, зависящей от архитектуры, поэтому можем делать такие предположения). К тому же, мы знаем из определения языка С, что строка
&pmd[middle_10_bits(address)]
(здесь автор для ясности ввел фиктивный массив pmd и функцию middle_10_bits — средние десять битов) эквивалентна
pmd + middle_10_bits(address)
которая, в свою очередь, представляет тот же адрес, что и
((char *) pmd) + middle_10_bits(address) * sizeof(pte_t*)
Здесь весь фокус состоит в том, чтобы понять, что эта последняя форма или, если быть точнее, та ее часть, которая следует за знаком +, больше всего напоминает команду, которая фактически вычисляется в строке 11425.
Чтобы убедиться в этом, вначале отметим, что
4 * (PTRS_PER_PTE - 1)
равно 4092 (величина PTRS_PER_PTE установлена директивой #define равной 1024 в строке 11059). В двоичном коде в числе 4092 установлены только младшие 12 битов, не считая последних двух. Это равно числу 1023, в котором установлены только 10 младших битов, сдвинутому влево на 2 бита. Затем имеется команда
(address >> (PAGE_SHIFT - 2))
которая сдвигает адрес на 10 битов вправо (величина PAGE_SHIFT установлена директивой #define равной 12 в строке 10790). Затем над этими двумя выражениями выполняется поразрядная операция AND. Чистый результат выглядит так:
((address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1)) << 2
Хотя это выражение все еще выглядит сложным, уже многое прояснилось: в нем адрес сдвигается на 12 битов вправо (для удаления части со смещением страницы), маскируются все биты, кроме младших 10 (для удаления части с индексом каталога страниц и сохранения только индекса промежуточного каталога страниц в младших 10 битах), а затем выполняется сдвиг полученного результат на 2 бита влево (что равносильно умножению на 4 — на число байтов в указателе — sizeof(pte_t*)). Однако более простой способ был бы, вероятно, немного медленнее, а в программировании ядра мы экономим циклы процессора везде, где это возможно. (Однако простой способ внешне не выглядит как более медленный: в версии ядра выполняются два сдвига, два вычитания и поразрядная операция AND, а в методе автора выполняются два сдвига и две поразрядные операции AND. В испытаниях, проведенных автором, оба эти варианта показали почти одинаковую скорость.)
Независимо от используемого метода, после этих вычислений сложение адреса с базовым адресом его таблицы PMD в формате unsigned long (что происходит в строке 11432 и в другом месте) позволяет определить вход для этого указателя в таблице РТЕ, связанной с первоначальным значением адреса.
11428: Если этот вход PMD не указывает ни на одну таблицу страниц, эта функция выполняет переход вперед, к метке getnew, для ее распределения.
11435: Попытка выбрать таблицу страниц из списка pte_quicklist с помощью вызова функции get_pte_fast (строка 11357). Это кэш таблиц страниц — идея состоит в том, что распределение таблиц страниц (которые сами являются просто отдельными страницами) происходит медленно, но распределение памяти из списка недавно освобожденных таблиц страниц происходит немного быстрее. Поэтому в коде часто выполняется освобождение таблиц страниц с помощью функции free_pte_fast (строка 11369), которая переводит таблицы страниц в список pte_quicklist, a не освобождает их на самом деле.
11439: Список pte_quicklist смог предоставить страницу для таблицы страниц. Таблица страниц введена в промежуточный каталог страниц и функция возвратила смещение этой страницы в таблице страниц.
11438: В кэше pte_quicklist не осталось ни одной страницы, поэтому функция pte_alloc вынуждена распределить страницу медленным способом, вызвав функцию get_pte_slow (строка 7216). Эта функция распределяет страницу с помощью функции __get_free_page и выполняет во многом такую же обработку, как и при получении страницы из кэша.
11430: Если вход PMD не равен 0, но является недействительным, функция pte_alloc выводит сообщение (с помощью вызова функции bad_pte, строка 7187) и отвергает попытку.
11432: Обычный случай, на который мы рассчитываем: функция pte_alloc возвращает указатель на таблицу РТЕ, которая содержит адрес.
32690: Функция handle_pte_fault пытается выбрать или создать отсутствующую таблицу РТЕ.
32702: Данный вход не был связан ни с одной страницей в физической памяти (строка 32700) и в действительности даже не был установлен (строка 32701). Поэтому вызывается функция do_no_page (строка 32633) для создания нового отображения страницы.
32704: Страница не присутствовала в памяти, но имела отображение, поэтому она должна находиться в пространстве свопинга. Для ее чтения обратно в память вызывается функция do_swap_page (строка 32569).
32708: Страница присутствовала в памяти, поэтому проблема, вероятно, была связана с тем, что ядро обработало ситуацию нарушения защиты страницы. Вначале функция handle_pte_fault отмечает, что к странице было выполнено обращение, с помощью pte_mkyoung (строка 11252).
32713: Если это был доступ для записи, а запись на странице не разрешена, функция handle_pte_fault вызывает функцию do_wp_page (строка 32401). Это именно та функция, которая выполняет копирование при записи, поэтому далее мы ее рассмотрим вкратце.
32715: Это был доступ для записи к странице, предназначенной для записи. Функция handle_pte_fault устанавливает бит страницы «dirty» (грязная), указывая, что она должна быть снова скопирована в пространство свопинга перед ее уничтожением.
32720: Затребованная страница теперь доступна вызвавшей программе для использования, поэтому функция handle_pte_fault возвращает ненулевое значение (а именно 1) для обозначения успешного выполнения.
11501: В архитектуре х86 функция update_mmu_cache — это пустая операция. Она находится здесь в качестве так называемой функции-заглушки, вызов которой осуществляется в соответствующих точках не зависящей от архитектуры части ядра, чтобы в версиях для разных архитектур ее можно было определять по мере необходимости.
32401: Как было упомянуто ранее, копирование при записи фактически реализовано здесь, поэтому рассмотрим эту функцию хотя бы вкратце. В функции tsk была предпринята попытка выполнить запись по адресу, который находился в пределах указанной области vma и контролировался переданным параметром page_table.
32410: Происходит вызов функции __get_free_page (строка 15364, которая просто возвращается назад и вызывает функцию __get_free_pages в строке 34696) для предоставления процессу новой страницы — это должна быть новая копия страницы, защищенной от записи. Обратите внимание, что это позволяет обеспечить переключение заданий. Любопытно отметить, что в этом коде не предусмотрена проверка того, смогла ли функция __get_free_page успешно распределить новую страницу: для него фактически может не потребоваться новая страница, как мы вскоре увидим, поэтому проверка откладывается до того момента, когда в этом появится смысл.
32422: Наращивается число «малых» ситуаций отсутствия страницы: ситуаций отсутствия страницы, которые были разрешены без обращения к диску.
32438: Существуют только два пользователя данной страницы, и одним из них является кэш свопинга — временный пул страниц, выведенных на диск, но еще не возвращенных в память. Страница просто удалена из кэша свопинга (с использованием функции delete_from_swap_cache, строка 37686) и теперь она имеет только одного пользователя.
32445: Либо эта страница была только что возвращена из кэша свопинга, либо она имела для начала только одного пользователя. Страница отмечается как предназначенная для записи и грязная (поскольку на ней была выполнена запись).
32448: Если новая страница была распределена ранее, она не понадобится: копия не нужна, поскольку страница имеет только одного пользователя. Функция do_wp_page освобождает новую страницу, а затем возвращает ненулевое значение в качестве свидетельства успеха.
32454: Страница имеет более одного пользователя и не могла быть просто возвращена в память из кэша свопинга. Поэтому в функции do_wp_page возникает потребность в получении новой страницы для копии. Если оказалось, что предыдущая попытка распределения страницы окончилась неудачей, теперь эта информация становится важной; функция do_wp_page должна возвратить отказ.
32459: Получение копии содержимого страницы с использованием функции copy_cow_page (строка 31814). Это обычно лишь вызов макрокоманды copy_page (строка 10801), которая представляет собой просто команду memcpy.
32460: Выполняется синхронизация содержимого старой и новой копии страницы с оперативной памятью с использованием функции flush_page_to_ram (строка 10900). Как и функция update_mmu_cache, эта функция в архитектуре х86 является пустой операцией.
32463: Как и прежде, обозначает страницу как предназначенную для записи и грязную, но оставляет нетронутыми все другие признаки защиты страницы (например, обозначение выполняемого кода) из содержащей ее области VMA.
32466: Этот вызов функции free_page (строка 15386, где просто происходит вызов функции free_pages, строка 34633) фактически не освобождает старую страницу, поскольку она имеет несколько пользователей — она только уменьшает на единицу число ссылок на старую страницу. Поскольку запрос вызвавшей программы был удовлетворен, функция do_wp_page может вернуть ненулевое значение в качестве свидетельства успеха.
netlib.narod.ru | < Назад | Оглавление | Далее > |