معماری درگاه‌های شبکه در فضای کاربر (userspace) که با زبان‌های مدرن مانند Rust، Go و C توسعه می‌یابند، به زیربنای اصلی چارچوب‌های اعتماد صفر و پراکسی‌های نرم‌افزاری ماژولار تبدیل شده‌اند. برخلاف ماژول‌های داخل هسته، سرویس‌های فضای کاربر مزایایی چون امنیت حافظه، چرخه استقرار پرشتاب و جداسازی فرآیندها را فراهم می‌سازند. با این حال، هنگامی که نرخ انتقال داده به ده‌ها گیگابیت بر ثانیه می‌رسد، این برنامه‌ها با تنگنای بنیادین سیستم‌عامل لینوکس روبه‌رو می‌شوند: هزینه جابه‌جایی بافت فراخوانی‌های سیستمی (syscall) و هشدارهای همگام ورودی و خروجی.

برای بیش از دو دهه، واسط epoll(7) استاندارد معتبر تسهیم ورودی و خروجی در لینوکس محسوب می‌شد. با وجود کارایی چشمگیر در مدیریت صدها هزار اتصال غیرفعال، الگوی خواندن و نوشتن همگام در epoll در پردازش بسته‌های پرسرعت با افت بازدهی و تاخیر زمانی همراه است. در خطوط ارتباطی ۱۰ و ۴۰ گیگابیت، زیرسیستم io_uring لینوکس با جایگزین کردن فراخوانی‌های مکرر با صف‌های مدور بدون قفل در حافظه اشتراکی، تحولی بنیادین ایجاد می‌کند.

موانع معماری Epoll: سربار فراخوانی‌های سیستمی و تعویض بافت

برای درک دلایل کندی حلقه‌های رویداد متداول در درگاه‌های ترافیک رمزنگاری‌شده، مسیر پیمایش بسته‌های UDP را با epoll(7) بررسی می‌کنیم:

[حلقه رویدادهای سنتی در Epoll]
برنامه فضای کاربر ──> epoll_wait() [تغییر سطح سیستمی] ──> انتظار در هسته لینوکس
                                                                   │
ورود بسته‌ها ──> وقفه سخت‌افزاری کارت شبکه ──> صف دریافت سوکت ─────┘
                           │
بیدار شدن برنامه <─────────┘ [تعویض بافت پردازنده و تخلیه رجیسترها]
        │
برنامه فضای کاربر ──> recvmmsg() [فراخوانی سیستم] ──> کپی داده به حافظه کاربر
        │
منطق برنامه: رمزگشایی داده‌ها و بررسی مسیر بسته‌ها
        │
برنامه فضای کاربر ──> sendmmsg() [فراخوانی سیستم] ──> ارسال به کارت خروجی

هر گردش در این حلقه پردازشی مستلزم عبور مکرر از مرز امتیازات میان فضای کاربر و حلقه هسته است. در پردازنده‌های امروزی x86_64 و ARM64 که به سازوکارهای امنیتی مقابله با نفوذهای کانال جانبی (مانند Meltdown و Spectre) مجهزند، هر فراخوانی سیستمی نیازمند تفکیک جداول صفحات هسته (KPTI)، پاک‌سازی رجیسترها و اعمال موانع پیش‌بینی انشعاب است.

هنگامی که یک درگاه شبکه ۱٫۵ میلیون بسته در ثانیه (Mpps) را جابه‌جا می‌کند، هسته‌های پردازنده بیش از ۴۵ درصد توان پردازشی خود را صرف جابه‌جایی مکرر میان لایه‌های امنیتی می‌کنند، نه اجرای محاسبات رمزنگاری. حتی با تجمیع بسته‌ها از طریق recvmmsg() و sendmmsg()، بازدهی تک‌هسته پردازشی به‌ندرت از مرز ۱٫۸ میلیون بسته در ثانیه فراتر می‌رود.

معماری io_uring: صف‌های مدور در حافظه اشتراکی

فناوری io_uring که توسط ینس اکسپو به هسته لینوکس ۵٫۱ اضافه شد و در نسخه‌های جدید بهبود یافت، این چالش بنیادین را برطرف می‌سازد.

به جای الگوهای سنتی استعلام آمادگی سوکت و سپس اجرای عملیات دریافت و ارسال، io_uring دو صف مدور بدون قفل از نوع تک‌تولیدکننده تک‌مصرف‌کننده (SPSC) ایجاد می‌کند که از طریق mmap() هم‌زمان در فضای آدرس برنامه و فضای هسته نگاشت می‌شوند:

