คืนหน้าหน่วยความจำไปยังระบบปฏิบัติการ

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

หน้านี้จะอธิบายวิธีใช้การเรียกใช้ระบบ madvise และ posix_fadvise เพื่อแสดงหน้าเว็บที่ไม่ระบุตัวตนและหน้าเว็บที่สำรองไฟล์ซึ่งไม่ได้ใช้ไปยังระบบปฏิบัติการ โดยยังคงรักษาการจองที่อยู่เสมือนและตัวบอกไฟล์ไว้เหมือนเดิม รวมถึงวิธีตรวจสอบผลกระทบใน adb shell และ Perfetto

เหตุใดการจัดสรรคืนเชิงตรรกะจึงยังคงใช้ RAM จริง

เคอร์เนล Linux จะติดตามหน่วยความจำกายภาพที่ระดับความละเอียดของหน้าฮาร์ดแวร์ ซึ่งมีขนาด 4 KB หรือ 16 KB ในอุปกรณ์ Android เมื่อกระบวนการจองพื้นที่ของพื้นที่ที่อยู่เสมือนด้วย mmap เคอร์เนลจะสร้างพื้นที่หน่วยความจำเสมือน (VMA) โดยไม่จัดสรร RAM จริง เคอร์เนลจะจัดสรรและเรียกเก็บเงินหน้าเว็บจริงก็ต่อเมื่อกระบวนการเข้าถึงหน้าเว็บนั้นเป็นครั้งแรกเท่านั้น สำหรับหน่วยความจำแบบไม่ระบุชื่อ การอ่านครั้งแรกจะแมปหน้าศูนย์ที่แชร์ ดังนั้นการเขียนเท่านั้นที่จะจัดสรร RAM

เมื่อหน้าเว็บเกิดข้อผิดพลาดใน RAM เคอร์เนลจะไม่ทราบว่าไบต์ภายในหน้าเว็บนั้นยังคงแสดงออบเจ็กต์ที่ทำงานอยู่ในรันไทม์ของแอปหรือไม่ รูปแบบที่พบบ่อย 2 รูปแบบที่ทำให้หน้าจริงยังคงอยู่หลังจากที่แอปใช้งานเสร็จแล้ว ได้แก่

  • อารีนาและพูลที่ไม่ระบุชื่อ: ตัวจัดสรรเนทีฟที่กำหนดเอง (เช่น ตัวจัดสรร Slab, พูล หรือเฟรมอารีนา) รันไทม์ที่มีการจัดการซึ่งมีพื้นที่ฮีปที่ไม่เคลื่อนที่ และตารางแฮชขนาดใหญ่จะจองช่วงที่อยู่เสมือนที่อยู่ติดกันด้วย mmap โดยใช้ MAP_PRIVATE | MAP_ANONYMOUS เมื่อแอปรีเซ็ตอารีนา ปล่อยบล็อกกลับไปยังพูลที่กำหนดเอง หรือล้างตารางแฮชโดยการตั้งค่าข้อมูลเมตาเป็น 0 หน้าที่เกี่ยวข้องจะยังคงเป็นหน้าสกปรก หากไม่มีคำแนะนำที่ชัดเจนสำหรับเคอร์เนล หน้าเหล่านั้นจะยังคงนับรวมใน Resident Set Size (RSS) และ Proportional Set Size (PSS) แบบไม่ระบุตัวตนของกระบวนการ และการเรียกเก็บเงิน cgroup memory.current ของกระบวนการ ภายใต้แรงกดดันด้านหน่วยความจำ เคอร์เนลจะบีบอัดหน้าหน่วยความจำเหล่านั้นเป็น ZRAM ซึ่งยังคงใช้พื้นที่ Swap ที่เรียกเก็บจาก memory.swap.current
  • แคชหน้าเว็บที่สำรองข้อมูลด้วยไฟล์จากการสตรีมเนื้อหา: เมื่อแอปอ่าน Asset Bundle ขนาดใหญ่, ไฟล์เก็บถาวรระดับ, คอนเทนเนอร์สื่อ หรือโมเดลแมชชีนเลิร์นนิงโดยใช้ read, pread หรือ mmap ที่สำรองข้อมูลด้วยไฟล์ เคอร์เนลจะแคชหน้าเว็บที่อ่านทุกหน้าในแคชหน้าเว็บ และเรียกเก็บเงินหน้าเว็บเหล่านั้นจากตัวนับ file ในไฟล์ memory.stat ของ cgroup ของกระบวนการ หากแอปคลายการบีบอัดหรือคัดลอกเนื้อหานั้นลงในบัฟเฟอร์รันไทม์ที่ไม่ระบุชื่อหรืออัปโหลดไปยัง GPU การเก็บหน้าไฟล์ข้อมูลดิบไว้ในแคชหน้าเว็บจะทำซ้ำเนื้อหาใน RAM
