การเรียกคืนหน่วยความจำ: การนำออกและการสลับ

ในส่วนก่อนหน้า เราได้ดูวิธีที่เคอร์เนลจัดการหน่วยความจำทั้งระบบ ในบทนี้ เราจะเจาะลึกเรื่องกลุ่มควบคุมหน่วยความจำ (memcg) ซึ่งเป็นกลไก ที่ Android ใช้เพื่อแบ่งพาร์ติชันและควบคุมการใช้งานหน่วยความจำของแต่ละแอป

การทำความเข้าใจ memcg มีความสำคัญอย่างยิ่งเนื่องจากเป็นระดับที่ระบบสามารถกำหนดเป้าหมายแอปแต่ละแอปเพื่อเรียกคืนหน่วยความจำหรือตั้งค่าขีดจำกัดหน่วยความจำสำหรับแอปเหล่านั้นได้ ซึ่งช่วยให้จัดการร่องรอยหน่วยความจำของแอปได้อย่างเชิงรุก

กลุ่มควบคุมหน่วยความจำ (memcg)

กลุ่มควบคุม (cgroups) เป็นฟีเจอร์ของเคอร์เนล Linux ที่ช่วยจัดระเบียบกระบวนการเป็นกลุ่มตามลำดับชั้นและกระจายทรัพยากรระบบ (เช่น CPU, หน่วยความจำ และ I/O) ระหว่างกลุ่มเหล่านั้น memcg เป็นตัวควบคุม cgroup สำหรับหน่วยความจำโดยเฉพาะ

แนวคิดหลักของ memcg

  • การเรียกเก็บเงิน memcg: เมื่อกระบวนการใน memcg จัดสรรหน่วยความจำ 1 หน้า (ทั้งแบบไม่ระบุตัวตนหรือแบบมีไฟล์สำรอง) ระบบจะ "เรียกเก็บเงิน" จาก memcg การเรียกเก็บเงินทั้งหมดของ memcg คือผลรวมของหน้าทั้งหมดที่กระบวนการทั้งหมดภายใน memcg ใช้
  • การบัญชีตามลำดับชั้น: ระบบจะบันทึกการใช้งานหน่วยความจำตามลำดับชั้น การเรียกเก็บเงินใน cgroup ย่อยจะนับรวมกับการใช้งานของ cgroup หลักด้วย
  • ขีดจำกัดหน่วยความจำ: memcg แต่ละรายการสามารถมีขีดจำกัด (เช่น memory.max หรือ memory.high) ที่ทริกเกอร์การเรียกคืนหน่วยความจำหรือแม้แต่ OOM Killer หากเกินขีดจำกัด โดยไม่ขึ้นอยู่กับหน่วยความจำระบบส่วนกลาง
  • การเรียกคืนหน่วยความจำต่อ memcg: เมื่อ memcg เกินขีดจำกัดหรือเมื่อระบบ ต้องการหน่วยความจำ เคอร์เนลสามารถกำหนดเป้าหมาย memcg ที่เฉพาะเจาะจงเพื่อเรียกคืนหน่วยความจำได้ ซึ่งหมายถึงการนำหน้าไฟล์ออกหรือสลับหน้าแบบไม่ระบุตัวตนไปยัง ZRAM

ลำดับชั้น memcg ของ Android

Android ใช้ลำดับชั้นที่เฉพาะเจาะจงเพื่อจัดการกระบวนการของแอป โครงสร้างนี้ช่วยให้ระบบใช้นโยบายที่แตกต่างกันกับแอปประเภทต่างๆ ได้ (เช่น แอปที่ทำงานอยู่เบื้องหน้าเทียบกับแอปที่ทำงานอยู่เบื้องหลัง)

