บทนำโดยย่อเกี่ยวกับ BPF และ eBPF

สวัสดีฮับ! เราขอแจ้งให้ทราบว่าเรากำลังเตรียมหนังสือออกจำหน่าย"ความสามารถในการสังเกต Linux ด้วย BPF".

บทนำโดยย่อเกี่ยวกับ BPF และ eBPF
เนื่องจากเครื่องเสมือน BPF มีการพัฒนาอย่างต่อเนื่องและมีการใช้งานในทางปฏิบัติ เราจึงได้แปลบทความที่อธิบายความสามารถหลักและสถานะปัจจุบันให้กับคุณ

ในช่วงไม่กี่ปีที่ผ่านมา เครื่องมือและเทคนิคการเขียนโปรแกรมได้รับความนิยมมากขึ้นเพื่อชดเชยข้อจำกัดของเคอร์เนล Linux ในกรณีที่จำเป็นต้องมีการประมวลผลแพ็กเก็ตประสิทธิภาพสูง หนึ่งในเทคนิคยอดนิยมประเภทนี้เรียกว่า บายพาสเคอร์เนล (เคอร์เนลบายพาส) และอนุญาตให้ทำการบายพาสเลเยอร์เครือข่ายเคอร์เนล เพื่อดำเนินการประมวลผลแพ็กเก็ตทั้งหมดจากพื้นที่ผู้ใช้ การข้ามเคอร์เนลยังเกี่ยวข้องกับการควบคุมการ์ดเครือข่ายด้วย พื้นที่ผู้ใช้. กล่าวอีกนัยหนึ่ง เมื่อทำงานกับการ์ดเครือข่าย เราต้องอาศัยไดรเวอร์ พื้นที่ผู้ใช้.

ด้วยการโอนการควบคุมการ์ดเครือข่ายเต็มรูปแบบไปยังโปรแกรมพื้นที่ผู้ใช้ เราจะลดโอเวอร์เฮดของเคอร์เนล (การสลับบริบท การประมวลผลเลเยอร์เครือข่าย การขัดจังหวะ ฯลฯ) ซึ่งค่อนข้างสำคัญเมื่อทำงานที่ความเร็ว 10Gb/s หรือสูงกว่า บายพาสเคอร์เนลพร้อมการผสมผสานคุณสมบัติอื่นๆ (การประมวลผลเป็นชุด) และการปรับประสิทธิภาพอย่างระมัดระวัง (การบัญชี NUMA, การแยกซีพียูฯลฯ) สอดคล้องกับพื้นฐานของการประมวลผลเครือข่ายประสิทธิภาพสูงในพื้นที่ผู้ใช้ บางทีตัวอย่างที่เป็นแบบอย่างของแนวทางใหม่ในการประมวลผลแพ็กเก็ตก็คือ ปปส จากอินเทล (ชุดพัฒนาระนาบข้อมูล) แม้ว่าจะมีเครื่องมือและเทคนิคที่รู้จักกันดีอื่นๆ รวมถึง VPP (Vector Packet Processing) ของ Cisco, Netmap และแน่นอนว่า สแน็บ.

การจัดการโต้ตอบเครือข่ายในพื้นที่ผู้ใช้มีข้อเสียหลายประการ:

  • เคอร์เนล OS เป็นเลเยอร์นามธรรมสำหรับทรัพยากรฮาร์ดแวร์ เนื่องจากโปรแกรมพื้นที่ผู้ใช้ต้องจัดการทรัพยากรโดยตรง พวกเขาจึงต้องจัดการฮาร์ดแวร์ของตนเองด้วย ซึ่งมักหมายถึงต้องตั้งโปรแกรมไดรเวอร์ของคุณเอง
  • เนื่องจากเราสละพื้นที่เคอร์เนลโดยสิ้นเชิง เราจึงละทิ้งฟังก์ชันการทำงานของเครือข่ายทั้งหมดที่เคอร์เนลมีให้ด้วย โปรแกรมพื้นที่ผู้ใช้จะต้องปรับใช้คุณสมบัติที่เคอร์เนลหรือระบบปฏิบัติการอาจมีให้อยู่แล้ว
  • โปรแกรมทำงานในโหมดแซนด์บ็อกซ์ ซึ่งจะจำกัดการโต้ตอบอย่างจริงจังและป้องกันไม่ให้รวมเข้ากับส่วนอื่น ๆ ของระบบปฏิบัติการ

