برگرداندن صفحه‌های حافظه به سیستم‌عامل

آزاد کردن شیء در کد برنامه یا بستن جریان موقت همیشه RAM فیزیکی را به سیستم‌عامل برنمی‌گرداند. تخصیص‌دهنده‌های بومی سفارشی، زمان‌های اجرای جمع‌آوری‌شده از زباله، و جدول‌های درهم‌سازی با آدرس باز اغلب حوزه‌های حافظه مجازی را حفظ می‌کنند که مدت‌ها پس‌از ازبین رفتن داده‌های منطقی درون آن‌ها، توسط صفحه‌های فیزیکی پشتیبانی می‌شوند. به‌طور مشابه، خواندن یا نگاشت حافظه بایگانی‌های دارایی بزرگ، حافظه پنهان صفحه هسته را پر می‌کند که همچنان به گروه کنترل حافظه (memcg) برنامه شما اختصاص داده می‌شود.

این صفحه نحوه استفاده از فراخوانی‌های سیستم madvise و posix_fadvise را برای برگرداندن صفحه‌های ناشناس و مبتنی بر فایل استفاده‌نشده به سیستم‌عامل، ضمن حفظ رزروهای نشانی مجازی و توصیف‌گرهای فایل، و نحوه درستی‌سنجی تأثیر در adb shell و Perfetto توضیح می‌دهد.

چرا لغو تخصیص منطقی حافظه دسترسی تصادفی فیزیکی را حفظ می‌کند

هسته Linux حافظه فیزیکی را در سطح جزئیات صفحه‌های سخت‌افزاری ردیابی می‌کند، که در دستگاه‌های Android‏ ۴ کیلوبایت یا ۱۶ کیلوبایت است. وقتی فرایندی منطقه‌ای از فضای نشانی مجازی را با mmap رزرو می‌کند، هسته بدون اختصاص دادن RAM فیزیکی، ناحیه حافظه مجازی (VMA) ایجاد می‌کند. هسته فقط زمانی صفحه فیزیکی را اختصاص می‌دهد و شارژ می‌کند که فرایند برای اولین‌بار آن صفحه را لمس کند. برای حافظه ناشناس، اولین خواندن یک صفحه صفر مشترک را نگاشت می‌کند، بنابراین فقط یک نوشتن RAM را اختصاص می‌دهد.

وقتی صفحه‌ای در RAM دچار خطا می‌شود، هسته نمی‌داند که آیا بایت‌های داخل آن صفحه هنوز نشان‌دهنده اشیاء زنده در زمان اجرای برنامه شما هستند یا خیر. دو الگوی رایج باعث می‌شوند صفحه‌های فیزیکی پس‌از اینکه برنامه شما استفاده از آن‌ها را تمام کرد، همچنان مقیم باقی بمانند:

  • محدوده‌ها و استخرهای ناشناس: تخصیص‌دهنده‌های بومی سفارشی (مانند تخصیص‌دهنده‌های slab، استخر، یا frame-arena)، زمان‌های اجرای مدیریت‌شده با فضاهای توده غیرمتحرک، و جدول‌های درهم‌سازی بزرگ، محدوده‌های نشانی مجازی پیوسته را با mmap بااستفاده از MAP_PRIVATE | MAP_ANONYMOUS رزرو می‌کنند. وقتی برنامه‌تان یک arena را بازنشانی می‌کند، یک بلوک را به یک استخر سفارشی برمی‌گرداند، یا یک جدول درهم‌سازی را با صفر کردن فراداده‌های آن پاک می‌کند، صفحات زیرین کثیف باقی می‌مانند. بدون اشاره صریح به هسته، آن صفحه‌ها همچنان در اندازه مجموعه مقیم (RSS) و اندازه مجموعه متناسب (PSS) ناشناس فرایند شما و هزینه cgroup memory.current آن محاسبه می‌شوند. تحت فشار حافظه، هسته آن‌ها را در ZRAM فشرده می‌کند، جایی که هنوز فضای مبادله اختصاص‌داده‌شده به memory.swap.current را اشغال می‌کنند.
  • حافظه نهان صفحه با پشتیبانی فایل از جاری‌سازی دارایی: وقتی برنامه‌تان دسته‌های دارایی بزرگ، بایگانی‌های سطح، محفظه‌های رسانه، یا مدل‌های یادگیری ماشین را بااستفاده از read، pread، یا mmap با پشتیبانی فایل می‌خواند، هسته هر صفحه خوانده‌شده را در حافظه نهان صفحه ذخیره می‌کند و آن صفحه‌ها را به شمارنده file در فایل memory.stat از cgroup فرایندتان اضافه می‌کند. اگر برنامه‌تان آن دارایی را از حالت فشرده خارج کند یا در میان‌گیر زمان اجرای ناشناس کپی کند یا آن را در GPU بارگذاری کند، نگه داشتن صفحه‌های فایل خام در حافظه پنهان صفحه، دارایی را در RAM تکرار می‌کند.
