Virtio balloon

This commit is contained in:
Rob Blanckaert
2025-02-02 12:36:37 -07:00
committed by Fabian
parent 368efb036f
commit 2b162b1e85
8 changed files with 445 additions and 4 deletions
+2 -1
View File
@@ -81,7 +81,7 @@ CARGO_FLAGS=$(CARGO_FLAGS_SAFE) -C target-feature=+bulk-memory -C target-feature
CORE_FILES=const.js config.js io.js main.js lib.js buffer.js ide.js pci.js floppy.js \
memory.js dma.js pit.js vga.js ps2.js rtc.js uart.js \
acpi.js apic.js ioapic.js \
state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js \
state.js ne2k.js sb16.js virtio.js virtio_console.js virtio_net.js virtio_balloon.js \
bus.js log.js cpu.js debug.js \
elf.js kernel.js
LIB_FILES=9p.js filesystem.js jor1k.js marshall.js
@@ -327,6 +327,7 @@ devices-test: all-debug
./tests/devices/fetch_network.js
USE_VIRTIO=1 ./tests/devices/fetch_network.js
./tests/devices/wisp_network.js
./tests/devices/virtio_balloon.js
rust-test: $(RUST_FILES)
env RUSTFLAGS="-D warnings" RUST_BACKTRACE=full RUST_TEST_THREADS=1 cargo test -- --nocapture
+1
View File
@@ -30,6 +30,7 @@
<script src="src/ne2k.js"></script>
<script src="src/state.js"></script>
<script src="src/virtio.js"></script>
<script src="src/virtio_balloon.js"></script>
<script src="src/virtio_console.js"></script>
<script src="src/virtio_net.js"></script>
<script src="src/bus.js"></script>
+1 -1
View File
@@ -39,7 +39,7 @@ let files = [
"src/virtio.js",
"src/virtio_console.js",
"src/virtio_net.js",
//"src/virtio_balloon.js",
"src/virtio_balloon.js",
"src/bus.js",
"src/debug.js",
+1
View File
@@ -304,6 +304,7 @@ V86.prototype.continue_init = async function(emulator, options)
settings.preserve_mac_from_state_image = options.preserve_mac_from_state_image;
settings.mac_address_translation = options.mac_address_translation;
settings.cpuid_level = options.cpuid_level;
settings.virtio_balloon = options.virtio_balloon;
settings.virtio_console = options.virtio_console;
settings.virtio_net = options.virtio_net;
settings.screen_options = options.screen_options;
+6 -1
View File
@@ -420,6 +420,7 @@ CPU.prototype.get_state = function()
state[81] = this.devices.uart3;
state[82] = this.devices.virtio_console;
state[83] = this.devices.virtio_net;
state[84] = this.devices.virtio_balloon;
return state;
};
@@ -552,6 +553,7 @@ CPU.prototype.set_state = function(state)
this.devices.uart3 && this.devices.uart3.set_state(state[81]);
this.devices.virtio_console && this.devices.virtio_console.set_state(state[82]);
this.devices.virtio_net && this.devices.virtio_net.set_state(state[83]);
this.devices.virtio_balloon && this.devices.virtio_balloon.set_state(state[84]);
this.fw_value = state[62];
@@ -651,7 +653,7 @@ CPU.prototype.pack_memory = function()
CPU.prototype.unpack_memory = function(bitmap, packed_memory)
{
this.zero_memory(this.memory_size[0]);
this.zero_memory(0, this.memory_size[0]);
const page_count = this.memory_size[0] >> 12;
let packed_page = 0;
@@ -979,6 +981,9 @@ CPU.prototype.init = function(settings, device_bus)
{
this.devices.virtio_console = new VirtioConsole(this, device_bus);
}
if(settings.virtio_balloon) {
this.devices.virtio_balloon = new VirtioBalloon(this, device_bus);
}
if(true)
{
+3 -1
View File
@@ -39,7 +39,9 @@ pub fn allocate_memory(size: u32) -> u32 {
}
#[no_mangle]
pub unsafe fn zero_memory(size: u32) { ptr::write_bytes(mem8, 0, size as usize); }
pub unsafe fn zero_memory(addr: u32, size: u32) {
ptr::write_bytes(mem8.offset(addr as isize), 0, size as usize);
}
#[allow(non_upper_case_globals)]
pub static mut vga_mem8: *mut u8 = ptr::null_mut();
+218
View File
@@ -0,0 +1,218 @@
"use strict";
// https://docs.oasis-open.org/virtio/virtio/v1.2/csd01/virtio-v1.2-csd01.html#x1-2900003
const VIRTIO_BALLOON_F_MUST_TELL_HOST = 0;
const VIRTIO_BALLOON_F_STATS_VQ = 1;
const VIRTIO_BALLOON_F_DEFLATE_ON_OOM = 2;
const VIRTIO_BALLOON_F_FREE_PAGE_HINT = 3;
const STAT_NAMES = [
"SWAP_IN",
"SWAP_OUT",
"MAJFLT",
"MINFLT",
"MEMFREE",
"MEMTOT",
"AVAIL",
"CACHES",
"HTLB_PGALLOC",
"HTLB_PGFAIL",
];
/**
* @constructor
* @param {CPU} cpu
* @param {BusConnector} bus
*/
function VirtioBalloon(cpu, bus)
{
/** @const @type {BusConnector} */
this.bus = bus;
this.num_pages = 0;
this.actual = 0;
this.fp_cmd = 0;
this.zeroed = 0;
const queues = [
{size_supported: 32, notify_offset: 0},
{size_supported: 32, notify_offset: 0},
{size_supported: 2, notify_offset: 1},
{size_supported: 64, notify_offset: 2},
];
//setInterval(() => this.GetStats(console.log.bind(console, "STATS")), 10000);
/** @type {VirtIO} */
this.virtio = new VirtIO(cpu,
{
name: "virtio-balloon",
pci_id: 0x0B << 3,
device_id: 0x1045,
subsystem_device_id: 5,
common:
{
initial_port: 0xD800,
queues: queues,
features:
[
VIRTIO_BALLOON_F_STATS_VQ,
VIRTIO_BALLOON_F_FREE_PAGE_HINT,
VIRTIO_F_VERSION_1,
],
on_driver_ok: () => {
dbg_log("Balloon setup", LOG_PCI);
},
},
notification:
{
initial_port: 0xD900,
single_handler: false,
handlers:
[
(queue_id) =>
{
const queue = this.virtio.queues[queue_id];
while(queue.has_request())
{
const bufchain = queue.pop_request();
const buffer = new Uint8Array(bufchain.length_readable);
bufchain.get_next_blob(buffer);
this.virtio.queues[queue_id].push_reply(bufchain);
let n = buffer.byteLength / 4;
this.actual += (queue_id === 0 ? n : -n);
//console.log(queue_id === 0 ? "Inflate" : "Deflate", this.num_pages, this.actual, bufchain.read_buffers);
}
this.virtio.queues[queue_id].flush_replies();
},
(queue_id) =>
{
const queue = this.virtio.queues[queue_id];
if(queue.has_request())
{
const bufchain = queue.pop_request();
const buffer = new Uint8Array(bufchain.length_readable);
bufchain.get_next_blob(buffer);
let result = {};
for(let i = 0; i < bufchain.length_readable; i += 10) {
let [cat, value] = marshall.Unmarshall(["h", "d"], buffer, { offset : i });
result[STAT_NAMES[cat]] = value;
}
this.virtio.queues[queue_id].push_reply(bufchain);
if(this.stats_cb) this.stats_cb(result);
}
},
(queue_id) =>
{
const queue = this.virtio.queues[queue_id];
while(queue.has_request())
{
const bufchain = queue.pop_request();
if(bufchain.length_readable > 0) {
const buffer = new Uint8Array(bufchain.length_readable);
bufchain.get_next_blob(buffer);
let [cmd] = marshall.Unmarshall(["w"], buffer, { offset : 0 });
if(cmd === 0) {
if(this.free_cb) this.free_cb(this.zeroed);
if(this.fp_cmd > 1) this.fp_cmd = 1; // Signal done
this.virtio.notify_config_changes();
}
}
if(bufchain.length_writable > 0) {
// console.log("Free pages hinted", bufchain.read_buffers, bufchain.write_buffers);
let zeros = new Uint8Array(0);
for(let i = 0; i < bufchain.write_buffers.length; ++i) {
let b = bufchain.write_buffers[i];
this.zeroed += b.len;
this.virtio.cpu.zero_memory(b.addr_low, b.len);
}
}
this.virtio.queues[queue_id].push_reply(bufchain);
}
this.virtio.queues[queue_id].flush_replies();
},
],
},
isr_status:
{
initial_port: 0xD700,
},
device_specific:
{
initial_port: 0xD600,
struct:
[
{
bytes: 4,
name: "num_pages",
read: () => this.num_pages,
write: data => { /* read only */ },
},
{
bytes: 4,
name: "actual",
read: () => {
return this.actual;
},
write: data => { /* read only */ },
},
{
bytes: 4,
name: "free_page_hint_cmd_id",
read: () => this.fp_cmd,
write: data => { /* read only */ },
}
]
},
});
}
VirtioBalloon.prototype.Inflate = function(amount) {
this.num_pages += amount;
this.virtio.notify_config_changes();
};
VirtioBalloon.prototype.Deflate = function(amount) {
this.num_pages -= amount;
this.virtio.notify_config_changes();
};
VirtioBalloon.prototype.Cleanup = function(cb) {
this.fp_cmd = 2;
this.free_cb = cb;
this.zeroed = 0;
this.virtio.notify_config_changes();
};
VirtioBalloon.prototype.get_state = function()
{
const state = [];
state[0] = this.virtio;
state[1] = this.num_pages;
state[2] = this.actual;
return state;
};
VirtioBalloon.prototype.set_state = function(state)
{
this.virtio.set_state(state[0]);
this.num_pages = state[1];
this.actual = state[2];
};
VirtioBalloon.prototype.GetStats = function(data)
{
this.stats_cb = data;
const queue = this.virtio.queues[2];
while(queue.has_request())
{
const bufchain = queue.pop_request();
this.virtio.queues[2].push_reply(bufchain);
}
this.virtio.queues[2].flush_replies();
};
VirtioBalloon.prototype.Reset = function() {
};
+213
View File
@@ -0,0 +1,213 @@
#!/usr/bin/env node
"use strict";
process.on("unhandledRejection", exn => { throw exn; });
const TEST_RELEASE_BUILD = +process.env.TEST_RELEASE_BUILD;
const V86 = require(`../../build/${TEST_RELEASE_BUILD ? "libv86" : "libv86-debug"}.js`).V86;
const assert = require("assert").strict;
const SHOW_LOGS = false;
function wait(time) {
return new Promise((res) => setTimeout(res, time));
}
let s1;
const tests =
[
{
name: "Use space",
start: async () =>
{
emulator.serial0_send("udhcpc\n");
emulator.serial0_send("free -h\n");
emulator.serial0_send("lspci -k\n");
emulator.serial0_send("dd if=/dev/urandom of=/tmp/file bs=1M count=64\n");
emulator.serial0_send("du -h /tmp/file\n");
emulator.serial0_send("free -h\n");
emulator.serial0_send("rm /tmp/file\n");
emulator.serial0_send("free -h\n");
emulator.serial0_send("echo done\n");
},
end_trigger: "done",
end: (capture) =>
{
},
},
{
name: "Delete space",
start: async () =>
{
emulator.serial0_send("echo 3 > /proc/sys/vm/drop_caches\n");
emulator.serial0_send("df -h; echo done\n");
},
end_trigger: "done",
end: (capture) =>
{
},
},
{
name: "Reclaim space",
start: async () =>
{
s1 = await emulator.save_state();
emulator.v86.cpu.devices.virtio_balloon.GetStats(s => { console.log(s); });
emulator.v86.cpu.devices.virtio_balloon.Cleanup(async (zeroed) => {
console.log("Zeroed ", zeroed / 1024 / 1024, "MB of pages");
await wait(2000);
emulator.serial0_send("echo done\n");
});
},
end_trigger: "done",
end: async (capture) =>
{
let s2 = await emulator.save_state();
let saved = s1.byteLength - s2.byteLength;
console.log(`Saved ${saved>>20}MB of ${s1.byteLength>>20}MB (${saved/s1.byteLength*100}%)`);
assert(saved > 1000000, "not enough space saved");
emulator.serial0_send("echo done\n");
},
},
{
name: "Wait",
start: async () =>
{
},
end_trigger: "done",
end: async (capture) =>
{
},
},
{
name: "Get Memory",
start: async () =>
{
emulator.serial0_send("free -m; echo done\n");
},
end_trigger: "done",
end: async (capture) =>
{
},
},
{
name: "Inflate Balloon",
start: async () =>
{
emulator.v86.cpu.devices.virtio_balloon.Inflate(30000);
await wait(1000);
emulator.serial0_send("free -m; echo done\n");
},
end_trigger: "done",
end: async (capture) =>
{
assert(emulator.v86.cpu.devices.virtio_balloon.actual === 30000, "Got 30000 pages from vm");
},
},
];
const emulator = new V86({
bios: { url: __dirname + "/../../bios/seabios.bin" },
vga_bios: { url: __dirname + "/../../bios/vgabios.bin" },
bzimage: { url: __dirname + "/../../images/buildroot-bzimage68.bin" },
autostart: true,
memory_size: 200 * 1024 * 1024,
disable_jit: +process.env.DISABLE_JIT,
virtio_console: true,
virtio_balloon: true,
uart0: true,
net_device: {
relay_url: "fetch",
type: "virtio",
},
log_level: SHOW_LOGS ? 0x400000 : 0,
cmdline: "console=/dev/ttyS0"
});
let test_num = 0;
let booted = false;
let line = "";
let capture = "";
let end_trigger;
emulator.bus.register("emulator-started", function()
{
console.log("Booting now, please stand by");
});
emulator.add_listener("serial0-output-byte", function(byte)
{
const chr = String.fromCharCode(byte);
if(chr < " " && chr !== "\n" && chr !== "\t" || chr > "~")
{
return;
}
let new_line = "";
if(chr === "\n")
{
console.log(" Captured: %s", line);
new_line = line;
capture += line + "\n";
line = "";
}
else
{
line += chr;
}
if(new_line === end_trigger)
{
let test_has_failed = false;
try {
tests[test_num].end(capture);
} catch(e) {
console.log(e);
test_has_failed = true;
}
if(!test_has_failed)
{
console.log("[+] Test #%d passed: %s", test_num, tests[test_num].name);
}
else
{
if(tests[test_num].allow_failure)
{
console.warn("[!] Test #%d failed: %s (failure allowed)", test_num, tests[test_num].name);
}
else
{
console.error("[-] Test #%d failed: %s", test_num, tests[test_num].name);
process.exit(1);
}
}
test_num++;
}
if(!booted && line.endsWith("~% ") || new_line === end_trigger)
{
booted = true;
if(test_num >= tests.length)
{
emulator.destroy();
console.log("Tests finished.");
}
else
{
console.log("Starting test #%d: %s", test_num, tests[test_num].name);
capture = "";
end_trigger = tests[test_num].end_trigger;
tests[test_num].start();
}
}
});