Thu hồi bộ nhớ: loại bỏ và hoán đổi

Trong các phần trước, chúng ta đã thấy cách kernel quản lý bộ nhớ trên toàn hệ thống. Trong chương này, chúng ta sẽ tìm hiểu sâu hơn về Nhóm kiểm soát bộ nhớ (memcg), cơ chế mà Android sử dụng để phân vùng và kiểm soát mức sử dụng bộ nhớ cho từng ứng dụng.

Việc hiểu rõ memcg là rất quan trọng vì đây là cấp độ mà hệ thống có thể nhắm đến các ứng dụng riêng lẻ để thu hồi hoặc đặt giới hạn bộ nhớ cho các ứng dụng đó, cho phép quản lý chủ động dấu vết bộ nhớ của ứng dụng.

Nhóm kiểm soát bộ nhớ (memcg)

Nhóm kiểm soát (cgroup) là một tính năng của kernel Linux cho phép sắp xếp các quy trình thành các nhóm phân cấp và phân phối tài nguyên hệ thống (chẳng hạn như CPU, bộ nhớ và I/O) giữa các nhóm đó. memcg là bộ điều khiển cgroup dành riêng cho bộ nhớ.

Các khái niệm chính về memcg

  • Memcg Charge: Khi một quy trình trong memcg phân bổ một trang bộ nhớ (ẩn danh hoặc được sao lưu bằng tệp), trang đó sẽ được "tính phí" cho memcg. Tổng mức phí của một memcg là tổng của tất cả các trang mà tất cả các quy trình trong đó sử dụng.
  • Kế toán theo hệ thống phân cấp: Mức sử dụng bộ nhớ được tính toán theo hệ thống phân cấp. Mức phí trong một nhóm kiểm soát con cũng được tính vào mức sử dụng của nhóm kiểm soát mẹ.
  • Giới hạn bộ nhớ: Mỗi memcg có thể có các giới hạn (chẳng hạn như memory.max hoặc memory.high) kích hoạt quá trình thu hồi hoặc thậm chí là OOM killer nếu vượt quá, độc lập với bộ nhớ hệ thống chung.
  • Thu hồi theo memcg: Khi một memcg vượt quá giới hạn hoặc khi hệ thống cần bộ nhớ, hạt nhân có thể nhắm đến một memcg cụ thể để thu hồi. Điều này có nghĩa là loại bỏ các trang tệp hoặc hoán đổi các trang ẩn danh sang ZRAM.

Hệ phân cấp memcg của Android

Android sử dụng một hệ thống phân cấp cụ thể để quản lý các quy trình của ứng dụng. Cấu trúc này cho phép hệ thống áp dụng các chính sách khác nhau cho các loại ứng dụng khác nhau (ví dụ: ứng dụng ở nền trước so với ứng dụng ở nền sau).

Hệ phân cấp memcg của Android

  • /sys/fs/cgroup/apps/: Thư mục gốc cho tất cả ứng dụng Android.
  • uid_<UID>/: Thư mục cho Mã nhận dạng người dùng của mỗi ứng dụng. Tất cả các quy trình thuộc cùng một gói ứng dụng đều dùng chung nhóm này.
  • pid_<PID>/: Một thư mục cho từng quy trình riêng lẻ và mọi quy trình con được phân nhánh từ quy trình đó. Điều này cho phép kiểm soát và tính toán chi tiết cho các ứng dụng có nhiều quy trình.

Bài tập thực hành: khám phá memcg

Trong bài tập này, bạn sẽ tìm thấy thư mục memcg cho MemoryLab và quan sát mức sử dụng bộ nhớ của thư mục này theo thời gian thực.

1. Khởi chạy MemoryLab

Đảm bảo MemoryLab đang chạy trên thiết bị của bạn.

2. Tìm memcg của MemoryLab

Trước tiên, hãy lấy PID của ứng dụng MemoryLab đang chạy:

adb shell pidof com.android.memorylab
# Example output: 11672

Bây giờ, hãy tìm thư mục cgroup của ứng dụng. Bạn có thể tìm thấy thông tin này trong /proc/<PID>/cgroup:

adb shell cat /proc/11672/cgroup
# Example output: 0::/apps/uid_10274/pid_11672

Trong cgroup phiên bản 2, đường dẫn sau 0:: biểu thị hệ thống phân cấp memcg tương ứng với /sys/fs/cgroup. Vậy đường dẫn đầy đủ là /sys/fs/cgroup/apps/uid_10274/pid_11672/.

3. Đọc số liệu thống kê memcg

Chuyển đến thư mục đó và xem các tệp khoá:

# Current memory usage (in bytes)
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current
# Example output:
# 1591324672

memory.current cho biết tổng dung lượng bộ nhớ (tính bằng byte) hiện được tính phí cho cgroup này.

# Detailed statistics
adb shell cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.stat | head -n 10
# Example output:
# anon 1585098752
# file 741376
# kernel 5484544
# kernel_stack 393216
# pagetables 4583424
# sec_pagetables 0
# percpu 216
# sock 0
# vmalloc 4096
# shmem 20480

memory.stat cung cấp thông tin chi tiết: * anon: Lượng bộ nhớ ẩn danh (heap, stack). * file: Lượng bộ nhớ được sao lưu bằng tệp (bộ nhớ đệm trang). * swap: Lượng bộ nhớ được hoán đổi ra ZRAM.

