10
4

More than 3 years have passed since last update.

root/drivers/nvdimm を読んでみよう

Last updated at Posted at 2019-12-06

この記事はLinux Advent Calendar 2019の7日目です。

ううう、レベル低くて申し訳ないですが、これが自分の実力ということで!!

はじめに

簡単な自己紹介

普段は、某家電メーカーで、色々低レベルなプログラムを書いています。
お仕事は、OSなしから、Linuxまででございますね…。

ちょっと気になったのでNVDIMMの実装について、簡単にまとめておこうと思いました。

そもそもNVDIMMとは?

Non-volatile DIMM、つまり「不揮発なDIMM」ですね。

NVDIMM」のすゝめ が非常にわかりやすいです!

Linux Kernelでは、どんな機能を提供しているのか?

@YasunoriGoto1 さんがNVDIMMに関して非常に有責な情報をまとめております。
(書き始める前に見つけたら、この記事別のネタになっていましたね…)

不揮発メモリ(NVDIMM)とLinuxの対応動向について

このページでは何を説明するのか?

今回はこのコードあたりを「なんか楽しそう」なところだけつまみ食いして読んでいきます。
ブロックデバイスとして扱おう、の巻ですね。

今回のまとめ

linux kernelのNVDIMM実装は、root/drivers/nvdimm だけでなく、drivers/acpi/nfit とかにも散らばっているから、
「ちょっと読んだだけで、わかるかも!」という淡い期待は捨てよう。

と、いうことで、なまあたたかーい目で見ていただければ幸いでございます。


本編。

root/drivers/nvdimm の構成

最初に、Makefileに基づいて、構造を分類するとこんな感じになります。

image.png


ブロックデバイスとしてアクセスしよう!

ブロックデバイスとしてLinux kernelが認識する手順は以下の通り。

blk.c
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が…

blk.c
static const struct block_device_operations nd_blk_fops = {
    .owner = THIS_MODULE,
    .revalidate_disk = nvdimm_revalidate_disk,
};

これだとアクセスできない?ということで、もうちょっと読んでみる。

どこからアクセスできそうかな?

blk.c
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が生えた!

btt.c
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に繋がった!

btt.c
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);
}

include/nd.h
/**
 * 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; なので…

blk.c
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 とかにも散らばっているから、
「ちょっと読んだだけで、わかるかも!」という淡い期待は捨てよう、です。

10
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
4