گیت‌وی‌های متمرکز VPN که روزانه به تبادل میلیون‌ها بسته در ثانیه (Mpps) می‌پردازند، همواره با محدودیت‌های فیزیکی سیستم مدیریت حافظه هسته لینوکس مواجه می‌شوند. هنگامی که خطوط پرسرعت ۱۰، ۲۵ یا ۴۰ گیگابیت بر ثانیه حجم انبوهی از بسته‌های UDP رمزنگاری‌شده را به سمت سرور هدایت می‌کنند، فرآیندهای استاندارد تخصیص حافظه به شدت کند می‌شوند. بررسی دقیق پردازش هسته نشان می‌دهد بخش قابل‌توجهی از توان پردازنده صرف توابع تخصیص صفحات فیزیکی (alloc_pages_node)، قفل‌های هم‌زمانی و عملیات تکراری نگاشت دسترسی مستقیم حافظه (DMA) می‌گردد.

در معماری درایورهای قدیمی لینوکس، درایور کارت شبکه برای هر فریم ورودی باید یک صفحه فیزیکی تازه از بخش تخصیص‌دهنده سراسری هسته (buddy allocator) درخواست می‌کرد. پس از عبور بسته از بافرهای سوکت (sk_buff) و مصرف محتوا توسط برنامه‌های لایه کاربر، آن صفحه آزاد شده و به حافظه اصلی بازگردانده می‌شد. در ترافیک‌های بسیار سنگین بالای ۱۰ میلیون بسته در ثانیه، این گردش دائمی تخصیص و آزادسازی حافظه تا ۳۰ درصد از توان پردازش وقفه‌های نرم‌افزاری (SoftIRQ) را هدر می‌دهد.

برای حل این چالش بنیادین، هسته لینوکس زیرسیستم Page Pool را به نشانی (net/core/page_pool.c) معرفی کرد. این سازوکار با نگهداری بافرهای حافظه از پیش آماده و بدون نیاز به قفل در سطح درایور و حفظ مداوم نگاشت‌های سخت‌افزاری DMA، امکان دریافت پرسرعت بسته‌ها را بدون نیاز به تخصیص مداوم حافظه فراهم می‌سازد.

چالش تلاطم حافظه در دریافت بسته‌ها با فرکانس بالا

برای درک بهتر چرایی افت سرعت سرورهای VPN در اثر تخصیص سنتی حافظه، می‌توان مسیر پردازش یک فریم داده را از لحظه رسیدن به کارت شبکه (NIC) بررسی کرد:

چرخه سنتی تخصیص بسته‌ها (تاخیر بالای حافظه):
[ حلقه دریافت کارت شبکه ] ---> نیاز به بافر؟ ---> فراخوانی alloc_pages()
                                                        |
                                          [ تخصیص‌دهنده Buddy ] (رقابت شدید روی قفل‌ها!)
                                                        |
                                                        v
[ صفحه فیزیکی آماده شد ] <--- dma_map_page() (تاخیر IOMMU و بافرهای واسط)
         |
         v
[ ورود بسته -> وقفه SoftIRQ -> رمزگشایی نفق -> تحویل به سوکت ]
         |
         v
[ آزادسازی بسته ] -------> dma_unmap_page() -> free_pages() -> [ بازگشت به RAM ]

این رویکرد سنتی در پردازش ترافیک‌های پرحجم با چهار اشکال ساختاری روبه‌رو است:

  1. رقابت بر سر قفل‌های Buddy Allocator: تابع استاندارد alloc_pages() نیازمند گرفتن یک قفل چرخشی به ازای هر محدوده حافظه (zone->lock) است. هنگامی که چند هسته پردازنده به شکل هم‌زمان بسته‌های ورودی کارت شبکه را دریافت می‌کنند، رقابت برای این قفل‌ها موجب تاخیر و توقف هسته‌ها می‌شود.
  2. سربار عملیات پی‌درپی نگاشت DMA: کارت‌های شبکه پرسرعت برای نوشتن مستقیم اطلاعات در رم نیازمند آدرس DMA هستند. فراخوانی توابع dma_map_page() و dma_unmap_page() برای هر بسته مجزا، سیستم‌عامل را مجبور می‌کند مدام جداول IOMMU را به‌روز کرده و بافرهای TLB را نامعتبر سازد.
  3. از دست رفتن خطوط کش پردازنده: صفحات حافظه‌ای که به تازگی تخصیص یافته‌اند معمولاً در حافظه کش سریع L1/L2 پردازنده حضور ندارند. نوشتن سرآیندهای شبکه در این حافظه سرد، زمان پاسخ‌دهی پردازنده را طولانی می‌کند.
  4. تکه‌تکه شدن حافظه اصلی: درخواست‌های متوالی صفحات کوتاه‌مدت شبکه در کنار حافظه پایدار برنامه‌ها، در درازمدت موجب تکه‌تکه شدن (Fragmentation) فضای فیزیکی رم سرور می‌شود.

معماری لایه‌ای زیرسیستم 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 حافظه را در دو لایه کش داخلی سازمان‌دهی می‌کند:

۱. کش تخصیص سریع اختصاصی هر هسته (alloc_cache)

لایه نخست تخصیص شامل آرایه‌ای از نشانگرهای صفحات فیزیکی (struct page *) است که فقط در چارچوب پیمایش NAPI در پردازش وقفه‌های نرم‌افزاری فراخوانی می‌شود. از آن‌جا که سازوکار NAPI تضمین می‌کند در هر لحظه تنها یک هسته پردازنده صف سخت‌افزاری مربوطه را مدیریت می‌کند، این کش بدون کوچک‌ترین قفل‌گذاری کار می‌کند. برداشتن یک صفحه بازیافتی تنها نیازمند خواندن یک عنصر از آرایه است و کمتر از ۱۰ سیکل ساعت پردازنده زمان می‌برد.

