Коротко нагадаємо структуру БД. Вона складається з таблиць і дерева.
- Таблиця (структур) гендлів, одна на всю систему, вона може бути фраґментована і дефраґментація може лише відбуватися на вставленні нових елементів. В разі переповнення можливе лише пряме розширення. В разі неуспіху його - крах системи.
- Таблиця (структур) протоколів. По одній на кожен валідний гендл. Містить масив протоколів, інстальованих на цьому гендлі. Ця таблиця нефраґментована - завсіди на видаленні протокола з гендла, крайній елемент таблиці переноситься в новоутворену від видалення дірку, і масив завсіди лишається суцільним. Може бути розширена прямо, або через перевиділення нового більшого буферу і копіювання старої таблиці туди.
- Таблиця агентів. По одній на кожен протокол (в масиві протоколів на гендлі). Так само - нефраґментована таблиця, розширювана прямо і через копіювання в крайньому разі.
- Дерево протоколів. Одне на всю систему. Червоно-чорне двійкове дерево пошуку. Грає роль словника - містить усі унікальні протоколи, які були інстальовані на систему під час однієї сесії. Це публічна інофрмація за протокол, глобальна для всієї системи - словник. Приватна - це масиви протоколів на гендлах. Ми не видаляємо вузол взагалі, він може бути інстальований знову, і це добра оптимізація. До того ж, нам не треба робити видалення на червоно-чорних деревах, а це дуже марудна справа.
- Масив вказівників на структури гендлів (з таблиці гендлів). По одному на кожен вузол словника протоколів. Нефраґментована, розширювна прямо і через копіювання таблиця.
Отже, ми маємо гендли на кожному з яких є таблиця істальованих на нього протоколів, і маємо дерево протоколів, на кожному вузлі якого є таблиця вказівників на гендл, на яких цей протокол істальований. Ця надлишковість дуже пришвидшує пошук за протоколом і не дуже ускладнює сервіси видалення/вставлення.
Які вони? Специфікація встановлює такі:
- InstallProtocolInterface()
- ReinstallProtocolInterface()
- UninstallProtocolInterface()
- InstallMultipleProtocolInterfaces()
- UninstallMultipleProtocolInterfaces()
Функції, які інсталюють або деінсталюють протокольні інтерфейси як вони представлені на рівні специфікації - суть функції, які взагалі будують БД, бо по-перше їхня базова функціональність уже диктує це, а по-друге, специфікація покладає на них неявну роботу з видалення/вставлення всієї ієрархії, наприклад якшо функція InstallProtocolInterface() отримує як значення гендла NULL, то вона має створити гендл, а потім вже інсталювати туди протокол. Так само, - ці функції беруть на себе тягар з побудови імплементцінйо-специфічної структури БД.
Спочатку про словник. Було вже згадано, шо видалення на словнику немає, це насправді дуже вигідно для нашого випадку - фірмваря матиме пам'ять для цього, а от робити зайву роботу не буде. Тож уся складність сервісів видалення - це складність визначена не структурою БД, а семантикою прив'язування агентів до протоколів. Це буде висвітлено окремо.
Переповнення масивів.
Ми вибрали організацію, коли може бути переповнення масивів, і, в разі цього, їх треба розширяти. Це було вибране як кращий варіант, за списки масивів. Розширення робиться спочатку спробою справді розширити виділену ділянку за допомгою внутрішнього сервісу пам'яти, а якшо виділити таке розширення не вдасться, тоді робиться виділення більшого буферу і копіювання старого туди. Це очікується як рідкісна подія. Єдина структура, яка не зможе бути перекопійована - це таблиця гендлів - значення гендлів (як змінних) це адреси структур гендлів в таблиці, якшо ми перенесемо таблицю, зміняться адреси, але клієнти користуються старими гендлами і оповіщати усіх про такі зміни нереалістично. Тобто ми можемо лише спробувати розширити цю таблицю прямо. Внутрішня функція для цього - MiExtendPool(). А взагалі, краще вгадати з кількістю гендлів з самого початку. Це розплата за константний час пошуку гендла.
InstallProtocolInterface(). З усього вище сказаного, витікає, шо більшість роботи пов'язаної з побудовою і перебудовою БД, лягає на цю функцію. Вона інсталює протокол на гендл, якшо треба - створює гендл. А значить, вона має вміти:
- вставляти протокол в словник (операція на червоно-чорному дереві - не найлегша операція) - "глобальна" інсталяція протоколу,
- вставляти гендл в таблицю - створення гендла,
- виділяти таблицю протоколів для новоствореного гендла,
- вставляти протокол в таблицю протоколів на гендлі - власне інсталяція протокола,
- вставляти вказівник на гендл в масив вказівників на вузлі дерева;
- розширювати прямо таблицю гендлів,
- розширювати прямо або копіюванням таблицю протоколів,
- розширювати прямо або копіюванням масив вказівників на гендл.
UninstallProtocolInterface(). Сервіс видалення, був би дуже складний, з т.з. БД, якби ми мали потребу справді видаляти вузол з словника. Але оскільки ми здогадались, шо цього насправді не треба робити - складність відпадає. Так само нема складности з розширенням навпаки - ми просто не робимо звуження масиву - це просто не потрібно. А от шо потрібно - робити дефраґментацію на видаленні, для нефраґментованих таблиць (а це масиви протоколів, масиви агентів і масиви вказівників на гендли). На видаленні, функція переносить крайній елемент таблиці в новостворену дірку. Також ми налаштовуємо поля в відповідних структурах - зменшуємо лічильник кількости елементів підлеглої таблиці (він же - індекс на першу вільну комірку таблиці). Ця функція робить дефраґментацію на таблицях протоколів і таблицях вказівників на гендли, а CloseProtocol() робить це на таблицях агентів. Декрементація лічильника робиться цією функцією на вузлах словника і гендлах (кількість гендлів і протоколів відповідно). На протоколах декрементація (кількість агентів) робиться CloseProtocol(). Деінсталяція гендла - це позначення поля валідности відповідної структури в стан "невалідний". І ще одна заввага - оскільки протоколи з словника не видаляються, може виникнути питання про стан тих записів протоколи яких насправді ніде не інстальовані (адже протоколи завсіди мають бути інстальвонаі на якийсь гендл). І як такі записи в словнику узгоджуються з консистентністю БД. Відповідь проста - поле кількости гендлів на масиві вказівників на гендл для такого вузла дорівнює нулю. Це і є ознака неістальованости протокола. З т.з. системи він не присутній в системі, - ніде не інстальований, але з т.з. імплементації ми покращили - спростили двигун БД і пришвидчили вставлення - якшо хтось захоче таки інсталювати цей протокол знову - ми вже маємо запис в словнику, і вставляти знову вузол не треба.
ReinstallProtocolInterface() - просто міняє вказівник на інтерфейс на протоколі інстальованому на гендлі. Там це "просто" - дешо складніше, але знову не з т.з БД, а з т.з. синхронізації і звязків між агентами і протоколами. Так же само як сервіси на агентах мають набагато більше мороки з цими зв'язками. Власне саме вони й обслуговують усю кашу з прив'язуванням драйверів і відслідкованим, контрольованим деінсталюванням/реінсталюванням. За це наступного разу.
Шодо виділення пам'яти. На цьому етапі, ми виділяємо пам'ять для структур з системного пулу, використовуючи функцію AllocatePool(). Це стосується всіх структур, крім таблиці гендлів, яка покишо не має імплементованої точки створення - це верхня ініціалізація фірмварі, вона ще не написана. Але там ми або так само використовуватимемо сервіси пулу, або сервіс сторінкового виділення. Невидно поки ніякої потреби тут створювати якийсь спеціальний алокаційний велосипед. Також ми додали внутрішню, вже згадану, функцію MiExtendPool(). Її прототип такий:
EFI_STATUS MiExtendPool( IN PVOID pBuffer, IN UINTN UnitSize, IN OUT PUINTN pUnitCount );
Вона розширює виділений за допомогою AllocatePool() буфер, на (UnitSize * (*pUnitCount)) байтів, якшо може, або на N одиніць довжиною UnitSize байтів, де 0 <= N <= UnitCount. І пише виділену кількість одиниць назад в змінну (*pUnitCount). Це дуже вдалий вибір логіки аргументів, бо з ним ми уникаємо проблеми невирівняного виділення. Справді, специфікація вимагає виділення з пулу робити вирівняними на 8 байтів - логічно і розумно. Шоб не створювати проблем, ми й виділювані блоки робитимемо цілим множником 8-ми. От уявімо наша структура протоколу має 24 байти в довжину. Якби ми просили просто розширити на скількись 8-батних блоків, могло б вийти неціле число 24-байтних блоків, тобто - неціле число одиниць протоколів. Тоді на другому розширенні, ми могли б мати хибну нестачу місця. Справді, уявімо, шо на першому розширенні масиву протоколів, функція розширення вернула нам 32 байти розширення. Нам вистачить і одного блоку (24 байти) на раз. Ми ділячи виділене на довжину нашого блоку взнаємо кількість нових місць, а зайве місце не чіпаємо. Але якшо нам знадобиться ще й вдруге доточувати масив, ми наприклад можемо зіткнутись з таким сценарієм і проблемою. Шоразу протяжного місця все менше, його може й не бути, отже вдруге функція може вернути лише 16 байтів. Цього не досить, і ми підемо перевиділяти місце і копіювати - дорога операція. Але насправді, ми мали 12 зайвих байтів уже, плюс 16 додано ще, - цього вистачить на один елемент! Але ми цього не бачимо, бо наша структура не має поля для "зайвого місця" - це було б кривобоко ліпити його туди. Отже, рішення полягає в запитуванні розширювати масив на кількість в блоках. Це не порушує внутрішнього алокаційного закону виділяти все на вирівняних на 8 адресах і виділяти цілими до 8-ми штуками; і робить доточування вільним від тієї кострубатости. Як воно не порушує? Ну, ми маємо просити так, шоб принаймні (UnitSize * UnitCount) ділився націло на 8, а краще й UnitSize мати таким, якшо можливо - нам це і так треба, - шоб не мати внутрішніх проблем з вирівнюванням в структурах. На 32-х платформах, вказівник на гендл - а ми маємо таку таблицю, 4-байтний. Ми отримаємо шонайменше 2 місця замість 1-го з ненульових. От і все. Ми просто не маємо просити дати один 4-байтний блок - функція верне неуспіх. Доточування робиться так:
Noh = gProtocolArrayLength; Status = MiExtendPool(pHandle->pProtocolArray, sizeof(PROTOCOL), &Noh);
Ми пишемо в змінну бажану кількість одиниць, на яку б ми хотіли доточити наш масив, і викликаємо функцію, а вона вертає в разі успіху, в цю змінну, - кількість одиниць, яку вона справді виділила, і ця кількість буде від нуля до нашої запитаної кількости. Тож, у випадку з 32-бітними платформами, функція шонайменше верне 2 з ненульвоих варіантів, адже всеодно семантика виділення на пулі диктує 8-байтне вирівнювання, і, як наслідок, - мінімальний блок виділення 8 байтів. В разі неуспіху, функція не чіпатиме масив і залишить все як було.
Цей сервіс дасть нам ще один шанс уникнути копіювання. А у випадку з таблицею гендлів - це єдина надія.