Skip to content

Commit dcbc30a

Browse files
authored
Merge pull request #1628 from refi64/virtiofsd-linux
qemu: Add virtiofs support on Linux
2 parents 658ef43 + 6323939 commit dcbc30a

File tree

12 files changed

+305
-23
lines changed

12 files changed

+305
-23
lines changed

docs/experimental.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
The following features are experimental and subject to change:
44

55
- `mountType: 9p`
6+
- `mountType: virtiofs` on Linux
67
- `vmType: vz` and relevant configurations (`mountType: virtiofs`, `rosetta`, `[]networks.vzNAT`)
78
- `arch: riscv64`
89
- `video.display: vnc` and relevant configuration (`video.vnc.display`)
9-
- `mode: user-v2` in `networks.yml` and relevant configuration in `lima.yaml`
10+
- `mode: user-v2` in `networks.yml` and relevant configuration in `lima.yaml`
1011
- `audio.device`
1112

1213
The following commands are experimental and subject to change:

docs/mount.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,19 +88,21 @@ The "9p" mount type requires Lima v0.10.0 or later.
8888
> **Warning**
8989
> "virtiofs" mode is experimental
9090

91-
| :zap: Requirement | Lima >= 0.14, macOS >= 13.0 |
92-
|-------------------|-----------------------------|
91+
| :zap: Requirement | Lima >= 0.14, macOS >= 13.0 | Lima >= 0.17.0, Linux, QEMU 4.2.0+, virtiofsd (Rust version) |
92+
|-------------------|-----------------------------| ------------------------------------------------------------ |
9393

94-
The "virtiofs" mount type is implemented by using apple Virtualization.Framework shared directory (uses virtio-fs) device.
94+
The "virtiofs" mount type is implemented via the virtio-fs device by using apple Virtualization.Framework shared directory on macOS and virtiofsd on Linux.
9595
Linux guest kernel must enable the CONFIG_VIRTIO_FS support for this support.
9696

9797
An example configuration:
9898
```yaml
99-
vmType: "vz"
99+
vmType: "vz" # only for macOS; Linux uses 'qemu'
100100
mountType: "virtiofs"
101101
mounts:
102102
- location: "~"
103103
```
104104

105105
#### Caveats
106-
- The "virtiofs" mount type is supported only on macOS 13 or above with `vmType: vz` config. See also [`vmtype.md`](./vmtype.md).
106+
- For macOS, the "virtiofs" mount type is supported only on macOS 13 or above with `vmType: vz` config. See also [`vmtype.md`](./vmtype.md).
107+
- For Linux, the "virtiofs" mount type requires the [Rust version of virtiofsd](https://gitlab.com/virtio-fs/virtiofsd).
108+
Using the version from QEMU (usually packaged as `qemu-virtiofsd`) will *not* work, as it requires root access to run.

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Container orchestration:
4444
Optional feature enablers:
4545
- [`vmnet.yaml`](./vmnet.yaml): ⭐enable [`vmnet.framework`](../docs/network.md)
4646
- [`experimental/9p.yaml`](experimental/9p.yaml): [experimental] use 9p mount type
47+
- [`experimental/virtiofs-linux.yaml`](experimental/9p.yaml): [experimental] use virtiofs mount type for Linux
4748
- [`experimental/riscv64.yaml`](experimental/riscv64.yaml): [experimental] RISC-V
4849
- [`experimental/net-user-v2.yaml`](experimental/net-user-v2.yaml): [experimental] user-v2 network
4950
to enable VM-to-VM communication without root privilege
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# This example requires Lima v0.17.0 or later, running on Linux with:
2+
# - QEMU v4.2.0 or later.
3+
# - virtiofsd's Rust implementation: https://gitlab.com/virtio-fs/virtiofsd
4+
# The QEMU version (qemu-virtiofsd) will *not* work, as it requires root access
5+
# for all operations.
6+
images:
7+
# Try to use release-yyyyMMdd image if available. Note that release-yyyyMMdd will be removed after several months.
8+
- location: "https://cloud-images.ubuntu.com/releases/23.04/release-20230502/ubuntu-23.04-server-cloudimg-amd64.img"
9+
arch: "x86_64"
10+
digest: "sha256:13965c84c65cbab0b34326ac34ac0c47a88030f9dff80e6391e56cb9077cadd0"
11+
- location: "https://cloud-images.ubuntu.com/releases/23.04/release-20230502/ubuntu-23.04-server-cloudimg-arm64.img"
12+
arch: "aarch64"
13+
digest: "sha256:76a0fc791ed48ea8d0325463e2748e06aa3836292df1178ee4af8daf12a643bf"
14+
# Fallback to the latest release image.
15+
# Hint: run `limactl prune` to invalidate the cache
16+
- location: "https://cloud-images.ubuntu.com/releases/23.04/release/ubuntu-23.04-server-cloudimg-amd64.img"
17+
arch: "x86_64"
18+
- location: "https://cloud-images.ubuntu.com/releases/23.04/release/ubuntu-23.04-server-cloudimg-arm64.img"
19+
arch: "aarch64"
20+
21+
mounts:
22+
- location: "~"
23+
- location: "/tmp/lima"
24+
writable: true
25+
26+
mountType: "virtiofs"

hack/test-example.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ case "$NAME" in
5454
"9p")
5555
CHECKS["snapshot-online"]=""
5656
;;
57+
"virtiofs-linux")
58+
CHECKS["snapshot-online"]=""
59+
;;
5760
"vmnet")
5861
CHECKS["vmnet"]=1
5962
;;

