مقدمة موجزة عن BPF و eBPF

يا هبر! نعلمكم أننا نستعد لإصدار كتاب "قابلية مراقبة Linux مع BPF".

مقدمة موجزة عن BPF و eBPF
مع استمرار تطور الجهاز الظاهري BPF واستخدامه بنشاط في الممارسة العملية ، قمنا بترجمة مقال لك يصف ميزاته الرئيسية وحالته الحالية.

في السنوات الأخيرة ، اكتسبت أدوات وتقنيات البرمجة شعبية لتعويض قيود نواة Linux في الحالات التي تتطلب معالجة حزم عالية الأداء. واحدة من أكثر الطرق شيوعًا من هذا النوع تسمى تجاوز الأساسية (kernel bypass) ويسمح ، بتخطي طبقة الشبكة للنواة ، بتنفيذ جميع عمليات معالجة الحزم من مساحة المستخدم. يتضمن تجاوز kernel أيضًا إدارة بطاقة الشبكة من مساحة المستخدم. بمعنى آخر ، عند العمل باستخدام بطاقة الشبكة ، نعتمد على برنامج التشغيل مساحة المستخدم.

من خلال نقل التحكم الكامل في بطاقة الشبكة إلى برنامج مساحة المستخدم ، نقوم بتقليل الحمل الزائد للنواة (مفاتيح السياق ، معالجة طبقة الشبكة ، المقاطعات ، إلخ) ، وهو أمر مهم جدًا عند التشغيل بسرعات 10 جيجابت / ثانية أو أعلى. تجاوز النواة بالإضافة إلى مجموعة من الميزات الأخرى (تجهيز الدفعات) وضبط دقيق للأداء (NUMA المحاسبة, عزل وحدة المعالجة المركزية، وما إلى ذلك) تناسب أساسيات الشبكات عالية الأداء لمساحة المستخدم. ربما يكون المثال النموذجي لهذا النهج الجديد لمعالجة الحزم دي بي دي كيه من إنتل (مجموعة أدوات تطوير مستوى البيانات) ، على الرغم من وجود أدوات وتقنيات أخرى معروفة جيدًا ، بما في ذلك VPP من Cisco (معالجة حزم المتجهات) و Netmap وبالطبع سناب.

تنظيم تفاعلات الشبكة في مساحة المستخدم له عدد من العيوب:

  • نواة نظام التشغيل عبارة عن طبقة تجريدية لموارد الأجهزة. نظرًا لأن برامج مساحة المستخدم يجب أن تدير مواردها بشكل مباشر ، يتعين عليها أيضًا إدارة أجهزتها الخاصة. هذا يعني غالبًا برمجة برامج التشغيل الخاصة بك.
  • نظرًا لأننا نتخلى تمامًا عن مساحة kernel ، فإننا نتخلى أيضًا عن جميع وظائف الشبكات التي توفرها kernel. يتعين على برامج مساحة المستخدم إعادة تنفيذ الميزات التي قد يتم توفيرها بالفعل بواسطة kernel أو نظام التشغيل.
  • تعمل البرامج في وضع الحماية ، مما يحد بشكل خطير من تفاعلها ويمنعها من الاندماج مع أجزاء أخرى من نظام التشغيل.

في الأساس ، عند الربط الشبكي في مساحة المستخدم ، تتحقق مكاسب الأداء عن طريق نقل معالجة الحزم من النواة إلى مساحة المستخدم. يقوم 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 ، أي بقيمة ethertype لـ IPv6. إذا كانت النتيجة صحيحة ، فسيذهب عداد البرنامج إلى التعليمات (002) ، وإذا لم يكن الأمر كذلك ، فينتقل إلى (006).
  • التعليمات (006): تقارن القيمة مع 0x800 (قيمة نوع ethertype لـ IPv4). إذا كانت الإجابة صحيحة ، ينتقل البرنامج إلى (007) ، وإذا لم يكن الأمر كذلك ، فحينئذٍ ينتقل إلى (015).

وهكذا ، حتى يقوم برنامج تصفية الحزم بإرجاع نتيجة. عادة ما يكون منطقيًا. إرجاع قيمة غير صفرية (التعليمات (014)) يعني أن الحزمة متطابقة ، وإرجاع الصفر (التعليمات (015)) يعني أن الحزمة غير متطابقة.

اقترح ستيف ماكان وفان جاكوبسون الآلة الافتراضية BPF ورمزها الثانوي في أواخر عام 1992 عندما صدرت ورقتهما. عامل تصفية حزمة BSD: بنية جديدة لالتقاط الحزمة على مستوى المستخدم، لأول مرة تم تقديم هذه التكنولوجيا في مؤتمر Usenix في شتاء عام 1993.

نظرًا لأن BPF عبارة عن آلة افتراضية ، فهي تحدد البيئة التي تعمل فيها البرامج. بالإضافة إلى bytecode ، فإنه يحدد أيضًا نموذج ذاكرة الحزمة (يتم تطبيق تعليمات التحميل ضمنيًا على الحزمة) ، والسجلات (A و X ؛ سجلات المجمع والفهرس) ، وتخزين الذاكرة المؤقتة ، وعداد البرامج الضمني. ومن المثير للاهتمام ، أن رمز بايت BPF تم تصميمه على غرار Motorola 6502 ISA. كما ذكر ستيف ماكان في كتابه تقرير عام في Sharkfest '11 ، كان على دراية بالبناء 6502 من المدرسة الثانوية عند البرمجة على Apple II ، وقد أثرت هذه المعرفة على عمله في تصميم رمز BPF الثانوي.