۲. صف حلقوی نشانگرهای بدون قفل (ptr_ring)

هنگامی که یک بسته داده توسط هسته پردازنده دیگری رمزگشایی یا به سوکت کاربر تحویل داده می‌شود، آن هسته راه دور نمی‌تواند صفحه مصرف‌شده را بدون هماهنگی به alloc_cache مبدا بازگرداند. در عوض، هسته‌های دیگر صفحات آزادشده را به یک صف حلقوی با چند تولیدکننده (ptr_ring) ارسال می‌کنند. در چرخه‌های بعدی NAPI، صف دریافت‌کننده بسته‌ای از صفحات را از این حلقه برای پر کردن کش محلی خود برداشت می‌کند.

۳. ماندگاری پیوسته نگاشت حافظه DMA

زیرسیستم Page Pool با پرچم PAGE_POOL_F_DMA_SYNC_DEV نگاشت سخت‌افزاری DMA را به شکل پیش‌فرض فعال نگه می‌دارد. هنگامی که صفحه‌ای به استخر بازمی‌گردد، درایور تنها یک دستور همگام‌سازی سبک کش نظیر dma_sync_single_for_cpu() یا dma_sync_single_for_device() را اجرا می‌کند و نیازی به از بین بردن جداول پیچیده سخت‌افزاری IOMMU ندارد.

پیاده‌سازی 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,                         // صفحات مجزای ۴ کیلوبایتی
        .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 درایور
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 در سرورهای عملیاتی

هسته لینوکس گزارش‌های دقیقی از وضعیت بازیافت حافظه را از طریق رابط 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  # صفحات بازیافتی بدون درگیر شدن کنترلر رم
rx_pp_recycle_ring: 68103587     # صفحات برگشتی ایمن از سایر هسته‌ها
rx_pp_recycle_freed: 12045       # صفحات آزادشده در رم هنگام تنظیم اندازه استخر

نرخ بازیافت بالای ۹۸ درصد نشان می‌دهد کارت شبکه میلیون‌ها بسته ورودی را با کمترین سربار حافظه مدیریت می‌کند.

یکپارچگی معماری در پروتکل RiceGuard VPN

پروتکل مسیریابی لایه سه RiceGuard که توسط تیم VPNRice طراحی شده است، تعامل تنگاتنگی با ساختار Page Pool در سرورهای انتقال دارد:

  1. مسیر رمزگشایی بدون نیاز به کپی مجدد: در هنگام دریافت جریان‌های سنگین رمزنگاری‌شده با WireGuard یا RiceGuard، محتوای بسته‌ها در صفحات از پیش رزروشده Page Pool باقی می‌ماند و فریم‌های داخلی پس از رمزگشایی بدون اتلاف حافظه به کارت‌های مجازی منتقل می‌شوند.
  2. جداسازی نودهای حافظه NUMA: پروتکل RiceGuard استخرهای حافظه را دقیقا روی همان نود NUMA که کارت شبکه فیزیکی به آن متصل است مستقر می‌سازد. این کار از ارسال داده روی خطوط بین‌پردازنده‌ای UPI/QPI جلوگیری کرده و تمام پهنای باند گذرگاه را در اختیار الگوریتم‌های ChaCha20-Poly1305 می‌گذارد.
  3. نتایج ثبت‌شده در تست بار: در آزمایش‌های پرفشار با ۳۵ گیگابیت بر ثانیه ترافیک و ۴۰ هزار تونل فعال، بهره‌گیری از Page Pool تاخیر تخصیص حافظه در هسته را ۷۸ درصد کاهش داد و پایداری کامل را در حداکثر توان کارت شبکه به نمایش گذاشت.

پرسش‌های متداول

تفاوت اصلی میان تخصیص‌دهنده Slab و سازوکار Page Pool در لینوکس چیست؟

تخصیص‌دهنده Slab (شامل kmalloc و SLUB) برای نگهداری شیءهای کوچک با اندازه‌های متغیر در هسته طراحی شده و هنگام تخصیص روی چند هسته نیازمند قفل‌های چرخشی است. در مقابل، Page Pool به شکل ویژه برای بافرهای شبکه و بر پایه صفحات کامل فیزیکی (معمولاً ۴ کیلوبایت) توسعه یافته تا تخصیص بدون قفل و نگاشت پایدار DMA را ممکن سازد.

آیا استفاده از Page Pool باعث مصرف بیش از حد رم سرور می‌شود؟

خیر. زیرسیستم Page Pool مقدار مشخص و بسیار محدودی از رم سرور (بین ۸ تا ۳۲ مگابایت به ازای هر صف کارت شبکه) را پیشاپیش رزرو می‌کند. با چرخش مداوم همین حافظه، از تکه‌تکه شدن رم جلوگیری شده و بار حافظه سیستم کاملاً کنترل می‌شود.

آیا قابلیت Page Pool روی کارت‌های شبکه مجازی مانند tun و tap پشتیبانی می‌شود؟

بله. نسخه‌های جدید هسته لینوکس (از نگارش 5.15 به بعد) پشتیبانی از Page Pool را برای درایورهای مجازی و virtio_net توسعه داده‌اند. این امر به سرورهای ابری و مجازی VPN اجازه می‌دهد از همان بازدهی بازیافت حافظه سرورهای فیزیکی بهره‌مند شوند.