0
0
Lập trình
TT

So Sánh PKTAP trên macOS và eBPF trên Linux cho Quá Trình Mạng

Đăng vào 6 tháng trước

• 7 phút đọc

Tóm tắt nhanh

PKTAP trên macOS cung cấp thông tin về quá trình ngay trong tiêu đề gói (chỉ 10 dòng mã), trong khi Linux yêu cầu sử dụng các chương trình eBPF để kết nối vào các hàm của kernel (hơn 100 dòng mã). Cả hai đều giải quyết vấn đề xác định quá trình nào sở hữu các gói mạng nhưng với mức độ phức tạp rất khác nhau.

📚 Đây là Phần 1 trong chuỗi bài viết "Xây Dựng Giám Sát Mạng".
Phần tiếp theo: Triển Khai Phát Hiện Quá Trình trong RustNet: Mã Nguồn

Mục lục

  • Thách thức
  • macOS: Phương pháp PKTAP
  • Linux: Lộ trình eBPF mạnh mẽ nhưng phức tạp
  • Những yếu tố đánh đổi
  • Ghi chú triển khai

Thách thức

Các phương pháp truyền thống như polling /proc/net/* trên Linux hoặc chạy lsof trong một vòng lặp trên macOS hoạt động tốt với các kết nối lâu dài, nhưng gặp khó khăn với các quá trình ngắn hạn. Đến khi bạn polling, quá trình có thể đã biến mất, để lại các kết nối mồ côi mà nguồn gốc vẫn là một bí ẩn. Ví dụ, khi chạy curl.

Trong quá trình làm việc để thêm khả năng nhận diện quá trình vào công cụ giám sát mạng RustNet, tôi đã phát hiện những khác biệt thú vị trong cách macOS và Linux giải quyết thách thức này.

Dòng dữ liệu

macOS PKTAP:

Copy
Gói → Kernel → [+Thông tin quá trình] → Tiêu đề PKTAP → Ứng dụng của bạn
         ↑
         Tự động!

Linux eBPF:

Copy
Gói → Hàm Kernel → Hook eBPF → Bản đồ → Không gian người dùng
           ↑                    ↑        ↑
      tcp_connect()     Bạn viết điều này   Bạn polling điều này
      udp_sendmsg()
      (và 10 cái khác...)

macOS: Phương pháp PKTAP

macOS cung cấp PKTAP (Packet Tap), nơi kernel tự động bao gồm thông tin quá trình trong tiêu đề gói. Điều này làm cho việc triển khai rất đơn giản:

c Copy
// Từ darwin-xnu của Apple (bsd/net/pktap.h)
struct pktap_header {
    // ... các trường khác
    pid_t pth_pid;        // ID quá trình
    char pth_comm[17];    // Tên quá trình (MAXCOMLEN + 1)
    pid_t pth_epid;       // ID quá trình hiệu lực
    char pth_ecomm[17];   // Tên lệnh hiệu lực
    // ... các trường khác
};

Bạn chỉ cần đọc các gói và thông tin quá trình ngay ở đó trong tiêu đề. Kernel xử lý tất cả các công việc nặng nhọc của việc ánh xạ các gói tới các quá trình. Muốn biết quá trình nào gửi một gói? Chỉ cần phân tích tiêu đề:

rust Copy
pub fn get_process_info(&self) -> (Option<String>, Option<u32>) {
    let process_name = extract_process_name_from_bytes(&self.pth_comm);
    let pid = if self.pth_epid != 0 {
        Some(self.pth_epid as u32)
    } else {
        None
    };
    (process_name, pid)
}

Đó là tất cả. Rõ ràng, đơn giản và hoạt động cho hầu hết các gói. Thú vị là, một số loại gói (như ICMP và ARP) không phải lúc nào cũng bao gồm thông tin quá trình—có thể vì chúng được xử lý khác nhau bởi kernel hoặc thiếu ngữ cảnh quá trình rõ ràng.

Linux: Lộ trình eBPF mạnh mẽ nhưng phức tạp

Linux không có tương đương với PKTAP, vì vậy một giải pháp bao gồm việc sử dụng các chương trình eBPF để kết nối vào các hàm mạng của kernel:

c Copy
SEC("kprobe/tcp_connect")
int trace_tcp_connect(struct pt_regs *ctx) {
    struct sock *sk = (struct sock *)PT_REGS_PARM1_CORE(ctx);

    // Trích xuất thông tin mạng từ socket
    key.saddr[0] = BPF_CORE_READ(sk, __sk_common.skc_rcv_saddr);
    key.daddr[0] = BPF_CORE_READ(sk, __sk_common.skc_daddr);

    // Lấy thông tin quá trình
    info.pid = bpf_get_current_pid_tgid() >> 32;
    bpf_get_current_comm(&info.comm, sizeof(info.comm));

    // Lưu vào bản đồ để người dùng truy cập
    bpf_map_update_elem(&socket_map, &key, &info, BPF_ANY);
    return 0;
}

Nhưng đây là nơi mọi thứ trở nên thú vị (và phức tạp):

  • Bạn cần các kprobes riêng biệt cho tcp_connect, inet_csk_accept, udp_sendmsg, tcp_v6_connect, v.v.
  • Trường comm bị giới hạn ở 16 ký tự—vì vậy "Firefox" trở thành "Socket Thread"
  • Bạn phải hiểu các chi tiết bên trong của kernel—cấu trúc socket, CO-RE relocations, BTF
  • Độ phức tạp xây dựng: yêu cầu libelf, clang, LLVM và các tiêu đề của kernel

Những yếu tố đánh đổi

Ưu điểm của PKTAP trên macOS:

  • API cực kỳ đơn giản
  • Hoạt động ngay lập tức
  • Tên quá trình đầy đủ (khi có)
  • Không yêu cầu lập trình kernel
  • Liên kết tự động giữa quá trình và gói cho hầu hết lưu lượng

Nhược điểm của PKTAP trên macOS:

  • Chỉ trên macOS (có thể là các BSD khác)
  • Cần thiết lập giao diện đặc biệt
  • Giới hạn vào những gì Apple cung cấp
  • Một số loại gói (ICMP, ARP) có thể thiếu thông tin quá trình

Ưu điểm của eBPF trên Linux:

  • Mạnh mẽ và linh hoạt đến mức đáng kinh ngạc
  • Có thể kết nối vào hầu như bất kỳ hàm kernel nào
  • Tốn ít tài nguyên hơn so với polling
  • Hoạt động trên hầu hết các kernel hiện đại

Nhược điểm của eBPF trên Linux:

  • Đường cong học tập dốc
  • Yêu cầu xây dựng phức tạp
  • Giới hạn tên quá trình 16 ký tự (trường comm)
  • Phải xử lý sự khác biệt giữa các phiên bản kernel
  • Nhiều bộ phận di động hơn

So sánh nhanh

Khía cạnh PKTAP macOS eBPF Linux
Số dòng mã ~10 ~100+
Độ phức tạp thiết lập Không Yêu cầu tiêu đề kernel, LLVM, libelf
Giới hạn tên quá trình 17 ký tự 16 ký tự
Lập trình kernel Không
Đường cong học tập Phút Ngày/ tuần
Tính khả dụng Chỉ trên macOS Linux 4.x+

Ghi chú triển khai

Đối với RustNet, tôi đã quyết định sử dụng libbpf thay vì framework aya của Rust, đặc biệt để tránh phụ thuộc vào công cụ rust nightly. Trong khi aya cung cấp nhiều tính năng idiomatic của Rust hơn, sự ổn định và khả năng tương thích rộng rãi của libbpf đã biến nó thành sự lựa chọn tốt hơn cho dự án này.

Sự tương phản thực sự làm nổi bật các triết lý thiết kế hệ điều hành khác nhau: macOS cung cấp các API cấp cao, được thiết kế cho mục đích cụ thể trong khi Linux cung cấp các nguyên lý cấp thấp có thể được kết hợp thành các giải pháp mạnh mẽ, mặc dù với độ phức tạp cao hơn nhiều.

Cả hai phương pháp đều giải quyết cùng một vấn đề một cách hiệu quả, nhưng trải nghiệm của lập trình viên rất khác nhau. Tôi tự hỏi liệu Linux có thể hưởng lợi từ các API mạng cấp cao hơn như PKTAP hay không, mặc dù có thể điều đó trái ngược với triết lý Unix về các công cụ có thể kết hợp.

Lưu ý về trường comm: Giới hạn 16 ký tự là một ràng buộc của kernel nơi tên luồng bị cắt ngắn. Firefox xuất hiện dưới dạng "Socket Thread", Chrome là "ThreadPoolForeg", v.v. Bạn có thể giải quyết vấn đề này bằng cách kết hợp eBPF với việc tìm kiếm procfs có chọn lọc, nhưng điều đó làm mất đi một số lợi ích về hiệu suất.


💭 Các điểm thảo luận

  • Bạn đã làm việc với PKTAP hoặc eBPF chưa? Kinh nghiệm của bạn như thế nào?
  • Liệu sự phức tạp của Linux có được biện minh bởi tính linh hoạt mà nó cung cấp không?
  • Linux có nên thêm một API đơn giản hơn như PKTAP cho các trường hợp sử dụng phổ biến không?
  • Có nhà phát triển Windows nào ở đây không? Windows xử lý vấn đề này như thế nào?
  • Bạn đã phát hiện các API mạng cụ thể cho hệ điều hành nào khác không?

🔗 Xem nó hoạt động

Triển khai này là một phần của RustNet, một công cụ giám sát mạng đa nền tảng nơi bạn có thể thấy cả hai phương pháp đang hoạt động. Triển khai eBPF có sẵn trong phiên bản v0.9.0 như một tính năng thử nghiệm (--features=ebpf).

Nếu bạn quan tâm đến việc giám sát mạng, kiểm tra gói, hoặc chỉ muốn xem cách các hệ điều hành khác nhau xử lý cùng một vấn đề, hãy kiểm tra dự án. Tôi luôn tìm kiếm phản hồi và sự đóng góp!


Bài viết này được xuất bản lần đầu trên blog của tôi. Theo dõi tôi để cập nhật thêm nhiều bài viết về lập trình hệ thống, Rust và những điều thú vị về các hệ điều hành khác nhau.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào