Функция драйвера выделяет ОЗУ

Обновлено: 21.11.2024

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

Для локального хранилища

Как объекты памяти WDF

В этом разделе представлена ​​основная информация о выделении памяти для локального хранилища, а также о создании и выделении буферов и объектов памяти.

Локальное хранилище

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

UMDF В зависимости от типа требуемого хранилища драйверы UMDF могут использовать new, malloc и другие методы выделения памяти, зависящие от пользовательского режима и языка Windows.

KMDF Драйверы KMDF используют DDI режима ядра Windows, обычно ExAllocatePoolWithTag, для выделения памяти, которая не является частью объекта памяти WDF. Например, следующие образцы драйверов используют ExAllocatePoolWithTag:

Образец Firefly выделяет буфер для отправки своего стека устройств в синхронном IOCTL.

В синхронном запросе использование объекта памяти WDF влечет за собой дополнительные накладные расходы без каких-либо преимуществ. Драйвер ожидает завершения запроса, поэтому память вряд ли освободится в неподходящее время.

Пример Featured Toaster выделяет память для использования в вызовах функций IoWmiXxx.

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

Пример драйвера KMDF 1394 выделяет память для управляющих блоков, которые он отправляет своему драйверу шины.

Драйвер шины ожидает параметры, упакованные в блок запроса ввода-вывода IEEE 1394.

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

В файле Pooltag.txt перечислены теги пула, которые используются компонентами режима ядра и драйверами, поставляемыми с Windows, а также связанный файл или компонент и имя компонента. Pooltag.txt устанавливается вместе со средствами отладки для Windows (в %windbg%\triage) и с WDK (в %wdk%\tools\other\platform\poolmon, где platform — amd64, i386 или ia64).

Объекты памяти и буферы ввода/вывода

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

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

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

Глава 8, "Поток ввода-вывода и диспетчеризация", и глава 9, "Цели ввода-вывода", предоставляют дополнительную информацию об использовании объектов памяти и буферов в запросах ввода-вывода, а также сведения о времени существования буферов и памяти. объекты.

Объекты памяти UMDF и интерфейсы

Методы, которые драйвер UMDF вызывает для создания объекта памяти и связанного с ним буфера, одинаковы независимо от того, как драйвер использует объект памяти. Как правило, драйвер одновременно выделяет буфер и создает объект памяти, вызывая IWDFDriver::CreateWdfMemory, чтобы время существования объекта и буфера совпадало. Если для буфера требуется более длительное время жизни, чем для объекта памяти, вместо этого драйвер должен использовать метод IWDFDriver::CreatePreallocatedWdfMemory.

Глава 9, "Цели ввода-вывода", описывает дополнительные сценарии создания и жизненного цикла.

Драйвер использует методы IWDFMemory для управления объектом памяти и доступа к базовому буферу. Таблица 12-1 суммирует эти методы.

Описание

Копировать из буфера

Копирует данные из исходного буфера в объект памяти.

Копировать из памяти

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

Копировать в буфер

Копирует данные из объекта памяти в буфер.

Получить буфер данных

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

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

Назначает буфер объекту памяти, созданному драйвером путем вызова IWDFDriver::CreatePreallocatedWdfMemory.

Интерфейс IWDFIoRequest включает методы, с помощью которых драйвер может получить объект памяти, встроенный в запрос ввода-вывода.

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

Объекты и методы памяти KMDF

Драйвер KMDF может одновременно выделять буфер и создавать объект памяти, вызывая WdfMemoryCreate. Если драйвер часто использует буферы одинакового размера, он может создать резервный список, содержащий буферы требуемого размера, а затем вызвать WdfMemoryCreateFromLookaside, чтобы назначить буфер из списка новому объекту памяти.

Среда определяет методы WdfMemoryXxx для управления объектами памяти WDF, а также для чтения и записи их буферов. Эти методы принимают дескриптор объекта памяти и передают данные между буфером объекта памяти и внешним буфером. Таблица 12-2 суммирует эти методы.

Описание

WdfMemoryAssignBuffer

Назначает указанный буфер объекту памяти, созданному драйвером.

WdfMemoryCopyFromBuffer

Копирует данные из исходного буфера в буфер объекта памяти.

Вдфмеморикопитобуффер