ลำดับชั้น memcg ของ Android

  • /sys/fs/cgroup/apps/: รูทสำหรับแอปพลิเคชัน Android ทั้งหมด
  • uid_<UID>/: ไดเรกทอรีสำหรับรหัสผู้ใช้ของแต่ละแอป กระบวนการทั้งหมดที่เป็นของแพ็กเกจแอปเดียวกันจะแชร์กลุ่มนี้
  • pid_<PID>/: ไดเรกทอรีสำหรับแต่ละกระบวนการและกระบวนการย่อยที่แยกออกมา ซึ่งช่วยให้ควบคุมและบันทึกข้อมูลสำหรับแอปที่มีหลายกระบวนการได้อย่างละเอียด

แบบฝึกหัดเชิงปฏิบัติ: การสำรวจ memcg

ในแบบฝึกหัดนี้ คุณจะได้ค้นหาไดเรกทอรี memcg สำหรับ MemoryLab และสังเกตการเรียกเก็บเงินหน่วยความจำแบบเรียลไทม์

1. เปิด MemoryLab

ตรวจสอบว่า MemoryLab ทำงานอยู่ในอุปกรณ์

2. ค้นหา memcg ของ MemoryLab

ขั้นแรก ให้รับ PID ของแอป MemoryLab ที่กำลังทำงานโดยทำดังนี้

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

จากนั้นค้นหาไดเรกทอรี cgroup ซึ่งจะอยู่ใน /proc/<PID>/cgroup

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

ใน cgroup v2 เส้นทางหลัง 0:: จะแสดงลำดับชั้น memcg ที่สัมพันธ์กับ /sys/fs/cgroup ดังนั้นเส้นทางแบบเต็มคือ /sys/fs/cgroup/apps/uid_10274/pid_11672/

3. อ่านสถิติ memcg

ไปที่ไดเรกทอรีนั้นแล้วดูไฟล์สำคัญ

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

memory.current จะแสดงหน่วยความจำทั้งหมด (เป็นไบต์) ที่เรียกเก็บเงินจาก cgroup นี้ในปัจจุบัน

# 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 จะแสดงรายละเอียดดังนี้ * anon: หน่วยความจำแบบไม่ระบุตัวตน (ฮีป, สแต็ก) * file: หน่วยความจำแบบมีไฟล์สำรอง (แคชหน้าเว็บ) * swap: หน่วยความจำที่สลับออกไปยัง ZRAM

4. สังเกตการเปลี่ยนแปลง

  1. เปิด MemoryLab
  2. จดค่า memory.current
  3. แตะจัดสรรหน่วยความจำ Java (10MB) หลายๆ ครั้ง
  4. อ่าน memory.current อีกครั้ง คุณควรเห็นค่าเพิ่มขึ้นประมาณ 10MB ต่อการแตะ 1 ครั้ง
  5. แตะจัดสรรบิตแมป ตรวจสอบ memory.stat เพื่อดูว่า anon เพิ่มขึ้นหรือไม่

การเรียกคืนหน่วยความจำเชิงรุกด้วย memory.reclaim

ฟีเจอร์ที่มีประสิทธิภาพของ memcg (v2) คือไฟล์ memory.reclaim การเขียนค่าลงในไฟล์นี้จะสั่งให้เคอร์เนลพยายามเรียกคืนหน่วยความจำจำนวนดังกล่าวจาก memcg นี้และ memcg ใดก็ตามที่อยู่ภายใต้ memcg นี้ ทันที

วิธีที่ Android ใช้ memory.reclaim

CachedAppOptimizer ของ Android ใช้ฟีเจอร์นี้เพื่อเรียกคืนหน่วยความจำจากแอปให้ได้มากที่สุดหลังจากที่แอปหยุดทำงาน App Freezer ของ Android ช่วยให้มั่นใจว่าแอปที่แคชไว้จะใช้ RAM น้อยที่สุดเท่าที่จะเป็นไปได้ในขณะที่แอปไม่ได้ทำงาน เมื่อแอปย้ายไปทำงานอยู่เบื้องหลังและหยุดทำงาน ระบบจะเขียนการใช้งานหน่วยความจำปัจจุบันของแอปไปยังไฟล์ memory.reclaim ของแอป ซึ่งจะบังคับให้เคอร์เนลนำหน้าไฟล์ที่เป็นไปได้ทั้งหมดออกและสลับหน้าแบบไม่ระบุตัวตนทั้งหมดไปยัง ZRAM เพื่อลดร่องรอยหน่วยความจำที่แอปใช้

