Skip to content

Commit 985d77e

Browse files
committed
Enable the FUSE 3 performance features
Three independently measurable improvements, each with an integration test that the test-suite harness discovers automatically: - Request sizes up to 1 MiB: init sets conn->max_write (tunable with -o max_write=N) and libfuse >= 3.6 negotiates the matching max_pages with the kernel. Measured 1 MiB writes and O_DIRECT reads reaching the daemon, against 128 KiB with fuse2 big_writes; one request now carries two default-size tape blocks. - -o direct_io: sets FOPEN_DIRECT_IO on every open, so reads and writes bypass the kernel page cache and arrive at the application's I/O size. Streaming large archives no longer fills the page cache and data is not buffered twice; mmap does not work on files opened this way and small-block applications lose readahead, so the option is off by default. When fuse_file_info has parallel_direct_writes it is set as well; the field appeared in libfuse 3.15, so it is detected at configure time instead of via version checks (which broke on ubuntu-24.04's libfuse 3.14). - readdirplus: ltfs_fsops_readdir_attr hands each entry's attributes to the filler straight from the in-memory index, and init clears FUSE_CAP_READDIRPLUS_AUTO so every listing chunk carries attributes. ls -l over 100 files needs 2 getattr requests instead of 102. The kernel prefill is only effective with libfuse >= 3.17, so the test asserts the suppression conditionally.
1 parent dd64ef3 commit 985d77e

8 files changed

Lines changed: 242 additions & 9 deletions

File tree