Копирует данные из буфера объекта памяти в другой буфер.

WdfMemoryCreate

Создает объект WDFMEMORY и выделяет буфер памяти указанного размера.

WdfMemoryCreateFromLookaside

Создает объект WDFMEMORY и получает буфер памяти из резервного списка.

WdfMemoryCreatePreallocated

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

WdfMemoryGetBuffer

Возвращает указатель на буфер, связанный с объектом памяти.

Каждый запрос ввода-вывода, который платформа отправляет драйверу KMDF, содержит один или несколько объектов WDFMEMORY. Драйвер может использовать методы WdfRequestRetrieveXxx, которые возвращают объекты памяти и буферы из запроса ввода-вывода.

Дополнительные списки KMDF

Дополнительные списки — это списки многократно используемых буферов фиксированного размера, предназначенные для структур, которые драйвер выделяет динамически и часто. Список просмотра полезен всякий раз, когда драйверу нужны буферы фиксированного размера, и особенно подходит для часто используемых и повторно используемых структур. Например, диспетчер ввода-вывода Windows выделяет собственные пакеты IRP из резервного списка.

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

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

Пример: использование дополнительных списков

Драйвер KMDF создает резервный список с помощью метода WdfLookasideListCreate. По умолчанию объект драйвера является родителем резервного списка. Чтобы указать другого родителя, драйвер устанавливает поле ParentObject в структуре атрибутов объекта для объекта списка. Платформа удаляет список при удалении родительского объекта. Вместо этого драйвер может удалить список вручную, вызвав WdfObjectDelete.

В листинге 12-1, взятом из файла Sys\Pcidrv.c в образце Pcidrv, показано, как драйвер создает резервный список для всего драйвера.

Листинг 12-1. Создание резервного списка

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

Каждый буфер из списка имеет длину sizeof(MP_RFD) байт и использует тег пула, определенный в константе PCIDRV_POOL_TAG. Использование уникального тега пула для вашего драйвера или даже для отдельных модулей в вашем драйвере важно, потому что это может помочь вам определить источник различных выделений памяти во время отладки. WdfLookasideListCreate принимает указатели на две структуры атрибутов объекта в качестве первого и четвертого параметров.Первая структура атрибутов описывает атрибуты самого объекта резервного списка, а вторая структура атрибутов описывает атрибуты объектов WDFMEMORY, которые драйвер позже выделяет из списка. Драйвер в листинге 12-1 не указывает атрибуты ни для одного из типов объектов.

Чтобы выделить буфер из списка, драйвер вызывает WdfMemoryCreateFromLookaside. Этот метод возвращает объект WDFMEMORY, который драйвер может использовать так же, как и любой другой объект WDFMEMORY.

В листинге 12-2 пример PCIDRV выделяет объект памяти из резервного списка, а затем получает указатель на буфер внутри этого объекта. Этот пример взят из файла Pcidrv\sys\hw\Nic_init.c.

Листинг 12-2. Выделение памяти из резервного списка

В этом примере дескриптор объекта списка резервного просмотра, сохраненного в области контекста драйвера, передается функции WdfMemoryCreateFromLookaside, которая возвращает дескриптор объекта WDFMEMORY в memoryHdl. После проверки состояния драйвер вызывает WdfMemoryGetBuffer, который возвращает указатель на буфер, встроенный в объект памяти. Первый параметр — это дескриптор объекта памяти, а второй параметр — это место, в которое возвращается длина буфера. Драйвер передает значение NULL для длины, поскольку он указал длину буфера при создании резервного списка.

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

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

Получите полный доступ к Linux Device Drivers, Second Edition и более чем 60 000 другим играм с бесплатной 10-дневной пробной версией O'Reilly.

Есть также прямые онлайн-мероприятия, интерактивный контент, материалы для подготовки к сертификации и многое другое.

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

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

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

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

Получение выделенного буфера во время загрузки

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

В версии ядра 2.4 такое распределение выполняется вызовом одной из следующих функций:

Функции выделяют либо целые страницы (если они заканчиваются на _pages ), либо области памяти, не выровненные по страницам. Они выделяют либо младшую, либо обычную память (см. обсуждение зон памяти ранее в этой главе). Нормальное распределение возвращает адреса памяти выше MAX_DMA_ADDRESS ; нехватка памяти находится по адресам ниже этого значения.

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

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