โดยพื้นฐานแล้ว เมื่อสร้างเครือข่ายในพื้นที่ผู้ใช้ ประสิทธิภาพจะเพิ่มขึ้นโดยการย้ายการประมวลผลแพ็กเก็ตจากเคอร์เนลไปยังพื้นที่ผู้ใช้ XDP ทำสิ่งที่ตรงกันข้ามโดยสิ้นเชิง: โดยจะย้ายโปรแกรมเครือข่ายจากพื้นที่ผู้ใช้ (ตัวกรอง ตัวแก้ไข การกำหนดเส้นทาง ฯลฯ) ไปยังพื้นที่เคอร์เนล XDP ช่วยให้เราสามารถดำเนินการฟังก์ชันเครือข่ายได้ทันทีที่แพ็กเก็ตกระทบอินเทอร์เฟซเครือข่าย และก่อนที่มันจะเริ่มเคลื่อนเข้าสู่ระบบย่อยเครือข่ายเคอร์เนล เป็นผลให้ความเร็วในการประมวลผลแพ็กเก็ตเพิ่มขึ้นอย่างมาก อย่างไรก็ตาม เคอร์เนลอนุญาตให้ผู้ใช้รันโปรแกรมในพื้นที่เคอร์เนลได้อย่างไร? ก่อนที่จะตอบคำถามนี้ เรามาดูกันว่า BPF คืออะไร

BPF และ eBPF

แม้จะมีชื่อที่น่าสับสน แต่จริงๆ แล้ว BPF (Berkeley Packet Filtering) ก็คือโมเดลเครื่องเสมือน เครื่องเสมือนนี้เดิมได้รับการออกแบบมาเพื่อจัดการการกรองแพ็กเก็ต จึงเป็นที่มาของชื่อ

หนึ่งในเครื่องมือที่มีชื่อเสียงที่สุดที่ใช้ 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 บิต ลงในตัวสะสม Offset 12 สอดคล้องกับ ethertype ของแพ็กเก็ต
  • คำสั่ง (001): เปรียบเทียบค่าในตัวสะสมกับ 0x86dd นั่นคือกับค่าอีเธอร์ไทป์สำหรับ IPv6 หากผลลัพธ์เป็นจริง ตัวนับโปรแกรมจะไปที่คำสั่ง (002) และถ้าไม่เป็นเช่นนั้น ให้ไปที่ (006)
  • คำสั่ง (006): เปรียบเทียบค่ากับ 0x800 (ค่า ethertype สำหรับ IPv4) หากคำตอบเป็นจริง โปรแกรมจะไปที่ (007) หากไม่ใช่ ให้ไปที่ (015)

และต่อๆ ไปจนกว่าโปรแกรมกรองแพ็กเก็ตจะส่งคืนผลลัพธ์ โดยปกติจะเป็นบูลีน การส่งคืนค่าที่ไม่ใช่ศูนย์ (คำสั่ง (014)) หมายความว่าแพ็กเก็ตได้รับการยอมรับ และการส่งคืนค่าศูนย์ (คำสั่ง (015)) หมายความว่าแพ็กเก็ตไม่ได้รับการยอมรับ

เครื่องเสมือน BPF และรหัสไบต์ถูกเสนอโดย Steve McCann และ Van Jacobson ในปลายปี 1992 เมื่อมีการตีพิมพ์บทความของพวกเขา ตัวกรองแพ็คเก็ต BSD: สถาปัตยกรรมใหม่สำหรับการจับแพ็คเก็ตระดับผู้ใช้เทคโนโลยีนี้ถูกนำเสนอครั้งแรกในการประชุม Usenix ในช่วงฤดูหนาวปี 1993