หน้าอารีนาที่สกปรกจะถูกปล่อยด้วย madvise และหน้าชิ้นงานที่แคชไว้จะถูกขับออกด้วย posix_fadvise ขณะที่ช่วงเสมือนและตัวบอกไฟล์ยังคงอยู่เหมือนเดิม
รูปที่ 1 การเรียกคืนหน้าจริงจากอารีนาเสมือนและแคชหน้าเว็บ

เผยแพร่หน้าเว็บที่ไม่ระบุชื่อ

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

หลังจาก madvise โดยมี MADV_DONTNEED แสดงผลดังนี้

  • ช่วงที่อยู่เสมือนยังคงสงวนไว้และใช้ได้ในพื้นที่ที่อยู่ของกระบวนการ (VmSize ไม่เปลี่ยนแปลง) ดังนั้นพอยน์เตอร์และขอบเขตอารีนาจึงยังคงอยู่โดยไม่ต้องเรียกใช้ munmap หรือ mmap
  • ระบบจะยกเลิกการแมปและปล่อยหน้าจริงทันที ซึ่งจะลด RssAnon, Pss, ตัวนับ anon ใน memory.stat และ memory.swap.current สำหรับ หน้าใดก็ตามที่อยู่ใน ZRAM
  • การอ่านหรือเขียนที่อยู่ใดๆ ในช่วงนั้นในภายหลังจะทำให้เกิดข้อผิดพลาดในหน้าเว็บที่เติมศูนย์ใหม่ตามต้องการ
แฟล็กคำแนะนำ การแมปเป้าหมาย ลักษณะการทำงานของเคอร์เนลใน Android
MADV_DONTNEED ไม่ระบุตัวตนแบบส่วนตัว (MAP_PRIVATE | MAP_ANONYMOUS) ทิ้งหน้าจริงและช่องสลับ ZRAM ทันที การเข้าถึงในภายหลังจะทำให้เกิดข้อผิดพลาดในหน้าเว็บที่ไม่มีข้อมูล การลดลงเชิงกำหนดใน RssAnon และ memory.current
MADV_DONTNEED ไฟล์ที่สำรองข้อมูล (MAP_PRIVATE หรือ MAP_SHARED) ยกเลิกการแมปหน้าเว็บจากตารางหน้าเว็บของกระบวนการ (RssFile การยกเลิก) ระบบจะทิ้งหน้า Copy-on-write ส่วนตัวในการแมป MAP_PRIVATE ดังนั้นการแก้ไขจะสูญหายและการเข้าถึงครั้งถัดไปจะอ่านไฟล์อีกครั้ง ระบบจะเขียนหน้า MAP_SHARED ที่มีการแก้ไขกลับไปยังพื้นที่เก็บข้อมูลในภายหลัง หน้าเว็บที่ไม่มีการแก้ไขจะยังคงอยู่ในแคชของหน้าเว็บจนกว่าจะถูกนำออก
MADV_FREE ไม่ระบุตัวตนแบบส่วนตัว (MAP_PRIVATE | MAP_ANONYMOUS) ทำเครื่องหมายหน้าเว็บว่านำมาใช้ซ้ำแบบ Lazy ได้ เคอร์เนลจะเก็บหน้าไว้ใน RssAnon หากไม่มีแรงกดดันด้านหน่วยความจำ และจะเรียกคืนหน้าเหล่านั้นก็ต่อเมื่อมีการเรียกคืนส่วนกลางหรือ memcg เท่านั้น
MADV_WILLNEED ไฟล์ที่สำรองข้อมูลหรือสลับแบบไม่ระบุตัวตน เริ่มการอ่านล่วงหน้าแบบอะซิงโครนัสสำหรับการแมปที่สำรองข้อมูลไฟล์ หรืออ่านหน้าเว็บที่ไม่ระบุตัวตนที่สลับกลับจาก ZRAM ไปยังแคชสลับล่วงหน้า