[خط لوله حافظه اشتراکی io_uring]
حافظه برنامه کاربر (Userspace)                فضای هسته لینوکس
┌───────────────────────────┐                ┌───────────────────────────┐
│ صف ارسال درخواست (SQ)     │ ──بدون قفل───> مصرف‌کننده در هسته           │
│ [SQE: RECVMSG Multishot]  │  حلقه مشترک    │ [رشته کاری SQPOLL]        │
└───────────────────────────┘                └───────────────────────────┘
                                                           │
                                                           ▼
┌───────────────────────────┐                ┌───────────────────────────┐
│ صف تکمیل عملیات (CQ)      │ <──بدون قفل─── تولیدکننده در هسته           │
│ [CQE: بسته آماده شد]      │  حلقه مشترک    │ [ارسال ناهمگام به کاربر]  │
└───────────────────────────┘                └───────────────────────────┘

۱. صف ارسال (Submission Queue یا SQ): برنامه ورودی‌های مشخص‌کننده عملیات (SQE مانند IORING_OP_RECVMSG یا IORING_OP_SENDMSG) را آماده ساخته و بدون واسطه در حلقه حافظه اشتراکی می‌نویسد. ۲. صف تکمیل (Completion Queue یا CQ): با پایان پردازش توسط هسته، گزارش‌های فرجام یافته (CQE) حاوی وضعیت و حجم داده دریافتی در حلقه تکمیلی ثبت می‌گردند و برنامه بی‌درنگ آن‌ها را دریافت می‌کند.

از آنجا که هر دو حلقه در یک بلوک پیوسته از حافظه مشترک قرار دارند، برنامه کاربر اشاره‌گرهای آغاز و پایان صف را با اعمال اتمیک ویرایش کرده و در زمان تبادل پایدار، نیاز به هرگونه فراخوانی هسته را به صفر می‌رساند.

ویژگی‌های ارتقای کارایی: بافرهای ثابت، SQPOLL و دریافت مکرر

زیرسیستم io_uring سه سازوکار کارآمد برای هدایت ترافیک شبکه‌ای فشرده ارائه می‌دهد:

۱. رشته پویشگر هسته (IORING_SETUP_SQPOLL)

با فعال‌سازی شناسه IORING_SETUP_SQPOLL در مرحله ایجاد حلقه، هسته لینوکس یک رشته ویژه در سطح کرنل (kthread) به راه می‌اندازد که پیوسته صف ارسال را ارزیابی می‌کند. به محض قرارگیری بسته‌های رمزشده جدید در صف SQ، پردازشگر هسته بلادرنگ آن‌ها را بدون نیاز به فراخوانی io_uring_enter(2) تحویل می‌گیرد.

۲. بافرهای ثابت ثبت‌شده (IORING_REGISTER_BUFFERS)

در تعاملات متداول شبکه، هسته در هر انتقال باید آدرس‌های حافظه کاربر را تحلیل کند، مجوزهای صفحات حافظه را بررسی نماید و شمارنده‌ها را از طریق get_user_pages() افزایش دهد.

در io_uring، درگاه شبکه در ابتدای راه‌اندازی مجموعه‌ای پیوسته از بافرهای داده را تخصیص داده و با دستور IORING_REGISTER_BUFFERS به هسته معرفی می‌کند. هسته صفحات فیزیکی را در رم ثابت نگه می‌دارد، سربار تبدیل نشانی را حذف کرده و مانع فرسایش بافر ترجمه آدرس (TLB) می‌شود.

۳. دریافت چندباره داده‌ها (IORING_RECV_MULTISHOT)

در روش‌های معمول برای خواندن چندین بسته پیاپی، فراخوانی‌های متعدد لازم بود. اما با قابلیت IORING_RECV_MULTISHOT، برنامه تنها یک درخواست SQE به سوکت UDP ارسال می‌کند. هسته به شکل مستمر بسته‌های ورودی را در بافرهای ثبت‌شده ذخیره کرده و به ازای هر بسته واصل‌شده، رویداد CQE مستقلی صادر می‌کند تا زمانی که دستور لغو گردد.

پیاده‌سازی کاربردی: پیکربندی واحد پردازشی io_uring

کد زیر به زبان C شیوه راه‌اندازی یک حلقه پردازش ناهمگام را برای جابه‌جایی جریان‌های پرحجم UDP نمایش می‌دهد:

#include <liburing.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define QUEUE_DEPTH 4096
#define BUF_SIZE 2048
#define NUM_BUFFERS 4096

struct vpn_worker {
    struct io_uring ring;
    int sockfd;
    char *buffer_pool;
};

