diff --git a/qubes/qmemman/algo.py b/qubes/qmemman/algo.py index 685a30eb0..4a86a7e72 100644 --- a/qubes/qmemman/algo.py +++ b/qubes/qmemman/algo.py @@ -1,5 +1,3 @@ -# pylint: skip-file - # # The Qubes OS Project, http://www.qubes-os.org # @@ -21,316 +19,271 @@ # import logging -import string +from typing import Optional -# This are only defaults - can be overridden by QMemmanServer with values from -# config file +# These defaults can be overridden by QMemmanServer with values from config +# file. CACHE_FACTOR = 1.3 MIN_PREFMEM = 200 * 1024 * 1024 DOM0_MEM_BOOST = 350 * 1024 * 1024 +# REQ_SAFETY_NET_FACTOR is a bit greater that 1. So that if the domain +# yields a bit less than requested, due to e.g. rounding errors, we will not +# get stuck. The surplus will return to the VM during "balance" call. +REQ_SAFETY_NET_FACTOR = 1.05 log = logging.getLogger("qmemman.daemon.algo") -# untrusted meminfo size is taken from xenstore key, thus its size is limited -# so splits do not require excessive memory -def sanitize_and_parse_meminfo(untrusted_meminfo): +def sanitize_and_parse_meminfo(untrusted_meminfo) -> Optional[int]: + # Untrusted meminfo size is read from xenstore, thus its size is limited + # and splits do not require excessive memory. if not untrusted_meminfo: return None - - # new syntax - just one int - if untrusted_meminfo.isdigit(): - return int(untrusted_meminfo) * 1024 - - return None + if not untrusted_meminfo.isdigit(): + return None + return int(untrusted_meminfo) * 1024 -# called when a domain updates its 'meminfo' xenstore key -def refresh_meminfo_for_domain(domain, untrusted_xenstore_key): - domain.mem_used = sanitize_and_parse_meminfo(untrusted_xenstore_key) +def refresh_meminfo_for_domain(dom, untrusted_xenstore_key) -> None: + """ + Called when a domain updates its 'meminfo' xenstore key. + """ + dom.mem_used = sanitize_and_parse_meminfo(untrusted_xenstore_key) -def prefmem(domain): - # dom0 is special, as it must have large cache, for vbds. Thus, give it - # a special boost - if domain.id == "0": - return int( - min( - domain.mem_used * CACHE_FACTOR + DOM0_MEM_BOOST, - domain.memory_maximum, - ) - ) - return int( - max( - min(domain.mem_used * CACHE_FACTOR, domain.memory_maximum), - MIN_PREFMEM, - ) - ) +def pref_mem(dom) -> int: + # As dom0 must have large cache for vbds, give it a special boost. + mem_used = dom.mem_used * CACHE_FACTOR + if dom.domid == "0": + mem_used += DOM0_MEM_BOOST + return int(min(mem_used, dom.mem_max)) + return int(max(min(mem_used, dom.mem_max), MIN_PREFMEM)) -def memory_needed(domain): - # do not change - # in balance(), "distribute total_available_memory proportionally to - # mempref" relies on this exact formula - ret = prefmem(domain) - domain.memory_actual +def needed_mem(dom) -> int: + # Do not change. In balance(), "distribute total_available_mem + # proportionally to pref_mem" relies on this exact formula. + ret = pref_mem(dom) - dom.mem_actual return ret -# prepare list of (domain, memory_target) pairs that need to be passed -# to "xm memset" equivalent in order to obtain "memsize" of memory -# return empty list when the request cannot be satisfied -def balloon(memsize, domain_dictionary): +# Prepare list of (dom, mem_target) pairs that need to be passed to "xm +# memset" equivalent in order to obtain "mem_size". +# Returns empty list when the request cannot be satisfied. +def balloon(mem_size, dom_dict) -> list: log.debug( - "balloon(memsize={!r}, domain_dictionary={!r})".format( - memsize, domain_dictionary - ) + "balloon(mem_size={!r}, dom_dict={!r})".format(mem_size, dom_dict) ) - REQ_SAFETY_NET_FACTOR = 1.05 - donors = list() - request = list() + donors = [] + request = [] available = 0 - for i in domain_dictionary.keys(): - if domain_dictionary[i].mem_used is None: - continue - if domain_dictionary[i].no_progress: + for domid, dom in dom_dict.items(): + if dom.mem_used is None or dom.no_progress: continue - need = memory_needed(domain_dictionary[i]) + need = needed_mem(dom) if need < 0: log.info( "balloon: dom {} has actual memory {}".format( - i, domain_dictionary[i].memory_actual + domid, dom.mem_actual ) ) - donors.append((i, -need)) + donors.append((domid, -need)) available -= need - log.info("req={} avail={} donors={!r}".format(memsize, available, donors)) + log.info("req={} avail={} donors={!r}".format(mem_size, available, donors)) - if available < memsize: + if available < mem_size: return [] - scale = 1.0 * memsize / available + scale = 1.0 * mem_size / available for donors_iter in donors: - dom_id, mem = donors_iter - memborrowed = mem * scale * REQ_SAFETY_NET_FACTOR - log.info("borrow {} from {}".format(memborrowed, dom_id)) - memtarget = int(domain_dictionary[dom_id].memory_actual - memborrowed) - request.append((dom_id, memtarget)) + domid, mem = donors_iter + mem_borrowed = mem * scale * REQ_SAFETY_NET_FACTOR + log.info("borrow {} from {}".format(mem_borrowed, domid)) + mem_target = int(dom_dict[domid].mem_actual - mem_borrowed) + request.append((domid, mem_target)) return request -# REQ_SAFETY_NET_FACTOR is a bit greater that 1. So that if the domain -# yields a bit less than requested, due to e.g. rounding errors, we will not -# get stuck. The surplus will return to the VM during "balance" call. - - -# redistribute positive "total_available_memory" of memory between domains, -# proportionally to prefmem -def balance_when_enough_memory( - domain_dictionary, xen_free_memory, total_mem_pref, total_available_memory +# Redistribute positive "total_available_mem" of memory between domains, +# proportionally to pref_mem. +def balance_when_enough_mem( + dom_dict, xen_free_mem, total_mem_pref, total_available_mem ): log.info( - "balance_when_enough_memory(xen_free_memory={!r}, " - "total_mem_pref={!r}, total_available_memory={!r})".format( - xen_free_memory, total_mem_pref, total_available_memory + "balance_when_enough_mem(xen_free_mem={!r}, " + "total_mem_pref={!r}, total_available_mem={!r})".format( + xen_free_mem, total_mem_pref, total_available_mem ) ) - target_memory = {} - # memory not assigned because of static max - left_memory = 0 + target_mem = {} + # Memory not assigned because of static max. + mem_left = 0 acceptors_count = 0 - for i in domain_dictionary.keys(): - if domain_dictionary[i].mem_used is None: + for domid, dom in dom_dict.items(): + if dom.mem_used is None or dom.no_progress: continue - if domain_dictionary[i].no_progress: - continue - # distribute total_available_memory proportionally to mempref - scale = 1.0 * prefmem(domain_dictionary[i]) / total_mem_pref - target_nonint = ( - prefmem(domain_dictionary[i]) + scale * total_available_memory - ) - # prevent rounding errors + # Distribute total_available_mem proportionally to pref_mem. + scale = 1.0 * pref_mem(dom) / total_mem_pref + target_nonint = pref_mem(dom) + scale * total_available_mem + # Prevent rounding errors. target = int(0.999 * target_nonint) - # do not try to give more memory than static max - if target > domain_dictionary[i].memory_maximum: - left_memory += target - domain_dictionary[i].memory_maximum - target = domain_dictionary[i].memory_maximum + # Do not try to give more memory than static max. + if target > dom.mem_max: + mem_left += target - dom.mem_max + target = dom.mem_max else: - # count domains which can accept more memory + # Count domains which can accept more memory. acceptors_count += 1 - target_memory[i] = target - # distribute left memory across all acceptors - while left_memory > 0 and acceptors_count > 0: + target_mem[domid] = target + # Distribute left memory across all acceptors. + while mem_left > 0 and acceptors_count > 0: log.info( - "left_memory={} acceptors_count={}".format( - left_memory, acceptors_count - ) + "mem_left={} acceptors_count={}".format(mem_left, acceptors_count) ) - new_left_memory = 0 + new_mem_left = 0 new_acceptors_count = acceptors_count - for i in target_memory.keys(): - target = target_memory[i] - if target < domain_dictionary[i].memory_maximum: - memory_bonus = int(0.999 * (left_memory / acceptors_count)) - if target + memory_bonus >= domain_dictionary[i].memory_maximum: - new_left_memory += ( - target - + memory_bonus - - domain_dictionary[i].memory_maximum - ) - target = domain_dictionary[i].memory_maximum + for domid, target in target_mem.items(): + dom = dom_dict[domid] + if target < dom.mem_max: + mem_bonus = int(0.999 * (mem_left / acceptors_count)) + if target + mem_bonus >= dom.mem_max: + new_mem_left += target + mem_bonus - dom.mem_max + target = dom.mem_max new_acceptors_count -= 1 else: - target += memory_bonus - target_memory[i] = target - left_memory = new_left_memory + target += mem_bonus + target_mem[domid] = target + mem_left = new_mem_left acceptors_count = new_acceptors_count - # split target_memory dictionary to donors and acceptors - # this is needed to first get memory from donors and only then give it - # to acceptors - donors_rq = list() - acceptors_rq = list() - for i in target_memory.keys(): - target = target_memory[i] - if target < domain_dictionary[i].memory_actual: - donors_rq.append((i, target)) + # Split target_mem dictionary to donors and acceptors. This is needed to + # first get memory from donors and only then give it to acceptors. + donors_rq = [] + acceptors_rq = [] + for domid, target in target_mem.items(): + dom = dom_dict[domid] + if target < dom.mem_actual: + donors_rq.append((domid, target)) else: - acceptors_rq.append((i, target)) - - # print 'balance(enough): xen_free_memory=', xen_free_memory, \ - # 'requests:', donors_rq + acceptors_rq + acceptors_rq.append((domid, target)) return donors_rq + acceptors_rq -# when not enough mem to make everyone be above prefmem, make donors be at -# prefmem, and redistribute anything left between acceptors -def balance_when_low_on_memory( - domain_dictionary, - xen_free_memory, +# When not enough mem to make everyone be above pref_mem, make donors be at +# pref_mem, and redistribute anything left between acceptors. +def balance_when_low_on_mem( + dom_dict, + xen_free_mem, total_mem_pref_acceptors, donors, acceptors, ): log.info( - "balance_when_low_on_memory(xen_free_memory={!r}, " + "balance_when_low_on_mem(xen_free_mem={!r}, " "total_mem_pref_acceptors={!r}, donors={!r}, acceptors={!r})".format( - xen_free_memory, total_mem_pref_acceptors, donors, acceptors + xen_free_mem, total_mem_pref_acceptors, donors, acceptors ) ) - donors_rq = list() - acceptors_rq = list() - squeezed_mem = xen_free_memory - for i in donors: - avail = -memory_needed(domain_dictionary[i]) + donors_rq = [] + acceptors_rq = [] + squeezed_mem = xen_free_mem + for domid in donors: + dom = dom_dict[domid] + avail = -needed_mem(dom) if avail < 10 * 1024 * 1024: - # probably we have already tried making it exactly at prefmem, - # give up + # Probably we have already tried making it exactly at pref_mem, give + # up. continue squeezed_mem -= avail - donors_rq.append((i, prefmem(domain_dictionary[i]))) - # the below can happen if initially xen free memory is below 50M + donors_rq.append((domid, pref_mem(dom))) + # The below condition can happen if initially xen free memory is below 50M. if squeezed_mem < 0: return donors_rq - for i in acceptors: - scale = 1.0 * prefmem(domain_dictionary[i]) / total_mem_pref_acceptors - target_nonint = ( - domain_dictionary[i].memory_actual + scale * squeezed_mem - ) - # do not try to give more memory than static max - target = min( - int(0.999 * target_nonint), domain_dictionary[i].memory_maximum - ) - acceptors_rq.append((i, target)) - # print 'balance(low): xen_free_memory=', xen_free_memory, 'requests:', - # donors_rq + acceptors_rq + for domid in acceptors: + dom = dom_dict[domid] + scale = 1.0 * pref_mem(dom) / total_mem_pref_acceptors + target_nonint = dom.mem_actual + scale * squeezed_mem + # Do not try to give more memory than static max. + target = min(int(0.999 * target_nonint), dom.mem_max) + acceptors_rq.append((domid, target)) return donors_rq + acceptors_rq -# get memory information -# called before and after domain balances -# return a dictionary of various memory data points -def memory_info(xen_free_memory, domain_dictionary): +# Get memory information. +# Called before and after domain balances. +# Return a dictionary of various memory data points. +def mem_info(xen_free_mem, dom_dict) -> dict: log.debug( - "memory_info(xen_free_memory={!r}, domain_dictionary={!r})".format( - xen_free_memory, domain_dictionary + "mem_info(xen_free_mem={!r}, dom_dict={!r})".format( + xen_free_mem, dom_dict ) ) - # sum of all memory requirements - in other words, the difference between - # memory required to be added to domains (acceptors) to make them be - # at their preferred memory, and memory that can be taken from domains - # (donors) that can provide memory. So, it can be negative when plenty - # of memory. - total_memory_needed = 0 + # Sum of all memory requirements - in other words, the difference between + # memory required to be added to domains (acceptors) to make them be at + # their preferred memory, and memory that can be taken from domains + # (donors) that can provide memory. So, it can be negative when plenty of + # memory. + total_needed_mem = 0 - # sum of memory preferences of all domains + # Sum of memory preferences of all domains. total_mem_pref = 0 - # sum of memory preferences of all domains that require more memory + # Sum of memory preferences of all domains that require more memory. total_mem_pref_acceptors = 0 - donors = list() # domains that can yield memory - acceptors = list() # domains that require more memory - # pass 1: compute the above "total" values - for i in domain_dictionary.keys(): - if domain_dictionary[i].mem_used is None: + donors = [] + acceptors = [] + # Pass 1: compute the above "total" values. + for domid, dom in dom_dict.items(): + if dom.mem_used is None or dom.no_progress: continue - if domain_dictionary[i].no_progress: - continue - need = memory_needed(domain_dictionary[i]) - # print 'domain' , i, 'act/pref', \ - # domain_dictionary[i].memory_actual, prefmem(domain_dictionary[i]), \ - # 'need=', need - if ( - need < 0 - or domain_dictionary[i].memory_actual - >= domain_dictionary[i].memory_maximum - ): - donors.append(i) + need = needed_mem(dom) + if need < 0 or dom.mem_actual >= dom.mem_max: + donors.append(domid) else: - acceptors.append(i) - total_mem_pref_acceptors += prefmem(domain_dictionary[i]) - total_memory_needed += need - total_mem_pref += prefmem(domain_dictionary[i]) - - total_available_memory = xen_free_memory - total_memory_needed - - mem_dictionary = {} - mem_dictionary["domain_dictionary"] = domain_dictionary - mem_dictionary["total_available_memory"] = total_available_memory - mem_dictionary["xen_free_memory"] = xen_free_memory - mem_dictionary["total_mem_pref"] = total_mem_pref - mem_dictionary["total_mem_pref_acceptors"] = total_mem_pref_acceptors - mem_dictionary["donors"] = donors - mem_dictionary["acceptors"] = acceptors - return mem_dictionary - - -# redistribute memory across domains -# called when one of domains update its 'meminfo' xenstore key -# return the list of (domain, memory_target) pairs to be passed to -# "xm memset" equivalent -def balance(xen_free_memory, domain_dictionary): + acceptors.append(domid) + total_mem_pref_acceptors += pref_mem(dom) + total_needed_mem += need + total_mem_pref += pref_mem(dom) + + total_available_mem = xen_free_mem - total_needed_mem + + mem_dict = {} + mem_dict["dom_dict"] = dom_dict + mem_dict["total_available_mem"] = total_available_mem + mem_dict["xen_free_mem"] = xen_free_mem + mem_dict["total_mem_pref"] = total_mem_pref + mem_dict["total_mem_pref_acceptors"] = total_mem_pref_acceptors + mem_dict["donors"] = donors + mem_dict["acceptors"] = acceptors + return mem_dict + + +# Redistribute memory across domains. +# Called when one of domains update its 'meminfo' xenstore key. +# Return the list of (domain, mem_target) pairs to be passed to "xm memset" +# equivalent +def balance(xen_free_mem, dom_dict) -> dict: log.debug( - "balance(xen_free_memory={!r}, domain_dictionary={!r})".format( - xen_free_memory, domain_dictionary + "balance(xen_free_mem={!r}, dom_dict={!r})".format( + xen_free_mem, dom_dict ) ) - memory_dictionary = memory_info(xen_free_memory, domain_dictionary) - - if memory_dictionary["total_available_memory"] > 0: - return balance_when_enough_memory( - memory_dictionary["domain_dictionary"], - memory_dictionary["xen_free_memory"], - memory_dictionary["total_mem_pref"], - memory_dictionary["total_available_memory"], - ) - else: - return balance_when_low_on_memory( - memory_dictionary["domain_dictionary"], - memory_dictionary["xen_free_memory"], - memory_dictionary["total_mem_pref_acceptors"], - memory_dictionary["donors"], - memory_dictionary["acceptors"], + mem_dict = mem_info(xen_free_mem, dom_dict) + + if mem_dict["total_available_mem"] > 0: + return balance_when_enough_mem( + mem_dict["dom_dict"], + mem_dict["xen_free_mem"], + mem_dict["total_mem_pref"], + mem_dict["total_available_mem"], ) + return balance_when_low_on_mem( + mem_dict["dom_dict"], + mem_dict["xen_free_mem"], + mem_dict["total_mem_pref_acceptors"], + mem_dict["donors"], + mem_dict["acceptors"], + ) diff --git a/qubes/qmemman/client.py b/qubes/qmemman/client.py index 3b6bb088f..8e0e28bac 100644 --- a/qubes/qmemman/client.py +++ b/qubes/qmemman/client.py @@ -1,5 +1,3 @@ -# pylint: skip-file - # # The Qubes OS Project, http://www.qubes-os.org # @@ -21,23 +19,23 @@ import socket import fcntl +from typing import Optional class QMemmanClient: - def request_memory(self, amount): - self.sock = socket.socket(socket.AF_UNIX) + def __init__(self) -> None: + self.sock: Optional[socket.socket] = None + def request_mem(self, amount) -> bool: + self.sock = socket.socket(socket.AF_UNIX) flags = fcntl.fcntl(self.sock.fileno(), fcntl.F_GETFD) flags |= fcntl.FD_CLOEXEC fcntl.fcntl(self.sock.fileno(), fcntl.F_SETFD, flags) - self.sock.connect("/var/run/qubes/qmemman.sock") self.sock.send(str(int(amount)).encode("ascii") + b"\n") received = self.sock.recv(1024).strip() - if received == b"OK": - return True - else: - return False + return bool(received == b"OK") - def close(self): + def close(self) -> None: + assert isinstance(self.sock, socket.socket) self.sock.close() diff --git a/qubes/qmemman/domainstate.py b/qubes/qmemman/domainstate.py index a476e5d7c..38a82b895 100644 --- a/qubes/qmemman/domainstate.py +++ b/qubes/qmemman/domainstate.py @@ -1,4 +1,3 @@ -# pylint: skip-file # # The Qubes OS Project, https://www.qubes-os.org/ # @@ -19,20 +18,29 @@ # You should have received a copy of the GNU General Public # License along with this library; if not, see . +from typing import Optional -class DomainState: - def __init__(self, id): - self.memory_current = 0 # the current memory size - self.memory_actual = None # the current memory allocation (what VM - # is using or can use at any time) - self.memory_maximum = None # the maximum memory size - self.mem_used = None # used memory, computed based on meminfo - self.id = id # domain id - self.last_target = 0 # the last memset target - self.use_hotplug = False # use memory hotplug for mem-set - self.no_progress = False # no react to memset - self.slow_memset_react = False # slow react to memset (after few - # tries still above target) - def __repr__(self): +class DomainState: # pylint: disable=too-few-public-methods + def __init__(self, domid) -> None: + # Current memory size. + self.mem_current: int = 0 + # Current memory allocation (what VM is using or can use at any time). + self.mem_actual: Optional[int] = None + # Maximum memory size. + self.mem_max: Optional[int] = None + # Used memory, computed based on meminfo. + self.mem_used: Optional[int] = None + # Domain ID. + self.domid: str = domid + # Last memset target. + self.last_target: int = 0 + # Use memory hotplug for mem-set. + self.use_hotplug: bool = False + # No reaction to memset. + self.no_progress: bool = False + # Slow react to memset (after few tries still above target). + self.slow_memset_react: bool = False + + def __repr__(self) -> str: return self.__dict__.__repr__() diff --git a/qubes/qmemman/systemstate.py b/qubes/qmemman/systemstate.py index a12011183..d85e17e1f 100644 --- a/qubes/qmemman/systemstate.py +++ b/qubes/qmemman/systemstate.py @@ -1,4 +1,3 @@ -# pylint: skip-file # # The Qubes OS Project, https://www.qubes-os.org/ # @@ -18,210 +17,207 @@ # # You should have received a copy of the GNU General Public # License along with this library; if not, see . + import functools import logging import os import time - -import xen.lowlevel +import xen.lowlevel # pylint: disable=import-error from pathlib import Path +from typing import Optional import qubes.qmemman from qubes.qmemman.domainstate import DomainState - -no_progress_msg = "VM refused to give back requested memory" -slow_memset_react_msg = "VM didn't give back all requested memory" - - -class SystemState(object): - def __init__(self): +BALLOON_DELAY = 0.1 +XEN_FREE_MEM_LEFT = 50 * 1024 * 1024 +XEN_FREE_MEM_MIN = 25 * 1024 * 1024 +# Overhead of per-page Xen structures, taken from OpenStack +# nova/virt/xenapi/driver.py +# see https://wiki.openstack.org/wiki/XenServer/Overhead +MEM_OVERHEAD_FACTOR = 1.0 / 1.00781 +CHECK_PERIOD_S = 3 +CHECK_MB_S = 100 +MIN_TOTAL_MEMORY_TRANSFER = 150 * 1024 * 1024 +MIN_MEM_CHANGE_WHEN_UNDER_PREF = 15 * 1024 * 1024 + + +class SystemState: + def __init__(self) -> None: self.log = logging.getLogger("qmemman.systemstate") self.log.debug("SystemState()") - self.domdict = {} - self.xc = None - self.xs = None + self.dom_dict: dict[str, DomainState] = {} + self.xc: xen.lowlevel.xc.xc = None + self.xs: xen.lowlevel.xs.xs = None + self.all_phys_mem: int = 0 - def init(self): + def init(self) -> None: self.xc = xen.lowlevel.xc.xc() self.xs = xen.lowlevel.xs.xs() - self.BALOON_DELAY = 0.1 - self.XEN_FREE_MEM_LEFT = 50 * 1024 * 1024 - self.XEN_FREE_MEM_MIN = 25 * 1024 * 1024 - # Overhead of per-page Xen structures, taken from OpenStack - # nova/virt/xenapi/driver.py - # see https://wiki.openstack.org/wiki/XenServer/Overhead - # we divide total and free physical memory by this to get - # "assignable" memory - self.MEM_OVERHEAD_FACTOR = 1.0 / 1.00781 + # We divide total and free physical memory by this to get "assignable" + # memory try: - self.ALL_PHYS_MEM = int( - self.xc.physinfo()["total_memory"] - * 1024 - * self.MEM_OVERHEAD_FACTOR + self.all_phys_mem = int( + self.xc.physinfo()["total_memory"] * 1024 * MEM_OVERHEAD_FACTOR ) except xen.lowlevel.xc.Error: - self.ALL_PHYS_MEM = 0 + pass - def add_domain(self, id): - self.log.debug("add_domain(id={!r})".format(id)) - self.domdict[id] = DomainState(id) + def get_xs_path(self, domid, key) -> str: + return "/local/domain/" + str(domid) + "/memory/" + key + + def add_domain(self, domid) -> None: + self.log.debug("add_domain(domid={!r})".format(domid)) + self.dom_dict[domid] = DomainState(domid) # TODO: move to DomainState.__init__ - target_str = self.xs.read("", "/local/domain/" + id + "/memory/target") + target_str = self.xs.read("", self.get_xs_path(domid, "target")) if target_str: - self.domdict[id].last_target = int(target_str) * 1024 + self.dom_dict[domid].last_target = int(target_str) * 1024 - def del_domain(self, id): - self.log.debug("del_domain(id={!r})".format(id)) - self.domdict.pop(id) + def del_domain(self, domid) -> None: + self.log.debug("del_domain(domid={!r})".format(domid)) + self.dom_dict.pop(domid) - def get_free_xen_memory(self): + def get_free_xen_mem(self) -> int: xen_free = int( - self.xc.physinfo()["free_memory"] * 1024 * self.MEM_OVERHEAD_FACTOR + self.xc.physinfo()["free_memory"] * 1024 * MEM_OVERHEAD_FACTOR ) - # now check for domains which have assigned more memory than really - # used - do not count it as "free", because domain is free to use it - # at any time - # assumption: self.refresh_memactual was called before - # (so domdict[id].memory_actual is up-to-date) + # Check for domains which have assigned more memory than really used - + # do not count it as "free", because domain is free to use it at any + # time. Assumption: self.refresh_mem_actual was called before (so + # dom.mem_actual is up-to-date) assigned_but_unused = functools.reduce( - lambda acc, dom: acc + max(0, dom.last_target - dom.memory_current), - self.domdict.values(), + lambda acc, dom: acc + max(0, dom.last_target - dom.mem_current), + self.dom_dict.values(), 0, ) - # If, at any time, Xen have less memory than XEN_FREE_MEM_MIN, - # it is a failure of qmemman. Collect as much data as possible to - # debug it - if xen_free < self.XEN_FREE_MEM_MIN: + # If, at any time, Xen have less memory than XEN_FREE_MEM_MIN, it is a + # failure of qmemman. Collect as much data as possible to debug it + if xen_free < XEN_FREE_MEM_MIN: self.log.error( "Xen free = {!r} below acceptable value! " - "assigned_but_unused={!r}, domdict={!r}".format( - xen_free, assigned_but_unused, self.domdict + "assigned_but_unused={!r}, dom_dict={!r}".format( + xen_free, assigned_but_unused, self.dom_dict ) ) - elif xen_free < assigned_but_unused + self.XEN_FREE_MEM_MIN: + elif xen_free < assigned_but_unused + XEN_FREE_MEM_MIN: self.log.error( "Xen free = {!r} too small to satisfy assignments! " - "assigned_but_unused={!r}, domdict={!r}".format( - xen_free, assigned_but_unused, self.domdict + "assigned_but_unused={!r}, dom_dict={!r}".format( + xen_free, assigned_but_unused, self.dom_dict ) ) return xen_free - assigned_but_unused - # refresh information on memory assigned to all domains - def refresh_memactual(self): + # Refresh information on memory assigned to all domains + def refresh_mem_actual(self) -> None: for domain in self.xc.domain_getinfo(): - id = str(domain["domid"]) - if id in self.domdict: - # real memory usage - self.domdict[id].memory_current = domain["mem_kb"] * 1024 - # what VM is using or can use - self.domdict[id].memory_actual = max( - self.domdict[id].memory_current, - self.domdict[id].last_target, + domid = str(domain["domid"]) + if domid in self.dom_dict: + dom = self.dom_dict[domid] + # Real memory usage + dom.mem_current = domain["mem_kb"] * 1024 + # What VM is using or can use + dom.mem_actual = max( + dom.mem_current, + dom.last_target, ) hotplug_max = self.xs.read( - "", "/local/domain/%s/memory/hotplug-max" % str(id) + "", self.get_xs_path(domid, "hotplug-max") ) static_max = self.xs.read( - "", "/local/domain/%s/memory/static-max" % str(id) + "", self.get_xs_path(domid, "static-max") ) if hotplug_max: - self.domdict[id].memory_maximum = int(hotplug_max) * 1024 - self.domdict[id].use_hotplug = True + dom.mem_max = int(hotplug_max) * 1024 + dom.use_hotplug = True elif static_max: - self.domdict[id].memory_maximum = int(static_max) * 1024 - self.domdict[id].use_hotplug = False + dom.mem_max = int(static_max) * 1024 + dom.use_hotplug = False else: - self.domdict[id].memory_maximum = self.ALL_PHYS_MEM + dom.mem_max = self.all_phys_mem # the previous line used to be - # self.domdict[id].memory_maximum = domain[ - # 'maxmem_kb']*1024 + # dom.mem_max = domain['maxmem_kb']*1024 # but domain['maxmem_kb'] changes in self.mem_set as well, - # and this results in the memory never increasing - # in fact, the only possible case of nonexisting - # memory/static-max is dom0 - # see #307 - - def clear_outdated_error_markers(self): - # Clear outdated errors - for i in self.domdict.keys(): - if self.domdict[i].mem_used is None: + # and this results in the memory never increasing in fact, + # the only possible case of nonexisting memory/static-max + # is dom0, see #307 + + def clear_outdated_error_markers(self) -> None: + # Clear outdated errors. + for dom in self.dom_dict.values(): + if dom.mem_used is None: continue - # clear markers excluding VM from memory balance, if: + # Clear markers excluding VM from memory balance, if: # - VM have responded to previous request (with some safety margin) # - VM request more memory than it has assigned # The second condition avoids starving a VM, even when there is - # some free memory available - if self.domdict[i].memory_actual <= self.domdict[ - i - ].last_target + self.XEN_FREE_MEM_LEFT / 2 or self.domdict[ - i - ].memory_actual < qubes.qmemman.algo.prefmem( - self.domdict[i] + # some free memory available. + assert isinstance(dom.mem_actual, int) + if ( + dom.mem_actual <= dom.last_target + XEN_FREE_MEM_LEFT / 2 + or dom.mem_actual < qubes.qmemman.algo.pref_mem(dom) ): - self.domdict[i].slow_memset_react = False - self.domdict[i].no_progress = False - - # the below works (and is fast), but then 'xm list' shows unchanged - # memory value - def mem_set(self, id, val): - self.log.info("mem-set domain {} to {}".format(id, val)) - self.domdict[id].last_target = val - # can happen in the middle of domain shutdown - # apparently xc.lowlevel throws exceptions too + dom.slow_memset_react = False + dom.no_progress = False + + # The below works (and is fast), but then 'xm list' shows unchanged memory + # value. + def mem_set(self, domid, val) -> None: + self.log.info("mem-set domain {} to {}".format(domid, val)) + dom = self.dom_dict[domid] + dom.last_target = val + # Can happen in the middle of domain shutdown apparently xc.lowlevel + # throws exceptions too. try: self.xc.domain_setmaxmem( - int(id), int(val / 1024) + 1024 + int(domid), int(val / 1024) + 1024 ) # LIBXL_MAXMEM_CONSTANT=1024 - self.xc.domain_set_target_mem(int(id), int(val / 1024)) - except: + self.xc.domain_set_target_mem(int(domid), int(val / 1024)) + except Exception: pass # VM sees about 16MB memory less, so adjust for it here - qmemman # handle Xen view of memory + # handle Xen view of memory. self.xs.write( "", - "/local/domain/" + id + "/memory/target", + self.get_xs_path(domid, "target"), str(int(val / 1024 - 16 * 1024)), ) - if self.domdict[id].use_hotplug: + if dom.use_hotplug: self.xs.write( "", - "/local/domain/" + id + "/memory/static-max", + self.get_xs_path(domid, "static-max"), str(int(val / 1024)), ) - # this is called at the end of ballooning, when we have Xen free mem already - # make sure that past mem_set will not decrease Xen free mem - def inhibit_balloon_up(self): + # This is called at the end of ballooning, when we have Xen free mem + # already, make sure that past mem_set will not decrease Xen free mem. + def inhibit_balloon_up(self) -> None: self.log.debug("inhibit_balloon_up()") - for i in self.domdict.keys(): - dom = self.domdict[i] + for domid, dom in self.dom_dict.items(): if ( - dom.memory_actual is not None - and dom.memory_actual + 200 * 1024 < dom.last_target + dom.mem_actual is not None + and dom.mem_actual + 200 * 1024 < dom.last_target ): self.log.info( "Preventing balloon up to {}".format(dom.last_target) ) - self.mem_set(i, dom.memory_actual) - - # perform memory ballooning, across all domains, to add "memsize" to Xen - # free memory - def do_balloon(self, memsize): - self.log.info("do_balloon(memsize={!r})".format(memsize)) - CHECK_PERIOD_S = 3 - CHECK_MB_S = 100 + self.mem_set(domid, dom.mem_actual) + # Perform memory ballooning, across all domains, to add "mem_size" to Xen + # free memory + def do_balloon(self, mem_size) -> bool: + self.log.info("do_balloon(mem_size={!r})".format(mem_size)) niter = 0 - prev_memory_actual = None + prev_mem_actual: dict[str, Optional[int]] = {} - for i in self.domdict.keys(): - self.domdict[i].no_progress = False + for dom in self.dom_dict.values(): + dom.no_progress = False #: number of loop iterations for CHECK_PERIOD_S seconds - check_period = max(1, int((CHECK_PERIOD_S + 0.0) / self.BALOON_DELAY)) + check_period = max(1, int((CHECK_PERIOD_S + 0.0) / BALLOON_DELAY)) #: number of free memory bytes expected to get during CHECK_PERIOD_S #: seconds check_delta = CHECK_PERIOD_S * CHECK_MB_S * 1024 * 1024 @@ -231,10 +227,10 @@ def do_balloon(self, memsize): while True: self.log.debug("niter={:2d}".format(niter)) - self.refresh_memactual() - xenfree = self.get_free_xen_memory() + self.refresh_mem_actual() + xenfree = self.get_free_xen_mem() self.log.info("xenfree={!r}".format(xenfree)) - if xenfree >= memsize + self.XEN_FREE_MEM_MIN: + if xenfree >= mem_size + XEN_FREE_MEM_MIN: self.inhibit_balloon_up() return True # fail the request if over past CHECK_PERIOD_S seconds, @@ -246,33 +242,30 @@ def do_balloon(self, memsize): ): return False xenfree_ring[ring_slot] = xenfree - if prev_memory_actual is not None: - for i in prev_memory_actual.keys(): - if prev_memory_actual[i] == self.domdict[i].memory_actual: - # domain not responding to memset requests, remove it - # from donors - self.domdict[i].no_progress = True - self.log.info( - "domain {} stuck at {}".format( - i, self.domdict[i].memory_actual - ) - ) + for domid, prev_mem in prev_mem_actual.items(): + dom = self.dom_dict[domid] + if prev_mem == dom.mem_actual: + # domain not responding to memset requests, remove it + # from donors + dom.no_progress = True + self.log.info( + "domain {} stuck at {}".format(domid, dom.mem_actual) + ) memset_reqs = qubes.qmemman.algo.balloon( - memsize + self.XEN_FREE_MEM_LEFT - xenfree, self.domdict + mem_size + XEN_FREE_MEM_LEFT - xenfree, self.dom_dict ) self.log.info("memset_reqs={!r}".format(memset_reqs)) if len(memset_reqs) == 0: return False - prev_memory_actual = {} - for i in memset_reqs: - dom, mem = i - self.mem_set(dom, mem) - prev_memory_actual[dom] = self.domdict[dom].memory_actual - self.log.debug("sleeping for {} s".format(self.BALOON_DELAY)) - time.sleep(self.BALOON_DELAY) + prev_mem_actual = {} + for domid, memset in memset_reqs: + self.mem_set(domid, memset) + prev_mem_actual[domid] = self.dom_dict[domid].mem_actual + self.log.debug("sleeping for {} s".format(BALLOON_DELAY)) + time.sleep(BALLOON_DELAY) niter = niter + 1 - def refresh_meminfo(self, domid, untrusted_meminfo_key): + def refresh_meminfo(self, domid, untrusted_meminfo_key) -> None: self.log.debug( "refresh_meminfo(domid={}, untrusted_meminfo_key={!r})".format( domid, untrusted_meminfo_key @@ -280,67 +273,60 @@ def refresh_meminfo(self, domid, untrusted_meminfo_key): ) qubes.qmemman.algo.refresh_meminfo_for_domain( - self.domdict[domid], untrusted_meminfo_key + self.dom_dict[domid], untrusted_meminfo_key ) self.do_balance() - # is the computed balance request big enough ? - # so that we do not trash with small adjustments - def is_balance_req_significant(self, memset_reqs, xenfree): + # Is the computed balance request big enough so that we do not trash with + # small adjustments. + def is_balance_req_significant(self, memset_reqs, xenfree) -> bool: self.log.debug( "is_balance_req_significant(memset_reqs={}, xenfree={})".format( memset_reqs, xenfree ) ) - total_memory_transfer = 0 - MIN_TOTAL_MEMORY_TRANSFER = 150 * 1024 * 1024 - MIN_MEM_CHANGE_WHEN_UNDER_PREF = 15 * 1024 * 1024 + total_mem_transfer = 0 - # If xenfree to low, return immediately - if self.XEN_FREE_MEM_LEFT - xenfree > MIN_MEM_CHANGE_WHEN_UNDER_PREF: + # If xenfree to low, return immediately. + if XEN_FREE_MEM_LEFT - xenfree > MIN_MEM_CHANGE_WHEN_UNDER_PREF: self.log.debug("xenfree is too low, returning") return True - for rq in memset_reqs: - dom, mem = rq - last_target = self.domdict[dom].last_target - memory_change = mem - last_target - total_memory_transfer += abs(memory_change) - pref = qubes.qmemman.algo.prefmem(self.domdict[dom]) + for domid, memset in memset_reqs: + last_target = self.dom_dict[domid].last_target + mem_change = memset - last_target + total_mem_transfer += abs(mem_change) + pref = qubes.qmemman.algo.pref_mem(self.dom_dict[domid]) if ( 0 < last_target < pref - and memory_change > MIN_MEM_CHANGE_WHEN_UNDER_PREF + and mem_change > MIN_MEM_CHANGE_WHEN_UNDER_PREF ): self.log.info( - "dom {} is below pref, allowing balance".format(dom) + "dom {} is below pref, allowing balance".format(domid) ) return True ret = ( - total_memory_transfer + abs(xenfree - self.XEN_FREE_MEM_LEFT) + total_mem_transfer + abs(xenfree - XEN_FREE_MEM_LEFT) > MIN_TOTAL_MEMORY_TRANSFER ) self.log.debug("is_balance_req_significant return {}".format(ret)) return ret - def print_stats(self, xenfree, memset_reqs): - for i in self.domdict.keys(): - if self.domdict[i].mem_used is not None: + def print_stats(self, xenfree, memset_reqs) -> None: + for domid, dom in self.dom_dict.items(): + if dom.mem_used is not None: self.log.info( "stat: dom {!r} act={} pref={} last_target={}" "{}{}".format( - i, - self.domdict[i].memory_actual, - qubes.qmemman.algo.prefmem(self.domdict[i]), - self.domdict[i].last_target, - " no_progress" if self.domdict[i].no_progress else "", - ( - " slow_memset_react" - if self.domdict[i].slow_memset_react - else "" - ), + domid, + dom.mem_actual, + qubes.qmemman.algo.pref_mem(dom), + dom.last_target, + " no_progress" if dom.no_progress else "", + (" slow_memset_react" if dom.slow_memset_react else ""), ) ) @@ -348,106 +334,103 @@ def print_stats(self, xenfree, memset_reqs): "stat: xenfree={} memset_reqs={}".format(xenfree, memset_reqs) ) - def do_balance(self): + def debug_stuck_balance( + self, stuck_domid, memset_reqs, prev_mem_actual + ) -> None: + for req in memset_reqs: + domid, mem = req + if domid == stuck_domid: + # All donors have been processed. + break + dom = self.dom_dict[domid] + # Allow some small margin. + assert isinstance(dom.mem_actual, int) + if dom.mem_actual > dom.last_target + XEN_FREE_MEM_LEFT / 4: + # VM didn't react to memory request at all, remove from donors. + if prev_mem_actual[domid] == dom.mem_actual: + self.log.warning( + "dom {!r} did not react to memory request (holds {}, " + "requested balloon down to {})".format( + domid, + dom.mem_actual, + mem, + ) + ) + dom.no_progress = True + else: + self.log.warning( + "dom {!r} still holds more memory than assigned ({} > " + "{})".format( + domid, + dom.mem_actual, + mem, + ) + ) + dom.slow_memset_react = True + + def do_balance(self) -> None: self.log.debug("do_balance()") if os.path.isfile("/var/run/qubes/do-not-membalance"): self.log.debug("do-not-membalance file present, returning") return - self.refresh_memactual() + self.refresh_mem_actual() self.clear_outdated_error_markers() - xenfree = self.get_free_xen_memory() + xenfree = self.get_free_xen_mem() memset_reqs = qubes.qmemman.algo.balance( - xenfree - self.XEN_FREE_MEM_LEFT, self.domdict + xenfree - XEN_FREE_MEM_LEFT, self.dom_dict ) if not self.is_balance_req_significant(memset_reqs, xenfree): return self.print_stats(xenfree, memset_reqs) - prev_memactual = {} - for i in self.domdict.keys(): - prev_memactual[i] = self.domdict[i].memory_actual - for rq in memset_reqs: - dom, mem = rq - # Force to always have at least 0.9*self.XEN_FREE_MEM_LEFT (some - # margin for rounding errors). Before giving memory to - # domain, ensure that others have gave it back. - # If not - wait a little. + prev_mem_actual: dict[str, Optional[int]] = {} + for domid, dom in self.dom_dict.items(): + prev_mem_actual[domid] = dom.mem_actual + for req in memset_reqs: + domid, mem = req + dom = self.dom_dict[domid] + # Force to always have at least 0.9*XEN_FREE_MEM_LEFT (some margin + # for rounding errors). Before giving memory to domain, ensure that + # others have gave it back. If not, wait a little. ntries = 5 while ( - self.get_free_xen_memory() - - (mem - self.domdict[dom].memory_actual) - < 0.9 * self.XEN_FREE_MEM_LEFT + self.get_free_xen_mem() - (mem - dom.mem_actual) + < 0.9 * XEN_FREE_MEM_LEFT ): self.log.debug( - "do_balance dom={!r} sleeping ntries={}".format(dom, ntries) + "do_balance dom={!r} sleeping ntries={}".format( + domid, ntries + ) ) - time.sleep(self.BALOON_DELAY) - self.refresh_memactual() + time.sleep(BALLOON_DELAY) + self.refresh_mem_actual() ntries -= 1 if ntries <= 0: - # Waiting haven't helped; Find which domain get stuck and - # abort balance (after distributing what we have) - for rq2 in memset_reqs: - dom2, mem2 = rq2 - if dom2 == dom: - # All donors have been processed - break - # allow some small margin - if ( - self.domdict[dom2].memory_actual - > self.domdict[dom2].last_target - + self.XEN_FREE_MEM_LEFT / 4 - ): - # VM didn't react to memory request at all, - # remove from donors - if ( - prev_memactual[dom2] - == self.domdict[dom2].memory_actual - ): - self.log.warning( - "dom {!r} did not react to memory request" - " (holds {}, requested balloon down to {})".format( - dom2, - self.domdict[dom2].memory_actual, - mem2, - ) - ) - self.domdict[dom2].no_progress = True - else: - self.log.warning( - "dom {!r} still holds more" - " memory than assigned ({} > {})".format( - dom2, - self.domdict[dom2].memory_actual, - mem2, - ) - ) - self.domdict[dom2].slow_memset_react = True + # Waiting hasn't helped. Find which domain got stuck and + # abort balance (after distributing what we have). + self.debug_stuck_balance( + domid, memset_reqs, prev_mem_actual + ) + assert isinstance(dom.mem_actual, int) self.mem_set( - dom, - self.get_free_xen_memory() - + self.domdict[dom].memory_actual - - self.XEN_FREE_MEM_LEFT, + domid, + self.get_free_xen_mem() + + dom.mem_actual + - XEN_FREE_MEM_LEFT, ) return - self.mem_set(dom, mem) + self.mem_set(domid, mem) - xenfree = self.get_free_xen_memory() - memory_dictionary = qubes.qmemman.algo.memory_info( - xenfree - self.XEN_FREE_MEM_LEFT, self.domdict + xenfree = self.get_free_xen_mem() + mem_dict = qubes.qmemman.algo.mem_info( + xenfree - XEN_FREE_MEM_LEFT, self.dom_dict ) avail_mem_file = qubes.config.qmemman_avail_mem_file avail_mem_file_tmp = Path(avail_mem_file).with_suffix(".tmp") with open(avail_mem_file_tmp, "w", encoding="ascii") as file: - file.write(str(memory_dictionary["total_available_memory"])) + file.write(str(mem_dict["total_available_mem"])) os.chmod(avail_mem_file_tmp, 0o644) os.replace(avail_mem_file_tmp, avail_mem_file) - - -# for i in self.domdict.keys(): -# print 'domain ', i, ' meminfo=', self.domdict[i].mem_used, 'actual mem', self.domdict[i].memory_actual -# print 'domain ', i, 'actual mem', self.domdict[i].memory_actual -# print 'xen free mem', self.get_free_xen_memory() diff --git a/qubes/tests/qmemman.py b/qubes/tests/qmemman.py index 2517880ff..b520227c5 100644 --- a/qubes/tests/qmemman.py +++ b/qubes/tests/qmemman.py @@ -23,28 +23,26 @@ import qubes.tests - -def MB(val): - return int(val * 1024 * 1024) +MB = 1024 * 1024 def construct_dominfo( - id, + domid, mem_used=None, - memory_maximum=None, - memory_actual=None, - memory_current=0, + mem_max=None, + mem_actual=None, + mem_current=0, last_target=0, use_hotplug=False, ): - d = qubes.qmemman.domainstate.DomainState(id) - d.mem_used = mem_used - d.memory_maximum = memory_maximum - d.memory_actual = memory_actual - d.memory_current = memory_current - d.last_target = last_target - d.use_hotplug = use_hotplug - return d + dom = qubes.qmemman.domainstate.DomainState(domid) + dom.mem_used = mem_used + dom.mem_max = mem_max + dom.mem_actual = mem_actual + dom.mem_current = mem_current + dom.last_target = last_target + dom.use_hotplug = use_hotplug + return dom class TC_00_Qmemman_algo(qubes.tests.QubesTestCase): @@ -66,141 +64,141 @@ def test_000_meminfo(self): qubes.qmemman.algo.sanitize_and_parse_meminfo(b"4096\n1024") ) - def test_010_prefmem_dom0(self): - d = qubes.qmemman.domainstate.DomainState("0") - d.mem_used = MB(1024) - d.memory_maximum = MB(4096) - self.assertEqual(qubes.qmemman.algo.prefmem(d), MB(1681.2)) - d.mem_used = MB(5000) - self.assertEqual(qubes.qmemman.algo.prefmem(d), MB(4096)) + def test_010_pref_mem_dom0(self): + dom = qubes.qmemman.domainstate.DomainState("0") + dom.mem_used = int(1024 * MB) + dom.mem_max = int(4096 * MB) + self.assertEqual(qubes.qmemman.algo.pref_mem(dom), int(1681.2 * MB)) + dom.mem_used = int(5000 * MB) + self.assertEqual(qubes.qmemman.algo.pref_mem(dom), int(4096 * MB)) - def test_011_prefmem_domU(self): - d = qubes.qmemman.domainstate.DomainState("10") - d.mem_used = MB(1024) - d.memory_maximum = MB(4096) - self.assertEqual(qubes.qmemman.algo.prefmem(d), MB(1331.2)) - d.mem_used = MB(5000) - self.assertEqual(qubes.qmemman.algo.prefmem(d), MB(4096)) + def test_011_pref_mem_domU(self): # pylint: disable=invalid-name + dom = qubes.qmemman.domainstate.DomainState("10") + dom.mem_used = int(1024 * MB) + dom.mem_max = int(4096 * MB) + self.assertEqual(qubes.qmemman.algo.pref_mem(dom), int(1331.2 * MB)) + dom.mem_used = int(5000 * MB) + self.assertEqual(qubes.qmemman.algo.pref_mem(dom), int(4096 * MB)) - def test_020_memory_needed(self): - d = qubes.qmemman.domainstate.DomainState("10") - d.mem_used = MB(1024) - d.memory_maximum = MB(4096) - d.memory_actual = MB(1024) - self.assertEqual(qubes.qmemman.algo.memory_needed(d), MB(307.2)) + def test_020_needed_mem(self): + dom = qubes.qmemman.domainstate.DomainState("10") + dom.mem_used = int(1024 * MB) + dom.mem_max = int(4096 * MB) + dom.mem_actual = int(1024 * MB) + self.assertEqual(qubes.qmemman.algo.needed_mem(dom), int(307.2 * MB)) - d.memory_actual = MB(2024) - self.assertEqual(qubes.qmemman.algo.memory_needed(d), MB(-692.800001)) + dom.mem_actual = int(2024 * MB) + self.assertEqual( + qubes.qmemman.algo.needed_mem(dom), int(-692.800001 * MB) + ) def test_100_balloon(self): domains = { "0": construct_dominfo( "0", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(1736), - memory_current=MB(1736), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1736 * MB), + mem_current=int(1736 * MB), ), "1": construct_dominfo( "1", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(1536), - memory_current=MB(1536), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1536 * MB), + mem_current=int(1536 * MB), ), - # at prefmem + # at pref_mem "2": construct_dominfo( "2", - mem_used=MB(4096), - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_used=int(4096 * MB), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), # no meminfo at all "3": construct_dominfo( "3", mem_used=None, - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), } - result = qubes.qmemman.algo.balloon(MB(400), domains) + result = qubes.qmemman.algo.balloon(int(400 * MB), domains) expected = [] self.assertEqual(result, expected) - domains["1"].mem_used = MB(512) + domains["1"].mem_used = int(512 * MB) - result = qubes.qmemman.algo.balloon(MB(400), domains) - released = sum(domains[l[0]].memory_current - l[1] for l in result) + result = qubes.qmemman.algo.balloon(int(400 * MB), domains) + released = sum(domains[l[0]].mem_current - l[1] for l in result) expected = [("0", 1794242737), ("1", 1196296014)] - self.assertGreater(released, MB(400)) + self.assertGreater(released, int(400 * MB)) # should be within about 5% margin - self.assertLess(released - MB(400), MB(21)) + self.assertLess(released - int(400 * MB), int(21 * MB)) self.assertEqual(result, expected) - def test_200_balance_whan_enough_memory(self): + def test_200_balance_when_enough_mem(self): domains = { "0": construct_dominfo( "0", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(1736), - memory_current=MB(1736), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1736 * MB), + mem_current=int(1736 * MB), ), "1": construct_dominfo( "1", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(1536), - memory_current=MB(1536), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1536 * MB), + mem_current=int(1536 * MB), ), # at maxmem "2": construct_dominfo( "2", - mem_used=MB(4096), - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_used=int(4096 * MB), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), # no meminfo at all "3": construct_dominfo( "3", mem_used=None, - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), - # at prefmem, but can get more + # at pref_mem, but can get more "4": construct_dominfo( "4", - mem_used=MB(1536), - memory_maximum=MB(4096), - memory_actual=MB(1536), - memory_current=MB(1536), + mem_used=int(1536 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1536 * MB), + mem_current=int(1536 * MB), ), # low maxmem "5": construct_dominfo( "5", - mem_used=MB(512), - memory_maximum=MB(1024), - memory_actual=MB(768), - memory_current=MB(768), + mem_used=int(512 * MB), + mem_max=int(1024 * MB), + mem_actual=int(768 * MB), + mem_current=int(768 * MB), ), } - total_prefmem = sum( - qubes.qmemman.algo.prefmem(d) - for d in domains.values() - if d.mem_used is not None - ) - # xen_free_memory is ignored, use dummy 0 there - result = qubes.qmemman.algo.balance_when_enough_memory( - domains, 0, total_prefmem, MB(4096) + total_pref_mem = sum( + qubes.qmemman.algo.pref_mem(dom) + for dom in domains.values() + if dom.mem_used is not None ) - total_allocated = sum( - l[1] - domains[l[0]].memory_actual for l in result + # xen_free_mem is ignored, use dummy 0 there + result = qubes.qmemman.algo.balance_when_enough_mem( + domains, 0, total_pref_mem, int(4096 * MB) ) + total_allocated = sum(l[1] - domains[l[0]].mem_actual for l in result) # FIXME: the current algo is broken here, thus +5% - self.assertLess(total_allocated, MB(4096) * 1.05) + self.assertLess(total_allocated, int(4096 * MB) * 1.05) # should be no repeats self.assertEqual( len(result), @@ -209,11 +207,11 @@ def test_200_balance_whan_enough_memory(self): ) # no meminfo -> no adjustment self.assertNotIn(("3", unittest.mock.ANY), result) - # prefmem==maxmem==current, shouldn't adjust + # pref_mem==maxmem==current, shouldn't adjust request = [x for x in result if x[0] == "2"][0] - self.assertEqual(request[1], domains["2"].memory_actual) + self.assertEqual(request[1], domains["2"].mem_actual) - # bigger prefmem -> bigger target + # bigger pref_mem -> bigger target request1 = [x for x in result if x[0] == "1"][0] # mem_used 1GB request2 = [x for x in result if x[0] == "4"][0] # mem_used 1.5GB self.assertGreater(request2[1], request1[1]) @@ -231,59 +229,59 @@ def test_200_balance_whan_enough_memory(self): ], ) - def test_250_balance_low_on_memory(self): + def test_250_balance_when_low_on_mem(self): domains = { - # below prefmem + # below pref_mem "0": construct_dominfo( "0", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(768), - memory_current=MB(768), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(768 * MB), + mem_current=int(768 * MB), ), "1": construct_dominfo( "1", - mem_used=MB(1024), - memory_maximum=MB(4096), - memory_actual=MB(1536), - memory_current=MB(1536), + mem_used=int(1024 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1536 * MB), + mem_current=int(1536 * MB), ), # at maxmem "2": construct_dominfo( "2", - mem_used=MB(4096), - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_used=int(4096 * MB), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), # no meminfo at all "3": construct_dominfo( "3", mem_used=None, - memory_maximum=MB(4096), - memory_actual=MB(4096), - memory_current=MB(4096), + mem_max=int(4096 * MB), + mem_actual=int(4096 * MB), + mem_current=int(4096 * MB), ), - # at prefmem, but can get more + # at pref_mem, but can get more "4": construct_dominfo( "4", - mem_used=MB(1536), - memory_maximum=MB(4096), - memory_actual=MB(1536), - memory_current=MB(1536), + mem_used=int(1536 * MB), + mem_max=int(4096 * MB), + mem_actual=int(1536 * MB), + mem_current=int(1536 * MB), ), # low maxmem "5": construct_dominfo( "5", - mem_used=MB(512), - memory_maximum=MB(1024), - memory_actual=MB(768), - memory_current=MB(768), + mem_used=int(512 * MB), + mem_max=int(1024 * MB), + mem_actual=int(768 * MB), + mem_current=int(768 * MB), ), } - # call "balance" instead of "balance_low_on_memory" directly, + # call "balance" instead of "balance_when_low_on_mem" directly, # to collect donors/acceptors list - result = qubes.qmemman.algo.balance(MB(50), domains) + result = qubes.qmemman.algo.balance(int(50 * MB), domains) # should be no repeats self.assertEqual( len(result), @@ -293,18 +291,18 @@ def test_250_balance_low_on_memory(self): # no meminfo -> no adjustment self.assertNotIn(("3", unittest.mock.ANY), result) for domid, target in result: - if domains[domid].mem_used > domains[domid].memory_actual: - # no domain should get less, if already below prefmem + if domains[domid].mem_used > domains[domid].mem_actual: + # no domain should get less, if already below pref_mem self.assertGreaterEqual( target, - domains[domid].memory_actual, + domains[domid].mem_actual, "Request for {} reduces in {!r}".format(domid, result), ) else: # otherwise it _should_ get reduced self.assertLess( target, - domains[domid].memory_actual, + domains[domid].mem_actual, "Request for {} increases in {!r}".format(domid, result), ) diff --git a/qubes/tools/qmemmand.py b/qubes/tools/qmemmand.py index 0e6b4392f..0a7e505b5 100644 --- a/qubes/tools/qmemmand.py +++ b/qubes/tools/qmemmand.py @@ -38,30 +38,25 @@ import qubes.utils SOCK_PATH = "/var/run/qubes/qmemman.sock" - +GLOBAL_LOCK = threading.Lock() system_state = qubes.qmemman.systemstate.SystemState() -global_lock = threading.Lock() -# If XSWatcher will -# handle meminfo event before @introduceDomain, it will use -# incomplete domain list for that and may redistribute memory -# allocated to some VM, but not yet used (see #1389). -# To fix that, system_state should be updated (refresh domain -# list) before processing other changes, every time some process requested -# memory for a new VM, before releasing the lock. Then XS_Watcher will check -# this flag before processing other event. -force_refresh_domain_list = False +# If XSWatcher handles meminfo event before @introduceDomain, it will use +# incomplete domain list for that and may redistribute memory allocated to some +# VM, but not yet used (see #1389). To fix that, system_state should be updated +# (refresh domain list) before processing other changes, every time some +# process requested memory for a new VM, before releasing the lock. Then +# XS_Watcher will check this flag before processing other event. +FORCE_REFRESH_DOMAIN_LIST = False -def only_in_first_list(list1, list2): - ret = [] - for i in list1: - if i not in list2: - ret.append(i) - return ret +def unique_per_list(list1, list2): + first = [item for item in list1 if item not in list2] + second = [item for item in list2 if item not in list1] + return first, second -def get_domain_meminfo_key(domain_id): - return "/local/domain/" + domain_id + "/memory/meminfo" +def get_domain_meminfo_key(domid): + return "/local/domain/" + domid + "/memory/meminfo" @dataclass @@ -91,7 +86,7 @@ def domain_list_changed(self, refresh_only=False): :param refresh_only If True, only refresh domain list, do not redistribute memory. In this mode, caller must already hold - global_lock. + GLOBAL_LOCK. """ self.log.debug( "domain_list_changed(only_refresh={!r})".format(refresh_only) @@ -99,43 +94,40 @@ def domain_list_changed(self, refresh_only=False): got_lock = False if not refresh_only: - self.log.debug("acquiring global_lock") + self.log.debug("acquiring GLOBAL_LOCK") # pylint: disable=consider-using-with - global_lock.acquire() + GLOBAL_LOCK.acquire() got_lock = True - self.log.debug("global_lock acquired") + self.log.debug("GLOBAL_LOCK acquired") try: curr = self.handle.ls("", "/local/domain") if curr is None: return - # check if domain is really there, it may happen that some empty + # Check if domain is really there, it may happen that some empty # directories are left in xenstore - curr = list( - filter( - lambda x: self.handle.read( - "", "/local/domain/{}/domid".format(x) - ) - is not None, - curr, - ) - ) + curr = [ + domid + for domid in curr + if self.handle.read("", "/local/domain/{}/domid".format(domid)) + is not None + ] self.log.debug("curr={!r}".format(curr)) - for i in only_in_first_list(curr, self.watch_token_dict.keys()): - # new domain has been created - watch = WatchType(XSWatcher.meminfo_changed, i) - self.watch_token_dict[i] = watch - self.handle.watch(get_domain_meminfo_key(i), watch) - system_state.add_domain(i) - - for i in only_in_first_list(self.watch_token_dict.keys(), curr): - # domain destroyed + created, destroyed = unique_per_list( + curr, self.watch_token_dict.keys() + ) + for domid in created: + watch = WatchType(XSWatcher.meminfo_changed, domid) + self.watch_token_dict[domid] = watch + self.handle.watch(get_domain_meminfo_key(domid), watch) + system_state.add_domain(domid) + for domid in destroyed: self.handle.unwatch( - get_domain_meminfo_key(i), self.watch_token_dict[i] + get_domain_meminfo_key(domid), self.watch_token_dict[domid] ) - self.watch_token_dict.pop(i) - system_state.del_domain(i) + self.watch_token_dict.pop(domid) + system_state.del_domain(domid) if not refresh_only: try: @@ -146,33 +138,33 @@ def domain_list_changed(self, refresh_only=False): self.log.exception("Updating domain list failed") finally: if got_lock: - global_lock.release() - self.log.debug("global_lock released") + GLOBAL_LOCK.release() + self.log.debug("GLOBAL_LOCK released") - def meminfo_changed(self, domain_id): - self.log.debug("meminfo_changed(domain_id={!r})".format(domain_id)) + def meminfo_changed(self, domid): + self.log.debug("meminfo_changed(domid={!r})".format(domid)) untrusted_meminfo_key = self.handle.read( - "", get_domain_meminfo_key(domain_id) + "", get_domain_meminfo_key(domid) ) if untrusted_meminfo_key is None or untrusted_meminfo_key == b"": return - self.log.debug("acquiring global_lock") - with global_lock: - self.log.debug("global_lock acquired") + self.log.debug("acquiring GLOBAL_LOCK") + with GLOBAL_LOCK: + self.log.debug("GLOBAL_LOCK acquired") try: - global force_refresh_domain_list - if force_refresh_domain_list: + global FORCE_REFRESH_DOMAIN_LIST + if FORCE_REFRESH_DOMAIN_LIST: self.domain_list_changed(refresh_only=True) - force_refresh_domain_list = False - if domain_id not in self.watch_token_dict: - # domain just destroyed + FORCE_REFRESH_DOMAIN_LIST = False + if domid not in self.watch_token_dict: + # Domain was just destroyed. return - system_state.refresh_meminfo(domain_id, untrusted_meminfo_key) + system_state.refresh_meminfo(domid, untrusted_meminfo_key) except: # pylint: disable=bare-except - self.log.exception("Updating meminfo for %s failed", domain_id) - self.log.debug("global_lock released") + self.log.exception("Updating meminfo for %s failed", domid) + self.log.debug("GLOBAL_LOCK released") def watch_loop(self): self.log.debug("watch_loop()") @@ -187,9 +179,8 @@ class QMemmanReqHandler(socketserver.BaseRequestHandler): """ The RequestHandler class for our server. - It is instantiated once per connection to the server, and must - override the handle() method to implement communication to the - client. + It is instantiated once per connection to the server, and must override the + handle() method to implement communication to the client. """ def handle(self): @@ -204,8 +195,8 @@ def handle(self): if len(self.data) == 0: self.log.info("client disconnected, resuming membalance") if got_lock: - global force_refresh_domain_list - force_refresh_domain_list = True + global FORCE_REFRESH_DOMAIN_LIST + FORCE_REFRESH_DOMAIN_LIST = True return # XXX something is wrong here: return without release? @@ -213,10 +204,10 @@ def handle(self): self.log.warning("Second request over qmemman.sock?") return - self.log.debug("acquiring global_lock") + self.log.debug("acquiring GLOBAL_LOCK") # pylint: disable=consider-using-with - global_lock.acquire() - self.log.debug("global_lock acquired") + GLOBAL_LOCK.acquire() + self.log.debug("GLOBAL_LOCK acquired") got_lock = True if self.data.isdigit() and system_state.do_balloon( @@ -233,33 +224,30 @@ def handle(self): ) finally: if got_lock: - global_lock.release() - self.log.debug("global_lock released") - - -parser = qubes.tools.QubesArgumentParser(want_app=False) - -parser.add_argument( - "--config", - "-c", - metavar="FILE", - action="store", - default="/etc/qubes/qmemman.conf", - help="qmemman config file", -) - -parser.add_argument( - "--foreground", - action="store_true", - default=False, - help="do not close stdio", -) + GLOBAL_LOCK.release() + self.log.debug("GLOBAL_LOCK released") def main(): + parser = qubes.tools.QubesArgumentParser(want_app=False) + parser.add_argument( + "--config", + "-c", + metavar="FILE", + action="store", + default="/etc/qubes/qmemman.conf", + help="qmemman config file", + ) + parser.add_argument( + "--foreground", + "-f", + action="store_true", + default=False, + help="do not close stdio", + ) args = parser.parse_args() - # setup logging + # Setup logging. ha_syslog = logging.handlers.SysLogHandler("/dev/log") ha_syslog.setFormatter( logging.Formatter("%(name)s[%(process)d]: %(message)s") @@ -313,13 +301,13 @@ def main(): log.debug("instantiating server") os.umask(0) - # Initialize the connection to Xen and to XenStore + # Initialize the connection to Xen and to XenStore. system_state.init() server = socketserver.UnixStreamServer(SOCK_PATH, QMemmanReqHandler) os.umask(0o077) - # notify systemd + # Notify systemd. nofity_socket = os.getenv("NOTIFY_SOCKET") if nofity_socket: log.debug("notifying systemd") diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index 4d8a71628..6dc5be0ee 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1432,7 +1432,7 @@ async def start( ) qmemman_client = await asyncio.get_event_loop().run_in_executor( - None, self.request_memory, mem_required + None, self.request_mem, mem_required ) await self.storage.start() @@ -2032,7 +2032,7 @@ def use_memory_hotplug(self): return bool(feature) return False - def request_memory(self, mem_required=None): + def request_mem(self, mem_required=None): if not qmemman_present: return None @@ -2065,9 +2065,7 @@ def request_memory(self, mem_required=None): # 2 pages per 1MB of RAM, see # libxl__get_required_paging_memory() mem_required_with_overhead += maxmem * 8192 - got_memory = qmemman_client.request_memory( - mem_required_with_overhead - ) + got_memory = qmemman_client.request_mem(mem_required_with_overhead) except IOError as e: raise IOError("Failed to connect to qmemman: {!s}".format(e)) @@ -2863,7 +2861,7 @@ def _update_libvirt_domain(self): # workshop -- those are to be reworked later # - def get_prefmem(self): + def get_pref_mem(self): # TODO: qmemman is still xen specific untrusted_meminfo_key = self.app.vmm.xs.read( "", "/local/domain/{}/memory/meminfo".format(self.xid) @@ -2879,9 +2877,9 @@ def get_prefmem(self): if domain.mem_used is None: # apparently invalid xenstore content return 0 - domain.memory_maximum = self.get_mem_static_max() * 1024 + domain.mem_max = self.get_mem_static_max() * 1024 - return qubes.qmemman.algo.prefmem(domain) / 1024 + return qubes.qmemman.algo.pref_mem(domain) / 1024 def _clean_volume_config(config):