จัดแนวช่วงภายในให้สอดคล้องกับขนาดหน้าของรันไทม์

madvise ทำงานกับทั้งหน้าเว็บ โดยต้องใช้ที่อยู่เริ่มต้นที่สอดคล้องกับหน้าและปัดเศษความยาวขึ้นเป็นขอบเขตหน้าถัดไป หากคุณปล่อยช่วงภายในอารีนาที่ใช้ร่วมกันซึ่งไบต์ที่อยู่ใกล้เคียงยังคงมีออบเจ็กต์ที่ใช้งานอยู่ การปัดเศษช่วงออกด้านนอกอาจทำให้การจัดสรรที่ใช้งานอยู่ซึ่งใช้หน้าขอบเขตร่วมกันเป็นศูนย์

ค้นหาขนาดหน้าหน่วยความจำรันไทม์ด้วย sysconf(_SC_PAGESIZE) เสมอเพื่อรองรับทั้งอุปกรณ์ที่มีหน้าหน่วยความจำขนาด 4 KB และ 16 KB และปัดเศษที่อยู่เริ่มต้นภายในขึ้นไปยังขอบเขตหน้าถัดไป และปัดเศษที่อยู่สิ้นสุดภายในลงไปยังขอบเขตหน้าก่อนหน้า ดูคำแนะนำเพิ่มเติมเกี่ยวกับตัวจัดสรรที่ไม่ขึ้นอยู่กับขนาดหน้าได้ที่เพิ่มประสิทธิภาพตัวจัดสรรหน่วยความจำที่กำหนดเอง

#include <sys/mman.h>
#include <unistd.h>
#include <cstdint>

// Reclaims physical pages strictly inside [region_start, region_start + length)
// without disturbing live objects on partially occupied boundary pages.
void ReleaseUnusedArenaSpan(void* region_start, size_t length) {
  const uintptr_t page_size = static_cast<uintptr_t>(sysconf(_SC_PAGESIZE));
  const uintptr_t page_mask = ~(page_size - 1);

  const uintptr_t raw_start = reinterpret_cast<uintptr_t>(region_start);
  const uintptr_t raw_end = raw_start + length;

  // Round start UP and end DOWN so only fully unused pages are discarded.
  const uintptr_t aligned_start = (raw_start + page_size - 1) & page_mask;
  const uintptr_t aligned_end = raw_end & page_mask;

  if (aligned_end > aligned_start) {
    madvise(reinterpret_cast<void*>(aligned_start),
            aligned_end - aligned_start,
            MADV_DONTNEED);
  }
}

เมื่อสร้างการแมปแบบไม่ระบุตัวตนที่กำหนดเองด้วย mmap ให้ติดป้ายกำกับ VMA ด้วย prctl โดยใช้ PR_SET_VMA_ANON_NAME เพื่อให้คุณติดตามตำแหน่งที่อยู่ตามชื่อใน showmap และ /proc/<pid>/smaps ได้ dumpsys meminfo นับขอบเขตที่ไม่ระบุชื่อซึ่งตั้งชื่อเองใน Unknown เคอร์เนลรุ่นเก่าบางรุ่นจะเก็บตัวชี้ไปยังชื่อแทนที่จะคัดลอกชื่อ ดังนั้นให้ส่งสตริงที่ยังคงใช้ได้ตลอดอายุการใช้งานของการแมป เช่น สตริงลิเทอรัล

#include <sys/mman.h>
#include <sys/prctl.h>

#ifndef PR_SET_VMA
#define PR_SET_VMA 0x53564d41
#define PR_SET_VMA_ANON_NAME 0
#endif

void* ReserveNamedArena(size_t capacity_bytes, const char* arena_name) {
  void* ptr = mmap(nullptr, capacity_bytes, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
  if (ptr != MAP_FAILED) {
    prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME,
          reinterpret_cast<unsigned long>(ptr), capacity_bytes,
          reinterpret_cast<unsigned long>(arena_name));
  }
  return ptr;
}