int init_vpn_worker(struct vpn_worker *worker, int port)
{
    struct io_uring_params params = {0};
    
    /* فعال‌سازی پویش هسته برای حذف کامل فراخوانی‌های سیستمی */
    params.flags = IORING_SETUP_SQPOLL | IORING_SETUP_CQSIZE;
    params.sq_thread_idle = 2000; /* زمان انتظار به میلی‌ثانیه قبل از خواب */
    params.cq_entries = QUEUE_DEPTH * 2;

    if (io_uring_queue_init_params(QUEUE_DEPTH, &worker->ring, &params) < 0) {
        perror("io_uring_queue_init_params failed");
        return -1;
    }

    /* راه‌اندازی سوکت غیرمسدودکننده UDP */
    worker->sockfd = socket(AF_INET, SOCK_DGRAM | SOCK_NONBLOCK, 0);
    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(port),
        .sin_addr.s_addr = INADDR_ANY
    };
    bind(worker->sockfd, (struct sockaddr *)&addr, sizeof(addr));

    /* ثبت شناسه فایل برای پرهیز از قفل‌های مکرر fget/fput */
    io_uring_register_files(&worker->ring, &worker->sockfd, 1);

    /* تخصیص بافرهای هم‌تراز پیوسته در حافظه */
    posix_memalign((void **)&worker->buffer_pool, 4096, NUM_BUFFERS * BUF_SIZE);

    struct iovec iov = {
        .iov_base = worker->buffer_pool,
        .iov_len = NUM_BUFFERS * BUF_SIZE
    };
    io_uring_register_buffers(&worker->ring, &iov, 1);

    return 0;
}

این شیوه تعامل، داده‌های رسیده را از پیچیدگی‌های معمول نرم‌افزاری رها ساخته و ارتباط مستقیمی بین حافظه کاربر و لایه شبکه سیستم‌عامل بنا می‌نهد.

ارزیابی و مقایسه عملکرد عملیاتی

در آزمایش‌های تحت فشار با کپسوله‌سازی دوطرفه روی سرورهای دوپردازنده‌ای AMD EPYC 9654 (شامل ۱۹۲ هسته واقعی) همراه با ۱۰۰ هزار تونل هم‌روند WireGuard، معماری io_uring برتری چشمگیری در قیاس با epoll نشان داد:

شاخص سنجش عملکرد ترکیب Epoll به همراه recvmmsg / sendmmsg ترکیب io_uring با SQPOLL و بافرهای ثابت
توان جابه‌جایی هر هسته ۱٫۷۲ میلیون بسته بر ثانیه ۶٫۸۴ میلیون بسته بر ثانیه (۳٫۹ برابر رشد)
تعداد syscall به ازای ۱۰۰k بسته حدود ۳٬۱۲۵ فراخوانی (دسته‌های ۳۲ تایی) صفر فراخوانی (در شرایط پایدار)
زمان تلف‌شده تعویض بافت ۳۸ درصد کل زمان CPU کمتر از ۲ درصد زمان CPU
تاخیر دنباله‌ای چارک ۹۹٫۹ ۴۸٫۲ میکروثانیه ۴٫۱ میکروثانیه
سطح دسترسی و ایمنی فرآیند فضای کاربری معمول فضای کاربری معمول (بدون نیاز به root)

برخلاف محیط‌های نیازمند به ریشه مانند DPDK که هسته‌ها را در حالت چرخش مداوم به انحصار درآورده و کارت شبکه را از لینوکس جدا می‌سازند، io_uring بدون خدشه وارد ساختن به مدل امنیتی لینوکس به کارایی بسیار بالایی دست می‌یابد.

کاربرد میدانی در گره‌های Anycast سامانه RiceGuard

شبکه جهانی RiceGuard این چارچوب نوآورانه io_uring را بر گره‌های پیرامونی Anycast خود مستقر نموده تا مسیریابی امن و توزیع داده‌ها را با بهره‌وری بالا عملیاتی کند:

  • انتقال پایدار در حداکثر توان کابل: سرورهای Anycast در مجموعه RiceGuard توانایی پایدار هدایت بیش از ۱۸ میلیون بسته در ثانیه را ثبت نموده‌اند و مرزهای بازدهی پیشین را گسترش داده‌اند.
  • توزیع با تاخیر در مقیاس میکروثانیه: با کنار رفتن پرش‌های پیاپی به فضای هسته، میانگین زمان توزیع داخلی بسته‌ها به ۱٫۸ میکروثانیه کاهش یافته که جهش ناگهانی تاخیر در پروتکل‌هایی همچون QUIC و WebRTC را برطرف می‌کند.
  • جداسازی مطمئن محیط اجرایی: به مدد فضاهای نام لینوکس (network namespaces) و گروه‌های کنترلی cgroups، مسیر پردازش داده‌های RiceGuard در محیط‌های کانتینری بدون امتیاز بالا اجرا شده و خطرات ارتقای سطح دسترسی مهار می‌شوند.

بهره‌گیری از حلقه‌های اشتراکی io_uring و حذف وقفه‌های مکرر سیستم، به زیرساخت‌های مدرن امکان می‌دهد تا آسودگی و امنیت کدنویسی در فضای کاربر را با شتاب پردازش هسته ترکیب نمایند.