4. Quan sát các thay đổi

  1. Mở MemoryLab.
  2. Lưu ý giá trị của memory.current.
  3. Nhấn vào Phân bổ bộ nhớ Java (10 MB) nhiều lần.
  4. Đọc lại memory.current. Bạn sẽ thấy kích thước tăng khoảng 10 MB cho mỗi lần nhấn.
  5. Nhấn vào Allocate Bitmaps (Phân bổ bitmap). Kiểm tra memory.stat để xem anon có tăng lên hay không.

Tính năng thu hồi chủ động bằng memory.reclaim

Một tính năng mạnh mẽ của memcg (v2) là tệp memory.reclaim. Việc ghi một giá trị vào tệp này sẽ hướng dẫn nhân ngay lập tức tìm cách thu hồi lượng bộ nhớ đó từ memcg này và mọi memcg bên dưới.

Cách Android sử dụng memory.reclaim

CachedAppOptimizer của Android sử dụng tính năng này để thu hồi tối đa bộ nhớ từ các ứng dụng sau khi chúng bị đóng băng. App Freezer (Trình đóng băng ứng dụng) của Android đảm bảo rằng các ứng dụng được lưu vào bộ nhớ đệm tiêu thụ ít RAM nhất có thể khi không chạy. Khi một ứng dụng chuyển sang chạy ở chế độ nền và bị đóng băng, hệ thống sẽ ghi mức sử dụng bộ nhớ hiện tại của ứng dụng vào tệp memory.reclaim. Thao tác này buộc kernel loại bỏ tất cả các trang tệp có thể và hoán đổi tất cả các trang ẩn danh sang ZRAM, giảm thiểu dấu vết thường trú của ứng dụng.

Bạn có thể đạt được "mức tối đa có thể thu hồi" theo cách thủ công bằng cách đọc memory.current và ghi lại vào memory.reclaim:

# Force reclaim of everything
adb shell "cat /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.current > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"

Bài tập: buộc thu hồi và theo dõi

Giờ đây, chúng ta sẽ buộc nhân thu hồi bộ nhớ từ MemoryLab và ghi lại hoạt động trong dấu vết Perfetto.

  1. Chuẩn bị: Đảm bảo MemoryLab có một số hoạt động phân bổ (Java và Bitmap).
  2. Bắt đầu theo dõi: Sử dụng cấu hình theo dõi nội tuyến này để ghi lại các sự kiện lập lịch, thu hồi và bộ nhớ đệm trang.

    # Use a config that captures scheduling, reclaim and page cache events
    adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/reclaim.perfetto-trace <<EOF
    buffers: {
        size_kb: 131072
        fill_policy: RING_BUFFER
    }
    data_sources: {
        config {
            name: "linux.ftrace"
            ftrace_config {
                ftrace_events: "sched/sched_switch"
                ftrace_events: "sched/sched_wakeup"
                ftrace_events: "kmem/rss_stat"
                ftrace_events: "mm_filemap_add_to_page_cache"
                ftrace_events: "mm_filemap_delete_from_page_cache"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_direct_reclaim_end"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_begin"
                ftrace_events: "vmscan/mm_vmscan_memcg_reclaim_end"
                symbolize_ksyms: true
            }
        }
    }
    data_sources: {
        config {
            name: "linux.process_stats"
            process_stats_config {
                scan_all_processes_on_start: true
            }
        }
    }
    EOF
    
  3. Áp suất kích hoạt và thu hồi:

    • Trong MemoryLab, hãy nhấn vào Allocate Native Memory (1GB) (Phân bổ bộ nhớ gốc (1 GB)). Điều này sẽ kích hoạt áp lực trên toàn hệ thống.
    • Trong một thiết bị đầu cuối riêng, hãy giải phóng 200 MB:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In (Quay lại lỗi): Chuyển về MemoryLab. Nhấn vào Thrash Pagecache (Kiểm thử lỗi tham chiếu).

  5. Dừng theo dõi: Nhấn Ctrl+C trong thiết bị đầu cuối theo dõi.

Phân tích việc thu hồi trong Perfetto

Khi mở dấu vết, bạn có thể thấy cả hoạt động thu hồi trên toàn hệ thống và theo từng ứng dụng:

Perfetto cho thấy việc thu hồi trực tiếp và memcg

Hãy tìm những thông tin sau trong dấu vết:

  • kswapd: Tìm kswapd0 trong phần Kernel threads (Luồng kernel) (trong ảnh chụp màn hình ở trên, luồng này được ghim thủ công ở trên cùng). Bạn sẽ thấy hệ thống này hoạt động (các lát màu xanh lục) khi hệ thống gặp khó khăn trong việc tìm các trang trống.
  • Direct Reclaim (Thu hồi trực tiếp): Xem các luồng quy trình com.android.memorylab. Bạn sẽ thấy các lát sự kiện ftrace màu tím (chẳng hạn như mm_vmscan_direct_reclaim_begin) xuất hiện ngay bên dưới bản theo dõi lập lịch của luồng. Điều này cho biết chính luồng ứng dụng đang bị tạm dừng để chờ nhân giải phóng các trang.
  • RSS và bộ đếm hoán đổi:
    • mem.rss.anon: Tăng lên khi bạn nhấn vào các nút phân bổ.
    • mem.swap: Tăng đều đặn khi kswapd và các luồng riêng của ứng dụng (chịu sự thu hồi trực tiếp) nén các trang ẩn danh đó vào ZRAM.
  • memcg Reclaim: Nếu phóng to thời điểm bạn kích hoạt tính năng thu hồi thủ công, bạn sẽ thấy cả rss.anonrss.file đều giảm mạnh, kèm theo các sự kiện mm_vmscan_memcg_reclaim.

← Toàn hệ thống | ↑ Lên | Tương tác giữa kswapd và lmkd →