Этот способ выделения памяти имеет несколько недостатков, не последним из которых является невозможность когда-либо освободить буфер. После того, как драйвер занял часть памяти, он не может вернуть ее в пул свободных страниц; пул создается после того, как все физические выделения были выполнены, и мы не рекомендуем взламывать структуры данных, внутренние для управления памятью. С другой стороны, преимущество этого метода состоит в том, что он делает доступной область последовательной физической памяти, подходящую для прямого доступа к памяти. В настоящее время это единственный безопасный способ в стандартном ядре выделить буфер из более чем 32 последовательных страниц, потому что максимальное значение порядка, которое принимает get_free_pages, равно 5. Однако, если вам нужно много страниц, и они не обязательно должны быть физически смежными, vmalloc — безусловно лучшая функция для использования.

Если вы собираетесь прибегнуть к захвату памяти во время загрузки, вы должны изменить init/main.c в исходниках ядра. Подробнее о main.c вы узнаете в главе 16.

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

Нашивка Bigphysarea

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

Резервирование больших адресов оперативной памяти

Последний вариант выделения смежных областей памяти и, возможно, самый простой — это резервирование области памяти в конце физической памяти (тогда как bigphysarea резервирует ее в начале физической памяти). Для этого вам необходимо передать ядру параметр командной строки, чтобы ограничить объем управляемой памяти. Например, один из ваших авторов использует mem=126M, чтобы зарезервировать 2 мегабайта в системе, которая фактически имеет 128 мегабайт оперативной памяти. Позже, во время выполнения, эта память может быть выделена и использована драйверами устройств.

Модуль allocator, часть примера кода, опубликованного на FTP-сайте O’Reilly, предлагает интерфейс распределения для управления любой верхней памятью, не используемой ядром Linux. Этот модуль более подробно описан в разделе 13.4.2.1 главы 13.

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

Получите Драйверы устройств для Linux, второе издание прямо сейчас с онлайн-обучением O’Reilly.

Члены O’Reilly проходят онлайн-обучение в режиме реального времени, а также получают книги, видео и цифровой контент от более чем 200 издателей.

Linux предоставляет множество API для выделения памяти. Вы можете выделять небольшие фрагменты, используя семейства kmalloc или kmem_cache_alloc, большие практически непрерывные области, используя vmalloc и его производные, или вы можете напрямую запрашивать страницы из распределителя страниц с помощью alloc_pages. Также можно использовать более специализированные распределители, например cma_alloc или zs_malloc .

Большинство API выделения памяти используют флаги GFP, чтобы указать, как эта память должна быть выделена. Аббревиатура GFP расшифровывается как «получить бесплатные страницы» — основная функция распределения памяти.

Разнообразие API-интерфейсов выделения в сочетании с многочисленными флагами GFP заставляет задаться вопросом «Как мне выделить память?» не так просто ответить, хотя, скорее всего, вам следует использовать

Конечно, бывают случаи, когда необходимо использовать другие API распределения и другие флаги GFP.

Получить бесплатные флаги страницы¶

Флаги GFP управляют поведением распределителей. Они сообщают, какие зоны памяти можно использовать, насколько усердно распределитель должен пытаться найти свободную память, может ли память быть доступна пользовательскому пространству и т. д. Documentation/core-api/mm-api.rst содержит справочную документацию по флагам GFP. и их комбинации, и здесь мы кратко опишем их рекомендуемое использование:

  • В большинстве случаев GFP_KERNEL — это то, что вам нужно. Память для структур данных ядра, память с возможностью DMA, кеш inode, все эти и многие другие типы распределения могут использовать GFP_KERNEL. Обратите внимание, что использование GFP_KERNEL подразумевает GFP_RECLAIM , что означает, что прямое восстановление может быть запущено при нехватке памяти; контекст вызова должен быть переведен в спящий режим.

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

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

  • Ненадежные выделения, инициированные из пользовательского пространства, должны учитываться kmem и должны иметь установленный бит __GFP_ACCOUNT. Существует удобный ярлык GFP_KERNEL_ACCOUNT для выделений GFP_KERNEL, которые следует учитывать.

  • При выделении пользовательского пространства должны использоваться флаги GFP_USER , GFP_HIGHUSER или GFP_HIGHUSER_MOVABLE. Чем длиннее имя флага, тем меньше ограничений.

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

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

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

    < /li>

