mirror of
https://github.com/freebsd/freebsd-src.git
synced 2026-10-28 11:25:21 +01:00
gzoned: Introduce Zoned Storage emulator
gzoned(8) is a new GEOM class that exposes a host-managed zoned device (similar to ZAC/ZBC drives) on top of regular, non-zoned providers. The created medium is sliced into equally sized zones, by default sequential-write-required. Such zones can be turned into conventional zones if desired. The zoned drive's state and configuration is persistent through metadata at the tail of the backing provider, meaning the zoned device gets recreated at the provider retaste. Zone state changes only mark the table dirty with BIO_FLUSH committing it, mirroring drives whose zone state is volatile until a cache flush. The new class tries to emulate real zoned drives by incorporating per-zone write pointers and support for BIO_ZONE management commands. Fault emulation through zone conditions (RWP recommended, offline, R/O), URSWRZ bit toggling and concurrent open zone limits are additional features useful for testing. While at it, fix zonectl's report of write pointer LBAs for zones that should have none, remove its forced debug flags and allow the geom(8) shared subroutines to detect host-managed drives. ATF-sh tests for gzoned(8) and zonectl(8) (dogfooded by gzoned(8)) are included, featuring helpers (see zoned_subr.sh) for testing zoned storage support in other components or GEOM classes. Reviewed by: asomers, fuz, ken Sponsored by: Google Summer of Code 2026 Pull Request: https://github.com/freebsd/freebsd-src/pull/2326
This commit is contained in:
1 parent
22e27e307f
commit
48efb6c4eb
26 files changed
+4142
-15
No files matched your search
@@ -333,6 +333,8 @@
|
||||
..
|
||||
virstor
|
||||
..
|
||||
zoned
|
||||
..
|
||||
..
|
||||
gssapi tags=package=kerberos-dev
|
||||
..
|
||||
|
||||
@@ -871,6 +871,8 @@
|
||||
..
|
||||
virstor
|
||||
..
|
||||
zoned
|
||||
..
|
||||
..
|
||||
..
|
||||
kern
|
||||
@@ -1323,6 +1325,8 @@
|
||||
..
|
||||
traceroute
|
||||
..
|
||||
zonectl
|
||||
..
|
||||
..
|
||||
..
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ LSUBDIRS= dev/acpica dev/agp dev/ciss dev/filemon dev/firewire \
|
||||
geom/cache geom/concat geom/eli geom/gate geom/journal geom/label \
|
||||
geom/mirror geom/mountver geom/multipath geom/nop \
|
||||
geom/raid geom/raid3 geom/shsec geom/stripe geom/union geom/virstor \
|
||||
geom/zoned \
|
||||
net/altq \
|
||||
net/route \
|
||||
netgraph/netflow \
|
||||
|
||||
@@ -22,3 +22,4 @@ GEOM_CLASSES+= shsec
|
||||
GEOM_CLASSES+= stripe
|
||||
GEOM_CLASSES+= union
|
||||
GEOM_CLASSES+= virstor
|
||||
GEOM_CLASSES+= zoned
|
||||
@@ -0,0 +1,5 @@
|
||||
PACKAGE=geom
|
||||
|
||||
GEOM_CLASS= zoned
|
||||
|
||||
.include <bsd.lib.mk>
|
||||
@@ -0,0 +1,16 @@
|
||||
# Autogenerated - do NOT edit!
|
||||
|
||||
DIRDEPS = \
|
||||
include \
|
||||
include/xlocale \
|
||||
lib/${CSU_DIR} \
|
||||
lib/libc \
|
||||
lib/libcompiler_rt \
|
||||
lib/libgeom \
|
||||
|
||||
|
||||
.include <dirdeps.mk>
|
||||
|
||||
.if ${DEP_RELDIR} == ${_DEP_RELDIR}
|
||||
# local dependencies - needed for -jN in clean tree
|
||||
.endif
|
||||
@@ -0,0 +1,289 @@
|
||||
/*-
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* Copyright (c) 2026 voidanix <voidanix@FreeBSD.org>
|
||||
*/
|
||||
|
||||
#include <sys/param.h>
|
||||
|
||||
#include <ctype.h>
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <strings.h>
|
||||
#include <libgeom.h>
|
||||
#include <geom/zoned/g_zoned.h>
|
||||
|
||||
#include "core/geom.h"
|
||||
#include "misc/subr.h"
|
||||
|
||||
uint32_t lib_version = G_LIB_VERSION;
|
||||
uint32_t version = G_ZONED_VERSION;
|
||||
|
||||
#define GZONED_ZONESIZE "256M"
|
||||
|
||||
static void zoned_main(struct gctl_req *req, unsigned flags);
|
||||
static void zoned_create(struct gctl_req *req);
|
||||
static void zoned_clear(struct gctl_req *req);
|
||||
|
||||
struct g_command class_commands[] = {
|
||||
{ "clear", G_FLAG_VERBOSE, zoned_main, G_NULL_OPTS, "[-v] dev ..." },
|
||||
{ "create", G_FLAG_VERBOSE | G_FLAG_LOADKLD, zoned_main,
|
||||
{
|
||||
{ 's', "zonesize", GZONED_ZONESIZE, G_TYPE_NUMBER },
|
||||
{ 'c', "conventional", "", G_TYPE_STRING },
|
||||
{ 'm', "maxopen", "0", G_TYPE_NUMBER },
|
||||
{ 'u', "restricted", NULL, G_TYPE_BOOL },
|
||||
G_OPT_SENTINEL
|
||||
},
|
||||
"[-uv] [-s zonesize] [-c zone[-zone][,zone[-zone]]...] "
|
||||
"[-m maxopen] dev"
|
||||
},
|
||||
{ "fault", G_FLAG_VERBOSE, NULL,
|
||||
{
|
||||
{ 'z', "zone", NULL, G_TYPE_NUMBER },
|
||||
{ 's', "state", NULL, G_TYPE_STRING },
|
||||
G_OPT_SENTINEL
|
||||
},
|
||||
"[-v] -z zone -s ro|offline|reset|clear name"
|
||||
},
|
||||
{ "destroy", G_FLAG_VERBOSE, NULL,
|
||||
{
|
||||
{ 'f', "force", NULL, G_TYPE_BOOL },
|
||||
G_OPT_SENTINEL
|
||||
},
|
||||
"[-fv] name ..." },
|
||||
{ "stop", G_FLAG_VERBOSE, NULL,
|
||||
{
|
||||
{ 'f', "force", NULL, G_TYPE_BOOL },
|
||||
G_OPT_SENTINEL
|
||||
},
|
||||
"[-fv] name ..." },
|
||||
G_CMD_SENTINEL
|
||||
};
|
||||
|
||||
static int verbose = 0;
|
||||
|
||||
static void
|
||||
zoned_main(struct gctl_req *req, unsigned flags)
|
||||
{
|
||||
const char *name;
|
||||
|
||||
if ((flags & G_FLAG_VERBOSE) != 0)
|
||||
verbose = 1;
|
||||
|
||||
name = gctl_get_ascii(req, "verb");
|
||||
if (name == NULL) {
|
||||
gctl_error(req, "No '%s' argument.", "verb");
|
||||
return;
|
||||
}
|
||||
if (strcmp(name, "create") == 0)
|
||||
zoned_create(req);
|
||||
else if (strcmp(name, "clear") == 0)
|
||||
zoned_clear(req);
|
||||
else
|
||||
gctl_error(req, "Unknown command: %s.", name);
|
||||
}
|
||||
|
||||
/*
|
||||
* Parse a conventional-zone specification into md_conv/md_nconv.
|
||||
*/
|
||||
static int
|
||||
zoned_parse_conv(struct gctl_req *req, const char *spec, uint32_t nzones,
|
||||
struct g_zoned_metadata *md)
|
||||
{
|
||||
char *buf, *ep, *sp, *tok;
|
||||
unsigned long first, last;
|
||||
uint32_t i;
|
||||
|
||||
md->md_nconv = 0;
|
||||
if (*spec == '\0')
|
||||
return (0);
|
||||
buf = strdup(spec);
|
||||
if (buf == NULL) {
|
||||
gctl_error(req, "Cannot allocate memory.");
|
||||
return (-1);
|
||||
}
|
||||
for (tok = strtok_r(buf, ",", &sp); tok != NULL;
|
||||
tok = strtok_r(NULL, ",", &sp)) {
|
||||
if (md->md_nconv == G_ZONED_MAXCONV) {
|
||||
gctl_error(req,
|
||||
"Too many conventional zone ranges (max %d).",
|
||||
G_ZONED_MAXCONV);
|
||||
goto fail;
|
||||
}
|
||||
errno = 0;
|
||||
if (!isdigit((unsigned char)*tok))
|
||||
goto syntax;
|
||||
first = strtoul(tok, &ep, 10);
|
||||
if (ep == tok || errno != 0)
|
||||
goto syntax;
|
||||
if (*ep == '-') {
|
||||
tok = ep + 1;
|
||||
if (!isdigit((unsigned char)*tok))
|
||||
goto syntax;
|
||||
last = strtoul(tok, &ep, 10);
|
||||
if (ep == tok || *ep != '\0' || errno != 0)
|
||||
goto syntax;
|
||||
} else if (*ep == '\0') {
|
||||
last = first;
|
||||
} else {
|
||||
goto syntax;
|
||||
}
|
||||
if (last < first) {
|
||||
gctl_error(req, "Backwards zone range %lu-%lu.",
|
||||
first, last);
|
||||
goto fail;
|
||||
}
|
||||
if (last >= nzones) {
|
||||
gctl_error(req,
|
||||
"Zone %lu does not exist (zones 0-%u).", last,
|
||||
nzones - 1);
|
||||
goto fail;
|
||||
}
|
||||
for (i = 0; i < md->md_nconv; i++) {
|
||||
uint32_t ofirst, olast;
|
||||
|
||||
ofirst = md->md_conv[i].cr_first;
|
||||
olast = ofirst + md->md_conv[i].cr_count - 1;
|
||||
if (first <= olast && ofirst <= last) {
|
||||
gctl_error(req, "Zone range %lu-%lu overlaps "
|
||||
"%u-%u.", first, last, ofirst, olast);
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
md->md_conv[md->md_nconv].cr_first = first;
|
||||
md->md_conv[md->md_nconv].cr_count = last - first + 1;
|
||||
md->md_nconv++;
|
||||
}
|
||||
free(buf);
|
||||
return (0);
|
||||
syntax:
|
||||
gctl_error(req, "Invalid conventional zone specification '%s'.",
|
||||
spec);
|
||||
fail:
|
||||
free(buf);
|
||||
return (-1);
|
||||
}
|
||||
|
||||
static void
|
||||
zoned_create(struct gctl_req *req)
|
||||
{
|
||||
struct g_zoned_metadata md;
|
||||
u_char sector[512];
|
||||
const char *conv, *dev;
|
||||
off_t msize, zonesize;
|
||||
intmax_t maxopen;
|
||||
unsigned int secsize;
|
||||
uint32_t i, nconv, nzones;
|
||||
int nargs;
|
||||
|
||||
bzero(sector, sizeof(sector));
|
||||
bzero(&md, sizeof(md));
|
||||
nargs = gctl_get_int(req, "nargs");
|
||||
if (nargs != 1) {
|
||||
gctl_error(req,
|
||||
"Usage: create [-u] [-s zonesize] [-c zones] "
|
||||
"[-m maxopen] dev");
|
||||
return;
|
||||
}
|
||||
zonesize = (off_t)gctl_get_intmax(req, "zonesize");
|
||||
conv = gctl_get_ascii(req, "conventional");
|
||||
dev = gctl_get_ascii(req, "arg0");
|
||||
|
||||
msize = g_get_mediasize(dev);
|
||||
secsize = g_get_sectorsize(dev);
|
||||
if (msize == 0 || secsize == 0) {
|
||||
gctl_error(req, "Can't get information about %s: %s.", dev,
|
||||
strerror(errno));
|
||||
return;
|
||||
}
|
||||
if (g_provider_is_host_managed(dev)) {
|
||||
gctl_error(req, "Cannot create a zoned device on %s: the tail "
|
||||
"of a host-managed zoned device cannot hold the metadata "
|
||||
"and zone state table.", dev);
|
||||
return;
|
||||
}
|
||||
if (zonesize <= 0 || (zonesize % secsize) != 0) {
|
||||
gctl_error(req, "Zone size must be a positive multiple of %u.",
|
||||
secsize);
|
||||
return;
|
||||
}
|
||||
if ((uint64_t)zonesize / secsize > G_ZONED_MAXZONESECS) {
|
||||
gctl_error(req, "Zone size %jd exceeds %u sectors.",
|
||||
(intmax_t)zonesize, G_ZONED_MAXZONESECS);
|
||||
return;
|
||||
}
|
||||
nzones = g_zoned_nzones(msize, zonesize, secsize);
|
||||
if (nzones == 0) {
|
||||
gctl_error(req, "Zone size %jd is unusable for %s: it must "
|
||||
"yield at least one zone and at most %u.",
|
||||
(intmax_t)zonesize, dev, G_ZONED_MAXZONES);
|
||||
return;
|
||||
}
|
||||
if (zoned_parse_conv(req, conv, nzones, &md) != 0)
|
||||
return;
|
||||
maxopen = gctl_get_intmax(req, "maxopen");
|
||||
if (maxopen < 0 || maxopen > nzones) {
|
||||
gctl_error(req, "Maximum open zones must be between 0 and "
|
||||
"the number of zones (%u).", nzones);
|
||||
return;
|
||||
}
|
||||
|
||||
memcpy(md.md_magic, G_ZONED_MAGIC, sizeof(G_ZONED_MAGIC));
|
||||
md.md_version = G_ZONED_VERSION;
|
||||
md.md_id = arc4random();
|
||||
md.md_zonesize = zonesize;
|
||||
md.md_sectorsize = secsize;
|
||||
md.md_provsize = msize;
|
||||
md.md_maxopen = (uint32_t)maxopen;
|
||||
if (gctl_get_int(req, "restricted"))
|
||||
md.md_flags |= G_ZONED_MD_RESTRICTED_READS;
|
||||
|
||||
/*
|
||||
* Hand the geometry to the kernel rather than writing it on the media
|
||||
* and waiting for the taste. The metadata should be written only once
|
||||
* the device is up: a request the kernel turns down leaves the provider
|
||||
* untouched.
|
||||
*/
|
||||
zoned_metadata_encode(&md, sector);
|
||||
gctl_ro_param(req, "metadata", sizeof(sector), sector);
|
||||
if (gctl_issue(req) != NULL)
|
||||
return;
|
||||
if (verbose) {
|
||||
nconv = 0;
|
||||
for (i = 0; i < md.md_nconv; i++)
|
||||
nconv += md.md_conv[i].cr_count;
|
||||
printf("Device %s zoned: %u zones of %jd bytes"
|
||||
" (%u conventional).\n", dev, nzones, (intmax_t)zonesize,
|
||||
nconv);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
zoned_clear(struct gctl_req *req)
|
||||
{
|
||||
const char *name;
|
||||
int error, i, nargs;
|
||||
|
||||
nargs = gctl_get_int(req, "nargs");
|
||||
if (nargs < 1) {
|
||||
gctl_error(req, "Missing device(s).");
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0; i < nargs; i++) {
|
||||
name = gctl_get_ascii(req, "arg%d", i);
|
||||
error = g_metadata_clear(name, G_ZONED_MAGIC);
|
||||
if (error != 0) {
|
||||
fprintf(stderr, "Can't clear metadata on %s: %s.\n",
|
||||
name, strerror(error));
|
||||
gctl_error(req, "Not fully done.");
|
||||
continue;
|
||||
}
|
||||
if (verbose)
|
||||
printf("Metadata cleared on %s.\n", name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
.\" SPDX-License-Identifier: BSD-2-Clause
|
||||
.\"
|
||||
.\" Copyright (c) 2026 voidanix <[email protected]>
|
||||
.\"
|
||||
.Dd August 4, 2026
|
||||
.Dt GZONED 8
|
||||
.Os
|
||||
.Sh NAME
|
||||
.Nm gzoned
|
||||
.Nd "emulate a host-managed zoned device on top of a regular provider"
|
||||
.Sh SYNOPSIS
|
||||
.Nm
|
||||
.Cm create
|
||||
.Op Fl uv
|
||||
.Op Fl s Ar zonesize
|
||||
.Op Fl c Ar zones
|
||||
.Op Fl m Ar maxopen
|
||||
.Ar dev
|
||||
.Nm
|
||||
.Cm fault
|
||||
.Op Fl v
|
||||
.Fl z Ar zone
|
||||
.Fl s Ar state
|
||||
.Ar name
|
||||
.Nm
|
||||
.Cm destroy
|
||||
.Op Fl fv
|
||||
.Ar name ...
|
||||
.Nm
|
||||
.Cm stop
|
||||
.Op Fl fv
|
||||
.Ar name ...
|
||||
.Nm
|
||||
.Cm clear
|
||||
.Op Fl v
|
||||
.Ar dev ...
|
||||
.Nm
|
||||
.Cm list
|
||||
.Nm
|
||||
.Cm status
|
||||
.Nm
|
||||
.Cm load
|
||||
.Nm
|
||||
.Cm unload
|
||||
.Sh DESCRIPTION
|
||||
The
|
||||
.Nm
|
||||
utility presents to the system a host-managed zoned block device backed by a
|
||||
regular, non-zoned provider.
|
||||
The exposed device is of the kind described by the
|
||||
SCSI Zoned Block Commands (ZBC) and ATA Zoned ATA Command Set (ZAC)
|
||||
specifications.
|
||||
.Pp
|
||||
The backing provider is divided into equally sized, sequential-write-required
|
||||
zones, unless marked conventional by the
|
||||
.Fl c
|
||||
option.
|
||||
For each zone the GEOM class maintains a write pointer and answers the
|
||||
zone-management commands
|
||||
.Pq see Xr zonectl 8
|
||||
issued through the
|
||||
.Dv DIOCZONECMD
|
||||
ioctl.
|
||||
.Pp
|
||||
Resetting a write pointer issues a best-effort
|
||||
.Dv BIO_DELETE
|
||||
for the zone's range to the backing provider, resembling a real drives
|
||||
de-allocating a reset zone.
|
||||
.Dv BIO_DELETE
|
||||
requests to the zoned device itself are rejected, as zoned devices have no
|
||||
unmap.
|
||||
Sequential zones are reclaimed by resetting the write pointer.
|
||||
.Pp
|
||||
The configuration for a device is persistent.
|
||||
A metadata block and the per-zone state are stored in a small area reserved at
|
||||
the tail of the backing provider, so the zoned device is recreated
|
||||
automatically whenever the provider appears
|
||||
.Pq at boot, module load or hot-plug ,
|
||||
with its write pointers restored.
|
||||
Note that because of the metadata location on the backing provider, the zoned
|
||||
device is torn down if the backing provider is resized with unflushed zone state
|
||||
changes being discarded.
|
||||
Zone state changes are committed to the backing provider on a BIO_FLUSH.
|
||||
Changes made since the last flush may be rolled back by an unclean shutdown,
|
||||
resembling a drive whose zone state is volatile until flushed.
|
||||
.Pp
|
||||
The first argument to
|
||||
.Nm
|
||||
indicates an action to be performed:
|
||||
.Bl -tag -width ".Cm destroy"
|
||||
.It Cm create
|
||||
Mark
|
||||
.Ar dev
|
||||
as zoned, creating a device named after the backing provider with
|
||||
.Dq .zoned
|
||||
appended
|
||||
.Pq e.g. Pa /dev/da0.zoned .
|
||||
The number of zones is the usable size of
|
||||
.Ar dev
|
||||
divided by
|
||||
.Ar zonesize ;
|
||||
a small reservation at the tail of
|
||||
.Ar dev
|
||||
holds the zone metadata; any trailing space that does not make up a full zone
|
||||
is not exposed.
|
||||
Host-managed zoned providers are refused: their tail normally lies in a
|
||||
sequential-write-required zone and cannot hold the metadata and zone state
|
||||
table.
|
||||
.It Cm fault
|
||||
Inject a zone fault into the running device
|
||||
.Ar name .
|
||||
.Fl z Ar zone
|
||||
selects the zone by number and
|
||||
.Fl s Ar state
|
||||
is one of
|
||||
.Ar ro
|
||||
.Pq Ar readonly ,
|
||||
.Ar offline ,
|
||||
.Ar reset
|
||||
.Pq zone needs a write-pointer reset
|
||||
or
|
||||
.Ar clear
|
||||
.Pq return the zone to its normal state implied by its write pointer .
|
||||
Readonly and offline zones reject writes and zone-management commands;
|
||||
the fault persists with the rest of the zone state until cleared.
|
||||
.It Cm clear
|
||||
Permanently un-zone the given backing device(s) by clearing the on-disk
|
||||
metadata.
|
||||
A backing device carrying a live zoned device is held open and cannot be
|
||||
cleared: tear the device down with
|
||||
.Cm stop
|
||||
first.
|
||||
.It Cm stop
|
||||
Tear down the given zoned device(s) without touching the on-disk metadata.
|
||||
.Ar name
|
||||
is the zoned device
|
||||
.Pq e.g. Pa da0.zoned .
|
||||
Unflushed zone state is committed to the backing provider first.
|
||||
The device comes back the next time the backing provider is tasted
|
||||
.Pq e.g. at boot or when it is opened and closed again .
|
||||
.It Cm destroy
|
||||
Same as
|
||||
.Cm stop .
|
||||
.It Cm list
|
||||
See
|
||||
.Xr geom 8 .
|
||||
.It Cm status
|
||||
See
|
||||
.Xr geom 8 .
|
||||
.It Cm load
|
||||
See
|
||||
.Xr geom 8 .
|
||||
.It Cm unload
|
||||
See
|
||||
.Xr geom 8 .
|
||||
.El
|
||||
.Pp
|
||||
Additional options:
|
||||
.Bl -tag -width ".Fl s Ar zonesize"
|
||||
.It Fl c Ar zones
|
||||
Mark the given zones as conventional
|
||||
.Pq randomly writable, no write pointer .
|
||||
.Ar zones
|
||||
is a comma-separated list of zone numbers and inclusive ranges, counted
|
||||
from zero, e.g.\&
|
||||
.Ar 60 ,
|
||||
.Ar 60-70
|
||||
or
|
||||
.Ar 0-3,60,90-95 .
|
||||
At most 16 comma-separated entries may be given.
|
||||
By default all zones are sequential-write-required.
|
||||
.It Fl f
|
||||
Tear down the device even if it is still open.
|
||||
.It Fl m Ar maxopen
|
||||
Limit the number of simultaneously open sequential zones.
|
||||
Once the limit is reached, implicitly opened zones are implicitly closed to make
|
||||
room.
|
||||
If every open zone was opened explicitly, further writes and explicit opens fail
|
||||
with
|
||||
.Er ENOSPC .
|
||||
The default of 0 means unlimited.
|
||||
.It Fl s Ar zonesize
|
||||
Size of a single zone.
|
||||
The value must be a multiple of the backing provider's sector size and may use
|
||||
the usual size suffixes
|
||||
.Pq e.g. Ar 7G , 64M .
|
||||
The default is
|
||||
.Ar 256M .
|
||||
It must also leave the device with at least one, and at most 4294967295 zones.
|
||||
.It Fl u
|
||||
Restrict reads in sequential-write-required zones
|
||||
.Pq unset URSWRZ bit .
|
||||
Similarly to real zoned drives, the device supports unrestricted reads that are
|
||||
passed through unchecked by default.
|
||||
With
|
||||
.Fl u
|
||||
the device instead reports URSWRZ as unset and rejects with
|
||||
.Er EIO
|
||||
any read in a sequential zone that extends beyond that zone's write pointer, as
|
||||
well as any read that crosses out of a conventional zone into a sequential one.
|
||||
Note that this renders the device unreadable to zone-unaware consumers.
|
||||
Tasting the zoned provider
|
||||
.Pq e.g. partition table scanning
|
||||
will also fail until the accessed zones have been written.
|
||||
.It Fl v
|
||||
Be more verbose.
|
||||
.El
|
||||
.Sh SYSCTL VARIABLES
|
||||
The following
|
||||
.Xr sysctl 8
|
||||
variable can be used to control the behavior of the
|
||||
.Nm ZONED
|
||||
GEOM class.
|
||||
.Bl -tag -width indent
|
||||
.It Va kern.geom.zoned.debug : No 0
|
||||
Debug level of the
|
||||
.Nm ZONED
|
||||
GEOM class.
|
||||
This can be set to a number between 0 and 3 inclusive.
|
||||
If set to 0, minimal debug information is printed; if set to 3, the maximum
|
||||
amount of debug information is printed.
|
||||
.El
|
||||
.Sh EXIT STATUS
|
||||
.Ex -std
|
||||
.Sh EXAMPLES
|
||||
Expose the memory disk
|
||||
.Pa /dev/md0
|
||||
as a host-managed zoned device
|
||||
.Pa /dev/md0.zoned
|
||||
with one-gigabyte zones, the first four of which are conventional.
|
||||
Inspect its zones, then permanently un-zone it:
|
||||
.Bd -literal -offset indent
|
||||
mdconfig -t malloc -s 10G -u md0
|
||||
gzoned create -s 1G -c 0-3 /dev/md0
|
||||
zonectl -d /dev/md0.zoned -c params
|
||||
zonectl -d /dev/md0.zoned -c rz
|
||||
gzoned clear /dev/md0
|
||||
.Ed
|
||||
.Pp
|
||||
The same operations are also available through the
|
||||
.Xr geom 8
|
||||
front end:
|
||||
.Bd -literal -offset indent
|
||||
geom zoned create -s 128M /dev/da0
|
||||
geom zoned stop da0.zoned
|
||||
geom zoned clear /dev/da0
|
||||
.Ed
|
||||
.Sh SEE ALSO
|
||||
.Xr geom 4 ,
|
||||
.Xr md 4 ,
|
||||
.Xr geom 8 ,
|
||||
.Xr mdconfig 8 ,
|
||||
.Xr zonectl 8
|
||||
.Sh HISTORY
|
||||
The
|
||||
.Nm
|
||||
utility appeared in
|
||||
.Fx 16.0 .
|
||||
.Sh AUTHORS
|
||||
.An voidanix Aq Mt [email protected]
|
||||
@@ -145,12 +145,12 @@ are:
|
||||
.Pp
|
||||
.Bl -column "* MULTIPATH" "* MULTIPATH" "* MULTIPATH" -offset indent
|
||||
.It \(bu CACHE Ta \(bu MIRROR Ta \(bu SCHED
|
||||
.It \(bu CONCAT Ta \(bu MOUNTVER Ta \(bu SHSEC
|
||||
.It \(bu CONCAT Ta \(bu MOUNTVER Ta \(bu SHSEC
|
||||
.It \(bu DISK Ta \(bu MULTIPATH Ta \(bu STRIPE
|
||||
.It \(bu ELI Ta \(bu NOP Ta \(bu UNION
|
||||
.It \(bu JOURNAL Ta \(bu PART Ta \(bu VFS
|
||||
.It \(bu LABEL Ta \(bu RAID Ta \(bu VIRSTOR
|
||||
.It \(bu MD Ta \(bu RAID3
|
||||
.It \(bu MD Ta \(bu RAID3 Ta \(bu ZONED
|
||||
.El
|
||||
.Sh ENVIRONMENT
|
||||
The following environment variables affect the execution of
|
||||
@@ -202,7 +202,8 @@ geom md unload
|
||||
.Xr gshsec 8 ,
|
||||
.Xr gstripe 8 ,
|
||||
.Xr gunion 8 ,
|
||||
.Xr gvirstor 8
|
||||
.Xr gvirstor 8 ,
|
||||
.Xr gzoned 8
|
||||
.Sh HISTORY
|
||||
The
|
||||
.Nm
|
||||
|
||||
@@ -28,7 +28,9 @@
|
||||
|
||||
#include <sys/param.h>
|
||||
#include <sys/disk.h>
|
||||
#include <sys/disk_zone.h>
|
||||
#include <sys/endian.h>
|
||||
#include <sys/ioctl.h>
|
||||
#include <sys/uio.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
@@ -218,6 +220,24 @@ g_get_sectorsize(const char *name)
|
||||
return ((unsigned int)sectorsize);
|
||||
}
|
||||
|
||||
int
|
||||
g_provider_is_host_managed(const char *name)
|
||||
{
|
||||
struct disk_zone_args zone_args;
|
||||
int error, fd;
|
||||
|
||||
bzero(&zone_args, sizeof(zone_args));
|
||||
zone_args.zone_cmd = DISK_ZONE_GET_PARAMS;
|
||||
fd = g_open(name, 0);
|
||||
if (fd == -1)
|
||||
return (0);
|
||||
error = ioctl(fd, DIOCZONECMD, &zone_args);
|
||||
(void)g_close(fd);
|
||||
return (error == 0 &&
|
||||
zone_args.zone_params.disk_params.zone_mode ==
|
||||
DISK_ZONE_MODE_HOST_MANAGED);
|
||||
}
|
||||
|
||||
int
|
||||
g_metadata_read(const char *name, unsigned char *md, size_t size,
|
||||
const char *magic)
|
||||
|
||||
@@ -36,6 +36,7 @@ int g_parse_lba(const char *lbastr, unsigned int sectorsize, off_t *sectors);
|
||||
|
||||
off_t g_get_mediasize(const char *name);
|
||||
unsigned int g_get_sectorsize(const char *name);
|
||||
int g_provider_is_host_managed(const char *name);
|
||||
|
||||
int g_metadata_read(const char *name, unsigned char *md, size_t size,
|
||||
const char *magic);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,291 @@
|
||||
/*-
|
||||
* SPDX-License-Identifier: BSD-2-Clause
|
||||
*
|
||||
* Copyright (c) 2026 voidanix <voidanix@FreeBSD.org>
|
||||
*/
|
||||
|
||||
#ifndef _G_ZONED_H_
|
||||
#define _G_ZONED_H_
|
||||
|
||||
#include <sys/endian.h>
|
||||
|
||||
#define G_ZONED_CLASS_NAME "ZONED"
|
||||
#define G_ZONED_MAGIC "GEOM::ZONED"
|
||||
#define G_ZONED_TABLE_MAGIC "GEOM::ZONEDTBL"
|
||||
/* Appended to the backing provider's name to name the zoned provider. */
|
||||
#define G_ZONED_SUFFIX ".zoned"
|
||||
|
||||
/*
|
||||
* Version history:
|
||||
* 1 - Initial version number.
|
||||
*/
|
||||
#define G_ZONED_VERSION 1
|
||||
|
||||
/*
|
||||
* Sentinel for "unlimited open/active sequential zones".
|
||||
* See SVPD_ZBDC_MAX_SEQ_UNLIMITED.
|
||||
*/
|
||||
#define G_ZONED_SEQ_UNLIMITED 0xffffffff
|
||||
|
||||
/*
|
||||
* On-disk layout, carved out of the end of the backing provider:
|
||||
*
|
||||
* [ ... usable (zoned) region ... | table header | zone entries | metadata ]
|
||||
* (1 sector) (N entries) (last sec)
|
||||
*
|
||||
* Table header + zone entries hold the live per-zone state and are maintained
|
||||
* entirely by the kernel.
|
||||
*/
|
||||
/* On-disk zone-state table header, at the start of the table's first sector. */
|
||||
struct g_zoned_table_hdr {
|
||||
char th_magic[16]; /* G_ZONED_TABLE_MAGIC. */
|
||||
uint32_t th_version; /* Version number. */
|
||||
uint32_t th_nzones; /* Zone entries that follow. */
|
||||
};
|
||||
_Static_assert(sizeof(struct g_zoned_table_hdr) == 24,
|
||||
"on-disk zone table header layout changed");
|
||||
|
||||
/* On-disk zone-state table entry. */
|
||||
struct g_zoned_disk_entry {
|
||||
uint8_t de_type; /* Zone type. */
|
||||
uint8_t de_condition; /* Zone condition. */
|
||||
uint8_t de_flags; /* Zone attribute flags. */
|
||||
uint8_t de_reserved;
|
||||
uint32_t de_write_pointer; /* WP offset in the zone. */
|
||||
};
|
||||
#define G_ZONED_ENTRY_SIZE (sizeof(struct g_zoned_disk_entry))
|
||||
_Static_assert(G_ZONED_ENTRY_SIZE == 8, "on-disk zone entry layout changed");
|
||||
|
||||
/*
|
||||
* write_pointer_lba for zones that have no valid write pointer. Report all
|
||||
* ones across the whole 64-bit field, like a ZBC drive might.
|
||||
*/
|
||||
#define G_ZONED_WP_NONE_LBA 0xffffffffffffffff
|
||||
/* On-disk, zone-relative counterpart in de_write_pointer. */
|
||||
#define G_ZONED_WP_NONE 0xffffffff
|
||||
/*
|
||||
* Zone size cap (in sectors) keeping every valid zone-relative write pointer,
|
||||
* including that of a full zone (== zone length), below G_ZONED_WP_NONE.
|
||||
*/
|
||||
#define G_ZONED_MAXZONESECS (G_ZONED_WP_NONE - 1)
|
||||
|
||||
/*
|
||||
* Maximum number of conventional-zone ranges that fit in the metadata.
|
||||
*/
|
||||
#define G_ZONED_MAXCONV 16
|
||||
|
||||
/* Maximum number of zones for a single device. */
|
||||
#define G_ZONED_MAXZONES UINT32_MAX
|
||||
|
||||
/*
|
||||
* A run of consecutive conventional (non-sequential-write) zones. ZBC/ZAC
|
||||
* identify zones by their start LBA, a 64-bit field; zone indices get the same
|
||||
* width.
|
||||
*/
|
||||
struct g_zoned_convrange {
|
||||
uint64_t cr_first; /* First zone of the range. */
|
||||
uint64_t cr_count; /* Number of zones in the range. */
|
||||
};
|
||||
|
||||
/* md_flags bits. */
|
||||
#define G_ZONED_MD_RESTRICTED_READS 0x00000001 /* URSWRZ off. */
|
||||
#define G_ZONED_MD_FLAGSMASK G_ZONED_MD_RESTRICTED_READS
|
||||
|
||||
struct g_zoned_metadata {
|
||||
char md_magic[16]; /* Magic value. */
|
||||
uint32_t md_version; /* Version number. */
|
||||
uint32_t md_id; /* Unique ID. */
|
||||
uint64_t md_zonesize; /* Zone size in bytes. */
|
||||
uint32_t md_sectorsize; /* Provider sector size in bytes. */
|
||||
uint32_t md_nconv; /* Conventional ranges in use. */
|
||||
uint64_t md_provsize; /* Provider size in bytes. */
|
||||
struct g_zoned_convrange md_conv[G_ZONED_MAXCONV];
|
||||
uint32_t md_flags; /* G_ZONED_MD_* flags. */
|
||||
uint32_t md_maxopen; /* Open zone limit, 0 = unlimited. */
|
||||
};
|
||||
_Static_assert(sizeof(struct g_zoned_metadata) == 312,
|
||||
"on-disk metadata layout changed");
|
||||
_Static_assert(__offsetof(struct g_zoned_metadata, md_conv) == 48,
|
||||
"on-disk metadata layout changed");
|
||||
|
||||
/*
|
||||
* The on-disk image has the same layout as the struct, so marshalling is a
|
||||
* matter of byte order. The image is copied in and out rather than cast to,
|
||||
* as the buffers holding it are only guaranteed to be byte aligned.
|
||||
*/
|
||||
static __inline void
|
||||
zoned_metadata_encode(const struct g_zoned_metadata *md, u_char *data)
|
||||
{
|
||||
struct g_zoned_metadata d;
|
||||
u_int i;
|
||||
|
||||
memset(&d, 0, sizeof(d));
|
||||
memcpy(d.md_magic, md->md_magic, sizeof(d.md_magic));
|
||||
d.md_version = htole32(md->md_version);
|
||||
d.md_id = htole32(md->md_id);
|
||||
d.md_zonesize = htole64(md->md_zonesize);
|
||||
d.md_sectorsize = htole32(md->md_sectorsize);
|
||||
d.md_nconv = htole32(md->md_nconv);
|
||||
d.md_provsize = htole64(md->md_provsize);
|
||||
for (i = 0; i < G_ZONED_MAXCONV; i++) {
|
||||
d.md_conv[i].cr_first = htole64(md->md_conv[i].cr_first);
|
||||
d.md_conv[i].cr_count = htole64(md->md_conv[i].cr_count);
|
||||
}
|
||||
d.md_flags = htole32(md->md_flags);
|
||||
d.md_maxopen = htole32(md->md_maxopen);
|
||||
memcpy(data, &d, sizeof(d));
|
||||
}
|
||||
|
||||
static __inline void
|
||||
zoned_metadata_decode(const u_char *data, struct g_zoned_metadata *md)
|
||||
{
|
||||
struct g_zoned_metadata d;
|
||||
u_int i;
|
||||
|
||||
memcpy(&d, data, sizeof(d));
|
||||
memcpy(md->md_magic, d.md_magic, sizeof(md->md_magic));
|
||||
md->md_version = le32toh(d.md_version);
|
||||
md->md_id = le32toh(d.md_id);
|
||||
md->md_zonesize = le64toh(d.md_zonesize);
|
||||
md->md_sectorsize = le32toh(d.md_sectorsize);
|
||||
md->md_nconv = le32toh(d.md_nconv);
|
||||
md->md_provsize = le64toh(d.md_provsize);
|
||||
for (i = 0; i < G_ZONED_MAXCONV; i++) {
|
||||
md->md_conv[i].cr_first = le64toh(d.md_conv[i].cr_first);
|
||||
md->md_conv[i].cr_count = le64toh(d.md_conv[i].cr_count);
|
||||
}
|
||||
md->md_flags = le32toh(d.md_flags);
|
||||
md->md_maxopen = le32toh(d.md_maxopen);
|
||||
}
|
||||
|
||||
/*
|
||||
* Number of zones a provider of a given geometry can hold after reserving room
|
||||
* at the tail for the metadata sector and the zone-state table. Returns 0 if
|
||||
* the zone size leaves room for no zone at all, or if it yields more zones
|
||||
* than G_ZONED_MAXZONES.
|
||||
*/
|
||||
static __inline uint32_t
|
||||
g_zoned_nzones(off_t mediasize, off_t zonesize, u_int secsize)
|
||||
{
|
||||
uint64_t nmax, nzones, reserve;
|
||||
|
||||
if (zonesize <= 0 || secsize == 0 || mediasize <= 0)
|
||||
return (0);
|
||||
/* Over-reserve using the zone count that ignores the reservation. */
|
||||
nmax = (uint64_t)mediasize / (uint64_t)zonesize;
|
||||
if (nmax == 0)
|
||||
return (0);
|
||||
/* Metadata sector + table header sector + sector-aligned entries. */
|
||||
reserve = 2 * (uint64_t)secsize +
|
||||
roundup2(nmax * G_ZONED_ENTRY_SIZE, (uint64_t)secsize);
|
||||
if ((uint64_t)mediasize <= reserve)
|
||||
return (0);
|
||||
nzones = ((uint64_t)mediasize - reserve) / (uint64_t)zonesize;
|
||||
/*
|
||||
* Reject rather than truncate, as wrapped counts can quietly describe
|
||||
* devices of the wrong size. Note that nzones must be wider than the
|
||||
* bound for this to work.
|
||||
*/
|
||||
if (nzones > G_ZONED_MAXZONES)
|
||||
return (0);
|
||||
return ((uint32_t)nzones);
|
||||
}
|
||||
|
||||
#ifdef _KERNEL
|
||||
#define G_ZONED_DEBUG(lvl, ...) \
|
||||
_GEOM_DEBUG("GEOM_ZONED", g_zoned_debug, (lvl), NULL, __VA_ARGS__)
|
||||
#define G_ZONED_LOGREQLVL(lvl, bp, ...) \
|
||||
_GEOM_DEBUG("GEOM_ZONED", g_zoned_debug, (lvl), (bp), __VA_ARGS__)
|
||||
#define G_ZONED_LOGREQ(bp, ...) G_ZONED_LOGREQLVL(2, bp, __VA_ARGS__)
|
||||
|
||||
/*
|
||||
* Live state of one emulated zoned device. The sc_zones array mirrors the
|
||||
* on-disk zone-state table; dirty entries are re-encoded and written out
|
||||
* lazily on BIO_FLUSH. Zone states are kept in RAM.
|
||||
*/
|
||||
struct g_zoned_softc {
|
||||
struct mtx sc_lock;
|
||||
uint32_t sc_id; /* Unique ID. */
|
||||
off_t sc_zonesize; /* Zone bytes. */
|
||||
u_int sc_secsize; /* Sector bytes. */
|
||||
uint64_t sc_zonesecs; /* Zone sectors. */
|
||||
uint32_t sc_nzones; /* Number of zones. */
|
||||
uint64_t sc_maxlba; /* Last LBA + 1. */
|
||||
uint32_t sc_nconv; /* Conv. ranges. */
|
||||
uint32_t sc_convzones; /* Conv. zones. */
|
||||
uint32_t sc_maxopen; /* 0 = unlimited. */
|
||||
uint32_t sc_nopen; /* Open zones. */
|
||||
bool sc_rdrestrict; /* URSWRZ off. */
|
||||
struct g_zoned_convrange sc_conv[G_ZONED_MAXCONV];
|
||||
struct disk_zone_rep_entry *sc_zones; /* sc_nzones long. */
|
||||
/* Zone-state table placement and dirty tracking. */
|
||||
uint32_t sc_tabsecs; /* Table sectors. */
|
||||
off_t sc_taboff; /* Table offset. */
|
||||
bool sc_dirty; /* Pending writes. */
|
||||
bool sc_hdrdirty; /* Header dirty. */
|
||||
uint32_t sc_dirtylo; /* First dirty zone. */
|
||||
uint32_t sc_dirtyhi; /* Last dirty zone. */
|
||||
/* Statistics. */
|
||||
uintmax_t sc_reads;
|
||||
uintmax_t sc_writes;
|
||||
uintmax_t sc_readbytes;
|
||||
uintmax_t sc_wrotebytes;
|
||||
uintmax_t sc_zonecmds;
|
||||
};
|
||||
|
||||
static __inline void
|
||||
zoned_table_hdr_encode(const struct g_zoned_table_hdr *th, u_char *data)
|
||||
{
|
||||
struct g_zoned_table_hdr d;
|
||||
|
||||
memset(&d, 0, sizeof(d));
|
||||
memcpy(d.th_magic, th->th_magic, sizeof(d.th_magic));
|
||||
d.th_version = htole32(th->th_version);
|
||||
d.th_nzones = htole32(th->th_nzones);
|
||||
memcpy(data, &d, sizeof(d));
|
||||
}
|
||||
|
||||
static __inline void
|
||||
zoned_table_hdr_decode(const u_char *data, struct g_zoned_table_hdr *th)
|
||||
{
|
||||
struct g_zoned_table_hdr d;
|
||||
|
||||
memcpy(&d, data, sizeof(d));
|
||||
memcpy(th->th_magic, d.th_magic, sizeof(th->th_magic));
|
||||
th->th_version = le32toh(d.th_version);
|
||||
th->th_nzones = le32toh(d.th_nzones);
|
||||
}
|
||||
|
||||
static __inline void
|
||||
g_zoned_entry_encode(const struct disk_zone_rep_entry *z, u_char *data)
|
||||
{
|
||||
struct g_zoned_disk_entry de;
|
||||
|
||||
de.de_type = z->zone_type;
|
||||
de.de_condition = z->zone_condition;
|
||||
de.de_flags = z->zone_flags;
|
||||
de.de_reserved = 0;
|
||||
de.de_write_pointer = htole32(
|
||||
z->write_pointer_lba == G_ZONED_WP_NONE_LBA ? G_ZONED_WP_NONE :
|
||||
(uint32_t)(z->write_pointer_lba - z->zone_start_lba));
|
||||
memcpy(data, &de, sizeof(de));
|
||||
}
|
||||
|
||||
static __inline void
|
||||
g_zoned_entry_decode(const u_char *data, struct disk_zone_rep_entry *z,
|
||||
uint64_t start_lba)
|
||||
{
|
||||
struct g_zoned_disk_entry de;
|
||||
uint32_t wp;
|
||||
|
||||
memcpy(&de, data, sizeof(de));
|
||||
z->zone_type = de.de_type;
|
||||
z->zone_condition = de.de_condition;
|
||||
z->zone_flags = de.de_flags;
|
||||
wp = le32toh(de.de_write_pointer);
|
||||
z->write_pointer_lba = (wp == G_ZONED_WP_NONE) ? G_ZONED_WP_NONE_LBA :
|
||||
start_lba + wp;
|
||||
}
|
||||
#endif /* _KERNEL */
|
||||
|
||||
#endif /* _G_ZONED_H_ */
|
||||
@@ -21,7 +21,8 @@ SUBDIR= geom_cache \
|
||||
geom_union \
|
||||
geom_uzip \
|
||||
geom_virstor \
|
||||
geom_zero
|
||||
geom_zero \
|
||||
geom_zoned
|
||||
|
||||
.if ${MK_CCD} != "no" || defined(ALL_MODULES)
|
||||
SUBDIR+= geom_ccd
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
.PATH: ${SRCTOP}/sys/geom/zoned
|
||||
|
||||
KMOD= geom_zoned
|
||||
SRCS= g_zoned.c
|
||||
|
||||
.include <bsd.kmod.mk>
|
||||
+16
-4
@@ -45,9 +45,9 @@
|
||||
|
||||
/*
|
||||
* There are currently three types of zoned devices:
|
||||
*
|
||||
*
|
||||
* Drive Managed:
|
||||
* Drive Managed drives look and act just like a standard random access
|
||||
* Drive Managed drives look and act just like a standard random access
|
||||
* block device, but underneath, the drive reads and writes the bulk of
|
||||
* its capacity using SMR zones. Sequential writes will yield better
|
||||
* performance, but writing sequentially is not required.
|
||||
@@ -58,7 +58,7 @@
|
||||
* is not required to manage the zones on the drive, though. Sequential
|
||||
* writes will yield better performance in Sequential Write Preferred
|
||||
* zones, but the host can write randomly in those zones.
|
||||
*
|
||||
*
|
||||
* Host Managed:
|
||||
* Host Managed drives expose the underlying zone layout via SCSI or ATA
|
||||
* commands. The host is required to access the zones according to the
|
||||
@@ -130,15 +130,27 @@ struct disk_zone_rep_entry {
|
||||
#define DISK_ZONE_COND_IMPLICIT_OPEN 0x02
|
||||
#define DISK_ZONE_COND_EXPLICIT_OPEN 0x03
|
||||
#define DISK_ZONE_COND_CLOSED 0x04
|
||||
#define DISK_ZONE_COND_INACTIVE 0x05
|
||||
#define DISK_ZONE_COND_READONLY 0x0D
|
||||
#define DISK_ZONE_COND_FULL 0x0E
|
||||
#define DISK_ZONE_COND_OFFLINE 0x0F
|
||||
uint8_t zone_flags;
|
||||
#define DISK_ZONE_FLAG_RESET 0x01 /* Zone needs RWP */
|
||||
#define DISK_ZONE_FLAG_NON_SEQ 0x02 /* Zone accssessed nonseq */
|
||||
#define DISK_ZONE_FLAG_NON_SEQ 0x02 /* Zone accessed nonseq */
|
||||
uint64_t zone_length;
|
||||
uint64_t zone_start_lba;
|
||||
uint64_t write_pointer_lba;
|
||||
/*
|
||||
* Only zone conditions that have a write pointer report one
|
||||
* here; ZBC and ZAC leave the field undefined otherwise, and
|
||||
* drives disagree on what they put in it.
|
||||
*/
|
||||
#define DISK_ZONE_WP_INVALID(cond) \
|
||||
((cond) == DISK_ZONE_COND_NOT_WP || \
|
||||
(cond) == DISK_ZONE_COND_INACTIVE || \
|
||||
(cond) == DISK_ZONE_COND_READONLY || \
|
||||
(cond) == DISK_ZONE_COND_FULL || \
|
||||
(cond) == DISK_ZONE_COND_OFFLINE)
|
||||
/* XXX KDM padding space may not be a good idea inside the bio */
|
||||
uint8_t reserved[32];
|
||||
};
|
||||
|
||||
@@ -57,5 +57,6 @@
|
||||
#define EXTERR_CAT_HWPMC_AMD 20
|
||||
#define EXTERR_CAT_HWPMC_MOD 21
|
||||
#define EXTERR_CAT_TTY 22
|
||||
#define EXTERR_CAT_GEOMZONED 23
|
||||
|
||||
#endif
|
||||
@@ -20,7 +20,9 @@ TESTS_SUBDIRS+= stripe
|
||||
TESTS_SUBDIRS+= union
|
||||
TESTS_SUBDIRS+= uzip
|
||||
TESTS_SUBDIRS+= virstor
|
||||
TESTS_SUBDIRS+= zoned
|
||||
|
||||
${PACKAGE}FILES+= geom_subr.sh
|
||||
${PACKAGE}FILES+= zoned_subr.sh
|
||||
|
||||
.include <bsd.test.mk>
|
||||
@@ -0,0 +1,9 @@
|
||||
PACKAGE= tests
|
||||
|
||||
TESTSDIR= ${TESTSBASE}/sys/geom/class/${.CURDIR:T}
|
||||
|
||||
ATF_TESTS_SH+= zoned_test
|
||||
|
||||
${PACKAGE}FILES+= conf.sh
|
||||
|
||||
.include <bsd.test.mk>
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/bin/sh
|
||||
|
||||
class="zoned"
|
||||
|
||||
gzoned_test_setup()
|
||||
{
|
||||
geom_atf_test_setup
|
||||
}
|
||||
|
||||
# Print the write pointer LBA of the zone containing the given LBA.
|
||||
zone_wp()
|
||||
{
|
||||
zoned_zone_wp /dev/${md}.zoned $1
|
||||
}
|
||||
|
||||
# Print the start LBA of the given zone number.
|
||||
zone_start()
|
||||
{
|
||||
zoned_zone_start /dev/${md}.zoned $1
|
||||
}
|
||||
|
||||
# Print the number of zones matching a report option.
|
||||
zone_count()
|
||||
{
|
||||
zoned_zone_count /dev/${md}.zoned $1
|
||||
}
|
||||
|
||||
wait_dev()
|
||||
{
|
||||
for _i in $(seq 1 50); do
|
||||
[ -c "$1" ] && return 0
|
||||
sleep 0.2
|
||||
done
|
||||
atf_fail "$1 did not appear"
|
||||
}
|
||||
|
||||
wait_dev_gone()
|
||||
{
|
||||
for _i in $(seq 1 50); do
|
||||
[ -c "$1" ] || return 0
|
||||
sleep 0.2
|
||||
done
|
||||
atf_fail "$1 did not go away"
|
||||
}
|
||||
|
||||
gzoned_test_cleanup()
|
||||
{
|
||||
if [ -f "$TEST_MDS_FILE" ]; then
|
||||
while read md; do
|
||||
[ -c /dev/${md}.zoned ] && \
|
||||
gzoned destroy $md.zoned 2>/dev/null
|
||||
mdconfig -d -u $md 2>/dev/null
|
||||
done < $TEST_MDS_FILE
|
||||
fi
|
||||
true
|
||||
}
|
||||
|
||||
. `dirname $0`/../geom_subr.sh
|
||||
. `dirname $0`/../zoned_subr.sh
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Helpers for exercising GEOM classes on top of a gzoned provider. Source this
|
||||
# in addition to geom_subr.sh.
|
||||
|
||||
# Create a vnode-backed md large enough for four 256m zones. gzoned reserves
|
||||
# space at the end of the provider for its metadata block and zone table, so an
|
||||
# exact multiple of the zone size (1024m) would lose a zone.
|
||||
zoned_backing_md()
|
||||
{
|
||||
atf_check truncate -s 1025m backing_file
|
||||
attach_md md -t vnode -f backing_file
|
||||
}
|
||||
|
||||
# Set up a gzoned provider on a fresh backing md. Any argument is passed to
|
||||
# gzoned create as its conventional zone specification.
|
||||
zoned_attach_md()
|
||||
{
|
||||
local conv=$1
|
||||
|
||||
zoned_backing_md
|
||||
atf_check gzoned create -s 256m ${conv:+-c ${conv}} ${md}
|
||||
}
|
||||
|
||||
# Number of zones on $1 matching the report option $2, e.g. "nonwp".
|
||||
zoned_zone_count()
|
||||
{
|
||||
zonectl -d $1 -c rz -o $2 -P summary | awk '/zones,/ {print $1}'
|
||||
}
|
||||
|
||||
# Write pointer LBA of the zone containing LBA $2 on $1.
|
||||
zoned_zone_wp()
|
||||
{
|
||||
zonectl -d $1 -c rz -l $2 -P script | \
|
||||
awk -F',' 'NR == 1 {gsub(/ /, "", $3); print $3}'
|
||||
}
|
||||
|
||||
# Start LBA of zone number $2 on $1.
|
||||
zoned_zone_start()
|
||||
{
|
||||
zonectl -d $1 -c rz -P script | \
|
||||
awk -F',' -v n=$(($2 + 1)) 'NR == n {gsub(/ /, "", $1); print $1}'
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
.include <src.opts.mk>
|
||||
|
||||
PROG= zonectl
|
||||
SRCS= zonectl.c
|
||||
SDIR= ${SRCTOP}/sys
|
||||
LIBADD= cam sbuf util
|
||||
MAN= zonectl.8
|
||||
CFLAGS+=-g -O0
|
||||
|
||||
HAS_TESTS=
|
||||
SUBDIR.${MK_TESTS}+= tests
|
||||
|
||||
.include <bsd.prog.mk>
|
||||
@@ -0,0 +1,5 @@
|
||||
PACKAGE= tests
|
||||
|
||||
ATF_TESTS_SH= zonectl_test
|
||||
|
||||
.include <bsd.test.mk>
|
||||
@@ -0,0 +1,221 @@
|
||||
# SPDX-License-Identifier: BSD-2-Clause
|
||||
#
|
||||
# Copyright (c) 2026 voidanix <[email protected]>
|
||||
#
|
||||
|
||||
MD_DEV_FILE="md.dev"
|
||||
|
||||
alloc_zoned_md()
|
||||
{
|
||||
atf_check truncate -s 1025m backing_file
|
||||
md=$(mdconfig -a -t vnode -f backing_file) || \
|
||||
atf_fail "mdconfig -a failed"
|
||||
echo ${md} > $MD_DEV_FILE
|
||||
}
|
||||
|
||||
# Print the number of zones matching a report option.
|
||||
zone_count()
|
||||
{
|
||||
zonectl -d /dev/${md}.zoned -c rz -o $1 -P summary | \
|
||||
awk '/zones,/ {print $1}'
|
||||
}
|
||||
|
||||
zoned_md_cleanup()
|
||||
{
|
||||
if [ -f "$MD_DEV_FILE" ]; then
|
||||
read md < $MD_DEV_FILE
|
||||
[ -c /dev/${md}.zoned ] && \
|
||||
gzoned destroy ${md}.zoned 2>/dev/null
|
||||
mdconfig -d -u ${md} 2>/dev/null
|
||||
fi
|
||||
true
|
||||
}
|
||||
|
||||
atf_test_case report_filter cleanup
|
||||
report_filter_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -o only reports zones matching the filter"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
report_filter_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m -c 0-1 ${md}
|
||||
|
||||
atf_check_equal "2" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -o nonwp -P summary | \
|
||||
awk '/zones,/ {print $1}')"
|
||||
atf_check_equal "2" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -o empty -P summary | \
|
||||
awk '/zones,/ {print $1}')"
|
||||
atf_check_equal "0" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -o full -P summary | \
|
||||
awk '/zones,/ {print $1}')"
|
||||
}
|
||||
report_filter_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case report_zones cleanup
|
||||
report_zones_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c rz reports every zone"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
report_zones_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m ${md}
|
||||
atf_check -o match:"^4 zones," \
|
||||
zonectl -d /dev/${md}.zoned -c rz -P summary
|
||||
atf_check_equal "4" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -P script | \
|
||||
awk 'END {print NR}')"
|
||||
}
|
||||
report_zones_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case report_starting_lba cleanup
|
||||
report_starting_lba_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c rz -l reports zones from a starting LBA"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
report_starting_lba_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m ${md}
|
||||
# Only zones 2 and 3 lie at or past LBA 0x100000 (512m).
|
||||
atf_check_equal "2" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -l 0x100000 -P summary | \
|
||||
awk '/zones,/ {print $1}')"
|
||||
atf_check_equal "0x100000" \
|
||||
"$(zonectl -d /dev/${md}.zoned -c rz -l 0x100000 -P script | \
|
||||
awk -F',' 'NR == 1 {gsub(/ /, "", $1); print $1}')"
|
||||
}
|
||||
report_starting_lba_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case report_params cleanup
|
||||
report_params_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c params reports the device parameters"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
report_params_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m -m 3 ${md}
|
||||
atf_check -o match:"Zone Mode: Host Managed" \
|
||||
-o match:"URSWRZ.*: Yes" \
|
||||
-o match:"Open Sequential Write Required Zones: 3" \
|
||||
zonectl -d /dev/${md}.zoned -c params
|
||||
}
|
||||
report_params_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case open_zone cleanup
|
||||
open_zone_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c open explicitly opens a zone"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
open_zone_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m ${md}
|
||||
atf_check zonectl -d /dev/${md}.zoned -c open -l 0
|
||||
atf_check_equal "1" "$(zone_count exp_open)"
|
||||
}
|
||||
open_zone_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case close_zone cleanup
|
||||
close_zone_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c close closes an open zone"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
close_zone_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m ${md}
|
||||
atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1
|
||||
atf_check_equal "1" "$(zone_count imp_open)"
|
||||
atf_check zonectl -d /dev/${md}.zoned -c close -l 0
|
||||
atf_check_equal "1" "$(zone_count closed)"
|
||||
atf_check_equal "0" "$(zone_count imp_open)"
|
||||
}
|
||||
close_zone_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case finish_zone cleanup
|
||||
finish_zone_head()
|
||||
{
|
||||
atf_set "descr" "zonectl -c finish transitions a zone to full"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
finish_zone_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m ${md}
|
||||
atf_check -e ignore dd if=/dev/zero of=/dev/${md}.zoned bs=1m count=1
|
||||
atf_check zonectl -d /dev/${md}.zoned -c finish -l 0
|
||||
atf_check_equal "1" "$(zone_count full)"
|
||||
}
|
||||
finish_zone_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_test_case zone_state_handling cleanup
|
||||
zone_state_handling_head()
|
||||
{
|
||||
atf_set "descr" \
|
||||
"zone commands follow ZBC spec outside their transition states"
|
||||
atf_set "require.user" "root"
|
||||
}
|
||||
zone_state_handling_body()
|
||||
{
|
||||
alloc_zoned_md
|
||||
atf_check gzoned create -s 256m -c 0 ${md}
|
||||
|
||||
# Explicitly targeting a conventional zone is the caller's error.
|
||||
atf_check -s not-exit:0 -e ignore \
|
||||
zonectl -d /dev/${md}.zoned -c finish -l 0
|
||||
# Closing an empty zone is a no-op.
|
||||
atf_check zonectl -d /dev/${md}.zoned -c close -l 0x80000
|
||||
atf_check_equal "3" "$(zone_count empty)"
|
||||
# So is opening a full zone: it must stay full.
|
||||
atf_check zonectl -d /dev/${md}.zoned -c finish -l 0x100000
|
||||
atf_check zonectl -d /dev/${md}.zoned -c open -l 0x100000
|
||||
atf_check_equal "1" "$(zone_count full)"
|
||||
atf_check_equal "0" "$(zone_count exp_open)"
|
||||
}
|
||||
zone_state_handling_cleanup()
|
||||
{
|
||||
zoned_md_cleanup
|
||||
}
|
||||
|
||||
atf_init_test_cases()
|
||||
{
|
||||
atf_add_test_case report_filter
|
||||
atf_add_test_case report_zones
|
||||
atf_add_test_case report_starting_lba
|
||||
atf_add_test_case report_params
|
||||
atf_add_test_case open_zone
|
||||
atf_add_test_case close_zone
|
||||
atf_add_test_case finish_zone
|
||||
atf_add_test_case zone_state_handling
|
||||
}
|
||||
@@ -292,13 +292,11 @@ zonectl_print_rz(struct disk_zone_report *report, zone_output_flags out_flags,
|
||||
(uintmax_t)entry->zone_start_lba,
|
||||
field_widths[ZONE_FW_LEN],
|
||||
(uintmax_t)entry->zone_length);
|
||||
if (entry->write_pointer_lba == 0xffffffffffff ||
|
||||
entry->write_pointer_lba == 0xffffffffffffffff) {
|
||||
if (DISK_ZONE_WP_INVALID(entry->zone_condition)) {
|
||||
/*
|
||||
* This value is reported by HDDs for conventional
|
||||
* zones. It really means "N/A". Reported it as -1,
|
||||
* even though it's technically unsigned, to save
|
||||
* space.
|
||||
* The zone has no write pointer, which really means
|
||||
* "N/A". Report it as -1, even though the field is
|
||||
* technically unsigned, to save space.
|
||||
*/
|
||||
printf("%*d, ", field_widths[ZONE_FW_WP], -1);
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user