Device Mapper / LVM 심화

Linux 커널의 Device Mapper(DM) 프레임워크를 심층 분석합니다. 블록 디바이스 가상화 계층의 핵심 구조체(mapped_device, dm_table, dm_target, target_type)부터 dm-linear, dm-crypt, dm-thin, dm-cache, dm-verity, dm-snapshot 등 주요 타겟 드라이버, LVM2 사용자 공간 도구, 소프트웨어 RAID(md), dmsetup 관리, 커널 내부 I/O 경로, 성능 튜닝과 디버깅 기법까지 종합적으로 다룹니다.

선행 지식: 이 문서는 Block I/O 서브시스템struct bio, blk-mq, I/O 스케줄러 개념을 이해하고 있다고 가정합니다. 커널 모듈 개발 경험이 있으면 코드 예제를 이해하는 데 도움이 됩니다.
전제 조건: Block I/O 서브시스템(bio, blk-mq 개념)을 반드시 먼저 읽으세요.
일상 비유: Device Mapper는 건물의 층 안내 시스템과 같습니다. 방문자(I/O 요청)가 "3층 302호"로 가려고 하면, 안내 데스크(DM)가 실제 물리적 위치(다른 건물 5층)로 안내합니다. LVM은 여러 건물의 층을 하나의 큰 건물처럼 보이게 하는 것, dm-crypt는 출입문에 암호 잠금장치를 다는 것, dm-thin은 아직 건설되지 않은 층도 "있는 것처럼" 안내하는 것입니다.

핵심 요약

  • Device Mapper — 물리 블록 디바이스 위에 가상 블록 디바이스를 생성하는 프레임워크입니다.
  • LVM2 — DM 위에 구현된 논리 볼륨 관리자. 디스크 크기 조정, 스냅샷 등을 제공합니다.
  • dm-crypt — 블록 레벨 투명 암호화. LUKS가 이를 활용합니다.
  • dm-thin — Thin provisioning. 실제 사용량보다 큰 볼륨을 생성(오버커밋)합니다.
  • dm-verity — 블록 무결성 검증. Android의 파티션 검증에 사용됩니다.

단계별 이해

  1. DM 확인dmsetup ls로 현재 시스템의 DM 디바이스 목록을 확인합니다.

    lsblk에서 TYPElvm, crypt인 것이 DM 디바이스입니다.

  2. LVM 기초 — PV(Physical Volume) → VG(Volume Group) → LV(Logical Volume) 계층으로 디스크를 관리합니다.

    pvs, vgs, lvs 명령어로 각 계층을 확인합니다.

  3. 매핑 테이블dmsetup table로 DM 디바이스의 매핑 규칙을 볼 수 있습니다.

    각 줄은 "시작 섹터, 길이, 타겟 타입, 타겟 인자"로 구성됩니다.

  4. 실전 활용 — LVM으로 볼륨 확장(lvextend), 스냅샷 생성, LUKS로 디스크 암호화 등을 수행합니다.

    이 모든 기능이 Device Mapper 프레임워크 위에서 동작합니다.

Device Mapper 개요

Device Mapper(DM)는 Linux 커널의 블록 디바이스 가상화 프레임워크입니다. 하나 이상의 물리 블록 디바이스 위에 가상 블록 디바이스를 생성하고, I/O 요청을 변환(매핑)하여 하위 디바이스에 전달합니다. LVM2, LUKS 암호화, dm-verity 무결성 검증, thin provisioning 등 현대 Linux 스토리지 스택의 핵심 기능이 모두 DM 위에 구축되어 있습니다.

핵심 특징

역사

Device Mapper는 2003년 Linux 2.6.0에서 Joe Thornber, Alasdair Kergon 등이 도입했습니다. 기존 LVM1의 커널 드라이버를 대체하며, 더 범용적인 블록 매핑 프레임워크로 설계되었습니다. 이후 dm-crypt(2.6.4), dm-snapshot(2.6.0), dm-thin(3.2), dm-cache(3.9), dm-verity(3.4) 등이 추가되었습니다.

Filesystem / Application Device Mapper Layer mapped_device (/dev/dm-0) dm_table (매핑 테이블) dm_target[0]: linear dm_target[1]: crypt dm_target[2]: linear /dev/sda1 /dev/sda2 /dev/sdb1 DM은 가상 디바이스의 섹터 범위를 물리 디바이스에 매핑합니다

DM 아키텍처

Device Mapper의 커널 코드는 drivers/md/ 디렉터리에 위치합니다. 핵심 구조체 네 가지가 DM의 뼈대를 이룹니다.

mapped_device

struct mapped_device는 하나의 가상 블록 디바이스를 나타냅니다. /dev/dm-N 또는 /dev/mapper/이름으로 사용자 공간에 노출됩니다.

/* drivers/md/dm-core.h */
struct mapped_device {
    struct dm_table __rcu   *map;          /* 현재 활성 매핑 테이블 (RCU 보호) */
    struct gendisk           *disk;         /* 블록 디바이스 디스크 구조체 */
    struct request_queue    *queue;        /* 블록 I/O 요청 큐 */
    unsigned long            flags;        /* DMF_* 플래그 */
    struct mutex             suspend_lock; /* suspend/resume 동기화 */
    atomic_t                 holders;      /* 참조 카운트 */
    struct bio_set            bs;           /* bio 할당 풀 */
    struct bio_set            io_bs;        /* I/O 전용 bio 풀 */
    /* ... */
};

dm_table

struct dm_table은 가상 디바이스의 전체 섹터 공간을 여러 타겟으로 분할하는 매핑 테이블입니다. 테이블 교체는 원자적(atomic swap)으로 이루어져 I/O 중단 없이 온라인 재구성이 가능합니다.

/* drivers/md/dm-table.c */
struct dm_table {
    struct mapped_device  *md;           /* 소속 mapped_device */
    unsigned int          num_targets;  /* 타겟 개수 */
    struct dm_target     *targets;     /* 타겟 배열 */
    struct dm_dev        **devices;     /* 참조된 하위 디바이스 배열 */
    fmode_t               mode;        /* 읽기/쓰기 모드 */
    /* ... */
};

dm_target

struct dm_target은 매핑 테이블 내의 하나의 연속 섹터 구간을 표현합니다. 각 타겟은 시작 섹터, 길이, 그리고 실제 매핑 로직을 수행하는 target_type을 가집니다.

