WWW.DISSERS.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА

   Добро пожаловать!

Pages:     | 1 |   ...   | 5 | 6 || 8 | 9 |   ...   | 10 |

«; ...»

-- [ Страница 7 ] --

Виртуальная файловая система Уровень блочного ввода-вывода стройства блочного ввода-вывода (блочные устройства, устройства ввода-вы вода блоками, block devices) — это аппаратные устройства, которые позволяют У случайным образом (т.е. не обязательно последовательно) осуществлять доступ к фрагментам данных фиксированного размера, называемых блоками. Наиболее часто встречающееся устройство блочного впода-вывода — это жесткий диск, но существу ют и другие блочные устройства, например устройства работы с гибкими дисками, оптическими компакт-дисками (CD-ROM) и флеш-памятью. Следует обратить внима ние, что файловые системы монтируются с таких устройств. Именно таким образом обычно и осуществляется доступ к устройствам блочного ввода-вывода.

Другой фундаментальный тип устройства — это устройство посимвольного вво да-вывода (символьное устройство, character device, char device). Это— устройство, к которому можно обращаться, только как к последовательному потоку данных, т.е.

байт за байтом. Пример символьных устройств — это последовательный порт и кла виатура. Если же устройстпо позволяет обращаться к данным случайным образом (не последовательно), то это блочное устройство.

Существенное различие между этими типами устройств проявляется, в основном, в возможности случайного доступа к данным, т.е. в возможности производить поиск (seek) по устройству, перемещаясь из одной позиции в другую. Как пример, рассмо трим клавиатуру. Драйвер устройства клавиатуры на выходе выдает поток данных.

Когда печатают слово "fox", то драйвер клавиатуры возвращает поток данных, в ко тором три символа идут строго в указанном порядке. Считывание символов в другом порядке или считывание какого-нибудь другого символа, кроме следующего символа в потоке, имеет немного смысла. Поэтому драйвер клавиатуры — это устройство по символьного ввода-вывода, он позволяет на выходе получить поток символов, кото рые пользователь вводит на клавиатуре. Операция чтения данных с устройства воз вращает сначала символ "f", затем символ "о" и в конце символ "х". Когда нажатий клавиш нет, то поток— пустой. Жесткий диск же работает по-другому Драйвер жест кого диска может потребовать чтения содержимого определенного блока, а затем прочитать содержимое другого блока, и эти блоки не обязательно должны следовать друг за другом. Поэтому доступ к данным жесткого диска может выполняться случай ным образом, а не как к потоку данных, и поэтому жесткий диск— блочное устрой ство.

Управление блочными устройствами в ядре требует большего внимания, подго товки и работы, чем управление устройствами посимвольного ввода-вывода. Все это потому, что символьные устройства имеют всего одну позицию — текущую, в то вре мя как блочные устройства должны иметь возможность перемещаться туда и обрат но между любыми позициями на физическом носителе информации. Оказывается, что нет необходимости создавать в ядре целую подсистему для обслуживания сим вольных устройств, а для блочных устройств это необходимо. Такая подсистема не обходима отчасти из-за сложности блочных устройств. Однако основная причина та кой мощной поддержки в том, что блочные устройства достаточно чувствительны к производительности. Выжать максимум производительности из жесткого диска зна чительно важнее, чем получить некоторый прирост скорости при работе с клавиату рой. Более того, как будет видно дальше, сложность блочных устройств обеспечивает большой простор для таких оптимизаций. Предмет данной главы — как ядро управ ляет работой блочных устройств и запросами к этим устройствам. Рассматриваемая часть ядра называется уровнем, блочного ввода-вывода (block I/O layer). Интересно, что усовершенствование подсистемы блочного ввода-вывода было одной из целей раз рабатываемой серии ядра 2.5. В этой главе рассматриваются все новые особенности уровня блочного ввода-вывода, которые появились в ядрах серии 2.6.

Анатомия блочного устройства Наименьший адресуемый элемент блочного устройства называется сектором.

Размеры секторов — это числа, которые являются целыми степенями двойки, одна ко наиболее часто встречающийся размер — 512 байт. Размер сектора— это физи ческая характеристика устройства, а сектор — фундаментальный элемент блочного устройства. Устройства не могут адресовать или другим образом работать с элемен тами данных, размер которых меньше, чем один сектор, тем не менее многие блоч ные устройства могут передавать несколько секторов за один раз. Хотя большинство блочных устройств и имеет размер сектора, равный 512 байт, все же существуют и другие стандартные размеры сектора (например, большинство компакт-дисков CD-ROM имеют размер сектора, равный 2 Кбайт).

У программного обеспечения несколько другие цели, и поэтому там существует другая минимально адресуемая единица, которая называется блок. Блок— это аб стракция файловой системы, т.е. все обращения к файловым системам могут вы полняться только с данными, кратными размеру блока. Хотя физические устройства сами по себе адресуются на уровне секторов, ядро выполняет все дисковые опера ции в терминах блоков. Так как наименьший возможный адресуемый элемент —- это сектор, то размер блока не может быть меньше размера одного сектора и должен быть кратен размеру сектора. Более того, для ядра (так же как и для аппаратного обеспечения в случае секторов) необходимо, чтобы размер блока был целой степе нью двойки. Ядро также требует, чтобы блок имел размер, не больший, чем размер страницы памяти (см. главу 11, "Управление памятью" и главу 12, "Виртуальная фай ловая система")1.

Это ограничение является искусственным и в будущем оно может быть отменено. Тем не менее требование, чтобы размер блока был меньше или равен размеру страницы памяти, позволяет зна чительно упростить ядро.

294 Глава Поэтому размер блока равен размеру сектора, умноженному на число, которое яв ляется целой степенью двойки. Наиболее часто встречающиеся размеры блока— это 512 байт, один килобайт и четыре килобайта.

Часто сбивает с толку то, что некоторые люди называют секторы и блоки по разному. Секторы, наименьшие адресуемые элементы устройства, иногда называют "аппаратными секторами" (hardware sector) или "блоками аппаратного устройства" (device block). В то время как блоки, наименьшие адресуемые единицы файловых систем, иногда называются "блоками файловой системы" (filesyst.em block) или "бло ками ввода-вывода" (I/O block). В этой главе будут использованы термины "сектор" (sector) и "блок" (block), однако следует помнить и о других возможных названиях.

На рис. 13.1 показана диаграмма соответствия между секторами и блоками.

Существует еще одна часто встречающаяся терминология. По крайней мере в отношении.к жестким дискам, используются термины кластеры, (clusters), цилиндры (cylinder, дорожка) и головки (head). Такие обозначения являются специфическими только для некоторых типов блочных устройств, они, в основном, невидимы для пользовательских программ и в этой главе рассматриваться не будут. Причина, по которой секторы являются важными для ядра, состоит в том, что все операции вво да-вывода должны выполняться в единицах секторов. Поэтому более высокоуров невые концепции ядра, которые основаны на блоках, являются надстройками над секторами.

Жесткий диск Блок Сектор Сектор Отображение секторов на блоки i Рис. 13.1. Связь между секторами и бликами Буферы и заголовки буферов Когда блок хранится в памяти (скажем, после считывания или в ожидании запи си), то он хранится в структуре данных, называемой буфером (buffer). Каждый буфер связан строго с одним блоком. Буфер играет роль объекта, который представляет блок в оперативной памяти. Вспомним, что блок состоит из одного или больше сек торов, но по размеру не может быть больше одной страницы памяти. Поэтому одна страница памяти может содержать один или больше блоков. Поскольку для ядра требуется некоторая управляющая информация, связанная с данными (например, какому блочному устройству и какому блоку соответствует буфер), то каждый буфер связан со своим дескриптором. Этот дескриптор называется заголовком, буфера (buffer head) и представляется с помощью структуры st ruct buffer_head.

Уровень блочного ввода-вывода Структура buffer_head содержит информацию, которая необходима ядру для управления буферами и определена в файле .

Рассмотрим эту структуру с комментариями, которые описывают назначение каж дого поля.

. struct buffer_head { unsigned long b_state

;

/* флаги состояния буфера */ atomic_t b_count

;

/* счетчик использования буфера */ struct buffer_head *b_this_page

;

/* список буферов в текущей странице памяти */ struct page *b_page

;

/* соответствующая страница памяти */ sector_t b_blocknr

;

/* логический номер блока */ u32 b_size

;

/* размер блока (в байтах) */ char *b_data

;

/*указатель на буфер в странице памяти */ struct block_device *b_bdev

;

/* соответствующее блочное устройство */ bh_end_io_t *b_end_io

;

/* метод завершения ввода-вывода */ void *b_private

;

/* данные метода завершения */ struct list_head b_assoc_buffers

;

/* список связанных отображений */ }

;

Поле b_state содержит состояние определенного буфера. Это значение мо жет содержать один или несколько флагов, которые перечислены в табл. 13.1.

Возможные значения флагов описаны в виде перечисления bh_state_bits, кото рое описано в файле .

Таблица 13.1. Значения флагов поля bh_state Флаг состояния Назначение BH_Uptodate Буфер содержит действительные данные BH_Dirty Буфер изменен (содержимое буфера новее соответствующих данных на диске, и поэтому буфер должен быть записан на диск) BH_Lock Для буфера выполняется операция чтения-записи дисковых данных, и буфер заблокирован, чтобы предотвратить конкурентный доступ BH_Req Буфер включен в запрос BH_Mapped Буфер является действительным и отображается на дисковый блок BH_NEW Буфер только что выделен и к нему еще не было доступа BH_Async_ Read Для буфера выполняется асинхронная операция чтения вн_Азуnс Wri te Для буфера выполняется асинхронная операция записи вн_Dеlау С буфером еще не связан дисковый блок BH_Boundary Буфер является последним в последовательности смежных блоков — следующий за ним блок не является смежным с этой серией Перечисление bh_state_bits также содержит в качестве последнего элемента флаг BH_PrivateStart. Этот флаг не является разрешенным значением флага, а со ответствует первому биту, который можно использовать по усмотрению разработчи ков кода. Все биты, номер которых больше или равен значению BH_PrivateStart

;

не используются подсистемой блочного ввода-вывода и безопасно могут использо ваться драйверами, которым необходимо хранить информацию в поле b_state.

296 Глава Флаги, которые используются драйверами, могут быть определены на основании значения этого флага, что позволяет гарантированно избежать перекрытия с бита ми, которые официально используются уровнем блочного ввода-вывода.

Поле b_count — это счетчик использования буфера. Значение этого поля увели чивается и уменьшается двумя функциями, которые определены в файле следующим образом.

static inline void get_bh (struct buffer_head *bh) { atomic_inc{&bh->b_count)

;

} static inline void put_bh (struct buffer_head *bh) { atomic_dec (&bh->b_count)

;

} Перед тем как манипулировать заголовком буфера, необходимо увеличить значе ние счетчика использования с помощью функции get_bh(), что гарантирует, что во время работы буфер не будет освобожден. Когда работа с заголовком буфера за кончена, необходимо уменьшить значение счетчика, ссылок с помощью функции put_bh ().

Физический блок на жестком диске, которому соответствует буфер, — это блок с логическим номером b_bl ocknr, который находится на блочном устройстве b_bdev.

Физическая страница памяти, в которой хранятся данные буфера, соответствует значению поля b_page. Поле b_data — это указатель прямо на данные блока (ко торые хранятся где-то в странице памяти b_page), размер блока хранится в поле b_size. Следовательно, блок хранится в памяти, начиная с адреса b_data и закан чивая адресом (b_data + b_size).

Назначение заголовка буфера— это описание отображения между блоком на дис ке и буфером в физической памяти (т.е. последовательностью байтов, которые хра нятся в указанной странице памяти). Выполнение роли дескриптора отображения буфер-блок —единственное назначение этой структуры данных ядра.

В ядрах до серии 2.6 заголовок буфера был значительно более важной структурой данных. По существу, это была единица ввода-вывода данных в ядре. Он не только выполнял роль дескриптора для отображения буфер-блок-страница физической памя ти, но и выступал контейнером для всех операций блочного ввода-вывода. Это при водило к двум проблемам. Первая проблема заключалась в том, что заголовок буфе ра был большой и громоздкой структурой данных (сегодня он несколько уменьшился в размерах), а кроме того, выполнение операций блочного ввода-вывода в терминах заголовков буферов было непростой и довольно непонятной задачей. Вместо этого, ядру лучше работать со страницами памяти, что одновременно и проще и позволяет получить большую производительность. Использовать большой заголовок буфера, описывающий отдельный буфер (который может быть размером со страницу памя ти), — неэффективно. В связи с этим в ядрах серии 2.6 было сделано много работы, чтобы дать возможность ядру выполнять операции непосредственно со страницами памяти и пространствами адресов, вместо операций с буферами. Некоторые из этих операций обсуждаются в главе 15, "Страничный кэш и обратная запись страниц", где также рассматривается структура address_space и демоны pdflush.

