Високопродуктивні шлюзи VPN, які обробляють мільйони пакетів за секунду (Mpps), стикаються із системними бар'єрами безпосередньо в підсистемі керування оперативною пам'яттю ядра. Коли магістральні канали зв'язку пропускною здатністю 10 Гбіт/с, 25 Гбіт/с або 40 Гбіт/с спрямовують інтенсивний потік зашифрованих UDP-датаграм у концентратор VPN, стандартні шляхи виділення пам'яті перевантажуються. Профілювання процесорного часу ядра показує значні втрати ресурсів на виклики процедур розподілу сторінок (alloc_pages_node), конкуренцію за спін-блокування зон пам'яті та безперервні операції зіставлення Direct Memory Access (DMA).

Класичні мережеві драйвери Linux виділяють фізичні сторінки пам'яті безпосередньо із системного buddy allocator ядра для кожного кадру, що надходить у чергу прийому. Після проходження через буфери сокетів (sk_buff) та обробки застосунками в просторі користувача сторінка вивільняється і повертається назад у системну пам'ять. За інтенсивності трафіку понад 10 Mpps такий безперервний цикл виділення та звільнення сторінок забирає до 30% усього часу обробки програмних переривань (SoftIRQ).

Для усунення цих накладних витрат ядро Linux отримало інтерфейс Page Pool API (net/core/page_pool.c). Завдяки підтримці виділених неблокуючих пулів пам'яті безпосередньо на рівні мережевого драйвера та збереженню активних структур DMA-відображення, Page Pool дає змогу високошвидкісним шлюзам VPN підтримувати тракт прийому пакетів без повторного звернення до менеджера пам'яті.

Проблема накладних витрат пам'яті під час швидкісного прийому пакетів

Щоб зрозуміти, чому традиційне виділення пам'яті обмежує продуктивність шлюзів VPN, простежимо шлях мережевого пакета після його надходження на мережевий адаптер (NIC):

