Ориентация главы
Глава 3 установила язык «владения и заимствования»; эта глава даёт физический выход для «владения»: вся динамическая память XRT проходит через пять точек входа — xrtMalloc / xrtCalloc / xrtRealloc / xrtMemDup / xrtFree — плюс xrtSecureZero для чувствительных случаев. Ценность стока не в синтаксическом сахаре: он делает возможными три иначе невозможные вещи — замену нижележащего аллокатора целиком (для подключения собственного пула или рантайма хоста), позылементное наблюдение (статистика и отладка главы 6) и точечное внедрение сбоев (для проверки путей отказа). После этой главы каждый ваш блок памяти стоит на едином выходе.
5.1引入
Реальная дилемма: вашу программу нужно встроить в чужую большую систему, и там требуют, чтобы вся память шла через их аллокатор (они ведут учёт, квоты и возврат по модулям); а ваш код уже содержит десятки вызовов malloc/free. Заменять по одному — катастрофа: вы определяете макросы, массово заменяете и молитесь, чтобы ничего не ускользнуло. Другая дилемма: в тестах вы хотите знать «если 1000-е выделение не удастся, корректно ли откатится программа» — у разрозненного malloc просто нет точки внедрения.
Обе дилеммы указывают на один и тот же проектный ответ: точки входа памяти должны быть стянуты в одно горлышко, причём на уровне библиотеки, а не дисциплины кодирования. XRT сводит всю динамическую память в пять функций, поэтому замена, наблюдение и внедрение — операции уровня одного глобального переключателя.
Сток приносит и третий дивиденд: единые гарантии выравнивания и раскладки. Указатели, возвращаемые пятью точками входа, выделяются с максимальным базовым выравниванием — в них можно напрямую размещать любые скаляры, структуры и даже SIMD-векторы, и вам не нужно обсуждать «этот буфер под массив float — не нужен ли aligned_alloc»; все контейнеры XRT (том 3) внутренне идут через те же точки входа — выравнивание элементов в контейнере гарантирует библиотека, независимо от того, что случайно делает переданный вами аллокатор. Выход, построенный в этой главе, — общая физическая база контейнеров, парсеров и сетевых буферов всех последующих томов.
5.2概念
Семантика пяти точек входа
| Точка входа | Семантика | Ключевые детали |
|---|---|---|
xrtMalloc | Выделить (владеющее) | Семантика как у стандартного malloc |
xrtCalloc | Выделить count × size и обнулить | Проверки переполнения как в стандарте |
xrtRealloc | Рост/сжатие | При неудаче возвращает NULL, а исходный блок остаётся валидным |
xrtMemDup | Скопировать диапазон памяти в новое владеющее выделение | Выделение + memcpy за один шаг |
xrtFree | Освободить | NULL допустим — проверки на пустоту не нужны |
xrtSecureZero | Безопасная очистка | Не будет выброшено компилятором как мёртвая запись |
Первые пять следуют владеющему соглашению: кто получил возвращаемое значение, тот и освобождает, ровно один раз. Законность xrtFree(NULL) выглядит мелочью на письме, а на чтение стоит дорого — коду очистки больше никогда не нужно кутаться в слои проверок на пустоту.
Замена аллокатора: одно решение, глобальный эффект
Структура xallocator несёт три колбэка — выделение, рост, освобождение — плюс указатель контекста; xrtSetAllocator меняет всю таблицу, xrtGetAllocator копирует текущую. Замена разрешена только до первого выделения — после этого таблица навсегда блокируется, и повторная замена возвращает неудачу. Это соглашение «ранней блокировки» покупает драгоценное свойство: во время работы аллокатор постоянен, поэтому никакому пути выделения не нужно рассматривать гонку «аллокатор сменился посреди полёта», и горячим путям не нужны блокировки.
Семейство At: выделения с местом рождения
xrtMallocAt / xCallocAt / xReallocAt / xFreeAt / xMemDupAt — отладочные точки входа, несущие __FILE__ / __LINE__: обычный макрос xrtMalloc при включённом отладочном модуле разворачивается в семейство At, и каждое выделение получает позицию. Это краеугольный камень поиска утечек в главе 6; здесь достаточно знать, что «информация о позиции прилагается к стоку бесплатно».
Когда действительно нужен другой аллокатор
Три типичных сценария стоит узнать заранее. Встраивание в хост: ваш модуль работает в чужом процессе (игровой движок, рантайм скриптов), и хост требует всю память через свой учёт — оберните функции хоста в три колбэка и замените в самый ранний момент; второй пример этой главы — полный шаблон. Квоты и лимиты: мультитенантный сервис ограничивает по модулям; собственный аллокатор накапливает байты в контексте и возвращает NULL сверх лимита — в сочетании с моделью ошибок главы 4 превышение ведёт себя как обычный OOM, и бизнес-коду не нужен особый случай. Измеримое сравнение: при настройке производительности вы хотите A/B-сравнить разные бэкенды (пул памяти главы 22 — готовый кандидат) — одна строка замены переключает. Общее предусловие одно: из-за «блокировки до первого выделения» замена обязана случиться в самом начале main — поставить её после чтения конфигурации может быть уже поздно.
5.3示例
Полная программа: пять точек входа за один проход
Из примера репозитория examples/core/memory/main.c — полный жизненный цикл выделения, роста, копирования, безопасной очистки и освобождения:
*/ #include <stdio.h> #include <string.h> #include <xrt.h> int main(void) { /* 被复制源的只读字节(static const:不占用栈也不可写)。 */ static const unsigned char Source[] = { 1, 2, 3, 4 }; unsigned char* pValues; unsigned char* pCopy; /* 分配 4 字节并清零:Calloc 语义与标准一致,返回拥有式指针。 */ pValues = (unsigned char*)xrtCalloc(4, sizeof(unsigned char)); if ( pValues == NULL ) { return 1; } /* 写入一个标记值,稍后验证 Realloc 保留了旧内容。 */ pValues[0] = 42; */ pValues = (unsigned char*)xrtRealloc(pValues, 16); if ( pValues == NULL ) { return 2; } /* 把 4 字节源复制为独立的拥有式缓冲区(分配 + memcpy 一步完成)。 */ pCopy = (unsigned char*)xrtMemDup(Source, sizeof(Source)); if ( pCopy == NULL ) { xrtFree(pValues); /* 失败路径也要释放已持有的资源 */ return 3; } /* 验证:扩容后旧值仍在;复制内容与源逐字节一致。 */ printf( "value=%u copied=%s\n", (unsigned int)pValues[0], memcmp(pCopy, Source, sizeof(Source)) == 0 ? "yes" : "no" ); */ xrtSecureZero(pValues, 16); xrtSecureZero(pCopy, sizeof(Source)); /* 释放顺序与持有顺序相反;xrtFree 允许 NULL,无需判空。 */ xrtFree(pCopy); xrtFree(pValues); return 0; }
Что только что произошло. (1) xrtCalloc(4, 1) выделяет и обнуляет; записывается маркер 42. (2) xrtRealloc растит до 16 байт — семантический нюанс на стороне неудачи: при возврате NULL исходный блок остаётся валидным; для краткости пример выходит напрямую — надёжная форма в ловушке 1. (3) xrtMemDup копирует статический источник в независимый владеющий буфер, memcmp сверяет побайтово; заметьте, путь отказа сначала освобождает уже удерживаемый pValues и лишь затем выходит — «на пути отказа нет полуфабрикатов» практикуется с первой главы, и это стандартная форма сотрудничества модели ошибок главы 4 с соглашением владения этой главы. (4) xrtSecureZero очищает оба буфера: он направлен против оптимизации компилятора «удаление мёртвой записи» — обычный memset прямо перед free может быть признан бессмысленным и удалён целиком, и чувствительные данные (ключи, токены) утекут в переиспользуемую память кучи; xrtSecureZero использует volatile-записи или барьеры, гарантируя, что очистка действительно происходит. (5) Порядок освобождения обратен порядку получения, а xrtFree не требует проверки на пустоту.
Полная программа: замена аллокатора и наблюдение блокировки
Из examples/core/allocator_tour/main.c — реализуем счётный аллокатор, заменяем до первого выделения и проверяем соглашение о ранней блокировке:
*/ #include <stdio.h> #include <string.h> #include <stdlib.h> #include <xrt.h> /* 计数分配器:仍路由系统堆,便于验证替换生效。 */ typedef struct examplecount { size_t iAllocs; size_t iFrees; } examplecount; static ptr exampleAlloc(ptr pContext, size_t iSize) { ++((examplecount*)pContext)->iAllocs; return (ptr)malloc(iSize); } static ptr exampleRealloc(ptr pContext, ptr pMemory, size_t iSize) { (void)pContext; return (ptr)realloc(pMemory, iSize); } static void exampleFree(ptr pContext, ptr pMemory) { ++((examplecount*)pContext)->iFrees; free(pMemory); } int main(void) { xallocator Default; xallocator Custom; examplecount Count; uint8* pBlock; uint8* pDup; int iResult = 1; /* ---- 必须在任何 XRT 分配前完成替换。 ---- */ xrtGetAllocator(&Default); memset(&Count, 0, sizeof(Count)); Custom.Context = (ptr)&Count; Custom.Alloc = exampleAlloc; Custom.Realloc = exampleRealloc; Custom.Free = exampleFree; if ( !xrtSetAllocator(&Custom) ) { goto Cleanup; } /* ---- At 族五接口:位置参数不影响堆行为。 ---- */ pBlock = (uint8*)xrtMallocAt(4u, __FILE__, __LINE__); if ( (pBlock == NULL) || ((pDup = (uint8*)xrtMemDupAt("hi", 2u, __FILE__, __LINE__)) == NULL) || (memcmp(pDup, "hi", 2u) != 0) || ((pBlock = (uint8*)xrtReallocAt(pBlock, 16u, __FILE__, __LINE__)) == NULL) || (xrtCallocAt(2u, 4u, __FILE__, __LINE__) == NULL) ) { goto Cleanup; } /* Calloc 清零验证单独做——保留指针用于释放。 */ { uint8* pZero = (uint8*)xrtCallocAt(2u, 4u, __FILE__, __LINE__); if ( (pZero == NULL) || (pZero[0] != 0u) || (pZero[7] != 0u) ) { goto Cleanup; } xrtFreeAt(pZero, __FILE__, __LINE__); } xrtFreeAt(pDup, __FILE__, __LINE__); xrtFreeAt(pBlock, __FILE__, __LINE__); printf("allocator: swap -> at-family alloc/calloc/dup/realloc ok\n"); * 且 GetAllocator 仍报告已安装的计数分配器。 ---- */ * 分配才经过自定义 Alloc;计数只需 >= 1 即证明替换生效。 */ if ( xrtSetAllocator(&Default) || (Count.iAllocs < 1u) ) { goto Cleanup; } { xallocator Current; xrtGetAllocator(&Current); if ( (Current.Alloc != Custom.Alloc) || (Current.Free != Custom.Free) ) { goto Cleanup; } } printf("allocator: locked after first alloc -> swap rejected ok\n"); iResult = 0; Cleanup: return iResult; }
Что только что произошло. (1) Счётный аллокатор по-прежнему маршрутизирует в системную кучу, лишь ведя учёт: три колбэка exampleAlloc и остальные заполняют таблицу xallocator, и main сразу при входе вызывает xrtSetAllocator. (2) После замены раунд выделений через семейство At идёт целиком через новую таблицу. (3) Вторая замена отвергнута: после первого выделения таблица заблокирована, и вызов возвращает неудачу, а не молчаливое игнорирование — сигнал ошибки явный, и инвариант «аллокатор постоянен» выполняется. Этот шаблон — стандартное подключение при встраивании в рантайм хоста: оберните функции хоста в три колбэка и завершите замену в самый ранний момент.
Прочтите и детали реализации примера — три вещи стоит украсть. Сигнатуры колбэков несут указатель контекста (exampleAlloc(ptr pContext, size_t iSize)) — собственное состояние аллокатора не живёт в глобальных переменных, и несколько экземпляров аллокатора не мешают друг другу. Счётное состояние живёт в структуре, переносимой контекстом — полностью изоморфно паттерну «пользовательские данные колбэка» из главы 3, который вы будете встречать снова и снова. И наконец, замена происходит до любого вызова XRT: первое действие main — xrtSetAllocator, даже printf стоит после — «самое начало» не риторика, а жёсткая последовательность.
5.4契约
- Владеющее: указатели из пяти точек входа освобождаются ровно один раз;
xrtFree(NULL)законен; коду очистки не нужны слои проверок на пустоту. - Сохранность при неудаче Realloc: неудача возвращает
NULL, содержимое и валидность исходного блока неизменны; после успеха старый указатель мёртв и больше не используется. - SecureZero не опционален: очистка чувствительной памяти только через него;
memsetможет быть оптимизирован прочь, и этот риск проявляется только в release-сборках. - Блокировка аллокатора:
xrtSetAllocatorдействует только до первого выделения; во время работы аллокатор постоянен, горячие пути без блокировок; «самое начало» — первое действиеmain. - Выравнивание: точки входа выделяют с максимальным базовым выравниванием; возвращённые указатели годятся для любых скаляров, структур и векторов; выравнивание элементов контейнеров гарантирует библиотека.
- Форма колбэков: три колбэка аллокатора следуют общебиблиотечной конвенции — ведущий указатель контекста несёт состояние, никаких глобальных переменных.
Типичные ошибки
Симптомы: случайные утечки памяти (ветви отказа), усиливающиеся под нагрузкой или при внедрении OOM.
Причина: xrtRealloc при неудаче возвращает NULL, а исходный блок остаётся валидным — перезаписав исходный указатель возвращаемым значением, вы теряете его в момент неудачи, и тот блок уже никто не освободит.
pValues = (unsigned char*)xrtRealloc(pValues, 4096); if ( pValues == NULL ) { return 1; /* исходный указатель перезаписан — блок до 4096 утекает навсегда */ }
unsigned char* pNew = (unsigned char*)xrtRealloc(pValues, 4096); if ( pNew == NULL ) { xrtFree(pValues); /* исходный блок валиден: чистим по пути отказа как обычно */ return 1; } pValues = pNew; /* принимаем новый указатель только при успехе */
Симптомы: аудит безопасности находит фрагменты ключей в дампах кучи или повторно используемой памяти; в release-сборках проявляется чаще, чем в debug.
Причина: компилятор видит memset(key, 0, n) сразу за которым следует free(key), признаёт очистку «мёртвой записью» и удаляет целиком — разрешённая стандартом оптимизация, оставляющая чувствительные данные в куче.
memset(pSecretKey, 0, iKeySize); xrtFree(pSecretKey); /* memset почти наверняка удалён оптимизацией — ключ остаётся в памяти кучи */
xrtSecureZero(pSecretKey, iKeySize); xrtFree(pSecretKey); /* очистка гарантированно происходит на самом деле */
Практические задания
Следуя examples/core/memory/main.c, напишите программу: Calloc диапазон, Realloc больше и проверьте сохранность старого содержимого, MemDup со сверкой memcmp и освобождение в обратном порядке. Напечатайте все результаты проверок.
Реализуйте «считающий байты» аллокатор (отслеживающий текущее число живых байт); после замены прогоните отрезок смешанного кода выделения/освобождения и напечатайте в конце число живых байт и вердикт об утечке. Подсказка: три колбэка несут состояние счёта в указателе контекста; выделение прибавляет, освобождение вычитает; когда колбэк realloc получает старый указатель, сначала вычтите старый размер — как его взять? Проще всего выделять один дополнительный заголовок с размером и читать его в колбэке. Закончив, вы лучше почувствуете, что значит «аллокатор — это полный бэкенд».
Напишите программу, проверяющую границу блокировки xrtSetAllocator: первая замена проходит, происходит одно выделение, вторая замена обязана провалиться. Затем ответьте: если бы XRT разрешал замену во время работы, какую защиту потребовал бы ваш счётный аллокатор? (Подсказка: подумайте, нужны ли блокировки для чтения и записи самой таблицы аллокатора, когда два потока выделяют одновременно.) Критерий приёмки: возвращаемые значения двух замен — одно истинное и одно ложное, вывод совпадает с комментариями.
Итоги
| Тема | Краткая справка |
|---|---|
| Пять точек входа | Malloc / Calloc / Realloc / MemDup / Free (NULL допустим; очистка без проверок) |
| Владеющее | Кто получил, тот освобождает, ровно один раз; пути отказа тоже убирают |
| Realloc | При неудаче NULL, исходный блок валиден — сначала принять новый указатель, потом перезаписывать |
| Чувствительная очистка | xrtSecureZero; никогда не полагаться на memset |
| Аллокатор | xrtSetAllocator заменяет до первого выделения; дальше блокирован навсегда; замена — первое действие main |
| Семейство At | xrtMallocAt и др. несут __FILE__/__LINE__ — краеугольный камень отладочного модуля |
| Сценарии смены бэкенда | Встраивание в хост (учёт), квоты (превышение как OOM), A/B-сравнение бэкендов |
📖 Справочники API в этой главе:memory (19 записей)core (25 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки