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:
voidanix authored and Alan Somers committed 2026-09-03 12:27:00 -06:00
1 parent 22e27e307f
commit 48efb6c4eb
26 files changed
+4142 -15

No files matched your search

+2
View File
@@ -333,6 +333,8 @@
..
virstor
..
zoned
..
..
gssapi tags=package=kerberos-dev
..
+4
View File
@@ -871,6 +871,8 @@
..
virstor
..
zoned
..
..
..
kern
@@ -1323,6 +1325,8 @@
..
traceroute
..
zonectl
..
..
..
+1
View File
@@ -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 \
+1
View File
@@ -22,3 +22,4 @@ GEOM_CLASSES+= shsec
GEOM_CLASSES+= stripe
GEOM_CLASSES+= union
GEOM_CLASSES+= virstor
GEOM_CLASSES+= zoned
+5
View File
@@ -0,0 +1,5 @@
PACKAGE=geom
GEOM_CLASS= zoned
.include <bsd.lib.mk>
+16
View File
@@ -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
+289
View File
@@ -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);
}
}
+260
View File
@@ -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]
+4 -3
View File
@@ -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
+20
View File
@@ -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)
+1
View File
@@ -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
+291
View File
@@ -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_ */
+2 -1
View File
@@ -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
+6
View File
@@ -0,0 +1,6 @@
.PATH: ${SRCTOP}/sys/geom/zoned
KMOD= geom_zoned
SRCS= g_zoned.c
.include <bsd.kmod.mk>
+16 -4
View File
@@ -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];
};
+1
View File
@@ -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
+2
View File
@@ -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>
+9
View File
@@ -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>
+59
View File
@@ -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
+43
View File
@@ -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}'
}
+5 -1
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
PACKAGE= tests
ATF_TESTS_SH= zonectl_test
.include <bsd.test.mk>
+221
View File
@@ -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
}
+4 -6
View File
@@ -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 {