صفحه‌های کثیف arena با madvise منتشر شد و صفحه‌های دارایی ذخیره‌شده در حافظه نهان
       با posix_fadvise تخلیه شد، درحالی‌که محدوده مجازی و توصیف‌گر فایل
       دست‌نخورده باقی ماند.
شکل ۱. Reclaiming physical pages from virtual arenas and the page cache.

انتشار صفحه‌های ناشناس

فراخوانی سیستم 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 افت). صفحه‌های خصوصی کپی-در-نوشتن در نگاشت MAP_PRIVATE دور انداخته می‌شوند، بنابراین اصلاحات ازدست می‌روند و دسترسی بعدی فایل را دوباره می‌خواند. صفحه‌های کثیف MAP_SHARED بعداً دوباره در فضای ذخیره‌سازی نوشته می‌شوند. صفحه‌های تمیز تا زمانی که تخلیه شوند در حافظه پنهان صفحه باقی می‌مانند.
MADV_FREE ناشناس خصوصی (MAP_PRIVATE | MAP_ANONYMOUS) صفحه‌ها را به‌عنوان قابل‌استفاده مجدد به‌صورت تنبل علامت‌گذاری می‌کند. هسته صفحه‌ها را در RssAnon حفظ می‌کند اگر فشار حافظه وجود نداشته باشد و آن‌ها را فقط زمانی که بازیابی سراسری یا memcg اجرا می‌شود بازیابی می‌کند.
MADV_WILLNEED ناشناس با پشتیبانی فایل یا مبادله‌شده پیش‌خوانی ناهمگام را برای نگاشت‌های با پشتیبانی فایل شروع می‌کند، یا صفحه‌های ناشناس مبادله‌شده را از ZRAM به حافظه نهان مبادله ازقبل بازخوانی می‌کند.

تراز کردن محدوده‌های داخلی با اندازه صفحه زمان اجرا

madvise روی کل صفحه‌ها کار می‌کند. این تابع به نشانی شروع هم‌تراز با صفحه نیاز دارد و طول را به مرز صفحه بعدی گرد می‌کند. If you release a span inside a shared arena where neighboring bytes still hold live objects, rounding the range outward can zero out live allocations that share the boundary pages.

همیشه اندازه صفحه زمان اجرا را با sysconf(_SC_PAGESIZE) پُرسمان کنید تا از هر دو دستگاه صفحه ۴ کیلوبایتی و ۱۶ کیلوبایتی پشتیبانی شود، و نشانی شروع داخلی را به مرز صفحه بعدی گرد کنید و نشانی پایان داخلی را به مرز صفحه قبلی گرد کنید. برای راهنمایی بیشتر درباره تخصیص‌دهنده‌های مستقل از اندازه صفحه، بهینه‌سازی تخصیص‌دهنده‌های حافظه سفارشی را ببینید.

