درگاه‌های ارتباطی با پهنای باند بالا و سرورهای خروجی شبکه به طور پیوسته حجم داده‌ای فراتر از ۴۰ گیگابیت بر ثانیه را روی سرورهای چند‌هسته‌ای مدرن مدیریت می‌کنند. در الگوی استاندارد سوکت‌های لینوکس، هر بار فراخوانی دستورهای ارسال مانند send(2) یا sendmsg(2) هسته سیستم‌عامل را وادار می‌سازد تا داده‌ها را از فضای حافظه برنامه به ساختارهای حافظه بسته هسته (sk_buff) رونویسی کند.

در حجم ترافیک پایین این فرایند چندان محسوس نیست؛ اما در سرعت‌های پایدار چند ده گیگابیت، تکرار مداوم رونویسی حافظه به یک گلوگاه جدی تبدیل می‌شود: هسته‌های پردازنده ممکن است تا ۳۵ درصد توان خود را صرف کپی کردن بایت‌های حافظه (copy_user_generic_string) کنند، که این امر گذرگاه حافظه رم را اشباع کرده و اطلاعات رمزنگاری فعال را از حافظه نهان سریع L1 و L2 پردازنده خارج می‌سازد.

هسته لینوکس با معرفی پرچم انتقال MSG_ZEROCOPY این مانع ساختاری را برطرف کرده است. این سازوکار با تثبیت صفحات حافظه برنامه در جدول صفحات هسته و ثبت ارجاعات مستقیم برای دسترسی مستقیم کارت شبکه به حافظه (DMA)، کپی داده در مسیر ارسال بسته‌ها را به صفر می‌رساند.

اشباع گذرگاه حافظه در شیوه سنتی ارسال سوکت

برای درک بهتر محدودیت‌های روش سنتی در درگاه‌های پرسرعت، مسیر عبور داده‌های رمزگذاری‌شده را بررسی می‌کنیم:

[ سرویس ارتباطی در فضای کاربر ]
       | (داده در حافظه رم برنامه قرار دارد)
       | sendto(fd, buf, len, 0, ...)
       v
[ لایه سوکت هسته: فراخوانی __copy_from_user() ]
       | (رونویسی کامل داده در صفحات sk_buff)
       v
[ پشته شبکه هسته: مسیریابی، فیلتر بسته‌ها و صف‌بندی ]
       v
[ بافر حلقه‌ای راه‌انداز کارت شبکه: نگاشت DMA ]
       | (کارت شبکه داده را مستقیما از رم می‌خواند)
       v
[ کارت شبکه فیزیکی (NIC) ]

هنگامی که برنامه‌ای یک بسته ۱۶ کیلوبایتی را ارسال می‌کند، پردازنده باید بایت‌ها را از حافظه مجازی کاربر خوانده و در صفحات تازه اختصاص یافته هسته بنویسد. در نرخ انتقال ۴۰ گیگابیت، این چرخه در هر ثانیه بیش از ۵ گیگابایت داده را روی گذرگاه حافظه منتقل می‌نماید.

این فرایند سه چالش اساسی ایجاد می‌کند:

  1. رقابت برای پهنای باند حافظه رم: الگوریتم‌های رمزنگاری مانند ChaCha20-Poly1305 برای پردازش نیازمند دسترسی مداوم به حافظه هستند. کپی مکرر داده‌ها پهنای باند مورد نیاز این الگوریتم‌ها را محدود می‌سازد.
  2. کاهش کارایی حافظه نهان پردازنده: نوشتن پیوسته بسته‌های موقت، جداول مسیریابی و داده‌های نشست را از حافظه سریع پردازنده بیرون می‌راند.
  3. افزایش تاخیر تعویض زمینه پردازشی: اجرای همگام کپی حافظه، رشته در حال اجرا را تا پایان رونویسی در حالت انتظار در هسته نگه می‌دارد.

بررسی ساختار فنی سازوکار MSG_ZEROCOPY

سازوکار MSG_ZEROCOPY خواندن داده از حافظه را مستقیما به تراشه کارت شبکه محول می‌نماید:

[ سرویس ارتباطی: بافر حافظه buf ]
       |
       | 1. send(fd, buf, len, MSG_ZEROCOPY)
       |    (هسته صفحات را با get_user_pages_fast قفل می‌کند)
       |
       +======================+ (ارجاع مستقیم، صفر بایت کپی)
       |                      |
       v                      v
[ ساختار skb: بخش frags[] ] -> [ سخت‌افزار کارت شبکه: خواندن مستقیم DMA از RAM ]
       |
       | 2. ارسال بسته به کابل شبکه فیزیکی
       | 3. کارت شبکه وقفه پایان ارسال را ثبت می‌کند
       v
[ صف خطای سوکت: SO_EE_ORIGIN_ZEROCOPY ]
       |
       | 4. آزادسازی بافر از طریق recvmsg(MSG_ERRQUEUE)
       v
