Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions fuse/read.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ type statefulResult interface {
Stateful() (fd uintptr, sz int)
}

// movableResult is a ReadResult whose pages the kernel may move into the
// inode's page cache instead of copying, when spliced to /dev/fuse.
type movableResult interface {
SpliceMove() bool
}

// ReadResultFd is the read return for zero-copy file data.
type readResultFd struct {
// Splice from the following file.
Expand Down
27 changes: 24 additions & 3 deletions fuse/splice_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"os"
"syscall"

"golang.org/x/sys/unix"

"github.com/hanwen/go-fuse/v2/splice"
)

Expand All @@ -35,6 +37,12 @@ func (r *fuseFD) trySplice(req *request, readResult ReadResult) error {
// readResult.Size(), so req.outHeaderBuf is correct for the optimistic case.
total := len(req.outHeaderBuf) + len(req.outDataBuf) + readResult.Size()

// The filesystem decides per reply whether its pages may be moved.
spliceFlags := 0
if m, ok := readResult.(movableResult); ok && m.SpliceMove() {
spliceFlags = unix.SPLICE_F_MOVE

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In future we might add more splice flags, so this is brittle.

Suggested change
spliceFlags = unix.SPLICE_F_MOVE
spliceFlags |= unix.SPLICE_F_MOVE

}

pair, err := splice.Get()
if err != nil {
return err
Expand Down Expand Up @@ -90,12 +98,12 @@ func (r *fuseFD) trySplice(req *request, readResult ReadResult) error {
// New length.
req.serializeHeader(payloadLen)

return r.trySplice(req, ReadResultPipe(pair, payloadLen))
return r.trySplice(req, &pipeReadResult{pair: pair, size: payloadLen, move: spliceFlags != 0})

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I would have expected spliceFlags & unix.splice_F_MOVE != 0 - in future if we add more bits the existing code will break.
  2. Can we use ReadResultPipeMove?!

}

// Write header + payload to /dev/fuse.
if cerr := r.withFD(func(fd int) {
_, err = pair.WriteTo(uintptr(fd), total)
_, err = pair.WriteToFlags(uintptr(fd), total, spliceFlags)
}); cerr != nil {
return cerr
}
Expand All @@ -105,6 +113,7 @@ func (r *fuseFD) trySplice(req *request, readResult ReadResult) error {
type pipeReadResult struct {
pair *splice.Pair
size int
move bool
}

func (r *pipeReadResult) Done() {
Expand All @@ -128,9 +137,21 @@ func (r *pipeReadResult) Stateful() (fd uintptr, sz int) {
return r.pair.ReadFd(), r.size
}

func (r *pipeReadResult) SpliceMove() bool { return r.move }

// ReadResultPipe returns a [ReadResult] of `size` bytes that was preloaded
// into the given pipe. The pipe is discarded with splice.Done()
// after the read completes.
func ReadResultPipe(pipe *splice.Pair, size int) ReadResult {
return &pipeReadResult{pipe, size}
return &pipeReadResult{pair: pipe, size: size}
}

// ReadResultPipeMove is [ReadResultPipe] for a pipe whose pages the kernel may
// move into the inode's page cache instead of copying into it. A move removes
// each page from the page cache of the file it came from and waits on its
// writeback, so use it only for pages the filesystem can lose. Only readahead
// reads are eligible. A ReadResult can also opt in by implementing
// SpliceMove() bool.
func ReadResultPipeMove(pipe *splice.Pair, size int) ReadResult {
return &pipeReadResult{pair: pipe, size: size, move: true}
}
28 changes: 28 additions & 0 deletions fuse/splice_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package fuse

import "testing"

// trySplice reaches the zero-copy path only for a result satisfying
// statefulResult, so the type carrying the move flag must keep satisfying it:
// wrapping a ReadResult hides Stateful and falls back to copying.
func TestReadResultPipeSpliceMove(t *testing.T) {
for _, tc := range []struct {
name string
r ReadResult
want bool
}{
{"ReadResultPipe", ReadResultPipe(nil, 4096), false},
{"ReadResultPipeMove", ReadResultPipeMove(nil, 4096), true},
} {
if _, ok := tc.r.(statefulResult); !ok {
t.Errorf("%s: not a statefulResult; trySplice would copy instead of splice", tc.name)
}
m, ok := tc.r.(movableResult)
if !ok {
t.Fatalf("%s: not a movableResult", tc.name)
}
if got := m.SpliceMove(); got != tc.want {
t.Errorf("%s: SpliceMove() = %v, want %v", tc.name, got, tc.want)
}
}
}
9 changes: 8 additions & 1 deletion splice/pair_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,18 @@ func (p *Pair) LoadFrom(fd uintptr, sz int) (int, error) {
}

func (p *Pair) WriteTo(fd uintptr, n int) (int, error) {
return p.WriteToFlags(fd, n, 0)
}

// WriteToFlags is WriteTo with splice(2) flags. /dev/fuse acts on SPLICE_F_MOVE even though the
// generic pipe-to-file path ignores it: fuse_dev_splice_write() moves the pages into the inode's
// page cache, which removes them from the page cache of the file they were spliced out of.
func (p *Pair) WriteToFlags(fd uintptr, n int, flags int) (int, error) {
var m int
var err error
p.rConn.Control(func(rfd uintptr) {
var sm int64
sm, err = syscall.Splice(int(rfd), nil, int(fd), nil, n, 0)
sm, err = syscall.Splice(int(rfd), nil, int(fd), nil, n, flags)
m = int(sm)
})
if err != nil {
Expand Down