Files
2026-10-19 08:35:59 +08:00

572 lines
16 KiB
C

/* SPDX-License-Identifier: GPL-2.0-or-later */
/* FreeBSD per-mount management controls for EROFS. */
#include <sys/param.h>
#include <sys/kernel.h>
#include <sys/libkern.h>
#include <sys/limits.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/sysctl.h>
#include "compress.h"
#include "internal.h"
#define EROFS_DIR_RA_DEFAULT_BYTES (16UL * 1024)
#define EROFS_DIR_RA_HARD_MAX_BYTES (1024UL * 1024)
#define EROFS_DIR_RA_SLOTS 256UL
enum erofs_cache_metric {
EROFS_CACHE_HITS,
EROFS_CACHE_MISSES,
EROFS_CACHE_BYPASSES,
EROFS_CACHE_EVICTIONS,
EROFS_CACHE_RECLAIMS,
EROFS_CACHE_WAITER_MERGES,
EROFS_CACHE_ENTRIES,
EROFS_CACHE_BYTES,
};
static struct mtx erofs_control_id_lock;
static uint64_t erofs_control_next_id = 1;
MTX_SYSINIT(erofs_control_id, &erofs_control_id_lock, "erofs control id",
MTX_DEF);
SYSCTL_NODE(_vfs, OID_AUTO, erofs, CTLFLAG_RD | CTLFLAG_MPSAFE, 0,
"EROFS filesystem");
SYSCTL_NODE(_vfs_erofs, OID_AUTO, mounts, CTLFLAG_RD | CTLFLAG_MPSAFE, 0,
"Mounted EROFS filesystems");
void
erofs_management_init(struct erofs_sb_info *sbi)
{
mtx_init(&sbi->control_lock, "erofs control", NULL, MTX_DEF);
sbi->dir_ra_bytes = EROFS_DIR_RA_DEFAULT_BYTES;
sbi->dir_ra_max_bytes = EROFS_DIR_RA_HARD_MAX_BYTES;
sbi->control_initialized = true;
}
void
erofs_management_fini(struct erofs_sb_info *sbi)
{
if (!sbi->control_initialized)
return;
KASSERT(!sbi->sysctl_registered,
("erofs management sysctls remain registered"));
mtx_destroy(&sbi->control_lock);
sbi->control_initialized = false;
}
uint64_t
erofs_dir_ra_bytes(struct erofs_sb_info *sbi)
{
uint64_t value;
mtx_lock(&sbi->control_lock);
value = sbi->dir_ra_bytes;
mtx_unlock(&sbi->control_lock);
return (value);
}
/* The readahead is limited by the size of the per-directory run lists. */
static uint64_t
erofs_dir_ra_limit(const struct erofs_sb_info *sbi)
{
return (MIN((uint64_t)EROFS_DIR_RA_HARD_MAX_BYTES,
(uint64_t)sbi->block_size * EROFS_DIR_RA_SLOTS));
}
int
erofs_set_dir_ra_bytes(struct erofs_sb_info *sbi, uint64_t value)
{
if (sbi->block_size == 0 || value > erofs_dir_ra_limit(sbi) ||
(value != 0 && value % sbi->block_size != 0))
return (EINVAL);
mtx_lock(&sbi->control_lock);
if (sbi->control_closing) {
mtx_unlock(&sbi->control_lock);
return (ENXIO);
}
sbi->dir_ra_bytes = value;
mtx_unlock(&sbi->control_lock);
return (0);
}
void
erofs_record_dir_readahead(struct erofs_sb_info *sbi, unsigned int blocks)
{
mtx_lock(&sbi->control_lock);
++sbi->dir_ra_requests;
sbi->dir_ra_blocks += blocks;
mtx_unlock(&sbi->control_lock);
}
static int
erofs_parse_uint64(const char *value, size_t len, uint64_t *result)
{
size_t i;
uint64_t parsed;
if (value == NULL || len <= 1 || value[len - 1] != '\0' ||
strnlen(value, len) != len - 1)
return (EINVAL);
parsed = 0;
for (i = 0; i < len - 1; ++i) {
if (value[i] < '0' || value[i] > '9')
return (EINVAL);
if (parsed > (UINT64_MAX - (value[i] - '0')) / 10)
return (ERANGE);
parsed = parsed * 10 + (value[i] - '0');
}
*result = parsed;
return (0);
}
int
erofs_parse_dir_ra_value(struct erofs_sb_info *sbi, struct vfsoptlist *opts,
uint64_t *valuep, bool *presentp)
{
struct vfsopt *opt;
uint64_t value;
int error;
*presentp = false;
TAILQ_FOREACH(opt, opts, link) {
if (strcmp(opt->name, "dir_ra_bytes") != 0)
continue;
error = erofs_parse_uint64(opt->value, opt->len, &value);
if (error != 0)
return (error);
if (sbi->block_size == 0 || value > erofs_dir_ra_limit(sbi) ||
(value != 0 && value % sbi->block_size != 0))
return (EINVAL);
*valuep = value;
*presentp = true;
return (0);
}
return (0);
}
int
erofs_parse_dir_ra_option(struct erofs_sb_info *sbi, struct vfsoptlist *opts)
{
uint64_t value;
bool present;
int error;
error = erofs_parse_dir_ra_value(sbi, opts, &value, &present);
if (error != 0 || !present)
return (error);
return (erofs_set_dir_ra_bytes(sbi, value));
}
static int
erofs_sysctl_dir_ra(SYSCTL_HANDLER_ARGS)
{
struct erofs_sb_info *sbi;
uint64_t value;
int error;
sbi = arg1;
value = erofs_dir_ra_bytes(sbi);
error = sysctl_handle_64(oidp, &value, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
return (erofs_set_dir_ra_bytes(sbi, value));
}
static int
erofs_sysctl_dir_ra_metric(SYSCTL_HANDLER_ARGS)
{
struct erofs_sb_info *sbi;
uint64_t value;
sbi = arg1;
mtx_lock(&sbi->control_lock);
value = arg2 == 0 ? sbi->dir_ra_requests : sbi->dir_ra_blocks;
mtx_unlock(&sbi->control_lock);
return (sysctl_handle_64(oidp, &value, 0, req));
}
#if WITH_EROFS_ZIP
static int
erofs_validate_zip_value(const char *value, int len)
{
if (value == NULL || len <= 1 || value[len - 1] != '\0' ||
strnlen(value, (size_t)len) != (size_t)len - 1)
return (EINVAL);
return (0);
}
static int
erofs_parse_zip_uint(const char *value, int len, unsigned int *result)
{
size_t i;
unsigned int parsed;
if (erofs_validate_zip_value(value, len) != 0)
return (EINVAL);
parsed = 0;
for (i = 0; i < (size_t)len - 1; ++i) {
if (value[i] < '0' || value[i] > '9' ||
parsed > (UINT_MAX - (value[i] - '0')) / 10)
return (EINVAL);
parsed = parsed * 10 + (value[i] - '0');
}
*result = parsed;
return (0);
}
int
erofs_parse_zip_options(struct mount *mp, struct erofs_sb_info_opts *opts,
unsigned int *syncp, bool *cache_presentp, bool *sync_presentp)
{
struct vfsopt *opt;
unsigned int value;
bool queue_priority_present;
int error;
*cache_presentp = false;
*sync_presentp = false;
queue_priority_present = false;
TAILQ_FOREACH(opt, mp->mnt_optnew, link) {
if (strcmp(opt->name, "cache_strategy") == 0) {
if (opt->value == NULL || opt->len <= 1 ||
((char *)opt->value)[opt->len - 1] != '\0' ||
strnlen(opt->value, opt->len) !=
(size_t)opt->len - 1)
return (EINVAL);
if (strcmp(opt->value, "disabled") == 0)
opts->cache_strategy = EROFS_ZIP_CACHE_DISABLED;
else if (strcmp(opt->value, "readahead") == 0)
opts->cache_strategy =
EROFS_ZIP_CACHE_READAHEAD;
else if (strcmp(opt->value, "readaround") == 0)
opts->cache_strategy =
EROFS_ZIP_CACHE_READAROUND;
else
return (EINVAL);
*cache_presentp = true;
continue;
}
if (strcmp(opt->name, "worker_queue_priority") == 0) {
error = erofs_parse_zip_uint(opt->value, opt->len,
&value);
if (error != 0 || value > UINT8_MAX)
return (EINVAL);
opts->worker_queue_priority = (uint8_t)value;
queue_priority_present = true;
continue;
}
if (strcmp(opt->name, "sync_decompress") != 0)
continue;
error = erofs_validate_zip_value(opt->value, opt->len);
if (error != 0)
return (error);
if (strcmp(opt->value, "auto") == 0)
value = EROFS_SYNC_DECOMPRESS_AUTO;
else if (strcmp(opt->value, "on") == 0)
value = EROFS_SYNC_DECOMPRESS_FORCE_ON;
else if (strcmp(opt->value, "off") == 0)
value = EROFS_SYNC_DECOMPRESS_FORCE_OFF;
else {
error = erofs_parse_zip_uint(opt->value, opt->len,
&value);
if (error != 0 ||
value > EROFS_SYNC_DECOMPRESS_FORCE_OFF)
return (EINVAL);
}
*syncp = value;
*sync_presentp = true;
}
if (*cache_presentp)
vfs_deleteopt(mp->mnt_optnew, "cache_strategy");
if (*sync_presentp)
vfs_deleteopt(mp->mnt_optnew, "sync_decompress");
if (queue_priority_present)
vfs_deleteopt(mp->mnt_optnew, "worker_queue_priority");
return (0);
}
static int
erofs_sysctl_drop_caches(SYSCTL_HANDLER_ARGS)
{
struct erofs_sb_info *sbi;
uint64_t value;
int error;
if (req->newptr == NULL)
return (EPERM);
sbi = arg1;
value = 0;
error = sysctl_handle_64(oidp, &value, 0, req);
if (error != 0)
return (error);
if (value != 1)
return (EINVAL);
return (z_erofs_extent_cache_drop(sbi));
}
static int
erofs_sysctl_cache_metric(SYSCTL_HANDLER_ARGS)
{
struct erofs_cache_stats stats;
struct erofs_sb_info *sbi;
uint64_t value;
sbi = arg1;
z_erofs_extent_cache_stats(sbi, &stats);
switch (arg2) {
case EROFS_CACHE_HITS:
value = stats.hits;
break;
case EROFS_CACHE_MISSES:
value = stats.misses;
break;
case EROFS_CACHE_BYPASSES:
value = stats.bypasses;
break;
case EROFS_CACHE_EVICTIONS:
value = stats.evictions;
break;
case EROFS_CACHE_RECLAIMS:
value = stats.reclaims;
break;
case EROFS_CACHE_WAITER_MERGES:
value = stats.waiter_merges;
break;
case EROFS_CACHE_ENTRIES:
value = stats.entries;
break;
case EROFS_CACHE_BYTES:
value = stats.bytes;
break;
default:
return (EINVAL);
}
return (sysctl_handle_64(oidp, &value, 0, req));
}
static int
erofs_sysctl_zip_setting(SYSCTL_HANDLER_ARGS)
{
struct erofs_sb_info *sbi;
int value;
int error;
sbi = arg1;
mtx_lock(&sbi->control_lock);
value = arg2 == 0 ? (int)sbi->sync_decompress :
(int)sbi->opt.cache_strategy;
mtx_unlock(&sbi->control_lock);
error = sysctl_handle_int(oidp, &value, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
if (arg2 == 0) {
if (value < EROFS_SYNC_DECOMPRESS_AUTO ||
value > EROFS_SYNC_DECOMPRESS_FORCE_OFF)
return (EINVAL);
} else {
if (value < EROFS_ZIP_CACHE_DISABLED ||
value > EROFS_ZIP_CACHE_READAROUND)
return (EINVAL);
}
mtx_lock(&sbi->control_lock);
if (sbi->control_closing) {
mtx_unlock(&sbi->control_lock);
return (ENXIO);
}
if (arg2 == 0)
sbi->sync_decompress = (unsigned int)value;
else
sbi->opt.cache_strategy = (uint8_t)value;
mtx_unlock(&sbi->control_lock);
return (0);
}
static int
erofs_sysctl_worker_queue_priority(SYSCTL_HANDLER_ARGS)
{
struct erofs_sb_info *sbi;
int error, value;
sbi = arg1;
mtx_lock(&sbi->control_lock);
value = sbi->decompress_queue_priority;
mtx_unlock(&sbi->control_lock);
error = sysctl_handle_int(oidp, &value, 0, req);
if (error != 0 || req->newptr == NULL)
return (error);
if (value < 0 || value > UINT8_MAX)
return (EINVAL);
mtx_lock(&sbi->control_lock);
if (sbi->control_closing) {
mtx_unlock(&sbi->control_lock);
return (ENXIO);
}
sbi->decompress_queue_priority = (uint8_t)value;
sbi->opt.worker_queue_priority = (uint8_t)value;
mtx_unlock(&sbi->control_lock);
return (0);
}
#endif
int
erofs_register_sysfs(struct erofs_sb_info *sbi)
{
struct sysctl_oid *cache_node, *mount_node;
char name[24];
int error;
mtx_lock(&erofs_control_id_lock);
sbi->control_id = erofs_control_next_id++;
mtx_unlock(&erofs_control_id_lock);
sbi->dir_ra_max_bytes = erofs_dir_ra_limit(sbi);
snprintf(name, sizeof(name), "%ju", (uintmax_t)sbi->control_id);
sysctl_ctx_init(&sbi->sysctl_ctx);
sbi->sysctl_registered = true;
mount_node = SYSCTL_ADD_NODE(&sbi->sysctl_ctx,
SYSCTL_STATIC_CHILDREN(_vfs_erofs_mounts), OID_AUTO, name,
CTLFLAG_RD | CTLFLAG_MPSAFE, NULL, "EROFS mount");
if (mount_node == NULL)
goto fail;
if (SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "mount_id", CTLFLAG_RD, &sbi->control_id, 0,
"Unique mount control identifier") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "dir_ra_bytes", CTLTYPE_U64 | CTLFLAG_RW | CTLFLAG_MPSAFE,
sbi, 0, erofs_sysctl_dir_ra, "QU",
"Directory readahead window in bytes") == NULL ||
SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "dir_ra_max_bytes", CTLFLAG_RD, &sbi->dir_ra_max_bytes, 0,
"Maximum directory readahead window") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "dir_ra_requests",
CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, 0, erofs_sysctl_dir_ra_metric, "QU",
"Directory readahead submissions") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "dir_ra_blocks",
CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, 1, erofs_sysctl_dir_ra_metric, "QU",
"Directory readahead blocks submitted") == NULL)
goto fail;
#if WITH_EROFS_ZIP
if (SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "drop_caches", CTLTYPE_U64 | CTLFLAG_WR | CTLFLAG_MPSAFE,
sbi, 0, erofs_sysctl_drop_caches, "QU",
"Write 1 to drop the FreeBSD decoded extent cache") == NULL)
goto fail;
if (SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "sync_decompress",
CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE,
sbi, 0, erofs_sysctl_zip_setting, "I",
"0=auto, 1=force synchronous, 2=taskqueue fallback") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(mount_node),
OID_AUTO, "cache_strategy",
CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE,
sbi, 1, erofs_sysctl_zip_setting, "I",
"0=disabled, 1=readahead, 2=readaround") == NULL)
goto fail;
cache_node = SYSCTL_ADD_NODE(&sbi->sysctl_ctx,
SYSCTL_CHILDREN(mount_node), OID_AUTO, "decoded_cache",
CTLFLAG_RD | CTLFLAG_MPSAFE, NULL,
"FreeBSD decoded extent cache, not Linux managed pclusters");
if (cache_node == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "hits", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi,
EROFS_CACHE_HITS, erofs_sysctl_cache_metric, "QU",
"Cache hits") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "misses", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi,
EROFS_CACHE_MISSES, erofs_sysctl_cache_metric, "QU",
"Cache misses") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "bypasses", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, EROFS_CACHE_BYPASSES, erofs_sysctl_cache_metric, "QU",
"Cache admission bypasses") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "evictions", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, EROFS_CACHE_EVICTIONS, erofs_sysctl_cache_metric, "QU",
"Cache evictions") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "reclaims", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, EROFS_CACHE_RECLAIMS, erofs_sysctl_cache_metric, "QU",
"Cache pressure reclaims") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "waiter_merges",
CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE,
sbi, EROFS_CACHE_WAITER_MERGES, erofs_sysctl_cache_metric, "QU",
"Inflight cache waiter merges") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "entries", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi,
EROFS_CACHE_ENTRIES, erofs_sysctl_cache_metric, "QU",
"Resident decoded extents") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "bytes", CTLTYPE_U64 | CTLFLAG_RD | CTLFLAG_MPSAFE, sbi,
EROFS_CACHE_BYTES, erofs_sysctl_cache_metric, "QU",
"Resident decoded bytes") == NULL ||
SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "worker_submissions", CTLFLAG_RD,
&sbi->decompress_submissions,
0, "Taskqueue decompression submissions") == NULL ||
SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "worker_runs", CTLFLAG_RD, &sbi->decompress_worker_runs,
0, "Taskqueue decompression worker runs") == NULL ||
SYSCTL_ADD_UINT(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "worker_count", CTLFLAG_RD, &sbi->decompress_worker_count,
0, "Bounded decompression worker count") == NULL ||
SYSCTL_ADD_U64(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "queue_fallbacks", CTLFLAG_RD,
&sbi->decompress_queue_fallbacks, 0,
"Direct decode after taskqueue fallback") == NULL ||
SYSCTL_ADD_U8(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "worker_cpu_bound", CTLFLAG_RD,
(uint8_t *)&sbi->decompress_worker_cpu_bound, 0,
"Workers restricted to the online CPU set") == NULL ||
SYSCTL_ADD_PROC(&sbi->sysctl_ctx, SYSCTL_CHILDREN(cache_node),
OID_AUTO, "worker_queue_priority",
CTLTYPE_INT | CTLFLAG_RW | CTLFLAG_MPSAFE, sbi, 0,
erofs_sysctl_worker_queue_priority, "I",
"Taskqueue order priority; 0 is highest and 255 is lowest") == NULL)
goto fail;
#else
(void)cache_node;
#endif
return (0);
fail:
error = erofs_unregister_sysfs(sbi);
return (error != 0 ? error : ENOMEM);
}
int
erofs_unregister_sysfs(struct erofs_sb_info *sbi)
{
int error;
if (!sbi->sysctl_registered)
return (0);
mtx_lock(&sbi->control_lock);
sbi->control_closing = true;
mtx_unlock(&sbi->control_lock);
error = sysctl_ctx_free(&sbi->sysctl_ctx);
if (error != 0) {
mtx_lock(&sbi->control_lock);
sbi->control_closing = false;
mtx_unlock(&sbi->control_lock);
return (error);
}
sbi->sysctl_registered = false;
return (0);
}