يتم تنفيذ دعم BPF في Linux kernel في الإصدار v2.5 والإصدارات الأحدث ، تمت إضافته بشكل أساسي بواسطة Jay Schullist. ظل رمز BPF دون تغيير حتى عام 2011 ، عندما أعاد Eric Dumaset تصميم مترجم BPF للعمل في وضع JIT (المصدر: JIT لمرشحات الحزمة). بعد ذلك ، بدلاً من تفسير رمز BPF الثانوي ، يمكن للنواة تحويل برامج BPF مباشرةً إلى البنية المستهدفة: x86 ، و ARM ، و MIPS ، وما إلى ذلك.

في وقت لاحق ، في عام 2014 ، اقترح Alexei Starovoitov آلية JIT جديدة لـ BPF. في الواقع ، أصبح هذا JIT الجديد بنية جديدة تعتمد على BPF وكان يسمى eBPF. أعتقد أن كلا الجهازين الظاهريين يتعايشان لبعض الوقت ، ولكن يتم تنفيذ تصفية الحزم حاليًا فوق eBPF. في الواقع ، في العديد من أمثلة التوثيق الحديثة ، يشار إلى BPF باسم eBPF ، ويعرف BPF الكلاسيكي اليوم باسم cBPF.

يوسع eBPF الجهاز الافتراضي الكلاسيكي BPF بعدة طرق:

  • يعتمد على معماريات 64 بت الحديثة. يستخدم eBPF مسجلات 64 بت ويزيد عدد السجلات المتاحة من 2 (المجمع و X) إلى 10. يوفر eBPF أيضًا أكواد تشغيل إضافية (BPF_MOV و BPF_JNE و BPF_CALL…).
  • منفصل عن النظام الفرعي لطبقة الشبكة. تم ربط BPF بنموذج البيانات الدفعي. نظرًا لأنه تم استخدامه لتصفية الحزم ، كان رمزه في النظام الفرعي الذي يوفر تفاعلات الشبكة. ومع ذلك ، لم يعد الجهاز الظاهري لـ eBPF مرتبطًا بنموذج بيانات ويمكن استخدامه لأي غرض. لذلك ، يمكن الآن توصيل برنامج eBPF بـ tracepoint أو kprobe. هذا يفتح الباب أمام أدوات eBPF وتحليل الأداء والعديد من حالات الاستخدام الأخرى في سياق أنظمة kernel الفرعية الأخرى. الآن رمز eBPF موجود في مساره الخاص: kernel / bpf.
  • مخازن البيانات العالمية تسمى الخرائط. الخرائط عبارة عن مخازن ذات قيمة أساسية توفر تبادل البيانات بين مساحة المستخدم ومساحة النواة. يوفر eBPF عدة أنواع من البطاقات.
  • وظائف ثانوية. على وجه الخصوص ، للكتابة فوق حزمة أو حساب المجموع الاختباري أو استنساخ حزمة. تعمل هذه الوظائف داخل النواة ولا تنتمي إلى برامج مساحة المستخدم. بالإضافة إلى ذلك ، يمكن إجراء مكالمات النظام من برامج eBPF.
  • إنهاء المكالمات. حجم البرنامج في eBPF محدد بـ 4096 بايت. تسمح ميزة إنهاء المكالمة لبرنامج eBPF بنقل التحكم إلى برنامج eBPF جديد وبالتالي تجاوز هذا القيد (يمكن ربط ما يصل إلى 32 برنامجًا بهذه الطريقة).

مثال على eBPF

هناك عدة أمثلة لـ eBPF في مصادر Linux kernel. كانت متوفرة في عينات / bpf /. لتجميع هذه الأمثلة ، ما عليك سوى كتابة:

$ sudo make samples/bpf/

لن أكتب مثالًا جديدًا لـ eBPF بنفسي ، لكنني سأستخدم إحدى العينات المتوفرة في العينات / bpf /. سوف ألقي نظرة على بعض أجزاء الكود وأشرح كيف يعمل. على سبيل المثال ، اخترت البرنامج tracex4.

بشكل عام ، يتكون كل من الأمثلة في العينات / bpf / من ملفين. في هذه الحالة:

  • tracex4_kern.c، يحتوي على كود مصدر ليتم تنفيذه في النواة كرمز eBPF bytecode.
  • tracex4_user.cيحتوي على برنامج من مساحة المستخدم.

في هذه الحالة ، نحتاج إلى التحويل البرمجي tracex4_kern.c إلى eBPF bytecode. في الوقت الحالي gcc لا يوجد جزء خادم لـ eBPF. لحسن الحظ، clang يمكن أن تنتج eBPF bytecode. 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) ، يتم تنفيذ الوظائف المرتبطة. توفر الخرائط الاتصال بين برنامج kernel وبرنامج مساحة المستخدم.

اختتام

في هذه المقالة ، تمت مناقشة BPF و eBPF بعبارات عامة. أعلم أن هناك الكثير من المعلومات والموارد حول eBPF اليوم ، لذلك سأوصي ببعض المواد الإضافية لمزيد من الدراسة.

أوصي القراءة:

المصدر: www.habr.com

إضافة تعليق