Главная страница
Навигация по странице:

  • Интерфейс слябового распределителя памяти

  • Честная игра со стеком

  • Второе издание


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница33 из 53
    1   ...   29   30   31   32   33   34   35   36   ...   53
    Рис. 11.1. Взаимоотношения между кэшами, слябами и объектами
    Каждый кэш представляется структурой kmem_cache_s. Эта структура содержит три списка s l a b _ f u l l , s l a b _ p a r t i a l и slab_empty, которые хранятся в структуре kmem_list3. Эти списки содержат все слябы, связанные с данным кэшем. Каждый сляб представлен следующей структурой s t r u c t s l a b , которая является дескрипто- ром сляба.
    struct slab {
    struct list head list; /* список полных, частично заполненных или пустых слябов */
    unsigned long colouroff; /* смещение для окрашивания слябов */
    void *s_mem; /* первый объект сляба */
    unsigned int inuse; /* количество выделенных объектов */
    kmem_bufctl_t free; /* первый свободный объект, если есть*/
    };
    Дескриптор сляба выделяется или за пределами сляба, в кэше общего назначе- ния, или в начале самого сляба. Дескриптор хранится внутри сляба, либо если об- щий размер сляба достаточно мал, либо если внутри самого сляба остается достаточ- но места, чтобы разместить дескриптор.
    Слябовый распределитель создает новые слябы, вызывая интерфейс ядра нижне- го уровня для выделения памяти _ _ g e t _ f r e e _ p a g e s () следующим образом.
    static void *kmem getpagss(kmem cache_t *cachep, int flags, int nodeid)
    {
    struct page *page;
    void *addr;
    int i;
    250
    Глава 11
    Кэш
    Сляб
    Объект
    Объект
    Объект
    Объект
    Сляб
    Объект
    flags |= cachep->gfpflags;
    if (likely(nodeid == -1)) {
    addr = (void*)__get_free_pages(flags, cachep->gfporder);
    if (!addr)
    return NULL;
    page = virt_to_page (addr) ;
    } else {
    page = alloc_pages_node(nodeid, flags, cachep->gfporder);
    if (!page)
    return NULL;
    addr = page_address(page);
    }
    i = (1 << cachep->gfporder);
    if (cachep->flags & SLAB_RECLAIM_ACCOUNT)
    atomic_add(i, &slab_reclaim_pages);
    add_page_state(nr_slab, i);
    while (i--) {
    SetPageSlab(page);
    page++;
    }
    return addr;
    }
    Первый параметр этой функции указывает на определенный кэш, для которого нужны новые страницы памяти. Второй параметр содержит флаги, которые пре- даются в функцию _ _ g e t _ f r e e _ p a g e s ( ) . Следует обратить внимание на то, как значения этих флагов объединяются с другими значениями с помощью логической операции ИЛИ. Данная операция дополняет флаги значением флагов кэша, которые используются по умолчанию и которые обязательно должны присутствовать п значе- нии параметра flags. Количество страниц памяти — целая степень двойки — хранит- ся в поле cachep->gfporder. Рассмотренная функция выглядит более сложной, чем это может показаться сначала, поскольку она также рассчитана на NUMA-системы
    (Non-Uniform Memory Access, системы с неоднородным доступом к памяти). Если па- раметр n o d e i d на равен - 1 , то предпринимается попытка выделить память с того же узла памяти, на котором выполняется запрос. Такое решение позволяет получить более высокую производительность для NUMA-систем. Для таких систем обращение к памяти других узлов приводит к снижению производительности.
    Для образовательных целей можно убрать код, рассчитанный на NUMA-системы,
    и получить более простой вариант функции kraem_getpages () в следующем виде.
    static inline void * kmem_getpages(kmem_cache_t *cachep, unsigned long flags)
    {
    void *addr;
    flags |= cachep->gfpflags;
    addr = (void*) __get_free_pages(flags, cachep->gfporder);
    return addr;
    }
    Управление памятью 251

    Память освобождается с помощью функции kmem_freepages (), которая вызыва- ет функцию free_pages () для освобождения необходимых страниц кэша. Конечно,
    назначение уровня слябового распределения — это воздержаться от выделения и освобождения страниц памяти. На самом деле слябовый распределитель использует функции выделения памяти только тогда, когда в данном кэше не доступен ни один частично заполненный или пустой сляб. Функция освобождения памяти вызывается только тогда, когда становится мало доступной памяти и система пытается освобо- дить память или когда кэш полностью ликвидируется.
    Уровень слябового распределения управляется с помощью простого интерфейса,
    и это можно делать отдельно для каждого кэша. Интерфейс позволяет создавать или ликвидировать новые кэши, а также выделять или уничтожать объекты в этих кэ- шах. Все механизмы оптимизации управления кэшами и слябами полностью управля- ются внутренними элементами уровня слябового распределения памяти. После того как кэш создан, слябовый распределитель памяти работает, как специализированная система создания объектов определенного типа.
    Интерфейс слябового распределителя памяти
    Новый кэш можно создать с помощью вызова следующей функции.
    kmern_cache_t * kmem_cache_create (const char *name, size_t size,
    size_t offset, unsigned long flags,
    void (*ctor) (void*, kmem_cache_t *,unsigned long),
    void (*dtor) (void*, kmem_cache_t *,unsigned long))
    Первый параметр — это строка, которая содержит имя кэша. Второй параметр —
    это размер каждого элемента кэша. Третий параметр — это смещение первого объек- та в слябе. Он нужен для того, чтобы обеспечить необходимое выравнивание по гра- ницам страниц памяти. Обычно достаточно указать значение, равное нулю, которое соответствует выравниванию по умолчанию. Параметр f l a g s указывает опциональ- ные параметры, которые управляют поведением кэша. Он может быть равен нулю,
    что выключает все специальные особенности поведения, или состоять из одного или более значений, показанных ниже и объединенных с помощью логической опе- рации ИЛИ.
    • SLAB_NO_REAP — этот флаг указывает, что кэш не должен автоматически "уби- рать мусор" (т.е. освобождать память, в которой хранятся неиспользуемые объ- екты) при нехватке памяти в системе. Обычно этот флаг не нужно устанавли- вать, поскольку если этот флаг установлен, то созданный кэш может помешать нормальной работе системы при нехватке памяти.
    • SLAB_HWCACHE_ALIGN — этот флаг указывает уровню слябового распределения памяти, что расположение каждого объекта в слябе должно выравниваться по строкам процессорного кэша. Это предотвращает так называемое "ошибочное распределение", когда два или более объектов отображаются в одну и ту же строку системного кэша, несмотря на то что они находятся по разным адре- сам памяти. При использовании этого флага возрастает производительность,
    но это делается ценой увеличения занимаемой памяти, потому что строгое вы- равнивание приводит к тому, что часть памяти сляба не используется. Степень
    2 5 2 Глава 11
    увеличения занимаемой памяти зависит от размера объектов кэша и от того,
    каким образом происходит их выравнинание по отношению к строкам систем- ного кэша. Для кэшей, которые часто используются в коде, критичном к про- изводительности, будет правильным установить этот флаг, в остальных случаях следует подумать, стоит ли это делать.
    • SLAB_MUST_HWCACHE_ALIGN. Если включен режим отладки, то может оказаться невозможным одновременная отладка и выравнивание положения объектов по строкам системного кэша. Этот флаг указывает уровню слябового распределе- ния памяти, что необходимо форсировать выравнивание положения объектов по строкам системного кэша. В обычной ситуации этот флаг указывать необя- зательно, и достаточно использовать предыдущий. Установка этого флага в ре- жиме отладки слябового распределителя памяти (по умолчанию отладка запре- щена) может привести к значительному увеличению затрат памяти. Только для объектов, которые критичны к выравниванию по строкам системного кэша,
    таких как дескрипторы процессов, необходимо указывать данный флаг.
    • SLABPOSON— этот флаг указывает на необходимость заполнения слябов из- вестным числовым значением (а5а5а5а5). Эта операция называется "отравле- нием" (poisoning) и служит для отслеживания доступа к неинициализированной памяти.
    • SLAB_RED_ZONE — этот флаг указывает на необходимость выделения так назы- ваемых "красных зон" (red zone) для облегчения детектирования переполнений буфера.
    • SLAB_PANIC — этот флаг указывает на необходимость перевода ядра в состоя- ние паники, если выделение памяти было неудачным. Данный флаг полезен,
    если выделение памяти всегда должно завершаться успешно, как, например,
    в случае создания кэша структур VMA (областей виртуальной памяти, см. гла- ву 14, "Адресное пространство процесса") при загрузке системы.
    • SLAB_CACHE_DMA — этот флаг указывает уровню слябового распределения, что все слябы должны выделяться в памяти, с которой возможны операции пря- мого доступа к памяти. Это необходимо, когда объекты используются в опе- рациях ПДП и должны находиться в зоне ZONE_DMA. В противном случае эта возможность не нужна и этот флаг не нужно устанавливать.
    Два последних параметра c t o r и d t o r — это конструктор и деструктор кэша со- ответственно. Конструктор вызывается, когда в кэш добавляются новые страницы памяти. Деструктор вызывается, когда из кэша удаляются страницы памяти. Если указан деструктор, то должен быть указан и конструктор. На практике кэши ядра ОС
    Linux обычно не используют функции конструктора и деструктора. В качестве этих параметров можно указывать значение NULL.
    В случае успешного выполнения функция kmem_cache_create () возвращает ука- затель на созданный кэш. В противном случае возвращается NULL. Данная функция не может вызываться в контексте прерывания, так как она может переводить про- цесс в состояние ожидания. Для ликвидации кэша необходимо вызвать следующую функцию.
    int kmem_cache_destroy(kmem_cache_t *cachep)
    Управление памятью 253

    Эта функция ликвидирует указанный кэш. Она обычно вызывается при выгрузке модуля, который создает свой кэш. Из контекста прерывания эту функцию вызывать нельзя, так как она может переводить вызывающий процесс в состояние ожидания.
    Перед вызовом этой функции необходимо, чтобы были выполнены следующие два условия.
    • Все слябы кэша являются пустыми. Действительно, если в каком-либо слябе су- ществует объект, который все еще используется, то как можно ликвидировать кэш?
    • Никто не будет обращаться к кэшу во время и особенно после вызова функции kmem_cache_destroy (). Эту синхронизацию должен обеспечить вызывающий код.
    В случае успешного выполнения функция возвращает нуль, в других случаях воз- вращается ненулевое значение.
    После того как кэш создан, из него можно получить объект путем вызова следую- щей функции.
    void * kmem_cache_alloc(kmem_cache_t *cachep, int flags)
    Эта функция возвращает указатель на объект из кэша, на который указывает пара- метр cachep. Если ни в одном из слябов нет свободных объектов, то уровень слябо- вого распределения должен получить новые страницы памяти с помощью функции kmem_getpages (), значение параметра flags передается в функцию__get_free_
    pages (). Это те самые флаги, которые были рассмотрены ранее. Скорее всего, не- обходимо указывать GFP_KERNEL или GFP_ATOMIC.
    Далее для удаления объекта и возвращения его в сляб, из которого он был выде- лен, необходимо использовать следующую функцию.
    void kmem_cache_free(kmem_cache_t *cachep, void *objp)
    Данная функция помечает объект, на который указывает параметр objp, как сво- бодный.
    Пример использования слябового распределителя памяти
    Давайте рассмотрим пример из реальной жизни, связанный с работой со струк- турами t a s k _ s t r u c t (дескрипторы процессов). Показанный ниже код в несколько более сложной форме приведен в файле kernel/fork.с.
    В ядре определена глобальная переменная, в которой хранится указатель на кэш объектов t a s k _ s t r u c t :
    kmem_cache_t *task_struct_cachep;
    Во время инициализации ядра, в функции f o r k i n i t (), этот кэш создается сле- дующим образом.
    task_struct_cachep = kmem_cache_create("task_struct",
    sizeof(struct task_struct),
    ARCH_M1N_TASKALIGN,
    SLAB_PANIC,
    NULL,
    NULL);
    254 Глава 11

    Данный вызов создает кэш с именем " t a s k _ s t r u c t " , который предназначен для хранения объектов тина s t r u c t t a s k _ s t r u c t . Объекты создаются с начальным сме- щением в слябе, равным ARCH_MIN_TASKALIGN байт, и положение всех объектов вы- равнивается по границам строк системного кэша, значение этого выравнивания зави- сит от аппаратной платформы. Обычно значение выравнивания задается для каждой аппаратной платформы с помощью определения препроцессора LI_CACHE_BYTES,
    которое равно размеру процессорного кэша первого уровня в байтах. Конструктор и деструктор отсутствуют. Следует обратить внимание, что возвращаемое значение не проверяется на рапенство NULL, поскольку указан флаг SLAB_PANIC. В случае, когда при выделении памяти произошла ошибка, слябовый распределитель памяти вызо- вет функцию p a n i c ( ) . Если этот флаг не указан, то нужно проверять возвращаемое значение на равенство NULL, что сигнализирует об ошибке. Флаг SLAB_PANIC здесь используется потому, что этот каш является необходимым для работы системы (без дескрипторов процессов работать как-то не хорошо).
    Каждый раз, когда процесс вызывает функцию f o r k (), должен создаваться но- вый дескриптор процесса (вспомните главу 3, "Управление процессами"). Это выпол- няется следующим образом в функции d u p _ t a s k _ s t r u c t () , которая вызывается из функции do_fork () .
    struct task_struct *tsk;
    tsk = kmem_cache_alloc(task struct_cachep, GFP_KERNEL);
    if (!tsk)
    return NULL;
    Когда процесс завершается, если нет порожденных процессов, которые ожидают на завершение родительского процесса, то дескриптор освобождается и возвраща- ется обратно в кэш t a s k _ s t r u c t _ c a c h e p . Эти действия выполняются в функции f r e e _ t a s k _ s t r u c t (), как показано ниже (где параметр t s k указывает на удаляе- мый дескриптор).
    kmem_cache_free(task_struct_cachep, tsk);
    Так как дескрипторы процессов принадлежат к основным компонентам ядра и всегда необходимы, то кэш t a s k _ s t r u c t _ c a c h e p никогда не ликвидируется. Если бы он ликвидировался, то делать это необходимо было бы следующим образом.
    int err;
    err = kmem_cache_destroy (task_struct_cachep);
    if (err)
    /* ошибка ликвидации кэша */
    Достаточно просто, не так ли? Уровень слябопого распределения памяти скрыва- ет все низкоуровневые операции, связанные с выравниванием, "раскрашипанием",
    выделением и освобождением памяти, "сборкой мусора" в случае нехватки памяти.
    Коли часто необходимо создавать много объектов одного типа, то следует подумать об использовании слябового кэша. И уж точно не нужно писать свою реализацию списка свободных ресурсов!
    Управление памятью 255

    Статическое выделение памяти в стеке
    В пространстве пользователя многие операции выделения памяти, в частности некоторые рассмотренные ранее примеры, могут быть выполнены с использовани- ем стека, потому что априори известен размер выделяемой области памяти. В про- странстве пользователя доступна такая роскошь, как очень большой и динамически увеличивающийся стек задачи, однако в режиме ядра такой роскоши нет — стек ядра маленький и фиксирован по размеру. Когда процессу выделяется небольшой и фик- сированный по размеру стек, то затраты памяти уменьшаются и ядру нет необходи- мости выполнять дополнительные функции по управлению памятью.
    Размер стека зависит как от аппаратной платформы, так и от конфигурационных параметров, которые были указаны на этапе компиляции. Исторически размер сте- ка ядра был равен двум страницам памяти для каждого процесса. Это соответствует
    8 Кбайт для 32-разрядных аппаратных платформ и 16 Кбайт для 64-разрядных аппа- ратных платформ.
    В первых версиях ядер серии 2.6 была введена возможность конфигурации, для которой размер стека ядра равен одной странице памяти. Когда устанавливается та- кая конфигурация, то процесс получает стек, по размеру равный всего одной страни- це памяти: 4 Кбайт на 32-разрядных аппаратных платформах и 8 Кбайт — на 64-раз- рядных. Это сделано по двум причинам. Во-первых это уменьшает затраты памяти на одну страницу для каждого процесса. Во-вторых, что наиболее важно, при увели- чении времени работы системы (uptime) становится все тяжелее искать две физиче- ски смежные страницы памяти. Физическая память становится все более фрагмен- тированной, и нагрузка на систему управления виртуальной памятью при создании новых процессов становится все более существенной.
    Существует еще одна сложность (оставайтесь с нами, и Вы узнаете все о стеках ядра). Вся последовательность вложенных вызовов функций в режиме ядра должна поместиться в стеке. Исторически обработчики прерываний используют стек того процесса, выполнение которого они прервали. Это означает, что в худшем случае
    8 Кбайт стека должно использоваться совместно всеми вложенными вызовами функ- ций и еще парой обработчиков прерываний. Все это эффективно и просто, но это накладывает еще больше ограничений на использование стека ядра. Когда размер стека сократился до одной страницы памяти, обработчики прерываний туда пере- стали помещаться.
    Для решения указанной проблемы была реализована еще одна возможность — сте- ки обработчиков прерываний. Стеки прерываний представляют собой один стек на каждый процессор, которые используются для обработки прерываний. При такой конфигурации обработчики прерываний больше не используют стеки ядра тех про- цессов, которые этими обработчиками прерываются. Вместо этого они используют свои собственные стеки. Это требует только одну страницу памяти на процессор.
    Подведем итоги. Стек ядра занимает одну или две страницы памяти, в зависимо- сти от конфигурации, которая выполняется перед компиляцией ядра. Следовательно,
    размер стека ядра может иметь диапазон от 4 до 16 Кбайт. Исторически обработчи- ки прерываний совместно использовали стек прерванного ими процесса. При по- явлении стеков ядра размером в одну страницу памяти обработчикам прерываний были назначены свои стеки. В любом случае неограниченная рекурсия и использова- ние функций вроде alloca() явно не допустимы.
    256 Глава 11

    Честная игра со стеком
    В любой функции необходимо сокращать использование стека до минимума. Хотя не существует твердых правил, тем не менее следует поддерживать максимальный суммарный объем всех локальных переменных (также известных как автоматические переменные или переменные, выделенные в стеке) не больше нескольких сотен бай- тов. Опасно статически выделять большие объекты в стеке, такие как большие мас- сивы структур. В противном случае выделение памяти в стеке будет выполняться так же, как и в пространстве пользователя. Переполнение стека происходит незаметно и обычно приводит к проблемам. Так как ядро не выполняет никакого управления стеком, то данные стека просто перепишут все, что находится за стеком. В первую очередь пострадает структура thread_info, которая расположена в самом конце стека процесса (вспомните главу 3). За пределами стека все данные ядра могут про- пасть. В лучшем случае при переполнении стека произойдет сбой в работе машины.
    В худших случаях может произойти повреждение данных.
    В связи с этим, для выделения больших объемов памяти необходимо использо- вать одну из динамических схем выделения памяти, которые были рассмотрены раньше в этой главе.
    1   ...   29   30   31   32   33   34   35   36   ...   53


    написать администратору сайта