From 70fd86598acc002befd45bea74e52debe947179e Mon Sep 17 00:00:00 2001 From: vipnydav Date: Tue, 4 Nov 2025 14:09:41 +0000 Subject: [PATCH 1/4] Allow both Vectored and non vectored reads to co exist within same mount --- conversions.go | 13 +++++-------- fuseops/ops.go | 5 +++-- mount_config.go | 7 ------- samples/mount_readbenchfs/mount.go | 11 +++++------ samples/readbenchfs/readbenchfs.go | 17 ++++++----------- 5 files changed, 19 insertions(+), 34 deletions(-) diff --git a/conversions.go b/conversions.go index 12132d8c..915e0878 100644 --- a/conversions.go +++ b/conversions.go @@ -394,11 +394,8 @@ func convertInMessage( Uid: inMsg.Header().Uid, }, } - if !config.UseVectoredRead { - // Use part of the incoming message storage as the read buffer - // For vectored zero-copy reads, don't allocate any buffers - to.Dst = inMsg.GetFree(int(in.Size)) - } + // Use part of the incoming message storage as the read buffer. + to.Dst = inMsg.GetFree(int(in.Size)) o = to case fusekernel.OpReaddir: @@ -933,10 +930,10 @@ func (c *Connection) kernelResponseForOp( } case *fuseops.ReadFileOp: - if o.Dst != nil { - m.Append(o.Dst) - } else { + if o.Data != nil { m.Append(o.Data...) + } else { + m.Append(o.Dst) } m.ShrinkTo(buffer.OutMessageHeaderSize + o.BytesRead) diff --git a/fuseops/ops.go b/fuseops/ops.go index c0bb76a8..607889c7 100644 --- a/fuseops/ops.go +++ b/fuseops/ops.go @@ -704,11 +704,12 @@ type ReadFileOp struct { Size int64 // The destination buffer, whose length gives the size of the read. - // For vectored reads, this field is always nil as the buffer is not provided. + // The file system can write to this buffer for non-vectored reads. Dst []byte // Set by the file system: - // A list of slices of data to send back to the client for vectored reads. + // A list of slices of data to send back to the client. + // If this field is populated, the contents of `Dst` will be ignored. Data [][]byte // Set by the file system: the number of bytes read. diff --git a/mount_config.go b/mount_config.go index 7f38a620..748c9a46 100644 --- a/mount_config.go +++ b/mount_config.go @@ -157,13 +157,6 @@ type MountConfig struct { // actually utilise any form of qualifiable UNIX permissions. DisableDefaultPermissions bool - // Use vectored reads. - // Vectored read allows file systems to avoid memory copying overhead if - // the data is already in memory when they return it to FUSE. - // When turned on, ReadFileOp.Dst is always nil and the FS must return data - // being read from the file as a list of slices in ReadFileOp.Data. - UseVectoredRead bool - // OS X only. // // The name of the mounted volume, as displayed in the Finder. If empty, a diff --git a/samples/mount_readbenchfs/mount.go b/samples/mount_readbenchfs/mount.go index 6fa37c7f..d7abe964 100644 --- a/samples/mount_readbenchfs/mount.go +++ b/samples/mount_readbenchfs/mount.go @@ -4,17 +4,17 @@ import ( "context" "flag" "fmt" - "github.com/jacobsa/fuse" - "github.com/jacobsa/fuse/samples/readbenchfs" "log" "net/http" _ "net/http/pprof" "os" + + "github.com/jacobsa/fuse" + "github.com/jacobsa/fuse/samples/readbenchfs" ) var fMountPoint = flag.String("mount_point", "", "Path to mount point.") var fReadOnly = flag.Bool("read_only", false, "Mount in read-only mode.") -var fVectored = flag.Bool("vectored", false, "Use vectored read.") var fDebug = flag.Bool("debug", false, "Enable debug logging.") var fPprof = flag.Int("pprof", 0, "Enable pprof profiling on the specified port.") @@ -27,7 +27,7 @@ func main() { }() } - server, err := readbenchfs.NewReadBenchServer(*fVectored) + server, err := readbenchfs.NewReadBenchServer() if err != nil { log.Fatalf("makeFS: %v", err) } @@ -38,8 +38,7 @@ func main() { } cfg := &fuse.MountConfig{ - ReadOnly: *fReadOnly, - UseVectoredRead: *fVectored, + ReadOnly: *fReadOnly, } if *fDebug { diff --git a/samples/readbenchfs/readbenchfs.go b/samples/readbenchfs/readbenchfs.go index 61b18dff..0a0152b4 100644 --- a/samples/readbenchfs/readbenchfs.go +++ b/samples/readbenchfs/readbenchfs.go @@ -15,11 +15,12 @@ package readbenchfs import ( - "golang.org/x/net/context" "io" "math/rand" "os" + "golang.org/x/net/context" + "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/fuseops" "github.com/jacobsa/fuse/fuseutil" @@ -27,8 +28,7 @@ import ( type readBenchFS struct { fuseutil.NotImplementedFileSystem - buf []byte - useVectoredRead bool + buf []byte } // 1 TB @@ -36,13 +36,12 @@ const fileSize = 1024 * 1024 * 1024 * 1024 var _ fuseutil.FileSystem = &readBenchFS{} -func NewReadBenchServer(useVectoredRead bool) (server fuse.Server, err error) { +func NewReadBenchServer() (server fuse.Server, err error) { // 1 GB of random data to exceed CPU cache buf := make([]byte, 1024*1024*1024) rand.Read(buf) server = fuseutil.NewFileSystemServer(&readBenchFS{ - buf: buf, - useVectoredRead: useVectoredRead, + buf: buf, }) return } @@ -134,11 +133,7 @@ func (fs *readBenchFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) err if e-s > end-pos { e = s + end - pos } - if fs.useVectoredRead { - op.Data = append(op.Data, fs.buf[s:e]) - } else { - copy(op.Dst[pos-op.Offset:], fs.buf[s:]) - } + op.Data = append(op.Data, fs.buf[s:e]) pos = op.Offset + e } op.BytesRead = int(end - op.Offset) From 68bb20e2c00b59171464f37349a5486edcbd2563 Mon Sep 17 00:00:00 2001 From: vipnydav Date: Wed, 5 Nov 2025 04:52:18 +0000 Subject: [PATCH 2/4] Add tests --- samples/memfs/inode.go | 18 ++++++++++++++++++ samples/memfs/memfs.go | 12 ++++++++++-- samples/memfs/memfs_test.go | 38 +++++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/samples/memfs/inode.go b/samples/memfs/inode.go index a462d6fe..bc8628b0 100644 --- a/samples/memfs/inode.go +++ b/samples/memfs/inode.go @@ -322,6 +322,24 @@ func (in *inode) ReadAt(p []byte, off int64) (int, error) { return n, nil } +// Perform a vectored read from the file's contents. +// +// REQUIRES: in.isFile() +func (in *inode) VectoredReadAt(op *fuseops.ReadFileOp) { + if !in.isFile() { + panic("VectoredReadAt called on non-file.") + } + + // For testing, split the file into 1-byte chunks for the vectored read. + op.Data = make([][]byte, 0, len(in.contents)) + for i := int64(0); i < int64(len(in.contents)); i++ { + if i >= op.Offset && op.BytesRead < int(op.Size) { + op.Data = append(op.Data, in.contents[i:i+1]) + op.BytesRead++ + } + } +} + // Write to the file's contents. See documentation for ioutil.WriterAt. // // REQUIRES: in.isFile() diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index 584e0fca..d1d5323d 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -31,8 +31,9 @@ import ( ) const ( - FileOpenFlagsXattrName = "fileOpenFlagsXattr" - CheckFileOpenFlagsFileName = "checkFileOpenFlags" + FileOpenFlagsXattrName = "fileOpenFlagsXattr" + CheckFileOpenFlagsFileName = "checkFileOpenFlags" + EnableVectoredReadXattrName = "enableVectoredRead" ) type memFS struct { @@ -662,6 +663,13 @@ func (fs *memFS) ReadFile( // Find the inode in question. inode := fs.getInodeOrDie(op.Inode) + // Check if we should perform a vectored read. + if _, ok := inode.xattrs[EnableVectoredReadXattrName]; ok { + inode.VectoredReadAt(op) + op.Callback = fs.readFileCallback + return nil + } + // Serve the request. var err error op.BytesRead, err = inode.ReadAt(op.Dst, op.Offset) diff --git a/samples/memfs/memfs_test.go b/samples/memfs/memfs_test.go index b5d156c3..0b021130 100644 --- a/samples/memfs/memfs_test.go +++ b/samples/memfs/memfs_test.go @@ -1919,6 +1919,44 @@ func (t *MemFSTest) RemoveXAttr() { AssertEq(fuse.ENOATTR, err) } +func (t *MemFSTest) NonVectoredRead() { + var err error + const contents = "taco" + const fileName = "foo" + filePath := path.Join(t.Dir, fileName) + + // Create a file. + err = ioutil.WriteFile(filePath, []byte(contents), 0600) + AssertEq(nil, err) + + // Read the file. This will be a non-vectored read by default. + readContents, err := ioutil.ReadFile(filePath) + AssertEq(nil, err) + + ExpectEq(contents, string(readContents)) +} + +func (t *MemFSTest) VectoredRead() { + var err error + const contents = "taco" + const fileName = "foo" + filePath := path.Join(t.Dir, fileName) + + // Create a file. + err = ioutil.WriteFile(filePath, []byte(contents), 0600) + AssertEq(nil, err) + + // Enable vectored reads for memfs for this file. + err = unix.Setxattr(filePath, memfs.EnableVectoredReadXattrName, []byte("true"), 0) + AssertEq(nil, err) + + // Read the file. + readContents, err := ioutil.ReadFile(filePath) + AssertEq(nil, err) + + ExpectEq(contents, string(readContents)) +} + //////////////////////////////////////////////////////////////////////// // Mknod //////////////////////////////////////////////////////////////////////// From 9bab92dba05c03e9b273b8e2a3f8a4d2982a8255 Mon Sep 17 00:00:00 2001 From: vipnydav Date: Wed, 5 Nov 2025 13:47:45 +0000 Subject: [PATCH 3/4] Reintroduce VectoredRead flag --- mount_config.go | 8 ++++++++ samples/memfs/inode.go | 9 +++++++-- samples/memfs/memfs.go | 16 ++++++++-------- samples/mount_readbenchfs/mount.go | 3 ++- samples/readbenchfs/readbenchfs.go | 17 +++++++++++------ 5 files changed, 36 insertions(+), 17 deletions(-) diff --git a/mount_config.go b/mount_config.go index 748c9a46..e407b380 100644 --- a/mount_config.go +++ b/mount_config.go @@ -215,6 +215,14 @@ type MountConfig struct { // If EnableReaddirplus is true and this flag is false, the kernel will always // use ReaddirPlus for directory listing. EnableAutoReaddirplus bool + + // Use vectored reads. + // This flag is a no-op and is kept for backward compatibility. + // Vectored read allows file systems to avoid memory copying overhead if + // the data is already in memory when they return it to FUSE. + // When turned on, ReadFileOp.Dst is always nil and the FS must return data + // being read from the file as a list of slices in ReadFileOp.Data. + UseVectoredRead bool } type FUSEImpl uint8 diff --git a/samples/memfs/inode.go b/samples/memfs/inode.go index bc8628b0..cb044844 100644 --- a/samples/memfs/inode.go +++ b/samples/memfs/inode.go @@ -325,7 +325,7 @@ func (in *inode) ReadAt(p []byte, off int64) (int, error) { // Perform a vectored read from the file's contents. // // REQUIRES: in.isFile() -func (in *inode) VectoredReadAt(op *fuseops.ReadFileOp) { +func (in *inode) VectoredReadAt(op *fuseops.ReadFileOp) (bytesRead int, err error) { if !in.isFile() { panic("VectoredReadAt called on non-file.") } @@ -335,9 +335,14 @@ func (in *inode) VectoredReadAt(op *fuseops.ReadFileOp) { for i := int64(0); i < int64(len(in.contents)); i++ { if i >= op.Offset && op.BytesRead < int(op.Size) { op.Data = append(op.Data, in.contents[i:i+1]) - op.BytesRead++ + bytesRead++ } } + + if bytesRead < int(op.Size) { + err = io.EOF + } + return bytesRead, err } // Write to the file's contents. See documentation for ioutil.WriterAt. diff --git a/samples/memfs/memfs.go b/samples/memfs/memfs.go index d1d5323d..8e3b877a 100644 --- a/samples/memfs/memfs.go +++ b/samples/memfs/memfs.go @@ -663,17 +663,17 @@ func (fs *memFS) ReadFile( // Find the inode in question. inode := fs.getInodeOrDie(op.Inode) - // Check if we should perform a vectored read. + // Serve the request. + var err error // Check if we should perform a vectored read. if _, ok := inode.xattrs[EnableVectoredReadXattrName]; ok { - inode.VectoredReadAt(op) - op.Callback = fs.readFileCallback - return nil + // For testing purpose only. + // Set attribute (name=EnableVectoredReadXattrName, value=true) to test + // whether vectored read is working correctly. + op.BytesRead, err = inode.VectoredReadAt(op) + } else { + op.BytesRead, err = inode.ReadAt(op.Dst, op.Offset) } - // Serve the request. - var err error - op.BytesRead, err = inode.ReadAt(op.Dst, op.Offset) - op.Callback = fs.readFileCallback // Don't return EOF errors; we just indicate EOF to fuse using a short read. diff --git a/samples/mount_readbenchfs/mount.go b/samples/mount_readbenchfs/mount.go index d7abe964..53c983fd 100644 --- a/samples/mount_readbenchfs/mount.go +++ b/samples/mount_readbenchfs/mount.go @@ -15,6 +15,7 @@ import ( var fMountPoint = flag.String("mount_point", "", "Path to mount point.") var fReadOnly = flag.Bool("read_only", false, "Mount in read-only mode.") +var fVectored = flag.Bool("vectored", false, "Use vectored read.") var fDebug = flag.Bool("debug", false, "Enable debug logging.") var fPprof = flag.Int("pprof", 0, "Enable pprof profiling on the specified port.") @@ -27,7 +28,7 @@ func main() { }() } - server, err := readbenchfs.NewReadBenchServer() + server, err := readbenchfs.NewReadBenchServer(*fVectored) if err != nil { log.Fatalf("makeFS: %v", err) } diff --git a/samples/readbenchfs/readbenchfs.go b/samples/readbenchfs/readbenchfs.go index 0a0152b4..61b18dff 100644 --- a/samples/readbenchfs/readbenchfs.go +++ b/samples/readbenchfs/readbenchfs.go @@ -15,12 +15,11 @@ package readbenchfs import ( + "golang.org/x/net/context" "io" "math/rand" "os" - "golang.org/x/net/context" - "github.com/jacobsa/fuse" "github.com/jacobsa/fuse/fuseops" "github.com/jacobsa/fuse/fuseutil" @@ -28,7 +27,8 @@ import ( type readBenchFS struct { fuseutil.NotImplementedFileSystem - buf []byte + buf []byte + useVectoredRead bool } // 1 TB @@ -36,12 +36,13 @@ const fileSize = 1024 * 1024 * 1024 * 1024 var _ fuseutil.FileSystem = &readBenchFS{} -func NewReadBenchServer() (server fuse.Server, err error) { +func NewReadBenchServer(useVectoredRead bool) (server fuse.Server, err error) { // 1 GB of random data to exceed CPU cache buf := make([]byte, 1024*1024*1024) rand.Read(buf) server = fuseutil.NewFileSystemServer(&readBenchFS{ - buf: buf, + buf: buf, + useVectoredRead: useVectoredRead, }) return } @@ -133,7 +134,11 @@ func (fs *readBenchFS) ReadFile(ctx context.Context, op *fuseops.ReadFileOp) err if e-s > end-pos { e = s + end - pos } - op.Data = append(op.Data, fs.buf[s:e]) + if fs.useVectoredRead { + op.Data = append(op.Data, fs.buf[s:e]) + } else { + copy(op.Dst[pos-op.Offset:], fs.buf[s:]) + } pos = op.Offset + e } op.BytesRead = int(end - op.Offset) From 36e30761491ec18453720bb54ae92a03071142f4 Mon Sep 17 00:00:00 2001 From: vipnydav Date: Thu, 6 Nov 2025 10:15:10 +0000 Subject: [PATCH 4/4] Updated comment --- mount_config.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/mount_config.go b/mount_config.go index e407b380..e0e64470 100644 --- a/mount_config.go +++ b/mount_config.go @@ -216,12 +216,25 @@ type MountConfig struct { // use ReaddirPlus for directory listing. EnableAutoReaddirplus bool - // Use vectored reads. - // This flag is a no-op and is kept for backward compatibility. - // Vectored read allows file systems to avoid memory copying overhead if - // the data is already in memory when they return it to FUSE. - // When turned on, ReadFileOp.Dst is always nil and the FS must return data - // being read from the file as a list of slices in ReadFileOp.Data. + // UseVectoredRead is a legacy flag kept for backward compatibility. It is now a no-op. + // + // The term vectored read was a misnomer for this flag. Its actual meaning was that + // the file system would allocate its own buffers for read operations. + // + // When this flag was disabled, the FUSE library provided a buffer + // in ReadFileOp.Dst. This buffer utilized unused space within the + // incoming kernel request message's buffer (InMessage.storage), which is + // sized to hold a page plus the maximum write size. Since read requests are + // small, there is ample room for read responses. + // + // Conversely, when this flag was enabled, ReadFileOp.Dst was always nil. + // This allowed file systems to avoid memory copying overhead if the data was + // already in memory, by requiring them to return the data as a list of slices + // in ReadFileOp.Data. + // + // Currently, both the read mechanisms can coexist. The library's behavior is + // to always provide ReadFileOp.Dst. If the file system populates ReadFileOp.Data, + // that data will be used for a vectored read, irrespective of this flag's value. UseVectoredRead bool }