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

  • Постоянное отображение

  • Временное отображение

  • Работа с данными, связанными с процессорами, на этапе компиляции

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


    Скачать 3.09 Mb.
    НазваниеВторое издание
    Дата08.09.2019
    Размер3.09 Mb.
    Формат файлаpdf
    Имя файлаLav_Robert_Razrabotka_yadra_Linux_Litmir.net_264560_original_254.pdf
    ТипДокументы
    #86226
    страница34 из 53
    1   ...   30   31   32   33   34   35   36   37   ...   53
    Отображение верхней памяти
    По определению, страницы верхней памяти не могут постоянно отображаться в адресное пространство ядра. Поэтому страницы памяти, которые были выделены с помощью функции a l l o c _ p a g e s (), при использовании флага __GFP__HIGHMEM мо- гут не иметь логического адреса.
    Для аппаратной платформы х86 вся физическая память свыше 896 Мбайт помеча- ется как верхняя память, и она не может автоматически или постоянно отображать- ся в адресное пространство ядра, несмотря на то что процессоры платформы х86
    могут адресовать до 4 Гбайт физической памяти (до 64 Гбайт при наличии расшире- ния РАЕ
    6
    ). После выделения эти страницы должны быть отображены в логическое адресное пространство ядра. Для платформы х86 страницы верхней памяти отобра- жаются где-то между отметками 3 и 4 Гбайт.
    Постоянное отображение
    Для того чтобы отобразить заданную структуру page в адресное пространство ядра, необходимо использовать следующую функцию.
    void *kmap(struct page *page)
    Эта функция работает как со страницами нижней, так и верхней памяти. Если структура page соответствует странице нижней памяти, то просто возвращается вир- туальный адрес. Если страница расположена в верхней памяти, то создается посто- янное отображение этой страницы памяти и возвращается полученный логический
    6
    РАЕ — Physical Address Extension (расширение физической адресации). Эта функция процессоров х86 позволяет физически адресовать до 36 разрядов (64 Гбайт) памяти, несмотря на то что размер виртуального адресного пространства соответствует только 32 бит.
    Управление памятью 257
    адрес. Функция kmap () может переводить процесс в состояние ожидания, поэтому ее можно вызывать только в контексте процесса.
    Поскольку количество постоянных отображений ограничено (если бы это было не так, то мы бы не мучились, а просто отобразили всю необходимую память), то отображение страниц верхней памяти должно быть отменено, если оно больше не нужно. Это можно сделать с помощью вызова следующей функции.
    void kunmap(struct page *page)
    Данная функция отменяет отображение страницы памяти, связанной с параме- тром page.
    Временное отображение
    В случаях, когда необходимо создать о т о б р а ж е н и е с т р а н и ц памяти в адресное пространство, а текущий контекст не может переходить в состояние ожидания, ядро предоставляет функцию временного отображении (которое также называется атомар-
    ным отображением). Существует некоторое количество зарезервированных постоян- ных отображений, которые могут временно выполнять отображение "на лету". Ядро может автоматически отображать страницу верхней памяти в одно из зарезервиро- ванных отображений. Временное отображение может использоваться в коде, кото- р ы й не может переходить в состояние ожидания, как, например, контекст прерыва- ния, потому что полученное отображение никогда не блокируется.
    Установка временного отображения выполняется с помощью следующей функ- ции.
    void *kmap_atomic(struct page *page, enum km_type type)
    Параметр t y p e — это одно из значений показанного ниже перечисления, опреде- ленного в файле < a s m / k m a p _ t y p e s . h > , которое описывает цель временного отобра- жения.
    enum km_type {
    KM_BOUNCE_READ,
    KM_SKB_SUNRPC_DATA,
    KM_SKB_DATA_SOFTIRQ,
    KM_USER0,
    KM_USER1,
    KM_BIO_SRC_IRQ,
    KM_BIO_DST_IRQ,
    KM_PTE0,
    KM_PTE1,
    KM_PTE2,
    KM_IRQ0,
    KM_IRQ1,
    KM_SOFTIRQ0,
    KM_SOFTIRQ1,
    KM TYPE_NR
    );
    Данная функция не блокируется и поэтому может использоваться в контексте прерывания и в других случаях, когда нельзя перепланировать выполнение. Эта
    2 5 8 Глава 11
    функция также запрещает преемптивность ядра, что необходимо потому, что отобра- жения являются уникальными для каждого процессора (а перепланирование может переместить задание для выполнения на другом процессоре).
    Отменить отображение можно с помощью следующей функции.
    void kunmap_atomic(void *kvaddr, enum km_type type)
    Эта функция также не блокирующая. На самом деле для большинства аппаратных платформ она ничего не делает, за исключением разрешения преемптивности ядра,
    потому что временное отображение действует только до тех пор, пока не создано новое временное отображение. Поэтому ядро просто "забывает" о вызове функции kmap_atomic () , и функции kunmap atomic () практически ничего не нужно делать.
    Следующее атомарное отображение просто заменяет предыдущее.
    Выделение памяти, связанной с определенным процессором
    В современных операционных системах широко используются данные, связанные с определенными процессорами (per-CPU data). Это данные, которые являются уни- кальными для каждого процессора. Данные, связанные с процессорами, хранятся в массиве. Каждый элемент массива соответствует своему процессору системы. Номер процессора является индексом в этом массиве. Таким образом была реализована ра- бота с данными, связанными с определенным процессором, в ядрах серии 2.4. В та- ком подходе нет ничего плохого, поэтому значительная часть кода ядра в серии 2.6
    все еще использует этот интерфейс. Данные объявляются следующим образом,
    unsigned long my_percpu[NR_CPUS];
    Доступ к этим данным выполняется, как показано ниже.
    int cpu;
    cpu = get_cpu(); /* получить номер текущего процессора и запретить вытеснение в режиме ядра */
    my_percpu[cpu]++;
    printk("значение данных процессора cpu=%d равно %ld\n",
    cpu, my_percpu[cpu]);
    put_cpu(); /* разрешить вытеснение в режиме ядра */
    Обратите внимание, что не нужно использовать никаких блокировок, потому что данные уникальны для каждого процессора. Поскольку никакой процессор, кроме текущего, не может обратиться к соответствующему элементу данных, то не может возникнуть и никаких проблем с конкурентным доступом, а следовательно, текущий процессор может безопасно обращаться к данным без блокировок.
    Возможность вытеснения процессов в режиме ядра— единственное, из-за чего могут возникнуть проблемы. В преемптивном ядре могут возникнуть следующие две проблемы.
    • Если выполняющийся код вытесняется и позже планируется для выполнения на другом процессоре, то значение переменной cpu больше не будет действи- тельным, потому что эта переменная будет содержать номер другого процессо-
    Управление памятью 259
    pa. (По той же причине, после получения номера текущего процессора, нельзя переходить в состояние ожидания.)
    • Если некоторый другой код вытеснит текущий, то он может параллельно об- ратиться к переменной my_percpu на том же процессоре, что соответствует состоянию гонок за ресурс.
    Однако все опасения напрасны, потому что вызов функции g e t c p u (), которая возвращает номер текущего процессора, также запрещает вытеснение в режиме ядра. Соответствующий вызов функции put_cpu () разрешает вытеснение кода в ре- жиме ядра. Обратите внимание, что функция smp_processor_icl (), которая также позволяет получить номер текущего процессора, не запрещает вытеснения кода в режиме ядра, поэтому для безопасной работы следует использовать указанный выше метод.
    Новый интерфейс percpu
    В ядрах серии 2.6 предложен новый интерфейс, именуемый percpu, который слу- жит для создания данных и работы с данными, связанными с определенным процес- сором. Этот интерфейс обобщает предыдущий пример. При использовании нового подхода работа с per-CPU-данными упрощается.
    Рассмотренный ранее метод работы с данными, которые связаны с определен- ным процессором, является вполне законным. Новый интерфейс возник из необ- ходимости иметь более простой и мощный метод работы с данными, связанными с процессорами, на больших компьютерах с симметричной мультипроцессорностью.
    Все подпрограммы объявлены в файле < l i n u x / p e r c p u . h > . Описания же нахо- дятся в файлах mm/slab.с и .
    Работа с данными, связанными
    с процессорами, на этапе компиляции
    Описать переменную, которая связана с определенным процессором, на этапе компиляции можно достаточно просто следующим образом.
    DEFINE_PER_CPU(type, name);
    Это описание создает переменную типа t y p e с именем name, которая имеет ин- терфейс связи с каждым процессором в системе. Если необходимо объявить соот- ветствующую переменную с целью избежания предупреждений компилятора, то не- обходимо использовать следующий макрос.
    DECLARE_PER_CPU(type, name);
    Работать с этими переменными можно с помощью функций g e t _ c p u _ v a r () и p u t _ c p u _ v a r () . Вызов функции g e t _ c p u _ v a r () возвращает 1-значенис (левый операнд, 1-value) указанной переменной на текущем процессоре. Этот вызов так- же запрещает вытеснение кода в режиме ядра, а соответственный вызов функции put_cpu_var () разрешает вытеснение.
    260 Глава 1
    get_cpu_var(name)++; /* увеличить на единицу значение переменной name, связанное с текущим процессором */
    put_cpu_var(); /* разрешить вытеснение кода в режиме ядра */
    М о ж н о также получить доступ к переменной, связанной с другим процессором.
    per_cpu(name, cpu)++; /* увеличить значение переменной name на указанном процессоре */
    Использовать функцию per_cpu () необходимо осторожно, так как этот вызов не запрещает вытеснение кода и не обеспечивает никаких блокировок. Необходимость использования блокировок при работе с данными, связанными с определенным про- цессором, отпадает, только если к этим данным может обращаться один процессор.
    Если процессоры обращаются к данным других процессоров, то необходимо исполь- зовать блокировки. Будьте осторожны! Применение блокировок рассматривается в главе 8, "Введение в синхронизацию выполнения кода ядра", и главе 9, "Средства синхронизации в ядре".
    Необходимо сделать еще одно важное замечание относительно создания данных.
    связанных с процессорами, на этапе компиляции. Загружаемые модули не могут ис- пользовать те из них, которые объявлены не в самом модуле, потому что компонов- щик создает эти данные в специальных сегментах кода (а именно, . d a t a . p e r c p u ) .
    Если необходимо использовать данные, связанные с процессорами, в загружаемых модулях ядра, то нужно создать эти данные для каждого модуля отдельно или ис- пользовать динамически создаваемые данные.
    Работа с данными процессоров на этапе выполнения
    Для динамического создания данных, связанных с процессорами, в ядре реа- лизован специальный распределитель памяти, который имеет интерфейс, анало- гичный kmalloc () . Эти функции позволяют создать экземпляр участка памяти для каждого процессора в системе. Прототипы этих функций объявлены в файле
    следующим образом.
    void *alloc percpu(type); /* макрос */
    void *__alloc_percpu(size_t size, size_t align);
    void free_percpu(const void * ) ;
    Функция a l l o c percpu () создает экземпляр объекта заданного типа (выделяет память) для каждого процессора в системе. Эта функция является оболочкой вокруг функции __alloc_percpu (). Последняя функция принимает в качестве аргументов количество байтов памяти, которые необходимо выделить, и количество байтов,
    но которому необходимо выполнить выравнивание этой области памяти. Функция alloc_percpu () выполняет выравнивание по той границе, которая используется для указанного типа данных. Такое выравнивание соответствует обычному поведе- нию, как показано в следующем примере.
    struct rabid_cheetah = alloc_percpu(struct rabid_cheetah);,
    что аналогично следующему вызову.
    struct rabid_cheetah = __alloc_percpu(sizeof (struct rabid_cheetah),
    alignof (struct rabid_cheetah));
    Управление памятью 261

    Оператор _ _ a l i g n o f _ _ — это расширение, предоставляемое компилятором gcc,
    который возвращает количество байтов, по границе которого необходимо выпол- нять выравнивание (или рекомендуется выполнять для тех аппаратных платформ,
    у которых нет жестких требований к выравниванию данных в памяти). Синтаксис этого вызова такой же как и у оператора s i z e o f (). В примере, показанном ниже,
    для аппаратной платформы х86 будет возвращено значение 4.
    __alignof__ (unsigned long)
    При передаче 1-значения (левое значение, lvalue) возвращается максимально возможное выравнивание, которое может потребоваться для этого 1-значения.
    Например, 1-значение внутри структуры может иметь большее значение выравнива- ния, чем это необходимо для хранения того же типа данных за пределами структуры,
    что связано с особенностями выравнивания структур данных в памяти. Проблемы выравнивания более подробно рассмотрены в главе 19, "Переносимость".
    Соответствующий вызов функции f r e e p e r c p u () освобождает память, которую занимают соответствующие данные на всех процессорах.
    Функции a l l o c _ p e r c p u () и _ _ a l l o c _ p e r c p u () возвращают указатель, который используется для косвенной ссылки на динамически созданные данные, связанные с каждым процессором в системе. Для простого доступа к данным ядро предоставляет два следующих макроса.
    get_cpu_ptr(ptr) ; /* возвращает указатель типа void на данные,
    соответствующие параметру ptr, связанные с текущим процессом*/
    put_cpu_ptr(ptr); /* готово, разрешаем вытеснение кода в режиме ядра
    */
    Макрос g e t _ c p u _ p t r () возвращает указатель на экземпляр данных, связанных с текущим процессором. Этот вызов также запрещает вытеснение кода в режиме ядра,
    которое снова разрешается вызовом функции p u t _ c p u _ p t r ().
    Рассмотрим пример использования этих функций. Конечно, этот пример не со- всем логичный, потому что память обычно необходимо выделять один раз (напри- мер, в некоторой функции инициализации), использовать ее в разных необходимых местах, а затем освободить также один раз (например, в некоторой функции, кото- рая вызывается при завершении работы). Тем не менее этот пример позволяет по- яснить особенности использования.
    void *percpu_ptr;
    unsigned long *foo;
    percpu_ptr = alloc_percpu(unsigned long);
    if (!ptr)
    /* ошибка выделения памяти .. */
    foo = get_cpu_ptr(percpu_ptr);
    /* работаем с данными foo .. */
    put_cpu_ptr(percpu_ptr);
    Еще одна функция — p e r _ c p u _ p t r ( ) — возвращает экземпляр данных, связанных с указанным процессором.
    per_cpu_ptr(ptr, cpu);
    2 6 2 Глава 11

    Эта функция не запрещает вытеснение в режиме ядра. Если вы "трогаете" дан- ные, связанные с другим процессором, то, вероятно, необходимо применить блоки- ровки.
    Когда лучше использовать данные,
    связанные с процессорами
    Использование данных, связанных с процессорами, позволяет получить ряд пре- имуществ. Во-первых, это ослабление требований по использованию блокировок.
    В зависимости от семантики доступа к данным, которые связаны с процессорами,
    может оказаться, что блокировки вообще не нужны. Следует помнить, что правило
    "только один процессор может обращаться к этим данным " является всего лишь рекомен- дацией для программиста. Необходимо специально гарантировать, что каждый про- цессор работает только со своими данными. Ничто не может помешать нарушению этого правила.
    Во-вторых, данные, связанные с процессорами, позволяют существенно умень- шить недостоверность данных, хранящихся в кэше. Это происходит потому, что процессоры поддерживают свои кэши в синхронизированном состоянии. Если один процессор начинает работать с данными, которые находятся в кэше другого процес- сора, то первый процессор должен обновить содержимое своего кэша. Постоянное аннулирование находящихся в кэше данных, именуемое перегрузкой кэша (cash thrashing), существенно снижает производительность системы. Использование дан- ных, связанных с процессорами, позволяет приблизить эффективность работы с кэшем к максимально возможной, потому что в идеале каждый процессор работает только со своими данными.
    Следовательно, использование данных, которые связаны с процессорами, часто избавляет от необходимости использования блокировок (или снижает требования,
    связанные с блокировками). Единственное требование, предъявляемое к этим дан- ным для безопасной работы, — это запрещение вытеснения кода, который работает в режиме ядра. Запрещение вытеснения — значительно более эффективная опера- ция по сравнению с использованием блокировок, а существующие интерфейсы вы- полняют запрещение и разрешение вытеснения автоматически. Данные, связанные с процессорами, можно легко использовать как в контексте прерывания, так и в кон- тексте процесса. Тем не менее следует обратить внимание, что при использовании данных, которые связаны с текущим процессором, нельзя переходить в состояние ожидания {в противном случае выполнение может быть продолжено на другом про- цессоре).
    Сейчас нет строгой необходимости где-либо использовать новый интерфейс ра- боты с данными, которые связаны с процессорами. Вполне можно организовать та- кую работу вручную (на основании массива, как было рассказано ранее), если при этом запрещается вытеснение кода в режиме ядра. Тем не менее новый интерфейс более простой в использовании и, возможно, позволит в будущем выполнять допол- нительные оптимизации. Если вы собираетесь использовать в своем коде данные,
    связанные с процессорами, то лучше использовать новый интерфейс. Единственный
    недостаток нового интерфейса — он не совместим с более ранними версиями ядер.
    Управление памятью 263

    Какой способ выделения памяти необходимо использовать
    Если необходимы смежные страницы физической памяти, то нужно использовать один из низкоуровневых интерфейсов выделения памяти, или функцию kmalloc().
    Это стандартный способ выделения памяти в ядре, и, скорее всего, в большинстве случаев следует использовать именно его. Необходимо вспомнить, что два наи- более часто встречающихся флага, которые передаются этой функции, это флаги
    GFP_ATOMIC и GFP_KERNEL. Для высокоприоритетных операций выделения памяти,
    которые не переводят процесс в состояние ожидания, необходимо указывать флаг
    GFP_ATOMIC. Это обязательно для обработчиков прерываний и других случаев, когда нельзя переходить в состояние ожидания. В коде, который может переходить в со- стояние ожидания, как, например код, выполняющийся в контексте процесса и не удерживающий спин-блокировку, необходимо использовать флаг GFP_KERNEL. Такой флаг указывает, что должна выполняться операция выделения памяти, которая при необходимости может перейти в состояние ожидания для получения необходимой памяти.
    Если есть необходимость выделить страницы верхней памяти, то следует исполь- зовать функцию a l l o c _ p a g e s ( ) . Функция alloc_pages () возвращает структуру s t r u c t page, а не логический адрес. Поскольку страницы верхней памяти могут не отображаться в адресное пространство ядра, единственный способ доступа к этой памяти — через структуру s t r u c t page. Для получения "настоящего" указателя на область памяти необходимо использовать функцию kmap(), которая позволяет ото- бразить верхнюю память в логическое адресное пространство ядра.
    Если нет необходимости в физически смежных страницах памяти, а необходима только виртуально непрерывная область памяти, то следует использовать функцию vmalloc() (также следует помнить о небольшой потере производительности при использовании функции vmalloc() по сравнению с функцией kmalloc() ) Функция vmalloc() выделяет область памяти, которая содержит только виртуально смежные страницы, но не обязательно физически смежные. Это выполняется почти так же,
    как и в программах пользователя путем отображения физически несмежных участ- ков памяти в логически непрерывную область памяти.
    Если необходимо создавать и освобождать много больших структур данных, то следует рассмотреть возможность построения слябового кэша. Уровень слябового распределения памяти позволяет поддерживать кэш объектов (список свободных объектов), уникальный для каждого процессора, который может значительно улуч- шить производительность операций выделения и освобождения объектов. Вместо того чтобы часто выделять и освобождать память, слябовый распределитель сохра- няет кэш уже выделенных объектов. При необходимости получения нового участка памяти для хранения структуры данных, уровню слябового распределения часто нет необходимости выделять новые страницы памяти, вместо этого можно просто воз- вращать объект из кэша.
    264 Глава 11

    1   ...   30   31   32   33   34   35   36   37   ...   53


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