Уровень блочного ввода-вывода Вторая проблема, связанная с заголовками буферов, — это то, что они описывают только один буфер. Когда заголовок буфера используется в качестве контейнера для операций ввода-вывода, то это требует, чтобы ядро разбивало потенциально боль шую операцию блочного ввода-вывода на множество мелких структур buffer_head, что в свою очередь приводит к ненужным затратам памяти для храпения структур данных. В результате, основной целью при создании серии ядра 2.5 была разработка нового гибкого и быстрого контейнера для операций блочного ввода-вывода. В ре зультат появилась структура bio, которая будет рассмотрена в следующем разделе.

Структура bi o Основным контейнером для операций ввода-вывода в ядре является структура bio, которая определена в файле . Эта структура представляет ак тивные операции блочного ввода-вывода в виде списка сегментов (segment). Сегмент — это участок буфера, который является непрерывным в физической памяти, т.е. от дельные буферы не обязательно должны быть непрерывными в физической памяти.

Благодаря тому, что буфер может представляться в виде нескольких участков, струк тура bio даст возможность выполнять операции блочного ввода-вывода, даже если данные одного буфера хранятся в разных местах памяти. Ниже показана структура bio с комментариями, описывающими назначение каждого поля.

struct bio { sector_t bi_sector

;

/* соответствующий сектор на диске */ struct bio *bi_next

;

/* список запросов */ struct block_device *bi_bdev

;

/* соответствующее блочное устройство */ unsigned long bi_flags

;

/* состояние и флаги команды */ unsigned long bi_rw

;

/* чтение или запись? */ unsigned short bi_vcnt

;

/* количество структур bio vec в массиве bi_io_vec */ unsigned short bi_idx

;

/* текущий индекс в массиве bi_io_vec */ unsigned short bi_phys_segments

;

/*количество сегментов после объединения */ unsigned short bi_hw_segments

;

/* количество сегментов после перестройки отображения */ unsigned int bi_size

;

/* объем данных для ввода-вывода */ unsigned int bi_hw_front_size

;

/* размер первого объединяемого сегмента */ unsigned int bi_hw_front_size

;

/* размер последнего объединяемого сегмента */ unsigned int bi_max_vecs

;

/* максимально возможное количество структур bio_vecs */ struct bio_vec *bi_io_vec

;

/* массив структур bio_vec */ bio_end_io_t *bi_end_io

;

/* метод завершения ввода-вывода */ atomic_t bi_cnb

;

/* счетчик использования */ void *bi_private

;

/* поле для информации создателя */ bio_destructor_t *bi_destructor

;

/* деструктор */ }

;

Главное назначение структуры bio — это представление активной (выполняющей ся) операции блочного ввода-вывода. В связи с этим большинство полей этой струк туры являются служебными. Наиболее важные поля — это bi_io_vecs, bi_vcnt и bi_idx.

298 Глава Структура bio bi_io_vec bi idx Массив структур biovec, содержащий bio_vcnt элементов bio_vec bio_vec bio vec bio_vec Структура page Структура page Структура page Сгруктура Структуры page, задействованные page в операции блочного ввода-вывода Рис. 13.2. Связь между структурами struct bio, struct b,io_vec u struct page Поле bi_io_vecs указывает па начало массива структур bio_vec, Эти структуры используются в качестве списка отдельных сегментов в соответствующей операции блочного ввода-вывода. Каждый экземпляр структуры bio_vec представляет собой вектор следующего вида: <страница памяти, смещение, размер>, который опи сывает определенный сегмент, соответственно страницу памяти, где этот сегмент хранится, положение блока — смещение внутри страницы — и размер блока. Массив рассмотренных векторов описывает весь буфер полностью. Структура bio_vec опре делена в файле следующим образом.

struct bio_vec { /* указатель на страницу физической памяти, где находится этот буфер */ struct page *bv_page

;

/* размер буфера в байтах */ Unsigned int bv_len

;

/* смещение в байтах внутри страницы памяти, где находится буфер */ unsigned int bv_offset

;

}

;

Для каждой операции блочного ввода-выпода создается массив из bi_vcnt эле ментов типа bio_vec, начало которого содержится в поле bi _io_vecs. В процессе выполнения операции блочного ввода-вывода поле bi_idx используется для указа ния па текущий элемент массива.

В общем, каждый запрос на выполнение блочного ввода-вывода представляется с помощью структуры bio. Каждый такой запрос состоит из одного или более бло ков, которые хранятся в массиве структур bio_vec. Каждая из этих структур пред ставляет собой вектор, который описывает положение в физической памяти каж дого сегмента запроса. На первый сегмент для операции ввода-вывода указывает поле bi_io_vec. Каждый следующий сегмент следует сразу за предыдущим. Всего Уровень блочного ввода-вывода в массиве bi_vcnt сегментов. В процессе того, как уровень блочного ввода-вывода обрабатывает сегменты запроса, обновляется значение поля bi_idx, чтобы его зна чение соответствовало номеру текущего сегмента. На рис. 13.2 показана связь между структурами bio, bio_vec и page.

Поле bi_idx указывает на текущую структуру bio_vec в массиве, что позволя ет уровню блочного ввода-вывода поддерживать частично выполненные операции блочного ввода-вывода. Однако более важное использование состоит в том, что драйверы таких устройств, как RAID (Redundant Array of Inexpensive/Independent Disks, массив недорогих/независимых дисковых устройств с избыточностью — спе циальный способ использования жестких дисков, при котором один логический том может быть распределен но нескольким физическим дискам для увеличения надеж ности или производительности), могут одну структуру bio, которая изначально была адресована одному устройству, разбивать на несколько частей, которые предназнача ются различным дискам RAID массива. Все, что необходимо сделать драйверу RAID, это создать необходимое количество копий структуры bio, которая предназначалась одному устройству, и изменить для каждой копии значение поля bi_idx, чтобы оно указывало на ту часть массива, откуда каждый диск должен начать свою операцию ввода-вывода.

Структура bi o содержит счетчик использования, который хранится в поле bi_cnt. Когда значение этого поля становится равным нулю, структура удаляется, и занятая память освобождается. Следующие две функции позволяют управлять счет чиком использования.

void bio_get(struct bio *bio) void bio_put(struct bio *bio) Первая увеличивает на единицу значение счетчика использования, а вторая уменьшает значение этого счетчика на единицу и, если это значение становится равным нулю, уничтожает соответствующую структуру bio. Перед тем как работать с активной структурой bio, необходимо увеличить счетчик использования, чтобы гарантировать, что экземпляр структуры не будет удален во время работы. После окончания работы необходимо уменьшить счетчик использования.

И наконец, поле bi o_pri vate — это поле данных создателя (владельца) структу ры. Как правило, это поле необходимо считывать или записывать только тому, кто создал данный экземпляр структуры bio.

Сравнение старой и новой реализаций Между заголовками буферов и новой структурой bi o существуют важные отли чия. Структура bio представляет операцию ввода-вывода, которая может включать одну или больше страниц в физической памяти. С другой стороны, заголовок буфера связан с одним дисковым блоком, который занимает не более одной страницы па мяти. Поэтому использование заголовков буферов приводит к ненужному делению запроса ввода-вывода на части, размером в один блок, только для того, чтобы их по том снова объединить. Работа со структурами bio выполняется быстрее, эта струк тура может описывать несмежные блоки и не требует без необходимости разбивать операции ввода-вывода на части.

Переход от структуры struct buffer_head к структурам st ruct bio позволяет получить также и другие преимущества.

300 Глава • Структура bio может легко представлять верхнюю память (см. главу 11), так как структура struct bio работает только со страницами физической памяти, а не с указателями.

• Структура bio может представлять как обычные страничные операции ввода вывода, так и операции непосредственного (direct) ввода-вывода (т.е. те, кото рые не проходят через страничный кэш

;

страничный кэш обсуждается в гла ве 15 ).

• Структура bio позволяет легко выполнять операции блочного ввода-вывода типа распределения-аккумуляции (scatter-gather), в которых данные находятся в нескольких страницах физической памяти.

• Структура bio значительно проще заголовка буфера, потому что она содержит только минимум информации, необходимой для представления операции блоч ного ввода-вывода, а не информацию, которая связана с самим буфером.

Тем не менее заголовки буферов все еще необходимы для функций, которые вы полняют отображение дисковых блоков на страницы физической памяти. Структура bio не содержит никакой информации о состоянии буфера, это просто массив век торов, которые описывают один или более сегментов данных одной операции блоч ного ввода-вывода, плюс соответствующая дополнительная информация. Структура buffer_head необходима для хранения информации о буферах. Применение двух отдельных структур позволяет сделать размер обеих этих структур минимальным.

Очереди запросов Для блочных устройств поддерживаются очереди запросов (request queue), в которых хранятся ожидающие запросы на выполнение операций блочного ввода-вывода.

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

Пока очередь запросов не пуста, драйвер блочного устройства, связанный с оче редью, извлекает запросы из головы очереди и отправляет их на соответствующее блочное устройство. Каждый элемент списка запросов очереди— это один запрос, представленный с помощью структуры struct request.

Запросы Отдельные запросы представляются с помощью структуры struct request, ко торая тоже определена в файле . Каждый запрос может состоять из более чем одной структуры bio, потому что один запрос может содержать обра щение к нескольким смежным дисковым блокам. Обратите внимание, что хотя бло ки на диске и должны быть смежными, данные этих блоков не обязательно должны быть смежными в физической памяти— каждая структура bio может содержать не сколько сегментов (вспомните, сегменты — это непрерывные участки памяти, в кото рых хранятся данные блока), а запрос может состоять из нескольких структур bio.

Уровень блочного ввода-вывода Планировщики ввода-вывода Простая отправка запросов на устройство ввода-вывода в том же порядке, в ко тором эти запросы направляет ядро, приводит к очень плохой производительности.

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

Минимизация количества операций поиска чрезвычайно критична для производи тельности всей системы.

Поэтому ядро не отправляет все запросы на выполнение операций блочного вво да-вывода жесткому диску в том же порядке, в котором они были получены, или сра зу же, как только они были получены. Вместо этого, оно выполняет так называемые операции слияния (объединения, merging) и сортировка (sorting), которые позволяют зна чительно увеличить производительность всей системы. Подсистема ядра, которая выполняет эти операции называется планировщиком ввода-вывода (J/0 scheduler).

Планировщик ввода-вывода разделяет дисковые ресурсы между ожидающими в очереди запросами впода-вывода. Это выполняется путем объединения и сортировки запросов ввода-вывода, которые находятся в очереди. Планировщик ввода-вывода не нужно путать с планировщиком выполнения процессов (см. главу 4, "Планирование выполнения процессов"), который делит ресурсы процессора между процессами си стемы. Эти две подсистемы похожи, но это— не одно и то же. Как планировщик выполнения процессов, так и планировщик ввода-вывода выполняют виртуализацию ресурсов для нескольких объектов. В случае планировщика процессов выполняется виртуализация процессора, который совместно используется процессами системы.

Это обеспечивает иллюзию того, что процессы выполняются одновременно, и яв ляется основой многозадачных операционных систем с разделением времени, таких как Unix. С другой стороны, планировщики ввода-вывода выполняют виртуализацию блочных устройств для ожидающих выполнения запросов блочного ввода-вывода.

Это делается для минимизации количества операций поиска по жесткому диску и для получения оптимальной производительности дисковых операций.

Задачи планировщика ввода-вывода Планировщик ввода-вывода работает управляя очередью запросов блочного устройства. Он определяет, в каком порядке должны следовать запросы в очере ди и в какое время каждый запрос должен передаваться на блочное устройство.

Управление производится с целью уменьшения количества операций поиска по жесткому диску, что значительно повышает общее быстродействие. Определение "об щее" здесь существенно. Планировщик ввода-вывода ведет себя "нечестно" по отно шению к некоторым запросам и за счет этого повышает производительность систе мы в целом.

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

302 Глава Планировщики ввода-вывода выполняют две основные задачи: объединение и со ртировку. Объединение — это слияние нескольких запросов в один. В качестве при мера рассмотрим запрос, который поставлен в очередь кодом файловой системы, например чтобы прочитать порцию данных из файла (конечно, на данном этапе все уже выполняется на уровне секторов и блоков, а не на уровне файлов). Если в очере ди уже есть запрос на чтение из соседнего сектора диска (например, другой участок того же файла), то два запроса могут быть объединены в один запрос для работы с одним или несколькими, расположенными рядом, секторами диска. Путем слия ния запросов планировщик ввода-вывода уменьятает затраты ресурсов, связанные с обработкой нескольких запросов, до уровня необходимого на обработку одного за проса. Наиболее важно здесь то, что диск выполняет всего одну команду и обслужи вание нескольких запросов может быть выполнено вообще без применения поиска.

