From 39329c8dada8b89a309ad36f463e478751b2ced3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Sun, 20 Oct 2024 05:29:09 +0200 Subject: [PATCH 1/2] Update Xen's memory overhead formula Experiments show that using memory hotplug or populate-on-demand makes no difference in required memory at startup. But PV vs PVH/HVM does make a difference - PV doesn't need extra per-MB overhead at all. On top of that, experimentally find the correct factor. Do it by starting VM (paused) with different parameters and compare `xl info free_memory` before and after. memory / maxmem: difference 400 / 4000: 434 600 / 4000: 634 400 / 400: 405 400 / 600: 407 400 / 2000: 418 2000 / 2000: 2018 600 / 600: 607 All above are with 2 vcpus. Testing with other vcpus count shows the 1.5MB per vcpu is quite accurate. As seen above, the initial memory doesn't affect the overhead. The maxmem counts. Applying linear regression to that shows it's about 0.008MB per MB of maxmem, so round it up to 8192 bytes. The base overhead of 4MB doesn't match exactly, but since the calculated number is smaller, leave it at 4MB as a safety margin. Fixes QubesOS/qubes-issues#9431 --- qubes/vm/qubesvm.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/qubes/vm/qubesvm.py b/qubes/vm/qubesvm.py index dd76c4a00..ab059a969 100644 --- a/qubes/vm/qubesvm.py +++ b/qubes/vm/qubesvm.py @@ -1761,10 +1761,12 @@ def request_memory(self, mem_required=None): try: mem_required_with_overhead = mem_required + MEM_OVERHEAD_BASE \ + self.vcpus * MEM_OVERHEAD_PER_VCPU - if self.use_memory_hotplug: - # extra overhead to account future hotplug memory - # 1 page per 1MB of RAM, see libxl__get_required_paging_memory() - mem_required_with_overhead += self.maxmem * 4096 + maxmem = self.maxmem if self.maxmem else self.memory + if self.virt_mode != "pv": + # extra overhead to account (possibly future hotplug) memory + # 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) From 2de9eb75670a523ffbc57df30ea5ecb996eb3a77 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Marczykowski-G=C3=B3recki?= Date: Thu, 24 Oct 2024 04:34:56 +0200 Subject: [PATCH 2/2] qmemman: always execute do_balance() under a lock Any memory adjustments must be done while holding a lock, to not interfere with client request handling. This is critical to prevent memory just freed for a new VM being re-allocated elsewhere. The domain_list_changed() function failed to do that - do_balance call was done after releasing the lock. It wasn't a problem for a long time because of Python's global interpreter lock. But Python 3.13 is finally starting to support proper parallel thread execution, and it revealed this bug. Fixes QubesOS/qubes-issues#9431 --- qubes/tools/qmemmand.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/qubes/tools/qmemmand.py b/qubes/tools/qmemmand.py index df61f172f..c3c8a7e2f 100644 --- a/qubes/tools/qmemmand.py +++ b/qubes/tools/qmemmand.py @@ -128,6 +128,12 @@ def domain_list_changed(self, refresh_only=False): self.watch_token_dict[i]) self.watch_token_dict.pop(i) system_state.del_domain(i) + + if not refresh_only: + try: + system_state.do_balance() + except: # pylint: disable=bare-except + self.log.exception('do_balance() failed') except: # pylint: disable=bare-except self.log.exception('Updating domain list failed') finally: @@ -135,13 +141,6 @@ def domain_list_changed(self, refresh_only=False): global_lock.release() self.log.debug('global_lock released') - if not refresh_only: - try: - system_state.do_balance() - except: # pylint: disable=bare-except - self.log.exception('do_balance() failed') - - def meminfo_changed(self, domain_id): self.log.debug('meminfo_changed(domain_id={!r})'.format(domain_id)) untrusted_meminfo_key = self.handle.read(