Skip to content

Commit 0d0d716

Browse files
committed
Set preload disposable memory before pause
For: QubesOS/qubes-issues#9917 For: QubesOS/qubes-issues#1512
1 parent 8de76b2 commit 0d0d716

File tree

5 files changed

+134
-22
lines changed

5 files changed

+134
-22
lines changed

qubes/qmemman/client.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,23 +19,31 @@
1919

2020
import socket
2121
import fcntl
22-
from typing import Optional
2322

2423

2524
class QMemmanClient:
2625
def __init__(self) -> None:
27-
self.sock: Optional[socket.socket] = None
26+
self.sock: socket.socket | None = None
2827

29-
def request_mem(self, amount) -> bool:
28+
def _send(self, data: str) -> bool:
3029
self.sock = socket.socket(socket.AF_UNIX)
3130
flags = fcntl.fcntl(self.sock.fileno(), fcntl.F_GETFD)
3231
flags |= fcntl.FD_CLOEXEC
3332
fcntl.fcntl(self.sock.fileno(), fcntl.F_SETFD, flags)
3433
self.sock.connect("/var/run/qubes/qmemman.sock")
35-
self.sock.send(str(int(amount)).encode("ascii") + b"\n")
34+
self.sock.send(data.encode("ascii"))
3635
received = self.sock.recv(1024).strip()
3736
return bool(received == b"OK")
3837

38+
def request_mem(self, amount: int | float) -> bool:
39+
return self._send("{}\n".format(int(amount)))
40+
41+
def set_mem(self, dom_memset: dict[int | str, int | float]) -> bool:
42+
dom_memset_str = " ".join(
43+
"{}:{}".format(key, value) for key, value in dom_memset.items()
44+
)
45+
return self._send("{}\n".format(dom_memset_str))
46+
3947
def close(self) -> None:
4048
assert isinstance(self.sock, socket.socket)
4149
self.sock.close()

qubes/qmemman/systemstate.py

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
CHECK_MB_S = 100
4141
MIN_TOTAL_MEMORY_TRANSFER = 150 * 1024 * 1024
4242
MIN_MEM_CHANGE_WHEN_UNDER_PREF = 15 * 1024 * 1024
43+
#: number of loop iterations for CHECK_PERIOD_S seconds
44+
CHECK_PERIOD = max(1, int((CHECK_PERIOD_S + 0.0) / BALLOON_DELAY))
45+
#: number of free memory bytes expected to get during CHECK_PERIOD_S
46+
#: seconds
47+
CHECK_DELTA = CHECK_PERIOD_S * CHECK_MB_S * 1024 * 1024
4348

4449

4550
class SystemState:
@@ -110,11 +115,13 @@ def get_free_xen_mem(self) -> int:
110115
)
111116
return xen_free - assigned_but_unused
112117

113-
# Refresh information on memory assigned to all domains
114-
def refresh_mem_actual(self) -> None:
118+
# Refresh information on memory assigned to all or specific domains
119+
def refresh_mem_actual(self, domid_list: Optional[list] = None) -> None:
115120
for domain in self.xc.domain_getinfo():
116121
domid = str(domain["domid"])
117122
if domid in self.dom_dict:
123+
if domid_list and domid not in domid_list:
124+
continue
118125
dom = self.dom_dict[domid]
119126
# Real memory usage
120127
dom.mem_current = domain["mem_kb"] * 1024
@@ -216,17 +223,12 @@ def do_balloon(self, mem_size) -> bool:
216223
for dom in self.dom_dict.values():
217224
dom.no_progress = False
218225

219-
#: number of loop iterations for CHECK_PERIOD_S seconds
220-
check_period = max(1, int((CHECK_PERIOD_S + 0.0) / BALLOON_DELAY))
221-
#: number of free memory bytes expected to get during CHECK_PERIOD_S
222-
#: seconds
223-
check_delta = CHECK_PERIOD_S * CHECK_MB_S * 1024 * 1024
224226
#: helper array for holding free memory size, CHECK_PERIOD_S seconds
225227
#: ago, at every loop iteration
226-
xenfree_ring = [0] * check_period
228+
xenfree_ring = [0] * CHECK_PERIOD
227229

228230
while True:
229-
self.log.debug("niter={:2d}".format(niter))
231+
self.log.debug("niter={:d}".format(niter))
230232
self.refresh_mem_actual()
231233
xenfree = self.get_free_xen_mem()
232234
self.log.info("xenfree={!r}".format(xenfree))
@@ -235,10 +237,10 @@ def do_balloon(self, mem_size) -> bool:
235237
return True
236238
# fail the request if over past CHECK_PERIOD_S seconds,
237239
# we got less than CHECK_MB_S MB/s on average
238-
ring_slot = niter % check_period
240+
ring_slot = niter % CHECK_PERIOD
239241
if (
240-
niter >= check_period
241-
and xenfree < xenfree_ring[ring_slot] + check_delta
242+
niter >= CHECK_PERIOD
243+
and xenfree < xenfree_ring[ring_slot] + CHECK_DELTA
242244
):
243245
return False
244246
xenfree_ring[ring_slot] = xenfree
@@ -265,6 +267,56 @@ def do_balloon(self, mem_size) -> bool:
265267
time.sleep(BALLOON_DELAY)
266268
niter = niter + 1
267269