/* include/linux/device-mapper.h */
struct dm_target {
    struct dm_table      *table;       /* 소속 테이블 */
    struct target_type   *type;        /* 타겟 유형 (linear, crypt 등) */
    sector_t              begin;       /* 가상 디바이스 내 시작 섹터 */
    sector_t              len;         /* 섹터 수 */
    unsigned int          max_io_len;  /* bio 분할 단위 */
    void                 *private;     /* 타겟 드라이버 전용 데이터 */
    char                 *error;       /* 에러 메시지 */
    /* ... */
};

target_type

struct target_type은 타겟 드라이버의 오퍼레이션 테이블입니다. 새로운 DM 타겟을 구현하려면 이 구조체를 정의하고 dm_register_target()으로 등록합니다.

/* include/linux/device-mapper.h */
struct target_type {
    uint64_t           features;
    const char        *name;         /* "linear", "crypt" 등 */
    struct module     *module;

    /* 생명주기 콜백 */
    dm_ctr_fn          ctr;           /* 생성자: 테이블 로드 시 호출 */
    dm_dtr_fn          dtr;           /* 소멸자: 테이블 해제 시 호출 */
    dm_map_fn          map;           /* bio 매핑 함수 (핵심!) */
    dm_clone_and_map_fn clone_and_map_rq; /* request 기반 매핑 */

    /* 선택적 콜백 */
    dm_presuspend_fn   presuspend;
    dm_postsuspend_fn  postsuspend;
    dm_preresume_fn    preresume;
    dm_resume_fn       resume;
    dm_status_fn       status;        /* dmsetup status 응답 */
    dm_message_fn      message;       /* dmsetup message 처리 */
    dm_io_hints_fn     io_hints;      /* I/O 힌트 (정렬, 제한 등) */
    /* ... */
};
map() 반환값: DM_MAPIO_SUBMITTED(bio가 타겟에 의해 제출됨), DM_MAPIO_REMAPPED(bio가 리매핑되어 DM 코어가 제출), DM_MAPIO_KILL(bio를 에러로 완료), DM_MAPIO_REQUEUE(bio를 재큐잉).

타겟 등록/해제

/* 타겟 드라이버 모듈 초기화 */
static struct target_type my_target = {
    .name    = "my_target",
    .version = {1, 0, 0},
    .module  = THIS_MODULE,
    .ctr     = my_ctr,
    .dtr     = my_dtr,
    .map     = my_map,
    .status  = my_status,
};

static int __init my_init(void)
{
    return dm_register_target(&my_target);
}