Традиційний цикл виділення сторінок (Високі затримки пам'яті):
[ Кільце прийому NIC ] ---> Потрібен буфер? ---> Виклик alloc_pages()
                                                       |
                                        [ Buddy Allocator зони ] (Конкуренція за блокування!)
                                                       |
                                                       v
[ Фізична сторінка ] <--- dma_map_page() (Затримки IOMMU / Тіньові буфери)
         |
         v
[ Прийом пакета -> SoftIRQ -> Дешифрування тунелю -> Доставка в сокет ]
         |
         v
[ Звільнення пакета ] ------> dma_unmap_page() -> free_pages() -> [ Повернення в RAM ]

Ця традиційна модель має чотири системні обмеження під час обробки трафіку в десятки гігабіт:

  1. Конкуренція за спін-блокування Buddy Allocator: Стандартний виклик alloc_pages() захоплює блокування зони пам'яті (zone->lock). Коли кілька процесорних ядер одночасно відповідають на сплески трафіку від багаточергових карт, блокування призводять до вимушеного очікування потоків.
  2. Високі витрати на операції DMA: Швидкісним мережевим картам необхідні адреси DMA для запису кадрів у системну RAM. Виклик функцій dma_map_page() та dma_unmap_page() для кожного окремого пакета змушує ядро постійно оновлювати таблиці трансляції IOMMU та скидати кеш TLB процесора.
  3. Витіснення даних із кеш-пам'яті: Щойно виділені сторінки пам'яті зазвичай відсутні у швидкому кеші процесора L1/L2. Запис заголовків пакетів у «холодну» пам'ять спричиняє миттєві затримки доступу.
  4. Фрагментація оперативної пам'яті: Безперервний обіг сторінок для короткочасних мережевих пакетів змішується з динамічною пам'яттю звичайних програм, що з часом веде до фрагментації пулів оперативної пам'яті.

Архітектура підсистеми Linux Page Pool

Підсистема Page Pool долає ці труднощі завдяки створенню закритого конвеєра рециркуляції сторінок, який працює повністю синхронно з циклом опитування NAPI мережевого драйвера.

Багаторівнева архітектура рециркуляції пам'яті Page Pool:
[ Мережевий драйвер: Цикл опитування NAPI ]
                    |
                    v
          Є сторінка в alloc_cache?
           /                     \
        [ ТАК ]                [ НІ ]
          |                       |
  Взяти сторінку (0 нс)    Поповнити з ptr_ring (Безпечна черга без блокувань)
          |                       |
          |               Порожньо? ---> Виділити партію з Buddy Allocator
          |                       |
          +-----------+-----------+
                      |
                      v
        [ Повторне використання DMA ] (Нульові витрати на IOMMU!)
                      |
                      v
          [ Прикріпити до sk_buff ]

Архітектура Page Pool організовує пам'ять у два внутрішні рівні кешування:

1. Швидкий локальний кеш процесора (alloc_cache)

Основним рівнем виділення є масив вказівників на фізичні сторінки (struct page *), до якого звертаються виключно в контексті опитування NAPI під час обробки SoftIRQ. Оскільки механізм NAPI гарантує, що в будь-який момент часу опитування певної апаратної черги виконується лише одним ядром CPU, цей кеш працює повністю без блокувань. Отримання підготовленої сторінки зводиться до читання елемента масиву та розіменування вказівника, що триває менше 10 тактів процесора.

2. Безпечне кільце вказівників (ptr_ring)

Коли пакет передається, дешифрується або зчитується сокетом на іншому процесорному ядрі, віддалене ядро не може повернути сторінку безпосередньо в alloc_cache вихідної черги без ризику конфлікту потоків. Замість цього віддалені ядра надсилають звільнені сторінки в потокобезпечну чергу ptr_ring. Під час наступних циклів опитування NAPI черга прийому забирає партії сторінок із цього кільця для поповнення власного кешу alloc_cache.

3. Постійне збереження структури DMA

Page Pool за замовчуванням зберігає активні зв'язки DMA завдяки прапорцю PAGE_POOL_F_DMA_SYNC_DEV. Коли сторінка повертається в пул, драйвер виконує лише швидку інструкцію синхронізації кешу dma_sync_single_for_cpu() або dma_sync_single_for_device(). Ресурсомістка структура відображення IOMMU зберігається в регістрах обладнання, що усуває етап деініціалізації DMA.

Реалізація Page Pool на рівні драйвера мережевої карти

Сучасні драйвери адаптерів (зокрема mlx5, ixgbe, virtio_net та віртуальні мережеві інтерфейси) ініціалізують пул сторінок для кожної черги прийому (RX):

#include <net/page_pool.h>

struct page_pool *init_vpn_rx_page_pool(struct net_device *netdev, int queue_idx) {
    struct page_pool_params params = {
        .flags = PP_FLAG_DMA_MAP | PP_FLAG_DMA_SYNC_DEV,
        .order = 0,                         // Одиночні сторінки 4 КБ
        .pool_size = 2048,                   // Глибина кільцевого буфера
        .nid = dev_to_node(&netdev->dev),   // Локальний вузол пам'яті NUMA
        .dev = &netdev->dev,                // Пристрій для відображення DMA
        .dma_dir = DMA_FROM_DEVICE,         // Напрямок прийому трафіку
        .max_len = PAGE_SIZE,
        .offset = 0,
    };

    return page_pool_create(&params);
}

Під час прийому кадрів у циклі NAPI драйвер отримує підготовлену сторінку безпосередньо з пулу:

// Швидке виділення пам'яті без блокувань у NAPI poll драйвера
struct page *page = page_pool_dev_alloc_pages(pool);
if (!page) {
    // Резервний варіант: відкидання кадру в разі вичерпання пулу
    return -ENOMEM;
}

// Приєднання сторінки безпосередньо до буфера сокета
skb_add_rx_frag(skb, 0, page, 0, packet_len, PAGE_SIZE);

// Передача керування життєвим циклом сторінки мережевому стеку
page_pool_release_page(pool, page);

Коли мережевий стек завершує роботу із вмістом пакета, лічильник посилань на сторінку зменшується. Щойно він сягає нуля, сторінка повертається в чергу рециркуляції Page Pool без знищення глобальним диспетчером пам'яті ядра.

Моніторинг показників Page Pool на діючих шлюзах

Ядро Linux надає точну статистику ефективності рециркуляції сторінок через інтерфейс ethtool та підсистему Netlink page_pool. Системні інженери можуть оцінювати стан обробки в реальному часі:

# Перевірка лічильників рециркуляції сторінок драйвера через ethtool
ethtool -S eth0 | grep -E "page_pool|recycle"

Типовий звіт демонструє високу ефективність конвеєра:

rx_pp_alloc_fast: 489210452      # Миттєві виділення з локального кешу ядра
rx_pp_alloc_slow: 12045          # Поповнення з кільцевої черги
rx_pp_alloc_slow_high_order: 0   # Резервні запити вищого порядку (нуль оптимальний)
rx_pp_alloc_empty: 0             # Події вичерпання пам'яті (має бути рівним нулю)
rx_pp_recycle_cached: 421094820  # Сторінки, повернуті напряму без звернення до шини RAM
rx_pp_recycle_ring: 68103587     # Сторінки, безпечно повернуті з віддалених ядер
rx_pp_recycle_freed: 12045       # Сторінки, повернуті в RAM під час оптимізації пулу

Показник рециркуляції понад 98% підтверджує, що мережева підсистема передає мільйони датаграм майже без витрат процесорного часу на виділення оперативної пам'яті.

Архітектурна взаємодія в технології RiceGuard VPN

RiceGuard, фірмовий протокол маршрутизації третього рівня від команди VPNRice, глибоко інтегрований із механізмами Page Pool ядра Linux:

  1. Тракт дешифрування без дублювання в пам'яті: Під час обробки мультигігабітного трафіку WireGuard або RiceGuard пакети корисного навантаження утримуються в попередньо виділених сторінках Page Pool. Дешифровані внутрішні IP-кадри спрямовуються у віртуальні інтерфейси без копіювання пам'яті.
  2. Ізоляція вузлів пам'яті NUMA: RiceGuard закріплює пули сторінок виключно за тими вузлами NUMA, до яких фізично підключено мережеві адаптери. Це запобігає перенесенню байтів між процесорними сокетами через шини UPI/QPI, зберігаючи пропускну здатність для криптографічних операцій.
  3. Результати тестування під навантаженням: У стрес-тестах під час передачі 35 Гбіт/с через 40 000 одночасних тунелів драйверний Page Pool скоротив затримки виділення пам'яті на 78%, усунувши стрибки завантаження SoftIRQ і забезпечивши стабільну передачу даних на повній швидкості каналу.

Поширені запитання

У чому полягає відмінність між розподільником Linux Slab та підсистемою Page Pool?

Розподільник Slab (kmalloc/SLUB) призначений для об'єктів довільного розміру і створює навантаження на спін-блокування під час одночасної роботи кількох ядер. Підсистема Page Pool оперує повнорозмірними сторінками (зазвичай 4 КБ) під мережеві буфери, пропонуючи роботу без блокувань та безперервне збереження DMA-відображень для високих швидкостей.

Чи призводить використання Page Pool до надмірного споживання оперативної пам'яті?

Page Pool резервує фіксований і помірний обсяг пам'яті (зазвичай від 8 до 32 МБ на одну мережеву чергу). Завдяки постійній рециркуляції цих сторінок він запобігає фрагментації пам'яті та стабілізує навантаження на підсистему RAM.

Чи підтримується робота Page Pool на віртуальних інтерфейсах tun та tap?

Так. Починаючи з версії Linux 5.15, підтримка Page Pool дедалі ширше впроваджується для віртуальних адаптерів та драйверів virtio_net. Завдяки цьому хмарні віртуалізовані шлюзи VPN отримують таку саму оптимізацію пам'яті, як і фізичні виділені сервери.