ลบหน้าแคชของไฟล์

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

การเรียกใช้ระบบ posix_fadvise ช่วยให้แอปแจ้งเคอร์เนลเกี่ยวกับรูปแบบการเข้าถึงไฟล์ที่คาดไว้ในตัวบอกไฟล์ที่เปิดอยู่ได้

  • POSIX_FADV_DONTNEED: สั่งให้เคอร์เนลทราบว่าจะไม่มีการเข้าถึงช่วงไบต์ที่ระบุในอนาคตอันใกล้ เคอร์เนลจะล้างหน้าแคชที่สะอาดและไม่ได้แมปในช่วงที่จัดหน้าจากแคชหน้าทันที ซึ่งจะลดตัวนับ file ใน memory.stat และการเรียกเก็บเงิน memory.current ของ cgroup
  • POSIX_FADV_SEQUENTIAL: เพิ่มหน้าต่างการอ่านล่วงหน้าของเคอร์เนลเป็น 2 เท่าสำหรับตัวบอกไฟล์ ซึ่งจะช่วยปรับปรุงอัตราการส่งผ่านข้อมูลเมื่อสตรีมไฟล์ที่เก็บถาวรขนาดใหญ่ตั้งแต่ต้นจนจบ ใช้ร่วมกับ POSIX_FADV_DONTNEED ในส่วนท้ายขณะที่ใช้ เพื่อให้รอยเท้าของแคชหน้าเว็บยังคงจำกัดอยู่ในหน้าต่างแบบเลื่อนขนาดเล็ก
  • POSIX_FADV_RANDOM: ปิดใช้การอ่านล่วงหน้าของเคอร์เนลเชิงคาดการณ์ในตัวบอกไฟล์ ใช้ตัวเลือกนี้เมื่อทำการค้นหาแบบกระจัดกระจายและไม่ต่อเนื่องตามออฟเซ็ตไบต์ในไฟล์ที่เก็บถาวรหรือไฟล์ฐานข้อมูลขนาดใหญ่ เพื่อให้การอ่านขนาดเล็กแต่ละครั้งไม่ดึงหน้าเว็บที่ไม่ได้ใช้ซึ่งอยู่ติดกันเข้าไปในแคชของหน้าเว็บ สำหรับไฟล์ที่แมปหน่วยความจำ ให้เรียกใช้ madvise โดยใช้ MADV_RANDOM ในการแมปแทน

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

#include <fcntl.h>
#include <unistd.h>
#include <cstdint>
#include <vector>

void StreamAssetArchive(int fd, size_t total_size) {
  const size_t page_size = static_cast<size_t>(sysconf(_SC_PAGESIZE));
  constexpr size_t kChunkSize = 4 * 1024 * 1024;  // 4 MB streaming window
  std::vector<uint8_t> buffer(kChunkSize);

  // Hint sequential access so the kernel prefetches upcoming chunks.
  posix_fadvise(fd, 0, total_size, POSIX_FADV_SEQUENTIAL);

  off_t offset = 0;
  while (offset < static_cast<off_t>(total_size)) {
    ssize_t bytes_read = pread(fd, buffer.data(), buffer.size(), offset);
    if (bytes_read <= 0) break;

    // Decode or upload buffer[0 .. bytes_read) into runtime structures here.

    // Evict full pages that have been completely consumed.
    const size_t aligned_len =
        (static_cast<size_t>(bytes_read) / page_size) * page_size;
    if (aligned_len > 0) {
      posix_fadvise(fd, offset, static_cast<off_t>(aligned_len),
                    POSIX_FADV_DONTNEED);
    }
    offset += bytes_read;
  }
}

เรียกคืนพื้นที่ mmap ที่มีไฟล์สำรอง