Следовательно, слияние запросов уменьшает накладные расходы и минимизирует количество операций поиска.

Теперь предположим, что наш запрос на чтение помещается в очередь запросов, но там нет других запросов на чтение соседних секторов. Поэтому нет возможности выполнить объединение этого запроса с другими запросами, находящимися в очере ди. Можно просто поместить запрос в конец очереди. Но что если в очереди есть запросы к близкорасположенным секторам диска? Не лучше ли будет поместить но вый запрос в очередь где-то рядом с запросами к физически близко расположенным секторам диска. На самом деле планировщики ввода-вывода именно так и делают, Вся очередь запросов поддерживается в отсортированном состоянии по секторам, чтобы последовательность запросов в очереди (насколько это возможно) соответ ствовала линейному движению по секторам жесткого диска. Цель состоит в том, что бы не только уменьшить количество перемещений в каждом индивидуальном случае, но и минимизировать общее количество операций поиска таким образом, чтобы головка двигалась по прямой линии. Это аналогично алгоритму, который использу ется в лифте (elevator) — лифт не прыгает между этажами. Вместо этого он плавно пытается двигаться в одном направлении. Когда лифт доходит до последнего этажа в одном направлении, он начинает двигаться в другую сторону. Из-за такой аналогии планировщик ввода-вывода (а иногда только алгоритм сортировки) называют лифто вым планировщиком (алгоритмом лифта, elevator).

Лифтовой алгоритм Линуса Рассмотрим некоторые планировщики ввода-вывода, применяемые в реальной жизни. Первый планировщик ввода-вывода, который мы рассмотрим, называется Linus Elevator (лифтовой алгоритм Линуса). Это не опечатка, действительно существу ет лифтовой планировщик, разработанный Лисусом Торвальдсом и названный в его честь! Это основной планировщик ввода-вывода в ядре 2.4. В ядре 2.6 его заменили другими планировщиками, которые мы ниже рассмотрим. Однако поскольку этот ал горитм значительно проще новых и в то же время позволяет выполнять почти те же функции, то он заслуживает внимания.

Лифтовой алгоритм Линуса позволяет выполнять как объединение, так и со ртировку запросов. Когда запрос добавляется в очередь, вначале он сравнивается со всеми ожидающими запросами, чтобы обнаружить все возможные кандидаты па объединение. Алгоритм Линуса выполняет два типа объединения: добавление в начало Уровень блочного ввода-вывода запроса (front merging) и добавление в конец запроса (back merging). Тип объединения со ответствует тому, с какой стороны найдено соседство. Если новый запрос следует перед существующим, то выполняется вставка в начало запроса. Если новый запрос следует сразу за существующим — добавление выполняется в конец очереди. В связи с тем, что секторы, в которых хранится файл, расположены по мере увеличения но мера сектора и операции ввода-вывода чаще всего выполняются от начала файла до конца, а не наоборот, то при обычной работе вставка в начало запроса встречается значительно реже, чем вставка в конец. Тем не менее алгоритм Линуса проверяет и выполняет оба типа объединения, Если попытка объединения была неудачной, то определяется возможное место вставки запроса в очередь (положение в очереди, в котором новый запрос наилуч шим образом вписывается по номеру сектора между окружающими запросами). Если такое положение находится, то новый запрос помещается туда. Если подходящего места не найдено, то запрос помещается в конец очереди. В дополнение к этому, если в очереди найден запрос, который является достаточно старым, то новый за прос также добавляется в конец очереди. Это предотвращает ситуацию, в которой наличие большого количества запросов к близко расположенным секторам при водит к недостатку обслуживания других запросов. К сожалению, такая проверка "на старость" не очень эффективна. В рассмотренном алгоритме не предпринимает ся никаких попыток обслуживания запросов в заданных временных рамках, а просто прекращается процесс сортировки-вставки при наличии определенной задержки.

Это в свою очередь приводит к задержке в обслуживании, что было веской причи ной для доработки планировщика ввода-пывода ядра 2.4.

Итак, когда запрос добавляется в очередь возможны четыре типа действий. Вот эти действия в необходимой последовательности.

• Если запрос к соседнему сектору находится в очереди, то существующий за прос и новый объединяются в один.

• Если в очереди существует достаточно старый запрос, то новый запрос поме щается в конец очереди, чтобы предотвратить отказ обслуживания для других запросов, которые долгое время находятся в очереди.

• Если для секторов данного запроса в очереди существует позиция, которая со ответствует рациональному перемещению между секторами, то данный запрос помещается в эту позицию, что позволяет поддерживать очередь в отсортиро ванном состоянии.

• И наконец, если такая позиция не найдена, то запрос помещается в конец оче реди.

Планировщик ввода-вывода с лимитом по времени Планировщик ввода-выпода с лимитом по времени (Deadline I/O scheduler, dead line-планировщик ввода-вывода) разработан с целью предотвращения задержек об служивания, которые могут возникать для алгоритма Линуса. Если задаться целью только минимизировать количество операций поиска, то при большом количестве операций ввода-вывода из одной области диска могут возникать задержки обслужи вания для операций с другими областями диска, причем на неопределенное время.

Более того, поток запросов к одной и той же области диска может привести к тому, Глава что запросы к области диска, которая находится далеко от первой, никогда не будут обработаны. Такой алгоритм не может обеспечить равнодоступность ресурсов.

Хуже того, общая проблема задержки обслуживания запросов приводит к частной проблеме задержки обслуживания чтения при обслуживании записи (writes-starving-reads).

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

Проблему усугубляет то, что запросы чтения обычно зависят друг от друга.

Например, рассмотрим чтение большого количества файлов. Каждая операция чтения выполняется небольшими порциями, которые соответствуют размеру буфе ра. Приложение не станет считывать следующую порцию данных (или следующий файл), пока предыдущая порция данных не будет считана с диска и возвращена при ложению. Следовательно, если существует задержка в обслуживании одного запро са чтения, то для программы эти задержки складываются, и общая задержка может стать недопустимой. Принимая во внимание, что синхронность и взаимозависимость запросов чтения приводят к большим задержкам обработки этих запросов (что в свою очередь сильно влияет на производительность системы), в планировщике вво да-вывода с лимитом по времени были добавлены некоторые функции, которые по зволяют гарантированно минимизировать задержки в обработке запросов вообще и в обработке запросов чтения в частности.

Следует обратить внимание, что уменьшение времени задержки в обслуживании может быть выполнено только за счет уменьшения общего быстродействия системы.

Даже для алгоритма Линуса такой компромисс существует, хотя и в более мягкой форме. Алгоритм Линуса мог бы обеспечить и большую общую пропускную способ ность (путем уменьшения количества операций поиска), если бы запросы всегда по мещались в очередь в соответствии с номерами секторов и не выполнялась проверка на наличие старых запросов и вставка в конец очереди. Хотя минимизация количе ства операций поиска и важна, тем не менее неопределенное время задержки тоже не очень хорошая вещь. Поэтому deadline-планировщик и выполняет много работы для уменьшения задержек в обслуживании. Достаточно сложно одновременно обе спечить равнодоступность и максимизировать общую пропускную способность.

Однако все же не желательно задерживать операции записи на неопределенное время..Запросы записи также должны немедленно отправляться на диск, но это не так критично, как в случае за просов чтения.

Уровень блочного ввода-вывода В планировщике ввода-вывода, с лимитом по времени с запросом связано предель ное время ожидания (expiration time). По умолчанию этот момент времени равен 500 миллисекунд в будущем для запросов чтения и 5 секунд в будущем для запросов записи. Планировщик ввода-вывода с лимитом по времени работает аналогично планировщику Линуса — он также поддерживает очередь запросов в отсортирован ном состоянии в соответствии с физическим расположением сектора на диске. Эта очередь называется отсортированной (sorted queue). Когда запрос помещается в от сортированную очередь, то deadlme-планировщик ввода-вывода выполняет объеди нение и вставку запросов так же, как это делается в лифтовом алгоритме Линуса4.

Кроме того, планировщик с лимитом по времени помещает каждый запрос и во вто рую очередь, в зависимости от типа запроса. Запросы чтения помещаются в специ альную очередь FIFO запросов чтения, а запросы записи— в очередь FIFO запросов записи. В то время как обычная очередь отсортирована по номеру сектора на диске, две очереди FIFO (first-in first-out— первым поступил, первым обслужен) сортируют ся по времени поступления запроса, так как новые запросы всегда добавляются в ко нец очереди. При нормальной работе deadline-планировщик ввода-вывода получает запросы из головы отсортированной очереди и помещает их в очередь диспетчери зации. Очередь диспетчеризации отправляет запросы жесткому диску. Это приводит к минимизации количества операций поиска.

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

Диск Очередь FIFO запросов чтения Очередь FIFO запросов записи Очередь диспетчеризации Отсортированная очередь Рис. 13.3. Три очереди планировщика ввода-вывода с лимитом по времени Следует заметить, что deadline-плаиировщик ввода-вывода не дает строгой га рантии времени задержки запроса. Однако он, в общем, позволяет отправлять на обработку запросы до или вскоре после того, как истек их период ожидания. Это позволяет предотвратить ситуацию недостатка обслуживания запросов. Так как для запросов чтения максимальное время ожидания значительно больше, чем для запро сов записи, то планировщик с лимитом по времени также позполяет гарантировать, что обслуживание запросов записи не приведет к недостатку обслуживания запросов чтения. Больший приоритет запросов чтения позволяет минимизировать время за держки при операциях чтения.

Для deadline-планировщика операция вставки в начало запроса выполняется опционально. Обычно невыполнение вставки в начало запроса не приводит к проблемам, так как в большинстве случаев количество запросов, которые могут быть добавлены в начало, очень незначительно.

306 Глава Код планировщика ввода-вывода с лимитом по времени находится в файле drivers/block/deadline-iosched.с.

Прогнозирующий планировщик ввода-вывода Хотя планировщик с лимитом по времени ввода-вывода и выполняет работу по минимизации задержек чтения, это делается ценой уменьшения глобального бы стродействия. Рассмотрим систему с большой активностью записи. Каждый раз, ког да приходит запрос на чтение, планировщик сразу же начинает его выполнять. Это приводит к тому, что сразу же запускается операция поиска того места на диске, где будет выполнено чтение и сразу после выполнения чтения снова осуществляется по иск того места, где будет выполнена запись, и так повторяется при каждом запросе чтения. Большой приоритет запросов чтения вещь хорошая, но две операции поис ка на каждый запрос чтения (перемещение в точку чтения и обратно в точку записи) очень плохо сказываются на общей дисковой производительности. Цель прогнози рующего планировщика ввода-вывода (anticipatory I/O scheduler) — обеспечение хо роших характеристик по задержкам чтения и в то же время обеспечение отличной общей производительности.

Прогнозирующий планировщик построен на базе планировщика ввода-вывода с лимитом но времени. Поэтому он не особо отличается. В прогнозирующем плани ровщике реализованы три очереди (плюс очередь диспетчеризации) и обработка времени ожидания для каждого запроса, так же как и В случае deadline-планировщи ка. Главное отличие состоит в наличии дополнительного эвристического прогнозирова ния (anticipation heuristic).

Прогнозирующий планировщик ввода-вывода пытается минимизировать "шторм операций поиска", который следует за каждым запросом чтения во время выполне ния других дисковых операций. Когда поступает запрос на чтение, он обрабатывает ся в рамках своего времени ожидания как обычно. После того как запрос отправлен жесткому диску, прогнозирующий планировщик сразу не возвращается к выполне нию предыдущих запросов и не запускает операцию поиска сразу же. Он абсолютно ничего не делает в течение нескольких миллисекунд (значение этого периода време ни можно настраивать, а по умолчанию оно равно 6 миллисекунд). Существует боль шой шанс, что за эти несколько миллисекунд приложение отправит еще один запрос на чтение. Все запросы к соседним секторам диска выполняются немедленно. После.того как время ожидания истекает, прогнозирующий планировщик возвращается к выполнению ранее оставленных запросов и выполняет поиск соответствующего ме ста на диске.

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

Конечно, если в течение периода ожидания не было никакой активности, то про гнозирующий планировщик зря потратит эти несколько миллисекунд.

Уровень блочного ввода-вывода Ключевой момент для получения максимальной производительности от прогно зирующего планировщика — правильно предсказать действия приложений и файло вых систем. Это выполняется на основе эвристических алгоритмов и сбора статики.

Прогнозирующий планировщик ведет статистику операций блочного ввода-вывода по каждому процессу в надежде предсказать действия приложений. При достаточно высоком проценте точных предсказаний прогнозирующий планировщик способен значительно снизить затраты на поиск при выполнении операций чтения и в то же время уделить внимание тем запросам, которые критичны для производительности системы. Это позволяет прогнозирующему планировщику минимизировать задержки чтения и в то же время уменьшить количество и продолжительность операций по иска, что в свою очередь проявляется в уменьшении времени реакции системы и в увеличении ее производительности.