Вы можете заметить, что довольно много выделений в существующем коде указывают GFP_NOIO или GFP_NOFS . Исторически они использовались для предотвращения взаимных блокировок рекурсии, вызванных прямым возвратом памяти к путям FS или ввода-вывода и блокировкой уже удерживаемых ресурсов. Начиная с версии 4.12 предпочтительным способом решения этой проблемы является использование API новой области, описанных в Documentation/core-api/gfp_mask-from-fs-io.rst .

Другими устаревшими флагами GFP являются GFP_DMA и GFP_DMA32 . Они используются для обеспечения доступности выделенной памяти аппаратным обеспечением с ограниченными возможностями адресации. Поэтому, если вы не пишете драйвер для устройства с такими ограничениями, избегайте использования этих флагов. И даже с оборудованием с ограничениями предпочтительнее использовать API dma_alloc*.

Флаги GFP и поведение при возврате¶

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

  • GFP_KERNEL & ~__GFP_RECLAIM — оптимистическое распределение без _какой-либо_ попытки освободить память. Самый легкий режим, который даже не пинает восстановление фона. Следует использовать с осторожностью, так как это может привести к истощению памяти и следующему пользователю может потребоваться более агрессивное восстановление.

  • GFP_KERNEL & ~__GFP_DIRECT_RECLAIM (или GFP_NOWAIT ) — оптимистическое распределение без каких-либо попыток освободить память из текущего контекста, но может разбудить kswapd для освобождения памяти, если зона находится ниже нижней отметки. Может использоваться либо из атомарных контекстов, либо когда запрос является оптимизацией производительности и есть еще один запасной вариант для медленного пути.

  • (GFP_KERNEL|__GFP_HIGH) и ~__GFP_DIRECT_RECLAIM (он же GFP_ATOMIC ) — неспящее выделение с дорогостоящим откатом, чтобы он мог получить доступ к некоторой части резервов памяти. Обычно используется из контекста прерывания/нижней половины с дорогостоящим резервным медленным путем.

  • GFP_KERNEL — разрешено как фоновое, так и прямое восстановление, и используется поведение распределителя страниц по умолчанию. Это означает, что недорогие запросы на выделение в основном безошибочны, но нет гарантии такого поведения, поэтому сбои должны быть должным образом проверены вызывающими объектами (например, в настоящее время допускается сбой жертвы-убийцы OOM).

  • GFP_KERNEL | __GFP_NORETRY — переопределяет поведение распределителя по умолчанию, и все запросы на выделение терпят неудачу раньше времени, а не приводят к разрушительному возврату (один раунд возврата в этой реализации). Убийца OOM не вызывается.

  • GFP_KERNEL | __GFP_RETRY_MAYFAIL — переопределяет поведение распределителя по умолчанию, и все запросы на выделение очень стараются. Запрос завершится ошибкой, если восстановление не может быть выполнено. Убийца OOM не сработает.

  • GFP_KERNEL | __GFP_NOFAIL — переопределяет поведение распределителя по умолчанию, и все запросы на выделение будут бесконечно повторяться, пока не будут выполнены успешно. Это может быть очень опасно, особенно для крупных заказов.

Выбор распределителя памяти¶

Самый простой способ выделить память — использовать функцию из семейства kmalloc(). И, чтобы быть в безопасности, лучше всего использовать подпрограммы, которые обнуляют память, например kzalloc(). Если вам нужно выделить память для массива, есть помощники kmalloc_array() и kcalloc(). Помощники struct_size() , array_size() и array3_size() можно использовать для безопасного вычисления размеров объектов без переполнения.

Максимальный размер чанка, который может быть выделен с помощью kmalloc, ограничен.Фактическое ограничение зависит от аппаратного обеспечения и конфигурации ядра, но рекомендуется использовать kmalloc для объектов меньше размера страницы.

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

Размер фрагментов, выделенных с помощью kmalloc(), можно изменить с помощью krealloc() . Аналогично kmalloc_array() : помощник для изменения размера массивов предоставляется в форме krealloc_array() .

