مقدمه ای کوتاه بر BPF و eBPF

سلام، هابر! به اطلاع شما می‌رسانیم که در حال آماده‌سازی کتابی برای انتشار هستیم.»قابلیت مشاهده لینوکس با BPF".

مقدمه ای کوتاه بر BPF و eBPF
از آنجایی که ماشین مجازی BPF به تکامل خود ادامه می دهد و به طور فعال در عمل استفاده می شود، مقاله ای را برای شما ترجمه کرده ایم که قابلیت های اصلی و وضعیت فعلی آن را شرح می دهد.

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

با انتقال کنترل کامل کارت شبکه به یک برنامه فضای کاربر، سربار هسته (تغییر متن، پردازش لایه شبکه، وقفه ها و غیره) را کاهش می دهیم، که در هنگام اجرا با سرعت 10 گیگابیت بر ثانیه یا بالاتر بسیار مهم است. بای پس کرنل به همراه ترکیبی از ویژگی های دیگر (پردازش دسته ای) و تنظیم دقیق عملکرد (حسابداری NUMA, جداسازی CPUو غیره) با اصول پردازش شبکه با کارایی بالا در فضای کاربر مطابقت دارد. شاید یک مثال نمونه از این رویکرد جدید برای پردازش بسته باشد DPDK از اینتل (کیت توسعه هواپیمای داده، اگرچه ابزارها و تکنیک های شناخته شده دیگری نیز وجود دارد، از جمله VPP سیسکو (پردازش بسته های برداری)، Netmap و البته، اسناب.

سازماندهی تعاملات شبکه در فضای کاربر دارای تعدادی معایب است:

  • هسته سیستم عامل یک لایه انتزاعی برای منابع سخت افزاری است. از آنجا که برنامه های فضای کاربر باید منابع خود را مستقیماً مدیریت کنند، آنها نیز باید سخت افزار خود را مدیریت کنند. این اغلب به این معنی است که باید درایورهای خود را برنامه ریزی کنید.
  • از آنجا که ما فضای هسته را به طور کامل کنار می گذاریم، همچنین از تمام عملکردهای شبکه ارائه شده توسط هسته صرف نظر می کنیم. برنامه های فضای کاربر باید ویژگی هایی را که ممکن است قبلاً توسط هسته یا سیستم عامل ارائه شده است، دوباره پیاده سازی کنند.
  • برنامه ها در حالت sandbox کار می کنند که به طور جدی تعامل آنها را محدود می کند و از ادغام آنها با سایر بخش های سیستم عامل جلوگیری می کند.

در اصل، هنگامی که شبکه در فضای کاربر رخ می دهد، دستاوردهای عملکرد با انتقال پردازش بسته از هسته به فضای کاربر به دست می آید. XDP دقیقاً برعکس عمل می کند: برنامه های شبکه را از فضای کاربر (فیلترها، حل کننده ها، مسیریابی و غیره) به فضای هسته منتقل می کند. XDP به ما این امکان را می دهد که به محض اینکه بسته ای به رابط شبکه برخورد کرد و قبل از اینکه به سمت زیر سیستم شبکه هسته حرکت کند، یک عملکرد شبکه را انجام دهیم. در نتیجه سرعت پردازش بسته ها به میزان قابل توجهی افزایش می یابد. با این حال، چگونه کرنل به کاربر اجازه می دهد تا برنامه های خود را در فضای هسته اجرا کند؟ قبل از پاسخ به این سوال، بیایید ببینیم BPF چیست.

BPF و eBPF

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

یکی از معروف ترین ابزارهای استفاده از BPF است tcpdump. هنگام گرفتن بسته ها با استفاده از tcpdump کاربر می تواند یک عبارت را برای فیلتر کردن بسته ها مشخص کند. فقط بسته هایی که با این عبارت مطابقت دارند گرفته می شوند. به عنوان مثال، عبارت "tcp dst port 80” به تمام بسته های TCP اشاره دارد که به پورت 80 می رسند. کامپایلر می تواند این عبارت را با تبدیل آن به بایت کد BPF کوتاه کند.

$ sudo tcpdump -d "tcp dst port 80"
(000) ldh [12] (001) jeq #0x86dd jt 2 jf 6
(002) ldb [20] (003) jeq #0x6 jt 4 jf 15
(004) ldh [56] (005) jeq #0x50 jt 14 jf 15
(006) jeq #0x800 jt 7 jf 15
(007) ldb [23] (008) jeq #0x6 jt 9 jf 15
(009) ldh [20] (010) jset #0x1fff jt 15 jf 11
(011) ldxb 4*([14]&0xf)
(012) ldh [x + 16] (013) jeq #0x50 jt 14 jf 15
(014) ret #262144
(015) ret #0

این همان کاری است که برنامه فوق اساساً انجام می دهد:

  • دستورالعمل (000): بسته را در آفست 12 به عنوان یک کلمه 16 بیتی در انباشته بارگذاری می کند. افست 12 مربوط به اتری نوع بسته است.
  • دستورالعمل (001): مقدار موجود در انباشته کننده را با 0x86dd، یعنی با مقدار اترتیپ برای IPv6 مقایسه می کند. اگر نتیجه درست باشد، شمارنده برنامه به دستورالعمل (002) و اگر نه، سپس به (006) می رود.
  • دستورالعمل (006): مقدار را با 0x800 (مقدار اترتیپ برای IPv4) مقایسه می کند. اگر پاسخ درست باشد، برنامه به (007) و اگر نه، سپس به (015) می رود.

و به همین ترتیب تا زمانی که برنامه فیلترینگ بسته نتیجه ای را برگرداند. این معمولا یک Boolean است. بازگرداندن یک مقدار غیر صفر (دستورالعمل (014)) به این معنی است که بسته پذیرفته شده است و بازگرداندن مقدار صفر (دستورالعمل (015)) به معنای عدم پذیرش بسته است.

ماشین مجازی BPF و بایت کد آن توسط استیو مک کان و ون جاکوبسون در اواخر سال 1992 هنگامی که مقاله آنها منتشر شد پیشنهاد شد. فیلتر بسته BSD: معماری جدید برای ضبط بسته در سطح کاربر، این فناوری برای اولین بار در کنفرانس Usenix در زمستان 1993 ارائه شد.

از آنجایی که BPF یک ماشین مجازی است، محیطی را که برنامه ها در آن اجرا می شوند را تعریف می کند. علاوه بر بایت کد، مدل حافظه دسته ای را نیز تعریف می کند (دستورالعمل های بارگذاری به طور ضمنی بر روی دسته اعمال می شود)، ثبات ها (A و X؛ انباشته کننده و ثبات های فهرست)، ذخیره سازی حافظه خراش، و شمارنده برنامه ضمنی. جالب اینجاست که بایت کد BPF از موتورولا 6502 ISA مدل شده است. همانطور که استیو مک کان در کتاب خود به یاد می آورد گزارش عمومی در Sharkfest '11، او با بیلد 6502 از دوران دبیرستان خود در برنامه نویسی Apple II آشنا بود و این دانش بر کار او برای طراحی بایت کد BPF تأثیر گذاشت.

پشتیبانی BPF در هسته لینوکس در نسخه‌های 2.5 و بالاتر اجرا می‌شود که عمدتاً با تلاش Jay Schullist اضافه شده است. کد BPF تا سال 2011 بدون تغییر باقی ماند، زمانی که اریک دومازت مفسر BPF را برای اجرا در حالت JIT دوباره طراحی کرد (منبع: JIT برای فیلترهای بسته). پس از این، هسته به جای تفسیر بایت کد BPF، می تواند برنامه های BPF را مستقیماً به معماری هدف تبدیل کند: x86، ARM، MIPS و غیره.

بعداً، در سال 2014، الکسی استاروویتوف مکانیسم جدید JIT را برای BPF پیشنهاد کرد. در واقع، این JIT جدید به یک معماری جدید مبتنی بر BPF تبدیل شد و eBPF نام گرفت. من فکر می کنم هر دو ماشین مجازی برای مدتی با هم وجود داشتند، اما در حال حاضر فیلترینگ بسته بر اساس eBPF پیاده سازی می شود. در واقع، در بسیاری از نمونه‌های مستندات مدرن، BPF به عنوان eBPF شناخته می‌شود و BPF کلاسیک امروزه به عنوان cBPF شناخته می‌شود.

eBPF ماشین مجازی کلاسیک BPF را به چندین روش گسترش می دهد:

  • بر اساس معماری های مدرن 64 ​​بیتی. eBPF از ثبات های 64 بیتی استفاده می کند و تعداد ثبات های موجود را از 2 (انجمن و X) به 10 افزایش می دهد.
  • از زیر سیستم لایه شبکه جدا شده است. BPF به مدل داده دسته ای گره خورده بود. از آنجایی که برای فیلتر کردن بسته ها استفاده می شد، کد آن در زیر سیستمی قرار داشت که ارتباطات شبکه را فراهم می کند. با این حال، ماشین مجازی eBPF دیگر به مدل داده گره نمی خورد و می تواند برای هر منظوری مورد استفاده قرار گیرد. بنابراین، اکنون برنامه eBPF را می توان به tracepoint یا kprobe متصل کرد. این راه را برای ابزار دقیق eBPF، تجزیه و تحلیل عملکرد و بسیاری موارد استفاده دیگر در زمینه سایر زیرسیستم‌های هسته باز می‌کند. اکنون کد eBPF در مسیر خودش قرار دارد: kernel/bpf.
  • فروشگاه های جهانی داده به نام Maps. نقشه‌ها فروشگاه‌هایی با ارزش کلیدی هستند که امکان تبادل داده بین فضای کاربر و فضای هسته را فراهم می‌کنند. eBPF انواع مختلفی از نقشه ها را ارائه می دهد.
  • توابع ثانویه به طور خاص، برای بازنویسی یک بسته، محاسبه یک چک‌سوم، یا شبیه‌سازی یک بسته. این توابع در داخل هسته اجرا می شوند و برنامه های فضای کاربر نیستند. همچنین می توانید از برنامه های eBPF تماس های سیستمی برقرار کنید.
  • پایان دادن به تماس ها اندازه برنامه در eBPF به 4096 بایت محدود شده است. ویژگی tail call به یک برنامه eBPF اجازه می دهد تا کنترل را به یک برنامه eBPF جدید منتقل کند و بنابراین این محدودیت را دور بزند (تا 32 برنامه را می توان از این طریق پیوند داد).

eBPF: مثال

چندین مثال برای eBPF در منابع هسته لینوکس وجود دارد. آنها در samples/bpf/ موجود هستند. برای کامپایل این نمونه ها کافی است وارد کنید:

$ sudo make samples/bpf/

من خودم مثال جدیدی برای eBPF نمی نویسم، اما از یکی از نمونه های موجود در samples/bpf/ استفاده خواهم کرد. من به برخی از قسمت های کد نگاه می کنم و نحوه عملکرد آن را توضیح می دهم. به عنوان مثال، من برنامه را انتخاب کردم tracex4.

به طور کلی هر یک از نمونه های نمونه/bpf/ از دو فایل تشکیل شده است. در این مورد:

  • tracex4_kern.c، حاوی کد منبعی است که باید در هسته به عنوان بایت کد eBPF اجرا شود.
  • tracex4_user.c، حاوی برنامه ای از فضای کاربر است.

در این صورت باید کامپایل کنیم tracex4_kern.c به بایت کد eBPF. در حال حاضر در gcc هیچ باطنی برای eBPF وجود ندارد. خوشبختانه، clang می تواند بایت کد eBPF را خروجی کند. Makefile استفاده می کند clang برای تدوین tracex4_kern.c به فایل شی

در بالا اشاره کردم که یکی از جالب ترین ویژگی های eBPF نقشه ها هستند. tracex4_kern یک نقشه را تعریف می کند:

struct pair {
    u64 val;
    u64 ip;
};  

struct bpf_map_def SEC("maps") my_map = {
    .type = BPF_MAP_TYPE_HASH,
    .key_size = sizeof(long),
    .value_size = sizeof(struct pair),
    .max_entries = 1000000,
};

BPF_MAP_TYPE_HASH یکی از انواع کارت های ارائه شده توسط eBPF است. در این مورد، این فقط یک هش است. ممکن است شما نیز متوجه یک تبلیغ شده باشید SEC("maps"). SEC یک ماکرو است که برای ایجاد یک بخش جدید از یک فایل باینری استفاده می شود. در واقع، در مثال tracex4_kern دو بخش دیگر تعریف شده است:

SEC("kprobe/kmem_cache_free")
int bpf_prog1(struct pt_regs *ctx)
{   
    long ptr = PT_REGS_PARM2(ctx);

    bpf_map_delete_elem(&my_map, &ptr); 
    return 0;
}
    
SEC("kretprobe/kmem_cache_alloc_node") 
int bpf_prog2(struct pt_regs *ctx)
{
    long ptr = PT_REGS_RC(ctx);
    long ip = 0;

    // получаем ip-адрес вызывающей стороны kmem_cache_alloc_node() 
    BPF_KRETPROBE_READ_RET_IP(ip, ctx);

    struct pair v = {
        .val = bpf_ktime_get_ns(),
        .ip = ip,
    };
    
    bpf_map_update_elem(&my_map, &ptr, &v, BPF_ANY);
    return 0;
}   

این دو تابع به شما امکان می دهد یک ورودی را از نقشه حذف کنید (kprobe/kmem_cache_free) و یک ورودی جدید به نقشه اضافه کنید (kretprobe/kmem_cache_alloc_node). همه نام توابع که با حروف بزرگ نوشته شده اند مطابق با ماکروهای تعریف شده در هستند bpf_helpers.h.

اگر بخش های فایل شی را تخلیه کنم، باید ببینم که این بخش های جدید از قبل تعریف شده اند:

$ objdump -h tracex4_kern.o

tracex4_kern.o: file format elf64-little

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000000 0000000000000000 0000000000000000 00000040 2**2
CONTENTS, ALLOC, LOAD, READONLY, CODE
1 kprobe/kmem_cache_free 00000048 0000000000000000 0000000000000000 00000040 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
2 kretprobe/kmem_cache_alloc_node 000000c0 0000000000000000 0000000000000000 00000088 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
3 maps 0000001c 0000000000000000 0000000000000000 00000148 2**2
CONTENTS, ALLOC, LOAD, DATA
4 license 00000004 0000000000000000 0000000000000000 00000164 2**0
CONTENTS, ALLOC, LOAD, DATA
5 version 00000004 0000000000000000 0000000000000000 00000168 2**2
CONTENTS, ALLOC, LOAD, DATA
6 .eh_frame 00000050 0000000000000000 0000000000000000 00000170 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

نیز وجود دارد tracex4_user.c، برنامه اصلی اساساً این برنامه به رویدادها گوش می دهد kmem_cache_alloc_node. هنگامی که چنین رویدادی رخ می دهد، کد eBPF مربوطه اجرا می شود. کد ویژگی IP شی را در نقشه ذخیره می کند و سپس شی از طریق برنامه اصلی حلقه می شود. مثال:

$ sudo ./tracex4
obj 0xffff8d6430f60a00 is 2sec old was allocated at ip ffffffff9891ad90
obj 0xffff8d6062ca5e00 is 23sec old was allocated at ip ffffffff98090e8f
obj 0xffff8d5f80161780 is 6sec old was allocated at ip ffffffff98090e8f

یک برنامه فضای کاربر و یک برنامه eBPF چگونه به هم مرتبط هستند؟ در مقداردهی اولیه tracex4_user.c یک فایل شی را بارگذاری می کند tracex4_kern.o با استفاده از تابع load_bpf_file.

int main(int ac, char **argv)
{
    struct rlimit r = {RLIM_INFINITY, RLIM_INFINITY};
    char filename[256];
    int i;

    snprintf(filename, sizeof(filename), "%s_kern.o", argv[0]);

    if (setrlimit(RLIMIT_MEMLOCK, &r)) {
        perror("setrlimit(RLIMIT_MEMLOCK, RLIM_INFINITY)");
        return 1;
    }

    if (load_bpf_file(filename)) {
        printf("%s", bpf_log_buf);
        return 1;
    }

    for (i = 0; ; i++) {
        print_old_objects(map_fd[1]);
        sleep(1);
    }

    return 0;
}

درحین انجام load_bpf_file پروب های تعریف شده در فایل eBPF به آن اضافه می شوند /sys/kernel/debug/tracing/kprobe_events. حالا ما به این رویدادها گوش می دهیم و برنامه ما می تواند وقتی اتفاق می افتد کاری انجام دهد.

$ sudo cat /sys/kernel/debug/tracing/kprobe_events
p:kprobes/kmem_cache_free kmem_cache_free
r:kprobes/kmem_cache_alloc_node kmem_cache_alloc_node

تمام برنامه های دیگر در نمونه/bpf/ به طور مشابه ساختار یافته اند. آنها همیشه حاوی دو فایل هستند:

  • XXX_kern.c: برنامه eBPF.
  • XXX_user.c: برنامه اصلی

برنامه eBPF نقشه ها و توابع مرتبط با یک بخش را شناسایی می کند. هنگامی که هسته یک رویداد از نوع خاصی را صادر می کند (به عنوان مثال، tracepoint، توابع محدود اجرا می شوند. کارت ها ارتباط بین برنامه هسته و برنامه فضای کاربر را فراهم می کنند.

نتیجه

این مقاله BPF و eBPF را به طور کلی مورد بحث قرار داد. من می دانم که امروزه اطلاعات و منابع زیادی در مورد eBPF وجود دارد، بنابراین من چند منبع دیگر را برای مطالعه بیشتر توصیه می کنم

خواندن را توصیه می کنم:

منبع: www.habr.com

اضافه کردن نظر