s390/dasd: Add dasd_eckd_build_cp_tpm_writefulltrack()

Add the channel program builder for WRITE_FULL_TRACK requests, used
by dasd_eckd_ese_format() (next patch) to format and write a set of
tracks atomically and avoid the format cycle on ESE devices.

The program is an ITCW with a TIDAW list. Per track it emits an eckd_r0
header, an eckd_count + data pair for every record (pad records before
and after the caller's data window use device->nulldata, records in the
window point into the bio payload), and a terminating 0xFF pseudo-count
with TIDAW_FLAGS_INSERT_CBC. The descriptors come from the per-device
fill_chunks pool so they can be freed in bulk in __dasd_cleanup_cqr().

Add inline helpers crosses_page() and reserve_nocross(), to keep each
descriptor within one page since TIDAW addressing must not cross a page
boundary.

Reviewed-by: Jan Höppner <hoeppner@linux.ibm.com>
Signed-off-by: Stefan Haberland <sth@linux.ibm.com>
Link: https://patch.msgid.link/20260805111612.1285190-12-sth@linux.ibm.com
Signed-off-by: Jens Axboe <axboe@kernel.dk>
This commit is contained in:
Stefan Haberland 2026-08-05 13:16:04 +02:00 committed by Jens Axboe
parent 44f9bf4798
commit 123ec1e9cb

View File

