آزاد کردن شیء در کد برنامه یا بستن جریان موقت همیشه 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) ناشناس فرایند شما و هزینه cgroupmemory.currentآن محاسبه میشوند. تحت فشار حافظه، هسته آنها را در ZRAM فشرده میکند، جایی که هنوز فضای مبادله اختصاصدادهشده بهmemory.swap.currentرا اشغال میکنند. - حافظه نهان صفحه با پشتیبانی فایل از جاریسازی دارایی: وقتی برنامهتان دستههای دارایی بزرگ، بایگانیهای سطح، محفظههای رسانه، یا مدلهای یادگیری ماشین را بااستفاده از
read،pread، یاmmapبا پشتیبانی فایل میخواند، هسته هر صفحه خواندهشده را در حافظه نهان صفحه ذخیره میکند و آن صفحهها را به شمارندهfileدر فایلmemory.statاز cgroup فرایندتان اضافه میکند. اگر برنامهتان آن دارایی را از حالت فشرده خارج کند یا در میانگیر زمان اجرای ناشناس کپی کند یا آن را در GPU بارگذاری کند، نگه داشتن صفحههای فایل خام در حافظه پنهان صفحه، دارایی را در RAM تکرار میکند.
انتشار صفحههای ناشناس
فراخوانی سیستم 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:
-
madvise(addr, length, MADV_DONTNEED)را در محدوده نشانی مجازی نگاشتشده فراخوانی کنید تا نگاشت آن صفحهها از جدولهای صفحه فرایندتان لغو شود (RssFileافت میکند). 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 فشرده شدهاند دور میاندازد.
شکل ۲ این قطعهها را برای برنامه نمونه 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 | ↑ بالا