เนื่องจาก BPF เป็นเครื่องเสมือน จึงกำหนดสภาพแวดล้อมที่โปรแกรมทำงาน นอกเหนือจากโค้ดไบต์แล้ว มันยังกำหนดโมเดลหน่วยความจำแบตช์อีกด้วย (คำสั่งการโหลดจะถูกนำไปใช้กับแบตช์โดยปริยาย), รีจิสเตอร์ (A และ X; รีจิสเตอร์ตัวสะสมและดัชนี), พื้นที่จัดเก็บหน่วยความจำเริ่มต้น และตัวนับโปรแกรมโดยนัย สิ่งที่น่าสนใจคือ BPF bytecode นั้นมีต้นแบบมาจาก Motorola 6502 ISA ดังที่ Steve McCann จำได้ในตัวเขา รายงานการประชุมใหญ่ ที่ Sharkfest '11 เขาคุ้นเคยกับ build 6502 จากการเขียนโปรแกรมสมัยมัธยมบน Apple II และความรู้นี้มีอิทธิพลต่องานของเขาในการออกแบบไบต์ BPF

การสนับสนุน BPF ถูกนำมาใช้ในเคอร์เนล Linux ในเวอร์ชัน v2.5 และสูงกว่า โดยเพิ่มโดยความพยายามของ Jay Schullist เป็นหลัก รหัส BPF ยังคงไม่เปลี่ยนแปลงจนถึงปี 2011 เมื่อ Eric Dumaset ออกแบบล่าม BPF ใหม่เพื่อให้ทำงานในโหมด JIT (ที่มา: JIT สำหรับตัวกรองแพ็คเก็ต). หลังจากนี้ เคอร์เนลแทนที่จะตีความรหัสไบต์ BPF สามารถแปลงโปรแกรม BPF เป็นสถาปัตยกรรมเป้าหมายได้โดยตรง: x86, ARM, MIPS ฯลฯ

ต่อมาในปี 2014 Alexey Starovoitov เสนอกลไก JIT ใหม่สำหรับ BPF ในความเป็นจริง JIT ใหม่นี้กลายเป็นสถาปัตยกรรมที่ใช้ BPF ใหม่และถูกเรียกว่า eBPF ฉันคิดว่า VM ทั้งสองอยู่ร่วมกันมาระยะหนึ่งแล้ว แต่ในปัจจุบันการกรองแพ็กเก็ตมีการใช้งานโดยยึดตาม eBPF ในความเป็นจริง ในหลายตัวอย่างของเอกสารสมัยใหม่ BPF เข้าใจว่าเป็น eBPF และ BPF แบบคลาสสิกในปัจจุบันเรียกว่า cBPF