คุณสามารถเรียกคืนหน่วยความจำ "ให้ได้มากที่สุด" ด้วยตนเองได้โดยอ่าน memory.current แล้วเขียนค่ากลับลงใน 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"

แบบฝึกหัด: บังคับให้เรียกคืนหน่วยความจำและติดตาม

ตอนนี้เราจะบังคับให้เคอร์เนลเรียกคืนหน่วยความจำจาก MemoryLab และบันทึกกิจกรรมใน Perfetto Trace

  1. เตรียม: ตรวจสอบว่า MemoryLab มีการจัดสรรหน่วยความจำบางส่วน (Java และบิตแมป)
  2. เริ่มการติดตาม: ใช้การกำหนดค่าการติดตามแบบอินไลน์นี้เพื่อบันทึก เหตุการณ์การจัดกำหนดการ การเรียกคืนหน่วยความจำ และแคชหน้าเว็บ

    # 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. ทริกเกอร์แรงกดดันและการเรียกคืนหน่วยความจำ:

    • ใน MemoryLab ให้แตะจัดสรรหน่วยความจำเนทีฟ (1GB) ซึ่งจะทริกเกอร์แรงกดดันทั้งระบบ
    • เรียกคืนหน่วยความจำ 200MB ในเทอร์มินัลแยกต่างหากโดยทำดังนี้

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In: กลับไปที่ MemoryLab แตะThrash Pagecache (Refault test)

  5. หยุดการติดตาม: กด Ctrl+C ในเทอร์มินัลการติดตาม

การวิเคราะห์การเรียกคืนหน่วยความจำใน Perfetto

เมื่อเปิดการติดตาม คุณจะเห็นการเรียกคืนหน่วยความจำทั้งระบบและต่อแอปที่ทำงานอยู่

Perfetto แสดงการเรียกคืนโดยตรงและ memcg

มองหาข้อมูลต่อไปนี้ในการติดตาม

  • kswapd: ค้นหา kswapd0 ในส่วนเธรดของเคอร์เนล (ในภาพหน้าจอ ด้านบน เราได้ปักหมุดไว้ที่ด้านบนด้วยตนเอง) คุณจะเห็นว่าเธรดนี้ตื่นขึ้นและทำงาน (สไลซ์สีเขียว) เนื่องจากระบบพยายามค้นหาหน้าว่าง
  • การเรียกคืนหน่วยความจำโดยตรง: ดูเธรดของกระบวนการ com.android.memorylab คุณจะเห็นสไลซ์เหตุการณ์ ftrace สีม่วง (เช่น mm_vmscan_direct_reclaim_begin) ปรากฏใต้แทร็กการจัดกำหนดการของเธรดโดยตรง ซึ่งบ่งชี้ว่าเธรดของแอปเองหยุดทำงานเพื่อรอให้เคอร์เนลปล่อยหน้า
  • ตัวนับ RSS และ Swap:
    • mem.rss.anon: เพิ่มขึ้นเมื่อคุณแตะปุ่มการจัดสรร
    • mem.swap: เพิ่มขึ้นอย่างต่อเนื่องเนื่องจาก kswapd และเธรดของแอปเอง (ขึ้นอยู่กับการเรียกคืนหน่วยความจำโดยตรง) บีบอัดหน้าแบบไม่ระบุตัวตนเหล่านั้นลงใน ZRAM
  • การเรียกคืนหน่วยความจำ memcg: หากซูมเข้าที่ช่วงเวลาที่คุณทริกเกอร์การเรียกคืนหน่วยความจำด้วยตนเอง คุณจะเห็น rss.anon และ rss.file ลดลงอย่างรวดเร็ว พร้อมกับเหตุการณ์ mm_vmscan_memcg_reclaim

← ทั้งระบบ | ↑ ขึ้น | การโต้ตอบระหว่าง kswapd กับ lmkd →