@ -123,6 +123,14 @@ static int prepare_itcw(struct itcw *, unsigned int, unsigned int, int,
unsigned int, unsigned int);
static int dasd_eckd_query_pprc_status(struct dasd_device *,
struct dasd_pprc_data_sc4 *);
static struct dasd_ccw_req *dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *,
struct dasd_block *,
struct request *,
sector_t, sector_t,
sector_t, sector_t,
unsigned int, unsigned int,
unsigned int, unsigned int,
struct dasd_ccw_req *);
/* initial attempt at a probe function. this can be simplified once
* the other detection code is gone */
@ -204,7 +212,7 @@ static void set_ch_t(struct ch_t *geo, __u32 cyl, __u8 head)
geo->head |= head;
}
static __maybe_unused void set_chr_t(void *addr, __u32 cyl, __u8 head, __u8 record)
static void set_chr_t(void *addr, __u32 cyl, __u8 head, __u8 record)
{
struct chr_t *geo = addr;
@ -4742,6 +4750,340 @@ static struct dasd_ccw_req *dasd_eckd_build_cp_tpm_track(
return ERR_PTR(ret);
}
static __always_inline bool crosses_page(const void *addr, size_t len)
{
return len && (offset_in_page(addr) + len > PAGE_SIZE);
}
static __always_inline void *reserve_nocross(char **p, size_t *space, size_t len)
{
size_t pad = crosses_page(*p, len) ? PAGE_SIZE - offset_in_page(*p) : 0;
void *ret;
if (*space < pad + len)
return NULL; /* out of space */
*p += pad;
*space -= pad;
ret = *p;
*p += len;
*space -= len;
return ret;
}
/*
* Helpers for dasd_eckd_build_cp_tpm_writefulltrack(): append the TIDAWs for
* one track-image element (R0 header, a count + data record, or the trailing
* pseudo track end count) to the itcw. Return the last TIDAW, or NULL on failure.
*/
static struct tidaw *add_track_r0(struct itcw *itcw, char **fill,
size_t *fillsize, u32 cyl, u16 head)
{
struct tidaw *tidaw;
struct eckd_r0 *r0;
r0 = reserve_nocross(fill, fillsize, sizeof(*r0));
if (WARN_ON_ONCE(!r0))
return NULL;
set_chr_t(r0, cyl, head, 0);
r0->count.dl = 8;
tidaw = itcw_add_tidaw(itcw, 0, r0, sizeof(*r0));
return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
}
static struct tidaw *add_track_record(struct itcw *itcw, char **fill,
size_t *fillsize, u32 cyl, u16 head,
u8 rec, void *data, u32 dl)
{
struct eckd_count *count;
struct tidaw *tidaw;
count = reserve_nocross(fill, fillsize, sizeof(*count));
if (WARN_ON_ONCE(!count))
return NULL;
set_chr_t(count, cyl, head, rec);
count->dl = dl;
tidaw = itcw_add_tidaw(itcw, 0, count, sizeof(*count));
if (IS_ERR_OR_NULL(tidaw))
return NULL;
tidaw = itcw_add_tidaw(itcw, 0, data, dl);
return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
}
static struct tidaw *add_track_end(struct itcw *itcw, char **fill,
size_t *fillsize)
{
struct eckd_count *count;
struct tidaw *tidaw;
count = reserve_nocross(fill, fillsize, sizeof(*count));
if (WARN_ON_ONCE(!count))
return NULL;
count->cyl = 0xffff;
count->head = 0xffff;
count->dl = 0xffff;
count->record = 0xff;
count->kl = 0xff;
tidaw = itcw_add_tidaw(itcw, TIDAW_FLAGS_INSERT_CBC, count, sizeof(*count));
return IS_ERR_OR_NULL(tidaw) ? NULL : tidaw;
}
static __maybe_unused struct dasd_ccw_req *
dasd_eckd_build_cp_tpm_writefulltrack(struct dasd_device *startdev,
struct dasd_block *block,
struct request *req,
sector_t first_rec,
sector_t last_rec,
sector_t first_trk,
sector_t last_trk,
unsigned int first_offs,
unsigned int last_offs,
unsigned int blk_per_trk,
unsigned int blksize,
struct dasd_ccw_req *ocqr)
{
struct dasd_eckd_private *private = block->base->private;
unsigned int seg_len, part_len, len_to_track_end;
unsigned int count, count_to_trk_end, offs;
unsigned int trkcount, ctidaw, tlf;
int itcw_op, rec_count, datasize;
struct tidaw *last_tidaw = NULL;
sector_t recid, trkid, curr_trk;
unsigned char cmd, new_track;
struct dasd_device *basedev;
size_t itcw_size, fillsize;
struct dasd_ccw_req *cqr;
struct req_iterator iter;
char *dst, *filldata;
unsigned long flags;
struct itcw *itcw;
struct bio_vec bv;
int ret = -EINVAL;
void *nullrecord;
u16 heads, head;
u32 cyl;
u8 rec;
basedev = block->base;
cmd = DASD_ECKD_CCW_WRITE_FULL_TRACK;
itcw_op = ITCW_OP_WRITE;
/*
* trackbased I/O needs address all memory via TIDAWs,
* not just for 64 bit addresses. This allows us to map
* each segment directly to one tidaw.
* In the case of write requests, additional tidaws may
* be needed when a segment crosses a track boundary.
* Per track we emit one R0 tidaw, two tidaws per record (count field
* plus data - a record never crosses a track or page boundary, as
* part_len is clamped to both blksize and the track end), and one track
* end tidaw: 2 * blk_per_trk + 2.
* Round the +2 up to blk_per_trk-independent headroom via 2 * (blk_per_trk + 2).
*/
trkcount = last_trk - first_trk + 1;
ctidaw = trkcount * 2 * (blk_per_trk + 2);
/*
* build_cp (ocqr == NULL): the request owns its CCW program - block in
* the pdu, ITCW in ccw_chunks. ese_format (ocqr != NULL): the failing
* origin still owns its pdu, so take the replacement from ese_chunks.
*/
itcw_size = itcw_calc_size(0, ctidaw, 0);
if (ocqr)
cqr = dasd_fmalloc_request(DASD_ECKD_MAGIC, 0, itcw_size, startdev);
else
cqr = dasd_smalloc_request(DASD_ECKD_MAGIC, 0, itcw_size, startdev,
blk_mq_rq_to_pdu(req));
if (IS_ERR(cqr))
return cqr;
fillsize = trkcount * (sizeof(struct eckd_r0) +
(sizeof(struct eckd_count) * (blk_per_trk + 2)));
/*
* reserve_nocross() pads elements away from page boundaries and draws
* that padding from fillsize; budget one element per page the buffer
* may span so it never runs short.
*/
fillsize += (fillsize / PAGE_SIZE + 1) * sizeof(struct eckd_r0);
spin_lock_irqsave(&startdev->mem_lock, flags);
filldata = dasd_alloc_chunk(&startdev->fill_chunks, fillsize);
spin_unlock_irqrestore(&startdev->mem_lock, flags);
if (!filldata) {
ret = -ENOMEM;
goto out_error;
}
memset(filldata, 0, fillsize);
cqr->filldata = filldata;
nullrecord = startdev->nulldata;
/* count + data for each record, plus r0 and the pseudo count */
tlf = blk_per_trk * (blksize + sizeof(struct eckd_count));
tlf += sizeof(struct eckd_r0) + sizeof(struct eckd_count);
itcw = itcw_init(cqr->data, itcw_size, itcw_op, 0, ctidaw, 0);
if (IS_ERR(itcw)) {
ret = -EINVAL;
goto out_error;
}
cqr->cpaddr = itcw_get_tcw(itcw);
datasize = trkcount * tlf;
if (prepare_itcw(itcw, first_trk, last_trk,
cmd, basedev, startdev,
0,
trkcount, blksize,
datasize,
tlf,
blk_per_trk) == -EAGAIN) {
/* Clock not in sync and XRC is enabled.
* Try again later.
*/
ret = -EAGAIN;
goto out_error;
}
heads = private->rdc_data.trk_per_cyl;
/*
* A tidaw can address 4k of memory, but must not cross page boundaries
* We can let the block layer handle this by setting seg_boundary_mask
* to page boundaries and max_segment_size to page size when setting up
* the request queue.
*/
curr_trk = first_trk;
recid = first_rec;
trkid = recid;
offs = sector_div(trkid, blk_per_trk);
count = blk_per_trk;
len_to_track_end = count * blksize;
recid += count - first_offs;
new_track = 0;
/* the R0 header of the first track */
cyl = curr_trk / heads;
head = curr_trk % heads;
last_tidaw = add_track_r0(itcw, &filldata, &fillsize, cyl, head);
if (!last_tidaw)
goto out_error;
/* empty records before the first data record */
for (int i = 1; i <= first_offs; i++) {
len_to_track_end -= blksize;
last_tidaw = add_track_record(itcw, &filldata, &fillsize,
cyl, head, i, nullrecord, blksize);
if (!last_tidaw)
goto out_error;
}
/* process data records */
rec = first_offs + 1;
rec_count = 0;
rq_for_each_segment(bv, req, iter) {
dst = bvec_virt(&bv);
seg_len = bv.bv_len;
while (seg_len) {
if (new_track) {
trkid = recid;
offs = sector_div(trkid, blk_per_trk);
count_to_trk_end = blk_per_trk - offs;
count = min((last_rec - recid + 1),
(sector_t)count_to_trk_end);
/*
* Size to the physical track end: a short last
* track is padded in out_skip, so the track-end
* marker must not be emitted early here.
*/
len_to_track_end = count_to_trk_end * blksize;
recid += count;
new_track = 0;
/* the R0 header of the next track */
cyl = curr_trk / heads;
head = curr_trk % heads;
last_tidaw = add_track_r0(itcw, &filldata,
&fillsize, cyl, head);
if (!last_tidaw)
goto out_error;
rec = 1;
}
/*
* One count + data record per block: a bvec segment can
* be up to a page, so clamp to blksize - otherwise the
* count field would describe one oversized record instead
* of several blksize ones for sub-page block sizes.
*/
part_len = min(seg_len, len_to_track_end);
part_len = min(part_len, blksize);
seg_len -= part_len;
len_to_track_end -= part_len;
/*
* This block ends the track; the next one starts a new
* track. The track-end marker emitted below carries the
* CBC flag.
*/
if (!len_to_track_end)
new_track = 1;
last_tidaw = add_track_record(itcw, &filldata, &fillsize,
cyl, head, rec, dst, part_len);
if (!last_tidaw)
goto out_error;
if (new_track) {
/* add track end marker */
last_tidaw = add_track_end(itcw, &filldata,
&fillsize);
if (!last_tidaw)
goto out_error;
curr_trk++;
}
rec++;
dst += part_len;
rec_count++;
if (rec_count >= (last_rec - first_rec + 1))
goto out_skip;
}
}
out_skip:
new_track = 0;
/* empty records after the last data record */
for (int i = last_offs + 2; i <= blk_per_trk; i++) {
len_to_track_end -= blksize;
last_tidaw = add_track_record(itcw, &filldata, &fillsize,
cyl, head, i, nullrecord, blksize);
if (!last_tidaw)
goto out_error;
new_track = 1;
}
/* add track end marker */
if (new_track) {
last_tidaw = add_track_end(itcw, &filldata, &fillsize);
if (!last_tidaw)
goto out_error;
}
last_tidaw->flags |= TIDAW_FLAGS_LAST;
last_tidaw->flags &= ~TIDAW_FLAGS_INSERT_CBC;
itcw_finalize(itcw);
if (blk_noretry_request(req) ||
block->base->features & DASD_FEATURE_FAILFAST)
set_bit(DASD_CQR_FLAGS_FAILFAST, &cqr->flags);
cqr->cpmode = 1;
cqr->startdev = startdev;
cqr->memdev = startdev;
cqr->block = block;
cqr->expires = startdev->default_expires * HZ; /* default 5 minutes */
cqr->lpm = dasd_path_get_ppm(startdev);
cqr->retries = startdev->default_retries;
cqr->buildclk = get_tod_clock();
cqr->status = DASD_CQR_FILLED;
return cqr;
out_error:
/* dasd_sfree_request frees from the right pool via cqr->mem_chunk */
dasd_sfree_request(cqr, startdev);
return ERR_PTR(ret);
}
static struct dasd_ccw_req *dasd_eckd_build_cp(struct dasd_device *startdev,
struct dasd_block *block,
struct request *req)