หากแอปของคุณแมปหน่วยความจำกับไฟล์เนื้อหาขนาดใหญ่ด้วย mmap แทนที่จะอ่านด้วย pread การเรียกใช้ posix_fadvise ด้วย POSIX_FADV_DONTNEED เพียงอย่างเดียวจะไม่ลบหน้าเว็บที่ยังคงแมปอยู่ในตารางหน้าเว็บของกระบวนการ วิธีวางช่วงที่ใช้ไปแล้วของการแมปไฟล์แบบอ่านอย่างเดียวโดยไม่ต้องยกเลิกการแมป VMA ทั้งหมด

  1. เรียก madvise(addr, length, MADV_DONTNEED) ในช่วงที่อยู่เสมือนที่แมปไว้เพื่อยกเลิกการแมปหน้าเหล่านั้นจากตารางหน้าของกระบวนการ (RssFile ลดลง)
  2. เรียกใช้ posix_fadvise(fd, offset, length, POSIX_FADV_DONTNEED) ในออฟเซ็ตไฟล์ที่เกี่ยวข้องเพื่อให้เคอร์เนลลบหน้าสะอาดที่ไม่ได้แมปออกจากแคชหน้าด้วย (file ใน memory.stat ลดลง)

ปฏิบัติตามหลักเกณฑ์เกี่ยวกับคำแนะนำในหน้าเว็บ

ใช้แนวทางปฏิบัติเหล่านี้เมื่อผสานรวม madvise และ posix_fadvise เข้ากับ Allocator, ตัวโหลดเนื้อหา และตัวแฮนเดิลวงจร

  • การเรียกใช้แบบกลุ่มในพื้นที่แบบหยาบ: การเรียกใช้ madvise แต่ละครั้งจะใช้การล็อกแผนที่หน่วยความจำของกระบวนการ (mmap_lock) เดินผ่านตารางหน้า และทำให้รายการ Translation Lookaside Buffer (TLB) ที่ตรงกันในแกน CPU อื่นๆ ไม่ถูกต้อง (พร้อมวิธีการบำรุงรักษา TLB แบบบรอดแคสต์ใน Arm และการขัดจังหวะระหว่างโปรเซสเซอร์ใน x86) อย่าเรียกใช้ madvise ทุกครั้งที่มีการจัดสรรคืนเล็กน้อย รวมช่วงที่ว่างและแจ้งเคอร์เนลเป็นกลุ่มขนาด 64 KB ถึง 1 MB หรือใหญ่กว่า หรือกวาดช่วงที่ว่างระหว่างขอบเขตเฟรมและการเปลี่ยนพื้นหลัง (onStop และ TRIM_MEMORY_UI_HIDDEN)
  • เรียกคืนความจุที่ว่างเปล่าในตารางแฮชและพูล: เมื่อตารางแฮชขนาดใหญ่ พูลออบเจ็กต์ หรือบัฟเฟอร์การทดลองใช้ถูกล้างหลังจากเปลี่ยนฉากหรือซิงค์ข้อมูล ให้ลดการจัดสรรสำรองหรือออก MADV_DONTNEED ในอาร์เรย์สำรองที่ล้างแล้ว การเก็บตารางที่อยู่แบบเปิดขนาด 32 MB ที่จัดสรรด้วย Tombstone จะทำให้หน้าจริงขนาด 32 MB ทั้งหมดถูกเรียกเก็บเงินจากกระบวนการของคุณ
  • ล้างข้อมูลไฟล์ที่สกปรกก่อนที่จะขับออก: POSIX_FADV_DONTNEED จะขับเฉพาะหน้าแคชของหน้าที่สะอาดออกเท่านั้น หากแอปเขียนไฟล์แคชหรือไฟล์ชั่วคราวขนาดใหญ่ที่ไม่ได้อ่านกลับทันที ให้เรียกใช้ fdatasync ก่อน posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) เพื่อให้ระบบเขียนหน้าเว็บที่สกปรกลงในพื้นที่เก็บข้อมูลแล้วลบออกจาก RAM
  • อย่าขับไล่โค้ดหรือชิ้นงานที่ใช้ร่วมกันซึ่งมีการใช้งานบ่อย: หลีกเลี่ยงการเรียกใช้ POSIX_FADV_DONTNEED ใน DEX, ODEX, VDEX, คลังที่ใช้ร่วมกัน (.so) หรือชิ้นงาน UI ที่มีการเข้าถึงซ้ำๆ การลบโค้ดหรือหน้าชิ้นงานที่ใช้งานอยู่จะบังคับให้เกิดข้อผิดพลาดในการจัดเก็บข้อมูลแบบซิงโครนัสซ้ำ ซึ่งจะทำให้เธรดหยุดทำงานและเพิ่มแรงกดดันด้าน I/O ตามที่รายงานโดยข้อมูลการหยุดทำงานเนื่องจากแรงกดดัน (PSI)

