diff --git a/OOMAnalyser.html b/OOMAnalyser.html index 3b05547..e7f0aa6 100644 --- a/OOMAnalyser.html +++ b/OOMAnalyser.html @@ -427,8 +427,11 @@ window.onerror = function (msg, url, lineNo, columnNo, errorObj) { Memory allocation flags
(gfp_mask) - These flags are used to control the kernel internal memory allocation
- GFP stands for __get_free_pages(). + These flags are also called GFP (get free pages) flags. They control the kernel-internal memory + allocation.
+ Some OOM variants include the individual flags in addition to the hexadecimal representation. The flags + are calculated if they are not specified. + Node mask to show on which CPU Cores this process can run
(nodemask) @@ -936,6 +939,7 @@ window.onerror = function (msg, url, lineNo, columnNo, errorObj) {
  1. Fix memory calculation in summary section
  2. Fix calculation of GFP flags
  3. +
  4. Rework handling of GFP flags
  5. ...
diff --git a/OOMAnalyser.py b/OOMAnalyser.py index 0b51663..dd11848 100644 --- a/OOMAnalyser.py +++ b/OOMAnalyser.py @@ -377,6 +377,18 @@ class BaseKernelConfig: (see https://github.com/torvalds/linux/commit/e67d4ca79aaf9d13a00d229b1b1c96b86828e8ba#diff-020720d0699e3ae1afb6fcd815ca8500) """ + gfp_reverse_lookup = [] + """ + Sorted list of flags used to do a reverse lookup. + + This list doesn't contain all flags. It contains the "useful flags" (GFP_*) as + well as "modifier flags" (__GFP_*). "Plain flags" (___GFP_*) are not part of + this list. + + @type: List(str) + @see: _gfp_create_reverse_lookup() + """ + pstable_items = [ "pid", "uid", @@ -445,6 +457,103 @@ class BaseKernelConfig: if self.EXTRACT_PATTERN_OVERLAY: self.EXTRACT_PATTERN.update(self.EXTRACT_PATTERN_OVERLAY) + self._gfp_calc_all_values() + self.gfp_reverse_lookup = self._gfp_create_reverse_lookup() + + def _gfp_calc_all_values(self): + """ + Calculate decimal values for all GFP flags and store in in GFP_FLAGS[]["_value"] + """ + # __pragma__ ('jsiter') + for flag in self.GFP_FLAGS: + value = self._gfp_flag2decimal(flag) + self.GFP_FLAGS[flag]["_value"] = value + # __pragma__ ('nojsiter') + + def _gfp_flag2decimal(self, flag): + """\ + Convert a single flag into a decimal value. + + The flags can be concatenated with "|" or "~" and negated with "~". The + flags will be processed from left to right. Parentheses are not supported. + """ + if flag not in self.GFP_FLAGS: + error("No definition for flag {} found".format(flag)) + return 0 + + value = self.GFP_FLAGS[flag]["value"] + if isinstance(value, int): + return value + + tokenlist = iter(re.split("([|&])", value)) + operator = "|" # set to process first flag + negate_rvalue = False + lvalue = 0 + while True: + try: + token = next(tokenlist) + except StopIteration: + break + token = token.strip() + if token in ["|", "&"]: + operator = token + continue + + if token.startswith("~"): + token = token[1:] + negate_rvalue = True + + if token.isdigit(): + rvalue = int(token) + elif token.startswith("0x") and token[2:].isdigit(): + rvalue = int(token, 16) + else: + # it's not a decimal nor a hexadecimal value - reiterate assuming it's a flag string + rvalue = self._gfp_flag2decimal(token) + + if negate_rvalue: + rvalue = ~rvalue + + if operator == "|": + lvalue |= rvalue + elif operator == "&": + lvalue &= rvalue + + operator = None + negate_rvalue = False + + return lvalue + + def _gfp_create_reverse_lookup(self): + """ + Create a sorted list of flags used to do a reverse lookup from value to the flag. + + @rtype: List(str) + """ + # __pragma__ ('jsiter') + useful = [ + key + for key in self.GFP_FLAGS + if key.startswith("GFP") and self.GFP_FLAGS[key]["_value"] != 0 + ] + useful = sorted( + useful, key=lambda key: self.GFP_FLAGS[key]["_value"], reverse=True + ) + modifier = [ + key + for key in self.GFP_FLAGS + if key.startswith("__GFP") and self.GFP_FLAGS[key]["_value"] != 0 + ] + modifier = sorted( + modifier, key=lambda key: self.GFP_FLAGS[key]["_value"], reverse=True + ) + # __pragma__ ('nojsiter') + + # useful + modifier produces a string with all values concatenated + res = useful + res.extend(modifier) + return res + class KernelConfig_4_6(BaseKernelConfig): # Support changes: @@ -1029,9 +1138,8 @@ class OOMAnalyser: flags = self.oom_result.details["trigger_proc_gfp_flags"] del self.oom_result.details["trigger_proc_gfp_flags"] else: - flags, unknown = self._hex2flags( + flags, unknown = self._gfp_hex2flags( self.oom_result.details["trigger_proc_gfp_mask"], - self.oom_result.kconfig.GFP_FLAGS, ) if unknown: flags.append("0x{0:x}".format(unknown)) @@ -1101,77 +1209,26 @@ class OOMAnalyser: self.oom_result.details["_pstable"][pid] = {} self.oom_result.details["_pstable"][pid].update(details) - def _hex2flags(self, hexvalue, flag_definition): + def _gfp_hex2flags(self, hexvalue): """\ Convert the hexadecimal value into flags specified by definition - @return: list of flags and the decimal sum of all unknown flags + @return: Unsorted list of flags and the sum of all unknown flags as integer + @rtype: List(str), int """ remaining = int(hexvalue, 16) converted_flags = [] - # __pragma__ ('jsiter') - for flag in flag_definition: - value = self._flag2decimal(flag, flag_definition) + for flag in self.oom_result.kconfig.gfp_reverse_lookup: + value = self.oom_result.kconfig.GFP_FLAGS[flag]["_value"] if (remaining & value) == value: # delete flag by "and" with a reverted mask remaining &= ~value converted_flags.append(flag) - # __pragma__ ('nojsiter') + converted_flags.sort() return converted_flags, remaining - def _flag2decimal(self, flag, flag_definition): - """\ - Convert a single flag into a decimal value - """ - if flag not in flag_definition: - error("No definition for flag {} found".format(flag)) - return 0 - - value = flag_definition[flag]["value"] - if isinstance(value, int): - return value - - tokenlist = iter(re.split("([|&])", value)) - operator = "|" # set to process first flag - negate_rvalue = False - lvalue = 0 - while True: - try: - token = next(tokenlist) - except StopIteration: - break - token = token.strip() - if token in ["|", "&"]: - operator = token - continue - - if token.startswith("~"): - token = token[1:] - negate_rvalue = True - - if token.isdigit(): - rvalue = int(token) - elif token.startswith("0x") and token[2:].isdigit(): - rvalue = int(token, 16) - else: - # it's not a decimal nor a hexadecimal value - reiterate assuming it's a flag string - rvalue = self._flag2decimal(token, flag_definition) - - if negate_rvalue: - rvalue = ~rvalue - - if operator == "|": - lvalue |= rvalue - elif operator == "&": - lvalue &= rvalue - - operator = None - negate_rvalue = False - - return lvalue - def _convert_numeric_results_to_integer(self): """Convert all *_pages and *_kb to integer""" # __pragma__ ('jsiter') diff --git a/test.py b/test.py index 30d1667..37b5c41 100755 --- a/test.py +++ b/test.py @@ -189,18 +189,20 @@ class TestInBrowser(TestBase): trigger_proc_gfp_mask = self.driver.find_element( By.CLASS_NAME, "trigger_proc_gfp_mask" ) - # 0x201da: - # __GFP_HIGHMEM 2 0x02 - # __GFP_MOVABLE 8 0x08 - # __GFP_RECLAIMABLE 16 0x10 - # __GFP_IO 64 0x40 - # __GFP_FS 128 0x80 - # __GFP_COLD 256 0x100 - # __GFP_HARDWALL 131072 0x20000 - # 0x201da + # 0x201da will split into + # GFP_HIGHUSER_MOVABLE 0x200da + # (__GFP_WAIT | __GFP_IO | __GFP_FS | __GFP_HARDWALL | __GFP_HIGHMEM | __GFP_MOVABLE) + # __GFP_WAIT 0x10 + # __GFP_IO 0x40 + # __GFP_FS 0x80 + # __GFP_HARDWALL 0x20000 + # __GFP_HIGHMEM 0x02 + # __GFP_MOVABLE 0x08 + # __GFP_COLD 0x100 + # sum: 0x201da self.assertEqual( trigger_proc_gfp_mask.text, - "0x201da (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_RECLAIMABLE | __GFP_IO | __GFP_FS | __GFP_COLD | __GFP_HARDWALL)", + "0x201da (GFP_HIGHUSER_MOVABLE | __GFP_COLD)", "Unexpected GFP Mask", ) @@ -257,6 +259,14 @@ class TestInBrowser(TestBase): trigger_proc_gfp_mask = self.driver.find_element( By.CLASS_NAME, "trigger_proc_gfp_mask" ) + # 0xcc0 will split into + # GFP_KERNEL (__GFP_RECLAIM | __GFP_IO | __GFP_FS) + # __GFP_RECLAIM (___GFP_DIRECT_RECLAIM | ___GFP_KSWAPD_RECLAIM) + # ___GFP_DIRECT_RECLAIM 0x400 + # ___GFP_KSWAPD_RECLAIM 0x800 + # __GFP_IO 0x40 + # __GFP_FS 0x80 + # sum: 0xCC0 self.assertEqual( trigger_proc_gfp_mask.text, "0xcc0 (GFP_KERNEL)", "Unexpected GFP Mask" ) @@ -653,19 +663,19 @@ Hardware name: HP ProLiant DL385 G7, BIOS A18 12/08/2012 "CPU: 4 PID: 29481 Comm: sed Not tainted 4.6.0-514 #1", ), ( - OOMAnalyser.KernelConfigRhel7(), + OOMAnalyser.KernelConfig_3_10_EL7(), "CPU: 4 PID: 29481 Comm: sed Not tainted 3.10.0-1062.9.1.el7.x86_64 #1", ), ( - OOMAnalyser.KernelConfig_5_0(), + OOMAnalyser.KernelConfig_5_1(), "CPU: 4 PID: 29481 Comm: sed Not tainted 5.5.1 #1", ), ( - OOMAnalyser.KernelConfig_5_8(), + OOMAnalyser.KernelConfig_5_18(), "CPU: 4 PID: 29481 Comm: sed Not tainted 5.23.0 #1", ), ( - OOMAnalyser.KernelConfig_5_8(), + OOMAnalyser.KernelConfig_5_18(), "CPU: 4 PID: 29481 Comm: sed Not tainted 6.12.0 #1", ), ( @@ -685,6 +695,142 @@ Hardware name: HP ProLiant DL385 G7, BIOS A18 12/08/2012 % (type(kcfg), type(result), kversion), ) + def test_007_gfp_processing(self): + """Test processing GFP flags""" + oom = OOMAnalyser.OOMEntity(OOMAnalyser.OOMDisplay.example_rhel7) + analyser = OOMAnalyser.OOMAnalyser(oom) + success = analyser.analyse() + self.assertTrue(success, "OOM analysis failed") + + self.assertEqual( + analyser.oom_result.kconfig.release, + (3, 10, ".el7."), + "Wrong KernelConfig release", + ) + + for flag, hex_value in [ + ("__GFP_DMA", 0x01), + ("__GFP_WAIT", 0x10), + ("__GFP_IO", 0x40), + ("__GFP_FS", 0x80), + ("GFP_KERNEL", 0xD0), # __GFP_WAIT | __GFP_IO | __GFP_FS + ("__GFP_UNKNOWN", 0x00), # unknown GFP flag + ]: + self.assertEqual( + analyser.oom_result.kconfig._gfp_flag2decimal(flag), + hex_value, + "Invalid decimal value for %s" % flag, + ) + + for hex_value, flags_expected, unknown_expected in [ + ( + "0x01", + ["__GFP_DMA"], + 0, + ), + ("0x05", ["__GFP_DMA", "__GFP_DMA32"], 0), + ( + "0x5000000", # 0x1000000 + 0x4000000 + ["__GFP_WRITE"], + 0x4000000, + ), + ("0x201da", ["GFP_HIGHUSER_MOVABLE", "__GFP_COLD"], 0), + ]: + flags_calculated, unknown_calculated = analyser._gfp_hex2flags(hex_value) + self.assertEqual( + flags_calculated, + flags_expected, + "Invalid flag(s) for hex value %s" % hex_value, + ) + self.assertEqual( + unknown_calculated, + unknown_expected, + "Invalid remaining / not resolved decimal flag value", + ) + + oom = OOMAnalyser.OOMEntity(OOMAnalyser.OOMDisplay.example_ubuntu2110) + analyser = OOMAnalyser.OOMAnalyser(oom) + success = analyser.analyse() + self.assertTrue(success, "OOM analysis failed") + + self.assertEqual( + analyser.oom_result.kconfig.release, + (5, 8, ""), + "Wrong KernelConfig release", + ) + + for flag, hex_value in [ + ("__GFP_DMA", 0x01), + ("__GFP_IO", 0x40), + ("__GFP_FS", 0x80), + ( + "GFP_KERNEL", + 0xCC0, + ), # (__GFP_RECLAIM (___GFP_DIRECT_RECLAIM | ___GFP_KSWAPD_RECLAIM) | __GFP_IO | __GFP_FS) + ("__GFP_UNKNOWN", 0x00), # unknown GFP flag + ]: + self.assertEqual( + analyser.oom_result.kconfig._gfp_flag2decimal(flag), + hex_value, + "Invalid decimal value for %s" % flag, + ) + + for hex_value, flags_expected, unknown_expected in [ + ( + "0x01", + ["__GFP_DMA"], + 0, + ), + ("0x05", ["__GFP_DMA", "__GFP_DMA32"], 0), + ( + "0x4001000", # 0x1000 + 0x4000000 + ["__GFP_WRITE"], + 0x4000000, + ), + ]: + flags_calculated, unknown_calculated = analyser._gfp_hex2flags(hex_value) + self.assertEqual( + flags_calculated, + flags_expected, + "Invalid flag(s) for hex value %s" % hex_value, + ) + self.assertEqual( + unknown_calculated, + unknown_expected, + "Invalid remaining / not resolved decimal flag value", + ) + + def test_008_kversion_check(self): + """Test check for minimum kernel version""" + oom = OOMAnalyser.OOMEntity(OOMAnalyser.OOMDisplay.example_rhel7) + analyser = OOMAnalyser.OOMAnalyser(oom) + + for kversion, min_version, expected_result in ( + ("5.19-rc6", (5, 16, ""), True), + ("5.19-rc6", (5, 19, ""), True), + ("5.19-rc6", (5, 20, ""), False), + ("5.18.6-arch1-1", (5, 18, ""), True), + ("5.18.6-arch1-1", (5, 1, ""), True), + ("5.18.6-arch1-1", (5, 19, ""), False), + ("5.13.0-1028-aws #31~20.04.1-Ubuntu", (5, 14, ""), False), + ("5.13.0-1028-aws #31~20.04.1-Ubuntu", (5, 13, ""), True), + ("5.13.0-1028-aws #31~20.04.1-Ubuntu", (5, 13, "-aws"), True), + ("5.13.0-1028-aws #31~20.04.1-Ubuntu", (5, 13, "not_in_version"), False), + ("5.13.0-1028-aws #31~20.04.1-Ubuntu", (5, 12, ""), True), + ("4.14.288", (5, 0, ""), False), + ("4.14.288", (4, 14, ""), True), + ("3.10.0-514.6.1.el7.x86_64 #1", (3, 11, ""), False), + ("3.10.0-514.6.1.el7.x86_64 #1", (3, 10, ".el7."), True), + ("3.10.0-514.6.1.el7.x86_64 #1", (3, 10, ""), True), + ("3.10.0-514.6.1.el7.x86_64 #1", (3, 9, ""), True), + ): + self.assertEqual( + analyser._check_kversion_greater_equal(kversion, min_version), + expected_result, + 'Failed to compare kernel version "%s" with minimum version "%s"' + % (kversion, min_version), + ) + if __name__ == "__main__": unittest.main(verbosity=2)