eBPF ขยายเครื่องเสมือน BPF แบบคลาสสิกได้หลายวิธี:

  • ขึ้นอยู่กับสถาปัตยกรรม 64 บิตที่ทันสมัย eBPF ใช้รีจิสเตอร์ 64 บิต และเพิ่มจำนวนรีจิสเตอร์ที่มีอยู่จาก 2 (ตัวสะสมและ X) เป็น 10 นอกจากนี้ eBPF ยังมี opcodes เพิ่มเติม (BPF_MOV, BPF_JNE, BPF_CALL...)
  • แยกออกจากระบบย่อยเลเยอร์เครือข่าย BPF เชื่อมโยงกับแบบจำลองข้อมูลแบทช์ เนื่องจากใช้สำหรับการกรองแพ็กเก็ต รหัสจึงอยู่ในระบบย่อยที่ให้การสื่อสารผ่านเครือข่าย อย่างไรก็ตาม เครื่องเสมือน eBPF จะไม่เชื่อมโยงกับโมเดลข้อมูลอีกต่อไป และสามารถใช้เพื่อวัตถุประสงค์ใดก็ได้ ตอนนี้โปรแกรม eBPF สามารถเชื่อมต่อกับจุดติดตามหรือ kprobe ได้แล้ว นี่เป็นการเปิดทางไปสู่การใช้เครื่องมือ eBPF การวิเคราะห์ประสิทธิภาพ และกรณีการใช้งานอื่นๆ อีกมากมายในบริบทของระบบย่อยเคอร์เนลอื่นๆ ตอนนี้โค้ด eBPF อยู่ในเส้นทางของตัวเอง: kernel/bpf
  • ที่เก็บข้อมูลทั่วโลกที่เรียกว่า Maps แผนที่เป็นที่เก็บคีย์-ค่าที่ช่วยให้สามารถแลกเปลี่ยนข้อมูลระหว่างพื้นที่ผู้ใช้และพื้นที่เคอร์เนล eBPF มีแผนที่หลายประเภท
  • ฟังก์ชั่นรอง โดยเฉพาะอย่างยิ่ง หากต้องการเขียนแพ็คเกจใหม่ คำนวณเช็คซัม หรือโคลนแพ็คเกจ ฟังก์ชันเหล่านี้ทำงานภายในเคอร์เนลและไม่ใช่โปรแกรมพื้นที่ผู้ใช้ คุณยังสามารถทำการเรียกระบบจากโปรแกรม eBPF ได้อีกด้วย
  • วางสาย ขนาดโปรแกรมใน eBPF จำกัดอยู่ที่ 4096 ไบต์ คุณลักษณะการเรียกส่วนท้ายช่วยให้โปรแกรม eBPF สามารถถ่ายโอนการควบคุมไปยังโปรแกรม eBPF ใหม่ และหลีกเลี่ยงข้อจำกัดนี้ (สามารถเชื่อมโยงโปรแกรมได้สูงสุด 32 โปรแกรมด้วยวิธีนี้)

eBPF: ตัวอย่าง

มีตัวอย่างหลายตัวอย่างสำหรับ eBPF ในแหล่งที่มาของเคอร์เนล Linux มีจำหน่ายที่ Sample/bpf/ หากต้องการรวบรวมตัวอย่างเหล่านี้ เพียงป้อน:

$ sudo make samples/bpf/

ฉันจะไม่เขียนตัวอย่างใหม่สำหรับ eBPF ด้วยตัวเอง แต่จะใช้หนึ่งในตัวอย่างที่มีอยู่ใน Samples/bpf/ ฉันจะดูโค้ดบางส่วนและอธิบายวิธีการทำงาน ตัวอย่าง ผมเลือกโปรแกรม tracex4.

โดยทั่วไป แต่ละตัวอย่างใน Sample/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

โปรแกรมอื่นๆ ทั้งหมดใน Sample/bpf/ มีโครงสร้างคล้ายกัน ประกอบด้วยสองไฟล์เสมอ:

  • XXX_kern.c: โปรแกรม eBPF
  • XXX_user.c: โปรแกรมหลัก

โปรแกรม eBPF ระบุแผนที่และฟังก์ชันที่เกี่ยวข้องกับส่วนต่างๆ เมื่อเคอร์เนลออกเหตุการณ์บางประเภท (เช่น tracepoint) ฟังก์ชันที่ถูกผูกไว้จะถูกดำเนินการ การ์ดจัดให้มีการสื่อสารระหว่างโปรแกรมเคอร์เนลและโปรแกรมพื้นที่ผู้ใช้

ข้อสรุป

บทความนี้กล่าวถึง BPF และ eBPF ในแง่ทั่วไป ฉันรู้ว่ามีข้อมูลและแหล่งข้อมูลมากมายเกี่ยวกับ eBPF ในปัจจุบัน ดังนั้นฉันจะแนะนำแหล่งข้อมูลเพิ่มเติมสองสามรายการสำหรับการศึกษาต่อไป

ฉันแนะนำให้อ่าน:

ที่มา: will.com

เพิ่มความคิดเห็น