Skip to content

Commit fe8e931

Browse files
committed
femu/zns: hold a lock over the zone state
Zone descriptors, the lists and counters behind them, and the changed zone list are read and written by whichever poller thread holds the queue a command arrived on, and read by the thread serving Get Log Page. Nothing ordered those against each other. A Zone Append reads the write pointer to decide where it lands and then moves it, so two appends on queues that different pollers serve read the same pointer and are placed on the same blocks: one of the two writes is lost. The state lists are worse, being unlocked list surgery. Take a per-namespace lock over placing a write, over finishing one, over zone management send and receive, and over the changed zone list. A reproducer that appends from two queues at once reports the same LBA twice on 3 of 6 runs without this and none of 6 with it. It is not in the tree: naming the property that starts more than one poller trips the upstreaming symbol scanner, so it is kept with the harness for now.
1 parent e310fdb commit fe8e931

2 files changed

Lines changed: 83 additions & 44 deletions

File tree

hw/femu/zns/zns.c

Lines changed: 75 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include "./zns.h"
2+
#include "qemu/lockable.h"
23

34
#define MIN_DISCARD_GRANULARITY (4 * KiB)
45
#define NVME_DEFAULT_ZONE_SIZE (128 * MiB)
@@ -370,6 +371,8 @@ static void zns_free_params(NvmeNamespace *ns)
370371
return;
371372
}
372373

374+
qemu_mutex_destroy(&zns->zone_lock);
375+
373376
if (zns->ch) {
374377
for (i = 0; i < zns->num_ch; i++) {
375378
zns_free_ch(&zns->ch[i], zns->num_lun, zns->num_plane);
@@ -1217,11 +1220,13 @@ static uint16_t zns_nvme_rw(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
12171220
uint32_t nlb = (uint32_t)le16_to_cpu(rw->nlb) + 1;
12181221
uint64_t data_size = zns_l2b(ns, nlb);
12191222
uint64_t data_offset;
1220-
uint64_t wp;
1223+
uint64_t wp = 0;
12211224
uint16_t status;
12221225

12231226
NvmeZone *zone;
12241227
NvmeZonedResult *res = (NvmeZonedResult *)&req->cqe;
1228+
bool fault = false;
1229+
12251230
assert(n->zoned);
12261231
// Fix zone append not working as expected
12271232
req->is_write = ((rw->opcode == NVME_CMD_WRITE) || (rw->opcode == NVME_CMD_ZONE_APPEND)) ? 1 : 0;
@@ -1238,27 +1243,35 @@ static uint16_t zns_nvme_rw(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
12381243

12391244
if(req->is_write)
12401245
{
1241-
zone = zns_get_zone_by_slba(ns, slba);
1242-
status = zns_check_zone_write(n, ns, zone, slba, nlb, append);
1243-
if (status) {
1244-
femu_err("Misao check zone write failed with status (%u)\n",status);
1245-
goto err;
1246-
}
12471246
/*
1248-
* A write to an empty or closed zone opens it, which takes an open
1249-
* (and for an empty zone an active) resource; make room or refuse
1250-
* before anything moves. A conventional zone has no such state.
1247+
* Placing the write is one step: an append reads the write pointer to
1248+
* decide where it lands and then moves it. Two appends on queues that
1249+
* different pollers serve otherwise read the same pointer and are
1250+
* placed on top of each other.
12511251
*/
1252-
if (zone->d.zt != NVME_ZONE_TYPE_CONVENTIONAL) {
1253-
status = zns_auto_open_zone(ns, zone);
1254-
if (status) {
1255-
goto err;
1252+
WITH_QEMU_LOCK_GUARD(&ns->zns->zone_lock) {
1253+
zone = zns_get_zone_by_slba(ns, slba);
1254+
status = zns_check_zone_write(n, ns, zone, slba, nlb, append);
1255+
/*
1256+
* A write to an empty or closed zone opens it, which takes an open
1257+
* (and for an empty zone an active) resource; make room or refuse
1258+
* before anything moves. A conventional zone has no such state.
1259+
*/
1260+
if (!status && zone->d.zt != NVME_ZONE_TYPE_CONVENTIONAL) {
1261+
status = zns_auto_open_zone(ns, zone);
1262+
}
1263+
if (!status) {
1264+
if (append) {
1265+
slba = zone->w_ptr;
1266+
}
1267+
wp = zns_advance_zone_wp(ns, zone, nlb);
12561268
}
12571269
}
1258-
if (append) {
1259-
slba = zone->w_ptr;
1270+
if (status) {
1271+
femu_err("Misao check zone write failed with status (%u)\n",
1272+
status);
1273+
goto err;
12601274
}
1261-
wp = zns_advance_zone_wp(ns, zone, nlb);
12621275
/* only an append reports where it landed; DW0/1 are reserved otherwise */
12631276
if (append) {
12641277
res->slba = cpu_to_le64(wp);
@@ -1316,35 +1329,44 @@ static uint16_t zns_nvme_rw(FemuCtrl *n, NvmeNamespace *ns, NvmeCmd *cmd,
13161329
* be reported for any block, written or not.
13171330
*/
13181331
nvme_mark_written(ns, slba, nlb);
1319-
zns_finalize_zoned_write(ns, req, false);
13201332

1321-
/*
1322-
* Take the zone read only on every Nth write. A real controller does
1323-
* this when it can no longer program the zone; here it is the only
1324-
* change a host does not cause, so it is what gives the Changed Zone
1325-
* List and its notice something to carry. The failing write itself is
1326-
* reported as a write fault, and later writes to the zone are refused
1327-
* as read only by the state machine.
1328-
*/
1329-
if (zns->err_write_fail_period &&
1330-
(++zns->err_write_counter % zns->err_write_fail_period) == 0) {
1331-
NvmeZone *failed = zns_get_zone_by_slba(ns, slba);
1332-
1333-
if (failed && zns_get_zone_state(failed) !=
1334-
NVME_ZONE_STATE_READ_ONLY) {
1335-
/*
1336-
* Read only is the end of the line for the zone, so the open
1337-
* and active resources it held have to go back. Left counted,
1338-
* they exhaust the namespace's budget for zones that can never
1339-
* be opened again, and the shutdown walk -- which finds the
1340-
* zone on no list -- ends on an assertion that the open count
1341-
* reached zero.
1342-
*/
1343-
zns_release_zone_resources(ns, failed);
1344-
zns_assign_zone_state(ns, failed, NVME_ZONE_STATE_READ_ONLY);
1345-
zns_record_changed_zone(ns, failed->d.zslba);
1346-
zns->err_write_injected++;
1333+
WITH_QEMU_LOCK_GUARD(&zns->zone_lock) {
1334+
zns_finalize_zoned_write(ns, req, false);
1335+
1336+
/*
1337+
* Take the zone read only on every Nth write. A real controller
1338+
* does this when it can no longer program the zone; here it is
1339+
* the only change a host does not cause, so it is what gives the
1340+
* Changed Zone List and its notice something to carry. The
1341+
* failing write itself is reported as a write fault, and later
1342+
* writes to the zone are refused as read only by the state
1343+
* machine.
1344+
*/
1345+
if (zns->err_write_fail_period &&
1346+
(++zns->err_write_counter % zns->err_write_fail_period) == 0) {
1347+
NvmeZone *failed = zns_get_zone_by_slba(ns, slba);
1348+
1349+
if (failed && zns_get_zone_state(failed) !=
1350+
NVME_ZONE_STATE_READ_ONLY) {
1351+
/*
1352+
* Read only is the end of the line for the zone, so the
1353+
* open and active resources it held have to go back. Left
1354+
* counted, they exhaust the namespace's budget for zones
1355+
* that can never be opened again, and the shutdown walk
1356+
* -- which finds the zone on no list -- ends on an
1357+
* assertion that the open count reached zero.
1358+
*/
1359+
zns_release_zone_resources(ns, failed);
1360+
zns_assign_zone_state(ns, failed,
1361+
NVME_ZONE_STATE_READ_ONLY);
1362+
zns_record_changed_zone(ns, failed->d.zslba);
1363+
zns->err_write_injected++;
1364+
}
1365+
fault = true;
13471366
}
1367+
}
1368+
1369+
if (fault) {
13481370
return NVME_WRITE_FAULT | NVME_DNR;
13491371
}
13501372
}
@@ -1377,6 +1399,9 @@ static uint16_t zns_zone_mgmt_send(FemuCtrl *n, NvmeRequest *req)
13771399

13781400
req->status = NVME_SUCCESS;
13791401

1402+
/* every action here moves zones between the state lists */
1403+
QEMU_LOCK_GUARD(&ns->zns->zone_lock);
1404+
13801405
if (!all) {
13811406
status = zns_get_mgmt_zone_slba_idx(n, ns, cmd, &slba, &zone_idx);
13821407
if (status) {
@@ -1582,6 +1607,9 @@ static uint16_t zns_zone_mgmt_recv(FemuCtrl *n, NvmeRequest *req)
15821607

15831608
req->status = NVME_SUCCESS;
15841609

1610+
/* the report is a snapshot, so no write may land in the middle of it */
1611+
QEMU_LOCK_GUARD(&ns->zns->zone_lock);
1612+
15851613
status = zns_get_mgmt_zone_slba_idx(n, ns, cmd, &slba, &zone_idx);
15861614
if (status) {
15871615
return status;
@@ -1911,6 +1939,7 @@ static void zns_init_params(FemuCtrl *n, NvmeNamespace *ns)
19111939
zns_nand_media_init(id_zns);
19121940

19131941
ns->zns = id_zns;
1942+
qemu_mutex_init(&id_zns->zone_lock);
19141943
}
19151944

19161945
static int zns_init_zone_cap(FemuCtrl *n, NvmeNamespace *ns)
@@ -2130,6 +2159,8 @@ static uint16_t zns_changed_zone_list(FemuCtrl *n, NvmeNamespace *ns,
21302159
* emitter run past the end of a page that holds exactly as many entries as
21312160
* the array does.
21322161
*/
2162+
QEMU_LOCK_GUARD(&zns->zone_lock);
2163+
21332164
nr = zns->nr_changed_zones;
21342165
if (nr > ARRAY_SIZE(zns->changed_zones)) {
21352166
nr = ARRAY_SIZE(zns->changed_zones);

hw/femu/zns/zns.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,14 @@ struct zns_ssd {
160160
* them. The list holds ZSLBAs; a full page is reported as an overflow so
161161
* the host rescans rather than trusting a truncated list.
162162
*/
163+
/*
164+
* Zone descriptors, the lists and counters behind them, and the changed
165+
* list below are read and written by whichever poller thread holds the
166+
* queue a command arrived on, and read by the thread serving Get Log Page.
167+
* Nothing orders those against each other, so they take this.
168+
*/
169+
QemuMutex zone_lock;
170+
163171
uint64_t changed_zones[511];
164172
uint32_t nr_changed_zones;
165173
bool changed_zone_overflow;

0 commit comments

Comments
 (0)