Skip to content

Commit fe98f37

Browse files
committed
feat(mount): add max-write option and kernel readahead tuning
Add --max-write option to control FUSE write request size and improve readahead configuration for better sequential read performance. Changes: - Add --max-write option (default 1 MiB) instead of hardcoded value - Increase --max-read-ahead default from 128 KiB to 1 MiB - Detect kernel max write limit (tunable on Linux 6.13+ via procfs) - Cap MaxWrite and MaxReadAhead to kernel limits with user notification - Tune kernel readahead via sysfs on Linux when running as root - Enforce minimum 128 KiB readahead to prevent performance degradation - Add platform-specific implementations (Linux sysfs, no-op for others) The kernel sysfs tuning writes to /sys/class/bdi/<major>:<minor>/read_ahead_kb to make --max-read-ahead actually effective, since FUSE only sets a ceiling and the kernel default remains 128 KiB otherwise. Signed-off-by: Anagh Kumar Baranwal <[email protected]>
1 parent 71ff5b2 commit fe98f37

File tree

6 files changed

+308
-6
lines changed

6 files changed

+308
-6
lines changed

cmd/cmount/mount.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ func mountOptions(VFS *vfs.VFS, device string, mountpoint string, opt *mountlib.
9999
options = append(options, "-o", option)
100100
}
101101
options = append(options, opt.ExtraFlags...)
102+
// FIXME: Use mountlib.Opt.MaxWrite and mountlib.Opt.MaxReadAhead options
103+
// instead of hardcoded values for consistency with mount2.
104+
// Also consider adding sysfs kernel readahead tuning via TuneKernelReadAhead.
102105
if runtime.GOOS == "linux" {
103106
options = append(options, "-o", "big_writes")
104107
options = append(options, "-o", fmt.Sprintf("max_write=%d", 1048576))

cmd/mount2/mount.go

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package mount2
55

66
import (
77
"fmt"
8+
"os"
89
"runtime"
910
"time"
1011

@@ -24,14 +25,39 @@ func init() {
2425
//
2526
// man mount.fuse for more info and note the -o flag for other options
2627
func mountOptions(fsys *FS, f fs.Fs, opt *mountlib.Options) (mountOpts *fuse.MountOptions) {
28+
// Get the kernel's effective max write size (1 MiB on Linux <6.13, tunable on 6.13+)
29+
kernelMaxWrite := mountlib.GetEffectiveMaxWrite()
30+
31+
// Cap MaxWrite to kernel limit
32+
requestedMaxWrite := int(fsys.opt.MaxWrite)
33+
effectiveMaxWrite := requestedMaxWrite
34+
if effectiveMaxWrite > kernelMaxWrite {
35+
effectiveMaxWrite = kernelMaxWrite
36+
fs.Infof(f, "MaxWrite %d exceeds kernel limit %d, capping to kernel limit",
37+
requestedMaxWrite, kernelMaxWrite)
38+
}
39+
40+
// Apply minimum readahead enforcement (128 KiB) for consistency with kernel sysfs tuning
41+
readAheadKiB := mountlib.EnforceMinReadAheadKiB(int(fsys.opt.MaxReadAhead / 1024))
42+
readAheadBytes := readAheadKiB * 1024
43+
44+
// Cap MaxReadAhead to effective MaxWrite (larger values can't be used in single requests)
45+
requestedReadAhead := readAheadBytes
46+
effectiveReadAhead := readAheadBytes
47+
if effectiveReadAhead > effectiveMaxWrite {
48+
effectiveReadAhead = effectiveMaxWrite
49+
fs.Infof(f, "MaxReadAhead %d exceeds effective MaxWrite %d, capping to MaxWrite",
50+
requestedReadAhead, effectiveMaxWrite)
51+
}
52+
2753
mountOpts = &fuse.MountOptions{
2854
AllowOther: fsys.opt.AllowOther,
2955
FsName: opt.DeviceName,
3056
Name: "rclone",
3157
DisableXAttrs: true,
3258
Debug: fsys.opt.DebugFUSE,
33-
MaxReadAhead: int(fsys.opt.MaxReadAhead),
34-
MaxWrite: 1024 * 1024, // Linux v4.20+ caps requests at 1 MiB
59+
MaxReadAhead: effectiveReadAhead,
60+
MaxWrite: effectiveMaxWrite,
3561
DisableReadDirPlus: true,
3662
MaxStackDepth: fsys.opt.MaxStackDepth,
3763

@@ -136,7 +162,6 @@ func mountOptions(fsys *FS, f fs.Fs, opt *mountlib.Options) (mountOpts *fuse.Mou
136162

137163
}
138164
var opts []string
139-
// FIXME doesn't work opts = append(opts, fmt.Sprintf("max_readahead=%d", maxReadAhead))
140165
if fsys.opt.AllowOther {
141166
opts = append(opts, "allow_other")
142167
}
@@ -263,5 +288,20 @@ func mount(VFS *vfs.VFS, mountpoint string, opt *mountlib.Options) (<-chan error
263288
}
264289

265290
fs.Debugf(f, "Mount started")
291+
292+
// Log effective FUSE settings for debugging
293+
fs.Debugf(f, "FUSE: MaxWrite=%d (kernel limit %d) MaxReadAhead=%d",
294+
mountOpts.MaxWrite, mountlib.GetEffectiveMaxWrite(), mountOpts.MaxReadAhead)
295+
296+
// Tune kernel readahead to match FUSE setting (Linux only, requires root)
297+
// This makes --max-read-ahead actually work by setting the sysfs value.
298+
// Only attempt if running as root to avoid warning spam for non-root users.
299+
// Use mountOpts.MaxReadAhead (already capped) to ensure FUSE and sysfs match exactly.
300+
if mountOpts.MaxReadAhead > 0 && os.Getuid() == 0 {
301+
if err := mountlib.TuneKernelReadAhead(mountpoint, mountOpts.MaxReadAhead/1024); err != nil {
302+
fs.LogLevelPrintf(fs.LogLevelWarning, nil, "Failed to tune kernel readahead: %v", err)
303+
}
304+
}
305+
266306
return errs, umount, nil
267307
}

cmd/mountlib/mount.go

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,9 +100,21 @@ var OptionsInfo = fs.Options{{
100100
Groups: "Mount",
101101
}, {
102102
Name: "max_read_ahead",
103-
Default: fs.SizeSuffix(128 * 1024),
104-
Help: "The number of bytes that can be prefetched for sequential reads (not supported on Windows)",
105-
Groups: "Mount",
103+
Default: fs.SizeSuffix(1024 * 1024),
104+
Help: "The number of bytes that can be prefetched for sequential reads. " +
105+
"On Linux as root, this also sets the kernel readahead via sysfs. " +
106+
"On other platforms or as non-root, only the FUSE ceiling is set " +
107+
"and actual readahead may remain at the kernel default (128 KiB). " +
108+
"Set to 0 to use FUSE default and leave kernel sysfs unchanged. " +
109+
"(not supported on Windows)",
110+
Groups: "Mount",
111+
}, {
112+
Name: "max_write",
113+
Default: fs.SizeSuffix(1024 * 1024),
114+
Help: "Maximum size of a FUSE write request. The FUSE library caps this at the kernel limit. " +
115+
"Linux kernels 4.20+ support up to 1 MiB. Kernel 6.13+ allows tuning via " +
116+
"/proc/sys/fs/fuse/max_pages_limit. (not supported on Windows)",
117+
Groups: "Mount",
106118
}, {
107119
Name: "write_back_cache",
108120
Default: false,
@@ -182,6 +194,7 @@ type Options struct {
182194
Daemon bool `config:"daemon"`
183195
DaemonWait fs.Duration `config:"daemon_wait"` // time to wait for ready mount from daemon, maximum on Linux or constant on macOS/BSD
184196
MaxReadAhead fs.SizeSuffix `config:"max_read_ahead"`
197+
MaxWrite fs.SizeSuffix `config:"max_write"`
185198
ExtraOptions []string `config:"option"`
186199
ExtraFlags []string `config:"fuse_flag"`
187200
AttrTimeout fs.Duration `config:"attr_timeout"` // how long the kernel caches attribute for

cmd/mountlib/readahead_linux.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
//go:build linux
2+
3+
package mountlib
4+
5+
import (
6+
"fmt"
7+
"os"
8+
"strconv"
9+
"strings"
10+
"syscall"
11+
"time"
12+
13+
"github.com/rclone/rclone/fs"
14+
"golang.org/x/sys/unix"
15+
)
16+
17+
const (
18+
// Maximum time to wait for sysfs path to appear
19+
sysfsTimeout = 1 * time.Second
20+
// Polling interval when waiting for sysfs
21+
sysfsPollInterval = 50 * time.Millisecond
22+
// MinReadAheadKiB is the minimum readahead value (kernel default)
23+
MinReadAheadKiB = 128
24+
// DefaultMaxWrite is the kernel's default max FUSE write size (1 MiB).
25+
// This has been the limit since Linux 4.20 (256 pages × 4 KiB).
26+
// Kernel 6.13+ allows tuning via /proc/sys/fs/fuse/max_pages_limit.
27+
DefaultMaxWrite = 1024 * 1024
28+
// procMaxPagesLimit is the procfs path for the tunable max pages (kernel 6.13+)
29+
procMaxPagesLimit = "/proc/sys/fs/fuse/max_pages_limit"
30+
)
31+
32+
// EnforceMinReadAheadKiB returns sizeKiB with minimum enforcement applied.
33+
// Values below MinReadAheadKiB (128 KiB) are raised to MinReadAheadKiB
34+
// to avoid accidentally degrading performance. Zero and negative values
35+
// are normalized to zero (meaning "use default").
36+
func EnforceMinReadAheadKiB(sizeKiB int) int {
37+
if sizeKiB <= 0 {
38+
return 0
39+
}
40+
if sizeKiB < MinReadAheadKiB {
41+
return MinReadAheadKiB
42+
}
43+
return sizeKiB
44+
}
45+
46+
// TuneKernelReadAhead sets the kernel readahead for the FUSE mount.
47+
// This must be called after the mount is ready.
48+
// sizeKiB is the desired readahead in KiB. Values below 128 KiB are
49+
// raised to 128 KiB (the kernel default) to avoid degraded performance.
50+
// Returns error if tuning fails (caller may choose to log and continue).
51+
func TuneKernelReadAhead(mountpoint string, sizeKiB int) error {
52+
sizeKiB = EnforceMinReadAheadKiB(sizeKiB)
53+
54+
var st syscall.Stat_t
55+
if err := syscall.Stat(mountpoint, &st); err != nil {
56+
return fmt.Errorf("stat mountpoint %q: %w", mountpoint, err)
57+
}
58+
59+
major := unix.Major(st.Dev)
60+
minor := unix.Minor(st.Dev)
61+
sysfsPath := fmt.Sprintf("/sys/class/bdi/%d:%d/read_ahead_kb", major, minor)
62+
63+
// Poll for sysfs path to appear (may take a moment after mount)
64+
deadline := time.Now().Add(sysfsTimeout)
65+
for {
66+
if _, err := os.Stat(sysfsPath); err == nil {
67+
break // Path exists
68+
}
69+
if time.Now().After(deadline) {
70+
return fmt.Errorf("sysfs path %q did not appear within %v", sysfsPath, sysfsTimeout)
71+
}
72+
time.Sleep(sysfsPollInterval)
73+
}
74+
75+
// Write the value
76+
if err := os.WriteFile(sysfsPath, []byte(strconv.Itoa(sizeKiB)), 0644); err != nil {
77+
return fmt.Errorf("write to %q: %w", sysfsPath, err)
78+
}
79+
80+
fs.LogLevelPrintf(fs.LogLevelNotice, nil, "Set kernel readahead to %d KiB for %q", sizeKiB, mountpoint)
81+
return nil
82+
}
83+
84+
// GetKernelReadAhead returns the current kernel readahead for the mountpoint in KiB.
85+
func GetKernelReadAhead(mountpoint string) (int, error) {
86+
var st syscall.Stat_t
87+
if err := syscall.Stat(mountpoint, &st); err != nil {
88+
return 0, err
89+
}
90+
91+
major := unix.Major(st.Dev)
92+
minor := unix.Minor(st.Dev)
93+
sysfsPath := fmt.Sprintf("/sys/class/bdi/%d:%d/read_ahead_kb", major, minor)
94+
95+
data, err := os.ReadFile(sysfsPath)
96+
if err != nil {
97+
return 0, err
98+
}
99+
100+
return strconv.Atoi(strings.TrimSpace(string(data)))
101+
}
102+
103+
// GetEffectiveMaxWrite returns the kernel's effective maximum FUSE write size.
104+
// On Linux 6.13+, this reads /proc/sys/fs/fuse/max_pages_limit and calculates
105+
// the limit as max_pages × page_size. On older kernels where the procfs tunable
106+
// doesn't exist, returns DefaultMaxWrite (1 MiB).
107+
func GetEffectiveMaxWrite() int {
108+
data, err := os.ReadFile(procMaxPagesLimit)
109+
if err != nil {
110+
// Pre-6.13 kernel or procfs not mounted
111+
return DefaultMaxWrite
112+
}
113+
114+
maxPages, err := strconv.Atoi(strings.TrimSpace(string(data)))
115+
if err != nil || maxPages <= 0 {
116+
return DefaultMaxWrite
117+
}
118+
119+
pageSize := os.Getpagesize()
120+
return maxPages * pageSize
121+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
//go:build linux
2+
3+
package mountlib
4+
5+
import (
6+
"os"
7+
"testing"
8+
)
9+
10+
func TestGetKernelReadAhead(t *testing.T) {
11+
// Test on a real mounted filesystem (root fs always exists)
12+
val, err := GetKernelReadAhead("/")
13+
if err != nil {
14+
t.Skipf("Cannot read sysfs (may need root or sysfs not mounted): %v", err)
15+
}
16+
if val <= 0 {
17+
t.Errorf("Expected positive readahead value, got %d", val)
18+
}
19+
t.Logf("Root filesystem readahead: %d KiB", val)
20+
}
21+
22+
func TestTuneKernelReadAhead_PermissionDenied(t *testing.T) {
23+
// Non-root users should get a permission error, not a crash
24+
if os.Getuid() == 0 {
25+
t.Skip("Test requires non-root user")
26+
}
27+
28+
err := TuneKernelReadAhead("/", 1024)
29+
if err == nil {
30+
t.Error("Expected permission error for non-root user")
31+
}
32+
// Verify it's a sensible error, not a panic
33+
t.Logf("Expected error: %v", err)
34+
}
35+
36+
func TestEnforceMinReadAheadKiB(t *testing.T) {
37+
tests := []struct {
38+
name string
39+
input int
40+
expected int
41+
}{
42+
{"zero passes through", 0, 0},
43+
{"below minimum raised to 128", 64, 128},
44+
{"well below minimum raised to 128", 1, 128},
45+
{"exactly minimum unchanged", 128, 128},
46+
{"above minimum unchanged", 256, 256},
47+
{"large value unchanged", 1024, 1024},
48+
{"negative normalized to zero", -1, 0},
49+
}
50+
51+
for _, tc := range tests {
52+
t.Run(tc.name, func(t *testing.T) {
53+
got := EnforceMinReadAheadKiB(tc.input)
54+
if got != tc.expected {
55+
t.Errorf("EnforceMinReadAheadKiB(%d) = %d, want %d", tc.input, got, tc.expected)
56+
}
57+
})
58+
}
59+
}
60+
61+
func TestGetEffectiveMaxWrite(t *testing.T) {
62+
maxWrite := GetEffectiveMaxWrite()
63+
64+
// Should return a positive, reasonable value (at least 64 KiB, typical minimum)
65+
minReasonable := 64 * 1024
66+
if maxWrite < minReasonable {
67+
t.Errorf("GetEffectiveMaxWrite() = %d, want >= %d", maxWrite, minReasonable)
68+
}
69+
70+
// Should be page-aligned
71+
pageSize := os.Getpagesize()
72+
if maxWrite%pageSize != 0 {
73+
t.Errorf("GetEffectiveMaxWrite() = %d, not page-aligned (page size %d)", maxWrite, pageSize)
74+
}
75+
76+
// Log the result for visibility
77+
if _, err := os.Stat(procMaxPagesLimit); err == nil {
78+
t.Logf("Kernel 6.13+: max_pages_limit present, effective MaxWrite = %d bytes", maxWrite)
79+
} else {
80+
t.Logf("Kernel <6.13: using DefaultMaxWrite = %d bytes", maxWrite)
81+
}
82+
}

cmd/mountlib/readahead_other.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//go:build !linux
2+
3+
package mountlib
4+
5+
import (
6+
"errors"
7+
8+
"github.com/rclone/rclone/fs"
9+
)
10+
11+
// MinReadAheadKiB is the minimum readahead value (kernel default).
12+
// On non-Linux platforms this is defined for API compatibility but not enforced.
13+
const MinReadAheadKiB = 128
14+
15+
// DefaultMaxWrite is the kernel's default max FUSE write size (1 MiB).
16+
// On non-Linux platforms this is defined for API compatibility.
17+
const DefaultMaxWrite = 1024 * 1024
18+
19+
// EnforceMinReadAheadKiB returns sizeKiB unchanged on non-Linux platforms.
20+
// Minimum enforcement only applies on Linux where kernel sysfs tuning is supported.
21+
func EnforceMinReadAheadKiB(sizeKiB int) int {
22+
return sizeKiB
23+
}
24+
25+
// TuneKernelReadAhead sets the kernel readahead for the FUSE mount.
26+
// On non-Linux platforms this is a no-op that returns nil.
27+
func TuneKernelReadAhead(mountpoint string, sizeKiB int) error {
28+
fs.Debugf(nil, "readahead: kernel tuning not supported on this platform")
29+
return nil // Not an error, just unsupported
30+
}
31+
32+
// GetKernelReadAhead returns the current kernel readahead for the mountpoint in KiB.
33+
// On non-Linux platforms this returns an error.
34+
func GetKernelReadAhead(mountpoint string) (int, error) {
35+
return 0, errors.New("not supported on this platform")
36+
}
37+
38+
// GetEffectiveMaxWrite returns the kernel's effective maximum FUSE write size.
39+
// On non-Linux platforms this returns DefaultMaxWrite (1 MiB) as we cannot
40+
// detect kernel limits.
41+
func GetEffectiveMaxWrite() int {
42+
return DefaultMaxWrite
43+
}

0 commit comments

Comments
 (0)