Високонавантажені концентратори VPN, мережеві маршрутизатори та вихідні шлюзи (egress gateways) регулярно обробляють агрегований потік понад 40 Гбіт/с на сучасних багатоядерних серверах. У класичній моделі мережевих сокетів Linux кожна операція надсилання через send(2) або sendmsg(2) змушує ядро копіювати байти з буфера користувацького простору в буфери мережевих пакетів ядра (sk_buff).

Для невеликих обсягів передавання ці витрати залишаються непомітними. Проте під час постійного передавання десятків гігабітів на секунду дублювання пам'яті стає критичним бар'єром: ядра процесора витрачають до 35% часу на виконання інструкцій копіювання (copy_user_generic_string), перевантажуючи шину пам'яті RAM та витісняючи гарячі криптографічні стани з кешів L1 і L2.

Ядро Linux вирішує це фундаментальне обмеження за допомогою прапорця передавання MSG_ZEROCOPY (впровадженого в Linux 4.14 та розширеного для сокетів TCP, UDP і raw packet). Закріплюючи фізичні сторінки пам'яті програми в таблиці сторінок ядра та пов'язуючи їх через списки розбиття (scatter-gather list) для прямого доступу мережевої карти до пам'яті (DMA), MSG_ZEROCOPY повністю усуває копіювання корисного навантаження.

Перевантаження шини пам'яті за стандартного передавання сокетів

Щоб зрозуміти причини зниження продуктивності швидкісних шлюзів VPN, простежимо рух зашифрованих пакетів тунелю в оперативній пам'яті:

[ Демон VPN у просторі користувача ]
       | (Буфер даних розташований в RAM застосунку)
       | sendto(fd, buf, len, 0, ...)
       v
[ Рівень сокетів ядра: Виклик __copy_from_user() ]
       | (Дублювання корисних даних у сторінки sk_buff)
       v
[ Мережевий стек ядра: Маршрутизація IP, Netfilter, Qdisc ]
       v
[ Кільцевий буфер драйвера мережевої карти: Прив'язка DMA ]
       | (Мережева карта зчитує дані з RAM через PCIe DMA)
       v
[ Фізичний мережевий адаптер (NIC) ]

Коли застосунок надсилає блок розміром 16 КБ, процесор зчитує байти з віртуальної пам'яті процесу через свої регістри й записує їх у щойно виділені сторінки ядра. За швидкості 40 Гбіт/с цей конвеєр переміщує понад 5 ГБ даних щосекунди через шину оперативної пам'яті.

Це спричиняє три негативні наслідки:

  1. Конкуренція за смугу пропускання пам'яті: Алгоритми потокового шифрування (зокрема ChaCha20-Poly1305) і хешування Blake2s активно використовують пам'ять. Зайве копіювання відбирає ресурси в модулів криптографічного прискорення.
  2. Засмічення кешу процесора: Безперервне читання та запис гігабайтів транзитних пакетів витісняють таблиці маршрутизації та контексти сесій із надшвидких кешів L1 і L2.
  3. Збільшення затримок перемикання контексту: Синхронне копіювання блокує потік виконання в контексті ядра до повного завершення перенесення байтів.

Архітектурний аналіз механізму MSG_ZEROCOPY

Механізм MSG_ZEROCOPY доручає читання пам'яті безпосередньо апаратному забезпеченню мережевої карти:

[ Демон VPN: Буфер даних buf ]
       |
       | 1. send(fd, buf, len, MSG_ZEROCOPY)
       |    (Ядро закріплює сторінки через get_user_pages_fast)
       |
       +======================+ (Пряме посилання, НУЛЬ операцій копіювання)
       |                      |
       v                      v
[ Структура skb: frags[] ] ---> [ Апаратна частина NIC: Прямий DMA з RAM ]
       |
       | 2. Пакет передається у фізичний канал зв'язку
       | 3. Мережевий адаптер генерує переривання DMA
       v
[ Черга помилок сокета: SO_EE_ORIGIN_ZEROCOPY ]
       |
       | 4. Демон звільняє buf через recvmsg(MSG_ERRQUEUE)
       v
[ Безпечне повторне використання буфера для шифрування ]

1. Закріплення сторінок функцією get_user_pages_fast()

Під час передавання прапорця MSG_ZEROCOPY у системний виклик send(2) ядро Linux не виділяє пам'ять під корисне навантаження. Натомість функція get_user_pages_fast() фіксує фізичні сторінки пам'яті застосунку. Ядро лише додає посилання на сторінки до структури спільної інформації буфера (skb_shinfo(skb)->frags[]).

Оскільки копіювання не відбувається, процесор опрацьовує виключно заголовки пакетів (UDP та IP). Мережева карта зчитує тіло пакета безпосередньо з оперативної пам'яті застосунку через шину PCIe завдяки апаратному механізму scatter-gather DMA.

2. Черга помилок сокета та асинхронні сповіщення

Оскільки мережевий адаптер зчитує пам'ять асинхронно, застосунок не може перезаписувати або звільняти буфер одразу після виклику send, щоб не порушити цілісність даних на лінії. Звільнення пам'яті координується через чергу помилок сокета (MSG_ERRQUEUE):

#define _GNU_SOURCE
#include <sys/socket.h>
#include <linux/errqueue.h>
#include <netinet/in.h>
#include <stdio.h>
#include <unistd.h>

/* Увімкнення режиму Zero-copy на дескрипторі сокета */
int enable_zerocopy(int sock_fd) {
    int one = 1;
    if (setsockopt(sock_fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)) < 0) {
        perror("Помилка setsockopt SO_ZEROCOPY");
        return -1;
    }
    return 0;
}

