Skip to content

Commit 6a5c498

Browse files
committed
femu/nand: a read suspends a program or erase, not another read
The busy-until timeline says when an array is free, not what it is busy with, so a read that found its LUN busy always preempted -- including a LUN busy with another read. Two reads then held one array at once, and a second read inside a suspension paid the overhead again and pushed the suspended program back a second time. Keep, per array position, when the latest program or erase and the latest read end. A read preempts only while a program or erase still owns the array; one that finds a suspension already open joins it behind the reads in it with no second overhead, and one that finds only reads queues behind them. The state exists only with suspend on, and the lock-free LUN gate is taken exactly as before when it is off.
1 parent fcfa0d5 commit 6a5c498

3 files changed

Lines changed: 191 additions & 40 deletions

File tree

hw/femu/nand/nand-media.c

Lines changed: 114 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,16 @@ void nand_media_init(NandMedia *m, const NandMediaConfig *cfg)
1818
{
1919
m->cfg = *cfg;
2020
m->bus_res = NULL;
21+
m->susp = NULL;
2122
if (cfg->policy.channel_mode == NAND_CH_STAGED && cfg->nchs) {
2223
m->bus_res = calloc(cfg->nchs, sizeof(*m->bus_res));
2324
}
25+
/* suspend needs the geometry to keep its per-position state */
26+
if (cfg->policy.pe_suspend &&
27+
cfg->nchs && cfg->luns_per_ch && cfg->planes_per_lun) {
28+
m->susp = calloc((size_t)cfg->nchs * cfg->luns_per_ch *
29+
cfg->planes_per_lun, sizeof(*m->susp));
30+
}
2431
}
2532

