Ориентация главы
Глава 133 показала OOM-тестирование как одно из четырёх оружия; эта глава расширяет его в полную дисциплину — за 185 файлами _oom стоит строгая методология. Четыре блока: поточечное вращение (циклическое перечисление «N-е выделение отказывает» — «каждое выделение, отказывая, проверяется отдельно», а не «какое-то однажды откажет»); три формы провального распределителя (xrtMemDebugFailAfter — счётная / самописная квотная (Limited+Remaining — форма OOM-теста map) / полностью провальная (FailAllocator у test_oom) — у каждой свой сценарий); выживание системы ошибок при истощении (отчёт об ошибке в OOM не выделяет память — при отказе xrtErrorCreate слот сохраняет XERR_MEMORY: сообщить «нет памяти» самой этой операции память не нужна); отладочная куча, вооружённая (FailAfter вместе с потоком событий — внедрённый отказ аудируем; карантина ловит двойное освобождение/UAF — формы сбоев не ограничиваются отказом выделения). Одна цель: «нет ресурсов» из рантайм-предположения становится исчерпывающе перечисленным фактом.
134.1引入
Необходимость поточечного вращения видна из одного вывода: в функции 5 выделений — очистка при отказе 3-го отличается от отказа 5-го (первая откатывает два полных побочных эффекта, вторая четыре с половиной); тестирование только «1-го отказа» (или вовсе без) оставляет 4 пути очистки неисполненными — любая утечка наполовину инициализированного/порча состояния среди них идёт прямо в прод. Метод вращения: точка отказа сканирует от 1 до 5 (каждый раунд перезапускает полный поток) — каждый путь очистки исполнен, каждое состояние отказа утверждено. 185 файлов — развёртка этого сканирования по всем модулям — само число есть доказательство дисциплины.
Выживание системы ошибок заслуживает отдельного акцента: если бы путь сообщения об ошибке требовал выделения, в OOM получилось бы «отчёт об отказе отказывает» — отладчик видит молчаливое падение вместо структурированной ошибки. Объект ошибки XRT (глава 4) избежал этого в проекте (неизменяемые объекты с минимальным выделением, при отказе деградация категории в статическую XERR_MEMORY) — а test_oom и есть машинная проверка этого обещания: «проверить, что отчёт об ошибке OOM сам не запрашивает память» — первая строка его комментария.
134.2概念
Поточечное вращение: форма перечисления
Сильное утверждение «состояние не изменилось» двухуровневое: продолжает работать (OOM временен — после освобождения памяти тот же объект работает как раньше, например map после неудачной вставки всё ещё ищет); безопасно разрушается (минимум путь Destroy не падает второй раз — очистка наполовину инициализированного полна). Оба уровня — контракт: первый — качество, второй — нижняя планка.
Три формы провального распределителя
| Форма | Механизм | Применение |
|---|---|---|
Счётная (xrtMemDebugFailAfter(N)) | первые N успешны, N+1-й отказывает; Clear сбрасывает и триггер-бит | одно-точечная инъекция + аудит событий (приложения и тесты делят) |
Квотная (самописная: Fail/Limited/Remaining) | пересылает вниз, пока не исчерпается квота | сканирование вращением (Remaining убывает — это и есть скан) |
Полностью провальная (FailAllocator) | всегда NULL | крайние пути (быстрая проверка отказа самого первого выделения) |
Три формы — «памятный бюджет как управляемая ручка» — тестирование из бинарного мира «есть память/нет» входит в точный мир «в какой момент нет».
Выживание системы ошибок при истощении
Цепочка утверждений test_oom — образец: xrtMalloc(32)==NULL + категория ошибки XERR_MEMORY + домен xrt.memory; затем xrtErrorCreate(...)==NULL (выделение самого объекта ошибки отказывает) + в слоте всё ещё XERR_MEMORY — «отказ создания ошибки не затирает факт OOM». Это проверяет глубокое обещание «минимальных зависимостей» главы 4: канал ошибок сохраняет работоспособность при истощении ресурсов. Семантика деградации при полном OOM (многократно в контрактах): «если OOM сделал невозможным выделение даже объектов ошибки, деградировать в статический XERR_MEMORY» — не каждый отказ несёт богатый контекст, но каждый отказ виден.
Расширение форм сбоев: не только отказ выделения
Карантин отладочной кучи (глава 6) расширяет инъекцию сбоев на сторону освобождения: двойное освобождение, использование после освобождения (UAF) — отложенное переиспользование карантина делает такие ошибки в тестах неизбежно видимыми (а не молча проходящими). Комбинация _mutation-тестов с OOM (мутация + поточечный отказ) — предельная форма давления: уродливый ввод × истощение ресурсов — глубочайшие углы парсера тоже освещены. Та же дисциплина в расширениях: поточечный откат xhttp, OOM-шлюз серверной маршрутизации xws — каждая библиотека-расширение везёт свой _oom-матрицу (тот же шлюз после наложения манифестов главы 130).
134.3示例
Первая полная программа: выживание системы ошибок при истощении
Программа ниже из tests/core/test_oom.c — это сам тест (не пример), показывающий центральное утверждение выживания:
#include "../test_allocator.h" /* 验证 OOM 错误报告本身不申请内存。 */ int main(void) { testRequire(testInstallFailAllocator(), "failure allocator install failed"); testRequire(xrtMalloc(32) == NULL, "allocation should fail"); testRequire(xrtErrorKind(xrtGetError()) == XERR_MEMORY, "allocation failure must report memory error"); testRequire(strcmp(xrtErrorDomain(xrtGetError()), "xrt.memory") == 0, "OOM domain mismatch"); xrtClearError(); testRequire(xrtErrorCreate(XERR_IO, "test", 1, "cannot allocate") == NULL, "error allocation should fail"); testRequire(xrtErrorKind(xrtGetError()) == XERR_MEMORY, "error allocation failure must preserve OOM"); xrtClearError(); printf("[PASS] oom\n"); return 0; }
Что только что произошло. (1) testInstallFailAllocator() — сборка полностью провальной формы (распределитель «всегда NULL» берёт память библиотеки на себя). (2) Первая группа утверждений: отказ выделения + категория + домен — тройка сообщения об ошибке в минимальном сценарии выполняется. (3) Вторая группа — душа главы: сам вызов создания объекта ошибки отказывает (возвращает NULL) — а в слоте ошибки всё ещё XERR_MEMORY — «канал, сообщающий об отказах, не немеет из-за отказа». (4) За единственной строкой [PASS] oom — нижняя гарантия всех путей истощения ресурсов. Файл в десяток строк — потому что он фундамент (_oom сложных модулей наслаивает сверху бизнес-утверждения).
Вторая полная программа: прикладная грань инъекции
Вторая программа из examples/memory/fail_inject — ощущение трёхрежимной ручки:
*/ #include <stdio.h> #include <xrt.h> /* 事件访问器:只打印前几个 alloc 事件的名字。 */ static bool printEvent(const xmemdebugevent* pEvent, ptr pUserData) { size_t* pCount = (size_t*)pUserData; if ( *pCount < 1u && pEvent->Kind == XMEMDEBUG_ALLOC ) { printf("event[%zu]: %s\n", *pCount, xrtMemDebugEventName(pEvent->Kind)); } (*pCount)++; return true; } int main(void) { ptr pFirst; ptr pSecond; ptr pThird; size_t iEvents = 0; (void)xrtMemDebugReset(); (void)xrtMemDebugEnable(true); printf("enabled=%d\n", xrtMemDebugEnabled() ? 1 : 0); /* 注入:第 2 次分配失败。 */ (void)xrtMemDebugFailAfter(1u); pFirst = xrtMalloc(16); pSecond = xrtMalloc(16); printf("fail-after-2: 1st=%s 2nd=%s triggered=%d\n", pFirst != NULL ? "ok" : "NULL", pSecond != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pFirst); /* 事件流:最早的 alloc 事件可按名访问。 */ (void)xrtMemDebugVisit(printEvent, &iEvents); /* 清除未触发注入后再分配恢复成功;已触发的标志保持。 */ xrtMemDebugFailClear(); pThird = xrtMalloc(16); printf("cleared: 3rd=%s triggered-still=%d\n", pThird != NULL ? "ok" : "NULL", xrtMemDebugFailTriggered() ? 1 : 0); xrtFree(pThird); (void)xrtMemDebugEnable(false); printf("reset=%d\n", xrtMemDebugReset() ? 1 : 0); return 0; }
Что только что произошло. (1) Параллельно с главой 132: там в центре таксономия «четырёх оружия»; здесь фокус на точном управлении инъекцией — граничную семантику FailAfter(1) (1st=ok 2nd=NULL) видно в выводе поле за полем. (2) Независимость флага triggered: после срабатывания доступен опрос, сбрасывает только Clear — состояние самого инъектора аудируемо (событие alloc стоит в потоке событий — отказ тоже событие). (3) Настоящие тесты пишут вращение на этих входах: внешний цикл for наращивает N, каждый раунд Reset-ит состояние инъектора и тестируемого объекта — этот образец есть одношаговый разрез того цикла.
Почему OOM-дисциплина C-библиотек строже, чем у управляемых языков
Сопоставление показывает необходимость этих вложений. В управляемых языках (Java/Go/C#) отказ выделения — Throwable — рантайм подстилает разматывание стека, GC гарантирует возврат — платформа забирает большую часть очистительной ответственности приложения. У C такой подстилки нет: отказ выделения — просто NULL в возврате, очистка на 100% в ответственности вызывающего — каждое поле наполовину инициализированной структуры, каждый блок частично построенного контейнера кто-то обязан вернуть в правильном порядке. 185 файлов _oom XRT — поточечная приёмка «обязанности возврата». Библиотек того же масштаба с такой плотностью почти нет — OOM-обработка большинства C-библиотек останавливается на «критический путь проверил NULL» — это и есть корень «случайных утечек памяти, в конце концов замораживающих OOM» во встраиваемых и долго работающих сервисах: не какой-то NULL пропущен, а какой-то путь очистки никогда никем не пройден. Вращательное перечисление делает «каждый путь очистки пройден» машинной гарантией — в этом вся его ценность.
134.4契约
- Вращательное перечисление: скан точек отказа покрывает каждое выделение — каждый путь очистки исполнен и утверждён.
- Тройное утверждение: отказ в возврате / состояние не изменилось (продолжает работать или безопасно разрушается) / ошибка верна (категория + домен).
- Дисциплина сверки: ноль утечек каждый раунд (Reset/Snapshot отладочной кучи перекрёстно).
- Три формы распределителя: счётная/квотная/полностью провальная — ручка точна до «в какой момент».
- Выживание ошибки: отчёт в OOM не выделяет память; при отказе создания ошибки слот сохраняет XERR_MEMORY.
- Семантика деградации: при невозможности выделить объект ошибки — деградация в статический XERR_MEMORY — отказы видны всегда.
- Инъекция на стороне освобождения: карантин ловит двойное освобождение/UAF — формы сбоев покрывают пути освобождения.
- Комбинированное давление: мутация × поточечный отказ — пространство произведения уродливого ввода и истощения ресурсов.
- Расширения — тот же закон: каждая библиотека-расширение с собственной _oom-матрицей — наложение манифеста есть наложение шлюза.
Типичные ошибки
Симптом: со второго раунда вращения всё «отказывает» — триггер-бит FailAfter первого раунда всё ещё висит; или Remaining квотного распределителя не заполнен заново — проверяется «всегда отказ», а не «отказ в N-й раз».
Причина: инъектор состояние держит — каждый раунд цикла обязан сбрасывать полностью (FailClear/заполнение Remaining/пересборка тестируемого объекта). Забытый сброс превращает «перечисление» в «повторное тестирование первого состояния».
for ( N = 1; N < 100; N++ ) { fail_after(N); run_flow(); /* остатки триггер-бита/квоты прошлого раунда - при N>1 всё отказывает */ }
for ( N = 1; N < 100; N++ ) { reset_injector(); /* Clear триггер-бита / заполнить квоту */ rebuild_target(); /* пересобрать тестируемый объект в согласованную стартовую точку */ fail_after(N); run_flow(); assert_three(); /* тройное утверждение */ }
Симптом: тест проходит, прод падает — тест после утверждения отказа выбросил объект (Destroy не шёл); вызывающий в проде прошёл путём очистки и наступил на наполовину инициализированное поле.
Причина: «безопасно разрушается» — обязательный пункт теста — Destroy пути отказа обязан реально исполниться в тесте (это удостоверение исполнения путей очистки).
testRequire(op(&Obj) == false, "oom"); /* сразу return - Destroy в состоянии отказа ни разу не исполнялся */
testRequire(op(&Obj) == false, "oom"); testRequire(destroy(&Obj), "失败态可安全销毁"); /* путь очистки исполнен */
Симптом: OOM выдан как XERR_IO или ошибка параметра — тест (только категория) пропускает — логика повторов верхнего уровня идёт не в ту ветку («нет памяти» принято за «временную IO-ошибку», буря повторов).
Причина: категория и домен вместе образуют идентичность ошибки (глава 4); OOM-тесты утверждают оба — неверный домен есть неверный отчёт.
testRequire(xrtErrorKind(xrtGetError()) == XERR_MEMORY, "kind"); /* домен не проверен: MEMORY домена xrt.io тоже пройдёт */
testRequire(xrtErrorKind(xrtGetError()) == XERR_MEMORY, "kind"); testRequire(strcmp(xrtErrorDomain(xrtGetError()), "xrt.memory") == 0, "domain"); /* двойное утверждение категория+домен */
Практические задания
На один и тот же код с 3 выделениями инъекция всех трёх форм: FailAfter(2) (отказ 3-го)/квота 2 (эквивалент)/полный отказ (отказ 1-го). Критерии приёмки: утверждения точек отказа трёх форм совпадают; вы понимаете эквивалентность и различие (счётная несёт аудит событий).
Написать родовой ротатор oom_scan(被测流程, 最大点数) (тестируемый поток, максимум точек): внутренний цикл инъекция-сброс-утверждение-сверка. Прогнать вставку map главы 18. Критерии приёмки: скан завершается автоматически (за последней точкой выделения); ноль утечек каждый раунд; намеренно оставить утечку на пути очистки — ротатор называет раунд.
Мутационные тесты парсера (скажем, percent-кодирования) плюс поточечный отказ: внешняя мутация ввода, внутренний скан точек отказа — выборка комбинаций (полное произведение слишком велико). Критерии приёмки: стратегия выборки обоснована (скажем, 1 точка отказа на 10 мутаций); найден дефект «виден только при уродливом вводе + истощении» — или доказано его отсутствие.
Итоги
| Тема | Кратко |
|---|---|
| Вращательное перечисление | скан точек отказа 1..N — каждый путь очистки пройден |
| Тройное утверждение | отказ в возврате / состояние не изменилось (два уровня) / категория+домен ошибки |
| Три формы | счётная (аудит)/квотная (скан)/полностью провальная (быстро) |
| Выживание ошибки | отчёт в OOM не выделяет память; отказ создания ошибки сохраняет XERR_MEMORY |
| Семантика деградации | объект ошибки не выделяется → статический XERR_MEMORY — отказы видны всегда |
| Сторона освобождения | карантин ловит двойное освобождение/UAF — инъекция не только отказ выделения |
| Комбинированное давление | мутация × поточечный отказ — выборочное исследование пространства произведения |
| Дисциплина сброса | полный сброс инъектора и тестируемого объекта каждый раунд — перечисление не деградирует |
| Факт масштаба | 185 файлов _oom — дисциплина, доказанная числом |
| Расширения — тот же закон | расширения с _oom-матрицами — наложение есть шлюз |
📖 Справочники API в этой главе:memory_debug (19 записей)memory_stats (5 записей)error (34 записей)
Полные сигнатуры, ограничения параметров, возвращаемые значения и ошибки