ยืนยันการเรียกคืนหน้าเว็บใน Shell

ตรวจสอบว่า madvise และ posix_fadvise เรียกคืน RAM จริงโดยใช้ showmap และไฟล์การบันทึกการใช้งาน cgroup v2 ของแอป

ตรวจสอบขนาดเสมือนเทียบกับขนาดจริงด้วย showmap

เมื่อคุณตั้งชื่ออารีนาที่ไม่ระบุชื่อด้วย PR_SET_VMA_ANON_NAME, showmap จะแสดงทั้งการจองเสมือน (virtual size) และหน้าจริงที่อยู่ (RSS และ PSS) เอาต์พุตตัวอย่างต่อไปนี้มาจากแอปตัวอย่าง MemoryLab ใน AOSP ซึ่งตั้งชื่ออารีนาว่า memorylab-arena แทนที่ com.example.app และ memorylab-arena ด้วยชื่อแพ็กเกจและชื่ออารีนาของคุณ ก่อนเรียกใช้ madvise ด้วย MADV_DONTNEED ในอารีนาขนาด 64 MB ที่มีการแตะ ให้เรียกใช้คำสั่งต่อไปนี้

adb shell "showmap \$(pidof com.example.app) | grep -E '^ +virtual|^ +size|memorylab-arena' | head -3"
 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536    65536    65536        0        0        0    65536        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

หลังจากเรียกใช้ madvise(arena_ptr, 64 * 1024 * 1024, MADV_DONTNEED) แล้ว ให้เรียกใช้ showmap อีกครั้ง ดังนี้

 virtual                     shared   shared  private  private                   Anon      Shmem     File      Shared   Private
    size      RSS      PSS    clean    dirty    clean    dirty     swap  swapPSS HugePages PmdMapped PmdMapped Hugetlb  Hugetlb    Locked    # object
   65536        0        0        0        0        0        0        0        0         0         0         0        0        0        0    1 [anon:memorylab-arena]

virtual size จะยังคงอยู่ที่ 65536 KB (64 MB) ซึ่งยืนยันว่าช่วงที่อยู่เสมือนยังคงสงวนไว้ ขณะที่ RSS, PSS และ private dirty ลดลงเหลือ 0 KB

ตรวจสอบ memory.current และ memory.stat ของ cgroup

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

adb shell '
  PID=$(pidof com.example.app)
  CG=/sys/fs/cgroup$(sed -n "s/^0:://p" /proc/$PID/cgroup)
  echo "memory.current: $(cat $CG/memory.current)"
  echo "memory.swap.current: $(cat $CG/memory.swap.current)"
  grep -E "^(anon|file) " $CG/memory.stat
'

ก่อนที่จะปล่อยอารีนาแบบไม่ระบุชื่อขนาด 64 MB และนำไฟล์เนื้อหาที่แมปหน่วยความจำขนาด 128 MB ออกจากแคชของหน้าเว็บ ให้ทำดังนี้

memory.current: 242962432
memory.swap.current: 16633856
anon 100671488
file 140009472

ทันทีหลังจากเรียก madvise ด้วย MADV_DONTNEED ในอารีนาและการแมปไฟล์เนื้อหา จากนั้นเรียก posix_fadvise ด้วย POSIX_FADV_DONTNEED ในตัวบอกไฟล์เนื้อหา

memory.current: 41766912
memory.swap.current: 16502784
anon 33722368
file 5914624

anon ลดลง 64 MB และ file ลดลง 128 MB ซึ่งจะลด memory.current ลงประมาณ 192 MB โดยไม่ต้องปิดตัวบอกไฟล์หรือยกเลิกการแมปพื้นที่ที่อยู่เสมือนของอารีนา memory.swap.current แทบไม่มีการเปลี่ยนแปลง เนื่องจากแทบไม่มีหน้าอารีนาที่สลับไปยัง ZRAM ค่าของคุณจะขึ้นอยู่กับอุปกรณ์และจำนวนไฟล์ที่เคอร์เนลถูกขับออกไปเองแล้ว ดูข้อมูลเพิ่มเติมเกี่ยวกับวิธีที่เคอร์เนลเรียกเก็บเงินและเรียกคืนหน่วยความจำต่อ cgroup ได้ที่การเรียกคืนหน่วยความจำ: การขับออกและการสลับ

