Phân tích Kỹ thuật: Debug Memory Leak trong Linux Kernel (Case Study)

Bài viết này phân tích một tình huống debug memory leak thực tế trong kernel, minh họa các kỹ thuật được sử dụng và quy trình xác định nguyên nhân gốc rễ.

1. Bối cảnh: Memory Leak ở User-Space vs. Kernel-Space

Trước tiên, cần phân biệt rõ hai loại memory leak:

  • Memory Leak ở User-Space: Đây là kết quả của việc một tiến trình (process) cấp phát bộ nhớ (ví dụ: qua malloc) nhưng quên không giải phóng. Tuy nhiên, loại leak này chỉ tồn tại trong vòng đời của tiến trình đó. Khi tiến trình kết thúc (exit), hệ điều hành (OS) sẽ tự động thu hồi và giải phóng toàn bộ những vùng nhớ mà tiến trình quên chưa giải phóng.
  • Memory Leak ở Kernel-Space: Đây là lỗi rò rỉ xảy ra bên trong chính nhân hệ điều hành. Do kernel chỉ “kết thúc” khi hệ thống tắt nguồn hoặc khởi động lại, nên nếu một vùng nhớ bị leak dưới kernel, nó sẽ tồn tại vĩnh viễn trong suốt thời gian hệ thống hoạt động, dần dần làm cạn kiệt tài nguyên.

2. Kỹ thuật Debug Memory Leak trong Kernel

Việc debug memory leak dưới kernel khác biệt so với user-space, do kernel sử dụng một bộ cấp phát bộ nhớ (Slab/Slub/Slub allocator) riêng. Rất may, kernel hỗ trợ nhiều tính năng mạnh mẽ để debug chính nó, nổi bật là SLUB_DEBUG.

Quy trình debug cơ bản thường bao gồm các bước:

  1. Kích hoạt SLUB_DEBUG: Tính năng này cần được bật (enable) khi cấu hình và build kernel image.
  2. Tái lập lỗi: Load kernel image mới vào thiết bị và tiến hành chạy các bài test để gây ra memory leak.
  3. Phân tích slabinfo: Đọc file /proc/slabinfo để lấy thông tin thống kê về các đối tượng (object) đang được cấp phát động. Chúng ta cần tìm kiếm các đối tượng có số lượng tăng cao một cách bất thường. Trong ảnh chụp slabinfo, cột ngoài cùng bên trái là tên đối tượng (slab cache), và cột thứ hai là số lượng đối tượng đang hoạt động (active).
  4. Truy vết (Trace) Allocator: Ghi ‘1’ vào file /sys/kernel/slab/<tên-đối-tượng>/alloc_calls (nếu kernel hỗ trợ) để lấy ra tên của các hàm đã gọi cấp phát đối tượng đó.
3. Phân tích Tình huống Thực tế (Case Study)

Dưới đây là một quy trình debug một lỗi memory leak thực tế:

A. Hiện tượng (Symptom)

Sau khi áp dụng kỹ thuật debug ở trên, hệ thống cho thấy một đối tượng tên là kmalloc-8192 được cấp phát liên tục, chiếm dụng lên đến 2GB bộ nhớ.

B. Phân tích Ban đầu
  • Thử nghiệm 1: Cố gắng ghi ‘1’ vào file /sys/kernel/slab/kmalloc-8192. Thao tác này thất bại vì kmalloc-8192 là một thư mục (chứa các slab con cho từng CPU node), không phải là một file.
  • Phân tích báo cáo lỗi: Báo cáo từ phía khách hàng (QA) cho thấy lỗi này chỉ xảy ra khi ghi dữ liệu nhiều lần vào port UART của thiết bị.
  • Thử nghiệm 2 (Review code): Kiểm tra lại code của driver UART, phát hiện có một số chỗ cấp phát bộ nhớ (ví dụ: cho DMA), nhưng không tìm thấy mối liên hệ rõ ràng ngay lập tức với đối tượng 8192 bytes.

Mấu chốt tại thời điểm này là phải tìm ra được hàm nào trong kernel đang liên tục gọi cấp phát kmalloc(8192).

C. Kỹ thuật Debug Tùy chỉnh (Custom Tracing)

Do các công cụ tiêu chuẩn chưa chỉ ra được thủ phạm, một phương pháp debug tùy chỉnh đã được áp dụng:

  1. Sửa code Kernel: Chỉnh sửa trực tiếp hàm kmalloc (hoặc hàm cấp phát nội bộ mà nó gọi).
  2. Thêm điều kiện: Thêm logic để kiểm tra if (size == 8192).
  3. In Call Trace: Nếu điều kiện đúng, gọi dump_stack() hoặc một hàm tương đương để in ra “call trace” (danh sách các hàm đã gọi) dẫn đến việc cấp phát này.
  4. Thêm Toggle: Để tránh làm ngập log hệ thống, một biến toggle (có thể bật/tắt qua /proc hoặc debugfs) được thêm vào. Chỉ khi toggle này được bật, hàm kmalloc mới in log ra.
D. Phát hiện và Kết luận

Sau khi build lại kernel với bản vá debug và chạy lại bài test:

  • Khi bật toggle, log hệ thống cho thấy hàm inode_create được gọi ra liên tục, và chính hàm này (hoặc các hàm con của nó) đã gọi kmalloc(8192).
  • Phân tích sâu hơn cho thấy inode_create là hàm được hệ thống gọi khi một file được mở (open).
  • Kiểm tra lại script test của khách hàng: Họ đang dùng lệnh echo "data" > /dev/ttyS_ trong một vòng lặp. Câu lệnh echo (với toán tử >) sẽ thực hiện mở file (open) cho mỗi lần nó được gọi.
  • Kết luận: Vấn đề được khoanh vùng nằm trong hàm open của driver port UART.

Nhìn lại code của hàm open() trong driver UART, đã phát hiện chính xác một hành vi cấp phát bộ nhớ nhưng không có lời gọi free() tương ứng trong hàm release() (hàm được gọi khi file bị đóng).

4. Tổng kết

Các lỗi rò rỉ bộ nhớ trong kernel, mặc dù nghiêm trọng, nhưng thường có thể được tìm ra một cách có hệ thống. Kinh nghiệm trong việc sử dụng các tính năng hỗ trợ debug của kernel (như SLUB_DEBUG, slabinfo) và khả năng áp dụng các kỹ thuật debug tùy chỉnh (như thêm trace có điều kiện) là chìa khóa để xác định nhanh chóng nguyên nhân của lỗi.

 

 

 

 

 

 

 

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top