device mapperの学習メモ

# device mapperとは

device mapperが何かは以下のリンクなどを参照。
http://lc.linux.or.jp/lc2009/slide/T-02-slide.pdf
https://en.wikipedia.org/wiki/Device_mapper

個人的には、Filesystemが他のUnixに比べて多様なLinuxでは、ZFSのようにFilesystemに統合すべき機能を切り出して提供する必要があったと理解している。



# 簡単なdevice mapperターゲットを使ってみる
まずは簡単なdevice mapper targetのコードを拾ってきて、ビルドして使ってみるところから始めた。

環境は以下の通り
CentOS Linux release 7.7.1908 (Core)　なお、Vagrantで構築したVirtualBox上の仮想マシン。

https://github.com/huwan/dm-target
からgit cloneする。

```bash
$ git clone https://github.com/huwan/dm-target.git
```

cloneしたフォルダに移動してmakeしてみる。

```bash
$ make 
make -C /lib/modules/3.10.0-957.12.2.el7.x86_64/build M=/home/vagrant/dm-target modules
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-957.12.2.el7.x86_64' に入ります
  CC [M]  /home/vagrant/dm-target/mapper.o
(中略)
include/linux/device-mapper.h:160:5: 備考: expected ‘struct dm_dev **’ but argument is of type ‘sector_t’
 int dm_get_device(struct dm_target *ti, const char *path, fmode_t mode,
     ^
/home/vagrant/dm-target/mapper.c:85:13: エラー: 関数 ‘dm_get_device’ への引数が多すぎます
             dm_table_get_mode(target->table), &mdt->dev)) {
             ^
In file included from /home/vagrant/dm-target/mapper.c:15:0:
include/linux/device-mapper.h:160:5: 備考: ここで宣言されています
 int dm_get_device(struct dm_target *ti, const char *path, fmode_t mode,
     ^
（略）
```

という感じで、困ったことに85行目でエラーが出てビルドが通らない。
該当する箇所は、

~~~c
 84    if (dm_get_device(target, argv[0], start, target->len,
 85                      dm_table_get_mode(target->table), &mdt->dev)) {
 86        target->error = "Device lookup failed";
 87        goto out;
 88    }
~~~
if文の中のdm_get_deviceが呼ばれているが、引数が多すぎると怒られている。
device-mapper.hを確認しよう。

まず、kernelのversionを確認する。

~~~bash
$ uname -a 
Linux localhost.localdomain 3.10.0-957.12.2.el7.x86_64 #1 SMP Tue May 14 21:24:32 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
~~~

3.10.0-957の方のヘッダを参照しているようだ。
device-mapper.hを検索する

~~~bash
$ sudo find / -name  device-mapper.h
/usr/src/kernels/3.10.0-957.12.2.el7.x86_64/include/linux/device-mapper.h
/usr/src/kernels/3.10.0-1062.18.1.el7.x86_64/include/linux/device-mapper.h
~~~~

ヘッダの中身を確認する

[/include/linux/device-mapper.h]

~~~c
/*                                                                                                                                                                      
 * Constructors should call these functions to ensure destination devices                                                                                               
 * are opened/closed correctly.                                                                                                                                         
 */
int dm_get_device(struct dm_target *ti, const char *path, fmode_t mode,
                  struct dm_dev **result);
~~~
コメントでコンストラクタだと書いてある。

[/drivers/md/dm-table.c]

~~~c
/*
 * Add a device to the list, or just increment the usage count if
 * it's already present.
 */
int dm_get_device(struct dm_target *ti, const char *path, fmode_t mode,
		  struct dm_dev **result)
~~~
どうも、ターゲットのブロックデバイスのリストに新しくブロックデバイスを追加する（しかも重複を認める？？）らしい。よくわからん。失敗したら0以外を返して来て、mapper.cのif文の中に入る仕掛けになっている。

ビルドが失敗した原因はdm_get_device が呼ばれたときに、startとtarget->lenが引数として余分だったせいだろう。
start は long long型、target->lenは sector_t型なので関数の宣言にない。
targetはdm_target構造体で、これはdevice-mapper.hで宣言されている。

mapper.cを修正する

[https://github.com/huwan/dm-target/blob/master/mapper.c] の82~88行目

~~~c
    /*  To add device in target's table and increment in device count */

    if (dm_get_device(target, argv[0], /*start, target->len,*/
		      dm_table_get_mode(target->table), &mdt->dev)) {
        target->error = "Device lookup failed";
        goto out;
    }
~~~

もう一度makeする

~~~bash
$ make
make -C /lib/modules/3.10.0-957.12.2.el7.x86_64/build M=/home/vagrant/dm-target modules
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-957.12.2.el7.x86_64' に入ります
  CC [M]  /home/vagrant/dm-target/mapper.o
/home/vagrant/dm-target/mapper.c:122:5: 警告: 互換性のないポインタ型からの初期化です [デフォルトで有効]
     .map = hello_target_map,
     ^
/home/vagrant/dm-target/mapper.c:122:5: 警告: (‘hello_target.map’ 用の初期化付近) [デフォルトで有効]
  Building modules, stage 2.
  MODPOST 1 modules
  CC      /home/vagrant/dm-target/mapper.mod.o
  LD [M]  /home/vagrant/dm-target/mapper.ko
make[1]: ディレクトリ `/usr/src/kernels/3.10.0-957.12.2.el7.x86_64' から出ます
$ ls
#mapper.c#  Makefile  Module.symvers  README.md  mapper.c  mapper.ko  mapper.mod.c  mapper.mod.o  mapper.o  modules.order  run-dmtarget.sh
~~~
無事ビルドは通った。


## インストール
セットアップ用のシェルスクリプトが同封されているので使ってみる。

~~~bash
$ sudo ./run-dmtarget.sh -s
128+0 records in
128+0 records out
134217728 bytes (134 MB) copied, 0.1957 s, 686 MB/s
/home/vagrant/dm-target/mapper.c:122:5: 警告: 互換性のないポインタ型からの初期化です [デフォルトで有効]
     .map = hello_target_map,
     ^
/home/vagrant/dm-target/mapper.c:122:5: 警告: (‘hello_target.map’ 用の初期化付近) [デフォルトで有効]
$ sudo dmsetup targets
hello_target     v1.0.0　<- これ
striped          v1.6.0
linear           v1.3.0
error            v1.5.0
$ lsblk 
NAME               MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda                  8:0    0   40G  0 disk 
└─sda1               8:1    0   40G  0 part /
loop0                7:0    0  128M  0 loop 
└─my_device_mapper 253:0    0  128M  0 dm   <- これ
~~~

こういう実験用のshellスクリプトが付属していると初心者には助かる。
中身を見て、何をしているかを整理すると見てみると、、、

~~~bash
dd if=/dev/zero of=/tmp/mydisk bs=1M count=128 # 128MB file                                                                                             
losetup /dev/loop0 /tmp/mydisk # losetup -f                                                                                                             
make -s
insmod ./mapper.ko
# echo <starting logical sector number> <logical size of device in terms of sector> <target name> <device path> <unsed paramter> | dmsetup create <mapper  name>                                                                                                                                                               
echo 0 262144 hello_target /dev/loop0 0 | dmsetup create my_device_mapper
~~~

読み解くと、下記のことをやっている。

1. ddでイメージファイルを作る
2. ループバックデバイス（が何かはわかってないが）を作成して、先程のイメージファイル/tmp/mydiskに割り当て
3. makeする
4. モジュールのインストール(insmod)
5. dmsetupでmy_device_mapperをhello_targetで作る


## 書き込み
次にddコマンドで書き込み試験をしてみる

~~~bash
sudo ./run-dmtarget.sh -d
16+0 records in
16+0 records out
16384 bytes (16 kB) copied, 0.0399802 s, 410 kB/s
~~~
上の処理が何をしているかというと

~~~bash
dd if=/dev/urandom of=/dev/mapper/my_device_mapper  bs=1K count=16
~~~
16KB分のランダムなデータを書き込んでいる。

## ファイルシステムでフォーマット
次に、作成したブロックデバイスをext4でフォーマットしてみる。

~~~bash
$ sudo ./run-dmtarget.sh -f 
copy.txt  lost+found  test.txt
~~~
上のコマンドで何をしているかと言うと、

~~~bash
if [ ! -d /mnt/mapper ]
then
　　　mkdir -p /mnt/mapper
fi
modprobe ext4
mkfs.ext4 -q /dev/mapper/my_device_mapper
mount /dev/mapper/my_device_mapper /mnt/mapper
cd /mnt/mapper
touch test.txt
cp test.txt copy.txt
ls
~~~

フォーマットして、マウントしている。実際にマウントされているかを確認する

~~~bash
$ lsblk 
NAME               MAJ:MIN RM  SIZE RO TYPE MOUNTPOINT
sda                  8:0    0   40G  0 disk 
└─sda1               8:1    0   40G  0 part /
loop0                7:0    0  128M  0 loop 
└─my_device_mapper 253:0    0  128M  0 dm   /mnt/mapper <- /mnt/mapperにマウントされている
$ ls /mnt/mapper/
copy.txt  lost+found  test.txt
$ cd /mnt/mapper/
$ du -sh 
du: cannot read directory ‘./lost+found’: 許可がありません
15K	.
$ sudo touch hoge
$ ls
copy.txt  hoge  lost+found  test.txt
~~~

普通に128MBのフォルダとして使えている。

## device mapperターゲットのコードリーディングをしてみる
今回扱ったのは、mapされたBlock IOを下に渡すだけの非常に簡単なdevice mapper targetなので単純で、ヘッダファイル(.h)すらなくてmapper.cに全部書いてある。

mapper.cではtarget_type構造体のインスタンス hello_targetのメンバを以下のように初期化している。

[https://github.com/huwan/dm-target/blob/master/mapper.c] の104行目から

~~~c
static struct target_type hello_target = {
    .name = "hello_target",
    .version = {1,0,0},
    .module = THIS_MODULE,
    .ctr = hello_target_ctr,
    .dtr = hello_target_dtr,
    .map = hello_target_map,
};
~~~

この構造体の初期化が最も重要である。
最低限必要なものは、ここに書いてあるとおり、
- ctr : コンストラクタ
- dtr :　デストラクタ
- map :　mapper
の３つの関数と、devicemapperのターゲット名、その他が定義されている。

なお、この構造体の定義はdevice-mapper.hで宣言されている。
block I/O (bio)をさばくための関数ポインタをmapメンバ変数に、コンストラクタとデストラクタをそれぞれctrとdtrに代入している。それだけ。

今回定義するターゲットの構造体は
[https://github.com/huwan/dm-target/blob/master/mapper.c] の20行目から

~~~c
/* For underlying device */
struct my_dm_target {
    struct dm_dev *dev;
    sector_t start;
};
~~~

dm_devはブロックデバイスと名前とfmode_tという型のよくわからん変数を持っている。複数のブロックデバイスを有するターゲットだとdm_devの変数を複数持つのだろう（多分）

device mapperターゲットのコンストラクタは、下記の通り。
[https://github.com/huwan/dm-target/blob/master/mapper.c] の49行目から

~~~c
/*
 * This is consHosttructor function of target gets called when we create some device of type 'hello_target'.
 * i.e on execution of command 'dmsetup create'. It gets called per device.
 */
static int hello_target_ctr(struct dm_target *target,
                            unsigned int argc, char **argv)
{
    struct my_dm_target *mdt;
    unsigned long long start;
    int ret = 0;
    DMINFO("Entry: %s", __func__);
　　（省略）
    mdt = kmalloc(sizeof(struct my_dm_target), GFP_KERNEL);
    if (sscanf(argv[1], "%llu", &start)!=1){
    (省略）}
    // ここでargv[1]をstart変数に代入して、mdtのメンバ変数startに代入している
    mdt->start=(sector_t)start;
　　// my_device_targetとdevメンバに代入している？
    if (dm_get_device(target, argv[0],
                      dm_table_get_mode(target->table), &mdt->dev)) {
(省略)}
    target->private = mdt;
    DMINFO("Exit : %s ", __func__);
    return ret;
}
~~~

my_dm_target *mdtの中身を順に埋めているのと、targetの初期化も行っている。
target->private は/* target specific data */と書いてあって、これに初期化したmy_device_targetを代入している。

うん？device mapperのターゲットの実態はmy_dm_targetではなくてtargetなのか?？?　よくわからん。

追記：struct dm_targetはdevice-mapperのターゲットが持っているべき共通の情報と、個々のtargetが持つべき情報がある。個々の情報はこの例の場合my_dm_targetに格納されていて、struct dm_targetのprivate変数にそのインスタンスへのポインタが格納されている。今回のtargetは何もしないターゲットのため、ほとんどなんの情報も入っていない。

ところで__func__はC言語のマクロで現在の関数名を示すらしい。
DMINFOはprintしているだけだろう。多分。

デコンストラクタは、
[https://github.com/huwan/dm-target/blob/master/mapper.c] の49行目から

```c
/*
 *  This is destruction function, gets called per device.
 *  It removes device and decrement device count.
 */
static void hello_target_dtr(struct dm_target *ti)
{
    struct my_dm_target *mdt = (struct my_dm_target *) ti->private;
    DMINFO("Entry: %s", __func__);
    dm_put_device(ti, mdt->dev);
    kfree(mdt);
    DMINFO("Exit : %s", __func__);
}
```
メモリの解法(kfree(mdt)) をしているだけのようだ。

mapする関数は以下の通り。
[https://github.com/huwan/dm-target/blob/master/mapper.c] の２６行目から

```c
/* Map function, called whenever target gets a bio request. */

static int hello_target_map(struct dm_target *target, struct bio *bio,
                            union map_info *map_context)
{
    struct my_dm_target *mdt = (struct my_dm_target *) target->private;
    DMINFO("Entry: %s", __func__);
    /*  bio should perform on our underlying device   */
    bio->bi_bdev = mdt->dev->bdev;

    if ((bio->bi_rw & WRITE) == WRITE)
        DMINFO("bio is a write request");
    else
        DMINFO("bio is a read request");
    submit_bio(bio->bi_rw, bio);
    DMINFO("Exit : %s", __func__);
    return DM_MAPIO_SUBMITTED;
}

```
dm_target　構造体のインスタンスを渡して、そこからprivateフィールドに格納されているmy_dm_targetインスタンスを取り出してる。
bio->bi_bdev = mdt->dev->bdev;のところで、CPUから降ってきたbioのbi_bdev(ブロックデバイスのID？）をmdt->dev->bdevに格納されているブロックデバイスに書き換え、submit_bioで命令を発行している。この処理によって、device_mapperで作成された仮想ブロックデバイスに対してbio(block I/O)が発行され、それを実デバイス（上の例はloopバックデバイスだが）にmapしている。

## ストレージトレースを取得する
ちょっと改造して、ストレージのTraceを取得してみたい。

~~~c
/* Map function, called whenever target gets a bio request. */                                                                                                          
static int hello_target_map(struct dm_target *target, struct bio *bio,                                                                                                  
                            union map_info *map_context)                                                                                                                
  {                                                                                                                                                                     
    struct my_dm_target *mdt = (struct my_dm_target *) target->private;                                                                                                 
    //DMINFO("Entry: %s", __func__);                                                                                                                                    
    /*  bio should perform on our underlying device   */                                                                                                                
    bio->bi_bdev = mdt->dev->bdev;                                                                                                                                      
                                                                                                                                                                        
    if ((bio->bi_rw & WRITE) == WRITE){                                                                                                                                 
      //DMINFO("bio is a write request");                                                                                                                               
      DMINFO("write, %llu, %d",(unsigned long long) bio->bi_sector, bio->bi_size);                                                                                      
    }                                                                                                                                                                   
    else{                                                                                                                                                               
      //DMINFO("bio is a read request");                                                                                                                                
      DMINFO("read, %llu, %d",(unsigned long long) bio->bi_sector, bio->bi_size);                                                                                      
    }                                                                                                                                                                   
                                                                                                                                                                        
    submit_bio(bio->bi_rw, bio);                                                                                                                                        
                                                                                                                                                                        
    //DMINFO("Exit : %s", __func__);                                                                                                                                    
    return DM_MAPIO_SUBMITTED;                                                                                                                                          
}         
~~~

ここで、struct bioの中身を知っておく必要がある。bio.hに定義されているだろうと思って見に行くと、
blk_types.hに書いてあるが、難しいのでこちら[https://wiki.bit-hive.com/linuxkernelmemo/pg/%E3%83%96%E3%83%AD%E3%83%83%E3%82%AFI%2FO]　
を見た。bio->bi_sectorとbio->bi_sizeをprintしているだけである。bio->bi_sectorはsector_tという型になっているのだが、unsinged long longにキャストしている。

以上を書き換えた上で、再度セットアップする。

~~~bash
[vagrant@localhost dm-target-trace]$ sudo ./run-dmtarget.sh -r
[vagrant@localhost dm-target-trace]$ sudo ./run-dmtarget.sh -s
128+0 records in
128+0 records out
134217728 bytes (134 MB) copied, 0.130902 s, 1.0 GB/s
/home/vagrant/dm-target-trace/mapper.c:121:5: 警告: 互換性のないポインタ型からの初期化です [デフォルトで有効]
     .map = hello_target_map,
     ^
/home/vagrant/dm-target-trace/mapper.c:121:5: 警告: (‘hello_target.map’ 用の初期化付近) [デフォルトで有効]
[vagrant@localhost dm-target-trace]$ sudo ./run-dmtarget.sh -d
1024+0 records in
1024+0 records out
1048576 bytes (1.0 MB) copied, 0.163913 s, 6.4 MB/s
~~~

/var/log/messageにtraceが吐き出されている。

~~~
vagrant@localhost log]$ sudo cat messages | grep "Feb 21 11:18" | grep "device-mapper" | more
Feb 21 11:18:19 localhost kernel: device-mapper: hello: Entry: init_hello_target
Feb 21 11:18:19 localhost kernel: device-mapper: hello: Target registered
Feb 21 11:18:19 localhost kernel: device-mapper: hello: Exit : init_hello_target
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 262016, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 262128, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 0, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 8, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 262136, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261880, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 262080, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261888, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261744, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261552, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261464, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261408, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261232, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261168, 4096
Feb 21 11:18:19 localhost kernel: device-mapper: hello: write, 261152, 4096
~~~

といった感じでtraceが取れている。

アドレスが小さい範囲をplotすると以下のような感じとなる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198144/d575e7b2-f77d-db20-e310-d5c527490cd0.png)