configure.ac

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,22 @@ then
322322
else
323323
AC_MSG_RESULT([3])
324324
fi
325+
326+
dnl
327+
dnl struct fuse_file_info.parallel_direct_writes appeared in libfuse 3.15;
328+
dnl detect the member instead of relying on version numbers.
329+
dnl
330+
if test "x${with_fuse2}" != "xyes"
331+
then
332+
SAVE_CFLAGS=$CFLAGS
333+
CFLAGS="$CFLAGS ${FUSE_MODULE_CFLAGS} -DFUSE_USE_VERSION=31"
334+
AC_CHECK_MEMBER([struct fuse_file_info.parallel_direct_writes],
335+
[AC_DEFINE([HAVE_FUSE_PARALLEL_DIRECT_WRITES], [1],
336+
[Define to 1 if struct fuse_file_info has parallel_direct_writes])],
337+
[],
338+
[[#include <fuse.h>]])
339+
CFLAGS="$SAVE_CFLAGS"
340+
fi
325341
PKG_CHECK_MODULES([LIBXML2_MODULE], [libxml-2.0 >= 2.6.16])
326342

327343
if test "x${host_mac}" = "xyes"

messages/bin_ltfs/root.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,9 @@ root:table {
154154
14115E:string { "Invalid scsi_append_only_mode option: %s." }
155155
14116E:string { "This medium is not supported (%d)." }
156156
14123W:string { "The main function of FUSE returned error (%d)." }
157-
157+
14124I:string { "FUSE maximum request size is %u KiB." }
158+
14125W:string { "The max_write option is ignored when built against FUSE 2." }
159+
158160
// 14150 - 14199 are reserved for LE+
159161

160162
// Help messages
@@ -244,5 +246,7 @@ root:table {
244246
// Reserved 14466I
245247
14467I:string { " -o syslogtrace Enable diagnostic output to stderr and syslog(same as verbose=303)" }
246248
// Reserved 14468I
249+
14469I:string { " -o max_write=<num> Maximum size of a FUSE request in bytes (FUSE 3 builds only, default: 1048576)" }
250+
14470I:string { " -o direct_io Bypass the kernel page cache for all file I/O (disables mmap)" }
247251
}
248252
}

src/libltfs/ltfs.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ struct device_data;
210210
* or a negative value on error. */
211211
typedef int (*ltfs_dir_filler) (void *buf, const char *name, void *priv);
212212

213+
/* Directory listing callback that also receives the entry's attributes.
214+
* attr may be NULL when the backing store yields names only. */
215+
typedef int (*ltfs_dir_filler_attr) (void *buf, const char *name,
216+
const struct dentry_attr *attr, void *priv);
217+
213218
/**
214219
* All capacities are relative to filesystem block size.
215220
*/

src/libltfs/ltfs_fsops.c

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1414,6 +1414,96 @@ int ltfs_fsops_readdir(struct dentry *d, void *buf, ltfs_dir_filler filler, void
14141414
return ret;
14151415
}
14161416

1417+
/* Copy a child's attributes without taking the volume lock again;
1418+
* the caller already holds it (same fields as ltfs_fsops_getattr). */
1419+
static void _fsops_child_attr(struct dentry *child, struct dentry_attr *attr,
1420+
struct ltfs_volume *vol)
1421+
{
1422+
acquireread_mrsw(&child->meta_lock);
1423+
1424+
if (child->isslink)
1425+
attr->size = strlen(child->target.name);
1426+
else
1427+
attr->size = child->size;
1428+
1429+
attr->alloc_size = child->realsize;
1430+
attr->blocksize = vol->label->blocksize;
1431+
attr->uid = child->uid;
1432+
attr->nlink = child->link_count;
1433+
attr->create_time = child->creation_time;
1434+
attr->access_time = child->access_time;
1435+
attr->modify_time = child->modify_time;
1436+
attr->change_time = child->change_time;
1437+
attr->backup_time = child->backup_time;
1438+
attr->readonly = child->readonly;
1439+
attr->isdir = child->isdir;
1440+
attr->isslink = child->isslink;
1441+
1442+
releaseread_mrsw(&child->meta_lock);
1443+
1444+
if (! child->isdir && ! child->isslink && iosched_initialized(vol))
1445+
attr->size = iosched_get_filesize(child, vol);
1446+
}
1447+
1448+
int ltfs_fsops_readdir_attr(struct dentry *d, void *buf, ltfs_dir_filler_attr filler,
1449+
void *filler_priv, struct ltfs_volume *vol)
1450+
{
1451+
int ret = 0;
1452+
struct name_list *entry, *tmp;
1453+
struct dentry_attr attr;
1454+
1455+
CHECK_ARG_NULL(d, -LTFS_NULL_ARG);
1456+
CHECK_ARG_NULL(filler, -LTFS_NULL_ARG);
1457+
CHECK_ARG_NULL(vol, -LTFS_NULL_ARG);
1458+
1459+
if (! d->isdir)
1460+
return -LTFS_ISFILE;
1461+
1462+
ret = ltfs_get_volume_lock(false, vol);
1463+
if (ret < 0)
1464+
return ret;
1465+
1466+
acquireread_mrsw(&d->contents_lock);
1467+
if (dcache_initialized(vol)) {
1468+
/* The dentry cache yields names only */
1469+
int i;
1470+
char **namelist = NULL;
1471+
ret = dcache_readdir(d, false, (void ***) &namelist, vol);
1472+
if (ret == 0 && namelist) {
1473+
for (i=0; namelist[i]; ++i) {
1474+
ret = filler(buf, namelist[i], NULL, filler_priv);
1475+
if (ret < 0)
1476+
break;
1477+
}
1478+
for (i=0; namelist[i]; ++i)
1479+
free(namelist[i]);
1480+
free(namelist);
1481+
}
1482+
} else {
1483+
if (HASH_COUNT(d->child_list) != 0) {
1484+
HASH_SORT(d->child_list, fs_hash_sort_by_uid);
1485+
HASH_ITER(hh, d->child_list, entry, tmp) {
1486+
_fsops_child_attr(entry->d, &attr, vol);
1487+
ret = filler(buf, entry->d->platform_safe_name, &attr, filler_priv);
1488+
if (ret < 0)
1489+
break;
1490+
}
1491+
}
1492+
}
1493+
releaseread_mrsw(&d->contents_lock);
1494+
1495+
/* Update access time */
1496+
if (ret == 0) {
1497+
acquirewrite_mrsw(&d->meta_lock);
1498+
get_current_timespec(&d->access_time);
1499+
releasewrite_mrsw(&d->meta_lock);
1500+
ltfs_set_index_dirty(true, true, vol->index);
1501+
}
1502+
1503+
releaseread_mrsw(&vol->lock);
1504+
return ret;
1505+
}
1506+
14171507
int _ltfs_fsops_read_direntry(struct dentry *d, struct ltfs_direntry *dirent,
14181508
unsigned long index, bool root, struct ltfs_volume *vol)
14191509
{

src/libltfs/ltfs_fsops.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,13 @@ int ltfs_fsops_removexattr(const char *path, const char *name, ltfs_file_id *id,
324324
int ltfs_fsops_readdir(struct dentry *d, void *buf, ltfs_dir_filler filler, void *filler_priv,
325325
struct ltfs_volume *vol);
326326

327+
/**
328+
* List a directory like ltfs_fsops_readdir, passing each entry's attributes
329+
* to the filler as well. attr is NULL when the backing store yields names only.
330+
*/
331+
int ltfs_fsops_readdir_attr(struct dentry *d, void *buf, ltfs_dir_filler_attr filler,
332+
void *filler_priv, struct ltfs_volume *vol);
333+
327334
/**
328335
* Get an entry in the directory.
329336
* It does get the "." and ".." entries only when d is specified non volume root directory.

src/ltfs_fuse.c

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,31 @@ static struct fuse_context *context;
104104
int ltfs_fuse_fgetattr(const char *path, struct stat *stbuf, struct fuse_file_info *fi);
105105
int ltfs_fuse_ftruncate(const char *path, off_t length, struct fuse_file_info *fi);
106106

107+
#if !defined(__APPLE__) && FUSE_VERSION > 27
108+
/* Per-open cache policy. With -o direct_io every read and write bypasses
109+
* the kernel page cache: requests arrive at the application's I/O size
110+
* (up to the negotiated maximum) and stream straight to the daemon, at
111+
* the cost of mmap support and kernel readahead. Otherwise the page
112+
* cache is used and kept across opens (the daemon is the only writer).
113+
* keep_cache must never be set while another open of the same file uses
114+
* direct_io; the policy is mount-wide, so the modes cannot mix. */
115+
static void _ltfs_fuse_set_cache_flags(struct fuse_file_info *fi, struct ltfs_fuse_data *priv)
116+
{
117+
if (priv->direct_io) {
118+
fi->direct_io = 1;
119+
fi->keep_cache = 0;
120+
#ifdef HAVE_FUSE_PARALLEL_DIRECT_WRITES
121+
/* Writes are serialized further down; this only removes the
122+
* kernel-side exclusive lock for non-extending direct writes. */
123+
fi->parallel_direct_writes = 1;
124+
#endif
125+
} else {
126+
fi->direct_io = 0;
127+
fi->keep_cache = 1;
128+
}
129+
}
130+
#endif
131+
107132
struct ltfs_file_handle *_new_ltfs_file_handle(struct file_info *fi)
108133
{
109134
int ret;
@@ -442,10 +467,7 @@ int ltfs_fuse_open(const char *path, struct fuse_file_info *fi)
442467
fi->direct_io = 1;
443468
fi->keep_cache = 0;
444469
#else
445-
/* cannot set keep cache if any process has the file open with direct_io set! so only
446-
* set it on newer FUSE versions, where we don't use direct_io. */
447-
fi->direct_io = 0;
448-
fi->keep_cache = 1;
470+
_ltfs_fuse_set_cache_flags(fi, priv);
449471
#endif
450472
#endif
451473

@@ -787,10 +809,7 @@ int ltfs_fuse_create(const char *path, mode_t mode, struct fuse_file_info *fi)
787809
fi->direct_io = 1;
788810
fi->keep_cache = 0;
789811
#else
790-
/* cannot set keep cache if any process has the file open with direct_io set! so only
791-
* set it on newer FUSE versions, where we don't use direct_io. */
792-
fi->direct_io = 0;
793-
fi->keep_cache = 1;
812+
_ltfs_fuse_set_cache_flags(fi, priv);
794813
#endif
795814
#endif
796815

@@ -972,6 +991,56 @@ int _ltfs_fuse_filldir(void *buf, const char *name, void *priv)
972991
return 0;
973992
}
974993

994+
#ifdef HAVE_FUSE3
995+
/* Context for _ltfs_fuse_filldir_plus */
996+
struct ltfs_fuse_fill_plus {
997+
fuse_fill_dir_t filler;
998+
struct ltfs_fuse_data *priv;
999+
};
1000+
1001+
/* readdirplus filler: hand the entry's attributes to the kernel so it can
1002+
* prefill its inode cache and no getattr round trip is needed per entry. */
1003+
static int _ltfs_fuse_filldir_plus(void *buf, const char *name,
1004+
const struct dentry_attr *attr, void *vpriv)
1005+
{
1006+
struct ltfs_fuse_fill_plus *fill = vpriv;
1007+
struct stat st;
1008+
char *new_name;
1009+
int ret;
1010+
1011+
if (! attr)
1012+
return _ltfs_fuse_filldir(buf, name, fill->filler);
1013+
1014+
memset(&st, 0, sizeof(st));
1015+
_ltfs_fuse_attr_to_stat(&st, (struct dentry_attr *)attr, fill->priv);
1016+
1017+
ret = pathname_unformat(name, &new_name);
1018+
if (ret < 0) {
1019+
ltfsmsg(LTFS_ERR, 14027E, "unformat", ret);
1020+
return ret;
1021+
}
1022+
1023+
#ifdef __APPLE__
1024+
free(new_name);
1025+
1026+
ret = pathname_nfd_normalize(name, &new_name);
1027+
if (ret < 0) {
1028+
ltfsmsg(LTFS_ERR, 14027E, "nfd", ret);
1029+
return ret;
1030+
}
1031+
1032+
ret = fill->filler(buf, new_name, &st, 0, FUSE_FILL_DIR_PLUS);
1033+
#else
1034+
ret = fill->filler(buf, name, &st, 0, FUSE_FILL_DIR_PLUS);
1035+
#endif
1036+
1037+
free(new_name);
1038+
if (ret)
1039+
return -ENOBUFS;
1040+
return 0;
1041+
}
1042+
#endif /* HAVE_FUSE3 */
1043+
9751044
#ifdef HAVE_FUSE3
9761045
int ltfs_fuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
9771046
off_t offset, struct fuse_file_info *fi, enum fuse_readdir_flags flags)
@@ -999,6 +1068,14 @@ int ltfs_fuse_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
9991068
return -ENOBUFS;
10001069
}
10011070

1071+
#ifdef HAVE_FUSE3
1072+
if (flags & FUSE_READDIR_PLUS) {
1073+
struct ltfs_fuse_fill_plus fill = { .filler = filler, .priv = priv };
1074+
1075+
ret = ltfs_fsops_readdir_attr(file->file_info->dentry_handle, buf,
1076+
_ltfs_fuse_filldir_plus, &fill, priv->data);
1077+
} else
1078+
#endif
10021079
ret = ltfs_fsops_readdir(file->file_info->dentry_handle, buf, _ltfs_fuse_filldir,
10031080
filler, priv->data);
10041081

@@ -1198,6 +1275,17 @@ void * ltfs_fuse_mount(struct fuse_conn_info *conn)
11981275
/* Tape reads must stay ordered; FUSE 3 enables asynchronous reads by
11991276
* default (the -o sync_read mount option was removed). */
12001277
conn->want &= ~FUSE_CAP_ASYNC_READ;
1278+
1279+
/* Request sizes up to max_write (libfuse >= 3.6 negotiates the
1280+
* matching max_pages with the kernel). Read requests are bounded by
1281+
* the same page limit. */
1282+
conn->max_write = priv->fuse_max_write;
1283+
ltfsmsg(LTFS_INFO, 14124I, (unsigned int)(conn->max_write / 1024));
1284+
1285+
/* Always use readdirplus, not only when the kernel heuristic asks
1286+
* for it: attributes come from the in-memory index, so handing them
1287+
* out with the listing is free and avoids a getattr per entry. */
1288+
conn->want &= ~FUSE_CAP_READDIRPLUS_AUTO;
12011289
#endif
12021290

12031291
if (priv->pid_orig != getpid()) {

src/ltfs_fuse.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ extern "C" {
6868
#include "libltfs/plugin.h"
6969
#include "libltfs/uthash.h"
7070

71+
/* Default and minimum for the -o max_write option (FUSE 3 builds) */
72+
#define LTFS_FUSE_MAX_WRITE_DEFAULT (1UL << 20)
73+
#define LTFS_FUSE_MAX_WRITE_MIN (128UL << 10)
74+
7175
struct ltfs_fuse_data {
7276
bool first_parsing_pass; /**< Just looking for a config file? If so, don't print help */
7377

@@ -131,6 +135,8 @@ struct ltfs_fuse_data {
131135
char *symlink_str; /**< Symbolic Link type fetched by option (live or posix)*/
132136
char *str_append_only_mode; /**< option sting of scsi_append_only_mode */
133137
int append_only_mode; /**< Use append-only mode */
138+
unsigned long fuse_max_write; /**< Maximum size of a FUSE request in bytes (FUSE 3) */
139+
int direct_io; /**< Bypass the kernel page cache for all file I/O */
134140

135141
bool advanced_help; /**< Include standard FUSE options on --help? */
136142

src/main.c

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,9 @@ static struct fuse_opt ltfs_options[] = {
139139
LTFS_OPT("capture_index", capture_index, 1),
140140
LTFS_OPT("symlink_type=%s", symlink_str, 0),
141141
LTFS_OPT("scsi_append_only_mode=%s", str_append_only_mode, 0),
142+
LTFS_OPT("max_write=%lu", fuse_max_write, 0),
143+
LTFS_OPT("direct_io", direct_io, 1),
144+
LTFS_OPT("nodirect_io", direct_io, 0),
142145
LTFS_OPT_KEY("-a", KEY_ADVANCED_HELP),
143146
FUSE_OPT_KEY("-h", KEY_HELP),
144147
FUSE_OPT_KEY("--help", KEY_HELP),
@@ -177,6 +180,8 @@ void single_drive_advanced_usage(const char *default_driver, struct ltfs_fuse_da
177180
ltfsresult(14448I); /* -o release_device */
178181
ltfsresult(14456I); /* -o capture_index */
179182
ltfsresult(14463I); /* -o scsi_append_only_mode=<on|off> */
183+
ltfsresult(14469I); /* -o max_write=<num> */
184+
ltfsresult(14470I); /* -o direct_io */
180185
ltfsresult(14406I); /* -a */
181186
/* TODO: future use for WORM */
182187
/* set worm rollback flag and rollback_str by this option */
@@ -982,6 +987,18 @@ int single_drive_main(struct fuse_args *args, struct ltfs_fuse_data *priv)
982987
ltfsmsg(LTFS_INFO, 14095I);
983988
}
984989

990+
#ifdef HAVE_FUSE3
991+
/* Maximum FUSE request size; the kernel rounds it to whole pages and
992+
* caps it (1 MiB unless raised via fs.fuse.max_pages_limit). */
993+
if (priv->fuse_max_write == 0)
994+
priv->fuse_max_write = LTFS_FUSE_MAX_WRITE_DEFAULT;
995+
else if (priv->fuse_max_write < LTFS_FUSE_MAX_WRITE_MIN)
996+
priv->fuse_max_write = LTFS_FUSE_MAX_WRITE_MIN;
997+
#else
998+
if (priv->fuse_max_write != 0)
999+
ltfsmsg(LTFS_WARN, 14125W);
1000+
#endif
1001+
9851002
#ifndef HAVE_FUSE3
9861003
/* If the local inode space is big enough, have FUSE pass through our UIDs as inode
9871004
* numbers instead of generating its own. On FUSE 3 this is set through

0 commit comments

Comments
 (0)