static void __exit my_exit(void)
{
    dm_unregister_target(&my_target);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

dm-linear

dm-linear은 가장 단순한 DM 타겟으로, 가상 디바이스의 섹터 범위를 물리 디바이스의 연속된 섹터 범위에 1:1로 매핑합니다. LVM2가 논리 볼륨을 구성할 때 가장 기본적으로 사용하는 타겟입니다.

매핑 원리

/* drivers/md/dm-linear.c */
struct linear_c {
    struct dm_dev  *dev;     /* 하위 디바이스 */
    sector_t        start;   /* 하위 디바이스 시작 오프셋 */
};

static int linear_map(struct dm_target *ti, struct bio *bio)
{
    struct linear_c *lc = ti->private;

    /* bio의 대상 디바이스와 섹터를 리매핑 */
    bio_set_dev(bio, lc->dev->bdev);
    bio->bi_iter.bi_sector = lc->start +
        dm_target_offset(ti, bio->bi_iter.bi_sector);

    return DM_MAPIO_REMAPPED;
}

dm_target_offset()bio->bi_iter.bi_sector - ti->begin으로, 가상 디바이스의 시작 섹터를 기준으로 한 상대 오프셋을 계산합니다. 이에 하위 디바이스의 시작 오프셋(lc->start)을 더하면 물리 섹터가 됩니다.

사용 예제

# /dev/sda의 섹터 0~2097151 (1GB)을 가상 디바이스로 매핑
$ echo "0 2097152 linear /dev/sda 0" | dmsetup create my_linear

# 테이블 확인
$ dmsetup table my_linear
0 2097152 linear 8:0 0

# 두 디바이스를 연결하여 하나의 큰 가상 디바이스 생성
$ dmsetup create my_concat <<EOF
0 2097152 linear /dev/sda1 0
2097152 4194304 linear /dev/sdb1 0
EOF

# 결과: /dev/mapper/my_concat = sda1(1GB) + sdb1(2GB) = 3GB 가상 디바이스

dm-striped

dm-striped는 RAID-0 스트라이핑을 구현합니다. I/O를 청크(stripe) 단위로 여러 하위 디바이스에 라운드 로빈 분산하여 대역폭을 극대화합니다.

스트라이프 레이아웃

/* drivers/md/dm-stripe.c */
struct stripe {
    struct dm_dev  *dev;
    sector_t        physical_start;
};

struct stripe_c {
    uint32_t        stripes;         /* 스트라이프 수 */
    uint32_t        chunk_size;      /* 청크 크기 (섹터) */
    sector_t        stripe_width;    /* 전체 스트라이프 폭 */
    struct stripe    stripe_devs[0]; /* 유연 배열 */
};

static int stripe_map(struct dm_target *ti, struct bio *bio)
{
    struct stripe_c *sc = ti->private;
    sector_t offset = dm_target_offset(ti, bio->bi_iter.bi_sector);
    uint32_t chunk = sector_div(offset, sc->chunk_size);
    uint32_t stripe_idx = sector_div(chunk, sc->stripes);

    bio_set_dev(bio, sc->stripe_devs[stripe_idx].dev->bdev);
    bio->bi_iter.bi_sector = sc->stripe_devs[stripe_idx].physical_start +
                              chunk * sc->chunk_size + offset;
    return DM_MAPIO_REMAPPED;
}

사용 예제

# 2개 디바이스, 64KB 청크 스트라이핑
$ echo "0 4194304 striped 2 128 /dev/sda1 0 /dev/sdb1 0" | dmsetup create my_stripe
#   ^시작  ^길이  ^타입  ^N ^청크   ^디바이스1    ^디바이스2
# 128 섹터 = 128 * 512 = 64KB 청크

# 스트라이프 상태 확인
$ dmsetup status my_stripe
0 4194304 striped 2 128 A A
# A = 활성(Active), D = 비활성(Dead)

dm-crypt

dm-crypt는 블록 레벨 투명 암호화를 제공합니다. 쓰기 시 bio 데이터를 암호화하고, 읽기 시 복호화합니다. LUKS(Linux Unified Key Setup)의 커널 측 백엔드입니다.

암호화 아키텍처

/* drivers/md/dm-crypt.c */
struct crypt_config {
    struct dm_dev           *dev;         /* 하위 디바이스 */
    sector_t                 start;       /* 데이터 시작 오프셋 */
    struct crypt_iv_operations *iv_gen_ops; /* IV 생성 방식 */
    struct crypto_skcipher   *tfm;         /* 암호화 변환 */
    unsigned int             iv_size;     /* IV 바이트 수 */
    sector_t                 iv_offset;   /* IV 계산용 섹터 오프셋 */
    struct workqueue_struct  *io_queue;    /* I/O 워크큐 */
    struct workqueue_struct  *crypt_queue; /* 암/복호화 워크큐 */
    u8                       key[0];      /* 암호화 키 */
};

I/O 경로

성능 참고: dm-crypt는 쓰기마다 bounce buffer를 할당하고 memcpy를 수행합니다. AES-NI/ARMv8 Crypto Extension 같은 하드웨어 가속이 없으면 CPU 오버헤드가 상당합니다. 최신 커널(5.4+)에서는 no_read_workqueue, no_write_workqueue 옵션으로 워크큐 우회가 가능합니다.

LUKS 사용 예제

# LUKS2 포맷 (AES-XTS-plain64, 512비트 키)
$ cryptsetup luksFormat --type luks2 --cipher aes-xts-plain64 \
    --key-size 512 --hash sha256 /dev/sda2

# 볼륨 열기 → /dev/mapper/cryptroot 생성
$ cryptsetup luksOpen /dev/sda2 cryptroot

# 내부적으로 생성되는 DM 테이블 확인
$ dmsetup table cryptroot
0 41943040 crypt aes-xts-plain64 0000...0000 0 8:2 4096
#                ^cipher         ^key(hex)  ^iv ^dev ^offset

# 파일시스템 생성 및 마운트
$ mkfs.ext4 /dev/mapper/cryptroot
$ mount /dev/mapper/cryptroot /mnt/secure

# 볼륨 닫기
$ umount /mnt/secure
$ cryptsetup luksClose cryptroot

# 성능 벤치마크: 워크큐 최적화 옵션
$ cryptsetup open --perf-no_read_workqueue --perf-no_write_workqueue \
    /dev/sda2 cryptroot

IV(Initialization Vector) 생성 방식

IV 모드설명보안 수준
plain섹터 번호를 32비트로 사용낮음 (4TB 이하만)
plain64섹터 번호를 64비트로 사용표준
essivEncrypted Salt-Sector IV높음 (워터마크 공격 방지)
benbiBig-Endian Narrow Block IV특수 용도
random랜덤 IV (쓰기 전용)높음 (읽기 불가)

dm-thin (Thin Provisioning)

dm-thin은 씬 프로비저닝과 스냅샷을 제공합니다. 실제 물리 공간을 사전에 할당하지 않고, 쓰기 시점에 동적으로 블록을 할당합니다. Copy-on-Write(CoW) 기반 스냅샷도 지원합니다.

핵심 개념

# thin-pool 생성
# metadata: 256MB, data: 100GB, 블록 크기: 64KB
$ dmsetup create thin_meta --table "0 524288 linear /dev/sdc1 0"
$ dmsetup create thin_data --table "0 209715200 linear /dev/sdc2 0"
$ dmsetup create thin_pool --table \
    "0 209715200 thin-pool /dev/mapper/thin_meta /dev/mapper/thin_data 128 0"
#                                                                    ^block_size(섹터)

# thin volume 생성 (ID=0, 논리 크기 500GB — 물리 100GB보다 큼!)
$ dmsetup message thin_pool 0 "create_thin 0"
$ dmsetup create thin_vol0 --table \
    "0 1048576000 thin /dev/mapper/thin_pool 0"

# 스냅샷 생성 (ID=1, 소스=ID 0)
$ dmsetup message thin_pool 0 "create_snap 1 0"
$ dmsetup create thin_snap1 --table \
    "0 1048576000 thin /dev/mapper/thin_pool 1"

# 풀 사용량 확인
$ dmsetup status thin_pool
0 209715200 thin-pool 42 310/524288 12048/209715200 - rw no_discard_passdown
#                       ^tx ^meta사용/전체  ^data사용/전체
overprovisioning 위험: thin-pool의 물리 공간이 고갈되면 I/O 에러가 발생하고, 파일시스템이 read-only로 전환될 수 있습니다. 프로덕션 환경에서는 반드시 모니터링과 자동 확장 정책을 구성하세요. LVM2의 thin_pool_autoextend_threshold 옵션을 활용할 수 있습니다.

dm-cache / dm-writecache

dm-cache는 느린 HDD 앞에 빠른 SSD를 캐시 계층으로 배치하여 성능을 향상시킵니다. dm-writecache는 쓰기 전용 캐싱에 특화된 타겟입니다.

dm-cache 구조

캐시 모드

모드읽기쓰기특징
writeback캐시 히트 시 SSD에서SSD에 먼저 기록최고 성능, SSD 장애 시 데이터 손실 위험
writethrough캐시 히트 시 SSD에서SSD + HDD 동시 기록안전하지만 쓰기 성능 향상 없음
passthrough항상 HDD에서항상 HDD에캐시 워밍만 수행, 마이그레이션용
# dm-cache 설정 (writeback 모드, smq 정책)
$ dmsetup create cache_meta --table "0 8192 linear /dev/nvme0n1p1 0"
$ dmsetup create cache_data --table "0 209715200 linear /dev/sda 0"
$ dmsetup create cache_ssd  --table "0 41943040 linear /dev/nvme0n1p2 0"

$ dmsetup create my_cache --table \
    "0 209715200 cache /dev/mapper/cache_meta /dev/mapper/cache_ssd \
     /dev/mapper/cache_data 128 1 writeback smq 0"
#                        ^meta          ^cache(SSD)
#     ^origin(HDD)     ^block_sectors ^feature_count ^mode ^policy

# 캐시 통계 확인
$ dmsetup status my_cache
0 209715200 cache 8 42/512 128 1638400/3276800 256 7890 1234 5 2 1 writeback 2 \
  migration_threshold 2048 smq 0 rw -
# read_hits write_hits read_misses write_misses demotions promotions

dm-writecache

dm-writecache는 쓰기 I/O만 SSD(또는 PMEM)에 캐시하는 단순한 타겟입니다. dm-cache보다 오버헤드가 적고, 특히 DAX 가능 PMEM 디바이스와 결합하면 매우 낮은 쓰기 지연을 달성합니다.

# dm-writecache (SSD 모드)
$ dmsetup create my_wc --table \
    "0 209715200 writecache s /dev/sda /dev/nvme0n1p1 4096 0"
#                        ^type(s=SSD,p=PMEM)  ^origin ^cache ^block_size

# PMEM 모드 (DAX 가능, 더 낮은 지연)
$ dmsetup create my_wc_pmem --table \
    "0 209715200 writecache p /dev/sda /dev/pmem0 4096 0"

dm-verity

dm-verity는 블록 디바이스의 무결성을 읽기 시점에 검증합니다. 데이터 블록마다 해시를 사전에 계산하여 해시 트리(Merkle Tree)를 구성하고, 읽기 I/O가 발생하면 해시를 검증하여 변조를 탐지합니다. Android의 Verified Boot와 Chrome OS의 dm-verity가 대표적인 사용 사례입니다.

해시 트리 구조

/* Merkle Hash Tree 구조
 *
 *         [Root Hash]           ← 단일 값, 커널 커맨드 라인에 전달
 *           /     \
 *      [H01]       [H23]       ← Level 1: 하위 해시들의 해시
 *      /   \       /   \
 *   [H0]  [H1]  [H2]  [H3]    ← Level 0: 데이터 블록의 해시
 *    |     |     |     |
 *  [D0]  [D1]  [D2]  [D3]     ← 데이터 블록 (4KB 각)
 */

커널 구현 핵심

/* drivers/md/dm-verity-target.c */
struct dm_verity {
    struct dm_dev          *data_dev;     /* 검증 대상 데이터 디바이스 */
    struct dm_dev          *hash_dev;     /* 해시 트리 저장 디바이스 */
    struct crypto_ahash    *tfm;          /* 해시 알고리즘 (sha256 등) */
    u8                     *root_digest;  /* 루트 해시 */
    u8                     *salt;         /* 솔트 */
    unsigned int            data_dev_block_bits;
    unsigned int            hash_dev_block_bits;
    unsigned int            hash_per_block_bits;
    sector_t                hash_start;   /* 해시 트리 시작 섹터 */
    unsigned int            levels;       /* 해시 트리 깊이 */
    /* ... */
};

사용 예제

# 해시 트리 생성 (veritysetup)
$ veritysetup format /dev/sda1 /dev/sda2
# VERITY header information for /dev/sda2
# UUID:            12345678-abcd-1234-abcd-123456789abc
# Hash type:       1
# Data block size: 4096
# Hash block size: 4096
# Hash algorithm:  sha256
# Salt:            abcdef0123456789...
# Root hash:       a1b2c3d4e5f6...  ← 이 값을 보관!

# 검증 디바이스 활성화
$ veritysetup open /dev/sda1 verified_root /dev/sda2 \
    a1b2c3d4e5f6...

# DM 테이블 확인
$ dmsetup table verified_root
0 2097152 verity 1 8:1 8:2 4096 4096 262144 1 sha256 \
  a1b2c3d4e5f6... abcdef0123456789...
# ^ver ^data ^hash ^dbs ^hbs ^blocks ^hs ^alg ^root_hash ^salt

# Android 부팅 시 커널 커맨드 라인 예:
# dm="1 vroot none ro 1,0 2097152 verity 1 /dev/sda1 /dev/sda2 ..."
dm-verity + FEC: Linux 4.6부터 Forward Error Correction(FEC)을 지원하여, 소수의 손상된 블록을 자동 복구할 수 있습니다. Android에서 OTA 업데이트 후 일부 블록 손상이 발생해도 부팅이 가능하도록 사용됩니다.

Android dm-verity 심화 — AVB 연동

Android Verified Boot(AVB)는 dm-verity를 핵심으로 사용하여 system, vendor 파티션의 런타임 무결성을 보장한다. 부트로더가 VBMeta 파티션의 RSA 서명을 검증하고, 각 파티션의 해시 트리 루트 해시를 커널에 전달한다.

/* AVB VBMeta → dm-verity 체인 */
vbmeta (RSA-4096 서명)
 ├── boot (hash descriptor)        → GKI 커널 검증
 ├── system (hashtree descriptor)  → dm-verity 루트 해시
 │   └── dm-verity: 블록 읽기 시마다 SHA-256 검증
 ├── vendor (hashtree descriptor)  → dm-verity 루트 해시
 └── vbmeta_system (chained)

# A/B 파티션 + dm-verity:
# system_a / system_b — OTA 업데이트 시 비활성 슬롯에 쓰고 전환
# dm-verity FEC로 minor corruption 자동 복구
# 복구 불가 시 dm-verity가 I/O 에러 반환 → 부트로더가 다른 슬롯으로 폴백

AVB의 VBMeta 구조, 롤백 보호, 부트 흐름 등 심화 내용은 Android 커널 — AVBSecure Boot를 참고하라.

dm-snapshot

dm-snapshot은 Copy-on-Write(CoW) 기반 스냅샷을 제공합니다. 원본 디바이스의 특정 시점 상태를 보존하면서 원본에 대한 쓰기를 허용합니다. 변경된 블록만 별도 COW 디바이스에 저장하므로 공간 효율적입니다.

스냅샷 유형

타겟역할설명
snapshot-origin원본원본 디바이스를 감싸며, 쓰기 시 스냅샷의 COW 디바이스에 원래 데이터 복사
snapshot스냅샷특정 시점의 읽기 전용(또는 R/W) 뷰. COW 디바이스에 변경 블록 저장
snapshot-merge병합스냅샷의 변경 사항을 원본에 병합
# 스냅샷 COW 디바이스 준비 (원본의 10% 정도)
$ lvcreate -L 1G -n cow_store vg0

# 원본에 snapshot-origin 적용
$ dmsetup create origin --table "0 20971520 snapshot-origin /dev/vg0/data"

# 스냅샷 생성 (persistent = 재부팅 후 유지, 청크 크기 8 섹터 = 4KB)
$ dmsetup create snap1 --table \
    "0 20971520 snapshot /dev/vg0/data /dev/vg0/cow_store P 8"
#                                                        ^P=persistent, N=non-persistent

# 스냅샷 사용량 확인
$ dmsetup status snap1
0 20971520 snapshot 1024/2097152 16
#                    ^사용/전체(청크)  ^메타데이터(청크)
COW 공간 고갈: COW 디바이스가 가득 차면 스냅샷이 무효화(invalid)됩니다. 스냅샷은 임시 용도(백업, 테스트)로 사용하고, 장기 보존에는 dm-thin의 씬 스냅샷을 권장합니다.

LVM2 (Logical Volume Manager)

LVM2는 Device Mapper 위에 구축된 사용자 공간 볼륨 관리 도구입니다. 물리 디스크를 유연하게 파티셔닝하고, 온라인 확장/축소, 스냅샷, 씬 프로비저닝, 미러링, 캐싱 등을 제공합니다.

계층 구조

/*
 * LVM2 계층:
 *
 *   Physical Volume (PV)     ← 물리 디스크 또는 파티션
 *        │
 *   Volume Group (VG)        ← PV들의 풀 (하나의 스토리지 풀)
 *        │
 *   Logical Volume (LV)      ← VG에서 할당된 가상 볼륨
 *        │
 *   /dev/mapper/vg-lv        ← DM 디바이스로 노출
 *
 * PV 내부: PE(Physical Extent) 단위로 분할 (기본 4MB)
 * LV: 하나 이상의 PE를 할당받아 구성
 */
PV: /dev/sda1 PV: /dev/sdb1 PV: /dev/sdc1 VG: my_vg (300GB pool) LV: root (50GB) LV: home (100GB) LV: data (150GB) /dev/mapper/my_vg-root /dev/mapper/my_vg-home /dev/mapper/my_vg-data

주요 명령어

# ===== Physical Volume (PV) =====
$ pvcreate /dev/sda1 /dev/sdb1 /dev/sdc1   # PV 초기화
$ pvdisplay /dev/sda1                       # PV 정보
$ pvs                                       # PV 요약 목록
$ pvmove /dev/sda1 /dev/sdd1               # PV 간 데이터 이동 (온라인)

# ===== Volume Group (VG) =====
$ vgcreate my_vg /dev/sda1 /dev/sdb1       # VG 생성
$ vgextend my_vg /dev/sdc1                 # VG에 PV 추가
$ vgreduce my_vg /dev/sda1                 # VG에서 PV 제거 (pvmove 선행)
$ vgs                                       # VG 요약 목록

# ===== Logical Volume (LV) =====
$ lvcreate -L 50G -n root my_vg            # 50GB LV 생성
$ lvcreate -l 100%FREE -n data my_vg       # 남은 공간 전부 사용
$ lvextend -L +20G /dev/my_vg/root         # 20GB 확장
$ resize2fs /dev/my_vg/root                # ext4 파일시스템도 확장
$ lvreduce -L -10G /dev/my_vg/data         # 축소 (파일시스템 먼저 축소!)
$ lvremove /dev/my_vg/data                 # LV 삭제

# ===== LVM 스냅샷 =====
$ lvcreate -L 5G -s -n root_snap /dev/my_vg/root  # CoW 스냅샷
$ lvconvert --merge /dev/my_vg/root_snap           # 스냅샷을 원본에 병합

# ===== LVM Thin Provisioning =====
$ lvcreate -L 100G --thinpool thin_pool my_vg      # thin-pool 생성
$ lvcreate -V 500G --thin -n thin_vol my_vg/thin_pool  # 씬 볼륨
$ lvcreate -s /dev/my_vg/thin_vol -n thin_snap     # 씬 스냅샷 (공간 0)

# ===== LVM Cache (dm-cache 기반) =====
$ lvcreate -L 20G -n cache_data my_vg /dev/nvme0n1p1   # 캐시 데이터
$ lvcreate -L 256M -n cache_meta my_vg /dev/nvme0n1p1  # 캐시 메타
$ lvconvert --type cache-pool --poolmetadata my_vg/cache_meta my_vg/cache_data
$ lvconvert --type cache --cachepool my_vg/cache_data my_vg/root
# 이제 my_vg/root는 NVMe 캐싱된 LV

LVM2 내부 동작

LVM2는 사용자 공간의 lvm2 도구들과 커널의 Device Mapper를 연결합니다. libdevmapper/dev/mapper/control을 통해 DM ioctl을 호출하여 매핑 테이블을 로드합니다.

/* LVM2가 내부적으로 생성하는 DM 테이블 예시 */

# 단순 선형 LV (하나의 PV에서 연속 할당)
$ dmsetup table my_vg-root
0 104857600 linear 8:1 2048

# 여러 PV에 걸친 LV (다중 linear 세그먼트)
$ dmsetup table my_vg-data
0 52428800 linear 8:1 104859648
52428800 52428800 linear 8:17 2048
104857600 52428800 linear 8:33 2048

# 미러 LV (dm-raid1)
$ dmsetup table my_vg-mirror
0 104857600 raid raid1 3 0 region_size 1024 2 8:1 2048 8:17 2048

MD (Multiple Devices) - 소프트웨어 RAID

MD(Multiple Devices)는 커널 내장 소프트웨어 RAID 드라이버입니다. Device Mapper와는 별도의 프레임워크이지만 drivers/md/ 디렉터리를 공유하며, LVM2와 결합하여 사용되는 경우가 많습니다.

RAID 레벨

레벨최소 디스크용량 효율내결함성특징
RAID 02100%없음스트라이핑, 최대 성능
RAID 1250%1디스크미러링, 읽기 성능 향상
RAID 53(N-1)/N1디스크분산 패리티, 범용
RAID 64(N-2)/N2디스크이중 패리티, 대용량 스토리지
RAID 10450%미러당 1미러+스트라이프, 고성능+안정성

커널 핵심 구조체

/* drivers/md/md.h */
struct mddev {
    struct gendisk          *gendisk;       /* /dev/mdN */
    struct md_personality   *pers;          /* RAID 성격 (레벨별 ops) */
    int                      level;         /* RAID 레벨 */
    int                      raid_disks;    /* 활성 디스크 수 */
    sector_t                 dev_sectors;   /* 각 디스크 사용 섹터 */
    int                      chunk_sectors; /* 스트라이프 청크 크기 */
    struct list_head         disks;         /* md_rdev 리스트 */
    struct md_thread        *thread;        /* 리싱크/복구 스레드 */
    unsigned long            recovery;      /* 복구 상태 비트맵 */
    struct bitmap           *bitmap;        /* 쓰기 인텐트 비트맵 */
    /* ... */
};

/* 각 디스크 장치 */
struct md_rdev {
    struct block_device  *bdev;        /* 블록 디바이스 */
    sector_t              sectors;     /* 사용 가능 섹터 */
    int                   raid_disk;   /* 배열 내 인덱스 */
    int                   desc_nr;     /* 슈퍼블록 내 번호 */
    unsigned long         flags;       /* In_sync, Faulty, ... */
    struct list_head      same_set;    /* mddev->disks 연결 */
    /* ... */
};

mdadm 사용 예제

# RAID 5 생성 (3 디스크 + 1 스페어)
$ mdadm --create /dev/md0 --level=5 --raid-devices=3 \
    --spare-devices=1 /dev/sd{a,b,c,d}1

# RAID 상태 확인
$ cat /proc/mdstat
Personalities : [raid6] [raid5] [raid4]
md0 : active raid5 sdc1[2] sdb1[1] sda1[0]
      2093056 blocks super 1.2 level 5, 512k chunk, algorithm 2 [3/3] [UUU]

$ mdadm --detail /dev/md0

# 디스크 장애 시뮬레이션
$ mdadm --fail /dev/md0 /dev/sdb1
$ mdadm --remove /dev/md0 /dev/sdb1

# 새 디스크로 교체 (자동 리빌드)
$ mdadm --add /dev/md0 /dev/sde1

# 리빌드 진행률 확인
$ cat /proc/mdstat
md0 : active raid5 sde1[4] sdc1[2] sda1[0]
      2093056 blocks super 1.2 level 5, 512k chunk, algorithm 2 [3/2] [U_U]
      [====>................]  recovery = 22.3% (234112/1046528) finish=1.2min

# 비트맵(Write-Intent Bitmap) 추가 — 리빌드 시간 단축
$ mdadm --grow /dev/md0 --bitmap=internal

# RAID 정보를 설정 파일에 저장
$ mdadm --detail --scan >> /etc/mdadm/mdadm.conf

# RAID 10 (near layout) 생성
$ mdadm --create /dev/md1 --level=10 --raid-devices=4 \
    --layout=n2 /dev/sd{a,b,c,d}2
# n2 = near-2 copies (각 청크를 2개 디스크에 미러)

MD vs DM RAID

dm-raid: LVM2는 MD의 RAID 기능을 Device Mapper 타겟(dm-raid)으로 감싸서 사용합니다. lvconvert --type raid1 같은 명령이 내부적으로 dm-raid 테이블을 생성합니다. dm-raid는 MD의 md_personality를 재사용하면서도 DM의 테이블 교체, 스택킹 기능을 활용합니다.

dmsetup 도구

dmsetup은 Device Mapper를 직접 제어하는 저수준 사용자 공간 도구입니다. LVM2, cryptsetup, veritysetup 등 고수준 도구가 내부적으로 libdevmapper를 통해 동일한 ioctl을 호출합니다.

핵심 명령

# ===== 디바이스 관리 =====
$ dmsetup create <name> --table "<table>"  # DM 디바이스 생성
$ dmsetup remove <name>                    # 디바이스 제거
$ dmsetup suspend <name>                   # I/O 일시 중지
$ dmsetup resume <name>                    # I/O 재개
$ dmsetup reload <name> --table "<table>"  # 테이블 교체 (suspend→reload→resume)

# ===== 정보 조회 =====
$ dmsetup ls                               # 모든 DM 디바이스 목록
$ dmsetup table <name>                     # 매핑 테이블
$ dmsetup status <name>                    # 런타임 상태
$ dmsetup info <name>                      # 디바이스 정보 (major, minor, open count 등)
$ dmsetup deps <name>                      # 의존하는 하위 디바이스

# ===== 고급 =====
$ dmsetup message <name> 0 "<msg>"         # 타겟에 메시지 전달
$ dmsetup wait <name> <event_nr>           # 이벤트 대기
$ dmsetup targets                          # 등록된 타겟 유형 목록

# ===== 실전 예: 온라인 테이블 교체 =====
# 기존: sda에 매핑 → 새로: sdb에 매핑
$ dmsetup suspend my_dev
$ dmsetup reload my_dev --table "0 2097152 linear /dev/sdb 0"
$ dmsetup resume my_dev
# I/O 중단 없이 매핑 대상 변경 완료

테이블 형식

# DM 테이블 라인 형식:
# <시작섹터> <길이(섹터)> <타겟타입> <타겟별 인자...>
#
# 예시:
0 2097152 linear /dev/sda 0
# 섹터 0부터 2097152개 섹터(1GB)를 /dev/sda의 섹터 0에 선형 매핑

0 1048576 crypt aes-xts-plain64 <key_hex> 0 /dev/sda 0
# AES-XTS 암호화 매핑

0 2097152 striped 2 128 /dev/sda 0 /dev/sdb 0
# 2-way 스트라이프, 128 섹터 청크

0 2097152 thin-pool /dev/mapper/meta /dev/mapper/data 128 0
# thin-pool 타겟

디바이스 트리 시각화

# DM 디바이스 의존 트리 확인
$ dmsetup ls --tree
my_vg-root (253:0)
 └─ (8:1)

my_vg-home (253:1)
 ├─ (8:1)
 └─ (8:17)

cryptroot (253:2)
 └─ my_vg-root (253:0)
     └─ (8:1)

커널 내부 구현

I/O 경로: bio 리매핑

DM 디바이스에 bio가 제출되면 다음 경로를 거칩니다:

  1. dm_submit_bio(): DM의 submit_bio 콜백. RCU로 현재 dm_table을 참조
  2. __split_and_process_bio(): bio가 여러 타겟에 걸치면 분할
  3. __map_bio(): 각 타겟의 map() 함수 호출
  4. 타겟이 DM_MAPIO_REMAPPED 반환 → submit_bio_noacct()로 하위 디바이스에 재제출
  5. 완료 시 clone_endio() → 원래 bio의 bi_end_io 호출
/* drivers/md/dm.c — 핵심 I/O 경로 (간략화) */
static void dm_submit_bio(struct bio *bio)
{
    struct mapped_device *md = bio->bi_bdev->bd_disk->private_data;
    struct dm_table *map;

    rcu_read_lock();
    map = rcu_dereference(md->map);
    if (unlikely(!map)) {
        rcu_read_unlock();
        bio_io_error(bio);
        return;
    }

    __split_and_process_bio(md, map, bio);
    rcu_read_unlock();
}

static void __map_bio(struct dm_target_io *tio)
{
    struct dm_target *ti = tio->ti;
    struct bio *clone = &tio->clone;
    int r;

    r = ti->type->map(ti, clone);  /* 타겟의 map() 콜백 호출 */

    switch (r) {
    case DM_MAPIO_SUBMITTED:
        break;  /* 타겟이 직접 제출함 */
    case DM_MAPIO_REMAPPED:
        submit_bio_noacct(clone);  /* 리매핑 후 재제출 */
        break;
    case DM_MAPIO_KILL:
    case DM_MAPIO_REQUEUE:
        /* 에러 또는 재큐잉 처리 */
        break;
    }
}

DM ioctl 인터페이스

사용자 공간은 /dev/mapper/control (misc device, major 10, minor 236)을 통해 DM을 제어합니다. ioctl() 시스템 콜로 struct dm_ioctl을 전달합니다.

/* include/uapi/linux/dm-ioctl.h — 주요 ioctl 명령 */
#define DM_DEV_CREATE     _IOWR(DM_IOCTL, DM_DEV_CREATE_CMD, ...)
#define DM_DEV_REMOVE     _IOWR(DM_IOCTL, DM_DEV_REMOVE_CMD, ...)
#define DM_TABLE_LOAD     _IOWR(DM_IOCTL, DM_TABLE_LOAD_CMD, ...)
#define DM_DEV_SUSPEND    _IOWR(DM_IOCTL, DM_DEV_SUSPEND_CMD, ...)
#define DM_TABLE_STATUS   _IOWR(DM_IOCTL, DM_TABLE_STATUS_CMD, ...)

/* DM 디바이스 생성 시퀀스:
 * 1. DM_DEV_CREATE    — 빈 mapped_device 생성
 * 2. DM_TABLE_LOAD    — 매핑 테이블 로드 (inactive table)
 * 3. DM_DEV_SUSPEND   — resume 플래그 설정하여 활성화
 *
 * 테이블 교체:
 * 1. DM_TABLE_LOAD    — 새 테이블을 inactive로 로드
 * 2. DM_DEV_SUSPEND   — suspend (기존 I/O 완료 대기)
 * 3. DM_DEV_SUSPEND   — resume (새 테이블 활성화)
 */

struct dm_ioctl {
    __u32   version[3];   /* ioctl 프로토콜 버전 */
    __u32   data_size;    /* 전체 버퍼 크기 */
    __u32   data_start;   /* 데이터 시작 오프셋 */
    __u32   target_count; /* 타겟 수 */
    __s32   open_count;   /* 열린 참조 수 */
    __u32   flags;        /* DM_*_FLAG */
    __u32   event_nr;     /* 이벤트 번호 */
    __u32   dev;          /* major:minor */
    char    name[128];   /* 디바이스 이름 */
    char    uuid[129];   /* UUID */
    /* ... */
};

Suspend/Resume 메커니즘

DM의 suspend/resume은 테이블 원자적 교체의 핵심입니다:

/* suspend 과정 (간략화) */
static int dm_suspend(struct mapped_device *md, unsigned int suspend_flags)
{
    /* 1. DMF_BLOCK_IO_FOR_SUSPEND 설정 → 새 bio를 보류 */
    set_bit(DMF_BLOCK_IO_FOR_SUSPEND, &md->flags);

    /* 2. 진행 중인 I/O 완료 대기 */
    dm_wait_for_completion(md, TASK_INTERRUPTIBLE);

    /* 3. 타겟의 postsuspend 콜백 호출 */
    dm_table_postsuspend_targets(map);

    return 0;
}

/* resume 과정 (간략화) */
static int dm_resume(struct mapped_device *md)
{
    struct dm_table *map = md->new_map;  /* inactive → active */

    /* 1. 새 테이블을 active로 교체 (RCU) */
    rcu_assign_pointer(md->map, map);
    md->new_map = NULL;

    /* 2. 타겟의 resume 콜백 호출 */
    dm_table_resume_targets(map);

    /* 3. DMF_BLOCK_IO_FOR_SUSPEND 해제 → 보류된 bio 재제출 */
    clear_bit(DMF_BLOCK_IO_FOR_SUSPEND, &md->flags);

    return 0;
}

DM 스택킹

DM 디바이스는 다른 DM 디바이스 위에 쌓을 수 있습니다. 실제 프로덕션에서 흔히 볼 수 있는 스택:

/* 일반적인 DM 스택 예:
 *
 *   /dev/mapper/vg-root          ← LVM LV (dm-linear)
 *        │
 *   /dev/mapper/cryptroot        ← LUKS 암호화 (dm-crypt)
 *        │
 *   /dev/mapper/vg-crypt_data    ← LVM LV, 캐시 적용 (dm-cache)
 *        │
 *   /dev/md0                     ← RAID 5 (md)
 *      │  │  │
 *   sda  sdb  sdc                ← 물리 디스크
 */

# 실제 스택 확인
$ lsblk
sda              8:0    disk   1T
├─sda1           8:1    part   1T
│ └─md0          9:0    raid5  2T
│   └─vg-data  253:0    lvm    2T
│     └─cryptdata 253:1 crypt  2T
sdb              8:16   disk   1T
├─sdb1           8:17   part   1T
│ └─md0          9:0    raid5  2T
sdc              8:32   disk   1T
├─sdc1           8:33   part   1T
│ └─md0          9:0    raid5  2T

성능 튜닝과 디버깅

DM 성능 파라미터

파라미터경로 / 설정설명
nr_requests/sys/block/dm-*/queue/nr_requests최대 요청 큐 깊이
read_ahead_kb/sys/block/dm-*/queue/read_ahead_kb미리 읽기 크기
scheduler/sys/block/dm-*/queue/schedulerDM 디바이스의 I/O 스케줄러 (보통 none)
max_sectors_kb/sys/block/dm-*/queue/max_sectors_kb최대 I/O 크기
dm-crypt workqueue--perf-no_read_workqueue읽기 워크큐 바이패스
dm-thin block sizethin-pool 생성 시 지정씬 풀 블록 크기 (64KB~1MB)

dm-crypt 성능 최적화

# 1. 워크큐 바이패스 (커널 5.9+)
$ cryptsetup open --perf-no_read_workqueue --perf-no_write_workqueue \
    --perf-submit_from_crypt_cpus /dev/sda2 cryptroot

# 2. 하드웨어 암호화 가속 확인
$ grep -m1 aes /proc/cpuinfo
flags : ... aes ...
# AES-NI 지원 확인

# 3. 암호화 벤치마크
$ cryptsetup benchmark
# PBKDF2-sha256    1234567 iterations per second for 256-bit key
# aes-xts   512b   4500.0 MiB/s   4600.0 MiB/s
# aes-xts   256b   5200.0 MiB/s   5300.0 MiB/s

# 4. I/O 스케줄러: DM 디바이스는 none 권장 (하위 디바이스에서 스케줄링)
$ echo none > /sys/block/dm-0/queue/scheduler

dm-thin 모니터링

# thin-pool 사용량 모니터링 스크립트
$ dmsetup status my_vg-thin_pool
0 209715200 thin-pool 1 42/524288 150234/209715200 - rw no_discard_passdown
#                      ^tx ^meta            ^data

# 사용률 계산
# 메타데이터: 42/524288 = 0.008%
# 데이터: 150234/209715200 = 0.07%

# LVM thin 자동 확장 설정 (/etc/lvm/lvm.conf)
# thin_pool_autoextend_threshold = 70  (70% 사용 시)
# thin_pool_autoextend_percent = 20    (20% 확장)

# dmeventd 데몬 확인 (자동 확장 트리거)
$ systemctl status dm-event.service

디버깅 기법

# 1. DM 커널 로그 확인
$ dmesg | grep -i "device-mapper\|dm-"

# 2. DM 디바이스 상세 정보
$ dmsetup info -c
Name         Maj Min Stat Open Targ Event UUID
my_vg-root   253   0 L--w    1    1      0 LVM-...
cryptroot    253   1 L--w    1    1      0 CRYPT-...

# 3. bio 트레이싱 (BPF 기반)
$ bpftrace -e 'kprobe:dm_submit_bio {
    @[comm] = count();
}'