Код прогнозирующего планировщика находится в файле dr i ver s /bl ock/ as-iosched.с дерева исходных кодов ядра.

Этот планировщик используется в ядре Linux по умолчанию и хорошо работает для большинства типов нагрузки на систему. Он идеальный для серверов, однако ра ботает очень плохо в случае определенных типов загрузки, которые встречаются не очень часто, как, например, в случае баз данных, рассчитанных на большое количе ство операций поиска но диску.

Планировщик ввода-вывода с полностью равноправными очередями Планировщик ввода-вывода с полностью равноправными очередями (Complete Fair Queuing, CFQ) был разработан для определенного типа нагрузок на систему, по на практике он позволяет получить хорошую производительность для широкого диа пазона типов нагрузки. Он фундаментальным образом отличается от всех ранее рас смотренных планировщиков ввода-вывода.

Планировщик CFQ распределяет все приходящие запросы ввода-вывода по определенным очередям на основании того, какой процесс прислал этот запрос.

Например, запросы от процесса foo идут в очередь foo, запросы от процесса bar — в очередь bar. В пределах каждой очереди запросы объединяются со смежными и со ртируются. Таким образом очереди поддерживаются в отсортированном состоянии, так же как и в случае других планировщиков ввода-вывода. Отличие планировщика CFQ состоит в том, что он поддерживает отдельную очередь для каждого процесса, который выполняет операции ввода-вывода.

После этого планировщик CFQ выполняет запросы из разных очередей по круго вому алгоритму, выполняя конфигурируемое количество запросов (по умолчанию 4) из каждой очереди, перед тем как перейти к следующей. Это позволяет получить равномерное распределение пропускной способности диска для каждого процесса в системе. Предполагаемое использование такого планировщика— мультимедийные приложения, для которых он позволяет гарантировать, что, например, аудиопро игрыватель всегда будет успевать вовремя заполнять аудиобуферы с диска. Тем не менее планировщик CFQ на практике хорошо работает для многих сценариев загру женности системы.

308 Глава Код CFQ планировщика находится в файле drivers/block/cfq-iosched.с.

Этот планировщик рекомендуется для офисных компьютеров, хотя хорошо работает практически для всех типов нагрузок, за исключением, может быть, уж очень экстре мальных типов загруженности.

Планировщик ввода-вывода nоор Четвертый, и последний, тип планировщика ввода-вывода— это планировщик noop (no operation, с отсутствием операций). Он назван так потому, что практиче ски ничего не делает. Этот планировщик не выполняет никакой сортировки или других операций для предотвращения поиска по устройству. Ему нет необходимости выполнять ничего, включая алгоритмы, которые минимизируют задержки и были рассмотрены для предыдущих планировщиков.

Планировщик ввода-вывода nоор выполняет только объединение приходящих за просов со смежными, которые находятся в очереди. Кроме этого, больше никаких функций у данного планировщика нет. Он просто обслуживает очередь запросов, ко торые передаются драйверу блочного устройства, в режиме FIFO.

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

Код планировщика nоор находится в файле drivers/block/noop-iosched.с.

Он предназначен только для устройств с произвольным доступом.

Выбор планировщика ввода-вывода В ядрах серии 2.6 есть четыре планировщика ввода-вывода. Каждый из этих пла нировщиков может быть активизирован. По умолчанию все блочные устройства используют прогнозирующий планировщик ввода-вывода. Планировщик можно из менить, указав параметр ядра elevator=<плaниpoвщик> в командной строке при загрузке системы, где <планировщик> — это один из поддерживаемых типов плани ровщика, которые показаны в табл. 13.2.

Таблица 13.2. Возможные значения параметра el evator Значение Тип планировщика as Прогнозирующий cfq С полностью равноправными очередями deadline С лимитом по времени noop С отсутствием операций (nоор) Например, указание параметра elevator=cfq в командной строке ядра при за грузке системы означает, что для всех блочных устройств будет использоваться пла нировщик с полностью равноправными очередями.

Уровень блочного ввода-вывода Резюме В этой главе были рассмотрены основы работы устройств блочного ввода-вывода, а также структуры данных, используемые для работы уровня ввода-вывода блоками:

структура bio, которая представляет выполняемую операцию ввода-вывода

;

структу ра buffer_head, которая представляет отображение блоков на страницы памяти

;

структура request, которая представляет собой отдельный запрос ввода-вывода.

После рассмотрения запросов ввода-вывода был описан их короткий, но важный путь, кульминацией которого является прохождение через планировщик ввода-вы вода. Были рассмотрены дилеммы, возникающие при планировании операций вво да-вывода, и четыре типа планировщика, которые на данный момент существуют в ядре Linux, а также планировщик ввода вывода из ядра 2.4 — лифтовой алгоритм Линуса.

Далее мы рассмотрим адресное пространство процесса.

Глава Адресное пространство процесса главе 11, "Управление памятью", было рассказано о том, как ядро управляет физической намятью. В дополнение к тому, что ядро должно управлять своей В памятью, оно также должно, управлять и адресным пространством процессов— тем, как память видится для каждого процесса в системе. Операционная система Linux — это операционная система с виртуальной памятью (virtual memory operating system), т.е. для всех процессов выполняется виртуализация ресурсов памяти. Для каждого процесса создается иллюзия того, что он один использует всю физическую память в системе. Еще более важно, что адресное пространство процессов может быть даже значительно больше объема физической памяти. В этой главе рассказывается о том, как ядро управляет адресным пространством процесса.

Адресное пространство процесса состоит из диапазона адресов, которые выде лены процессу, и, что более важно, в этом диапазоне выделяются адреса, которые процесс может так или иначе использовать. Каждому процессу выделяется "плоское" 32- или 64-битовое адресное пространство. Термин "плоское" обозначает, что адрес ное пространство состоит из одного диапазона адресов (например, 32-разрядное адресное пространство занимает диапазон адресов от 0 до 429496729). Некоторые операционные системы предоставляют сегментированное адресное простран ство — адресное пространство состоит больше чем из одного диапазона адресов, т.е.

состоит из сегментов. Современные операционные системы обычно предоставляют плоское адресное пространство. Размер адресного пространства зависит от аппарат ной платформы. Обычно для каждого процесса существует свое адресное простран ство. Адрес памяти в адресном пространстве одного процесса не имеет никакого от ношения к такому же адресу памяти в адресном пространстве другого процесса. Тем не менее несколько процессов могут совместно использовать одно общее адресное пространство. Такие процессы называются потоками.

Значение адреса памяти — это заданное значение из диапазона адресов адресного пространства, как, например, 41021f000. Это значение идентифицирует определен ный байт в 32-битовом адресном пространстве. Важной частью адресного простран ства являются интервалы адресов памяти, к которым процесс имеет право доступа, как, например, 08048000-0804с000. Такие интервалы разрешенных адресов называ ются областями памяти (memory area). С помощью ядра процесс может динамически добавлять и удалять области памяти своего адресного пространства.

Процесс имеет право доступа только к действительным областям памяти. Более того, на область памяти могут быть установлены права только для чтения или запрет на выполнение. Если процесс обращается к адресу памяти, который не находится в действительной области памяти, или доступ к действительной области выполня ется запрещенным образом, то ядро уничтожает процесс с ужасным сообщением "Segmentation Fault" (ошибка сегментации).

Области памяти могут содержать следующую нужную информацию.

• Отображение выполняемого кода из выполняемого файла в область памяти процесса, которая называется сегментом кода (text section).

• Отображение инициализированных переменных из выполняемого файла в об ласть памяти процесса, которая называется сегментом данных (data section).

• Отображение страницы памяти, заполненной нулями, в область памяти про цесса, которая содержит неинициализированные глобальные переменные и называется сегментом bss1 (bss section). Нулевая страница памяти (zero page, стра ница памяти, заполненная нулями) — это страница памяти, которая полностью заполнена нулевыми значениями и используется, например, для указанной выше цели.

• Отображение страницы памяти, заполненной нулями, в память процесса, ко торая используется в качестве стека процесса пространства пользователя (не нужно путать со стеком процесса в пространстве ядра, который является от дельной структурой данных и управляется и используется ядром).

• Дополнительные сегменты кода, данных и BSS каждой совместно используемой библиотеки, таких как библиотека libc и динамический компоновщик, которые загружаются в адресное пространство процесса.

• Все файлы, содержимое которых отображено в память.

• Все области совместно используемой памяти.

• Все анонимные отображения в память, как, например, связанные с функцией malloc().

Каждое действительное значение адреса памяти в адресном пространстве про цесса принадлежит только и только одной области памяти (области памяти не перекрываются). Как будет показано, для каждого отдельного участка памяти в вы полняющемся процессе существует своя область: стек, объектный код, глобальные переменные, отображенный в память файл и т.д.

Термин "BSS" сложился исторически и является достаточно старым. Он означает block started by symbol (блок, начинающийся с символа). Неинициализированные переменные в выпол няемом файле не хранятся, поскольку с ними не связано никакого значения. Тем не менее стан дарт языка С требует, чтобы неинициализированным переменным присваивалось определенное значение по умолчанию (обычно все заполняется нулями). Поэтому ядро загружает переменные (без их значений) из выполняемого файла в память и отображает в эту память нулевую страницу, тем самым переменным присваивается нулевое значение без необходимости зря тратить место в объектном файле на ненужную инициализацию.

В более новых версиях библиотеки giibc фушщия mal l oc() реализована через системный вызов ттар(), а не через вызов brk().

312 Глава Дескриптор памяти Ядро представляет адресное пространство процесса в виде структуры данных, ко торая называется дескриптором памяти. Эта структура содержит всю информацию, которая относится к адресному пространству процесса. Дескриптор памяти пред ставляется с помощью структуры struct mm_struct, которая определена в файле <1inux/sched.h>3.

Рассмотрим эту структуру с комментариями, поясняющими назначение каждого поля.

struct mm_struct { struct vm_area_struct *mmap

;

/* список областей памяти */ struct rb_root mm_rb

;

/* красно-черное дерево областей памяти */ struct vm_area_struct *mmap_cache

;

/*последняя использованная область памяти */ unsigned long free_area_cache

;

/* первый незанятый участок адресного пространства */ pgd_t *pgd

;

/* глобальный каталог страниц */ atomic_t mm_users

;

/* счетчик пользователей адресного пространства */ atomic_t mm_count

;

/* основной счетчик использования */ int map_count

;

/* количество областей памяти */ struct rw_semaphore mmap_sem

;

/* семафор для областей памяти */ spinlock_t page_table_lock

;

/* спин-блокировка таблиц страниц */ struct list_head mmlist

;

/* список всех структур mm_struct */ unsigned long start_code

;

/* начальный адрес сегмента кода */ unsigned long end code

;

/* конечный адрес сегмента кода */ unsigned long start_data

;

/* начальный адрес сегмента данных */ unsigned long end_data

;

/* конечный адрес сегмента данных */ unsigned long start_brk

;

/* начальный адрес сегмента "кучи" */ unsigned long brk

;

/* конечный адрес сегмента "кучи" */ unsigned long start_stack

;

/* начало стека процесса */ unsigned long arg_start

;

/* начальный адрес области аргументов */ unsigned long arg_end

;

/* конечный адрес области аргументов */ unsigned long env_start

;

/*начальный адрес области переменных среды */ unsigned long env_end

;

/*конечный адрес области переменных среды */ unsigned long rss

;

/* количество физических страниц памяти */ unsigned long total_vm

;

/* общее количество страниц памяти */ unsigned long locked_vm

;

/* количество заблокированных страниц памяти */ unsigned long def_flags

;

/* флаги доступа, используемые по умолчанию */ unsigned long cpu_vm_mask

;

/*MacKa отложенного переключения буфера TLB */ unsigned long swap_address

;

/* последний сканированный адрес */ unsigned dumpable:l

;

/* можно ли создавать файл core? */ int used_hugetlb

;

/* используются ли гигантские страницы памяти (hugetlb)? */ Между дескриптором процесса, дескриптором памяти и соответствующими функциями существует тесная связь. Поэтому структура s t r uct mm_struct и определена в заголовочном файле sched.h.

Адресное пространство процесса mm_context_t context

;

/* данные, специфичные для аппаратной платформы */ int core_waiters

;

/* количество потоков, ожидающих на создание файла core */ struct completion *core_startup_donc

;

/* условная переменная начала создания файла core */ struct completion core_done

;

/* условная переменная завершения создания файла core */ rwlock_t ioctx_l.ist_lock

;

/* блокировка списка асинхронного ввода-вывода (AIO) */ struct kioctx *ioctx_list

;

/* список асинхронного ввода-вывода (AIO) V struct kioctx default kioctx

;

/* контекст асинхронного ввода вывода, используемый по умолчанию */ }

