この記事はLinux Advent Calendar 2019の7日目です。
ううう、レベル低くて申し訳ないですが、これが自分の実力ということで!!
はじめに
簡単な自己紹介
普段は、某家電メーカーで、色々低レベルなプログラムを書いています。
お仕事は、OSなしから、Linuxまででございますね…。
ちょっと気になったのでNVDIMMの実装について、簡単にまとめておこうと思いました。
そもそもNVDIMMとは?
Non-volatile DIMM、つまり「不揮発なDIMM」ですね。
NVDIMM」のすゝめ が非常にわかりやすいです!
Linux Kernelでは、どんな機能を提供しているのか?
@YasunoriGoto1 さんがNVDIMMに関して非常に有責な情報をまとめております。
(書き始める前に見つけたら、この記事別のネタになっていましたね…)
このページでは何を説明するのか?
今回はこのコードあたりを「なんか楽しそう」なところだけつまみ食いして読んでいきます。
ブロックデバイスとして扱おう、の巻ですね。
今回のまとめ
linux kernelのNVDIMM実装は、root/drivers/nvdimm だけでなく、drivers/acpi/nfit とかにも散らばっているから、
「ちょっと読んだだけで、わかるかも!」という淡い期待は捨てよう。
と、いうことで、なまあたたかーい目で見ていただければ幸いでございます。
#本編。
root/drivers/nvdimm の構成
最初に、Makefileに基づいて、構造を分類するとこんな感じになります。
ブロックデバイスとしてアクセスしよう!
ブロックデバイスとしてLinux kernelが認識する手順は以下の通り。
module_init(nd_blk_init)
⇒ nd_driver_register(&nd_blk_driver);
⇒ nd_blk_driver = { .probe = nd_blk_probe, ... }
⇒ nsblk_attach_disk(nsblk)
static int nsblk_attach_disk(struct nd_namespace_blk *nsblk)
struct request_queue *q;
q = blk_alloc_queue(GFP_KERNEL);
if (devm_add_action_or_reset(dev, nd_blk_release_queue, q))
return -ENOMEN;
blk_queue_make_request(q, nd_blk_make_request); // (1)
blk_queue_max_hw_sectors(q, UINT_MAX); // (2)
blk_queue_logical_block_size(q, nsblk_sector_size(nsblk));
q->queuedata = nsblk;
set_capacity(disk, available_disk_size >> SECTOR_SHIFT);
device_add_disk(dev, disk, NULL);
(1)nd_blk_make_request()
static blk_qc_t nd_blk_make_request(struct request_queue *q, struct bio *bio) の中身はこんな感じ
do_acct = nd_iostat_start(bio, &start);
bio_for_each_segment(bvec, bio, iter) {
err = nsblk_do_bvec(nsblk, bip, bvec.bv_page, len,
bvec.bv_offset, rw, iter.bi_sector);
if (do_acct)
nd_iostat_end(bio, start);
nd_iostat_start() と、nd_iostat_end() の中身は特に新しいことはやっていないので ( https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/drivers/nvdimm/nd.h?h=linux-5.3.y ) 説明省略し…。
ポイントはどう見てもnsblk_do_bvec()ですね!
static int nsblk_do_bvec(struct nd_namespace_blk *nsblk,
struct bio_integrity_payload *bip, struct page *page,
unsigned int len, unsigned int off, int rw, sector_t sector)
{
while (len) {
unsigned int cur_len;
iobuf = kmap_atomic(page);
err = ndbr->do_io(ndbr, dev_offset, iobuf + off, cur_len, rw);
kunmap_atomic(iobuf);
if (bip) {
err = nd_blk_rw_integrity(nsblk, bip, lba, rw);
if (err)
return err;
}
len -= cur_len;
off += cur_len;
sector += sector_size >> SECTOR_SHIFT;
}
return err;
}
この、do_io はどこから?
nd_region_create()
⇒ to_blk_region_desc(ndr_desc);
⇒ ndbr->do_io = ndbr_desc->do_io;
ndbr_desc->do_ioの実装を探すと…。
drivers/acpi/nfit/core.c の中に繋がってる…
そこを手繰ると、ここでようやく、blkに対するWrite/Readコマンドがメモリコピーに置き換わりました!
これで、ブロックデバイスとしてアクセスする道筋が通りました。
acpi_desc->blk_do_io = acpi_nfit_blk_region_do_io;
ndbr_desc->enable = acpi_nfit_blk_region_enable;
ndbr_desc->do_io = acpi_desc->blk_do_io;
static int acpi_nfit_blk_region_do_io(struct nd_blk_region *ndbr,
resource_size_t dpa, void *iobuf, u64 len, int rw)
{
lane = nd_region_acquire_lane(nd_region);
while (len) {
u64 c = min(len, mmio->size);
rc = acpi_nfit_blk_single_io(nfit_blk, dpa + copied,
iobuf + copied, c, rw, lane);
if (rc)
break;
copied += c;
len -= c;
}
nd_region_release_lane(nd_region, lane);
return rc;
}
static int acpi_nfit_blk_single_io(struct nfit_blk *nfit_blk,
resource_size_t dpa, void *iobuf, size_t len, int rw,
unsigned int lane)
{
write_blk_ctl(nfit_blk, lane, dpa, len, rw);
while (len) {
<略>
if (rw)
memcpy_flushcache(mmio->addr.aperture + offset, iobuf + copied, c);
else {
if (nfit_blk->dimm_flags & NFIT_BLK_READ_FLUSH)
arch_invalidate_pmem((void __force *)
mmio->addr.aperture + offset, c);
memcpy(iobuf + copied, mmio->addr.aperture + offset, c);
}
copied += c;
len -= c;
}
if (rw)
nvdimm_flush(nfit_blk->nd_region, NULL);
rc = read_blk_stat(nfit_blk, lane) ? -EIO : 0;
return rc;
}
(2)バルクのパラメータは?
blk_queue_max_hw_sectors(q, UINT_MAX);
とあるが、これはNVDIMMが特に管理単位がないから、セクタ数上限はなしよ・・・という扱いにしているのかな?と思われる。
blk_queue_logical_block_size(q, nsblk_sector_size(nsblk));
は、容量をゲットしてサイズセットしているはずだけど…
static u32 nsblk_sector_size(struct nd_namespace_blk *nsblk)
{
return nsblk->lbasize - nsblk_meta_size(nsblk);
}
nsblk->lbasize = __le64_to_cpu(nd_label->lbasize);
nd_label->lbasize = __cpu_to_le64(nspm->lbasize);
nspm->lbasize = __le64_to_cpu(label0->lbasize);
??? よくわからなかった… パース!
ところで、NVDIMMはブロックデバイスとして扱えるの?
あれ?block_device_operationsが…
static const struct block_device_operations nd_blk_fops = {
.owner = THIS_MODULE,
.revalidate_disk = nvdimm_revalidate_disk,
};
これだとアクセスできない?ということで、もうちょっと読んでみる。
どこからアクセスできそうかな?
static int nd_blk_probe(struct device *dev)
{
struct nd_namespace_common *ndns;
struct nd_namespace_blk *nsblk;
ndns = nvdimm_namespace_common_probe(dev);
if (IS_ERR(ndns))
return PTR_ERR(ndns);
nsblk = to_nd_namespace_blk(&ndns->dev);
nsblk->size = nvdimm_namespace_capacity(ndns);
dev_set_drvdata(dev, nsblk);
ndns->rw_bytes = nsblk_rw_bytes;
if (is_nd_btt(dev))
return nvdimm_namespace_attach_btt(ndns);
else if (nd_btt_probe(dev, ndns) == 0) {
/* we'll come back as btt-blk */
return -ENXIO;
} else
return nsblk_attach_disk(nsblk);
}
fops->rw_pageが生えた!
int nvdimm_namespace_attach_btt(struct nd_namespace_common *ndns)
{
struct nd_btt *nd_btt = to_nd_btt(ndns->claim);
struct nd_region *nd_region;
struct btt_sb *btt_sb;
struct btt *btt;
size_t rawsize;
<略>
nd_region = to_nd_region(nd_btt->dev.parent);
btt = btt_init(nd_btt, rawsize, nd_btt->lbasize, nd_btt->uuid,
nd_region);
if (!btt)
return -ENOMEM;
nd_btt->btt = btt;
return 0;
}
static struct btt *btt_init(struct nd_btt *nd_btt, unsigned long long rawsize,
u32 lbasize, u8 *uuid, struct nd_region *nd_region)
{
<略>
ret = btt_blk_init(btt);
if (ret) {
dev_err(dev, "init: error in blk_init: %d\n", ret);
return NULL;
}
btt_debugfs_init(btt);
return btt;
}
static const struct block_device_operations btt_fops = {
.owner = THIS_MODULE,
.rw_page = btt_rw_page,
.getgeo = btt_getgeo,
.revalidate_disk = nvdimm_revalidate_disk,
};
static int btt_blk_init(struct btt *btt)
{
struct nd_btt *nd_btt = btt->nd_btt;
struct nd_namespace_common *ndns = nd_btt->ndns;
<略>
nvdimm_namespace_disk_name(ndns, btt->btt_disk->disk_name);
btt->btt_disk->first_minor = 0;
btt->btt_disk->fops = &btt_fops;
btt->btt_disk->private_data = btt;
btt->btt_disk->queue = btt->btt_queue;
btt->btt_disk->flags = GENHD_FL_EXT_DEVT;
btt->btt_disk->queue->backing_dev_info->capabilities |=
BDI_CAP_SYNCHRONOUS_IO;
<略>
}
fops->rw_pageから、さっきのnsblk_rw_bytesに繋がった!
static int btt_rw_page(struct block_device *bdev, sector_t sector,
struct page *page, unsigned int op)
{
struct btt *btt = bdev->bd_disk->private_data;
int rc;
unsigned int len;
len = hpage_nr_pages(page) * PAGE_SIZE;
rc = btt_do_bvec(btt, NULL, page, len, 0, op, sector);
if (rc == 0)
page_endio(page, op_is_write(op), 0);
return rc;
}
static int btt_do_bvec(struct btt *btt, struct bio_integrity_payload *bip,
struct page *page, unsigned int len, unsigned int off,
unsigned int op, sector_t sector)
{
int ret;
if (!op_is_write(op)) {
ret = btt_read_pg(btt, bip, page, off, sector, len);
flush_dcache_page(page);
} else {
flush_dcache_page(page);
ret = btt_write_pg(btt, bip, sector, page, off, len);
}
return ret;
}
static int btt_read_pg(struct btt *btt, struct bio_integrity_payload *bip,
struct page *page, unsigned int off, sector_t sector,
unsigned int len)
{
int ret = 0;
int t_flag, e_flag;
struct arena_info *arena = NULL;
u32 lane = 0, premap, postmap;
while (len) {
<略>
ret = btt_map_read(arena, premap, &postmap, &t_flag, &e_flag,
NVDIMM_IO_ATOMIC);
if (ret)
goto out_lane;
<略>
}
static int btt_map_read(struct arena_info *arena, u32 lba, u32 *mapping,
int *trim, int *error, unsigned long rwb_flags)
{
<略>
ret = arena_read_bytes(arena, ns_off, &in, MAP_ENT_SIZE, rwb_flags);
<略>
}
static int arena_read_bytes(struct arena_info *arena, resource_size_t offset,
void *buf, size_t n, unsigned long flags)
{
struct nd_btt *nd_btt = arena->nd_btt;
struct nd_namespace_common *ndns = nd_btt->ndns;
/* arena offsets may be shifted from the base of the device */
offset = adjust_initial_offset(nd_btt, offset);
return nvdimm_read_bytes(ndns, offset, buf, n, flags);
}
/**
* nvdimm_read_bytes() - synchronously read bytes from an nvdimm namespace
* @ndns: device to read
* @offset: namespace-relative starting offset
* @buf: buffer to fill
* @size: transfer length
*
* @buf is up-to-date upon return from this routine.
*/
static inline int nvdimm_read_bytes(struct nd_namespace_common *ndns,
resource_size_t offset, void *buf, size_t size,
unsigned long flags)
{
return ndns->rw_bytes(ndns, offset, buf, size, READ, flags);
}
ndns->rw_bytes = nsblk_rw_bytes;
なので…
static int nsblk_rw_bytes(struct nd_namespace_common *ndns,
resource_size_t offset, void *iobuf, size_t n, int rw,
unsigned long flags)
{
struct nd_namespace_blk *nsblk = to_nd_namespace_blk(&ndns->dev);
struct nd_blk_region *ndbr = to_ndbr(nsblk);
resource_size_t dev_offset;
dev_offset = to_dev_offset(nsblk, offset, n);
if (unlikely(offset + n > nsblk->size)) {
dev_WARN_ONCE(&ndns->dev, 1, "request out of range\n");
return -EFAULT;
}
if (dev_offset == SIZE_MAX)
return -EIO;
return ndbr->do_io(ndbr, dev_offset, iobuf, n, rw);
}
あとはさっきの説明の、メモリアクセスまでのパスと同じ。
まとめ
linux kernelのNVDIMM実装は、root/drivers/nvdimm だけでなく、drivers/acpi/nfit とかにも散らばっているから、
「ちょっと読んだだけで、わかるかも!」という淡い期待は捨てよう、です。