วิเคราะห์การเรียกคืนหน้าเว็บใน Perfetto

Perfetto ช่วยให้คุณเชื่อมโยงการเรียก madvise และ posix_fadvise กับตัวนับตารางหน้าและแคชหน้าของเคอร์เนลในไทม์ไลน์แบบรวมได้

บันทึกการติดตาม

บันทึกการติดตามด้วย linux.process_stats สำหรับตัวนับกระบวนการเป็นระยะและ linux.ftrace สำหรับเหตุการณ์ RSS และแคชหน้าเว็บแบบเรียลไทม์ จากนั้นดึงข้อมูลไปยังคอมพิวเตอร์ของคุณ

adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/page_advice.perfetto-trace <<EOF
buffers: {
    size_kb: 65536
    fill_policy: RING_BUFFER
}
data_sources: {
    config {
        name: "linux.process_stats"
        process_stats_config {
            scan_all_processes_on_start: true
            proc_stats_poll_ms: 100
        }
    }
}
data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "kmem/rss_stat"
            ftrace_events: "filemap/mm_filemap_add_to_page_cache"
            ftrace_events: "filemap/mm_filemap_delete_from_page_cache"
        }
    }
}
duration_ms: 20000
EOF
adb pull /data/misc/perfetto-traces/page_advice.perfetto-trace

ตรวจสอบแทร็กใน UI ของ Perfetto

เปิดการติดตามใน Perfetto UI ขยายกลุ่มกระบวนการของแอป และปักหมุดแทร็กตัวนับต่อไปนี้เพื่อให้อยู่ที่ด้านบนของไทม์ไลน์

  • mem.virt: ยังคงคงที่ในการเรียก MADV_DONTNEED ครั้ง ซึ่งยืนยันว่าการจองที่อยู่เสมือน (mmap) ไม่มีการเปลี่ยนแปลง
  • mem.rss.anon: ลดลงอย่างรวดเร็วที่การประทับเวลาที่แน่นอนซึ่งแอปของคุณเรียกใช้ madvise ด้วย MADV_DONTNEED ในอารีนาที่ไม่ระบุตัวตนหรือพื้นที่ฮีปที่สกปรก
  • mem.rss.file: ลดระดับลงเมื่อแอปของคุณเรียกใช้ madvise ด้วย MADV_DONTNEED ในพื้นที่ mmap ที่สำรองข้อมูลไว้ในไฟล์ พร้อมกับเหตุการณ์ ftrace mm_filemap_delete_from_page_cache ที่เกิดขึ้นอย่างรวดเร็วเมื่อ posix_fadvise ด้วย POSIX_FADV_DONTNEED นำหน้าเว็บที่สะอาดออกจากแคชของหน้าเว็บ
  • mem.swap: ลดลงเมื่อ MADV_DONTNEED ทิ้งหน้าเว็บที่ไม่ระบุตัวตนซึ่งก่อนหน้านี้บีบอัดเป็น ZRAM
Perfetto ติดตามตัวนับสำหรับ 1 กระบวนการ mem.rss.anon และ mem.rss.file จะเพิ่มขึ้นเมื่อมีการสร้างอารีนาและไฟล์เนื้อหา และจะลดลงหลังจาก madvise และ posix_fadvise ขณะที่ mem.swap และ mem.virt จะคงที่
รูปที่ 2 RSS ลดลงหลังจากมีการเรียกใช้คำแนะนำในหน้าเว็บ

รูปที่ 2 แสดงแทร็กเหล่านี้สำหรับแอปตัวอย่าง MemoryLab หากต้องการสำรวจมุมมองเดียวกัน ให้เปิดการติดตามตัวอย่างใน Perfetto UI

ค้นหาหน่วยความจำสูงสุดและหน่วยความจำสุดท้ายด้วย PerfettoSQL

เรียกใช้การค้นหา PerfettoSQL ต่อไปนี้ใน Perfetto UI (แท็บการค้นหา (SQL)) เพื่อเปรียบเทียบค่าสูงสุดและค่าสุดท้ายของ mem.virt, mem.rss.anon, mem.rss.file และ mem.swap สำหรับกระบวนการเป้าหมาย