;

Поле mm_users — это количество процессов, которые используют данное адрес ное пространство. Например, если одно и то же адресное пространство совмест но используется двумя потоками, то значение поля mm_users равно двум. Поле ram_count — это основной счетчик использования структуры mm_struct. Наличие пользователей структуры, которым соответствует поле mm_users, приводит к уве личению счетчика mm_count на единицу. В предыдущем примере значение поля mm_count равно единице. Когда значение поля mm_users становится равным нулю (т.е. когда два потока завершатся), только тогда значение поля mm_count уменьша ется на единицу. Когда значение поля mm_count становится равным нулю, то на соответствующую структуру mm_struct больше нет ссылок, и она освобождается, Поддержка двух счетчиков позволяет ядру отличать главный счетчик использова ния (mm_count) от количества процессов, которые используют данную структуру (mm_users).

Поля mmap и mm_rb — это два различных контейнера данных, которые содержат одну и ту же информацию: информацию обо всех областях памяти в соответству ющем адресном пространстве. В первом контейнере эта информация хранится в виде связанного списка, а во втором— в виде красно-черного бинарного дерева.

Поскольку красно-черное дерево — это разновидность бинарного дерева, то, как и для всех типов бинарного дерева, количество операций поиска заданного элемента в нем равно О(log (n) ). Более детальное рассмотрение красно-черных деревьев най дете в разделе "Списки и деревья областей памяти".

Хотя обычно в ядре избегают избыточности, связанной с введением нескольких структур для хранения одних и тех же данных, тем не менее в данном случае эта избыточность очень кстати. Контейнер mmap — это связанный список, который по зволяет очень быстро проходить по всем элементам. С другой стороны, контейнер mm_rb — это красно-черное дерево, которое очень хорошо подходит для поиска за данного элемента. Области памяти будут рассмотрены в этой главе несколько ниже, Все структуры mm_struct объединены в двухсвязный список с помощью нолей mmlist. Первым элементом этого списка является дескриптор памяти init_mm, ко торый является дескриптором памяти процесса ink. Этот список защищен от конку рентного доступа с помощью блокировки mmlist_lock, которая определена в фай ле kernel/fork.с. Общее количество дескрипторов памяти хранится в глобальной целочисленной переменной mmlist_nr, которая определена в том же файле.

314 Глава Выделение дескриптора памяти Указатель на дескриптор памяти, выделенный для какой-либо задачи, хранится в поле mm дескриптора процесса этой задачи. Следовательно, выражение current->rnm позволяет получить дескриптор памяти текущего процесса. Функция copy_mm() ис пользуется для копирования дескриптора родительского процесса в дескриптор по рожденного процесса во время выполнения вызова fоrk (). Структура mm_st ruct выделяется из слябового кэша mm_cachep с помощью макроса allocate_mm (). Это реализовано в файле kernel /f ork. с. Обычно каждый процесс получает уникаль ный экземпляр структуры mm_struct и соответственно уникальное адресное про странство.

Процесс может использовать одно и то же адресное пространство совместно со своими порожденными процессами, путем указания флага CLONE_VM при выполне нии вызова clone (). Такие процессы называются потоками. Вспомните из матери ала главы 3, "Управление процессами", что в операционной системе Linux в этом и состоит единственное существенное отличие между обычными процессами и потока ми. Ядро Linux больше никаким другим образом их не различает. Потоки с точки зрения ядра— это обычные процессы, которые просто совместно используют неко торые общие ресурсы.

В случае, когда указан флаг CLONE_VM, макрос allocate_mm() не вызывается, а в поле mm дескриптора порожденного процесса записывается значение указателя на дескриптор памяти родительского процесса. Это реализовано с. помощью следующе го оператора ветвления в функции сору_mm ().

if (clone_flags & CLONE_VM) { /* * current — это родительский процесс * tsk — это процесс, порожденный в вызове fork() */ atomic_inc(¤t->mm->mm_users)

;

tsk->mm = current->mm

;

} Удаление дескриптора памяти Когда процесс, связанный с определенным адресным пространством, завершает ся, то вызывается функция exit_mm(). Эта функция выполняет некоторые служеб ные действия и обновляет некоторую статистическую информацию. Далее вызыва ется функция input(), которая уменьшает на единицу значение счетчика количества пользователей mm_users для дескриптора памяти. Когда значение счетчика коли чества пользователей становится равным нулю, то вызывается функция mmdrop(), которая уменьшает значение основного счетчика использования mm_count. Когда и этот счетчик использования наконец достигает нулевого значения, то вызывается функция free_mm(), которая возвращает экземпляр структуры mm_struct в слябо вый кэш mm_cachep с помощью вызова функции kmem_cache_fгее(), поскольку де-.

скриптор памяти больше не используется.

Адресное пространство процесса Структура mm_struct и потоки пространства ядра Потоки пространства ядра не имеют своего адресного пространства процесса и, следовательно, связанного с ним дескриптора памяти. Значение поля mm для потока пространства ядра равно NULL. Еще одно определение потока ядра — это процесс, ко торый не имеет пользовательского контекста.

Отсутствие адресного пространства— хорошее свойство, поскольку потоки ядра вообще не обращаются к памяти в пространстве пользователя (действительно, к ка кому адресному пространству им обращаться?). Поскольку потоки ядра не обращают ся к страницам памяти в пространстве пользователя, им вообще не нужен дескрип тор памяти и таблицы страниц (таблицы страниц обсуждаются дальше в этой главе).

Несмотря на это, потокам пространства ядра все же нужны некоторые структуры данных, такие как таблицы страниц, чтобы обращаться к памяти ядра. Чтобы обе спечить потоки ядра всеми данными без необходимости тратить память на дескрип тор памяти и таблицы страниц, а также процессорное время на переключение на новое адресное пространство и так далее, каждый поток ядра использует дескрип тор памяти задания, которое выполнялось перед ним.

Когда процесс запланирован на выполнение, то загружается адресное простран ство, на которое указывает поле mm этого процесса. Поле acti ve_mm дескриптора процесса обновляется таким образом, чтобы указывать на новое адресное простран ство. Потоки ядра не имеют своего адресного пространства, поэтому значение поля mm для них равно NULL. Поэтому, когда поток ядра планируется на выполнение, ядро определяет, что значение ноля mm равно NULL, и оставляет загруженным предыдущее адресное пространство. После этого ядро обновляет поле active_mm дескриптора процесса для потока ядра, чтобы он указывал на дескриптор памяти предыдущего процесса. При необходимости поток ядра может использовать таблицы страниц предыдущего процесса. Так как потоки ядра не обращаются к памяти в пространстве пользователя, то они используют только ту информацию об адресном пространстве ядра, которая связана с памятью ядра и является общей для всех процессов.

Области памяти Области памяти (memory areas) представляются с помощью объектов областей памяти, которые хранятся в структурах типа vm_area_struct. Эта структура опре делена в файле . Области памяти часто называются областями вирту альной памяти (virtual memory area, или VMA).

Структура vm_area_struct описывает одну непрерывную область памяти в дан ном адресном пространстве. Ядро рассматривает каждую область памяти, как уни кальный объект. Для каждой области памяти определены некоторые общие свой ства, такие как права доступа и набор соответствующих операций. Таким образом, одна структура VMA может представлять различные типы областей памяти, напри мер файлы, отображаемые в память, или стек пространства пользователя. Это анало гично объектно-ориентированному подходу, который используется в подсистеме VFS (см.главу 12, "Виртуальная файловая система").

Ниже показана эта структура данных с комментариями, описывающими назначе ние каждого поля.

316 Глава struct vm_area_struct { struct mm_struct *vm_mm

;

/* соответствующая структура mm_struct */ unsigned long vm_start

;

/* начало диапазона адресов */ unsigned long vm_end

;

/* конец диапазона адресов */ struct vm_area_struct *vm_next

;

/* список областей VMA */ pgprot_t vm_page_prot

;

/* права доступа */ unsigned long vm_flags

;

/* флаги */ struct rb_node vm_rb

;

/* узел текущей области VMA */ union { /* связь с address_space->i_mmap, или i_mmap_nonlinear */ struct { struct list_head list

;

void *parent

;

struct vm_area_struct *head

;

} vm_set

;

struct prio_tree_node prio_tree_node

;

} shared

;

struct list_head anon_vma_node

;

/* анонимные области */ struct anon_vma *anon_vma

;

/* объект анонимной VMA */ struct vm_operations_struct *vm_ops

;

/* операции */ unsigned long vm_pgoff

;

/* смещение в файле */ struct file *vm_file

;

/* отображенный файл (если есть) */ void *vm_private_data

;

/* приватные данные */ }

;

Как уже было рассказано, каждый дескриптор памяти связан с уникальным диа пазоном (интервалом) адресов в адресном пространстве процесса. Поле vm_start — это начальный (минимальный) адрес, а поле vm_end— конечный (максимальный) адрес данного интервала. Следовательно, значение (vm_end - vm_start) — это размер (длина) интервала адресов в байтах. Интервалы адресов разных областей па мяти одного адресного пространства не могут перекрываться.

Поле vm_mm указывает па структуру mm_struct, связанную с данной областью VMA. Заметим, что каждая область VMA уникальна для той структуры mm_struct, с которой эта область связана. Поэтому, даже если два разных процесса отобража ют один и тот же файл на свои адресные пространства, то для каждого процесса создается своя структура vm_area_struct, чтобы идентифицировать уникальные области памяти каждого процесса. Следовательно, два потока, которые совместно используют адресное пространство, также совместно используют и все структуры vm_area_struct в этом адресном пространстве.

Флаги областей VMA Поле флагов vm_flags содержит битовые флаги, которые определены в файле . Они указывают особенности поведения и содержат описательную информацию о страницах памяти, которые входят в данную область памяти. В от личие от прав доступа, которые связаны с определенной физической страницей па мяти, флаги областей VMA указывают особенности поведения, за которые отвечает ядро, а не аппаратное обеспечение. Более того, поле vm_flags содержит информа цию, которая относится к каждой странице в области памяти или,.что то же самое, ко всей области памяти в целом. В табл. 14.1 приведен список возможных значений флагов vm_flags.

Адресное пространство процесса Таблица 14.1. Флаги областей VMA Флаг Влияние на область УМА и на ее страницы памяти VM_READ Из страниц памяти можно считывать информацию VM_WRITE В страницы памяти можно записывать информацию VM_EXEC Можно выполнять код, хранящийся в страницах памяти VM_SHARED Страницы памяти являются совместно используемыми VM_MAYREAD Можно устанавливать флаг VM_READ VM_MAYWRITE Можно устанавливать флаг VM_WRITE VM_MAYEXEC Можно устанавливать флаг VM_EXEC VM_MAYSHARE Можно устанавливать флаг VM_SHARED VM_GROWSDOWN Область памяти может расширяться "вниз" VM_GROWSUP Область памяти может расширяться "вверх" VM_SHM Область используется для разделяемой (совместно используемой) памяти VM_DENYWRITE В область отображается файл, в который нельзя выполнять запись VM_EXECUTABLE В область отображается выполняемый файл VM_LOCKED Страницы памяти в области являются заблокированными VM_IQ В область памяти отображается пространство ввода-вывода аппаратного устройства VM_SEQ_READ К страницам памяти, вероятнее всего, осуществляется последовательный доступ VM_RAND_READ К страницам памяти, вероятнее всего, осуществляется случайный доступ VM_DONTCOPY Область памяти не должна копироваться при вызове f ork () VM_DONTEXPAND Область памяти не может быть увеличена с помощью вызова remap () VM_RESERVED Область памяти не должна откачиваться на диск VM_ACCOUNT Область памяти является объектом, по которому выполняется учет ресурсов VM_HUGETLB В области памяти используются гигантские (hugetl b) страницы памяти VM_NONLINEAR Область памяти содержит нелинейное отображение Рассмотрим подробнее назначение наиболее интересных и важных флагов. Флаги VM_READ, VM_WRITE и VM_EXEC указыпают обычные права на чтение-запись и выпол нение для страниц памяти, которые принадлежат данной области памяти. При необ ходимости их можно комбинировать для формирования соответствующих прав до ступа. Например, отображение выполняемого кода процесса может быть выполнено с указанием флагов VM_READ и VM_EXEC, но никак не с указанием флага VM_WRITE.

С другой стороны, сегмент данных из выполняемого файла может отображаться с указанием флагов VM_READ и VM_WRITE, указывать при этом флаг VM_EXEC не име ет смысла. Файл данных, который отображается только для чтения, должен отобра жаться с указанием только флага VM_READ.