/* Звільнення зафіксованих буферів після завершення передавання DMA */
void reap_zerocopy_completions(int sock_fd) {
    char control[1024];
    struct msghdr msg = {0};
    struct cmsghdr *cmsg;
    struct sock_extended_err *serr;

    msg.msg_control = control;
    msg.msg_controllen = sizeof(control);

    /* Асинхронне читання сповіщень із черги помилок */
    while (recvmsg(sock_fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT) > 0) {
        for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
            if (cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) {
                serr = (struct sock_extended_err *)CMSG_DATA(cmsg);
                if (serr->ee_origin == SO_EE_ORIGIN_ZEROCOPY) {
                    uint32_t lo = serr->ee_info;  /* Початковий індекс завершення */
                    uint32_t hi = serr->ee_data;  /* Кінцевий індекс завершення */
                    /* Буфери в діапазоні [lo, hi] готові до повторного використання */
                }
            }
        }
    }
}

Ядро присвоює послідовний 32-бітний номер кожній операції zero-copy. Коли мережевий драйвер опрацьовує дескриптор і звільняє sk_buff, у чергу помилок сокета надходить сповіщення з діапазоном підтверджених номерів (ee_info – ee_data).

Критерії практичної ефективності Zero-Copy

Режим Zero-copy не є оптимальним для невеликих пакетів. Закріплення сторінок пам'яті через get_user_pages_fast() та обробка сповіщень із черги помилок створюють постійне обчислювальне навантаження. Необхідно враховувати поріг розміру корисного навантаження:

Розмір корисного навантаження Традиційний sendmsg() Передавання з MSG_ZEROCOPY Інженерна порада
Малий (< 1 500 байтів) Швидке виконання (з кешу CPU) Витрати на фіксацію сторінок Застосовувати sendmsg()
Середній (1 500 – 4 000 байтів) Паритетна швидкодія Паритетна швидкодія Нейтрально
Великий (4 000 – 16 000 байтів) Зростання навантаження на шину Зниження навантаження CPU на 30% Рекомендовано
Надвеликий (> 16 КБ / GSO) Значні затримки шини пам'яті Зниження навантаження CPU на 55% Критична оптимізація

Для шлюзів VPN, що застосовують сегментацію UDP GSO (об'єднання пакетів у супер-блоки від 16 до 64 КБ), MSG_ZEROCOPY забезпечує значний приріст пропускної здатності.

Впровадження в інфраструктуру VPNRice та протокол RiceGuard

У мережевій структурі VPNRice граничні шлюзи Anycast використовують MSG_ZEROCOPY у вихідних чергах протоколу RiceGuard:

[ Криптографічний рушій RiceGuard ]
       | (Шифрування пакетів у циклічний буфер 64 КБ)
       |
       | 1. sendto(udp_fd, ring_slot, 64KB, MSG_ZEROCOPY, ...)
       v
[ Рівень сокетів ядра Linux ]
       | (Фіксація пам'яті ring_slot без копіювання)
       v
[ Мережева карта 100 GbE Mellanox / Intel ]
       | (Передавання пакетів через PCIe DMA)
       |
       | 2. Сповіщення Error Queue повертає слот у пул
       v
[ Повернення та повторне використання буфера RiceGuard ]
  1. Кільцеві пули пам'яті: RiceGuard попередньо виділяє зафіксований пул пам'яті. Зашифровані пакети записуються безпосередньо у відповідні комірки, що виключає динамічне виділення пам'яті.
  2. Спільне застосування UDP GSO та Zero-Copy: RiceGuard об'єднує до 45 тунельних дейтаграм в один блок GSO розміром 64 КБ і надсилає його викликом MSG_ZEROCOPY. Це об'єднує переваги одного системного виклику з відсутністю копіювання даних.
  3. Збереження ресурсів RAM для криптографії: Звільнення шини пам'яті від копіювання дає змогу повністю задіяти її для паралельних обчислень ChaCha20-Poly1305, гарантуючи стійке передавання 40 Гбіт/с без втрат пакетів.

Моніторинг та налаштування параметрів у Linux

Для спостереження за сокетами в режимі zero-copy на серверах під керуванням Linux використовуються стандартні інструменти діагностики:

# Перевірка лічильників мережевих сокетів
cat /proc/net/snmp | grep -E "Udp:|Ip:"

# Аналіз стану черг сокетів та буферів помилок
ss -u -m -i

Усунення зайвого копіювання пам'яті дозволяє сучасним шлюзам VPN повністю реалізувати можливості мережевого обладнання, зберігаючи обчислювальні ресурси процесора.