Для больших выделений вы можете использовать vmalloc() и vzalloc() или напрямую запрашивать страницы из распределителя страниц. Память, выделенная vmalloc и связанными с ней функциями, физически не является непрерывной.

Если вы не уверены, не слишком ли велик размер выделения для kmalloc , можно использовать kvmalloc() и его производные. Он попытается выделить память с помощью kmalloc, и если выделение не удастся, попытка будет повторена с помощью vmalloc. Существуют ограничения на то, какие флаги GFP можно использовать с kvmalloc ; см. справочную документацию по kvmalloc_node(). Обратите внимание, что kvmalloc может возвращать память, которая физически не является непрерывной.

Если вам нужно выделить много одинаковых объектов, вы можете использовать распределитель кеша slab. Кэш должен быть настроен с помощью kmem_cache_create() или kmem_cache_create_usercopy(), прежде чем его можно будет использовать. Вторую функцию следует использовать, если часть кеша может быть скопирована в пользовательское пространство. После создания кеша kmem_cache_alloc() и его удобные оболочки могут выделить память из этого кеша.

Когда выделенная память больше не нужна, ее необходимо освободить. Вы можете использовать kvfree() для памяти, выделенной с помощью kmalloc, vmalloc и kvmalloc. Кеши slab должны быть освобождены с помощью kmem_cache_free() . И не забудьте уничтожить кеш с помощью kmem_cache_destroy().

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

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

Пространства памяти C/C++

Возможно, полезно представить, что память данных в C и C++ разделена на три отдельных пространства:

Статическая память. Здесь находятся переменные, которые определены вне функций. Ключевое слово static обычно не влияет на расположение таких переменных; он определяет их область действия как локальную по отношению к текущему модулю. Переменные, определенные внутри функции, явно объявленные статическими, также хранятся в статической памяти. Обычно статическая память располагается в начале области ОЗУ. Фактическое распределение адресов по переменным выполняется встроенным набором инструментов для разработки программного обеспечения: сотрудничество между компилятором и компоновщиком. Обычно секции программы используются для управления размещением, но более продвинутые методы, такие как мелкозернистое распределение, дают больше контроля. Обычно вся оставшаяся память, которая не используется для статического хранения, используется для создания области динамического хранения, в которой размещаются два других пространства памяти.

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

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

Динамическая память в C

В C динамическая память выделяется из кучи с помощью некоторых стандартных библиотечных функций. Двумя ключевыми функциями динамической памяти являются malloc() и free().

Функция malloc() принимает единственный параметр — размер запрошенной области памяти в байтах. Он возвращает указатель на выделенную память. Если выделение не удается, возвращается NULL. Прототип стандартной библиотечной функции выглядит следующим образом:

void *malloc(size_t size);

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

void free(void *pointer);

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

целый_массив[10];
мой_массив[3] = 99;

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

инт *указатель;
указатель = malloc(10 * sizeof(int));
*(указатель+3) = 99;

Синтаксис разыменования указателя трудно читать, поэтому можно использовать обычный синтаксис ссылки на массив, поскольку [ и ] — это просто операторы:

Когда массив больше не нужен, память может быть освобождена следующим образом:

свободно(указатель);
указатель = NULL;

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

Объем пространства кучи, фактически выделенный функцией malloc(), обычно на одно слово больше запрошенного. Дополнительное слово используется для хранения размера выделения и для последующего использования функцией free(). Это «слово размера» предшествует области данных, на которую malloc() возвращает указатель.

Есть еще два варианта функции malloc(): calloc() и realloc().

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

void *calloc(size_t elements, size_t elementSize);

Функция realloc() изменяет размер памяти, выделенной ранее функцией malloc(). Он принимает в качестве параметров указатель на область памяти и новый требуемый размер. При уменьшении размера данные могут быть потеряны. Если размер увеличен и функция не может расширить существующее выделение, она автоматически выделит новую область памяти и скопирует данные в нее. В любом случае он возвращает указатель на выделенную память. Вот прототип:

void *realloc(void *pointer, size_t size);

Динамическая память в C++