Флаг VM_SHARED указывает на то, что область памяти содержит отображение, которое может совместно использоваться несколькими процессами. Если этот флаг установлен, то такое отображение называют совместно используемым (shared mapping), что интуитивно понятно. Если этот флаг не установлен, то такое отобра жение доступно только одному процессу и оно называется частным отображением, (private mapping).

318 Глава Флаг VM_IO указывает, что область памяти содержит отображение области вво да-вывода аппаратного устройства. Этот флаг обычно устанавливается драйверами устройств при выполнении вызова mmap () для отображения в память области вво да-вывода аппаратного устройства. Кроме всего прочего, этот флаг указывает, что область памяти не должна включаться в файл core процесса. Флаг VM_RESERVED ука зывает, что область памяти не должна откачиваться на диск. Этот флаг также укалы вается при отображении на память областей ввода-вывода аппаратных устройств.

Флаг VM_SEQ_READ является подсказкой ядру, что приложение выполняет после довательное (т.е. линейное и непрерывное) чтение из соответствующего отображе ния. При этом ядро может повысить производительность чтения за счет выполнения упреждающего чтения (read-ahead) из отображаемого файла. Флаг VM_RAND_READ указывает обратное, т.е. приложение выполняет операции чтения из случайно вы бранных мест отображения (т.е. не последовательно). При этом ядро может умень шить или совсем отключить выполнение упреждающего чтения из отображаемого файла. Эти флаги устанавливаются с помощью системного вызова madvice () путем указания соответственно флагов MADV_SEQUENTIAL и MADV_RANDOM для этого вызо ва. Упреждающее чтение — это последовательное чтение несколько большего коли чества данных, чем было запрошено, в надежде на то, что дополнительно считанные данные могут скоро понадобиться. Такой режим полезен для приложений, которые считывают данные последовательно. Однако если считывание данных выполняется случайным образом, то режим упреждающего чтения не эффективен.

Операции с областями VMA Поле vm_ops структуры vm_area_struct содержит указатель на таблицу опера ций, которые связаны с данной областью памяти и которые ядро может вызывать для манипуляций с областью VMA. Структура vm_area_struct служит общим объ ектом для представления всех типов областей виртуальной памяти, а в таблице опе раций описаны конкретные методы, которые могут быть применены к каждому кон кретному экземпляру объекта.

Таблица операций представлена с помощью структуры vm_operations_struct, которая определена в файле следующим образом.

struct vm_operations_struct { void (*open) (struct vm_area_struct *)

;

void (*close) (struct vm_area_struct * )

;

struct page * (*nopage) (struct vm_area_struct *, unsigned long, int)

;

int (*populate) (struct vm_area struct *, unsigned long, unsigned long, pgprot_t, unsigned long, int)

;

}

;

Рассмотрим каждый метод в отдельности.

• void open (struct vm_area_struct *area) Эта функция вызывается, когда соответствующая область памяти добавляется в адресное пространство.

• void close(struct vm_area_struct *area) Эта функция вызывается, когда соответствующая область памяти удаляется из адресного пространства.

Адресное пространство процесса • struct page * nopage(struct vm_area_sruct *area, unsigned long address, int unused) Эта функция вызывается обработчиком прерывания из-за отсутствия страницы (page fault), когда производится доступ к странице, которая отсутствует в фи зической памяти.

• int populate {struct vm_area_struct *area, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock) Эта функция вызывается из системного вызова. remap_pages() для предвари тельного заполнения таблиц страниц области памяти (prefault) при создании нового отображения.

Списки и деревья областей памяти Как уже рассказывалось, к областям памяти осуществляется доступ с помощью двух структур данных дескриптора памяти: полей mmap и mm_rb. Эти две структу ры данных независимо друг от друга указывают на все области памяти, связанные с данным дескриптором памяти. Они содержат указатели на одни и те же структуры vm_area_struct, просто эти указатели связаны друг с другом по-разному.

Первый контейнер, поле mmap, объединяет все объекты областей памяти в одно связный список. Структуры vm_area_struct объединяются в список с помощью сво их полей vm_next. Области памяти отсортированы в порядке увеличения адресов (от наименьшего и до наибольшего). Первой области памяти соответствует струк тура vm area_st ruct, на которую указывает само поле mmap. Указатель на самую последнюю структуру равен значению NULL.

Второе поле, mm_rb, объединяет все объекты областей памяти в красно-чер ное (red-black) дерево. На корень дерева указывает поле mm_rb, а каждая структура vm_area st ruct присоединяется к дереву с помощью поля vm_rb.

Красно-черное дерево — это один из типов бинарного дерева. Каждый элемент красно-черного дерева называется узлом. Начальный узел является корнем дерева.

Большинство узлов имеет два дочерних узла: левый дочерний узел и правый дочер ний узел. Некоторые узлы имеют всего один дочерний узел, и, наконец, узлы, кото рые не имеют дочерних, называются листьями. Для любого узла все элементы дерева, которые находятся слева от данного узла, всегда меньше по своему значению, чем зна чение данного узла, а все элементы дерева, которые находятся справа от некоторого узла, всегда больше по значению, чем значение этого узла. Более того, каждому узлу присвоен цвет (красный или черный, отсюда и название этого типа деревьев) в соот ветствии со следующими двумя правилами: дочерние элементы красного узла являют ся черными и любой путь по дереву от узла к листьям должен содержать одинаковое количество черных узлов. Корень дерева всегда красный. Поиск, вставка и удаление элементов из такого дерева требуют количество операций порядка О (log (n) ).

Связанный список используется, когда необходимо пройти по всем узлам. Красно черное дерево используется, когда необходимо найти определенную область памяти адресного пространства. Таким образом, ядро использует избыточные структуры данных для обеспечения оптимальной производительности независимо от того, ка кие операции выполняются с областями памяти.

320 Глава Области памяти в реальной жизни Рассмотрим пример адресного пространства процесса и области памяти в этом адресном пространстве. Для этой цели можно воспользоваться полезной файловой системой /ргос и утилитой pmар (1). В качестве примера рассмотрим следующую простую прикладную программу, которая работает в пространстве пользователя. Эта программа не делает абсолютно ничего, кроме того, что служит примером.

int main(int argc, char *argv[]) return 0

;

} Рассмотрим список областей памяти из адресного пространства этого процесса.

Этих областей немного. Мы уже знаем, что среди них есть сегмент кода, сегмент данных сегмент bss. Если учесть, что эта программа динамически скомпонована с библиотекой функций языка С, то соответствующие области существуют также для модуля l i bc. so и для модуля ld.so. И наконец, среди областей памяти также есть стек процесса.

Результат вывода списка областей адресного пространства этого процесса из фай ла /proc//maps имеет следующий вид.

rml@phantasy:~$ cat /proc/1426/maps 00e80000-00faf000 r-xp 00000000 03:01 208530 /lib/tls/libc-2.3.2.so 00faf000-00fb2000 rw-p 0012fOOO 03:01 208530 /lib/tls/libc-2.3.2.so 00fb2000-00fb4000 rw-p 00000000 00:00 08048000-08049000 r-xp 00000000 03:03 439029 /home/rml/src/example 08049000-0804a000 rw-p 00000000 03:03 439029 /home/rml/src/example 40000000-40015000 r-xp 00000000 03:01 80276 /lib/ld-2.3.2.so 40015000-40016000 rw-p 00015000 03:01 80276 /lib/ld-2.3.2.so 4001e000-4001f000 rw-p 00000000 00:00 bfffe000-c0000000 rwxp fffffOOO 00:00 Информация об областях памяти выдается в следующем формате.

начало-конец права доступа смещение старший:младший номера устройства файловый индекс файл Утилита рmар (1)4 форматирует эту информацию в следующем, более удобочита емом виде.

rml@phantasy:~$ pmap example[1426] OOe8OOOO (1212 KB) r-xp (03:01 208530) /lib/tls/libc-2.3.2.so OOfafOOO (12 KB) rw-p (03:01 208530) /lib/tls/libc-2.3.2.so 00fb2000 (8 KB) rw-p (00:00 0) 08048000 (4 KB) r-xp (03:03 439029) /home/rml/src/example 08049000 (4 KB) rw-p (03:03 439029) /home/rml/src/example 40000000 (84 KB) r-xp (03:01 80276) /lib/ld-2.3.2.so Утилита pmap(l) печатает форматированный список областей памяти процесса. Результат ее вы вода несколько более удобочитаем, чем информация, получаемая из файловой системы /ргос, но это одна и та же информация. Данная утилита включена в новые версии пакета procps.

Адресное пространство процесса 40015000 (4KB) rw-p (03:01 80276) /lib/ld-2.3.2.so 4001e000 (4 KB) rw-p (00:00 0) bfffeOOO (8 KB) rwxp (00:00 0) mapped: 1340 KB writable/private: 40 KB shared: 0 KB Первые три строчки соответствуют сегменту кода, сегменту данных и сегменту bss модуля libc.so (библиотека функций языка С). Следующие две строчки описы вают соответственно сегмент кода и сегмент данных выполняемого образа. Далее три строчки— описание сегментов кода, данных и bss модуля ld. so (динамический компоновщик). Последняя строчка описывает стек процесса.

Обратите внимание, что все сегменты кода имеют права на чтение и выполне ние, что и должно быть для выполняемых образов. С другой стороны, сегменты данных и bss, которые содержат глобальные переменные, помечаются как имеющие права на запись и чтение, а не на выполнение.

Все адресное пространство составляет порядка 1340 Кбайт, но только 40 Кбайт из них имеют право на запись и соответствуют частному отображению. Если область памяти является совместно используемой и не имеет прав на запись, то ядро хранит в памяти всего одну копию отображаемого файла. Это может показаться обычным для совместно используемых отображений

;

однако, случай, когда при этом еще и от сутствуют права на запись, проявляется несколько неожиданно. Если учесть факт, что когда на отображение нет прав записи, то соответствующая информация никог да не может быть изменена (из отображения возможно только чтение), становится ясно, что можно совершенно безопасно загрузить выполняемый образ в память все го один раз. Поэтому динамически загружаемая библиотека функций языка С и за нимает в памяти всего 1212 Кбайт, а не 1212 Кбайт, умноженное на количество про цессов, которые эту библиотеку используют. В связи с этим, процесс, код и данные которого имеют объем порядка 1340 Кбайт, на самом деле занимает всего 40 Кбайт физической памяти. Экономия памяти из-за такого совместного использования по лучается существенной.

Обратите внимание на области памяти, которые не имеют отображаемого файла, находятся на устройстве с номерами 00:00 и номер файлового индекса для которых равен нулю. Это отображение страницы, заполненной нулями (zero page, пулевая страница). Если отобразить страницу, заполненную нулями, на область памяти, ко торая имеет права на запись, то побочным эффектом является инициализация всех переменных в нулевые значения. Это важно, поскольку в таком случае получается область памяти, заполненная нулями, которая нужна для сегмента bss.

Каждой области памяти, связанной с процессом, соответствует структура vm_ area_struct. Так как процесс не является потоком (thread), то для него существует отдельная структура min_struct, на которую есть ссылка из структуры task_struct.

Работа с областями памяти Ядру часто необходимо определять, соответствует ли та или иная область па мяти в адресном пространстве процесса заданному критерию, например, существу ет ли заданный адрес в области памяти. Эти операции являются основой работы функции mmap (), которая будет рассмотрена в следующем разделе, и выполнять их приходится часто. Несколько полезных для этого функций объявлены в файле .

322 Глава Функция find_vma() Функция f ind_vma () определена в файле mm/mmap.с.

Эта функция позволяет найти в заданном адресном пространстве ту первую об ласть памяти, для которой значение поля vm_end больше заданного адреса addr.

Другими словами, эта функция позволяет найти первую область памяти, которая со держит адрес addr или начинается с адреса, большего адреса addr. Если такой об ласти памяти не существует, то функция возвращает значение NULL.

В противном случае возвращается указатель на соответствующую структуру vm_area_struct. Обратите внимание, что найденная область VMA может начинать ся с адреса, большего адреса addr, и этот адрес не обязательно принадлежит, най денной области памяти. Результат выполнения функции find_vma () кэшируется в поле map_cache дескриптора памяти. Поскольку очень велика вероятность того, что после одной операции с областью памяти последуют еще операции с ней же, то процент попаданий в кэш получается достаточно большим (на практике получа ются значения порядка 30-40%). Проверка кэшированных результатов выполняется очень быстро. Если нужный адрес в кэше не найден, то выполняется поиск по всем областям памяти, связанным с заданным дескриптором. Этот поиск выполняется с помощью красно-черного дерева следующим образом.

struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr) { struct vm_area_struct *vma = NULL

;

if (mm) { vma = mm->mmap_cache

;

if (! (vma && vma->vm_end > addr && vma->vm start <= addr)) { struct rb node * rb_node

;

rb node = mm->mm_rb.rb_node

;

vma = NULL

;

while (rb_node) { struct vm_area_struct * vma_tmp

;

vma_tmp = rb_entry (rb_node, struct vm_area_struct, vm_rb)

;

if (vma_tmp->vm_end > addr) { vma = vma_tmp

;

if (vma_tmp->vm_start <= addr) break

;

rb_node = rb_node->rb_left

;

} else rb_node = rb_node->rb_right

;

} if (vma) mm->mmap_cache = vma

;

} } return vma

;

} Адресное пространство процесса Вначале выполняется проверка поля vma_cache на предмет того, содержит ли кэшированная область VMA необходимый адрес. Обратите внимание, что простая проверка того, является ли значение поля vm_end большим addr, не гарантирует что проверяемая область памяти является первой, в которой есть адреса, большие addr. Поэтому, для того чтобы кэш в этой ситуации оказался полезным, проверяе мый адрес должен принадлежать кэшированной области памяти. К счастью, это как раз и соответствует случаю выполнения последовательных операций с одной и той же областью VMA.

Если кэш не содержит нужную область VMA, то функция должна выполнять по иск по красно-черному дереву Это выполняется путем проверки узлов дерева. Если значение поля vma_end для области памяти текущего узла больше addr, то текущим становится левый дочерний узел, в противном случае — правый. Функция завершает свою работу, как только находится область памяти, которая содержит адрес addr.

Если такая область VMA не найдена, то функция продолжает поиск по дереву и воз вращает ту область памяти, которая начинается после адреса addr. Если вообще не найдена ни одна область памяти, то возвращается значение NULL.

Функция find_vma_prev() Функция find_vma_prev () работает аналогично функции find vma (), но до полнительно она еще возвращает последнюю область VMA, которая заканчивается перед адресом addr. Эта функция также определена в файле mma/mmap.c и объявле на в файле следующим образом.

struct vm_area_struct * find vma_prev (struct mm_struct *mm, unsigned long addr, struct vm_area_struct **pprev) Параметр pprev после возвращения из функции содержит указатель на предыду щую область VMA.

Функция find_VMA_intersection() Функция f ind_vma_intersection () возвращает первую область памяти, кото рая перекрывается с указанным интервалом адресов. Эта функция определена в фай ле следующим образом. Это функция с подстановкой тела.

static inline struct vm_area_struct * find_vma_intersection( struct mm_struct *mm, unsigned long start_addr, unsigned long end addr) { struct vm_area_struct *vma

;

vma = find_vma (mm, start_addr)

;

if (vma && end_addr <= vma->vm_start) vma = NULL

;

return vma

;

} Первый параметр — адресное пространство, в котором выполняется поиск, пара метр start_addr — это первый адрес интервала адресов, а параметр end_addr — по следний адрес интервала.

Очевидно, что если функция find_vma() возвращает значение NULL, то это же значение будет возвращать и функция f i nd_vma_i ntersecti on(). Если функция 324 Глава find_vma () возвращает существующую область VMA, то функция find_vma_inter section () возвратит ту же область только тогда, когда эта область не начинается после конца данного диапазона адресов. Если область памяти, которая возвращается функцией find_vma (), начинается после последнего адреса из указанного диапазо на, то функция f ind_vma_intersection () возвращает значение NULL.

Функции mmap() и do_mmap():

создание интервала адресов Функция do_mmap() используется ядром для создания нового линейного интер вала адресов. Говорить, что эта функция создает новую область VMA, — технически не корректно, поскольку если создаваемый интервал адресов является смежным с существующим интервалом адресов и у этих интервалов одинаковые права доступа, то два интервала объединяются в один. Если это невозможно, то создается новая об ласть VMA. В любом случае функция do_mmap() — это функция, которая добавляет интервал адресов к адресному пространству процесса, независимо от того, создается ли при этом новая область VMA или расширяется существующая.

Функция do_ramap() объявлена в файле следующим образом.

unsigned long do_mmap(struct file *file, unsigned long addr, unsigned long len, unsigned long prot, unsigned long flag, unsigned long offset) Эта функция выполняет отображение на память содержимого файла fi l e на чиная с позиции в файле offset

;

размер отображаемого участка равен len байт.

Значения параметров file и offset могут быть нулевыми, в этом случае отображе ние не будет резервироваться (сохраняться) в файле. Такое отображение называется анонимным (anonymous mapping). Если указан файл и смещение, то отображение назы вается отображением, файла в память (file-backed mapping).

Параметр addr указывает (точнее, всего лишь подсказывает), откуда начинать по иск свободного интервала адресов.

Параметр prot указывает права доступа для страниц памяти в данной области.

Возможные значение флагов зависят от аппаратной платформы и описаны в файле. Хотя на практике для всех аппаратных платформ определены флаги, приведенные в табл. 14.2.

Параметр flags позволяет указать все остальные флаги области VMA. Эти флаги также определены в и приведены в табл. 14.3.

Таблица 14.2. Флаги защиты страниц памяти Флаг Влияние на страницы памяти в созданном интервале адресов PROT_READ Соответствует флагу VM_READ PROT_WRITE Соответствует флагу VM_WRITE PROT_EXEC Соответствует флагу VM_EXEC PROT_NONE К страницам памяти нет доступа Адресное пространство процесса Таблица 14.3. Флаги защиты страниц памяти Флаг Влияние на созданный интервал адресов MAP_SHARED Отображение может быть совместно используемым MAP_PRIVATE Отображение не может быть совместно используемым MAP_FIXED Создаваемый интервал адресов должен начинаться с указанного адреса addr MAP_ANONYMOUS Отображение является анонимным, а не отображением файла MAP_GROWSDOWN Соответствует флагу VM_GROWSDOWN MAP_DENYWRIIE Соответствует флагу VM DENYWRITE MAP_EXECUTABLE Соответствует флагу VM_EXECUTABLE MAP_LOCKED Соответствует флагу VM_LOCKED MAP_NORESERVE Нет необходимости резервировать память для отображения MAP_POPULATE Предварительно заполнить (prefault) таблицы страниц MAP_NONBLOCK Не блокировать при операциях ввода-вывода Если какой-либо из параметров имеет недопустимое значение, то функция do_mmap() возвращает отрицательное число. В протипном случае создастся не обходимый интервал адресов. Если это возможно, то этот интервал объединяется с соседней областью памяти. Если это невозможно, то создается новая структура vm_area_struct, которая выделяется в слябовом кэше vm_area_cachep. После этого новая область памяти добавляется в связанный список и красно-черное дерево областей памяти адресного пространства с помощью функции vma_link(). Затем обновляется значение поля total_vm в дескрипторе памяти. В конце концов, функ ция возвращает начальный адрес вновь созданного интервала адресов.

Системный вызов 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 Глава Функции munmap() и do_munmap():

удаление интервала адресов Функция do_manmap() удаляет интервал адресов из указанного адресного про странства процесса. Эта функция объявлена в файле следующим об разом.

int do_munmap(struct mm_struct *mm, unsigned long start, size t_len) Первый параметр указывает адресное пространство, из которого удаляется ин тервал адресов, начинающийся с адреса st ar 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

;

} Таблицы страниц Хотя пользовательские программы и работают с виртуальной памятью, которая отображается на физические адреса, процессоры работают непосредственно с эти ми физическими адресами. Следовательно, когда приложение обращается к адресу виртуальной памяти, этот адрес должен быть конвертирован в физический адрес, чтобы процессор смог выполнить запрос. Соответствующий поиск выполняется с помощью таблиц страниц. Таблицы страниц работают путем разбиения виртуально го адреса на части. Каждая часть используется в качестве индекса (номера) записи в таблице. Таблица содержит или указатель на другую таблицу, или указатель на соот ветствующую страницу физической памяти.

Адресное пространство процесса В операционной системе 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 показана диаграмма того, как происходит перевод виртуального адреса в физический с помощью табли цы страниц.

PGD PMD РТЕ Страница физической Структура памяти mm_struct Структура page Рис. 14.1. Таблицы страниц Начиная с ядра версии 2.6.11 таблицы страниц в ОС Linux для 64-разрядных аппаратных платформ стали 4-уровневыми, что позволяет в полном объеме использовать все виртуальное адресное про странство. Для 32-разрядных аппаратных платформ осталось 3 уровня, как и раньше. — Примеч. ред.

328 Глава Каждый процесс имеет свои таблицы страниц (разумеется, потоки эти таблицы используют совместно). Поле pgd дескриптора памяти указывает на глобальный ка талог страниц. Манипуляции с таблицами и прохождение по ним требуют захвата блокировки page_table_lock, которая также находится в соответствующем де скрипторе памяти.

Структуры данных, связанные с таблицами страниц, сильно зависят от аппарат ной платформы и определены в файле.

Поскольку практически каждое обращение к страницам виртуальной памяти тре бует определения соответствующего адреса физической памяти, производительность операций с таблицами страниц является очень критичной. Поиск всех этих адресов в памяти должен всегда выполняться очень быстро. Чтобы посодействовать этому, большинство процессоров имеют буфер быстрого преобразования адреса (translation loo kaside buffer, или TLB), который работает, как аппаратный кэш отображения вирту альных адресов на физические. При обращении к виртуальному адресу процессор вначале проверяет, не кэшировано ли это отображение в TLB. Если обращение в кэш было удачным, то сразу же возвращается физический адрес. В противном случае поиск физического адреса выполняется с помощью таблиц страниц.

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

В такой схеме таблицы страниц будут совместно использоваться родительским и по рожденным процессами даже после выполнения вызова fork(). Если же родитель ский или порожденный процесс изменит некоторую запись таблицы страниц, то будет создана копия этой записи, и эти процессы больше не будут совместно исполь зовать данную запись. Совместное использование таблиц страниц позволит устра нить затраты, связанные с копированием таблиц страниц при вызове fork().

Заключение В этой главе была рассмотрена абстракция виртуальной памяти, которая предо ставляется каждому процессу. Было рассказано, как ядро представляет адресное пространство процесса (с помощью структуры struct mm_struct) и каким обра зом ядро представляет области памяти внутри этого адресного пространства (str uct vrn_area_struct). Также рассказывалось о том, как ядро создает (с помощью функции mmap()) и удаляет (с помощью функции munmap()) области памяти. Б кон це были рассмотрены таблицы страниц. Так как операционная система Linux — это система с виртуальной памятью, то все эти понятия очень важны для понимания работы системы и используемой модели процессов.

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

Адресное пространство процесса Страничный кэш и обратная запись страниц ядре операционной системы Linux реализован один главный дисковый кэш, который называется страничным (page cache). Назначение этого кэша— мини В мизировать количество дисковых операций ввода-вывода путем хранения в памяти тех данных, для обращения к которым необходимо выполнять дисковые операции, Эта глава посвящена рассмотрению страничного кэша.

Кэширование дисковых данных важно по двум причинам. Во-первых, доступ к диску выполняется значительно медленнее, чем доступ к памяти. Доступ к данным в памяти выполняется значительно быстрее, чем к данным на диске. Во-вторых, если к некоторым данным осуществлялся доступ, то с достаточно большой вероятностью к этим же данным в ближайшем будущем потребуется обратиться снова. Принцип, согласно которому операции обращения к некоторым данным имеют тенденцию группироваться друг с другом во времени, называется сосредоточенностью во време ни (temporal locality). Сосредоточенность во времени гарантирует, что если данные кэшируются при первом доступе к ним, то существует большая вероятность удачного обращения в кэш к этим данным в ближайшем будущем.

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

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

Рассмотрим те типы операций и данных, которые связаны со страничным кэшем.

Страничный кэш в основном пополняется при выполнении страничных операций ввода-вывода, таких как read() и write(). Страничные операции ввода-вывода вы полняются с целыми страницами памяти, в которых хранятся данные, что соответ ствует операциям с более, чем одним дисковым блоком. В страничном кэше данные файлов хранятся порциями. Размер одной порции равен одной странице памяти.

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

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

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

Страничный кэш Как следует из названия, страничный кэш (page cache) — это кэш страниц

;

памяти.

Соответствующие страницы памяти получаются в результате чтения и записи обыч ных файлов на файловых системах, специальных файлов блочных устройств и фай лов, отображаемых в память. Таким образом, в страничном кэше содержатся стра ницы памяти, полностью заполненные данными из файлов, к которым только что производился доступ. Перед выполнением операции страничного ввода-вывода, как, например, read()1, ядро проверяет, есть ли те данные, которые нужно считать, в страничном кэше. Если данные находятся в кэше, то ядро может быстро возвратить требуемую страницу памяти.

Объект address_space Физическая страница памяти может содержать данные из нескольких несмежных физических дисковых блоков2.

Как было показано в главе 12," Виртуальная файловая система", операции страничного ввода-вы вода непосредственно выполняются не системными вызовами read() и write(), а специфичными для файловых систем методами file->f_op->read() и file~>f_op->wriie().