2633
/*
@@ -37,6 +44,8 @@ void nand_media_destroy(NandMedia *m)
3744
}
3845
free(m->bus_res);
3946
m->bus_res = NULL;
47+
free(m->susp);
48+
m->susp = NULL;
4049
}
4150

4251
static inline uint64_t mx(uint64_t a, uint64_t b) { return a > b ? a : b; }
@@ -247,6 +256,76 @@ static void array_commit(NandMedia *m, const NandLoc *loc, uint64_t done)
247256
}
248257
}
249258

259+
/*
260+
* The suspend state for an array position: the plane under a plane-only gate,
261+
* otherwise the LUN, since a LUN gate makes the whole LUN one position.
262+
*/
263+
static NandSuspendState *suspend_state(NandMedia *m, const NandLoc *loc)
264+
{
265+
const NandMediaConfig *c = &m->cfg;
266+
uint64_t idx;
267+
268+
if (!m->susp || loc->ch >= c->nchs || loc->lun >= c->luns_per_ch ||
269+
loc->pl >= c->planes_per_lun) {
270+
return NULL;
271+
}
272+
idx = (uint64_t)loc->ch * c->luns_per_ch + loc->lun;
273+
if (c->policy.array_gate == NAND_GATE_PLANE_ONLY) {
274+
idx = idx * c->planes_per_lun + loc->pl;
275+
}
276+
277+
return &m->susp[idx];
278+
}
279+
280+
/* record what an operation that went through the ordinary gate occupies */
281+
static void suspend_note(NandMedia *m, const NandLoc *loc, NandMediaOp op,
282+
uint64_t done)
283+
{
284+
NandSuspendState *st = suspend_state(m, loc);
285+
286+
if (!st) {
287+
return;
288+
}
289+
if (op == NAND_MEDIA_READ) {
290+
st->rd_end = mx(st->rd_end, done);
291+
} else {
292+
st->pe_end = mx(st->pe_end, done);
293+
}
294+
}
295+
296+
/*
297+
* Where a read starts under program/erase suspend, given the time the array is
298+
* busy until. Only a program or erase is suspended: a read that finds the array
299+
* busy with other reads queues behind them, and one that finds a suspension
300+
* already open joins it behind the reads in it without paying the overhead a
301+
* second time. Returns false to leave the read to the ordinary gate; otherwise
302+
* sets *start, and *shift, how far the suspended work slides.
303+
*
304+
* The state is read and written without a lock, so the caller must serialize
305+
* the operations on a position -- bbssd and ZNS run them on one FTL thread.
306+
*/
307+
static bool suspend_read_start(NandMedia *m, const NandLoc *loc, uint64_t t,
308+
uint64_t busy_until, uint64_t alat,
309+
uint64_t *start, uint64_t *shift)
310+
{
311+
NandSuspendState *st = suspend_state(m, loc);
312+
313+
if (!st || t >= busy_until || t >= st->pe_end) {
314+
return false;
315+
}
316+
if (st->rd_end > t) {
317+
*start = st->rd_end;
318+
*shift = alat;
319+
} else {
320+
*start = t + m->cfg.timing.tsusp_ns;
321+
*shift = alat + m->cfg.timing.tsusp_ns;
322+
}
323+
st->pe_end += *shift;
324+
st->rd_end = *start + alat;
325+
326+
return true;
327+
}
328+
250329
NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
251330
NandMediaOp op, uint64_t stime)
252331
{
@@ -272,36 +351,23 @@ NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
272351
* mutex. Falls through to the locked path for LUN_AND_PLANE, which is
273352
* two words and not atomically CAS-able.
274353
*/
275-
if (m->cfg.policy.array_gate == NAND_GATE_LUN_ONLY) {
354+
/*
355+
* Suspend keeps state beside the timeline that one CAS cannot update
356+
* with it, so a device with suspend on takes the general path below.
357+
*/
358+
if (m->cfg.policy.array_gate == NAND_GATE_LUN_ONLY && !m->susp) {
276359
uint64_t *lun = m->cfg.timeline->lun_avail(m->cfg.timeline_opaque, loc);
277360
uint64_t old = __atomic_load_n(lun, __ATOMIC_RELAXED);
278-
bool susp = m->cfg.policy.pe_suspend && op == NAND_MEDIA_READ;
279361
for (;;) {
280-
uint64_t s, avail;
281-
if (susp && t < old) {
282-
/*
283-
* Program/erase suspend on the plain LUN gate: the read
284-
* starts after the suspend overhead instead of waiting out
285-
* the busy LUN, and whatever the LUN was doing resumes
286-
* after the read, so its completion slides by the read's
287-
* array time plus the overhead. Same arithmetic as the
288-
* staged path below.
289-
*/
290-
s = t + m->cfg.timing.tsusp_ns;
291-
done = s + alat;
292-
avail = old + alat + m->cfg.timing.tsusp_ns;
293-
} else {
294-
s = (t > old) ? t : old;
295-
done = s + alat;
296-
avail = done;
297-
}
362+
uint64_t s = (t > old) ? t : old;
363+
done = s + alat;
298364
/*
299365
* x86-64: inlines to `lock cmpxchg` (no libatomic call). On
300366
* success `old` is unchanged and we stop; on failure the
301367
* builtin writes the observed value back into `old` and we
302368
* retry with it (recomputing done from what we actually saw).
303369
*/
304-
if (__atomic_compare_exchange_n(lun, &old, avail, false,
370+
if (__atomic_compare_exchange_n(lun, &old, done, false,
305371
__ATOMIC_ACQ_REL,
306372
__ATOMIC_RELAXED)) {
307373
break;
@@ -316,19 +382,20 @@ NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
316382
}
317383
{
318384
uint64_t s = array_gate_start(m, loc, t);
319-
if (m->cfg.policy.pe_suspend && op == NAND_MEDIA_READ && s > t) {
385+
uint64_t rs, shift;
386+
387+
if (op == NAND_MEDIA_READ &&
388+
suspend_read_start(m, loc, t, s, alat, &rs, &shift)) {
320389
/*
321-
* Suspend on the plane / lun+plane gate (ZNS, OCSSD): the
322-
* read goes first, and every gated timeline that was busy
323-
* resumes its work after the read.
390+
* The read goes ahead of the program or erase, and every gated
391+
* timeline that was busy resumes its work after it.
324392
*/
325-
uint64_t shift = alat + m->cfg.timing.tsusp_ns;
326-
s = t + m->cfg.timing.tsusp_ns;
327-
done = s + alat;
393+
done = rs + alat;
328394
array_suspend_extend(m, loc, t, shift);
329395
} else {
330396
done = s + alat;
331397
array_commit(m, loc, done);
398+
suspend_note(m, loc, op, done);
332399
}
333400
}
334401
if (m->cfg.timeline->unlock_lun) {
@@ -351,19 +418,19 @@ NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
351418
if (op == NAND_MEDIA_READ) {
352419
t = bus_now(m, loc->ch, stime, t, m->cfg.timing.cmd_addr_ns);
353420
/*
354-
* Program/erase suspend: if the LUN is mid-P/E when this read arrives, real NAND
355-
* lets the read preempt the slow operation. Model it by starting the read after a
356-
* small suspend overhead (tsusp) instead of waiting out the whole P/E, then pushing
357-
* the suspended op's completion back by the read's array occupancy (it resumes after
358-
* the read). Default off (pe_suspend=false) => identical to the plain gate.
421+
* Program/erase suspend: a read that finds its array mid-program or
422+
* mid-erase starts after a small suspend overhead instead of waiting
423+
* the operation out, and the suspended operation resumes after it.
424+
* Default off (pe_suspend=false) => identical to the plain gate.
359425
*/
360-
if (m->cfg.policy.pe_suspend) {
361-
uint64_t *lun = m->cfg.timeline->lun_avail(m->cfg.timeline_opaque, loc);
362-
if (t < *lun) {
363-
uint64_t s = t + m->cfg.timing.tsusp_ns;
364-
uint64_t done = s + alat;
365-
/* suspended P/E resumes after the read: its remaining work shifts later */
366-
*lun += (alat + m->cfg.timing.tsusp_ns);
426+
{
427+
uint64_t busy = array_gate_start(m, loc, t);
428+
uint64_t rs, shift;
429+
430+
if (suspend_read_start(m, loc, t, busy, alat, &rs, &shift)) {
431+
uint64_t done = rs + alat;
432+
433+
array_suspend_extend(m, loc, t, shift);
367434
if (m->cfg.policy.cache_read && m->cfg.timeline->page_reg_ready) {
368435
uint64_t *prr =
369436
m->cfg.timeline->page_reg_ready(m->cfg.timeline_opaque, loc);
@@ -385,6 +452,7 @@ NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
385452
uint64_t s = array_gate_start(m, loc, t);
386453
uint64_t done = s + alat;
387454
array_commit(m, loc, done);
455+
suspend_note(m, loc, op, done);
388456
if (m->cfg.policy.cache_read && m->cfg.timeline->page_reg_ready) {
389457
uint64_t rcbsy = m->cfg.timing.trcbsy_ns ? m->cfg.timing.trcbsy_ns : alat;
390458
*m->cfg.timeline->lun_avail(m->cfg.timeline_opaque, loc) = s + rcbsy;
@@ -403,12 +471,14 @@ NandOpCompletion nand_media_op(NandMedia *m, const NandLoc *loc,
403471
uint64_t s = array_gate_start(m, loc, t);
404472
uint64_t done = s + alat;
405473
array_commit(m, loc, done);
474+
suspend_note(m, loc, op, done);
406475
c.done_ns = done;
407476
} else { /* erase */
408477
t = bus_now(m, loc->ch, stime, t, m->cfg.timing.cmd_addr_ns);
409478
uint64_t s = array_gate_start(m, loc, t);
410479
uint64_t done = s + alat;
411480
array_commit(m, loc, done);
481+
suspend_note(m, loc, op, done);
412482
t = bus_later(m, loc->ch, stime, done, m->cfg.timing.status_ns);
413483
c.done_ns = t;
414484
}
@@ -469,6 +539,7 @@ NandOpCompletion nand_media_multiplane(NandMedia *m, const NandLoc *locs, int nl
469539
uint64_t done = start + alat;
470540
for (i = 0; i < nlocs; i++) {
471541
array_commit(m, &locs[i], done);
542+
suspend_note(m, &locs[i], op, done);
472543
}
473544

474545
t = done;
@@ -502,8 +573,11 @@ NandOpCompletion nand_media_copyback(NandMedia *m, const NandLoc *src,
502573
*/
503574
s = array_gate_start(m, src, stime) + array_lat(m, src, NAND_MEDIA_READ);
504575
array_commit(m, src, s);
576+
/* the on-chip read is part of the program, so it is suspended with it */
577+
suspend_note(m, src, NAND_MEDIA_PROGRAM, s);
505578
s = array_gate_start(m, dst, s) + array_lat(m, dst, NAND_MEDIA_PROGRAM);
506579
array_commit(m, dst, s);
580+
suspend_note(m, dst, NAND_MEDIA_PROGRAM, s);
507581

508582
c.done_ns = s;
509583
c.latency_ns = 0; /* GC-internal; host effect is via freed channel + LUN busy */

hw/femu/nand/nand-media.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,9 +148,21 @@ typedef struct NandBusResList {
148148
int n;
149149
} NandBusResList;
150150

151+
/*
152+
* What occupies an array position, which the busy-until timelines do not say:
153+
* when the latest program or erase there ends, and when the latest read there
154+
* ends. Program/erase suspend needs both to let a read preempt only a program
155+
* or erase. One per LUN, or per plane under a plane-only gate.
156+
*/
157+
typedef struct NandSuspendState {
158+
uint64_t pe_end;
159+
uint64_t rd_end;
160+
} NandSuspendState;
161+
151162
typedef struct NandMedia {
152163
NandMediaConfig cfg;
153164
NandBusResList *bus_res; /* nchs entries; NULL unless NAND_CH_STAGED */
165+
NandSuspendState *susp; /* NULL unless policy.pe_suspend */
154166
} NandMedia;
155167

156168
typedef struct NandOpCompletion {

hw/femu/tests/unit/test-nand-media.c

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,71 @@ static void test_pe_suspend(void)
500500
lat, (2000000 + 65000 + 5000 - 1000) + 450000);
501501
nand_media_destroy(&m);
502502

503+
/*
504+
* Only a program or erase is suspended. A read that finds the LUN busy with
505+
* another read queues behind it: two reads cannot hold one array at once.
506+
*/
507+
bb_config(&cfg);
508+
cfg.policy.pe_suspend = true;
509+
cfg.timing.tsusp_ns = 5000;
510+
nand_media_init(&m, &cfg);
511+
nand_media_op(&m, &a, NAND_MEDIA_READ, t0);
512+
lat = nand_media_op(&m, &a, NAND_MEDIA_READ, t0 + 1000).latency_ns;
513+
check("suspend on: a read queues behind a read",
514+
lat, (10000 - 1000) + 10000);
515+
nand_media_destroy(&m);
516+
517+
/*
518+
* Two reads inside one suspension: the second waits for the first and pays
519+
* no second overhead, and the program slides by both reads.
520+
*/
521+
bb_config(&cfg);
522+
cfg.policy.pe_suspend = true;
523+
cfg.timing.tsusp_ns = 5000;
524+
nand_media_init(&m, &cfg);
525+
nand_media_op(&m, &a, NAND_MEDIA_PROGRAM, t0);
526+
lat = nand_media_op(&m, &a, NAND_MEDIA_READ, t0 + 1000).latency_ns;
527+
check("first read in a suspension: tsusp + array", lat, 5000 + 10000);
528+
lat = nand_media_op(&m, &a, NAND_MEDIA_READ, t0 + 2000).latency_ns;
529+
check("second read in the same suspension: behind the first",
530+
lat, (1000 + 5000 + 10000 - 2000) + 10000);
531+
lat = nand_media_op(&m, &a, NAND_MEDIA_PROGRAM, t0 + 2000).latency_ns;
532+
check("the program slides by the overhead and both reads",
533+
lat, (40000 + 5000 + 10000 + 10000 - 2000) + 40000);
534+
nand_media_destroy(&m);
535+
536+
/*
537+
* A read that arrives after an earlier read but before a program queued
538+
* behind it goes after the read and ahead of the program.
539+
*/
540+
bb_config(&cfg);
541+
cfg.policy.pe_suspend = true;
542+
cfg.timing.tsusp_ns = 5000;
543+
nand_media_init(&m, &cfg);
544+
nand_media_op(&m, &a, NAND_MEDIA_READ, t0);
545+
nand_media_op(&m, &a, NAND_MEDIA_PROGRAM, t0 + 500);
546+
lat = nand_media_op(&m, &a, NAND_MEDIA_READ, t0 + 1000).latency_ns;
547+
check("read between a read and a queued program: after the read",
548+
lat, (10000 - 1000) + 10000);
549+
nand_media_destroy(&m);
550+
551+
/* a multi-plane erase is suspended the same way (the collector's erase) */
552+
bb_config(&cfg);
553+
cfg.planes_per_lun = 2;
554+
cfg.policy.pe_suspend = true;
555+
cfg.timing.tsusp_ns = 5000;
556+
nand_media_init(&m, &cfg);
557+
{
558+
NandLoc pls[2];
559+
560+
memset(pls, 0, sizeof(pls));
561+
pls[1].pl = 1;
562+
nand_media_multiplane(&m, pls, 2, NAND_MEDIA_ERASE, t0);
563+
lat = nand_media_op(&m, &pls[1], NAND_MEDIA_READ, t0 + 1000).latency_ns;
564+
check("read preempts a multi-plane erase", lat, 5000 + 10000);
565+
}
566+
nand_media_destroy(&m);
567+
503568
/* staged channel: the existing suspend branch, now reachable */
504569
bb_config(&cfg);
505570
cfg.policy.channel_mode = NAND_CH_STAGED;

0 commit comments

Comments
 (0)