#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: پنجره پیش‌خوان هسته را برای توصیف‌گر فایل دوبرابر می‌کند و توان عملیاتی را هنگام جاری‌سازی بایگانی بزرگ از ابتدا تا انتها بهبود می‌بخشد. این را با 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 در تخصیص‌دهنده‌ها، بارکننده‌های دارایی، و کنترل‌کننده‌های چرخه حیات، این روش‌ها را اعمال کنید:

  • فراخوانی‌های دسته‌ای در مناطق درشت: هر فراخوانی madvise قفل نقشه حافظه فرایند شما (mmap_lock) را می‌گیرد، جدول‌های صفحه را پیمایش می‌کند، و ورودی‌های منطبق با میان‌گیر ترجمه (TLB) را در سایر هسته‌های CPU نامعتبر می‌کند (با دستورالعمل‌های نگهداری TLB همه‌فرستی در Arm و وقفه‌های بین‌پردازنده‌ای در x86). madvise را برای هر لغو تخصیص کوچکی فراخوانی نکنید. فاصله‌های آزاد را ادغام کنید و هسته را در تکه‌های ۶۴ کیلوبایتی تا ۱ مگابایتی یا بزرگ‌تر توصیه کنید، یا فاصله‌های آزاد را درطول مرزهای قاب و انتقال‌های پس‌زمینه (onStop و TRIM_MEMORY_UI_HIDDEN) پاک کنید.
  • بازیابی ظرفیت خالی در جدول‌های درهم‌سازی و استخرها: وقتی جدول درهم‌سازی بزرگ، استخر شیء، یا میان‌گیر آماده‌سازی پس‌از انتقال صحنه یا همگام‌سازی داده‌ها پاک می‌شود، یا تخصیص پشتیبان آن را کوچک کنید یا MADV_DONTNEED را در سراسر آرایه پشتیبان پاک‌شده صادر کنید. نگه داشتن جدول آدرس‌دهی باز ۳۲ مگابایتی اختصاص‌داده‌شده با سنگ قبر باعث می‌شود کل ۳۲ مگابایت صفحه‌های فیزیکی به فرایند شما اختصاص داده شود.
  • پاک کردن داده‌های فایل کثیف قبل‌از بیرون راندن آن: POSIX_FADV_DONTNEED فقط صفحه‌های حافظه پنهان صفحه تمیز را بیرون می‌راند. اگر برنامه‌تان فایل موقت یا حافظه نهان بزرگی می‌نویسد که بلافاصله آن را بازخوانی نمی‌کند، fdatasync را قبل‌از posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED) فراخوانی کنید تا صفحه‌های کثیف در فضای ذخیره‌سازی نوشته شوند و سپس از RAM حذف شوند.
  • کد یا دارایی‌های مشترک پرکاربرد را حذف نکنید: از فراخوانی POSIX_FADV_DONTNEED در DEX،‏ ODEX،‏ VDEX، کتابخانه‌های مشترک (.so)، یا دارایی‌های میانای کاربری که مکرراً به آن‌ها دسترسی پیدا می‌شود خودداری کنید. بیرون کردن کد فعال یا صفحه‌های دارایی باعث ایجاد خطاهای همگام‌سازی مجدد فضای ذخیره‌سازی می‌شود که رشته‌ها را متوقف می‌کند و فشار ورودی/خروجی را افزایش می‌دهد، همان‌طور که توسط اطلاعات توقف فشار (PSI) گزارش شده است.

درستی‌سنجی بازیابی صفحه در پوسته

تأیید کنید که madvise و posix_fadvise بااستفاده از showmap و فایل‌های حسابداری cgroup v2 برنامه‌تان، RAM فیزیکی را پس می‌گیرند.

بازرسی کردن اندازه مجازی درمقابل اندازه فیزیکی با showmap

وقتی یک عرصه ناشناس را با PR_SET_VMA_ANON_NAME نام‌گذاری می‌کنید، showmap هم رزرو مجازی آن (virtual size) و هم صفحات فیزیکی ساکن آن (RSS و PSS) را نمایش می‌دهد. خروجی نمونه زیر از برنامه نمونه MemoryLab در AOSP گرفته شده است که عرصه خود را memorylab-arena نام‌گذاری می‌کند. com.example.app و memorylab-arena را با نام بسته و نام عرصه خودتان جایگزین کنید. قبل‌از فراخوانی madvise با MADV_DONTNEED در یک عرصه ۶۴ مگابایتی لمس‌شده، این دستور را اجرا کنید:

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 روی ۶۵۵۳۶ کیلوبایت (۶۴ مگابایت) باقی می‌ماند که تأیید می‌کند محدوده نشانی مجازی همچنان رزرو شده است، درحالی‌که RSS، PSS، و private dirty به ۰ کیلوبایت کاهش می‌یابند.

بررسی cgroup memory.current و memory.stat