pkg/limayaml/defaults.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ const (
3131
Default9pMsize string = "128KiB"
3232
Default9pCacheForRO string = "fscache"
3333
Default9pCacheForRW string = "mmap"
34+
35+
DefaultVirtiofsQueueSize int = 1024
3436
)
3537

3638
func defaultContainerdArchives() []File {
@@ -510,6 +512,9 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
510512
if mount.NineP.Cache != nil {
511513
mounts[i].NineP.Cache = mount.NineP.Cache
512514
}
515+
if mount.Virtiofs.QueueSize != nil {
516+
mounts[i].Virtiofs.QueueSize = mount.Virtiofs.QueueSize
517+
}
513518
if mount.Writable != nil {
514519
mounts[i].Writable = mount.Writable
515520
}
@@ -543,6 +548,9 @@ func FillDefault(y, d, o *LimaYAML, filePath string) {
543548
if mount.NineP.Msize == nil {
544549
mounts[i].NineP.Msize = pointer.String(Default9pMsize)
545550
}
551+
if mount.Virtiofs.QueueSize == nil {
552+
mounts[i].Virtiofs.QueueSize = pointer.Int(DefaultVirtiofsQueueSize)
553+
}
546554
if mount.Writable == nil {
547555
mount.Writable = pointer.Bool(false)
548556
}

pkg/limayaml/defaults_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ func TestFillDefault(t *testing.T) {
186186
expect.Mounts[0].NineP.ProtocolVersion = pointer.String(Default9pProtocolVersion)
187187
expect.Mounts[0].NineP.Msize = pointer.String(Default9pMsize)
188188
expect.Mounts[0].NineP.Cache = pointer.String(Default9pCacheForRO)
189+
expect.Mounts[0].Virtiofs.QueueSize = pointer.Int(DefaultVirtiofsQueueSize)
189190
// Only missing Mounts field is Writable, and the default value is also the null value: false
190191

191192
expect.MountType = pointer.String(NINEP)
@@ -369,6 +370,7 @@ func TestFillDefault(t *testing.T) {
369370
expect.Mounts[0].NineP.ProtocolVersion = pointer.String(Default9pProtocolVersion)
370371
expect.Mounts[0].NineP.Msize = pointer.String(Default9pMsize)
371372
expect.Mounts[0].NineP.Cache = pointer.String(Default9pCacheForRO)
373+
expect.Mounts[0].Virtiofs.QueueSize = pointer.Int(DefaultVirtiofsQueueSize)
372374
expect.HostResolver.Hosts = map[string]string{
373375
"default": d.HostResolver.Hosts["default"],
374376
}
@@ -494,6 +496,9 @@ func TestFillDefault(t *testing.T) {
494496
Msize: pointer.String("8KiB"),
495497
Cache: pointer.String("none"),
496498
},
499+
Virtiofs: Virtiofs{
500+
QueueSize: pointer.Int(2048),
501+
},
497502
},
498503
},
499504
Provision: []Provision{
@@ -569,6 +574,7 @@ func TestFillDefault(t *testing.T) {
569574
expect.Mounts[0].NineP.ProtocolVersion = pointer.String("9p2000")
570575
expect.Mounts[0].NineP.Msize = pointer.String("8KiB")
571576
expect.Mounts[0].NineP.Cache = pointer.String("none")
577+
expect.Mounts[0].Virtiofs.QueueSize = pointer.Int(2048)
572578

573579
expect.MountType = pointer.String(NINEP)
574580

pkg/limayaml/limayaml.go

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -80,11 +80,12 @@ type Image struct {
8080
type Disk = string
8181

8282
type Mount struct {
83-
Location string `yaml:"location" json:"location"` // REQUIRED
84-
MountPoint string `yaml:"mountPoint,omitempty" json:"mountPoint,omitempty"`
85-
Writable *bool `yaml:"writable,omitempty" json:"writable,omitempty"`
86-
SSHFS SSHFS `yaml:"sshfs,omitempty" json:"sshfs,omitempty"`
87-
NineP NineP `yaml:"9p,omitempty" json:"9p,omitempty"`
83+
Location string `yaml:"location" json:"location"` // REQUIRED
84+
MountPoint string `yaml:"mountPoint,omitempty" json:"mountPoint,omitempty"`
85+
Writable *bool `yaml:"writable,omitempty" json:"writable,omitempty"`
86+
SSHFS SSHFS `yaml:"sshfs,omitempty" json:"sshfs,omitempty"`
87+
NineP NineP `yaml:"9p,omitempty" json:"9p,omitempty"`
88+
Virtiofs Virtiofs `yaml:"virtiofs,omitempty" json:"virtiofs,omitempty"`
8889
}
8990

9091
type SFTPDriver = string
@@ -107,6 +108,10 @@ type NineP struct {
107108
Cache *string `yaml:"cache,omitempty" json:"cache,omitempty"`
108109
}
109110

111+
type Virtiofs struct {
112+
QueueSize *int `yaml:"queueSize,omitempty" json:"queueSize,omitempty"`
113+
}
114+
110115
type SSH struct {
111116
LocalPort *int `yaml:"localPort,omitempty" json:"localPort,omitempty"`
112117

pkg/limayaml/validate.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@ func Validate(y LimaYAML, warn bool) error {
159159
return fmt.Errorf("field `mountType` must be %q or %q or %q, got %q", REVSSHFS, NINEP, VIRTIOFS, *y.MountType)
160160
}
161161

162+
if warn && runtime.GOOS != "linux" {
163+
for i, mount := range y.Mounts {
164+
if mount.Virtiofs.QueueSize != nil {
165+
logrus.Warnf("field mounts[%d].virtiofs.queueSize is only supported on Linux", i)
166+
}
167+
}
168+
}
169+
162170
// y.Firmware.LegacyBIOS is ignored for aarch64, but not a fatal error.
163171

164172
for i, p := range y.Provision {
@@ -442,6 +450,9 @@ func warnExperimental(y LimaYAML) {
442450
if *y.MountType == NINEP {
443451
logrus.Warn("`mountType: 9p` is experimental")
444452
}
453+
if *y.MountType == VIRTIOFS && runtime.GOOS == "linux" {
454+
logrus.Warn("`mountType: virtiofs` on Linux is experimental")
455+
}
445456
if *y.VMType == VZ {
446457
logrus.Warn("`vmType: vz` is experimental")
447458
}

pkg/qemu/qemu.go

Lines changed: 124 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package qemu
22

33
import (
44
"bytes"
5+
"encoding/json"
56
"errors"
67
"fmt"
78
"io/fs"
@@ -478,6 +479,12 @@ func Cmdline(cfg Config) (string, []string, error) {
478479
memBytes = adjustMemBytesDarwinARM64HVF(memBytes, accel, features)
479480
args = appendArgsIfNoConflict(args, "-m", strconv.Itoa(int(memBytes>>20)))
480481

482+
if *y.MountType == limayaml.VIRTIOFS {
483+
args = appendArgsIfNoConflict(args, "-object",
484+
fmt.Sprintf("memory-backend-file,id=virtiofs-shm,size=%s,mem-path=/dev/shm,share=on", strconv.Itoa(int(memBytes))))
485+
args = appendArgsIfNoConflict(args, "-numa", "node,memdev=virtiofs-shm")
486+
}
487+
481488
// CPU
482489
cpu := y.CPUType[*y.Arch]
483490
if runtime.GOOS == "darwin" && runtime.GOARCH == "amd64" {
@@ -775,7 +782,7 @@ func Cmdline(cfg Config) (string, []string, error) {
775782

776783
// We also want to enable vsock here, but QEMU does not support vsock for macOS hosts
777784

778-
if *y.MountType == limayaml.NINEP {
785+
if *y.MountType == limayaml.NINEP || *y.MountType == limayaml.VIRTIOFS {
779786
for i, f := range y.Mounts {
780787
tag := fmt.Sprintf("mount%d", i)
781788
location, err := localpathutil.Expand(f.Location)
@@ -785,14 +792,30 @@ func Cmdline(cfg Config) (string, []string, error) {
785792
if err := os.MkdirAll(location, 0755); err != nil {
786793
return "", nil, err
787794
}
788-
options := "local"
789-
options += fmt.Sprintf(",mount_tag=%s", tag)
790-
options += fmt.Sprintf(",path=%s", location)
791-
options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
792-
if !*f.Writable {
793-
options += ",readonly"
795+
796+
switch *y.MountType {
797+
case limayaml.NINEP:
798+
options := "local"
799+
options += fmt.Sprintf(",mount_tag=%s", tag)
800+
options += fmt.Sprintf(",path=%s", location)
801+
options += fmt.Sprintf(",security_model=%s", *f.NineP.SecurityModel)
802+
if !*f.Writable {
803+
options += ",readonly"
804+
}
805+
args = append(args, "-virtfs", options)
806+
case limayaml.VIRTIOFS:
807+
// Note that read-only mode is not supported on the QEMU/virtiofsd side yet:
808+
// https://gitlab.com/virtio-fs/virtiofsd/-/issues/97
809+
chardev := fmt.Sprintf("char-virtiofs-%d", i)
810+
vhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, i))
811+
args = append(args, "-chardev", fmt.Sprintf("socket,id=%s,path=%s", chardev, vhostSock))
812+
813+
options := "vhost-user-fs-pci"
814+
options += fmt.Sprintf(",queue-size=%d", *f.Virtiofs.QueueSize)
815+
options += fmt.Sprintf(",chardev=%s", chardev)
816+
options += fmt.Sprintf(",tag=%s", tag)
817+
args = append(args, "-device", options)
794818
}
795-
args = append(args, "-virtfs", options)
796819
}
797820
}
798821

@@ -812,6 +835,99 @@ func Cmdline(cfg Config) (string, []string, error) {
812835
return exe, args, nil
813836
}
814837

838+
func FindVirtiofsd(qemuExe string) (string, error) {
839+
type vhostUserBackend struct {
840+
BackendType string `json:"type"`
841+
Binary string `json:"binary"`
842+
}
843+
844+
currentUser, err := user.Current()
845+
if err != nil {
846+
return "", err
847+
}
848+
849+
const relativePath = "share/qemu/vhost-user"
850+
851+
binDir := filepath.Dir(qemuExe) // "/usr/local/bin"
852+
usrDir := filepath.Dir(binDir) // "/usr/local"
853+
userLocalDir := filepath.Join(currentUser.HomeDir, ".local") // "$HOME/.local"
854+
855+
candidates := []string{
856+
filepath.Join(userLocalDir, relativePath),
857+
filepath.Join(usrDir, relativePath),
858+
}
859+
860+
if usrDir != "/usr" {
861+
candidates = append(candidates, filepath.Join("/usr", relativePath))
862+
}
863+
864+
for _, vhostConfigsDir := range candidates {
865+
logrus.Debugf("Checking vhost directory %s", vhostConfigsDir)
866+
867+
configEntries, err := os.ReadDir(vhostConfigsDir)
868+
if err != nil {
869+
logrus.Debugf("Failed to list vhost directory: %v", err)
870+
continue
871+
}
872+
873+
for _, configEntry := range configEntries {
874+
logrus.Debugf("Checking vhost config %s", configEntry.Name())
875+
if !strings.HasSuffix(configEntry.Name(), ".json") {
876+
continue
877+
}
878+
879+
var config vhostUserBackend
880+
contents, err := os.ReadFile(filepath.Join(vhostConfigsDir, configEntry.Name()))
881+
if err == nil {
882+
err = json.Unmarshal(contents, &config)
883+
}
884+
885+
if err != nil {
886+
logrus.Warnf("Failed to load vhost-user config %s: %v", configEntry.Name(), err)
887+
continue
888+
}
889+
logrus.Debugf("%v", config)
890+
891+
if config.BackendType != "fs" {
892+
continue
893+
}
894+
895+
// Only rust virtiofsd supports --version, so use that to make sure this isn't
896+
// QEMU's virtiofsd, which requires running as root.
897+
cmd := exec.Command(config.Binary, "--version")
898+
output, err := cmd.CombinedOutput()
899+
if err != nil {
900+
logrus.Warnf("Failed to run %s --version (is this QEMU virtiofsd?): %s: %s",
901+
config.Binary, err, output)
902+
continue
903+
}
904+
905+
return config.Binary, nil
906+
}
907+
}
908+
909+
return "", errors.New("Failed to locate virtiofsd")
910+
}
911+
912+
func VirtiofsdCmdline(cfg Config, mountIndex int) ([]string, error) {
913+
mount := cfg.LimaYAML.Mounts[mountIndex]
914+
location, err := localpathutil.Expand(mount.Location)
915+
if err != nil {
916+
return nil, err
917+
}
918+
919+
vhostSock := filepath.Join(cfg.InstanceDir, fmt.Sprintf(filenames.VhostSock, mountIndex))
920+
// qemu_driver has to wait for the socket to appear, so make sure any old ones are removed here.
921+
if err := os.Remove(vhostSock); err != nil && !errors.Is(err, fs.ErrNotExist) {
922+
logrus.Warnf("Failed to remove old vhost socket: %v", err)
923+
}
924+
925+
return []string{
926+
"--socket-path", vhostSock,
927+
"--shared-dir", location,
928+
}, nil
929+
}
930+
815931
func getExe(arch limayaml.Arch) (string, []string, error) {
816932
exeBase := "qemu-system-" + arch
817933
var args []string

0 commit comments

Comments
 (0)