Skip to content

Commit b8fbb61

Browse files
committed
Use virtiofsd for sharing file system data with host
Switch over to using virtiofsd for sharing file system data with the host. virtiofs is a file system designed for the needs of virtual machines and environments. That is in contrast to 9P fs, which we currently use for sharing data with the host, which is first and foremost a network file system. 9P is problematic if for no other reason that it lacks proper support for usage of the "open-unlink-fstat idiom", in which files are unlinked and later referenced via file descriptor (see #83). virtiofs does not have this problem. This change replaces usage of 9P with that of virtiofs. In order to work, virtiofsd needs a user space server. Contrary to what is the case for 9P, it is not currently integrated into Qemu itself and so we have to manage it separately (and require the user to install it). I benchmarked both the current master as well as this version with a bare-bones custom kernel: Benchmark 1: target/release/vmtest -k bzImage-9p 'echo test' Time (mean ± σ): 1.316 s ± 0.087 s [User: 0.462 s, System: 1.104 s] Range (min … max): 1.232 s … 1.463 s 10 runs Benchmark 1: target/release/vmtest -k bzImage-virtiofsd 'echo test' Time (mean ± σ): 1.244 s ± 0.011 s [User: 0.307 s, System: 0.358 s] Range (min … max): 1.227 s … 1.260 s 10 runs So it seems there is a ~0.7s speed up, on average (and significantly less system time being used). This is great, but I suspect a more pronounced speed advantage will be visible when working with large files, in which virtiofs is said to significantly outperform 9P (typically >2x from what I understand, but I have not done any benchmarks of that nature). A few other notes: - we solely rely on guest level read-only mounts to enforce read-only state. The virtiofsd recommended way is to use read-only bind mounts [0], but doing so would require root. - we are not using DAX, because it still is still incomplete and apparently requires building Qemu (?) from source. In any event, it should not change anything functionally and be solely a performance improvement. - interestingly, there may be the option of just consuming the virtiofsd crate as a library and not require any shelling out. That would be *much* nicer, but the current APIs make this somewhat cumbersome. I'd think we'd pretty much have to reimplement their entire main() functionality [1]. I consider this way out of scope for this first version. I have adjusted the configs, but because I don't have Docker handy I can't really create those kernel. CI seems incapable of producing the artifacts without doing a fully-blown release dance. No idea what empty is about, really. I suspect the test failures we see are because it lacks support? Some additional resources worth keeping around: - https://virtio-fs.gitlab.io/howto-boot.html - https://virtio-fs.gitlab.io/howto-qemu.html [0] https://gitlab.com/virtio-fs/virtiofsd/-/blob/main/README.md?ref_type=heads#faq [1] https://gitlab.com/virtio-fs/virtiofsd/-/blob/main/src/main.rs?ref_type=heads#L1242 Closes: #16 Closes: #83 Signed-off-by: Daniel Müller <[email protected]>
1 parent b713eba commit b8fbb61

File tree

11 files changed

+248
-66
lines changed

11 files changed

+248
-66
lines changed

.github/workflows/rust.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ env:
1616
jobs:
1717
build-test:
1818

19-
runs-on: ubuntu-latest
19+
# ubuntu-24.04 is the earliest image with a `virtiofsd` package.
20+
runs-on: ubuntu-24.04
2021

2122
steps:
2223
- uses: actions/checkout@v3
@@ -36,7 +37,7 @@ jobs:
3637
run: |
3738
sudo apt-get update
3839
# Virtualization deps
39-
sudo apt-get install -y qemu-system-x86-64 qemu-guest-agent qemu-utils ovmf
40+
sudo apt-get install -y qemu-system-x86-64 qemu-guest-agent qemu-utils ovmf virtiofsd
4041
4142
- name: Cache test assets
4243
uses: actions/cache@v3

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ readme = "README.md"
77
version = "0.14.0"
88
edition = "2021"
99
license = "Apache-2.0"
10-
rust-version = "1.70.0"
10+
rust-version = "1.80.0"
1111

1212
[dependencies]
1313
anyhow = "1.0.66"

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,15 @@ The following are required dependencies, grouped by location:
2626

2727
Host machine:
2828

29-
* [`qemu`](https://pkgs.org/download/qemu)
29+
* [`qemu`](https://pkgs.org/download/qemu) (version 5.9 or higher)
3030
* [`qemu-guest-agent`](https://pkgs.org/search/?q=qemu-guest-agent)
3131
* [`OVMF`](https://pkgs.org/download/ovmf)
32+
* [`virtiofsd`](https://gitlab.com/virtio-fs/virtiofsd)
3233

3334
Virtual machine image:
3435

3536
* `qemu-guest-agent`
36-
* Kernel 9p filesystem support, either compiled in or as modules (see kernel
37+
* Kernel `virtiofs` support, either compiled in or as modules (see kernel
3738
dependencies)
3839
* Most (if not all) distros already ship support as modules or better
3940

@@ -42,9 +43,8 @@ Kernel:
4243
* `CONFIG_VIRTIO=y`
4344
* `CONFIG_VIRTIO_PCI=y`
4445
* `CONFIG_VIRTIO_CONSOLE=y`
45-
* `CONFIG_NET_9P=y`
46-
* `CONFIG_NET_9P_VIRTIO=y`
47-
* `CONFIG_9P_FS=y`
46+
* `CONFIG_FUSE_FS=y`
47+
* `CONFIG_VIRTIO_FS=y`
4848

4949
Note the virtual machine image dependencies are only required if you're using
5050
the `image` target parameter. Likewise, the same applies for kernel

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ pub use crate::vmtest::*;
1717
mod qemu;
1818
mod qga;
1919
mod util;
20+
mod virtiofsd;

src/output.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ use anyhow::Result;
1010
/// Receivers should treat failures as terminal and not expect any more
1111
/// updates.
1212
pub enum Output {
13+
/// On-host initialization starts
14+
InitializeStart,
15+
/// Initialization finished with provided with provided result
16+
InitializeEnd(Result<()>),
17+
1318
/// VM boot begins
1419
BootStart,
1520
/// Output related to VM boot

src/qemu.rs

Lines changed: 77 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,17 @@ use tinytemplate::{format_unescaped, TinyTemplate};
2727
use crate::output::Output;
2828
use crate::qga::QgaWrapper;
2929
use crate::util::gen_sock;
30+
use crate::virtiofsd::Virtiofsd;
3031
use crate::{Mount, Target, VMConfig};
3132

3233
const INIT_TEMPLATE: &str = include_str!("init/init.sh.template");
3334
const COMMAND_TEMPLATE: &str = include_str!("init/command.template");
34-
// Needs to be `/dev/root` for kernel to "find" the 9pfs as rootfs
35-
const ROOTFS_9P_FS_MOUNT_TAG: &str = "/dev/root";
36-
const SHARED_9P_FS_MOUNT_TAG: &str = "vmtest-shared";
35+
const ROOT_FS_MOUNT_TAG: &str = "rootfs";
36+
const SHARED_FS_MOUNT_TAG: &str = "vmtest-shared";
3737
const COMMAND_OUTPUT_PORT_NAME: &str = "org.qemu.virtio_serial.0";
3838
const MAGIC_INTERACTIVE_COMMAND: &str = "-";
3939

40-
const SHARED_9P_FS_MOUNT_PATH: &str = "/mnt/vmtest";
41-
const MOUNT_OPTS_9P_FS: &str = "trans=virtio,cache=mmap,msize=1048576";
40+
const SHARED_FS_MOUNT_PATH: &str = "/mnt/vmtest";
4241
const OVMF_PATHS: &[&str] = &[
4342
// Fedora
4443
"/usr/share/edk2/ovmf/OVMF_CODE.fd",
@@ -55,6 +54,8 @@ type QmpUnixStream = qapi::Stream<BufReader<UnixStream>, UnixStream>;
5554
/// Represents a single QEMU instance
5655
pub struct Qemu {
5756
process: Command,
57+
/// `virtiofsd` instances for each of the mounts in use.
58+
virtiofsds: Vec<Virtiofsd>,
5859
qga_sock: PathBuf,
5960
qmp_sock: PathBuf,
6061
command: String,
@@ -241,6 +242,18 @@ fn guest_agent_args(sock: &Path) -> Vec<OsString> {
241242
args
242243
}
243244

245+
/// Generate general arguments necessary for working with `virtiofs`.
246+
fn virtiofs_general_args(vm: &VMConfig) -> Vec<OsString> {
247+
let mut args: Vec<OsString> = Vec::new();
248+
249+
args.push("-object".into());
250+
args.push(format!("memory-backend-memfd,id=mem,share=on,size={}", vm.memory.as_str()).into());
251+
args.push("-numa".into());
252+
args.push("node,memdev=mem".into());
253+
254+
args
255+
}
256+
244257
/// Generate arguments for full KVM virtualization if host supports it
245258
fn kvm_args(arch: &str) -> Vec<&'static str> {
246259
let mut args = Vec::new();
@@ -291,30 +304,17 @@ fn machine_protocol_args(sock: &Path) -> Vec<OsString> {
291304
args
292305
}
293306

294-
/// Generate arguments for setting up 9p FS server on host
307+
/// Generate per-file-system arguments necessary for working with `virtiofs`.
295308
///
296-
/// `id` is the ID for the FS export (currently unused AFAICT)
309+
/// `id` is the ID for the FS export
297310
/// `mount_tag` is used inside guest to find the export
298-
fn plan9_fs_args(host_shared: &Path, id: &str, mount_tag: &str, ro: bool) -> Vec<OsString> {
311+
fn virtiofs_per_fs_args(virtiofsd: &Virtiofsd, id: &str, mount_tag: &str) -> Vec<OsString> {
299312
let mut args: Vec<OsString> = Vec::new();
300313

301-
args.push("-virtfs".into());
302-
303-
let mut arg = OsString::new();
304-
arg.push(format!("local,id={id},path="));
305-
arg.push(if host_shared.as_os_str().is_empty() {
306-
// This case occurs when the config file path is just "vmtest.toml"
307-
Path::new(".")
308-
} else {
309-
host_shared
310-
});
311-
arg.push(format!(
312-
",mount_tag={mount_tag},security_model=none,multidevs=remap"
313-
));
314-
if ro {
315-
arg.push(",readonly=on")
316-
}
317-
args.push(arg);
314+
args.push("-chardev".into());
315+
args.push(format!("socket,id={id},path={}", virtiofsd.socket_path().display()).into());
316+
args.push("-device".into());
317+
args.push(format!("vhost-user-fs-pci,queue-size=1024,chardev={id},tag={mount_tag}").into());
318318

319319
args
320320
}
@@ -371,9 +371,9 @@ fn kernel_args(
371371
// The guest kernel command line args
372372
let mut cmdline: Vec<OsString> = Vec::new();
373373

374-
// Tell kernel the rootfs is 9p
375-
cmdline.push("rootfstype=9p".into());
376-
cmdline.push(format!("rootflags={}", MOUNT_OPTS_9P_FS).into());
374+
// Tell kernel the rootfs is on a virtiofs and what "tag" it uses.
375+
cmdline.push("rootfstype=virtiofs".into());
376+
cmdline.push(format!("root={ROOT_FS_MOUNT_TAG}").into());
377377

378378
// Mount rootfs readable/writable to make experience more smooth.
379379
// Lots of tools expect to be able to write logs or change global
@@ -455,16 +455,6 @@ fn vmconfig_args(vm: &VMConfig) -> Vec<OsString> {
455455
vm.memory.clone().into(),
456456
];
457457

458-
for mount in vm.mounts.values() {
459-
let name = format!("mount{}", hash(&mount.host_path));
460-
args.append(&mut plan9_fs_args(
461-
&mount.host_path,
462-
&name,
463-
&name,
464-
!mount.writable,
465-
));
466-
}
467-
468458
let mut extra_args = vm
469459
.extra_args
470460
.clone()
@@ -650,6 +640,7 @@ impl Qemu {
650640
let command_sock = gen_sock("cmdout");
651641
let (init, guest_init) = gen_init(&target.rootfs).context("Failed to generate init")?;
652642

643+
let mut virtiofsds = Vec::new();
653644
let mut c = Command::new(format!("qemu-system-{}", target.arch));
654645

655646
c.args(QEMU_DEFAULT_ARGS)
@@ -660,6 +651,7 @@ impl Qemu {
660651
.args(machine_args(&target.arch))
661652
.args(machine_protocol_args(&qmp_sock))
662653
.args(guest_agent_args(&qga_sock))
654+
.args(virtiofs_general_args(&target.vm))
663655
.args(virtio_serial_args(&command_sock));
664656
// Always ensure the rootfs is first.
665657
if let Some(image) = &target.image {
@@ -668,28 +660,42 @@ impl Qemu {
668660
c.args(uefi_firmware_args(target.vm.bios.as_deref()));
669661
}
670662
} else if let Some(kernel) = &target.kernel {
671-
c.args(plan9_fs_args(
672-
target.rootfs.as_path(),
663+
let virtiofsd = Virtiofsd::new(target.rootfs.as_path())?;
664+
c.args(virtiofs_per_fs_args(
665+
&virtiofsd,
673666
"root",
674-
ROOTFS_9P_FS_MOUNT_TAG,
675-
false,
667+
ROOT_FS_MOUNT_TAG,
676668
));
677669
c.args(kernel_args(
678670
kernel,
679671
&target.arch,
680672
guest_init.as_path(),
681673
target.kernel_args.as_ref(),
682674
));
675+
virtiofsds.push(virtiofsd);
683676
} else {
684677
panic!("Config validation should've enforced XOR");
685678
}
679+
686680
// Now add the shared mount and other extra mounts.
687-
c.args(plan9_fs_args(
688-
host_shared,
681+
let virtiofsd = Virtiofsd::new(host_shared)?;
682+
c.args(virtiofs_per_fs_args(
683+
&virtiofsd,
689684
"shared",
690-
SHARED_9P_FS_MOUNT_TAG,
691-
false,
685+
SHARED_FS_MOUNT_TAG,
692686
));
687+
virtiofsds.push(virtiofsd);
688+
689+
for mount in target.vm.mounts.values() {
690+
let name = format!("mount{}", hash(&mount.host_path));
691+
let virtiofsd = Virtiofsd::new(&mount.host_path)?;
692+
c.args(virtiofs_per_fs_args(
693+
&virtiofsd,
694+
&name,
695+
&name,
696+
));
697+
virtiofsds.push(virtiofsd);
698+
}
693699
c.args(vmconfig_args(&target.vm));
694700

695701
if log_enabled!(Level::Error) {
@@ -706,6 +712,7 @@ impl Qemu {
706712

707713
let mut qemu = Self {
708714
process: c,
715+
virtiofsds,
709716
qga_sock,
710717
qmp_sock,
711718
command: target.command.to_string(),
@@ -838,16 +845,18 @@ impl Qemu {
838845
// We can race with VM/qemu coming up. So retry a few times with growing backoff.
839846
let mut rc = 0;
840847
for i in 0..5 {
841-
let mount_opts = if ro {
842-
format!("{},ro", MOUNT_OPTS_9P_FS)
843-
} else {
844-
MOUNT_OPTS_9P_FS.into()
845-
};
848+
let mut args = vec![
849+
"-t", "virtiofs", mount_tag, guest_path
850+
];
851+
if ro {
852+
args.push("-oro")
853+
}
854+
846855
rc = run_in_vm(
847856
qga,
848857
&output_fn,
849858
"mount",
850-
&["-t", "9p", "-o", &mount_opts, mount_tag, guest_path],
859+
&args,
851860
false,
852861
None,
853862
)?;
@@ -1052,7 +1061,7 @@ impl Qemu {
10521061
// Mount shared directory inside guest
10531062
let _ = self.updates.send(Output::SetupStart);
10541063
if let Err(e) =
1055-
self.mount_in_guest(qga, SHARED_9P_FS_MOUNT_PATH, SHARED_9P_FS_MOUNT_TAG, false)
1064+
self.mount_in_guest(qga, SHARED_FS_MOUNT_PATH, SHARED_FS_MOUNT_TAG, false)
10561065
{
10571066
return Err(e).context("Failed to mount shared directory in guest");
10581067
}
@@ -1075,6 +1084,21 @@ impl Qemu {
10751084
/// Errors and return status are reported through the `updates` channel passed into the
10761085
/// constructor.
10771086
pub fn run(mut self) {
1087+
let _ = self.updates.send(Output::InitializeStart);
1088+
for phase_fn in [Virtiofsd::launch, Virtiofsd::await_launched] {
1089+
for virtiofsd in self.virtiofsds.iter_mut() {
1090+
match phase_fn(virtiofsd) {
1091+
Ok(()) => (),
1092+
Err(e) => {
1093+
let _ = self.updates.send(Output::InitializeEnd(Err(e)));
1094+
return;
1095+
}
1096+
}
1097+
}
1098+
}
1099+
1100+
let _ = self.updates.send(Output::InitializeEnd(Ok(())));
1101+
10781102
// Start QEMU
10791103
let (mut child, qga, mut qmp) = match self.boot_vm() {
10801104
Ok((c, qga, qmp)) => (c, qga, qmp),

src/ui.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,16 @@ impl Ui {
177177
};
178178

179179
match &msg {
180+
Output::InitializeStart => {
181+
stage = Stage::new(term.clone(), &heading("Initializing host environment", 2), Some(stage));
182+
stages += 1;
183+
}
184+
Output::InitializeEnd(r) => {
185+
if let Err(e) = r {
186+
error_out_stage(&mut stage, e);
187+
errors += 1;
188+
}
189+
}
180190
Output::BootStart => {
181191
stage = Stage::new(term.clone(), &heading("Booting", 2), Some(stage));
182192
stages += 1;

0 commit comments

Comments
 (0)