برای درستی‌سنجی بازیابی صفحه ناشناس و تخلیه حافظه پنهان صفحه نگاشت‌نشده (که اگر با 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
'

قبل‌از انتشار یک عرصه ناشناس ۶۴ مگابایتی و بیرون کردن یک فایل دارایی نگاشت‌شده در حافظه ۱۲۸ مگابایتی از حافظه نهان صفحه:

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 به‌اندازه ۶۴ مگابایت و file به‌اندازه ۱۲۸ مگابایت کاهش می‌یابد، که memory.current را بدون بستن توصیف‌گر فایل یا لغو نگاشت فضای آدرس مجازی آرنا حدود ۱۹۲ مگابایت کاهش می‌دهد. memory.swap.current به‌ندرت تغییر می‌کند، زیرا تقریباً هیچ‌کدام از صفحه‌های arena به ZRAM منتقل نشده‌اند. مقادیر شما به دستگاه و میزان فایلی که هسته قبلاً به‌تنهایی حذف کرده است بستگی دارد. برای اطلاعات بیشتر درباره نحوه شارژ و بازیابی حافظه توسط هسته به‌ازای هر cgroup، به بازیابی حافظه: تخلیه و تعویض مراجعه کنید.

تجزیه‌وتحلیل بازیابی صفحه در Perfetto

Perfetto به شما امکان می‌دهد فراخوانی‌های madvise و posix_fadvise را با شمارنده‌های جدول صفحه و حافظه پنهان صفحه هسته در یک خط زمان یکپارچه مرتبط کنید.

ضبط کردن ردیابی

ردیابی را با linux.process_stats برای شمارنده‌های فرایند دوره‌ای و linux.ftrace برای رویدادهای هم‌بس و حافظه پنهان صفحه هم‌زمان ضبط کنید، سپس آن را به رایانه‌تان بکشید:

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

بررسی قطعه‌ها در واسط کاربر Perfetto

ردگیری را در رابط کاربری Perfetto باز کنید، گروه فرایند برنامه‌تان را ازهم باز کنید، و قطعه‌های شمارنده زیر را سنجاق کنید تا در بالای خط زمان باقی بمانند:

  • mem.virt: در سراسر فراخوانی‌های MADV_DONTNEED ثابت می‌ماند، که تأیید می‌کند رزرو نشانی مجازی (mmap) بدون تغییر است.
  • mem.rss.anon: در مُهر زمان دقیقی که برنامه‌تان madvise را با MADV_DONTNEED در نواحی ناشناس کثیف یا مناطق توده فراخوانی می‌کند، به‌شدت کاهش می‌یابد.
  • mem.rss.file: وقتی برنامه‌تان madvise را با MADV_DONTNEED در ناحیه mmap با پشتیبانی فایل فرامی‌خواند، کاهش می‌یابد و با فوران mm_filemap_delete_from_page_cache رویدادهای ftrace همراه است، وقتی posix_fadvise با POSIX_FADV_DONTNEED صفحه‌های پاک را از حافظه نهان صفحه بیرون می‌کند.
  • mem.swap: زمانی کاهش می‌یابد که MADV_DONTNEED صفحه‌های ناشناسی را که قبلاً در ZRAM فشرده شده‌اند دور می‌اندازد.
Perfetto counter tracks for one process. mem.rss.anon and
       mem.rss.file step up when an arena and an asset file are populated and
       step back down after madvise and posix_fadvise, while mem.swap and
       mem.virt stay flat.
شکل ۲. RSS dropping after page advice calls.

شکل ۲ این قطعه‌ها را برای برنامه نمونه MemoryLab نشان می‌دهد. برای کاوش همان نما، رد نمونه را در میانای کاربری Perfetto باز کنید.

پرس‌وجو کردن اوج و حافظه نهایی با PerfettoSQL

پُرسمان PerfettoSQL زیر را در واسط کاربر Perfetto (برگه پُرسمان (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;

نمونه برونداد برای ردگیری در شکل ۲:

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 ۶۳ مگابایت و mem.rss.file ۱۲۸ مگابایت از اوج خود کاهش می‌یابند، درحالی‌که mem.virt در اوج خود به پایان می‌رسد زیرا رزروهای مجازی بدون تغییر باقی می‌مانند.

حل کردن مشکل صفحه‌های تخلیه‌شده به فایل‌ها

رویدادهای mm_filemap_add_to_page_cache و mm_filemap_delete_from_page_cache فایل‌ها را به‌جای مسیر با شماره گره ایندکس (i_ino) شناسایی می‌کنند. برای تأیید اینکه صفحه‌های تخلیه‌شده متعلق به فایلی است که توصیه کرده‌اید، تعداد تخلیه‌ها را به‌ازای هر گره ایندکس بشمارید:

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;

نمونه برونداد برای ردگیری در شکل ۲:

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

درحالی‌که دستگاهی که رد را ضبط کرده است هنوز متصل است، هر گره ایندکس را به یک مسیر حل کنید. شماره‌های گره ایندکس فقط در یک سیستم فایل منحصربه‌فرد هستند، بنابراین جستجو را به سیستم فایلی که داده‌های برنامه‌تان را نگه می‌دارد محدود کنید:

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

نمونه خروجی برای گره ایندکس 9761:

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

هر دو مسیر به یک فایل اشاره می‌کنند، زیرا /data/user/0 و /data/data دو نام برای یک دایرکتوری داده‌های برنامه هستند. تعداد اخراج‌ها برای فایل دارایی (۳۵٬۷۸۱ صفحه) بزرگ‌تر از خود فایل (۳۲٬۷۶۸ صفحه با اندازه ۴ کیلوبایت) است، زیرا kswapd قبلاً بخشی از فایل را تحت فشار حافظه اخراج کرده بود، قبل‌از اینکه برنامه آن را دوباره بخواند و posix_fadvise را فراخوانی کند.

برای اسکریپتی که چندین گره ایندکس را به‌طور هم‌زمان حل می‌کند، به حل کردن خطاها در گره‌های ایندکس، سپس در فایل‌ها مراجعه کنید.


← تعامل kswapd و lmkd | ↑ بالا