270+
def do_balloon_dom(self, dom_memset: dict) -> bool:
271+
self.log.info("do_balloon_dom(dom_memset={!r})".format(dom_memset))
272+
niter = 0
273+
if not dom_memset:
274+
return False
275+
276+
domid_list = list(dom_memset.keys())
277+
dom_dict = {
278+
domid: state
279+
for domid, state in self.dom_dict.items()
280+
if domid in domid_list
281+
}
282+
283+
for _, dom in dom_dict.items():
284+
dom.no_progress = False
285+
286+
memset_reqs = {}
287+
for domid, memset in dom_memset.items():
288+
if memset == 0:
289+
mem_pref = qubes.qmemman.algo.pref_mem(dom_dict[domid])
290+
memset_reqs[domid] = mem_pref
291+
self.log.debug(
292+
"mem for dom '%s' is 0, using its pref '%s'",
293+
domid,
294+
mem_pref,
295+
)
296+
else:
297+
memset_reqs[domid] = memset
298+
299+
succeeded = []
300+
while True:
301+
self.log.debug("niter={:d}".format(niter))
302+
self.refresh_mem_actual(domid_list)
303+
for domid, dom in dom_dict.items():
304+
assert isinstance(dom.mem_actual, int)
305+
if (
306+
domid not in succeeded
307+
and dom.mem_actual / memset_reqs[domid] < 1.1
308+
):
309+
succeeded.append(domid)
310+
if all(dom in succeeded for dom in domid_list):
311+
return True
312+
if niter >= CHECK_PERIOD:
313+
return False
314+
for domid, memset in memset_reqs.items():
315+
self.mem_set(domid, memset)
316+
self.log.debug("sleeping for {} s".format(BALLOON_DELAY))
317+
time.sleep(BALLOON_DELAY)
318+
niter += 1
319+
268320
def refresh_meminfo(self, domid, untrusted_meminfo_key) -> None:
269321
self.log.debug(
270322
"refresh_meminfo(domid={}, untrusted_meminfo_key={!r})".format(

qubes/tools/qmemmand.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,7 @@ def handle(self):
191191
# self.request is the TCP socket connected to the client
192192
while True:
193193
self.data = self.request.recv(1024).strip()
194+
data_args = self.data.decode("ascii").split()
194195
self.log.debug("data=%r", self.data)
195196
if len(self.data) == 0:
196197
self.log.info("client disconnected, resuming membalance")
@@ -210,12 +211,25 @@ def handle(self):
210211
self.log.debug("GLOBAL_LOCK acquired")
211212

212213
got_lock = True
213-
if self.data.isdigit() and system_state.do_balloon(
214-
int(self.data.decode("ascii"))
215-
):
216-
resp = b"OK\n"
217-
else:
218-
resp = b"FAIL\n"
214+
215+
resp = "INVALID_ARG"
216+
if self.data.isdigit():
217+
resp = "FAIL"
218+
memory = int(data_args[0])
219+
if system_state.do_balloon(memory):
220+
resp = "OK"
221+
elif ":" in data_args[0]:
222+
resp = "FAIL"
223+
dom_memset = {
224+
str(key): int(value)
225+
for key, value in (
226+
pair.split(":") for pair in data_args
227+
)
228+
}
229+
if system_state.do_balloon_dom(dom_memset):
230+
resp = "OK"
231+
resp = str(resp + "\n").encode("ascii")
232+
219233
self.log.debug("resp={!r}".format(resp))
220234
self.request.send(resp)
221235
except BaseException as e:

qubes/vm/dispvm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,25 @@ async def on_domain_started_dispvm(
370370
self.app.save()
371371
self.preload_complete.set()
372372

373+
@qubes.events.handler("domain-pre-paused")
374+
async def on_domain_pre_paused(
375+
self, event, **kwargs
376+
): # pylint: disable=unused-argument
377+
if not self.is_preload or self.maxmem == 0:
378+
return
379+
qmemman_client = None
380+
try:
381+
qmemman_client = await asyncio.get_event_loop().run_in_executor(
382+
None, self.set_mem
383+
)
384+
except Exception as exc:
385+
self.log.warning(
386+
"Preload memory request before pause failed: %s", str(exc)
387+
)
388+
if qmemman_client:
389+
qmemman_client.close()
390+
raise
391+
373392
@qubes.events.handler("domain-paused")
374393
def on_domain_paused(
375394
self, event, **kwargs

qubes/vm/qubesvm.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2076,6 +2076,25 @@ def request_mem(self, mem_required=None):
20762076

20772077
return qmemman_client
20782078

2079+
def set_mem(self):
2080+
"""
2081+
Balloon to mem_pref.
2082+
"""
2083+
if not qmemman_present or self.maxmem == 0:
2084+
return None
2085+
2086+
qmemman_client = qubes.qmemman.client.QMemmanClient()
2087+
try:
2088+
result = qmemman_client.set_mem({self.xid: 0})
2089+
except IOError as e:
2090+
raise IOError("Failed to connect to qmemman: {!s}".format(e))
2091+
2092+
if not result:
2093+
qmemman_client.close()
2094+
self.log.warning("Failed to set memory")
2095+
2096+
return qmemman_client
2097+
20792098
@staticmethod
20802099
async def start_daemon(*command, input=None, **kwargs):
20812100
"""Start a daemon for the VM

0 commit comments

Comments
 (0)