ในส่วนก่อนหน้า เราได้ดูวิธีที่เคอร์เนลจัดการหน่วยความจำทั้งระบบ ในบทนี้ เราจะเจาะลึกเรื่องกลุ่มควบคุมหน่วยความจำ (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 ใช้ลำดับชั้นที่เฉพาะเจาะจงเพื่อจัดการกระบวนการของแอป โครงสร้างนี้ช่วยให้ระบบใช้นโยบายที่แตกต่างกันกับแอปประเภทต่างๆ ได้ (เช่น แอปที่ทำงานอยู่เบื้องหน้าเทียบกับแอปที่ทำงานอยู่เบื้องหลัง)

/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. สังเกตการเปลี่ยนแปลง
- เปิด MemoryLab
- จดค่า
memory.current - แตะจัดสรรหน่วยความจำ Java (10MB) หลายๆ ครั้ง
- อ่าน
memory.currentอีกครั้ง คุณควรเห็นค่าเพิ่มขึ้นประมาณ 10MB ต่อการแตะ 1 ครั้ง - แตะจัดสรรบิตแมป ตรวจสอบ
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
- เตรียม: ตรวจสอบว่า MemoryLab มีการจัดสรรหน่วยความจำบางส่วน (Java และบิตแมป)
เริ่มการติดตาม: ใช้การกำหนดค่าการติดตามแบบอินไลน์นี้เพื่อบันทึก เหตุการณ์การจัดกำหนดการ การเรียกคืนหน่วยความจำ และแคชหน้าเว็บ
# 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ทริกเกอร์แรงกดดันและการเรียกคืนหน่วยความจำ:
- ใน MemoryLab ให้แตะจัดสรรหน่วยความจำเนทีฟ (1GB) ซึ่งจะทริกเกอร์แรงกดดันทั้งระบบ
เรียกคืนหน่วยความจำ 200MB ในเทอร์มินัลแยกต่างหากโดยทำดังนี้
adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
Fault Back In: กลับไปที่ MemoryLab แตะThrash Pagecache (Refault test)
หยุดการติดตาม: กด Ctrl+C ในเทอร์มินัลการติดตาม
การวิเคราะห์การเรียกคืนหน่วยความจำใน Perfetto
เมื่อเปิดการติดตาม คุณจะเห็นการเรียกคืนหน่วยความจำทั้งระบบและต่อแอปที่ทำงานอยู่

มองหาข้อมูลต่อไปนี้ในการติดตาม
- 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 →