WITH counters AS (
  SELECT t.name AS counter_name, c.ts, c.value
  FROM counter c
  JOIN process_counter_track t ON c.track_id = t.id
  JOIN process p USING (upid)
  WHERE p.name = 'com.example.app'
    AND t.name IN ('mem.virt', 'mem.rss.anon', 'mem.rss.file', 'mem.swap')
)
SELECT
  counter_name,
  CAST(MAX(value) / 1048576 AS INT) AS peak_mb,
  CAST((
    SELECT value FROM counters AS last
    WHERE last.counter_name = counters.counter_name
    ORDER BY ts DESC LIMIT 1
  ) / 1048576 AS INT) AS final_mb
FROM counters
GROUP BY counter_name
ORDER BY counter_name;

ตัวอย่างเอาต์พุตสำหรับการติดตามในรูปที่ 2

counter_name  peak_mb  final_mb
mem.rss.anon      111        48
mem.rss.file      212        84
mem.swap           36        36
mem.virt        13952     13952

mem.rss.anon ลดลง 63 MB และ mem.rss.file ลดลง 128 MB จากจุดสูงสุด ส่วน mem.virt สิ้นสุดที่จุดสูงสุดเนื่องจากการจองเสมือนไม่มีการเปลี่ยนแปลง

แก้ไขหน้าเว็บที่ถูกนำออกไปยังไฟล์

เหตุการณ์ mm_filemap_add_to_page_cache และ mm_filemap_delete_from_page_cache จะระบุไฟล์ตามหมายเลข inode (i_ino) แทนที่จะเป็นเส้นทาง หากต้องการยืนยันว่าหน้าที่ถูกลบเป็นของไฟล์ที่คุณแนะนำ ให้นับการลบต่อ inode ดังนี้

SELECT
  EXTRACT_ARG(arg_set_id, 'i_ino') AS inode,
  COUNT(*) AS evicted_pages
FROM ftrace_event
WHERE name = 'mm_filemap_delete_from_page_cache'
GROUP BY inode
ORDER BY evicted_pages DESC
LIMIT 5;

ตัวอย่างเอาต์พุตสำหรับการติดตามในรูปที่ 2

inode     evicted_pages
9761              35781
76792192            767
126                 668
0                   612
11587712            514

ขณะที่อุปกรณ์ที่บันทึกการติดตามยังคงเชื่อมต่ออยู่ ให้แก้ไข inode แต่ละรายการเป็นเส้นทาง หมายเลข Inode จะไม่ซ้ำกันภายในระบบไฟล์เดียวเท่านั้น ดังนั้นให้จำกัดการค้นหาไว้ที่ระบบไฟล์ที่มีข้อมูลของแอป ดังนี้

INODE=<inode from the query>
adb shell "find /data -xdev -inum $INODE 2>/dev/null"

ตัวอย่างเอาต์พุตสำหรับ inode 9761:

/data/user/0/com.android.memorylab/files/asset_pack.bin
/data/data/com.android.memorylab/files/asset_pack.bin

ทั้ง 2 เส้นทางอ้างอิงถึงไฟล์เดียวกัน เนื่องจาก /data/user/0 และ /data/data เป็นชื่อ 2 ชื่อสำหรับไดเรกทอรีข้อมูลแอปเดียวกัน จำนวนการขับออกสำหรับไฟล์เนื้อหา (35,781 หน้า) มีขนาดใหญ่กว่าตัวไฟล์เอง (32,768 หน้าที่ 4 KB) เนื่องจาก kswapd ได้ขับส่วนหนึ่งของไฟล์ออกไปแล้วภายใต้แรงกดดันด้านหน่วยความจำก่อนที่แอปจะอ่านไฟล์กลับมาและเรียกใช้ posix_fadvise

สำหรับสคริปต์ที่แก้ไข inode หลายรายการพร้อมกัน โปรดดูหัวข้อการแก้ไขข้อผิดพลาดใน inode แล้วจึงแก้ไขไฟล์


← การโต้ตอบระหว่าง kswapd และ lmkd | ↑ ขึ้น | กระบวนการเบื้องหลังแบบ Lean →