بازپس‌گیری حافظه: تخلیه و مبادله

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

درک memcg بسیار مهم است زیرا در این سطح است که سیستم می‌تواند برنامه‌های جداگانه را برای بازیابی هدف قرار دهد یا محدودیت‌های حافظه را برای آن‌ها تنظیم کند، که این امر امکان مدیریت پیشگیرانه ردپای حافظه برنامه را فراهم می‌کند.

گروه‌های کنترل حافظه (memcg)

«گروه‌های کنترل» (cgroups) یکی از ویژگی‌های هسته Linux است که امکان سازمان‌دهی فرایندها در گروه‌های سلسله‌مراتبی و توزیع منابع سیستم (مانند CPU، حافظه، و I/O) را در میان آن‌ها فراهم می‌کند. memcg کنترل‌کننده cgroup مخصوص حافظه است.

مفاهیم کلیدی memcg

  • Memcg Charge: وقتی فرایندی در memcg صفحه حافظه‌ای را اختصاص می‌دهد (چه ناشناس یا با پشتیبانی فایل)، آن صفحه به memcg «شارژ» می‌شود. The total charge of a memcg is the sum of all pages used by all processes within it.
  • حسابداری سلسله‌مراتبی: استفاده از حافظه در بالای درخت حساب می‌شود. A charge in a child cgroup also counts towards the usage of its parent.
  • محدودیت‌های حافظه: هر memcg می‌تواند محدودیت‌هایی (مانند memory.max یا memory.high) داشته باشد که درصورت فراتر رفتن از آن‌ها، مستقل از حافظه سیستم جهانی، باعث فعال شدن بازیابی یا حتی OOM killer می‌شود.
  • Per-memcg Reclaim: وقتی memcg از حد مجاز خود فراتر می‌رود، یا وقتی سیستم به حافظه نیاز دارد، هسته می‌تواند memcg خاصی را برای بازیابی هدف قرار دهد. این یعنی صفحه‌های فایل آن را تخلیه می‌کند یا صفحه‌های ناشناس آن را به ZRAM منتقل می‌کند.

سلسله‌مراتب memcg در Android

Android از سلسله‌مراتب خاصی برای مدیریت فرایندهای برنامه استفاده می‌کند. این ساختار به سیستم امکان می‌دهد خط‌مشی‌های متفاوتی را برای انواع مختلف برنامه‌ها (مثلاً پیش‌زمینه درمقابل پس‌زمینه) اعمال کند.

سلسله‌مراتب memcg در Android

  • /sys/fs/cgroup/apps/: ریشه همه برنامه‌های Android.
  • uid_<UID>/: دایرکتوری برای «شناسه کاربر» هر برنامه. همه فرایندهای متعلق به یک بسته برنامه در این گروه مشترک هستند.
  • pid_<PID>/: دایرکتوری برای هر فرایند جداگانه و هر فرزند منشعب‌شده از آن. این کار امکان کنترل دقیق و حسابداری برای برنامه‌های دارای چند فرایند را فراهم می‌کند.

تمرین عملی: کاوش memcg

در این تمرین، دایرکتوری memcg را برای MemoryLab پیدا خواهید کرد و شارژ حافظه آن را به‌طور هم‌زمان مشاهده خواهید کرد.

۱. راه‌اندازی MemoryLab

مطمئن شوید MemoryLab در دستگاهتان درحال اجرا باشد.

۲. یافتن memcg مربوط به MemoryLab

ابتدا، شناسه پردازش برنامه 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/ است.

۳. خواندن آمار 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.

۴. مشاهده تغییرات

  1. MemoryLab را باز کنید.
  2. مقدار memory.current را یادداشت کنید.
  3. چندین‌بار روی اختصاص دادن حافظه Java‏ (۱۰ مگابایت) تک‌ضرب بزنید.
  4. memory.current را دوباره بخوانید. باید ببینید که با هر ضربه، تقریباً ۱۰ مگابایت افزایش می‌یابد.
  5. روی اختصاص دادن نگاشت‌های بیتی تک‌ضرب بزنید. memory.stat را بررسی کنید تا افزایش anon را ببینید.

بازپس‌گیری پیش‌کنشی با memory.reclaim

یکی از ویژگی‌های قدرتمند memcg (v2) فایل memory.reclaim است. نوشتن مقدار در این فایل به هسته دستور می‌دهد که فوراً تلاش کند آن مقدار حافظه را از این 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 ضبط کند.

  1. آماده‌سازی: مطمئن شوید MemoryLab چند تخصیص (Java و Bitmaps) داشته باشد.
  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، روی اختصاص دادن حافظه بومی (۱ گیگابایت) تک‌ضرب بزنید. این کار باعث ایجاد فشار در سراسر سیستم می‌شود.
    • در پایانه جداگانه‌ای، ۲۰۰ مگابایت را بازیابی کنید:

      adb shell "echo 200M > /sys/fs/cgroup/apps/uid_10274/pid_11672/memory.reclaim"
      
  4. Fault Back In: به MemoryLab برگردید. روی Thrash Pagecache (Refault test) (Thrash Pagecache (آزمایش Refault)) تک‌ضرب بزنید.

  5. توقف ردیابی: کلیدهای Ctrl+C را در پایانه ردیابی فشار دهید.

درحال تجزیه‌وتحلیل بازپس‌گیری در Perfetto

وقتی ردیابی را باز می‌کنید، می‌توانید هم بازیابی سراسری سیستم و هم بازیابی برای هر برنامه را در عمل ببینید:

Perfetto درحال نمایش بازیابی مستقیم و memcg

در ردیابی به‌دنبال موارد زیر بگردید:

  • kswapd: kswapd0 را در بخش رشته‌های هسته جستجو کنید (در نماگرفت بالا، به‌صورت دستی در بالا سنجاق شده است). خواهید دید که بیدار می‌شود و اجرا می‌شود (بخش‌های سبز) چون سیستم برای پیدا کردن صفحه‌های آزاد تلاش می‌کند.
  • Direct Reclaim: Look at the com.android.memorylab process threads. برش‌های بنفش رویداد ftrace (مانند mm_vmscan_direct_reclaim_begin) را خواهید دید که مستقیماً زیر قطعه زمان‌بندی رشته ظاهر می‌شوند. این نشان می‌دهد که رشته برنامه خودش متوقف شده است و منتظر است تا هسته صفحه‌ها را آزاد کند.
  • شمارنده‌های RSS و Swap:
    • ‫mem.rss.anon: با تک‌ضرب زدن روی دکمه‌های تخصیص افزایش می‌یابد.
    • mem.swap: به‌طور پیوسته افزایش می‌یابد زیرا kswapd و رشته‌های خود برنامه (مشمول بازپس‌گیری مستقیم) آن صفحه‌های ناشناس را در ZRAM فشرده می‌کنند.
  • memcg Reclaim: اگر روی لحظه‌ای که بازیابی دستی را راه‌اندازی کردید بزرگ‌نمایی کنید، افت شدیدی را در هر دو rss.anon و rss.file خواهید دید که با رویدادهای mm_vmscan_memcg_reclaim همراه است.

← در سراسر سیستم | ↑ بالا | تعامل kswapd و lmkd →