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 경로, 성능 튜닝과 디버깅 기법까지 종합적으로 다룹니다.
struct bio, blk-mq, I/O 스케줄러 개념을 이해하고 있다고 가정합니다. 커널 모듈 개발 경험이 있으면 코드 예제를 이해하는 데 도움이 됩니다.
핵심 요약
- Device Mapper — 물리 블록 디바이스 위에 가상 블록 디바이스를 생성하는 프레임워크입니다.
- LVM2 — DM 위에 구현된 논리 볼륨 관리자. 디스크 크기 조정, 스냅샷 등을 제공합니다.
- dm-crypt — 블록 레벨 투명 암호화. LUKS가 이를 활용합니다.
- dm-thin — Thin provisioning. 실제 사용량보다 큰 볼륨을 생성(오버커밋)합니다.
- dm-verity — 블록 무결성 검증. Android의 파티션 검증에 사용됩니다.
단계별 이해
- DM 확인 —
dmsetup ls로 현재 시스템의 DM 디바이스 목록을 확인합니다.lsblk에서TYPE이lvm,crypt인 것이 DM 디바이스입니다. - LVM 기초 — PV(Physical Volume) → VG(Volume Group) → LV(Logical Volume) 계층으로 디스크를 관리합니다.
pvs,vgs,lvs명령어로 각 계층을 확인합니다. - 매핑 테이블 —
dmsetup table로 DM 디바이스의 매핑 규칙을 볼 수 있습니다.각 줄은 "시작 섹터, 길이, 타겟 타입, 타겟 인자"로 구성됩니다.
- 실전 활용 — LVM으로 볼륨 확장(
lvextend), 스냅샷 생성, LUKS로 디스크 암호화 등을 수행합니다.이 모든 기능이 Device Mapper 프레임워크 위에서 동작합니다.
Device Mapper 개요
Device Mapper(DM)는 Linux 커널의 블록 디바이스 가상화 프레임워크입니다. 하나 이상의 물리 블록 디바이스 위에 가상 블록 디바이스를 생성하고, I/O 요청을 변환(매핑)하여 하위 디바이스에 전달합니다. LVM2, LUKS 암호화, dm-verity 무결성 검증, thin provisioning 등 현대 Linux 스토리지 스택의 핵심 기능이 모두 DM 위에 구축되어 있습니다.
핵심 특징
- 모듈러 타겟 아키텍처: 매핑 로직이 타겟 드라이버로 분리되어 새로운 기능을 독립적으로 추가 가능
- 스택 가능: DM 디바이스 위에 또 다른 DM 디바이스를 쌓아 복잡한 스토리지 토폴로지 구성
- 런타임 재구성: 매핑 테이블을 원자적으로 교체하여 온라인 상태에서 스토리지 레이아웃 변경
- 투명한 bio 리매핑: 상위 파일시스템은 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) 등이 추가되었습니다.
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 힌트 (정렬, 제한 등) */
/* ... */
};
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 경로
- 쓰기: bio 수신 → 새 bio 할당(bounce buffer) →
crypt_queue에서 데이터 암호화 → 하위 디바이스에 암호화된 bio 제출 - 읽기: 하위 디바이스에 bio 제출 → 완료 콜백에서
crypt_queue로 복호화 → 원래 bio 완료
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비트로 사용 | 표준 |
essiv | Encrypted Salt-Sector IV | 높음 (워터마크 공격 방지) |
benbi | Big-Endian Narrow Block IV | 특수 용도 |
random | 랜덤 IV (쓰기 전용) | 높음 (읽기 불가) |
dm-thin (Thin Provisioning)
dm-thin은 씬 프로비저닝과 스냅샷을 제공합니다. 실제 물리 공간을 사전에 할당하지 않고, 쓰기 시점에 동적으로 블록을 할당합니다. Copy-on-Write(CoW) 기반 스냅샷도 지원합니다.
핵심 개념
- thin-pool: 물리 블록 풀. 데이터 디바이스(data_dev)와 메타데이터 디바이스(metadata_dev)로 구성
- thin volume: 풀에서 블록을 할당받는 가상 디바이스. 논리 크기가 물리 크기보다 클 수 있음
- 메타데이터: B-tree 기반으로 논리 블록 → 물리 블록 매핑을 관리 (on-disk persistent)
- overprovisioning: 논리 볼륨 합계가 물리 풀보다 커도 됨. 실제 쓰기 시점까지 할당 지연
# 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사용/전체
thin_pool_autoextend_threshold 옵션을 활용할 수 있습니다.
dm-cache / dm-writecache
dm-cache는 느린 HDD 앞에 빠른 SSD를 캐시 계층으로 배치하여 성능을 향상시킵니다. dm-writecache는 쓰기 전용 캐싱에 특화된 타겟입니다.
dm-cache 구조
- origin device: 원본 데이터가 저장되는 느린 디바이스 (HDD)
- cache device: 핫 데이터를 캐시하는 빠른 디바이스 (SSD)
- metadata device: 캐시 매핑과 더티 비트를 저장
- 정책(policy): 캐시 교체 알고리즘 (
smq= Stochastic Multi-Queue, 기본값)
캐시 모드
| 모드 | 읽기 | 쓰기 | 특징 |
|---|---|---|---|
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 ..."
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 커널 — AVB와 Secure 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
# ^사용/전체(청크) ^메타데이터(청크)
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를 할당받아 구성
*/
주요 명령어
# ===== 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 0 | 2 | 100% | 없음 | 스트라이핑, 최대 성능 |
RAID 1 | 2 | 50% | 1디스크 | 미러링, 읽기 성능 향상 |
RAID 5 | 3 | (N-1)/N | 1디스크 | 분산 패리티, 범용 |
RAID 6 | 4 | (N-2)/N | 2디스크 | 이중 패리티, 대용량 스토리지 |
RAID 10 | 4 | 50% | 미러당 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)으로 감싸서 사용합니다. 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가 제출되면 다음 경로를 거칩니다:
dm_submit_bio(): DM의submit_bio콜백. RCU로 현재dm_table을 참조__split_and_process_bio(): bio가 여러 타겟에 걸치면 분할__map_bio(): 각 타겟의map()함수 호출- 타겟이
DM_MAPIO_REMAPPED반환 →submit_bio_noacct()로 하위 디바이스에 재제출 - 완료 시
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/scheduler | DM 디바이스의 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 size | thin-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
자주 발생하는 문제
dmsetup info에서 Suspended 상태 확인 — 의도치 않은 suspend가 원인일 수 있음cat /proc/mdstat으로 MD 리빌드 진행 중인지 확인- dm-thin의 경우 풀 공간 고갈 여부 (
dmsetup status) - dm-crypt 워크큐 병목 (
/proc/pressure/io확인) iostat -x 1로 하위 디바이스의%util,await모니터링
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"