# 4. dm-crypt I/O 지연 측정
$ bpftrace -e '
kprobe:crypt_endio { @start[arg0] = nsecs; }
kretprobe:crypt_endio /@start[arg0]/ {
    @lat = hist(nsecs - @start[arg0]);
    delete(@start[arg0]);
}'

# 5. blktrace로 DM 디바이스 추적
$ blktrace -d /dev/dm-0 -o dm_trace
$ blkparse -i dm_trace

# 6. DM 통계 (dmstats)
$ dmstats create --alldevices
$ dmstats print --alldevices
# reads, writes, io_ticks, queue_size 등 세부 통계
$ dmstats delete --alldevices

자주 발생하는 문제

I/O 행(hang) 진단: DM 디바이스에서 I/O가 멈추면 다음을 확인하세요:
  • dmsetup info에서 Suspended 상태 확인 — 의도치 않은 suspend가 원인일 수 있음
  • cat /proc/mdstat으로 MD 리빌드 진행 중인지 확인
  • dm-thin의 경우 풀 공간 고갈 여부 (dmsetup status)
  • dm-crypt 워크큐 병목 (/proc/pressure/io 확인)
  • iostat -x 1로 하위 디바이스의 %util, await 모니터링
DM 디바이스 제거 실패: dmsetup remove 시 "Device or resource busy" 에러가 발생하면, dmsetup info의 Open count가 0인지 확인합니다. 마운트, 스왑, 다른 DM 스택, 또는 udev 규칙이 디바이스를 열고 있을 수 있습니다. dmsetup remove --force는 deferred removal을 사용하며, 마지막 참조가 해제될 때 제거됩니다.

