diff --git a/doc/api.rst b/doc/api.rst index d1dc6c8e..eb985082 100644 --- a/doc/api.rst +++ b/doc/api.rst @@ -105,3 +105,9 @@ drgn_tools.rds .. automodule:: drgn_tools.rds :members: + +drgn_tools.meminfo +----------------------- + +.. automodule:: drgn_tools.meminfo + :members: diff --git a/drgn_tools/corelens.py b/drgn_tools/corelens.py index b6030522..bb32b1d6 100644 --- a/drgn_tools/corelens.py +++ b/drgn_tools/corelens.py @@ -184,6 +184,7 @@ def all_corelens_modules() -> Dict[str, CorelensModule]: "drgn_tools.block", "drgn_tools.md", "drgn_tools.rds", + "drgn_tools.meminfo", ] for python_module in python_mods: importlib.import_module(python_module) diff --git a/drgn_tools/meminfo.py b/drgn_tools/meminfo.py new file mode 100644 index 00000000..189494f0 --- /dev/null +++ b/drgn_tools/meminfo.py @@ -0,0 +1,755 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +""" +Helpers for dumping memory usage information and statistics +""" +import argparse +from typing import Dict +from typing import Iterator +from typing import List + +from drgn import Object +from drgn import Program +from drgn.helpers.linux import list_for_each_entry +from drgn.helpers.linux.percpu import percpu_counter_sum + +from drgn_tools.corelens import CorelensModule +from drgn_tools.mm import totalram_pages +from drgn_tools.util import enum_value_get +from drgn_tools.util import get_uts +from drgn_tools.util import has_member + + +__all__ = ("show_all_meminfo", "get_all_meminfo") + + +# Machine info +_IS_X86_64 = False +_IS_AARCH_64 = False +_UEK_VERSION = 5 + +# Basic memory subsystem parameters are in the following: +# The number of NUMA +_NODES_SHIFT = 10 +_MAX_NUM_NODES = 1 << _NODES_SHIFT +_MAX_NUM_ZONES = 0 + +# Page settings. +_PAGE_SIZE = 0 +_PAGE_SHIFT = 0 +_PMD_SHIFT = 0 +_HPAGE_SHIFT = 0 + +_HPAGE_PMD_SHIFT = 0 +_HPAGE_PMD_ORDER = 0 +_HPAGE_PMD_NR = 0 + +# vmalloc memory range. +_VMALLOC_START = 0 +_VMALLOC_END = 0 + +# Using five bits for the type means that the maximum number of +# swapcache pages is 27 bits * on 32-bit-pgoff_t architectures. +_MAX_SWAPFILES_SHIFT = 5 +# NUMA node memory migration support +_SWP_DEVICE_NUM = 2 +# NUMA node memory migration support +_SWP_MIGRATION_NUM = 2 +# Handling of hardware poisoned pages with memory corruption. +_SWP_HWPOISON_NUM = 1 + +# |MAX_SWAPFILES| defines the maximum number of swaptypes. +_MAX_SWAPFILES = ( + (1 << _MAX_SWAPFILES_SHIFT) + - _SWP_DEVICE_NUM + - _SWP_MIGRATION_NUM + - _SWP_HWPOISON_NUM +) + +# The index for unknown stats ID for node stats and zone stats. +UNKNOWN_GLOBAL_STAT_ID = -1 + + +def print_val_kb(text: str, num: int) -> None: + """ + Produce the formatted output that matches the output in /proc/meminfo. + + param text: The output item's name, e.g, "MemTotal" + param num: The output item's value + """ + if num < 0: + return + text += ":" + num_kb = num << (_PAGE_SHIFT - 10) + print("%s%s kB" % (text.ljust(16, " "), str(num_kb).rjust(8, " "))) + + +def parse_machine_info(prog: Program) -> None: + """ + Parse the system's information to determine architecture-specific parameters. + This function must be called before parsing vmcore's memory statistics. + """ + global _IS_X86_64, _IS_AARCH_64, _UEK_VERSION + global _MAX_NUM_NODES, _MAX_NUM_ZONES + global _PAGE_SIZE, _PAGE_SHIFT, _PMD_SHIFT, _HPAGE_SHIFT + global _HPAGE_PMD_SHIFT, _HPAGE_PMD_ORDER, _HPAGE_PMD_NR + global _VMALLOC_END, _VMALLOC_START, _VMALLOC_SPACE + + uts = get_uts(prog) + # Set UEK version + if "4.14.35" in uts["release"]: + _UEK_VERSION = 5 + elif "5.4.17" in uts["release"]: + _UEK_VERSION = 6 + else: + _UEK_VERSION = 7 + + # Determine page size + _PAGE_SIZE = prog["PAGE_SIZE"].value_() + _PAGE_SHIFT = prog["PAGE_SHIFT"].value_() + + # Set arch + if "aarch64" in uts["machine"]: + _IS_AARCH_64 = True + + # include/linux/numa.h + _NODES_SHIFT = 2 + + # arch/arm64/include/asm/pgtable-hwdef.h + n = 2 + _PMD_SHIFT = (_PAGE_SHIFT - 3) * (4 - (n)) + 3 + n = 1 + PGDIR_SHIFT = (_PAGE_SHIFT - 3) * (4 - (n)) + 3 + + # include/asm-generic/pgtable-nopud.h + P4D_SHIFT = PGDIR_SHIFT + PUD_SHIFT = P4D_SHIFT + PUD_SIZE = 1 << PUD_SHIFT + + # arch/arm64/include/asm/pgtable.h + _HPAGE_SHIFT = _PMD_SHIFT + + SZ_64K = 0x00010000 + SZ_128M = 0x08000000 + VA_BITS = 48 + VA_START = 0xFFFFFFFFFFFFFFFF - (1 << VA_BITS) + 1 + PAGE_OFFSET = 0xFFFFFFFFFFFFFFFF - (1 << (VA_BITS - 1)) + 1 + STRUCT_PAGE_MAX_SHIFT = 6 + KASAN_SHADOW_SIZE = 0 + MODULES_VADDR = VA_START + KASAN_SHADOW_SIZE + MODULES_VSIZE = SZ_128M + MODULES_END = MODULES_VADDR + MODULES_VSIZE + VMEMMAP_SIZE = 1 << (VA_BITS - _PAGE_SHIFT - 1 + STRUCT_PAGE_MAX_SHIFT) + _VMALLOC_START = MODULES_END + _VMALLOC_END = PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K + + else: + _IS_X86_64 = True + + # include/linux/numa.h + _NODES_SHIFT = 10 + + # arch/x86/include/asm/pgtable_64_types.h + _PMD_SHIFT = 21 + + VMALLOC_SIZE_TB = 32 + _VMALLOC_START = prog["vmalloc_base"].value_() + _VMALLOC_END = _VMALLOC_START + (VMALLOC_SIZE_TB << 40) - 1 + + # arch/x86/include/asm/page_types.h + _HPAGE_SHIFT = _PMD_SHIFT + + _MAX_NUM_NODES = 1 << _NODES_SHIFT + _MAX_NUM_ZONES = prog["__MAX_NR_ZONES"].value_() + + _HPAGE_PMD_SHIFT = _PMD_SHIFT + _HPAGE_PMD_ORDER = _HPAGE_PMD_SHIFT - _PAGE_SHIFT + _HPAGE_PMD_NR = 1 << _HPAGE_PMD_ORDER + + +def get_active_numa_nodes(prog: Program) -> List[Object]: + """ + Get a list of all active NUMA nodes. + + :returns: a list of ``struct pglist_data *`` objects. + """ + try: + active_numa_nodes = prog.cache["active_numa_nodes"] + except KeyError: + active_numa_nodes = [] + node_data = prog["node_data"].read_() + for i in range(_MAX_NUM_NODES): + if node_data[i].value_() != 0x0: + active_numa_nodes.append(node_data[i]) + prog.cache["active_numa_nodes"] = active_numa_nodes + return active_numa_nodes + + +def for_each_node_zone(node: Object) -> Iterator[Object]: + """ + Iterate over all memory zones in a NUMA node. + + :returns: Iterator of ``struct zone *`` objects. + """ + node_zones = node.member_("node_zones") + for j in range(_MAX_NUM_ZONES): + if node_zones[j].value_() != 0x0: + yield node_zones[j] + + +def for_each_zone(prog: Program) -> Iterator[Object]: + """ + Iterate over all zones in a system that contains 1+ NUMA nodes. + + :returns: Iterator of ``struct zone *`` objects. + """ + for node in get_active_numa_nodes(prog): + for zone in for_each_node_zone(node): + yield zone + + +def for_each_block_dev(prog: Program) -> Iterator[Object]: + """ + Iterate over all block devices in a system. + + :returns: Iterator of ``struct block_device *`` objects. + """ + return list_for_each_entry( + "struct block_device", prog["all_bdevs"].address_of_(), "bd_list" + ) + + +def for_each_block_dev_inode(prog: Program) -> Iterator[Object]: + """ + Iterate over all block devices in a system. + + :returns: Iterator of ``struct inode *`` objects. + """ + return list_for_each_entry( + "struct inode", + prog["blockdev_superblock"].member_("s_inodes").address_of_(), + "i_sb_list", + ) + + +def for_each_hstate(prog: Program) -> Iterator[Object]: + """ + Iterate over all hugepage pools in a system. + Note: each |hstate| represents a pool for a certain unit size hugepages of a NUMA node. + + :returns: Iterator of ``struct hstate`` objects. + """ + hstates = prog["hstates"].read_() + for i in range(prog["hugetlb_max_hstate"].value_()): + yield hstates[i] + + +def is_valid_node_page_state(prog: Program, key: str, offset: int = 0) -> bool: + try: + valid_node_states = prog.cache["valid_node_states"] + except KeyError: + valid_node_states = [ + s for (s, _) in prog.type("enum node_stat_item").enumerators + ] + prog.cache["valid_node_states"] = valid_node_states + + if key not in valid_node_states: + return False + return 0 <= valid_node_states.index(key) + offset < len(valid_node_states) + + +def is_valid_zone_page_state(prog: Program, key: str, offset: int = 0) -> bool: + try: + valid_zone_states = prog.cache["valid_zone_states"] + except KeyError: + valid_zone_states = [ + s for (s, _) in prog.type("enum zone_stat_item").enumerators + ] + prog.cache["valid_zone_states"] = valid_zone_states + + if key not in valid_zone_states: + return False + return 0 <= valid_zone_states.index(key) + offset < len(valid_zone_states) + + +def get_global_node_page_state( + prog: Program, key: str, offset: int = 0 +) -> int: + """ + Get the global stats item from all NUMA nodes at the target key. + + :param prog: drgn program + :param key: The target node statistics item's name + :param offset: The target node statistics item's offset + :returns: The target node statistics item's value + """ + if not is_valid_node_page_state(prog, key): + print("INVALID enum item %s in node statistics" % (key)) + return 0 + + item = enum_value_get(prog, key, offset) + try: + vm_node_stat = prog.cache["vm_node_stat"] + except KeyError: + vm_node_stat = prog["vm_node_stat"].read_() + prog.cache["vm_node_stat"] = vm_node_stat + + try: + ret = vm_node_stat[item] + except LookupError: + raise IndexError("UNKNOWN enum item %d in node statistics" % (item)) + return max(0, ret.counter.value_()) + + +def get_global_zone_page_state( + prog: Program, key: str, offset: int = 0 +) -> int: + """ + Get the global stats item from all memory zones at the target key. + + :param prog: drgn program + :param key: The target zone statistics item's name + :param offset: The target zone statistics item's offset + :returns: The target zone statistics item's name + """ + if not is_valid_zone_page_state(prog, key): + print("INVALID enum item %s in zone statistics" % (key)) + return 0 + + item = enum_value_get(prog, key, offset) + try: + vm_zone_stat = prog.cache["vm_zone_stat"] + except KeyError: + vm_zone_stat = prog["vm_zone_stat"].read_() + prog.cache["vm_zone_stat"] = vm_zone_stat + + try: + ret = vm_zone_stat[item] + except LookupError: + raise IndexError("UNKNOWN enum item %d in zone statistics" % (item)) + return max(0, ret.counter.value_()) + + +def get_global_page_state(prog: Program, key: str, offset: int = 0) -> int: + """ + Get either global node page statistics or global zone page statistics. + |get_global_zone_page_state| and |get_global_node_page_state| should NOT + be directly called from other functions. + """ + if key == "NR_SLAB_RECLAIMABLE" and key not in prog: + key = "NR_SLAB_RECLAIMABLE_B" + if key == "NR_SLAB_UNRECLAIMABLE" and key not in prog: + key = "NR_SLAB_UNRECLAIMABLE_B" + + if is_valid_zone_page_state(prog, key): + return get_global_zone_page_state(prog, key, offset) + if is_valid_node_page_state(prog, key): + return get_global_node_page_state(prog, key, offset) + raise IndexError("INVALID enum item %s in node/zone statistics", key) + + +def get_total_available_pages(prog: Program) -> int: + """ + Get an estimation of the amount of memory available for future use. + This includes the number of free memory pages and the number of pages + that can be reclaimed from caches. + + :returns: The number of available memory in pages. + """ + free_ram_pages = get_global_page_state(prog, "NR_FREE_PAGES") + totalreserve_pages = prog["totalreserve_pages"].value_() + available_pages = free_ram_pages - totalreserve_pages + + # Calculate the global low watermark + wmark_low = 0 + for zone in for_each_zone(prog): + if has_member(zone, "_watermark"): + zone_low_wmark = zone.member_("_watermark")[ + enum_value_get(prog, "WMARK_LOW") + ] + else: + zone_low_wmark = zone.member_("watermark")[ + enum_value_get(prog, "WMARK_LOW") + ] + wmark_low += zone_low_wmark.value_() + + # Estimate reclaimable page cache + lru_pages = get_lru_pages(prog) + lru_active_file = lru_pages[enum_value_get(prog, "LRU_ACTIVE_FILE")] + lru_inactive_file = lru_pages[enum_value_get(prog, "LRU_INACTIVE_FILE")] + pagecache = lru_active_file + lru_inactive_file + pagecache -= min(pagecache // 2, wmark_low) + available_pages += pagecache + + # Determine the number of reclaimable pages + reclaimable_pages = 0 + # Estimate reclaimable slab cache + slab_reclaimable = get_global_page_state(prog, "NR_SLAB_RECLAIMABLE") + reclaimable_pages += slab_reclaimable + + if "NR_KERNEL_MISC_RECLAIMABLE" in prog: + misc = get_global_page_state(prog, "NR_KERNEL_MISC_RECLAIMABLE") + reclaimable_pages += misc + + reclaimable_pages -= min(reclaimable_pages // 2, wmark_low) + available_pages += reclaimable_pages + + # Estimate reclaimable kernel memory + if "NR_INDIRECTLY_RECLAIMABLE_BYTES" in prog: + page_shift = prog["PAGE_SHIFT"].value_() + indirect_reclaimable_pages = ( + get_global_page_state(prog, "NR_INDIRECTLY_RECLAIMABLE_BYTES") + >> page_shift + ) + available_pages += indirect_reclaimable_pages + + available_pages = max(0, available_pages) + return available_pages + + +def get_total_swapcache_pages(prog: Program) -> int: + """ + Get the total number of cached swap pages for all swap types. + + :returns: The number of cached memory back by swap space in pages. + """ + ret = 0 + nr_swapper_spaces = prog["nr_swapper_spaces"] + swapper_spaces = prog["swapper_spaces"] + + for i in range(_MAX_SWAPFILES): + nr = nr_swapper_spaces[i] + spaces = swapper_spaces[i] + if nr == 0 or spaces.value_() == 0x0: + continue + + for j in range(nr): + ret += spaces[j].member_("nrpages").value_() + return ret + + +def get_lru_pages(prog: Program) -> List[int]: + """ + Get the number of pages for all LRU lists, including: INACTIVE_ANON, + ACTIVE_ANON, INACTIVE_FILE, ACTIVE_FILE, and LRU_UNEVICTABLE. + + :returns: A list of integer numbers that represents numbers of pages + in each LRU list. + """ + try: + lru_pages = prog.cache["lru_pages"] + except KeyError: + num_lru_lists = prog.constant("NR_LRU_LISTS").value_() + lru_pages = [0 for _ in range(num_lru_lists)] + for lru in range(num_lru_lists): + lru_pages[lru] = get_global_page_state(prog, "NR_LRU_BASE", lru) + prog.cache["lru_pages"] = lru_pages + return lru_pages + + +def get_blockdev_pages(prog: Program) -> int: + """Get the sum of memory pages used by all block devices.""" + ret = 0 + if "all_bdevs" in prog: + for bdev in for_each_block_dev(prog): + inode = bdev.member_("bd_inode") + ret += inode.member_("i_mapping").member_("nrpages").value_() + else: + for inode in for_each_block_dev_inode(prog): + ret += inode.member_("i_mapping").member_("nrpages").value_() + return ret + + +def get_total_hugetlb_pages(prog: Program) -> int: + """Get sum of (4 kB) memory pages from all hstate hugepage pools.""" + ret = 0 + for h in for_each_hstate(prog): + pages_per_hugepage = 1 << (h.member_("order").value_()) + ret += h.member_("nr_huge_pages").value_() * pages_per_hugepage + return ret + + +def get_vm_commit_limit(prog: Program) -> int: + """ + Get the total amount of memory (in pages) available to be allocated on the + system. Linux kernel may overcommit (i.e., allocating more than the amount + of physical memory). The result should consider |overcommit_kbytes| or + |overcommit_ratio|. + """ + allowed = 0 + total_pages = totalram_pages(prog).value_() + total_swap_pages = prog["total_swap_pages"].value_() + overcommit_kbytes = prog["sysctl_overcommit_kbytes"].value_() + overcommit_ratio = prog["sysctl_overcommit_ratio"].value_() + + if overcommit_kbytes: + allowed = overcommit_kbytes >> (_PAGE_SHIFT - 10) + else: + hugetlb_pages = get_total_hugetlb_pages(prog) + allowed = (total_pages - hugetlb_pages) * overcommit_ratio // 100 + allowed += total_swap_pages + return allowed + + +def show_hugetlb_meminfo(prog: Program) -> None: + """Dump memory information for hugepages.""" + hstate = prog["hstates"][prog["default_hstate_idx"]] + nr_huge_pages = hstate.member_("nr_huge_pages").value_() + free_huge_pages = hstate.member_("free_huge_pages").value_() + resv_huge_pages = hstate.member_("resv_huge_pages").value_() + surplus_huge_pages = hstate.member_("surplus_huge_pages").value_() + huge_page_order = hstate.member_("order").value_() + huge_page_size = 1 << (huge_page_order + _PAGE_SHIFT - 10) + + print("HugePages_Total: %5d" % (nr_huge_pages)) + print("HugePages_Free: %5d" % (free_huge_pages)) + print("HugePages_Rsvd: %5d" % (resv_huge_pages)) + print("HugePages_Surp: %5d" % (surplus_huge_pages)) + print("Hugepagesize: %8d kB" % (huge_page_size)) + + if _UEK_VERSION == 6 or _UEK_VERSION == 7: + total = 0 + for h in for_each_hstate(prog): + count = h.member_("nr_huge_pages").value_() + total += (_PAGE_SIZE << huge_page_order) * count + + print("Hugetlb: %8d kB" % (total / 1024)) + + +def show_arch_meminfo(prog: Program) -> None: + """Dump numbers of pages mapped for supported page sizes.""" + if _IS_AARCH_64: + return + + direct_pages_count = prog["direct_pages_count"].read_() + + direct_4k = direct_pages_count[prog.constant("PG_LEVEL_4K")].value_() + print("DirectMap4k: %8d kB" % (direct_4k << 2)) + + if _IS_X86_64: + direct_2m = direct_pages_count[prog.constant("PG_LEVEL_2M")].value_() + print("DirectMap2M: %8d kB" % (direct_2m << 11)) + else: + direct_4m = direct_pages_count[prog.constant("PG_LEVEL_2M")].value_() + print("DirectMap4M: %8d kB" % (direct_4m << 12)) + + if prog["direct_gbpages"].value_() != 0: + direct_1g = direct_pages_count[prog.constant("PG_LEVEL_1G")].value_() + print("DirectMap1G: %8d kB" % (direct_1g << 20)) + + +def get_all_meminfo(prog: Program) -> Dict[str, int]: + """ + Collect detailed memory statistics items that match /proc/meminfo. + + :returns: A dictionary that contains relevant memory statistics items. + """ + # Parse machine info and set arch-specific parameters + parse_machine_info(prog) + + stats = {} + # Collect basic meminfo + stats["MemTotal"] = totalram_pages(prog).value_() + stats["MemFree"] = get_global_page_state(prog, "NR_FREE_PAGES") + stats["MemAvailable"] = get_total_available_pages(prog) + + file_pages = get_global_page_state(prog, "NR_FILE_PAGES") + buffer_pages = get_blockdev_pages(prog) + swapcache_pages = get_total_swapcache_pages(prog) + stats["Buffers"] = buffer_pages + stats["Cached"] = max(0, file_pages - swapcache_pages - buffer_pages) + stats["SwapCached"] = swapcache_pages + + # Collect numbers of pages in all LRU lists + lru_pages = get_lru_pages(prog) + lru_inactive_anon = lru_pages[enum_value_get(prog, "LRU_INACTIVE_ANON")] + lru_active_anon = lru_pages[enum_value_get(prog, "LRU_ACTIVE_ANON")] + lru_inactive_file = lru_pages[enum_value_get(prog, "LRU_INACTIVE_FILE")] + lru_active_file = lru_pages[enum_value_get(prog, "LRU_ACTIVE_FILE")] + lru_unevictable = lru_pages[enum_value_get(prog, "LRU_UNEVICTABLE")] + + stats["Active"] = lru_active_anon + lru_active_file + stats["Inactive"] = lru_inactive_anon + lru_inactive_file + stats["Active(anon)"] = lru_active_anon + stats["Inactive(anon)"] = lru_inactive_anon + stats["Active(file)"] = lru_active_file + stats["Inactive(file)"] = lru_inactive_file + stats["Unevictable"] = lru_unevictable + stats["Mlocked"] = get_global_page_state(prog, "NR_MLOCK") + + # Collect swap meminfo + nr_swapfiles = prog["nr_swapfiles"].value_() + nr_to_be_unused = 0 + for i in range(nr_swapfiles): + si = prog["swap_info"][i] + si_swp_used = si.member_("flags") & prog["SWP_USED"].value_() + si_swp_writeok = si.member_("flags") & prog["SWP_WRITEOK"].value_() + if si_swp_used and not si_swp_writeok: + nr_to_be_unused += si.member_("inuse_pages").value_() + + stats["SwapTotal"] = prog["total_swap_pages"].value_() + nr_to_be_unused + stats["SwapFree"] = ( + prog["nr_swap_pages"].counter.value_() + nr_to_be_unused + ) + stats["Dirty"] = get_global_page_state(prog, "NR_FILE_DIRTY") + stats["Writeback"] = get_global_page_state(prog, "NR_WRITEBACK") + stats["AnonPages"] = get_global_page_state(prog, "NR_ANON_MAPPED") + stats["Mapped"] = get_global_page_state(prog, "NR_FILE_MAPPED") + stats["Shmem"] = get_global_page_state(prog, "NR_SHMEM") + + # Collect slab meminfo + slab_reclaimable_pages = get_global_page_state(prog, "NR_SLAB_RECLAIMABLE") + slab_unreclaimable_pages = get_global_page_state( + prog, "NR_SLAB_UNRECLAIMABLE" + ) + try: + kernel_misc = get_global_page_state(prog, "NR_KERNEL_MISC_RECLAIMABLE") + stats["KReclaimable"] = slab_reclaimable_pages + kernel_misc + except LookupError: + stats["KReclaimable"] = -1 + stats["Slab"] = slab_reclaimable_pages + slab_unreclaimable_pages + stats["SReclaimable"] = slab_reclaimable_pages + stats["SUnreclaim"] = slab_unreclaimable_pages + + stats["KernelStack"] = get_global_page_state(prog, "NR_KERNEL_STACK_KB") + stats["PageTables"] = get_global_page_state(prog, "NR_PAGETABLE") + stats["NFS_Unstable"] = 0 + if "NFS_Unstable" in prog: + stats["NFS_Unstable"] = get_global_page_state(prog, "NR_UNSTABLE_NFS") + stats["Bounce"] = get_global_page_state(prog, "NR_BOUNCE") + stats["WritebackTmp"] = get_global_page_state(prog, "NR_WRITEBACK_TEMP") + + stats["CommitLimit"] = get_vm_commit_limit(prog) + if _UEK_VERSION == 5 or _UEK_VERSION == 6: + committed_as = prog["vm_committed_as"].member_("count").value_() + else: + committed_as = percpu_counter_sum(prog["vm_committed_as"]) + stats["Committed_AS"] = max(0, committed_as) + stats["VmallocTotal"] = (_VMALLOC_END - _VMALLOC_START) >> 10 + stats["VmallocUsed"] = 0 + if "nr_vmalloc_pages" in prog: + stats["VmallocUsed"] = prog["nr_vmalloc_pages"].counter.value_() + stats["VmallocChunk"] = 0 + + try: + pcpu_nr_populated = prog["pcpu_nr_populated"].value_() + pcpu_nr_units = prog["pcpu_nr_units"].value_() + stats["Percpu"] = pcpu_nr_populated * pcpu_nr_units + except LookupError: + stats["Percpu"] = -1 + stats["HardwareCorrupted"] = prog[ + "num_poisoned_pages" + ].counter.value_() << (_PAGE_SHIFT - 10) + stats["AnonHugePages"] = get_global_page_state(prog, "NR_ANON_THPS") + stats["ShmemHugePages"] = get_global_page_state(prog, "NR_SHMEM_THPS") + stats["ShmemPmdMapped"] = get_global_page_state(prog, "NR_SHMEM_PMDMAPPED") + try: + stats["FileHugePages"] = get_global_page_state(prog, "NR_FILE_THPS") + except LookupError: + stats["FileHugePages"] = -1 + try: + stats["FilePmdMapped"] = get_global_page_state( + prog, "NR_FILE_PMDMAPPED" + ) + except LookupError: + stats["FilePmdMapped"] = -1 + + stats["CmaTotal"] = prog["totalcma_pages"].value_() + stats["CmaFree"] = get_global_page_state(prog, "NR_FREE_CMA_PAGES") + return stats + + +def show_all_meminfo(prog: Program) -> None: + """ + Dump various details about the memory subsystem. + This function must parse machine info to determine arch-specific parameters + before parsing all memory statistics. + """ + stats = get_all_meminfo(prog) + + # Ignore highmem stats as x86-64 and aarch64 do not have no highmem zones. + basic_meminfo_items = [ + "MemTotal", + "MemFree", + "MemAvailable", + "Buffers", + "Cached", + "SwapCached", + "Active", + "Inactive", + "Active(anon)", + "Inactive(anon)", + "Active(file)", + "Inactive(file)", + "Unevictable", + "Mlocked", + "SwapTotal", + "SwapFree", + "Dirty", + "Writeback", + "AnonPages", + "Mapped", + "Shmem", + "KReclaimable", + "Slab", + "SReclaimable", + "SUnreclaim", + "KernelStack", + "PageTables", + "NFS_Unstable", + "Bounce", + "WritebackTmp", + "CommitLimit", + "Committed_AS", + "VmallocTotal", + "VmallocUsed", + "VmallocChunk", + "Percpu", + "HardwareCorrupted", + ] + hugepage_meminfo_items = [ + "AnonHugePages", + "ShmemHugePages", + "ShmemPmdMapped", + "FileHugePages", + "FilePmdMapped", + ] + cma_meminfo_items = ["CmaTotal", "CmaFree"] + + # Output + for item in basic_meminfo_items: + if item in ["KernelStack", "VmallocTotal", "HardwareCorrupted"]: + # These statistics items are in kB. + if item == "HardwareCorrupted": + print("HardwareCorrupted: %5d kB" % (stats[item])) + else: + print( + "%s%s kB" + % ( + (item + ":").ljust(16, " "), + str(stats[item]).rjust(8, " "), + ) + ) + else: + print_val_kb(item, stats[item]) + + for item in hugepage_meminfo_items + cma_meminfo_items: + if _UEK_VERSION != 7: + print_val_kb(item, stats[item] * _HPAGE_PMD_NR) + else: + print_val_kb(item, stats[item]) + + # Report hugepage related meminfo + show_hugetlb_meminfo(prog) + + # Report tlb related meminfo + show_arch_meminfo(prog) + + +class MeminfoModule(CorelensModule): + """Show various details about the memory management subsystem""" + + name = "mem" + + def run(self, prog: Program, args: argparse.Namespace) -> None: + show_all_meminfo(prog) diff --git a/drgn_tools/util.py b/drgn_tools/util.py index 419851f1..80d6df56 100644 --- a/drgn_tools/util.py +++ b/drgn_tools/util.py @@ -123,6 +123,23 @@ def enum_name_get( return default +def enum_value_get(prog: Program, key: str, offset: int = 0) -> int: + """ + Get the corresponding index of a enum item. + If |offset| is not 0, then return the final index as the |key|'s index + |offset|. + + :param prog: drgn program + :param key: The target enum item + :param offset: The target enum item's offset + :returns: The target enum item's index + """ + try: + enum_item = prog.constant(key) + except LookupError: + raise IndexError("UNKNOWN enum %s" % (key)) + return enum_item.value_() + offset + + def enum_flags_str(prog: Program, t: str, flags: int) -> str: """ Convert enum flag bit to string diff --git a/tests/test_meminfo.py b/tests/test_meminfo.py new file mode 100644 index 00000000..b4afa0ec --- /dev/null +++ b/tests/test_meminfo.py @@ -0,0 +1,39 @@ +# Copyright (c) 2023, Oracle and/or its affiliates. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ +from drgn import ProgramFlags + +from drgn_tools import meminfo + + +def test_meminfo(prog): + meminfo.show_all_meminfo(prog) + + if not (ProgramFlags.IS_LIVE & prog.flags): + return + + corelens_mm_stats = meminfo.get_all_meminfo(prog) + proc_mm_stats = {} + f = open("/proc/meminfo", "r") + lines = f.readlines() + for line in lines: + try: + title, value = line.split(":") + title, value = title.strip(), value.strip() + proc_mm_stats[title] = value + except Exception: + continue + + test_exact_match_mm_stats = [ + "MemTotal", + "SwapTotal", + "CommitLimit", + "VmallocTotal", + "CmaTotal", + "Hugepagesize", + ] + + for item in test_exact_match_mm_stats: + if item not in proc_mm_stats: + assert item not in corelens_mm_stats + else: + assert corelens_mm_stats[item] == proc_mm_stats[item]