Например, размер страницы физической памяти для аппаратной платформы х86 равен 4 Кбайт, в то время как размер дискового блока для большинства устройств и файловых систем равен 512 байт. Следовательно, в одной странице памяти может храниться 8 блоков. Блоки не обязатель но должны быть смежными, так как один файл может быть физически "разбросанным" по диску.

332 Глава Проверка наличия определенных данных в страничном кэше может быть затруд нена, если смежные блоки принадлежат совершенно разным страницам памяти.

Невозможно проиндексировать данные в страничном кэше, используя только имя устройства и номер блока, что было бы наиболее простым решением.

Более того, страничный кэш ядра Linux является хранилищем данных доста точно общего характера в отношении того, какие страницы памяти в нем могут кэшироваться. Первоначально страничный кэш был предложен в операционной системе System V (SVR 4) для кэширования только данных из файловых систем.

Следовательно, для управления страничным кэшем операционной системы SVR использовался эквивалент файлового объекта, который назывался struct vnode.

Кэш операционной системы Linux разрабатывался с целью кэширования любых объ ектов, основанных на страницах памяти, что включает множество типов файлов и отображений в память.

Для получения необходимой общности в страничном кэше операционной систе мы Linux используется структура address_space (адресное пространство), которая позволяет идентифицировать страницы памяти, находящиеся в кэше. Эта структура определена в файле следующим образом.

struct address_space { struct inode *host

;

/* файловый индекс, которому принадлежит объект */ struct radix_tree_root page_tree

;

/* базисное дерево всех страниц */ /* блокировка для защиты spinlock_ t tree_lock

;

поля page_tree */ /* количество областей памяти unsigned int i_mmap_wrltable

;

с флагом VM_SHARED */ /* список всех отображений */ struct prio_tree_root i_mmap

;

/* список областей памяти struct list_head i_mmap_nonlinear

;

с флагом VM_NONLINEAR */ /* Блокировка поля i_mmap */ spinlock_t i_mmap_lock

;

/* счетчик запросов truncate */ atomic_t truncate_counl

;

/* общее количество страниц */ unsigned long nrpages

;

/* смещения начала обратной записи */ pgoff_t writeback_index

;

/* таблица операций */ struct address_space_operations *a_ops

;

/* маска gfp_mask и флаги ошибок */ unsigned long flags

;

/* информация упреждающего чтения */ struct backing_dev_info *backing_dev_info

;

/* блокировка для частных отображений */ spinlock_t private_lock

;

/* список частных отображений */ struct list_head private_list

;

/* соответствующие буферы */ struct address_spacs *assoc_mapping

;

}

;

Поле i_mmap — это дерепо поиска по приоритетам для всех совместно использу емых и частных отображений. Дерево поиска по приоритетам— это хитрая смесь базисных и частично упорядоченных бинарных деревьев3.

Всего в адресном пространстве nrpages страниц памяти.

Реализация ядра основана на базисном дереве поиска по приоритетам, предложенном в работе Edward M. McCreight, опубликованной в журнале SIAM Journal of Computing, May 1985, vol. 14.

№2, P. 257-276.

Страничный кэш и обратная запись страниц Объект addressspace связан с некоторым другим объектом ядра, обычно с файловым индексом. Если это так, то поле host указывает на соответствующий фай ловый индекс. Если значение поля host равно NULL, то соответствующий объект не является файловым индексом

;

например, объект address_space может быть связан с процессом подкачки страниц (swapper).

Поле a_ops указывает на таблицу операгций с адресным пространством так же, как и в случае объектов подсистемы VFS. Таблица операций представлена с помо щью структуры struct 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)

;

}

;

Методы read_page и wri te_page являются наиболее важными. Рассмотрим шаги, которые выполняются при страничной операции чтения.

Методу чтения в качестве параметров передается пара значений: объект address_ space и смещение. Эти значения используются следующим образом для поиска не обходимых данных в страничном кэше.

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 Глава Наконец, необходимые данные могут быть считаны с диска, добавлены в стра ничный кэш и возвращены пользователю. Это делается следующим образом.

error = mapping->a_ops->readpage(file, page)

;

Операции записи несколько отличаются. Для отображаемых в память файлов при изменении страницы памяти система управления виртуальной памятью просто вызывает следующую функцию.

SetPageDirty(page)

;

Ядро выполняет запись этой страницы памяти позже с помощью вызова метода writepage (). Операции записи для файлов, открытых обычным образом (без ото бражения в память), выполняются более сложным путем. В основном, общая опе рация записи, которая реализована в файле 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}

;

Выполняется поиск необходимой страницы памяти в кэше. Если такая страница в кэше не найдена, то создается соответствующий элемент кэша. Затем вызывает ся метод prepare_write (), чтобы подготовить запрос на запись. После этого дан ные копируются из пространства пользователя в буфер памяти в пространстве ядра.

И наконец данные записываются на диск с помощью функции comnit_write ().

Поскольку все описанные шаги выполняются при всех операциях страничного ввода-вывода, то все операции страничного ввода-вывода выполняются только че рез страничный каш. Ядро пытается выполнить все запросы чтения из страничного кэша. Если этого сделать не удается, то страница считывается с диска и добавляется в страничный кэш. Для операций записи страничный кэш выполняет роль "старто вой площадки". Следовательно, все записанные страницы также добавляются в стра ничный кэш.

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

Как было показано в предыдущем разделе, поиск в страничном кэше выполняется на основании информации объекта address_space и значения смещения. Каждый объект address_space имеет свое уникальное базисное дерево (radix tree), кото рое хранится в поле paget r ee. Базисное дерево - это один из типов бинарных де ревьев. Базисное дерево позволяет выполнять очень быстрый поиск необходимой страницы только на основании значения смещения в файле. Функции поиска в стра ничном кэше, такие как find_get_page () и radix_tree_lookup (), выполняют по иск с использованием заданного дерева и заданного объекта.

Страничный кэш и обратная запись страниц Основной код для работы с базисными деревьями находится в файле l i b/ radix-tree.с. Для использования базисных деревьев необходимо подключить за головочный файл .

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

Использование глобальной хеш-таблицы приводило к четырем основным проб лемам.

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

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

• Производительность в случае неудачного обращения в кэш (когда искомая страница памяти не находится в кэше) падала из-за необходимости просматри вать все элементы списка, связанного с заданным ключом.

• Хеш-таблица требовала больше памяти, чем другие возможные решения.

Применение в ядрах серии 2.6 страничного кэша на основании базисных дере вьев позволило решить эти проблемы.

Буферный кэш В операционной системе Linux больше нет отдельного буферного кэша. В ядрах серии 2.2 существовало два отдельных кэша: страничный и буферный. В первом кэ шировалисы страницы памяти, а в другом — буферы. Эти два кэша не были объедине ны между собой. Дисковый блок мог находиться в обоих кэшах одновременно. Это требовало больших усилий по синхронизации двух кэшированных копий, не говоря уже о напрасной трате памяти.

Так было в ядрах серии 2.2 и более ранних, но начиная с ядер Linux серии 2. оба кэша объединили вместе. Сегодня существует только один дисковый кэш — стра ничный кэш.

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

336 Глава Демон pdflush Измененные (dirty, "грязные") страницы памяти когда-нибудь должны быть запи саны на диск. Обратная запись страниц памяти выполняется в следующих двух слу чаях.

• Когда объем свободной памяти становится меньше определенного порога, ядро должно записать измененные данные обратно на диск, чтобы освободить память.

• Когда несохраненные данные хранятся в памяти достаточно долго, то ядро должно их записать на диск, чтобы гарантировать, что эти данные не будут на ходиться в Несохраненном состоянии неопределенное время.

Эти два типа записи имеют разные цели. В более старых ядрах они выполнялись двумя разными потоками пространства ядра (см. следующий раздел). Однако в ядре 2.6 эту работу выполняет группа (gang ) потоков ядра pdflush, которые называются демонами фоновой обратной записи (или просто потоками pdflush). Ходят слухи, что название pdflush — это сокращение от "dirty page flush" ("очистка грязных стра ниц"). Не обращайте внимание на это сомнительное название, давайте лучше более детально рассмотрим, для чего нужны эти процессы.

Во-первых, потоки pdflush служат для записи измененных страниц на диск, ког да объем свободной памяти в системе уменьшается до определенного уровня. Цель такой фоновой записи— освобождение памяти, которую занимают незаписанные страницы, в случае недостатка физических страниц памяти. Уровень, когда начи нается обратная запись, может быть сконфигурирован с помощью параметра di r ty_background_ratio утилиты sysctl. Когда объем свободной памяти становится меньше этого порога, ядро вызывает функцию wakeup_bdf lush () для перевода в состояние выполнения потока pdflush, который пыполняет функцию обратной за писи измененных страниц памяти background_writeout (). Эта функция получает один параметр, равный количеству страниц, которые функция должна попытаться записать на диск.

Функция продолжает запись до тех пор, пока не выполнятся два следующих условия.

• Указанное минимальное количество страниц записано на диск.

• Объем свободной памяти превышает соответствующее значение параметра dirty_background_ratio.

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

Во-вторых, назначение демона pdflush — периодически переходить в состояние выполнения (независимо от состояния нехватки памяти) и записывать на диск очень Слово "gang" не является жаргонным. Этот термин часто используется в компьютерных науках, чтобы указать группу чего-либо, что может выполняться параллельно.

Да, название функции не совсем верное. Должно было бы быть wakeup_pdflush (). В следующем разделе рассказано, откуда произошло это название.

Страничный кэш и обратная запись страниц давно измененные страницы памяти. Это гарантирует, что измененные страницы не будут находиться в памяти неопределенное время. При сбоях системы будут потеря ны те страницы памяти, которые не были сохранены на диске, так как содержимое памяти после перегрузки не сохраняется. Следовательно, периодическая синхрони зация страничного кэша с данными на диске является важным делом. При загрузке системы инициализируется таймер, периодически возвращающий к выполнению по ток pdflush, который выполняет функцию wb_kupdate (). Эта функция выполняет обратную запись данных, которые были изменены более чем dirty_expire_centi seсs сотых секунды тому назад. После этого таймер снова инициализируется, чтобы сработать через di rt y_expi re_cent i secs сотых секунды. Таким образом потоки pdflush периодически возвращаются к выполнению и записывают на диск все из мененные страницы, данные в которых старше, чем указанный лимит.

Системный администратор может установить эти значения с помощью каталога /proc/sys/vrn и утилиты sysctl. Втабл. 15.1 приведен список всех соответствую щих переменных.

Таблица 15.1. Параметры для настройки демона pdf l ush Переменная Описание dirty_background_ratio Объем свободной оперативной памяти, при котором демон pdf l ush начинает обратную запись незаписанных данных dirty_expire_centisecs Время, в сотых долях секунды, в течение которого неза писанные данные могут оставаться в памяти, перед тем как демон pdfl ush не запишет их на диск при следующем периоде обратной записи dirty_ratio Процент от общей оперативной памяти, соответствующий страницам памяти одного процесса, при котором начинает ся обратная запись незаписанных данных • • i dirty_writeback_centisecs Насколько часто, в сотых долях секунды, процесс bdf l ush возвращается к выполнению для обратной записи данных laptop_mode Переменная булевого типа, которая включает или выключает режим ноутбука (см. следующий раздел) Код потока pdf l us h находится в файлах mm/page-wr i t eback. c и fs/ fs-writeback.с.

Режим ноутбука Режим ноутбука — это специальная политика обратной записи страниц с целью оптимизации использования батареи и продления срока ее работы. Это делается путем минимизации активности жестких дисков, чтобы они оставались в останов ленном состоянии по возможности долго. Конфигурировать этот режим можно с по мощью файла /proc/sys/vm/laptop_mode. По умолчанию в этом файле записано значение 0 и режим ноутбука выключен. Запись значения 1 в этот файл позволяет включить режим ноутбука.

В режиме ноутбука существует всего одно изменение в выполнении обратной записи страниц. В дополнение к обратной записи измененных страниц

;

памяти, когда они становятся достаточно старыми, демон pdflush также выполняет и все остальные операции дискового ввода-вывода, записывая все дисковые буферы на 338 Глава диск. Таким образом демон pdfl ush пользуется тем преимуществом, что диск уже запущен, а также он гарантирует, что в ближайшем будущем диск снова запущен не будет.

Такое поведение имеет смысл, когда параметры di r t y_expi re_cent i secs и di rty_wri teback_centi secs установлены в большие значения, скажем 10 минут.

При таких задержках обратной записи диск запускается не часто, а когда он все-таки запускается, то работа в режиме ноутбука гарантирует, что этот момент будет ис пользован с максимальной эффективностью.

Pages:     | 1 |   ...   | 5 | 6 || 8 | 9 |   ...   | 10 |



© 2011 www.dissers.ru - «Бесплатная электронная библиотека»

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