커널 설정 옵션

# DM 관련 커널 설정 (make menuconfig)
# Device Drivers → Multiple devices driver support (RAID and LVM)

CONFIG_BLK_DEV_DM=y              # Device Mapper 코어
CONFIG_DM_CRYPT=m                # dm-crypt
CONFIG_DM_SNAPSHOT=m             # dm-snapshot
CONFIG_DM_THIN_PROVISIONING=m    # dm-thin
CONFIG_DM_CACHE=m                # dm-cache
CONFIG_DM_WRITECACHE=m           # dm-writecache
CONFIG_DM_VERITY=y               # dm-verity (부트 검증용이므로 빌트인 권장)
CONFIG_DM_VERITY_FEC=y           # dm-verity FEC
CONFIG_DM_MIRROR=m               # dm-mirror (dm-raid1)
CONFIG_DM_RAID=m                 # dm-raid (LVM RAID)
CONFIG_DM_ZERO=m                 # dm-zero (/dev/zero 유사)
CONFIG_DM_DELAY=m                # dm-delay (I/O 지연 주입, 테스트용)
CONFIG_DM_FLAKEY=m               # dm-flakey (장애 시뮬레이션, 테스트용)

# MD (소프트웨어 RAID)
CONFIG_MD=y                      # MD 코어
CONFIG_BLK_DEV_MD=y              # MD 블록 디바이스
CONFIG_MD_RAID0=m                # RAID 0
CONFIG_MD_RAID1=m                # RAID 1
CONFIG_MD_RAID456=m              # RAID 4/5/6
CONFIG_MD_RAID10=m               # RAID 10
CONFIG_MD_AUTODETECT=y           # 부팅 시 자동 감지

테스트용 DM 타겟

# dm-flakey: 장애 시뮬레이션
# 60초 정상 → 5초 동안 모든 쓰기 실패
$ dmsetup create flakey_test --table \
    "0 2097152 flakey /dev/sda1 0 60 5"
#                            ^dev ^off ^up ^down

# dm-delay: I/O 지연 주입
# 읽기 100ms, 쓰기 200ms 지연
$ dmsetup create delay_test --table \
    "0 2097152 delay /dev/sda1 0 100 /dev/sda1 0 200"
#                        ^dev ^off ^read_ms ^dev ^off ^write_ms

# dm-zero: /dev/zero와 유사한 블록 디바이스 (읽기=0, 쓰기=무시)
$ dmsetup create zero_dev --table "0 2097152 zero"

# dm-error: 모든 I/O를 에러로 반환 (에러 처리 테스트)
$ dmsetup create error_dev --table "0 2097152 error"