[ استفاده مجدد از بافر برای بسته بعدی ]

۱. تثبیت صفحات با get_user_pages_fast()

با ارسال پرچم MSG_ZEROCOPY، هسته لینوکس هیچ حافظه جداگانه‌ای برای متن پیام ایجاد نمی‌کند، بلکه با تابع get_user_pages_fast() صفحات فیزیکی حافظه کاربر را در جدول قفل می‌نماید. هسته فقط آدرس این صفحات را در ساختار سوکت ذخیره می‌کند.

بدین ترتیب پردازنده فقط سرآمد بسته (UDP و IP) را پردازش می‌کند و کارت شبکه محتوای پیام را مستقیما از حافظه رم کاربر از طریق مسیر PCIe DMA فراخوانی می‌نماید.

۲. صف خطای سوکت و اعلان پایان انتقال داده

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

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

/* فعال‌سازی قابلیت انتقال بدون کپی روی سوکت */
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] برای استفاده مجدد آماده هستند */
                }
            }
        }
    }
}

هسته لینوکس برای هر ارسال یک شماره افزایشی ۳۲ بیتی اختصاص می‌دهد. با پایان فرایند ارسال توسط کارت شبکه، گزارش تکمیل بازه بسته‌ها به صف خطای سوکت واریز می‌گردد.

شرایط و معیارهای بهره‌وری انتقال بدون کپی

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

اندازه داده بسته ارسال با sendmsg() معمولی ارسال با MSG_ZEROCOPY پیشنهاد فنی
کوچک (کمتر از ۱۵۰۰ بایت) بسیار سریع به کمک حافظه نهان هزینه قفل کردن صفحات بالاست استفاده از روش سنتی
متوسط (۱۵۰۰ تا ۴۰۰۰ بایت) عملکرد نزدیک به هم عملکرد نزدیک به هم برابر
بزرگ (۴۰۰۰ تا ۱۶۰۰۰ بایت) فشار بر گذرگاه حافظه رم کاهش ۳۰ درصدی پردازش CPU پیشنهاد می‌شود
بسیار بزرگ (بیش از ۱۶ کیلوبایت) افت محسوس توان پردازنده کاهش ۵۵ درصدی مصرف منابع اکیدا توصیه می‌شود

در درگاه‌هایی که از تجمیع بسته‌ها (UDP GSO) استفاده می‌کنند و بسته‌های بزرگ ۱۶ تا ۶۴ کیلوبایتی می‌سازند، سازوکار MSG_ZEROCOPY جهشی بزرگ در نرخ انتقال ایجاد می‌کند.

کاربرد عملیاتی در سرویس‌های VPNRice و پروتکل RiceGuard

در زیرساخت شبکه VPNRice، سرورهای Anycast این فناوری را در مسیر ارسال داده‌های پروتکل اختصاصی RiceGuard به کار بسته‌اند:

[ موتور رمزنگاری RiceGuard ]
       | (رمزگذاری داده‌ها در بافر حلقه‌ای ۶۴ کیلوبایتی)
       |
       | 1. sendto(udp_fd, ring_slot, 64KB, MSG_ZEROCOPY, ...)
       v
[ لایه سوکت هسته لینوکس ]
       | (قفل حافظه بدون رونویسی بایت‌ها)
       v
[ کارت شبکه ۱۰۰ گیگابیت اینتل / ملانوکس ]
       | (انتقال مستقیم داده با PCIe DMA)
       |
       | 2. بازگرداندن بافر به مخزن با اعلان Error Queue
       v
[ استفاده مجدد از حافظه در چرخه جدید RiceGuard ]
  1. مدیریت بافرهای حلقه‌ای ثابت: بافرهای حافظه از پیش قفل شده‌اند تا در طول زمان فعالیت نیازی به تخصیص حافظه جدید نباشد.
  2. هم‌افزایی با فناوری UDP GSO: بسته‌های متعدد تا سقف ۶۴ کیلوبایت تجمیع شده و در یک مرحله بدون رونویسی به کارت شبکه فرستاده می‌شوند.
  3. آزاد ماندن حافظه برای پردازش‌های امنیتی: با محول شدن جابجایی داده‌ها به کارت شبکه، گذرگاه رم برای محاسبات پیچیده رمزنگاری آزاد باقی می‌ماند و سرعت انتقال ۴۰ گیگابیت بدون افت بسته حفظ می‌شود.

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

مدیران سیستم می‌توانند وضعیت سوکت‌ها را با دستورهای زیر پایش کنند:

# بررسی آمارهای ارسال سوکت‌های شبکه
cat /proc/net/snmp | grep -E "Udp:|Ip:"

# مشاهده وضعیت و عمق صف‌های سوکت
ss -u -m -i

سازوکار MSG_ZEROCOPY با از میان برداشتن مراحل اضافی جابجایی حافظه، شرایط لازم برای دستیابی به بالاترین راندمان شبکه را در درگاه‌های مدرن ارتباطی مهیا می‌سازد.