Управление динамической памятью в C++ во многом похоже на C. Хотя библиотечные функции, скорее всего, будут доступны, в C++ есть два дополнительных оператора — new и delete — которые позволяют писать код более четко, лаконично и гибко, с меньшей вероятностью ошибок. Новый оператор можно использовать тремя способами:

p_var = новое имя типа;
p_var = новый тип(инициализатор);
p_array = новый тип [размер];

В первых двух случаях выделяется место для одного объекта; второй включает инициализацию. Третий случай — это механизм выделения места под массив объектов.

Оператор удаления можно вызвать двумя способами:

удалить p_var;
удалить[] p_array;

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

Не существует оператора, обеспечивающего функциональность функции C realloc().

Вот код для динамического выделения массива и инициализации четвертого элемента:

int* указатель;
указатель = новый int[10];
указатель[3] = 99;

Использование нотации доступа к массиву является естественным. Освобождение выполняется следующим образом:

удалить[] указатель;
указатель = NULL;

Опять же, присвоение указателю значения NULL после освобождения — это просто хорошая практика программирования. Другой вариант управления динамической памятью в C++ — использование стандартной библиотеки шаблонов. Это может быть нежелательно для встроенных систем реального времени.

Вопросы и проблемы

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

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

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

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

Фрагментация памяти

Лучший способ понять фрагментацию памяти — посмотреть на пример. В этом примере предполагается, что существует куча размером 10 КБ. Во-первых, запрашивается область размером 3 КБ, таким образом:

Затем запрашивается еще 4 КБ:

3 КБ памяти теперь свободно.

Некоторое время спустя первое выделение памяти, на которое указывает p1, освобождается:

При этом остается 6 КБ свободной памяти, разбитой на два фрагмента по 3 КБ. Выдается дополнительный запрос на выделение 4K:

Это приводит к сбою — в p1 возвращается NULL — потому что, хотя доступно 6 КБ памяти, нет доступного непрерывного блока размером 4 КБ. Это фрагментация памяти.

Память с RTOS

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

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

Блокирование/разделение памяти

Обычно выделение блочной памяти выполняется с использованием «пула разделов», который определяется статически или динамически и настраивается таким образом, чтобы содержать определенное количество блоков определенного фиксированного размера. Для ОС Nucleus вызов API для определения пула разделов имеет следующий прототип:

STATUS
NU_Create_Partition_Pool (NU_PAR TITION_POOL *pool, CHAR *name, VOID *start_address, UNSIGNED pool_size, UNSIGNED partition_size, OPTION suspend_type);

Наиболее наглядно это можно понять на примере:

При этом создается пул разделов с дескриптором MyPool, содержащий 2000 байт памяти, заполненный разделами размером 40 байт (т. е. существует 50 разделов). Пул расположен по адресу 0xB000. Пул настроен таким образом, что, если задача пытается выделить блок, когда его нет в наличии, и запрашивает приостановку при вызове API выделения, приостановленные задачи будут разбужены в порядке «первым поступил — первым вышел». . Другим вариантом был бы порядок приоритета задач.

Для запроса выделения раздела доступен другой вызов API. Вот пример использования ОС Nucleus:

Это запрашивает выделение раздела из MyPool. В случае успеха указатель на выделенный блок возвращается в ptr. Если памяти нет, задача приостанавливается, так как указано NU_SUSPEND; другими вариантами, которые могли быть выбраны, были бы приостановка с тайм-аутом или просто возврат с ошибкой.

Когда раздел больше не нужен, его можно освободить следующим образом:

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

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

Обнаружение утечки памяти

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

Решения для оперативной памяти

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

Динамическая память

Можно использовать выделение памяти разделов для надежной и детерминированной реализации malloc(). Идея состоит в том, чтобы определить серию пулов разделов с размерами блоков в геометрической прогрессии; например 32, 64, 128, 256 байт. Функция malloc() может быть написана для детерминированного выбора правильного пула, чтобы обеспечить достаточно места для данного запроса на выделение.В этом подходе используется детерминированное поведение вызова API выделения разделов, надежная обработка ошибок (например, приостановка задачи) и иммунитет к фрагментации, обеспечиваемый блочной памятью.

Выводы

C и C++ используют память различными способами, как статическими, так и динамическими. Динамическая память включает стек и кучу.

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

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

Читайте также: