RAM 디스크 (Ramdisk)
리눅스 커널의 RAM 디스크 서브시스템을 심층 분석합니다.
BRD 블록 드라이버(drivers/block/brd.c), ramfs 파일시스템(Filesystem),
레거시 initrd와 현대적 initramfs 부팅 메커니즘,
zram 압축 블록 디바이스, 메모리 백킹 전략, 블록 레이어 통합,
커널 부트 파라미터와 실전 활용 가이드까지 RAM 기반 스토리지의 모든 것을 다룹니다.
/dev/ram0) 또는 메모리 기반 파일시스템(ramfs/tmpfs)으로 동작하며,
부팅 과정(Boot Process)을 이해하려면 부팅 과정도 함께 보시면 좋습니다.
zram은 "작은 글씨로 빽빽하게 적는 것"(압축)으로 더 많은 내용을 담을 수 있습니다.
핵심 요약
- BRD (
/dev/ram0) — 고정 크기 RAM 블록 디바이스. 페이지(Page) 단위로 메모리를 할당하며, 전통적인 블록 디바이스 인터페이스를 제공합니다. - ramfs — 페이지 캐시(Page Cache) 기반 메모리 파일시스템. 크기 제한이 없어 메모리를 모두 소진할 수 있습니다.
- tmpfs — ramfs에 크기 제한과 스왑(Swap) 지원을 추가한 파일시스템.
/tmp,/run등에 널리 사용됩니다. - initrd — 레거시 방식으로 부트로더(Bootloader)가 RAM에 로드한 디스크 이미지를
/dev/ram0에 기록하여 초기 루트 파일시스템으로 사용합니다. - initramfs — cpio 아카이브를 커널이 직접 rootfs(기본 tmpfs, 필요시 ramfs)에 풀어
/init을 실행하는 현대적 방식입니다. - zram — 메모리 내 압축 블록 디바이스. 주로 스왑 파티션으로 사용하여 메모리 효율을 2~3배 높입니다.
단계별 이해
- 형태 먼저 구분
BRD,ramfs,tmpfs,zram이 모두 메모리를 쓰지만 인터페이스와 수명주기가 다르다는 점부터 구분합니다. - 블록 디바이스와 파일시스템 구분
/dev/ram0처럼 블록 레이어를 거치는 경우와,tmpfs처럼 VFS 파일시스템으로 바로 보이는 경우를 나눠서 봅니다. - 부팅 경로 연결
레거시initrd와 현대적initramfs가 부팅 초기에 각각 어디에 풀리고 어떤 역할을 하는지 확인합니다. - 메모리 비용 이해
RAM 디스크는 빠르지만 메모리를 직접 소비하므로, 크기 제한과 압축 여부가 실사용에서 왜 중요한지 확인합니다. - 실전 선택 기준 정리
임시 파일, 초기 루트 파일시스템, 압축 스왑처럼 용도별로 어떤 RAM 기반 스토리지를 선택해야 하는지 정리합니다.
RAM 디스크 개요
RAM 디스크(Ramdisk)는 시스템 메모리(DRAM)의 일부를 디스크처럼 사용하는 기술입니다. 물리적 디스크 대비 수십~수백 배 빠른 I/O 성능을 제공하지만, 전원이 꺼지면 데이터가 소실됩니다. 리눅스 커널에서 RAM 디스크는 크게 네 가지 형태로 존재합니다:
| 유형 | 인터페이스 | 소스 위치 | 핵심 용도 |
|---|---|---|---|
| BRD | 블록 디바이스 (/dev/ram*) | drivers/block/brd.c | 범용 RAM 블록 디바이스, initrd 백엔드 |
| ramfs | VFS 파일시스템 | fs/ramfs/ | rootfs 대체 백엔드, 예제 파일시스템 |
| tmpfs | VFS 파일시스템 | mm/shmem.c | /tmp, /run, /dev/shm, rootfs 기본 백엔드 |
| zram | 블록 디바이스 (/dev/zram*) | drivers/block/zram/ | 압축 스왑, 압축 RAM 디스크 |
역사적 배경
RAM 디스크의 역사는 리눅스 커널 초기부터 시작됩니다:
| 시기 | 사건 | 의의 |
|---|---|---|
| 1991 | Linux 0.01 — 램디스크 미포함 | 초기 커널은 플로피에서 직접 부팅 |
| 1995 | Linux 1.3.x — rd.c RAM 디스크 드라이버 | initrd 지원 시작, 모듈 로딩 전 루트 마운트(Mount) 가능 |
| 2000 | Linux 2.4 — ramfs 도입 | 페이지 캐시 기반 메모리 FS, VFS 예제 코드 |
| 2001 | Linux 2.4.4 — tmpfs(shmem) 도입 | 크기 제한 + 스왑 지원 |
| 2002 | Linux 2.5.x — initramfs 도입 | cpio 기반, BRD 블록 디바이스 불필요 |
| 2009 | Linux 2.6.33 — brd.c 리팩토링 | 기존 rd.c를 현대화, XArray 도입 준비 |
| 2014 | Linux 3.14 — zram mainline | staging에서 승격, drivers/block/zram/ |
| 2019 | Linux 5.1 — zram idle page tracking | writeback/recompress 대상 선별이 더 세밀해짐 |
| 2020 | Linux 5.6 — BRD XArray 전환 | 현재 brd.c 구조의 기반 정착 |
| 2026 | Linux 6.19.11 기준 BRD 상태 | 여전히 submit_bio 직접 경로 유지, 별도 blk-mq request queue 없음 |
RAM 디스크 아키텍처 총괄
리눅스 커널에서 RAM 기반 스토리지는 두 가지 경로로 제공됩니다: 블록 디바이스 계층(BRD, zram)과 VFS 파일시스템 계층(ramfs, tmpfs). 각각의 커널 스택 위치와 데이터 흐름을 살펴봅시다.
BRD 드라이버 (drivers/block/brd.c)
BRD(Block RAM Device)는 리눅스 커널의 전통적인 RAM 디스크 블록 드라이버입니다.
/dev/ram0, /dev/ram1, ... 형태의 블록 디바이스를 생성하며,
요청 시 페이지를 동적으로 할당하여 데이터를 저장합니다.
주요 자료구조
/* drivers/block/brd.c */
struct brd_device {
int brd_number; /* 디바이스 번호 (ram0, ram1, ...) */
struct gendisk *brd_disk; /* gendisk */
struct list_head brd_list; /* 전역 BRD 리스트 연결 */
/* sector >> PAGE_SECTORS_SHIFT → struct page * */
struct xarray brd_pages; /* 페이지 백킹 저장소 */
u64 brd_nr_pages; /* 현재 할당된 페이지 수 */
};
/* 전역 변수 */
static LIST_HEAD(brd_devices); /* 모든 BRD 디바이스 리스트 */
static int rd_nr = CONFIG_BLK_DEV_RAM_COUNT;
unsigned long rd_size = CONFIG_BLK_DEV_RAM_SIZE;
static int max_part = 1;
모듈 파라미터
| 파라미터 | 기본값 | 설명 | 설정 방법 |
|---|---|---|---|
rd_nr | 16 | 생성할 BRD 디바이스 수 | modprobe brd rd_nr=4 |
rd_size | 4096 (KB) | 각 디스크의 최대 크기 | modprobe brd rd_size=65536, 또는 커널 파라미터 ramdisk_size=65536 |
max_part | 1 | 파티션 최대 수 | modprobe brd max_part=15 |
rd_size는 최대 크기입니다. BRD는 실제 쓰기가 발생할 때만 페이지를 할당하므로,
생성 직후에는 메모리를 거의 소비하지 않습니다. 이를 온디맨드(on-demand) 할당이라 합니다.
블록 연산
/* BRD 블록 연산 구조체 */
static const struct block_device_operations brd_fops = {
.owner = THIS_MODULE,
.submit_bio = brd_submit_bio, /* BIO 제출 콜백 (no-queue 방식) */
};
/*
* brd_submit_bio() — 현재 stable BRD의 핵심 경로
* 별도 request_fn이나 blk-mq 큐를 두지 않고 BIO를 직접 소모합니다.
*/
static void brd_submit_bio(struct bio *bio)
{
struct brd_device *brd = bio->bi_bdev->bd_disk->private_data;
if (unlikely(op_is_discard(bio->bi_opf))) {
brd_do_discard(brd, bio->bi_iter.bi_sector, bio->bi_iter.bi_size);
bio_endio(bio);
return;
}
do {
if (!brd_rw_bvec(brd, bio))
return; /* helper가 error/endio 처리 */
} while (bio->bi_iter.bi_size);
bio_endio(bio);
}
페이지 관리
BRD는 XArray(구 Radix Tree)를 사용하여 섹터 번호를 페이지에 매핑(Mapping)합니다. 쓰기 시 처음 접근하는 섹터에 대해서만 페이지를 할당하므로, 메모리 사용이 효율적입니다.
/*
* brd_lookup_page() — 섹터에 해당하는 페이지 조회
*/
static struct page *brd_lookup_page(struct brd_device *brd,
sector_t sector)
{
XA_STATE(xas, &brd->brd_pages, sector >> PAGE_SECTORS_SHIFT);
struct page *page;
rcu_read_lock();
repeat:
page = xas_load(&xas);
if (xas_retry(&xas, page)) {
xas_reset(&xas);
goto repeat;
}
if (page && !get_page_unless_zero(page)) {
xas_reset(&xas);
goto repeat;
}
rcu_read_unlock();
return page; /* NULL이면 해당 섹터에 아직 쓰기 없음 */
}
/*
* brd_insert_page() — 새 페이지 할당 후 XArray에 조건부 삽입
*/
static struct page *brd_insert_page(struct brd_device *brd,
sector_t sector, blk_opf_t opf)
{
gfp_t gfp = (opf & REQ_NOWAIT) ? GFP_NOWAIT : GFP_NOIO;
struct page *page, *ret;
page = alloc_page(gfp | __GFP_ZERO | __GFP_HIGHMEM);
if (!page)
return ERR_PTR(-ENOMEM);
xa_lock(&brd->brd_pages);
ret = __xa_cmpxchg(&brd->brd_pages, sector >> PAGE_SECTORS_SHIFT,
NULL, page, gfp);
if (!ret) {
brd->brd_nr_pages++;
get_page(page);
xa_unlock(&brd->brd_pages);
return page;
}
if (!xa_is_err(ret))
get_page(ret);
xa_unlock(&brd->brd_pages);
put_page(page);
return xa_is_err(ret) ? ERR_PTR(xa_err(ret)) : ret;
}
DISCARD 지원
BRD는 DISCARD(TRIM) 연산을 지원합니다.
DISCARD가 들어오면 해당 섹터 범위의 페이지를 XArray에서 제거하고 메모리를 해제합니다.
이를 통해 fstrim이나 blkdiscard 명령으로 사용하지 않는 영역의 메모리를 회수할 수 있습니다.
/*
* brd_do_discard() — 페이지 해제
*/
static void brd_do_discard(struct brd_device *brd,
sector_t sector, u32 size)
{
sector_t aligned_sector = round_up(sector, PAGE_SECTORS);
sector_t aligned_end = round_down(sector + (size >> SECTOR_SHIFT),
PAGE_SECTORS);
struct page *page;
xa_lock(&brd->brd_pages);
while (aligned_sector < aligned_end && aligned_sector < rd_size * 2) {
page = __xa_erase(&brd->brd_pages,
aligned_sector >> PAGE_SECTORS_SHIFT);
if (page) {
put_page(page);
brd->brd_nr_pages--;
}
aligned_sector += PAGE_SECTORS;
}
xa_unlock(&brd->brd_pages);
}
fstrim /mnt/ramdisk을 실행하면, 실제 메모리가 반환됩니다.
이는 ext4의 DISCARD 지원(-o discard)과 결합하여 자동화할 수도 있습니다.
fstrim/discard와 관련된 이슈와 특성
fstrim과 discard는 블록 디바이스 위의 파일시스템과
메모리 파일시스템 자체를 구분해서 봐야 합니다. 최신 stable 소스 기준으로 BRD는
discard BIO를 직접 처리하지만, ramfs/tmpfs는 블록 디바이스가 아니므로 같은 방식의 TRIM 대상이 아닙니다.
| 유형 | DISCARD 지원 | fstrim 동작 | 메모리 회수(Memory Reclaim) 방식 |
|---|---|---|---|
| brd (Block RAM Disk) | ✓ 지원 | 실제 메모리 반환 | __xa_erase() → put_page() |
| tmpfs | 해당 없음 | 일반적으로 의미 없음 | 파일 삭제/회수 시 shmem 경로로 페이지 정리 |
| ramfs | 해당 없음 | 일반적으로 의미 없음 | VFS 페이지 캐시에만 상주, 별도 trim 경로 없음 |
tmpfs와 ramfs에서의 해석
tmpfs와 ramfs는 각각 mm/shmem.c, fs/ramfs/inode.c로 구현되는
파일시스템입니다. 둘 다 BRD처럼 블록 레이어에 DISCARD BIO를 전달하지 않으므로,
메모리 회수는 fstrim이 아니라 파일 삭제, 페이지 회수, 스왑 아웃 여부로 결정됩니다.
특히 tmpfs는 noswap 여부와 shmem reclaim 경로가 더 중요합니다.
# BRD: 블록 디바이스이므로 fstrim/blkdiscard가 직접 의미가 있음
mkfs.ext4 /dev/ram0
mount /dev/ram0 /mnt/ramdisk
fstrim -v /mnt/ramdisk
# tmpfs: 파일 삭제/회수 자체가 핵심이며 별도 trim 워크플로는 주 경로가 아님
mount -t tmpfs -o size=1g,noswap tmpfs /mnt/tmpfs
rm -rf /mnt/tmpfs/build-cache
BRD (Block RAM Disk) DISCARD 상세
BRD 드라이버는 블록 디바이스이므로 blkdev_issue_discard()를 통해 실제 메모리 회수가 가능합니다.
그러나 다음과 같은 제약이 있습니다:
- 정렬 요구사항: discard 요청은 페이지 경계(4KB)에 정렬되어야 합니다
- 단순화된 구현:
brd_do_discard()는 각 페이지를 개별적으로 처리하므로 대량 discard 시 성능 오버헤드가 있습니다 - 메모리 단편화: 빈번한 할당/해제로 XArray에 단편화가 발생할 수 있습니다
brd_submit_bio() 안에서 discard BIO를 직접 받고,
brd_do_discard()가 XArray 엔트리를 제거하면서 메모리를 회수합니다.
반면 tmpfs/ramfs는 블록 디바이스가 아니므로 같은 의미의 discard 경로가 없습니다.
fstrim vs discard 마운트 옵션
실제 저장장치(SSD 등)에서도 mount -o discard 옵션보단 fstrim을
주기적으로 실행하는 것이 권장됩니다:
- discard 옵션: 파일 삭제 시마다 실시간으로 TRIM을 발생시키므로 SSD 수명에 영향을 줄 수 있음
- fstrim: 주기적으로(예: 주 1회) 대량 TRIM을 수행하여 효율적
# 주기적 fstrim 설정 (systemd 사용)
# /etc/systemd/system/fstrim.timer
[Timer]
OnCalendar=weekly
Persistent=true
# systemctl enable --now fstrim.timer
ramfs 파일시스템
ramfs는 리눅스 커널에서 가장 단순한 파일시스템 중 하나입니다.
페이지 캐시를 직접 데이터 저장소로 사용하며, VFS의 simple_* 헬퍼로 구현됩니다.
커널 소스에서 "VFS 구현 예제"로도 자주 인용됩니다.
ramfs 내부 구현
/* fs/ramfs/inode.c — 핵심 구조 */
static const struct super_operations ramfs_ops = {
.statfs = simple_statfs,
.drop_inode = inode_just_drop,
.show_options = ramfs_show_options,
};
static const struct inode_operations ramfs_dir_inode_operations = {
.create = ramfs_create, /* 파일 생성 */
.lookup = simple_lookup, /* 디렉터리 조회 */
.link = simple_link, /* 하드링크 */
.unlink = simple_unlink, /* 파일 삭제 */
.symlink = ramfs_symlink, /* 심볼릭 링크 */
.mkdir = ramfs_mkdir, /* 디렉터리 생성 */
.rmdir = simple_rmdir, /* 디렉터리 삭제 */
.mknod = ramfs_mknod, /* 특수 파일 생성 */
.rename = simple_rename, /* 이름 변경 */
.tmpfile = ramfs_tmpfile, /* O_TMPFILE 지원 */
};
/* ramfs의 address_space_operations — 페이지 캐시 직접 사용 */
static const struct address_space_operations ramfs_aops = {
.read_folio = simple_read_folio, /* 페이지 읽기 */
.write_begin = simple_write_begin, /* 쓰기 시작 */
.write_end = simple_write_end, /* 쓰기 완료 */
.dirty_folio = noop_dirty_folio, /* dirty 마킹 (no-op) */
};
/*
* 핵심 포인트: ramfs는 writeback 하지 않음
* dirty_folio가 noop이므로 페이지가 dirty로 표시되지 않고,
* 따라서 커널의 writeback 메커니즘이 동작하지 않습니다.
* 데이터는 페이지 캐시에 영원히 남습니다.
*/
/* fs/ramfs/inode.c — ramfs_fill_super(): 슈퍼블록 초기화 */
static int ramfs_fill_super(struct super_block *sb, struct fs_context *fc)
{
struct ramfs_fs_info *fsi = sb->s_fs_info;
struct inode *inode;
/* 슈퍼블록 기본 설정 */
sb->s_maxbytes = MAX_LFS_FILESIZE; /* 최대 파일 크기 */
sb->s_blocksize = PAGE_SIZE; /* 블록 크기 = 페이지 크기 */
sb->s_blocksize_bits = PAGE_SHIFT;
sb->s_magic = RAMFS_MAGIC; /* 0x858458f6 */
sb->s_op = &ramfs_ops;
sb->s_time_gran = 1; /* 1나노초 해상도 */
/* 루트 inode 생성 */
inode = ramfs_get_inode(sb, NULL,
S_IFDIR | fsi->mount_opts.mode, 0);
/* ramfs_get_inode():
* inode = new_inode(sb);
* inode->i_mapping->a_ops = &ramfs_aops; ← 핵심!
* inode->i_atime = inode->i_mtime = inode->i_ctime
* = current_time(inode);
*/
/* 루트 dentry 생성 */
sb->s_root = d_make_root(inode);
if (!sb->s_root)
return -ENOMEM;
return 0;
}
/* init/do_mounts.c — rootfs로서의 ramfs 등록 */
/*
* rootfs는 ramfs의 특수 인스턴스이며,
* CONFIG_TMPFS=y이면 기본적으로 tmpfs를 사용합니다.
* 최신 문서는 VFS의 true root로 nullfs를 두고 그 위에 rootfs를 올린다고 설명합니다.
*/
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.init_fs_context = rootfs_init_fs_context,
/* CONFIG_TMPFS=y → shmem_init_fs_context()
* CONFIG_TMPFS=n → ramfs_init_fs_context() */
};
| address_space_operations 함수 | ramfs 구현 | 동작 |
|---|---|---|
read_folio | simple_read_folio() | folio를 0으로 초기화 후 uptodate 마킹 (디스크 I/O 없음) |
write_begin | simple_write_begin() | grab_cache_page_write_begin()으로 페이지 캐시에 folio 할당/조회 |
write_end | simple_write_end() | 쓰기 완료, inode 크기 갱신, folio를 uptodate 마킹 |
dirty_folio | noop_dirty_folio() | 아무것도 안 함 → writeback 대상에서 제외 → 영구 메모리 상주 |
CONFIG_TMPFS=n인 임베디드 환경에서
initramfs를 풀어놓을 rootfs가 필요하기 때문입니다. 또한 VFS 구현의 최소 참조 예제로서
커널 문서(Documentation/filesystems/ramfs-rootfs-initramfs.rst)에서 공식 권장됩니다.
CONFIG_TMPFS=y인 일반 시스템에서는 rootfs가 기본적으로 tmpfs를 사용하고,
강제로 ramfs를 쓰려면 rootfstype=ramfs를 지정합니다.
마운트와 옵션
# ramfs 마운트 (크기 제한 없음!)
mount -t ramfs ramfs /mnt/ramdisk
# 마운트 옵션
mount -t ramfs -o mode=0755 ramfs /mnt/ramdisk
# ramfs는 size 옵션을 무시합니다 (크기 제한이 없음)
# 아래 명령의 size=100m은 효과 없음
mount -t ramfs -o size=100m ramfs /mnt/ramdisk
ramfs vs tmpfs 핵심 차이
| 속성 | ramfs | tmpfs |
|---|---|---|
| 크기 제한 | 없음 (메모리 전체 소진 가능) | 있음 (size= 옵션, 기본 RAM 50%) |
| 스왑 지원 | 불가 | 가능 (메모리 부족 시 스왑 아웃) |
df 표시 | 항상 0 (사용량 추적 없음) | 정확한 사용량/가용량 표시 |
| 구현 복잡도 | ~200줄 | ~4000줄 (mm/shmem.c) |
| OOM 위험 | 높음 (제한 없으므로) | 낮음 (제한 + 스왑) |
| 용도 | rootfs 내부 백엔드, 개발 테스트 | /tmp, /run, /dev/shm |
| 소스 위치 | fs/ramfs/ | mm/shmem.c |
ramfs는 크기 제한이 없으므로, root 사용자도 실수로 전체 메모리를 소진할 수 있습니다.
실무에서는 항상 tmpfs를 사용하세요. ramfs는 커널 내부(rootfs 백엔드)와 VFS 학습 용도로 남아있습니다.
tmpfs/shmem 개요
tmpfs는 ramfs를 확장한 메모리 기반 파일시스템으로,
내부적으로 shmem(Shared Memory) 인프라를 사용합니다.
ramfs와 달리 크기 제한, 스왑 지원, xattr, POSIX ACL 등 완전한 파일시스템 기능을 제공합니다.
/* mm/shmem.c — shmem_get_folio() 핵심 흐름 */
/*
* shmem_get_folio_gfp() — tmpfs 페이지 조회/할당의 핵심
* 1. XArray에서 folio 조회
* 2. 캐시 히트 → 바로 반환
* 3. swap entry 발견 → shmem_swapin_folio()로 스왑인
* 4. 없음 → 새 folio 할당
*/
static int shmem_get_folio_gfp(struct inode *inode,
pgoff_t index, struct folio **foliop,
enum sgp_type sgp, gfp_t gfp,
struct mm_struct *fault_mm, int *fault_type)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info = SHMEM_I(inode);
struct folio *folio;
repeat:
/* 1. XArray에서 folio 조회 */
folio = filemap_get_folio(mapping, index);
if (!IS_ERR(folio)) {
/* 캐시 히트: folio가 이미 메모리에 있음 */
if (sgp == SGP_WRITE)
folio_set_referenced(folio);
return 0;
}
/* 2. XArray에 swap entry가 있는 경우 → 스왑인 */
folio = shmem_swapin_folio(inode, index, ...);
if (folio)
goto got_folio;
/* 3. 새 folio 할당 */
folio = shmem_alloc_folio(gfp, info, index);
if (!folio)
return -ENOMEM;
/* THP: huge=always이면 2MB folio 할당 시도 */
if (shmem_huge_enabled(inode)) {
folio = shmem_alloc_and_add_folio(mapping, gfp | __GFP_COMP,
index, HPAGE_PMD_ORDER, ...);
}
...
}
/* shmem_swapin_folio() — 스왑에서 folio 복구 */
static int shmem_swapin_folio(struct inode *inode, pgoff_t index, ...)
{
swp_entry_t swap;
/* XArray에서 swap entry 추출 */
swap = radix_to_swp_entry(
xa_load(&inode->i_mapping->i_pages, index));
/* swap 영역에서 데이터 읽기 */
folio = swap_cache_get_folio(swap, NULL, 0);
if (!folio) {
folio = shmem_swapin(swap, gfp, info, index);
/* 디스크/zram에서 4KB 데이터 읽기 */
}
/* XArray에서 swap entry를 folio로 교체 */
xa_store(&mapping->i_pages, index, folio, GFP_KERNEL);
info->swapped--;
return 0;
}
/* shmem_writepage() — 메모리 부족 시 스왑 아웃 */
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct shmem_inode_info *info = SHMEM_I(inode);
swp_entry_t swap;
/* 1. 스왑 슬롯 할당 */
swap = get_swap_page(folio);
if (!swap.val)
return 0; /* 스왑 공간 부족 → 메모리에 유지 */
/* 2. XArray에 swap entry 저장 (shadow) */
xa_store(&mapping->i_pages, folio->index,
swp_to_radix_entry(swap), GFP_ATOMIC);
/* 3. folio를 스왑 캐시로 이동 */
set_page_dirty(page);
add_to_swap_cache(folio, swap, ...);
info->swapped++;
/* → kswapd가 나중에 실제 디스크/zram에 기록 */
return 0;
}
# tmpfs 마운트 — 다양한 옵션
mount -t tmpfs -o size=512m,nr_inodes=10k,mode=1777 tmpfs /tmp
# 주요 마운트 옵션
# size=512m — 최대 크기 (기본: RAM의 50%)
# nr_inodes=10k — 최대 inode 수
# mode=1777 — 루트 디렉터리 권한
# uid=0,gid=0 — 소유자/그룹
# huge=always — Transparent Huge Pages 사용
# noswap — 스왑 비활성화 (5.18+)
# 시스템에서 기본 사용되는 tmpfs
df -h -t tmpfs
# tmpfs 7.8G 1.2G 6.7G 15% /run
# tmpfs 7.8G 0 7.8G 0% /dev/shm
# tmpfs 7.8G 4.0K 7.8G 1% /tmp
| tmpfs 마운트 포인트 | 용도 | 일반적 크기 |
|---|---|---|
/tmp | 임시 파일 저장소 | RAM의 50% |
/run (/var/run) | 런타임 데이터 (PID 파일, 소켓(Socket)) | RAM의 20% |
/dev/shm | POSIX 공유 메모리 (shm_open) | RAM의 50% |
/sys/fs/cgroup | cgroup 파일시스템 (cgroup v1) | 제한 없음 |
/dev | devtmpfs (디바이스 노드) | 제한 없음 |
| 마운트 옵션 | 기본값 | 설명 | 예시 |
|---|---|---|---|
size= | RAM의 50% | 최대 사용 공간 (bytes, k, m, g, %) | size=2g, size=50% |
nr_inodes= | RAM 기반 자동 | 최대 inode 수 | nr_inodes=1m |
mode= | 01777 | 루트 디렉터리 권한 | mode=0755 |
uid= / gid= | 0 | 루트 디렉터리 소유자 | uid=1000,gid=1000 |
huge= | never | THP 정책: never/always/within_size/advise | huge=always |
noswap | (스왑 허용) | 이 tmpfs 인스턴스의 스왑 비활성화 (5.18+) | noswap |
inode32 / inode64 | inode64 | inode 번호 범위 (32비트 호환) | inode32 |
mpol= | default | NUMA 메모리 정책(Memory Policy) | mpol=interleave |
nr_blocks= | size 기반 | 최대 블록 수 (PAGE_SIZE 단위) | nr_blocks=262144 |
# /proc/meminfo에서 tmpfs(shmem) 메모리 사용 확인
grep -E 'Shmem|SwapCached|SwapTotal|SwapFree' /proc/meminfo
# Shmem: 1234568 kB ← tmpfs가 사용하는 총 메모리
# SwapCached: 45678 kB ← 스왑에서 다시 읽어온 페이지
# SwapTotal: 4194304 kB ← 전체 스왑 공간
# SwapFree: 3891200 kB ← 사용 가능한 스왑
# shmem이 스왑 아웃되는 과정 관찰
# 1. tmpfs에 대량 데이터 쓰기
dd if=/dev/urandom of=/tmp/bigfile bs=1M count=2048
# 2. 메모리 압박 유발 → shmem 페이지 스왑 아웃
cat /proc/vmstat | grep pswp
# pswpout 12345 ← 스왑 아웃된 페이지 수
# pswpin 6789 ← 스왑 인 된 페이지 수
# 3. 스왑 아웃된 shmem 페이지 수 확인
cat /proc/meminfo | grep Shmem
# Shmem: 1234568 kB ← 메모리 상주 shmem
# ShmemHugePages: 0 kB ← THP로 할당된 shmem
# ShmemPmdMapped: 0 kB ← PMD로 매핑된 shmem
shmem_writepage()가 호출되면 해당 folio의 데이터는 스왑 영역(Swap Area)(디스크 또는 zram)에 기록되고,
XArray에는 swap_entry가 shadow entry로 남습니다. 다음에 해당 파일 오프셋(Offset)을 읽으면
shmem_swapin_folio()가 스왑에서 데이터를 복구합니다.
noswap 옵션(5.18+)을 사용하면 해당 tmpfs 인스턴스의 페이지는 절대 스왑 아웃되지 않습니다.
initrd (레거시 RAM 디스크)
initrd(Initial RAM Disk)는 커널 2.0부터 사용된 레거시 초기 루트 파일시스템 메커니즘입니다.
부트로더가 압축된 디스크 이미지를 메모리에 로드하면, 커널이 이를 /dev/ram0(BRD)에 기록하고
파일시스템으로 마운트하여 초기 사용자 공간(User Space)을 제공합니다.
initrd 부팅 흐름
커널 코드 분석
/* init/do_mounts_rd.c — initrd 이미지 로딩 */
/*
* rd_load_image() — initrd 이미지를 /dev/ram0에 기록
* 부트로더가 메모리에 로드한 initrd를 BRD 디바이스에 복사
*/
static int __init rd_load_image(char *from)
{
int nblocks, i;
struct file *in_file, *out_file;
/* initrd 원본 열기 (커널이 물리 주소를 /initrd.image로 매핑) */
in_file = filp_open(from, O_RDONLY, 0);
/* /dev/ram0 출력 열기 */
out_file = filp_open("/dev/ram0", O_RDWR, 0);
/* 이미지가 gzip/bzip2 압축인지 확인 */
identify_ramdisk_image(in_file, &decompressor);
/* 블록 단위로 복사 */
for (i = 0; i < nblocks; i++) {
kernel_read(in_file, buf, BLOCK_SIZE, &in_pos);
kernel_write(out_file, buf, BLOCK_SIZE, &out_pos);
}
return 1;
}
/* init/do_mounts.c — initrd 마운트와 전환 */
void __init prepare_namespace(void)
{
if (initrd_load()) {
/* /dev/ram0을 루트로 마운트 성공 */
/* /linuxrc 실행 → 실제 루트 탐색 */
handle_initrd();
}
mount_root(); /* 실제 루트 파일시스템 마운트 */
}
/*
* handle_initrd() 흐름:
* 1. mount /dev/ram0 as root
* 2. execute /linuxrc (사용자 스크립트)
* 3. mount real root device
* 4. pivot_root(new_root, put_old)
* 5. umount old ramdisk, free memory
*/
initrd 이미지 포맷
| 포맷 | 매직 바이트 | 파일시스템 | 커널 지원 |
|---|---|---|---|
| 비압축 | 파일시스템 매직 | ext2, minix, romfs | 항상 |
| gzip 압축 | 1f 8b | ext2 (일반적) | CONFIG_RD_GZIP |
| bzip2 압축 | 42 5a | ext2 | CONFIG_RD_BZIP2 |
| xz 압축 | fd 37 7a 58 5a | ext2 | CONFIG_RD_XZ |
| lz4 압축 | 02 21 4c 18 | ext2 | CONFIG_RD_LZ4 |
| zstd 압축 | 28 b5 2f fd | ext2 | CONFIG_RD_ZSTD |
# initrd 이미지의 매직 바이트 확인 (hexdump)
hexdump -C /boot/initrd.img | head -3
# 00000000 1f 8b 08 00 00 00 00 00 00 03 ... ← gzip 압축 (0x1F 0x8B)
# 압축 해제 후 cpio인지 ext2인지 확인
file /boot/initrd.img
# /boot/initrd.img: gzip compressed data, ...
# gzip 해제 후 내부 확인
zcat /boot/initrd.img | file -
# /dev/stdin: ASCII cpio archive (SVR4 with no CRC) ← initramfs (cpio)
# 레거시 initrd ext2 이미지의 슈퍼블록 매직 확인
# 오프셋 1080 (0x438)에 2바이트 리틀엔디언 = 0xEF53
hexdump -C initrd-legacy.img -s 0x438 -n 2
# 00000438 53 ef ← ext2 매직!
/* init/do_mounts_rd.c — identify_ramdisk_image() 핵심 로직 */
static int __init identify_ramdisk_image(int fd, int start_block,
decompress_fn *decompressor)
{
const int size = 512; /* 첫 512바이트 읽기 */
struct ext2_super_block *ext2sb;
int nblocks = -1;
unsigned char *buf;
buf = kmalloc(size, GFP_KERNEL);
sys_lseek(fd, start_block * BLOCK_SIZE, 0);
sys_read(fd, buf, size);
/* 1단계: 압축 감지 — 매직 바이트로 decompressor 선택 */
*decompressor = decompress_method(buf, size, NULL);
if (*decompressor) {
printk(KERN_NOTICE "RAMDISK: compressed image found "
"at block %d\n", start_block);
nblocks = 0; /* 압축 → 크기 알 수 없음 */
goto done;
}
/* 2단계: ext2 슈퍼블록 확인 (오프셋 1024) */
sys_lseek(fd, start_block * BLOCK_SIZE + 1024, 0);
sys_read(fd, buf, sizeof(struct ext2_super_block));
ext2sb = (struct ext2_super_block *)buf;
if (ext2sb->s_magic == cpu_to_le16(EXT2_SUPER_MAGIC)) {
/* ext2 이미지 → initrd로 처리 */
nblocks = le32_to_cpu(ext2sb->s_blocks_count);
printk(KERN_NOTICE "RAMDISK: ext2 filesystem found "
"at block %d\n", start_block);
goto done;
}
/* 3단계: romfs, minix 등 기타 파일시스템 확인 */
...
done:
kfree(buf);
return nblocks; /* -1 = 인식 불가, 0 = 압축, >0 = 블록 수 */
}
initrd 생성 방법
# 레거시 initrd 이미지 수동 생성 (ext2 기반)
dd if=/dev/zero of=initrd.img bs=1M count=32
mkfs.ext2 -F initrd.img
mkdir /tmp/initrd_mount
mount -o loop initrd.img /tmp/initrd_mount
# 필요한 파일 복사 (BusyBox: busybox.html 참고)
cp -a /bin/busybox /tmp/initrd_mount/bin/
# /linuxrc 스크립트 생성
cat > /tmp/initrd_mount/linuxrc << 'SCRIPT'
#!/bin/busybox sh
/bin/busybox --install -s /bin
mount -t proc proc /proc
mount -t sysfs sysfs /sys
# 드라이버 모듈 로드
insmod /lib/modules/scsi_mod.ko
insmod /lib/modules/sd_mod.ko
# 실제 루트 디바이스 설정
echo 0x0801 > /proc/sys/kernel/real-root-dev
SCRIPT
chmod +x /tmp/initrd_mount/linuxrc
umount /tmp/initrd_mount
gzip initrd.img # → initrd.img.gz
pivot_root가 필요하며,
파일시스템 드라이버가 커널에 내장되어야 합니다. 새로운 시스템에서는 항상 initramfs를 사용하세요.
initramfs (cpio 기반)
initramfs는 커널 2.6에서 도입된 현대적 초기 루트 파일시스템 메커니즘입니다.
newc 또는 crc 형식의 cpio 아카이브를 커널이 직접 rootfs에 풀어놓고
/init을 실행합니다. rootfs는 보통 tmpfs이지만, CONFIG_TMPFS=n이거나
rootfstype=ramfs를 지정한 경우 ramfs가 사용됩니다.
BRD 블록 디바이스가 불필요하고, 메모리 사용도 효율적입니다.
커널 언패킹 코드
/* init/initramfs.c — 핵심 언패킹 코드 */
/*
* populate_rootfs() — initramfs 메인 진입점
* 빌트인 + 외부 initramfs를 순서대로 rootfs에 풀어놓음
*/
static int __init populate_rootfs(void)
{
/* 1. 빌트인 initramfs (항상 존재, 최소 /dev/console 포함) */
err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
/* 2. 외부 initramfs (부트로더가 전달한 경우) */
if (initrd_start) {
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start);
if (!err) {
free_initrd(); /* 원본 메모리 해제 */
return 0;
}
/* cpio가 아니면 레거시 initrd로 폴백 */
...
}
return 0;
}
/*
* unpack_to_rootfs() — cpio 아카이브 언패킹
* newc/crc 포맷(070701/070702) 파싱 → 파일/디렉터리/링크 생성
*/
static int __init unpack_to_rootfs(char *buf, unsigned long len)
{
/* 압축 감지 및 해제 */
decompress_fn decompress = decompress_method(buf, len, &compress_name);
if (decompress) {
/* CONFIG_DECOMPRESS_* 경로로 압축 해제 → cpio 스트림 */
int res = decompress(buf, len, NULL, flush_buffer, ...);
}
/* cpio 엔트리 순회 */
while (!eof) {
if (S_ISDIR(mode))
do_mkdir(name, mode); /* 디렉터리 생성 */
else if (S_ISREG(mode))
do_create(name, mode, body); /* 일반 파일 생성 */
else if (S_ISLNK(mode))
do_symlink(name, target); /* 심볼릭 링크 */
else
do_mknod(name, mode, rdev); /* 디바이스 노드 */
}
return 0;
}
빌트인 initramfs
모든 리눅스 커널에는 최소한의 빌트인 initramfs가 포함됩니다.
CONFIG_INITRAMFS_SOURCE를 설정하면 커널 이미지에 직접 initramfs를 내장할 수 있습니다.
# .config에서 빌트인 initramfs 설정
CONFIG_INITRAMFS_SOURCE="/path/to/initramfs_list"
# initramfs_list 예시 (gen_init_cpio 포맷)
# 형식: file/dir/nod/slink <name> <mode> <uid> <gid> [추가인자]
dir /dev 0755 0 0
nod /dev/console 0600 0 0 c 5 1
nod /dev/null 0666 0 0 c 1 3
dir /bin 0755 0 0
file /init /path/to/init 0755 0 0
file /bin/busybox /path/to/busybox 0755 0 0
slink /bin/sh /bin/busybox 0777 0 0
# 또는 디렉터리를 직접 지정
CONFIG_INITRAMFS_SOURCE="/my/initramfs/root"
# 빌드 시 커널 이미지에 포함됨
make bzImage # vmlinuz 안에 initramfs 포함
외부 initramfs
대부분의 배포판은 외부 initramfs 파일을 별도로 생성하여 부트로더에서 커널과 함께 로드합니다.
# GRUB 설정 예시
menuentry 'Linux' {
linux /vmlinuz root=/dev/sda2 ro
initrd /initramfs.img # 외부 initramfs
}
# initramfs 내용 확인
file /boot/initramfs-$(uname -r).img
# → /boot/initramfs-6.1.0.img: gzip compressed data
# 내용 추출
mkdir /tmp/initramfs && cd /tmp/initramfs
zcat /boot/initramfs-$(uname -r).img | cpio -idmv
# 또는 (microcode + main cpio 결합 형태)
skipcpio /boot/initramfs-$(uname -r).img | zcat | cpio -idmv
# 디렉터리 구조 확인
ls -la
# init -> /usr/lib/systemd/systemd (또는 /init 스크립트)
# bin/ sbin/ lib/ etc/ dev/ proc/ sys/ ...
# microcode + initramfs 결합 이미지 분석
# 최신 배포판은 CPU microcode + 실제 initramfs를 하나의 파일에 결합
# 첫 번째 cpio = microcode, 두 번째 cpio = initramfs
# 크기별 분석
cpio -t < /boot/initramfs-$(uname -r).img 2>/dev/null | head -5
# early_cpio 부분 → kernel/x86/microcode/AuthenticAMD.bin
# initramfs 전체 크기와 구성 파악
lsinitrd /boot/initramfs-$(uname -r).img 2>/dev/null | tail -20
# 또는 Debian/Ubuntu:
lsinitramfs /boot/initrd.img-$(uname -r) 2>/dev/null | wc -l
# → 일반적으로 500~3000개 파일
initramfs 생성
# Debian/Ubuntu: mkinitramfs
mkinitramfs -o /boot/initramfs-$(uname -r).img $(uname -r)
# RHEL/Fedora: dracut
dracut /boot/initramfs-$(uname -r).img $(uname -r)
# dracut 옵션 예시
dracut --force --verbose \
--add "lvm dm crypt" \
--drivers "ahci nvme" \
/boot/initramfs-custom.img $(uname -r)
# 수동 cpio 생성
cd /my/initramfs/root
find . | cpio -o -H newc | gzip > /boot/initramfs-custom.img
# 수동 cpio 생성 (zstd 압축, 더 빠름)
find . | cpio -o -H newc | zstd -19 > /boot/initramfs-custom.img.zst
#!/bin/sh
# === 최소 initramfs /init 스크립트 예시 ===
# initramfs 내 /init으로 저장 (실행 권한 필수)
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# 커널 모듈 로드 (필요시)
modprobe ext4
modprobe nvme
# 실제 루트 마운트
ROOTDEV=$(cat /proc/cmdline | sed 's/.*root=\([^ ]*\).*/\1/')
mount -o ro "$ROOTDEV" /mnt/root
# switch_root: rootfs를 정리하고 새 루트로 전환
exec switch_root /mnt/root /sbin/init
# dracut 모듈 구조 (모듈 방식 initramfs 생성)
# /usr/lib/dracut/modules.d/
# 90lvm/ — LVM 볼륨 활성화
# 90crypt/ — LUKS 암호화 볼륨 해제
# 95rootfs-block/ — 블록 디바이스 루트 마운트
# 99base/ — 기본 init 프레임워크
# dracut으로 특정 모듈만 포함한 최소 initramfs 생성
dracut --no-hostonly --force \
--modules "base rootfs-block" \
--drivers "ext4 nvme" \
/boot/initramfs-minimal.img $(uname -r)
initrd vs initramfs 비교
| 속성 | initrd (레거시) | initramfs (현대) |
|---|---|---|
| 이미지 포맷 | 파일시스템 이미지 (ext2) | cpio 아카이브 |
| 커널 저장소 | /dev/ram0 (BRD 블록 디바이스) | rootfs (기본 tmpfs, 선택적으로 ramfs) |
| 메모리 사용 | 이중 사용 (원본 + ram0 복사) | 단일 사용 (직접 rootfs에 풀기) |
| 루트 전환 | pivot_root() | 보통 switch_root, 필요하면 pivot_root()도 가능 |
| 초기 실행 파일 | /linuxrc | /init |
| 블록 디바이스 필요 | 예 (CONFIG_BLK_DEV_RAM) | 아니오 |
| 파일시스템 드라이버 | 커널 내장 필요 (ext2 등) | 불필요 (tmpfs 자동 지원) |
| 크기 제한 | rd_size 파라미터 | 없음 (가용 메모리까지) |
| 소스 코드 | init/do_mounts_rd.c | init/initramfs.c |
| CONFIG | CONFIG_BLK_DEV_RAM | CONFIG_BLK_DEV_INITRD |
| 현재 상태 | 레거시 (비권장) | 표준 (모든 배포판) |
/linuxrc를 /init으로 변경, ② 사용자 공간 전환 로직을 switch_root 중심으로 재작성하되
필요하면 pivot_root() 직접 호출도 고려,
③ ext2 이미지 대신 find . | cpio -o -H newc | gzip으로 cpio 아카이브 생성,
④ CONFIG_BLK_DEV_RAM 의존성 제거. 대부분의 배포판은 dracut(Fedora/RHEL) 또는
mkinitramfs(Debian/Ubuntu)로 자동 생성합니다.
/* init/initramfs.c — initramfs vs initrd 자동 판별 핵심 코드 */
/*
* populate_rootfs()에서 initramfs와 initrd를 자동 판별:
* 1. __initramfs_start에 빌트인 initramfs가 있으면 먼저 풀기
* 2. initrd_start가 설정되어 있으면:
* a. cpio 매직("070701"/"070702")이면 → initramfs로 풀기
* b. ext2 매직(0xEF53)이면 → /dev/ram0에 복사 (레거시 initrd)
* c. 압축되어 있으면 → 해제 후 재판별
*/
static int __init populate_rootfs(void)
{
/* 빌트인 initramfs 풀기 (항상 존재, 최소 빈 cpio) */
unpack_to_rootfs(__initramfs_start, __initramfs_size);
if (initrd_start) {
/* 외부 initrd/initramfs 처리 */
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start);
if (err) {
/* cpio가 아님 → 레거시 initrd로 처리 */
/* /initrd.image에 저장 후 rd_load_image()로 /dev/ram0 복사 */
clean_rootfs();
populate_initrd_image(initrd_start);
}
free_initrd();
}
return 0;
}
zram 압축 블록 디바이스
zram은 메모리 내 압축 블록 디바이스입니다.
데이터를 쓰면 실시간(Real-time)으로 압축하여 저장하고, 읽을 때 해제하여 반환합니다.
주로 스왑 파티션으로 사용하여 메모리 용량을 실질적으로 2~3배 늘리는 효과를 얻습니다.
Android, Chrome OS, Fedora 등에서 기본 활성화되어 있습니다.
zram 아키텍처
주요 자료구조
/* drivers/block/zram/zram_drv.h */
struct zram {
struct zram_table_entry *table; /* 섹터별 메타데이터 배열 */
struct zs_pool *mem_pool; /* zsmalloc 메모리 풀 */
struct zcomp *comps[ZRAM_MAX_COMPS]; /* 압축 알고리즘 */
struct gendisk *disk; /* 블록 디바이스 */
struct rw_semaphore init_lock; /* 초기화 락 */
/* 통계 */
atomic64_t stats[NR_ZRAM_STAT_ITEM];
u64 disksize; /* 디스크 크기 (비압축 기준) */
/* Writeback 관련 */
struct block_device *backing_dev; /* 백킹 디바이스 */
spinlock_t wb_limit_lock;
unsigned long *bitmap; /* writeback 비트맵 */
...
};
struct zram_table_entry {
union {
unsigned long handle; /* zsmalloc 핸들 (압축 데이터 위치) */
unsigned long element; /* same_element 값 */
};
unsigned int flags; /* ZRAM_SAME, ZRAM_WB, ZRAM_HUGE 등 */
#ifdef CONFIG_ZRAM_TRACK_ENTRY_ACTIME
ktime_t ac_time; /* 접근 시간 (writeback 판단용) */
#endif
};
/* 플래그 비트 */
#define ZRAM_SAME (1 << 0) /* 모든 바이트가 동일 (예: 0으로 채워진 페이지) */
#define ZRAM_WB (1 << 1) /* backing device로 writeback됨 */
#define ZRAM_HUGE (1 << 2) /* 압축이 비효율적 (원본 저장) */
#define ZRAM_IDLE (1 << 3) /* 유휴 상태 (writeback 후보) */
압축 알고리즘
| 알고리즘 | CONFIG | 압축률 | 속도 | 특징 |
|---|---|---|---|---|
lzo-rle | CONFIG_CRYPTO_LZO | 중간 (~2:1) | 빠름 | 커널 기본값, RLE 최적화 |
lz4 | CONFIG_CRYPTO_LZ4 | 낮음 (~1.8:1) | 매우 빠름 | 압축/해제 속도 최우선 |
zstd | CONFIG_CRYPTO_ZSTD | 높음 (~2.8:1) | 보통 | Facebook 개발, 높은 압축률 |
lzo | CONFIG_CRYPTO_LZO | 중간 (~2:1) | 빠름 | 레거시, lzo-rle 권장 |
842 | CONFIG_CRYPTO_842 | 중간 | 보통 | IBM POWER 하드웨어 가속 |
deflate | CONFIG_CRYPTO_DEFLATE | 높음 (~3:1) | 느림 | zlib, CPU 부하 높음 |
# zram 압축 알고리즘 변경
echo lz4 > /sys/block/zram0/comp_algorithm
# 사용 가능한 알고리즘 확인 (현재 선택은 [ ] 표시)
cat /sys/block/zram0/comp_algorithm
# lzo lzo-rle [lz4] zstd 842 deflate
# 다중 압축 스트림 (커널 6.2+)
# 기본(빠른) + 보조(높은 압축률) 이중 압축
echo "algo=zstd priority=1" > /sys/block/zram0/recomp_algorithm
echo type=huge > /sys/block/zram0/recompress # 비효율 페이지 재압축
zram writeback
zram writeback은 유휴(idle) 페이지나 비압축(huge) 페이지를 실제 디스크로 이동하여 메모리를 추가로 확보하는 기능입니다. 커널 4.14에서 도입되었습니다.
# 1. backing device 설정 (초기화 전에)
echo /dev/sdb1 > /sys/block/zram0/backing_dev
# 2. zram 디스크 크기 설정 및 활성화
echo 4G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon /dev/zram0
# 3. 유휴 페이지 마킹
echo all > /sys/block/zram0/idle # 모든 페이지를 idle로 마킹
# 4. writeback 실행
echo "type=idle" > /sys/block/zram0/writeback # idle 페이지 writeback
echo "type=huge" > /sys/block/zram0/writeback # huge(비압축) 페이지 writeback
echo "type=huge_idle" > /sys/block/zram0/writeback # 둘 다
# 5. writeback 제한 (4KB 페이지 단위)
echo 1 > /sys/block/zram0/writeback_limit_enable
echo 131072 > /sys/block/zram0/writeback_limit # 512MB 제한 (131072 * 4KB)
/* drivers/block/zram/zram_drv.c — writeback 핵심 함수 */
static int zram_writeback_page(struct zram *zram, u32 index)
{
struct bio bio;
struct bio_vec bvec;
struct page *page;
unsigned long handle;
int ret;
/* 1. 플래그 확인: IDLE 또는 HUGE만 writeback 대상 */
if (!zram_test_flag(zram, index, ZRAM_IDLE) &&
!zram_test_flag(zram, index, ZRAM_HUGE))
return 0;
/* 이미 writeback 된 페이지는 건너뜀 */
if (zram_test_flag(zram, index, ZRAM_WB))
return 0;
/* 2. backing device의 빈 블록 찾기 (bitmap 검색) */
unsigned long blk_idx;
blk_idx = alloc_block_bdev(zram);
if (!blk_idx)
return -ENOSPC; /* backing 공간 부족 */
/* 3. zsmalloc에서 압축 데이터 읽기 → 해제 → 임시 페이지 */
page = alloc_page(GFP_NOIO);
handle = zram->table[index].handle;
src = zs_map_object(zram->mem_pool, handle, ZS_MM_RO);
zcomp_decompress(zram->comps[ZRAM_PRIMARY_COMP],
src, PAGE_SIZE, page_address(page));
zs_unmap_object(zram->mem_pool, handle);
/* 4. backing device에 페이지 크기(4KB) 기록 */
bio_init(&bio, zram->backing_dev, &bvec, 1,
REQ_OP_WRITE | REQ_SYNC);
bio.bi_iter.bi_sector = blk_idx * (PAGE_SIZE >> 9);
bio_add_page(&bio, page, PAGE_SIZE, 0);
ret = submit_bio_wait(&bio);
/* 5. 메타데이터 갱신 */
if (!ret) {
/* zsmalloc 핸들 해제 → 메모리 반환! */
zs_free(zram->mem_pool, handle);
/* entry를 WB 상태로 전환 */
zram_set_flag(zram, index, ZRAM_WB);
zram_set_element(zram, index, blk_idx);
/* handle → blk_idx (backing device 오프셋) */
atomic64_inc(&zram->stats[ZRAM_WB_COUNT]);
}
__free_page(page);
return ret;
}
/* 커널 6.2+: recompression — huge 페이지를 더 강한 알고리즘으로 재압축 */
/*
* lz4로 1차 압축한 페이지 중 압축률이 낮은(huge) 것을
* zstd로 재압축하여 메모리 절약. writeback 전 시도.
*
* echo "algo=zstd priority=1" > /sys/block/zram0/recomp_algorithm
* echo type=huge > /sys/block/zram0/recompress
*/
static int zram_recompress(struct zram *zram, u32 index,
struct page *page, int *comp_len_p,
u32 prio)
{
/* 1차 압축 데이터를 해제 */
src = zs_map_object(zram->mem_pool, handle, ZS_MM_RO);
zcomp_decompress(zram->comps[ZRAM_PRIMARY_COMP], ...);
/* 보조 알고리즘(zstd 등)으로 재압축 */
ret = zcomp_compress(zram->comps[prio], page, dst, comp_len_p);
if (*comp_len_p < old_comp_len) {
/* 재압축 성공: 새 핸들 할당 후 교체 */
new_handle = zs_malloc(zram->mem_pool, *comp_len_p, ...);
zs_free(zram->mem_pool, old_handle);
/* → 메모리 절약 */
}
return ret;
}
| Backing Device | 장점 | 단점 | 권장 시나리오 |
|---|---|---|---|
| HDD | 저렴, 대용량 | 랜덤 I/O 매우 느림 | 순차 writeback 위주, 읽기 빈도 낮은 경우 |
| SSD (SATA) | 빠른 랜덤 I/O | 쓰기 수명(WAF) | 일반 데스크톱/서버 |
| NVMe SSD | 최고 성능 | 비용 | 고성능 워크로드 |
| 파일 (루프) | 유연한 크기 | 이중 파일시스템 오버헤드(Overhead) | 테스트/개발 환경 |
# writeback 제한(limit) 관리
# writeback_limit은 backing device 사용량 제한 (4KB 페이지 단위)
echo 131072 > /sys/block/zram0/writeback_limit # 512MB 제한 (131072 * 4KB)
echo 1 > /sys/block/zram0/writeback_limit_enable
# backing device 통계 확인
cat /sys/block/zram0/bd_stat
# bd_count bd_reads bd_writes
# 12345 6789 12345
# writeback된 페이지 수 / backing에서 읽은 횟수 / 쓴 횟수
writeback_limit을 반드시 설정하세요.
zsmalloc 내부 구조
zsmalloc은 zram 전용 메모리 할당자(Memory Allocator)로, 가변 크기 압축 데이터를 효율적으로 저장합니다.
일반 slab/slub 할당자는 고정 크기 객체에 최적화되어 있어 가변 크기 압축 데이터에는 내부 단편화(Fragmentation)가 심합니다.
zsmalloc은 여러 물리 페이지를 하나의 zspage로 결합하여 단편화를 최소화합니다.
/* mm/zsmalloc.c — 핵심 API */
/* zs_malloc: 압축 데이터 저장 공간 할당 */
unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp)
{
struct size_class *class;
struct zspage *zspage;
unsigned long handle;
/* 1. 크기 클래스 결정 (16바이트 단위 올림) */
class = pool->size_class[get_size_class_index(size)];
/* 2. ALMOST_EMPTY에서 빈 슬롯이 있는 zspage 검색 */
zspage = find_get_zspage(class);
if (!zspage) {
/* 3. 새 zspage 할당 (class->pages_per_zspage개의 페이지) */
zspage = alloc_zspage(pool, class, gfp);
}
/* 4. 빈 슬롯에서 obj_index 할당 */
obj = obj_malloc(pool, zspage, handle);
/* 5. fullness group 재분류 */
fix_fullness_group(class, zspage);
return handle; /* PFN + obj_index 인코딩 */
}
/* zs_map_object: handle → 가상 주소 매핑 */
void *zs_map_object(struct zs_pool *pool, unsigned long handle,
enum zs_mapmode mm)
{
/* handle에서 PFN, obj_index 디코딩 */
obj_to_location(handle, &page, &obj_idx);
/* 객체가 페이지 경계를 넘는 경우:
* 두 페이지를 연속 가상 주소로 매핑 (kmap) */
if (ZS_HANDLE_SIZE + size > PAGE_SIZE) {
/* cross-page mapping */
area->vm_buf = kmap_local_page(page);
area->vm_buf2 = kmap_local_page(next_page);
memcpy(area->vm_buf, ...); /* 임시 버퍼로 복사 */
}
return area->vm_addr + obj_offset;
}
/* zs_free: 객체 해제 */
void zs_free(struct zs_pool *pool, unsigned long handle)
{
obj_to_location(handle, &page, &obj_idx);
zspage = get_zspage(page);
class = zspage_class(pool, zspage);
obj_free(class->size, handle); /* 프리리스트에 반환 */
/* zspage가 완전히 비었으면 페이지 해제 */
if (get_fullness_group(class, zspage) == ZS_EMPTY) {
free_zspage(pool, class, zspage);
}
}
| 크기 범위 | size_class index | pages_per_zspage | objects_per_zspage | 외부 단편화율 |
|---|---|---|---|---|
| 32~48B | 0~1 | 1 | 85~128 | <2% |
| 64~256B | 2~14 | 1 | 16~64 | <5% |
| 272~1024B | 15~62 | 1~2 | 4~15 | <10% |
| 1040~2048B | 63~126 | 2~3 | 3~7 | <15% |
| 2064~3072B | 127~190 | 3~4 | 4~5 | <20% |
| 3088~4096B | 191~254 | 1 (PAGE_SIZE) | 1 | 0% (전체 페이지) |
struct inode 전용 캐시(Cache))에 최적화되어 있습니다.
그러나 zram의 압축 데이터는 32B~4KB까지 크기가 극도로 다양합니다. slab을 사용하면 예를 들어
500B 압축 데이터를 512B 슬랩 객체에 넣어야 하고, 다른 크기의 데이터는 각각 다른 캐시가 필요합니다.
zsmalloc은 16B 단위의 세밀한 크기 클래스와 페이지 경계를 넘는 객체 배치로
내부 단편화를 최대 15%에서 최대 3%로 줄입니다.
zcomp 압축 스트림
zram의 압축/해제는 zcomp(zram compression) 계층을 통해 수행됩니다.
각 CPU에 전용 압축 스트림(zcomp_strm)을 할당하여 잠금 경합(Contention) 없이 병렬 처리합니다.
/* drivers/block/zram/zcomp.c — per-CPU 스트림 관리 */
struct zcomp_strm {
struct crypto_comp *tfm; /* 커널 crypto API 핸들 */
void *buffer; /* 압축 출력 버퍼 (PAGE_SIZE * 2) */
};
struct zcomp {
struct zcomp_strm __percpu *stream; /* per-CPU 스트림 배열 */
const char *name; /* 알고리즘 이름 */
};
/* 현재 CPU의 스트림 획득 (preempt 비활성화) */
struct zcomp_strm *zcomp_stream_get(struct zcomp *comp)
{
return get_cpu_ptr(comp->stream);
/* preempt_disable() → 현재 CPU의 zcomp_strm 반환
* 다른 CPU로 마이그레이션 방지 → 락 불필요 */
}
void zcomp_stream_put(struct zcomp *comp)
{
put_cpu_ptr(comp->stream);
/* preempt_enable() */
}
/* 압축 실행 */
int zcomp_compress(struct zcomp *comp, struct page *page,
void *dst, unsigned int *dst_len)
{
struct zcomp_strm *strm = zcomp_stream_get(comp);
/* 커널 crypto API로 압축 */
*dst_len = PAGE_SIZE * 2; /* 최대 출력 크기 */
ret = crypto_comp_compress(strm->tfm,
page_address(page), PAGE_SIZE, /* 입력: 4KB */
strm->buffer, dst_len); /* 출력: 압축 데이터 */
memcpy(dst, strm->buffer, *dst_len);
zcomp_stream_put(comp);
return ret;
}
| 알고리즘 | 압축 처리량(Throughput) (MB/s) | 해제 처리량 (MB/s) | 압축률 (평균) | 지연(Latency) (us/page) | 권장 환경 |
|---|---|---|---|---|---|
lzo-rle | ~600 | ~900 | 2.0:1 | ~7 | 범용 (커널 기본값) |
lz4 | ~750 | ~1200 | 1.8:1 | ~5 | 지연 민감 (Android, 데스크톱) |
zstd | ~350 | ~700 | 2.8:1 | ~12 | 메모리 제약 (임베디드, 서버) |
lzo | ~550 | ~850 | 2.0:1 | ~8 | 레거시 호환 |
deflate | ~150 | ~400 | 3.0:1 | ~27 | 최대 압축 (CPU 여유 시) |
842 | ~500 (HW) | ~800 (HW) | 2.2:1 | ~6 (HW) | IBM POWER (하드웨어 가속) |
sysfs 인터페이스
| 경로 | R/W | 설명 |
|---|---|---|
/sys/block/zram0/disksize | RW | 디스크 크기 (비압축 기준, 예: 4G) |
/sys/block/zram0/comp_algorithm | RW | 압축 알고리즘 선택 |
/sys/block/zram0/algorithm_params | WO | 압축 알고리즘 파라미터(예: 사전, 레벨) 설정 |
/sys/block/zram0/mem_limit | WO | 최대 메모리 사용량 제한 |
/sys/block/zram0/mem_used_max | WO | mem_used_max 카운터 리셋 |
/sys/block/zram0/mm_stat | R | 메모리 통계 (원본/압축/사용/제한/max/같은/페이지) |
/sys/block/zram0/io_stat | R | I/O 통계 (읽기/쓰기/실패) |
/sys/block/zram0/bd_stat | R | backing device 통계 |
/sys/block/zram0/debug_stat | R | 디버깅용 추가 통계 |
/sys/block/zram0/backing_dev | RW | writeback 대상 블록 디바이스 |
/sys/block/zram0/idle | W | 유휴 페이지 마킹 (all) |
/sys/block/zram0/writeback | W | writeback 실행 (type=idle, type=huge, type=huge_idle, type=incompressible, page_indexes=) |
/sys/block/zram0/writeback_limit | RW | writeback 예산 (4KB 단위) |
/sys/block/zram0/writeback_limit_enable | RW | writeback 예산 제한 on/off |
/sys/block/zram0/writeback_batch_size | RW | 동시 writeback 요청 수 조정 |
/sys/block/zram0/recomp_algorithm | RW | 보조 압축 알고리즘 선택 (CONFIG_ZRAM_MULTI_COMP) |
/sys/block/zram0/recompress | WO | 재압축 실행 (type=idle, type=huge, type=huge_idle, threshold=, max_pages=, algo=, priority=) |
/sys/block/zram0/compact | WO | zsmalloc 컴팩션 강제 실행 |
/sys/block/zram0/reset | W | zram 리셋 (1 기록) |
# mm_stat 해석 (공백 구분, 9개 필드)
cat /sys/block/zram0/mm_stat
# orig_data_size compr_data_size mem_used_total mem_limit mem_used_max
# same_pages pages_compacted huge_pages huge_pages_since
# 예시 해석:
# 2147483648 715827882 734003200 0 891289600 12345 0 567 890
# 원본 2GB → 압축 후 682MB → 실제 메모리 700MB (오버헤드 포함)
# 압축률: 715827882 / 2147483648 ≈ 33% (3:1 압축)
zram swap 설정
# === 기본 zram swap 설정 ===
# 1. 모듈 로드 (num_devices로 디바이스 수 지정)
modprobe zram num_devices=1
# 2. 압축 알고리즘 선택
echo lz4 > /sys/block/zram0/comp_algorithm
# 3. 디스크 크기 설정 (물리 RAM의 50~100%)
echo 4G > /sys/block/zram0/disksize
# 4. 스왑으로 활성화 (높은 우선순위)
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# 5. 확인
swapon --show
# NAME TYPE SIZE USED PRIO
# /dev/zram0 partition 4G 1.2G 100
# === systemd-zram-setup (Fedora/Arch) ===
# /etc/systemd/zram-generator.conf
# [zram0]
# zram-size = ram / 2
# compression-algorithm = zstd
# swap-priority = 100
# === 해제 ===
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset
-p)를 디스크 swap보다 높게 설정하세요.
커널은 높은 우선순위의 swap을 먼저 사용합니다. 일반적으로 zram은 100, 디스크는 1로 설정합니다.
메모리 백킹 메커니즘
RAM 디스크의 네 가지 유형은 각각 다른 메모리 관리 전략을 사용합니다. 이 차이가 성능, 메모리 효율, 기능의 근본적인 차이를 만듭니다.
| 항목 | 페이지 캐시 (ramfs/tmpfs) | XArray (BRD) | zsmalloc (zram) |
|---|---|---|---|
| per-page 메타데이터 | ~64B (struct page) | ~72B (page + xa_node) | ~12B (table_entry) |
| 데이터 저장 비율 | 1:1 (비압축) | 1:1 (비압축) | 2~3:1 (압축) |
| 4GB 데이터의 실제 메모리 | 4GB + 64MB | 4GB + 72MB | ~1.5GB + 12MB |
| 할당 단위 | PAGE_SIZE (4KB/2MB THP) | PAGE_SIZE (4KB) | 16B 단위 가변 |
| 해제 메커니즘 | truncate/unlink | DISCARD/xa_erase | zs_free/reset |
| NUMA 인식 | 예 (mpol= 옵션) | NUMA_NO_NODE | NUMA_NO_NODE |
# NUMA 환경에서 tmpfs 메모리 할당 정책 설정
# mpol=interleave: 모든 NUMA 노드에 균등 분배
mount -t tmpfs -o size=4g,mpol=interleave tmpfs /mnt/balanced
# mpol=bind:0: 특정 노드에만 할당 (지역성 최적화)
mount -t tmpfs -o size=2g,mpol=bind:0 tmpfs /mnt/node0
# mpol=prefer:1: 노드 1 선호, 불가능하면 다른 노드
mount -t tmpfs -o size=2g,mpol=prefer:1 tmpfs /mnt/prefer1
# BRD와 zram은 NUMA 비인식 (NUMA_NO_NODE로 할당)
# → 어떤 NUMA 노드에서 페이지가 할당될지 예측 불가
- 최대 속도 필요 →
tmpfs(블록 레이어 바이패스, VFS 직접 경로) - 블록 디바이스 필요 (ext4/xfs 마운트, 디바이스 매퍼) →
BRD - 메모리 효율 필요 (스왑, 메모리 제약 환경) →
zram - 임시 데이터, 크기 제한 필요 →
tmpfs - 커널 내부/rootfs →
ramfs(자동, 직접 사용하지 않음)
페이지 캐시 기반 (ramfs/tmpfs)
/*
* ramfs: 페이지 캐시(address_space)를 직접 데이터 저장소로 사용
*
* 일반 파일시스템에서 페이지 캐시는 "디스크 데이터의 캐시"이지만,
* ramfs에서는 페이지 캐시가 "유일한 데이터 저장소"입니다.
* writeback이 없으므로 dirty 페이지가 영원히 메모리에 남습니다.
*/
/* 읽기: 페이지 캐시에서 직접 반환 */
static int ramfs_read_folio(struct file *file, struct folio *folio)
{
folio_zero_range(folio, 0, folio_size(folio));
folio_mark_uptodate(folio);
folio_unlock(folio);
return 0;
}
/* 쓰기: 페이지 캐시에 직접 기록 (디스크 writeback 없음) */
static int ramfs_write_begin(struct file *file,
struct address_space *mapping, loff_t pos, unsigned len,
struct page **pagep, void **fsdata)
{
return simple_write_begin(file, mapping, pos, len, pagep, fsdata);
/* simple_write_begin은 grab_cache_page_write_begin() 호출 */
/* → 페이지 캐시에 페이지 할당/조회 */
}
/*
* tmpfs (shmem): 유사하지만 스왑 지원
* - shmem_get_folio()로 페이지 조회/할당
* - 메모리 부족 시 shmem 페이지를 스왑 파일/파티션으로 이동
* - shmem_swapin_folio()로 스왑에서 복구
*/
XArray 기반 (BRD)
/*
* BRD: XArray로 섹터→페이지 매핑 관리
*
* 파일시스템의 페이지 캐시와 독립적으로 동작.
* 블록 디바이스이므로 그 위에 어떤 파일시스템이든 마운트 가능.
*
* 메모리 레이아웃:
* XArray[0] → struct page (섹터 0~7, 4KB)
* XArray[1] → struct page (섹터 8~15, 4KB)
* XArray[2] → NULL (아직 미사용)
* XArray[3] → struct page (섹터 24~31, 4KB)
* ...
*
* 특징:
* - 쓰기 시에만 페이지 할당 (온디맨드)
* - DISCARD로 페이지 해제 가능
* - 읽기 시 NULL이면 0 반환 (할당 없이)
*/
/* BRD 위에 ext4 마운트 예시 */
// dd → brd_submit_bio → brd_insert_page → XArray 저장
// mount → ext4 → BIO → brd_submit_bio → brd_lookup_page → XArray 조회
// fstrim → DISCARD → brd_do_discard → xa_erase → __free_page
zsmalloc 기반 (zram)
/*
* zram: zsmalloc으로 압축 데이터 관리
*
* zsmalloc은 "가변 크기 작은 객체"를 효율적으로 저장하는 할당자.
* 일반 slab/slub은 고정 크기 객체에 최적화되어 있지만,
* 압축 데이터는 크기가 가변적이므로 zsmalloc이 적합.
*
* zsmalloc 내부 동작:
* 1. 크기 클래스(size class) 결정 — 16바이트 단위
* 2. 해당 클래스의 zspage에서 빈 슬롯 할당
* 3. 여러 물리 페이지를 하나의 zspage로 결합 (외부 단편화 최소화)
* 4. handle 반환 (페이지 + 오프셋 인코딩)
*/
/* 쓰기 흐름 */
static int zram_write_page(struct zram *zram, struct page *page, u32 index)
{
unsigned int comp_len;
unsigned long handle;
void *dst, *src;
src = kmap_local_page(page);
/* 1. 전체 0인 페이지 체크 (same_page 최적화) */
if (page_same_filled(src, &element)) {
zram_set_flag(zram, index, ZRAM_SAME);
zram_set_element(zram, index, element);
kunmap_local(src);
return 0; /* 메모리 할당 없이 플래그만 저장 */
}
/* 2. 압축 */
comp_len = PAGE_SIZE; /* 최대 출력 크기 */
zcomp_compress(zram->comps[ZRAM_PRIMARY_COMP],
src, dst, &comp_len);
/* 3. 압축 효과 없으면 원본 저장 (huge) */
if (comp_len >= PAGE_SIZE) {
comp_len = PAGE_SIZE;
zram_set_flag(zram, index, ZRAM_HUGE);
}
/* 4. zsmalloc 할당 + 복사 */
handle = zs_malloc(zram->mem_pool, comp_len, GFP_NOIO);
dst = zs_map_object(zram->mem_pool, handle, ZS_MM_WO);
memcpy(dst, compressed_data, comp_len);
zs_unmap_object(zram->mem_pool, handle);
/* 5. 메타데이터 저장 */
zram->table[index].handle = handle;
zram->table[index].flags = ...;
return 0;
}
블록 레이어 통합
BRD와 zram은 블록 디바이스로서 리눅스 블록 레이어와 통합됩니다. 그러나 실제 하드웨어 디스크와 달리 I/O 스케줄링이 불필요하며, BIO를 직접 처리하는 특수한 방식을 사용합니다.
BIO 처리 흐름
요청 큐 설정
/* BRD: submit_bio 방식 (no-queue) */
static const struct block_device_operations brd_fops = {
.owner = THIS_MODULE,
.submit_bio = brd_submit_bio, /* 직접 BIO 처리, 큐 없음 */
};
/* BRD 디스크 초기화 — brd_alloc() 전체 분석 */
static int brd_alloc(int i)
{
struct brd_device *brd;
struct gendisk *disk;
brd = kzalloc(sizeof(*brd), GFP_KERNEL);
xa_init(&brd->brd_pages); /* XArray 초기화 */
/* gendisk 할당 (NUMA 노드 무관) */
disk = blk_alloc_disk(NUMA_NO_NODE);
/* 디바이스 번호 설정 */
disk->major = RAMDISK_MAJOR; /* major 1 (/dev/ram*) */
disk->first_minor = i * max_part;
disk->minors = max_part;
disk->fops = &brd_fops;
disk->private_data = brd;
snprintf(disk->disk_name, sizeof(disk->disk_name),
"ram%d", i);
/* 큐 속성 설정 */
blk_queue_physical_block_size(disk->queue, PAGE_SIZE);
blk_queue_max_hw_sectors(disk->queue, 1024);
blk_queue_flag_set(QUEUE_FLAG_NONROT, disk->queue);
/* NONROT: 회전 미디어 아님 → 스케줄러 힌트 */
/* DISCARD 지원 — fstrim/TRIM으로 페이지 해제 */
blk_queue_max_discard_sectors(disk->queue, UINT_MAX);
/* 디스크 용량 (512바이트 섹터 단위) */
set_capacity(disk, rd_size * 2);
/* rd_size=4096(KB) → 4096*2=8192 섹터 → 4MB */
brd->brd_disk = disk;
list_add_tail(&brd->brd_list, &brd_devices);
add_disk(disk); /* /dev/ram{i} 등록 */
return 0;
}
/*
* zram: 마찬가지로 submit_bio 방식 사용
* drivers/block/zram/zram_drv.c
*/
static const struct block_device_operations zram_devops = {
.submit_bio = zram_submit_bio,
.open = zram_open,
.owner = THIS_MODULE,
};
| blk_queue 설정 | BRD 값 | zram 값 | 의미 |
|---|---|---|---|
physical_block_size | PAGE_SIZE | PAGE_SIZE | 물리적 최소 I/O 단위 |
logical_block_size | 512 (기본) | PAGE_SIZE | 논리적 최소 I/O 단위 (zram은 4KB 정렬 필수) |
max_hw_sectors | 1024 | 기본값 | 한 번에 처리할 최대 섹터 수 |
io_opt | 설정 안 함 | PAGE_SIZE | 최적 I/O 크기 |
QUEUE_FLAG_NONROT | 설정 | 설정 | 회전 미디어 아님 (SSD 스케줄링 힌트) |
max_discard_sectors | UINT_MAX | UINT_MAX | DISCARD/TRIM 지원 범위 |
submit_bio 콜백(Callback)으로 BIO를 직접 처리합니다(no-queue).
일반 블록 디바이스(NVMe, SCSI 등)는 blk-mq(multi-queue)를 사용하여
하드웨어 큐에 요청을 디스패치(Dispatch)합니다. RAM 디스크는 하드웨어 큐가 없으므로
요청 큐 오버헤드 없이 BIO를 즉시 처리하는 것이 효율적입니다.
최신 안정판 v6.19.11의 drivers/block/brd.c도 여전히
.submit_bio 직접 경로를 사용하며 별도 blk-mq request queue를 두지 않습니다.
I/O 스케줄러
RAM 디스크는 탐색 시간(seek time)이 없으므로 I/O 스케줄링이 불필요합니다.
BRD/zram은 submit_bio 콜백을 사용하여 스케줄러를 완전히 바이패스합니다.
# 스케줄러 확인 (none이면 바이패스)
cat /sys/block/ram0/queue/scheduler
# none
cat /sys/block/zram0/queue/scheduler
# none
# 비교: 실제 디스크
cat /sys/block/sda/queue/scheduler
# [mq-deadline] kyber bfq none
# RAM 디스크의 큐 깊이와 I/O 속성 확인
cat /sys/block/ram0/queue/nr_requests # 128 (기본)
cat /sys/block/ram0/queue/read_ahead_kb # 128 (기본)
cat /sys/block/ram0/queue/rotational # 0 (비회전 미디어)
cat /sys/block/ram0/queue/physical_block_size # 4096 (PAGE_SIZE)
cat /sys/block/ram0/queue/logical_block_size # 512
submit_bio 방식을 사용하여 blk-mq 소프트웨어 큐를
완전히 바이패스합니다. BIO가 submit_bio_noacct()를 통해 직접 드라이버의 submit_bio 콜백으로
전달됩니다. 이는 하드웨어 큐가 없는 RAM 디스크에서 불필요한 큐 오버헤드를 제거합니다.
read_ahead_kb도 의미가 없지만 기본값이 설정됩니다 (무시해도 됨).
RAM 디스크 유형 비교
| 속성 | BRD | ramfs | tmpfs | zram |
|---|---|---|---|---|
| 인터페이스 | 블록 디바이스 | 파일시스템 | 파일시스템 | 블록 디바이스 |
| 디바이스 노드 | /dev/ram0 | 없음 | 없음 | /dev/zram0 |
| 크기 제한 | rd_size | 없음 | size= | disksize |
| 메모리 할당 | 온디맨드 페이지 | 페이지 캐시 | shmem 페이지 | zsmalloc (압축) |
| 압축 | 없음 | 없음 | 없음 | lzo/lz4/zstd |
| 스왑 가능 | 아니오 | 아니오 | 예 | 아니오 (자체가 스왑 대상) |
| DISCARD | 지원 | 해당 없음 | 해당 없음 | 지원 |
| FS 마운트 | ext4, xfs 등 | 자체 FS | 자체 FS | ext4, xfs 등 |
| 주 용도 | initrd, 테스트 | rootfs 내부 | /tmp, /run, /dev/shm | 스왑, 캐시 |
| 메모리 효율 | 1:1 | 1:1 | 1:1 (스왑 시 개선) | 2~3:1 (압축) |
| 소스 | drivers/block/brd.c | fs/ramfs/ | mm/shmem.c | drivers/block/zram/ |
# 모든 RAM 디스크 유형의 상태를 한 번에 확인하는 스크립트
echo "=== BRD ==="
lsblk -d | grep ram 2>/dev/null || echo "(none)"
echo -e "\n=== tmpfs ==="
df -h -t tmpfs 2>/dev/null
echo -e "\n=== zram ==="
zramctl 2>/dev/null || echo "(none)"
echo -e "\n=== Memory Usage ==="
grep -E 'MemTotal|MemAvailable|Shmem|Buffers|SwapTotal' /proc/meminfo
/* 각 RAM 디스크 유형의 file_system_type / block_device_operations 비교 */
/* BRD — 블록 디바이스 */
static const struct block_device_operations brd_fops = {
.submit_bio = brd_submit_bio, /* BIO 직접 처리 */
};
/* zram — 블록 디바이스 */
static const struct block_device_operations zram_devops = {
.submit_bio = zram_submit_bio, /* 압축 + zsmalloc */
.open = zram_open,
};
/* ramfs — 파일시스템 */
static struct file_system_type ramfs_fs_type = {
.name = "ramfs",
.init_fs_context = ramfs_init_fs_context,
.kill_sb = ramfs_kill_sb, /* 슈퍼블록 해제 */
};
/* tmpfs — 파일시스템 */
static struct file_system_type shmem_fs_type = {
.name = "tmpfs",
.init_fs_context = shmem_init_fs_context,
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT | FS_ALLOW_IDMAP, /* 네임스페이스 지원 */
};
CONFIG 옵션 종합
| CONFIG 옵션 | 업스트림 Kconfig 상태 | 설명 |
|---|---|---|
CONFIG_BLK_DEV_RAM | n (tristate) | BRD 드라이버 활성화 |
CONFIG_BLK_DEV_RAM_COUNT | 16 | 기본 RAM 디스크 수 |
CONFIG_BLK_DEV_RAM_SIZE | 4096 | 기본 RAM 디스크 크기 (KB) |
CONFIG_BLK_DEV_INITRD | n | initrd/initramfs 지원 |
CONFIG_INITRAMFS_SOURCE | "" | 빌트인 initramfs 소스 경로 |
CONFIG_INITRAMFS_COMPRESSION_GZIP | y | 빌트인 initramfs gzip 압축 |
CONFIG_INITRAMFS_COMPRESSION_LZ4 | n | 빌트인 initramfs lz4 압축 |
CONFIG_INITRAMFS_COMPRESSION_ZSTD | n | 빌트인 initramfs zstd 압축 |
CONFIG_ZRAM | n (tristate) | zram 모듈 활성화 |
CONFIG_ZRAM_WRITEBACK | n | zram writeback 지원 |
CONFIG_ZRAM_MULTI_COMP | n | zram 다중 압축 알고리즘 |
CONFIG_ZRAM_DEF_COMP | "lzo-rle" | zram 기본 압축 알고리즘 |
CONFIG_ZSMALLOC | n (ZRAM이 select) | zsmalloc 메모리 할당자 (zram 의존) |
CONFIG_TMPFS | n | tmpfs 지원 |
CONFIG_TMPFS_POSIX_ACL | n | tmpfs POSIX ACL 지원 |
CONFIG_TMPFS_XATTR | n | tmpfs 확장 속성(Extended Attribute) 지원 |
CONFIG_RD_GZIP | y | initrd/initramfs gzip 해제 |
CONFIG_RD_LZ4 | y | initrd/initramfs lz4 해제 |
CONFIG_RD_ZSTD | y | initrd/initramfs zstd 해제 |
# 현재 커널의 CONFIG 확인
zcat /proc/config.gz | grep -E 'BLK_DEV_RAM|ZRAM|INITRAMFS|TMPFS'
# 또는
grep -E 'BLK_DEV_RAM|ZRAM|INITRAMFS|TMPFS' /boot/config-$(uname -r)
# 필요한 CONFIG 한 번에 확인하는 스크립트
for opt in BLK_DEV_RAM ZRAM ZRAM_WRITEBACK ZRAM_MULTI_COMP \
ZSMALLOC TMPFS BLK_DEV_INITRD; do
val=$(zcat /proc/config.gz 2>/dev/null | grep "CONFIG_${opt}=" || echo "not set")
printf "%-30s %s\n" "CONFIG_${opt}" "$val"
done
# CONFIG_BLK_DEV_RAM CONFIG_BLK_DEV_RAM=m
# CONFIG_ZRAM CONFIG_ZRAM=m
# CONFIG_ZRAM_WRITEBACK CONFIG_ZRAM_WRITEBACK=y
# ...
CONFIG_BLK_DEV_INITRD=y (initramfs/initrd 지원),
CONFIG_TMPFS=y (tmpfs rootfs와 일반 tmpfs 사용 시 권장, 필수는 아님),
CONFIG_ZRAM=m + CONFIG_ZSMALLOC=m (메모리 효율적 스왑).
CONFIG_TMPFS=n이어도 rootfs는 ramfs로 동작할 수 있으며,
BRD(CONFIG_BLK_DEV_RAM)는 레거시 initrd나 블록형 RAM 디스크가 필요하지 않으면 비활성화해도 됩니다.
커널 부트 파라미터
| 파라미터 | 설명 | 예시 |
|---|---|---|
ramdisk_size= | BRD 디스크 크기 (KB) | ramdisk_size=131072 (128MB) |
initrd=<file> | x86 부트 프로토콜의 bootloader-dependent special option. 일반적인 GRUB에서는 별도 initrd 명령을 더 자주 사용 | initrd=/boot/initrd.img (예: LILO 계열) |
noinitrd | initrd/initramfs 무시 | noinitrd |
root= | 루트 파일시스템 디바이스 | root=/dev/ram0 (BRD 루트) |
rdinit= | initramfs에서 실행할 init 경로 | rdinit=/bin/sh (디버깅용) |
init= | 실제 루트의 init 경로 | init=/sbin/init |
rootfstype= | 실제 루트 파일시스템 유형 또는 rootfs 구현 선택 | rootfstype=ramfs, rootfstype=ext4 |
initramfs_async= | initramfs 비동기 언패킹 | initramfs_async=0 (동기) |
| 플랫폼 | initrd 전달 방식 | 커널 수신 코드 | 물리 주소(Physical Address) 저장 |
|---|---|---|---|
| x86 (BIOS/GRUB) | boot_params.hdr.ramdisk_image | arch/x86/kernel/setup.c | initrd_start / initrd_end |
| x86 (UEFI) | EFI stub이 LoadFile2로 로드 | drivers/firmware/efi/libstub/ | boot_params.hdr.ramdisk_image |
| ARM (DT) | /chosen/linux,initrd-start | arch/arm/kernel/atags_parse.c | phys_initrd_start / phys_initrd_size |
| ARM64 (DT) | /chosen/linux,initrd-start | arch/arm64/kernel/setup.c | 동일 |
| RISC-V (DT) | /chosen/linux,initrd-start | arch/riscv/kernel/setup.c | 동일 |
# GRUB에서 BRD를 루트로 사용 (테스트/복구)
linux /vmlinuz root=/dev/ram0 ramdisk_size=262144 rw
initrd /initrd.img
# initramfs 디버깅: /init 대신 셸 실행
linux /vmlinuz rdinit=/bin/sh
# initramfs 무시하고 직접 루트 마운트
linux /vmlinuz noinitrd root=/dev/sda2 rootfstype=ext4
# 실전 GRUB menuentry: LUKS + LVM + initramfs
menuentry 'Linux (encrypted root)' {
set root='hd0,gpt2'
linux /vmlinuz-6.8.0 \
root=/dev/mapper/vg0-root \
rd.luks.uuid=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
rd.lvm.lv=vg0/root \
rootfstype=ext4 \
resume=/dev/mapper/vg0-swap \
quiet splash
initrd /initramfs-6.8.0.img
}
# Device Tree에서 initrd 전달 (ARM/ARM64)
# chosen {
# linux,initrd-start = <0x48000000>;
# linux,initrd-end = <0x49000000>;
# bootargs = "root=/dev/mmcblk0p2 rootfstype=ext4";
# };
실전 활용 가이드
성능 벤치마크 방법
# === BRD 벤치마크 ===
# 1. BRD 생성 및 파일시스템 구성
modprobe brd rd_nr=1 rd_size=1048576 # 1GB
mkfs.ext4 /dev/ram0
mount /dev/ram0 /mnt/ramdisk
# 2. fio 벤치마크
fio --name=seqwrite --directory=/mnt/ramdisk \
--rw=write --bs=4k --size=512m --numjobs=4 \
--group_reporting --runtime=30
fio --name=randread --directory=/mnt/ramdisk \
--rw=randread --bs=4k --size=512m --numjobs=4 \
--group_reporting --runtime=30
# 3. dd 간단 테스트
dd if=/dev/zero of=/mnt/ramdisk/test bs=1M count=512 oflag=direct
dd if=/mnt/ramdisk/test of=/dev/null bs=1M iflag=direct
# === tmpfs 벤치마크 ===
mount -t tmpfs -o size=1g tmpfs /mnt/tmpfs
fio --name=seqwrite --directory=/mnt/tmpfs \
--rw=write --bs=4k --size=512m --numjobs=4 \
--group_reporting
# === zram 벤치마크 ===
echo 2G > /sys/block/zram0/disksize
mkfs.ext4 /dev/zram0
mount /dev/zram0 /mnt/zram
fio --name=seqwrite --directory=/mnt/zram \
--rw=write --bs=4k --size=1g --numjobs=4 \
--group_reporting
# 압축률 확인
cat /sys/block/zram0/mm_stat | awk '{printf "압축률: %.1f:1\n", $1/$2}'
- tmpfs: 가장 빠름 (VFS→페이지 캐시 직접 경로, 블록 레이어 바이패스)
- BRD (ext4): 약간 느림 (VFS→블록 레이어→BRD→페이지 복사)
- zram (ext4): BRD보다 느림 (압축/해제 CPU 오버헤드), 하지만 메모리 효율 2~3배
- SSD: RAM 대비 10~100배 느림
- IOPS: tmpfs > BRD > zram >> SSD (tmpfs는 수백만 IOPS 가능)
- 지연 시간: tmpfs ~1us, BRD ~2us, zram ~10us (압축 오버헤드), SSD ~100us
- 대역폭(Bandwidth): 모두 DRAM 대역폭에 근접 (10~40 GB/s), zram은 CPU가 병목(Bottleneck)
- fio
--ioengine=sync로 실제 시스템 콜(System Call) 경로 측정 권장
데이터 영속성 처리
# RAM 디스크 데이터는 전원 꺼짐 시 소실
# 주기적으로 디스크에 동기화하는 방법:
# 방법 1: rsync 주기적 동기화
while true; do
rsync -a /mnt/ramdisk/ /backup/ramdisk/ --delete
sleep 300 # 5분마다
done
# 방법 2: systemd timer
# /etc/systemd/system/ramdisk-sync.service
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/rsync -a /mnt/ramdisk/ /backup/ramdisk/ --delete
# 방법 3: 종료 시 자동 백업 (systemd)
# /etc/systemd/system/ramdisk-backup.service
# [Unit]
# Description=Backup ramdisk on shutdown
# DefaultDependencies=no
# Before=shutdown.target reboot.target halt.target
# [Service]
# Type=oneshot
# ExecStart=/usr/bin/rsync -a /mnt/ramdisk/ /backup/ramdisk/
# [Install]
# WantedBy=halt.target reboot.target shutdown.target
# 방법 4: tmpfs + overlayfs (읽기 전용 베이스 + RAM 변경분)
mount -t overlay overlay \
-o lowerdir=/base,upperdir=/mnt/ramdisk/upper,workdir=/mnt/ramdisk/work \
/merged
컨테이너(Container)/Docker 활용
# === Docker에서 tmpfs 활용 ===
# 컨테이너에 tmpfs 마운트 (메모리 제한 포함)
docker run --tmpfs /tmp:rw,size=256m,noexec,nosuid \
--tmpfs /run:rw,size=64m \
-it ubuntu:24.04 bash
# Docker Compose에서 tmpfs 설정
# services:
# app:
# tmpfs:
# - /tmp:size=512m,mode=1777
# - /run:size=64m
# Kubernetes에서 emptyDir tmpfs
# volumes:
# - name: cache
# emptyDir:
# medium: Memory
# sizeLimit: 1Gi
# === 컨테이너에서 zram swap ===
# 호스트에서 zram 설정 (모든 컨테이너가 공유)
echo lz4 > /sys/block/zram0/comp_algorithm
echo 4G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# cgroup v2에서 컨테이너별 메모리/스왑 제한
# memory.max = 2G (메모리 제한)
# memory.swap.max = 4G (zram 스왑 제한)
임베디드/IoT 최적 설정
# === 512MB RAM IoT 디바이스에서 zram 최적 설정 ===
# 물리 RAM의 50%를 zram swap으로 할당
modprobe zram num_devices=1
echo lz4 > /sys/block/zram0/comp_algorithm # 저지연
echo 256M > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# vm 파라미터 튜닝 (임베디드 최적화)
echo 60 > /proc/sys/vm/swappiness # 스왑 적극 사용
echo 100 > /proc/sys/vm/vfs_cache_pressure # 캐시 적극 회수
# === 읽기 전용 rootfs + tmpfs overlay (임베디드) ===
# 부팅 시 실행:
mount -t tmpfs -o size=64m tmpfs /mnt/overlay
mkdir -p /mnt/overlay/upper /mnt/overlay/work
mount -t overlay overlay \
-o lowerdir=/,upperdir=/mnt/overlay/upper,workdir=/mnt/overlay/work \
/mnt/merged
# → 읽기 전용 NAND/eMMC + RAM 변경분 = 쓰기 수명 보호
CI/CD 빌드 가속
# === BRD를 빌드 디렉터리로 활용 ===
# 4GB RAM 디스크 생성
modprobe brd rd_nr=1 rd_size=4194304 # 4GB
mkfs.ext4 -E lazy_itable_init=0 /dev/ram0
mount -o noatime,discard /dev/ram0 /build
# 빌드 실행 (디스크 I/O 제거 → 빌드 30~50% 가속)
cd /build
git clone --depth 1 https://github.com/project/repo .
make -j$(nproc)
# === tmpfs를 빌드 디렉터리로 (더 간단) ===
mount -t tmpfs -o size=4g tmpfs /build
# tmpfs가 BRD보다 빠름 (블록 레이어 바이패스)
| 활용 시나리오 | 권장 유형 | 설정 포인트 | 기대 효과 |
|---|---|---|---|
| Docker /tmp | tmpfs | size=256m,noexec | 임시파일 I/O 가속, 보안 강화 |
| IoT 스왑 | zram (lz4) | RAM 50%, swappiness=60 | 유효 메모리 1.5~2배 확대 |
| CI 빌드 | tmpfs 또는 BRD | size=4g,noatime | 빌드 시간 30~50% 단축 |
| Redis 캐시 | tmpfs | size=2g,noswap | 영속성 불필요 데이터 가속 |
| 임베디드 rootfs | tmpfs + overlayfs | 읽기 전용(Read-Only) base + RAM overlay | NAND 수명 보호 |
| 커널 테스트 | BRD | rd_size=1048576 | 블록 디바이스 인터페이스 테스트 |
동적 크기 조정
# === tmpfs 온라인 리사이즈 ===
# tmpfs는 마운트 상태에서 즉시 크기 변경 가능 (데이터 보존)
mount -o remount,size=2g /tmp
# BRD: 리사이즈 불가 (재생성 필요)
umount /mnt/ramdisk
rmmod brd
modprobe brd rd_nr=1 rd_size=2097152 # 2GB
mkfs.ext4 /dev/ram0
mount /dev/ram0 /mnt/ramdisk
# zram: 리셋 후 재설정 필요
swapoff /dev/zram0
echo 1 > /sys/block/zram0/reset
echo 8G > /sys/block/zram0/disksize
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# zram: hot_add/hot_remove로 동적 추가/제거
cat /sys/class/zram-control/hot_add # 새 zram 디바이스 번호 반환
echo 1 > /sys/class/zram-control/hot_remove # zram1 제거
디버깅(Debugging)과 트러블슈팅
# === BRD 디버깅 ===
# BRD 디바이스 확인
ls -la /dev/ram*
lsblk | grep ram
cat /proc/devices | grep ramdisk # major 1
# BRD 메모리 사용량 (근사값)
for dev in /sys/block/ram*/size; do
name=$(dirname $dev | xargs basename)
sectors=$(cat $dev)
echo "$name: $((sectors * 512 / 1024 / 1024))MB capacity"
done
# === zram 디버깅 ===
# zram 상태 종합 확인
zramctl
# NAME ALGORITHM DISKSIZE DATA COMPR TOTAL STREAMS MOUNTPOINT
# /dev/zram0 lz4 4G 2.1G 712M 734M 8 [SWAP]
# 상세 통계
cat /sys/block/zram0/mm_stat
cat /sys/block/zram0/io_stat
cat /sys/block/zram0/bd_stat
# 디버그 메시지 (커널 로그)
dmesg | grep -i "zram\|brd\|ramdisk\|initramfs\|initrd"
# === initramfs 디버깅 ===
# initramfs 내용 확인
lsinitrd /boot/initramfs-$(uname -r).img # RHEL/Fedora
lsinitramfs /boot/initrd.img-$(uname -r) # Debian/Ubuntu
# initramfs 부팅 문제 디버깅
# 커널 파라미터에 추가:
# rd.break — initramfs의 switch_root 직전에 셸 진입
# rd.debug — dracut 디버그 로그 활성화
# rdinit=/bin/sh — /init 대신 셸 실행
# === 메모리 추적 ===
# /proc/meminfo에서 RAM 디스크 관련 항목
grep -E 'Shmem|Buffers|Cached' /proc/meminfo
# Buffers: BRD가 사용하는 블록 버퍼 캐시
# Shmem: tmpfs가 사용하는 메모리
# Cached: 페이지 캐시 (ramfs 포함)
# slabinfo에서 zsmalloc 확인
grep zspage /proc/slabinfo
# === ftrace로 brd_submit_bio 추적 ===
# 1. function tracer 설정
cd /sys/kernel/debug/tracing
echo function > current_tracer
echo brd_submit_bio > set_ftrace_filter
echo 1 > tracing_on
# 2. BRD에 I/O 발생시키기
dd if=/dev/zero of=/dev/ram0 bs=4k count=100
# 3. 추적 결과 확인
cat trace
# dd-12345 [002] ... brd_submit_bio <-submit_bio_noacct
# dd-12345 [002] ... brd_submit_bio <-submit_bio_noacct
echo 0 > tracing_on
echo > set_ftrace_filter
# === bpftrace: zram 압축률 실시간 모니터링 ===
# 압축 전후 크기 추적 (zram_write_page 프로브)
bpftrace -e '
kprobe:zram_write_page {
@write_count = count();
}
kretprobe:zram_write_page /retval == 0/ {
@success = count();
}
interval:s:5 {
printf("writes: %d, success: %d\n",
@write_count, @success);
clear(@write_count);
clear(@success);
}
'
# zram I/O 레이턴시 히스토그램
bpftrace -e '
kprobe:zram_submit_bio { @start[tid] = nsecs; }
kretprobe:zram_submit_bio /@start[tid]/ {
@latency_us = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}
'
# === /proc/vmstat zram 관련 카운터 해석 ===
grep -E 'pswpin|pswpout|pgfault|pgmajfault' /proc/vmstat
# pswpin 12345 ← 스왑 인 페이지 수 (zram 읽기 포함)
# pswpout 67890 ← 스왑 아웃 페이지 수 (zram 쓰기 포함)
# pgfault 1234567 ← 페이지 폴트 총 수
# pgmajfault 456 ← major 폴트 (디스크/스왑 접근)
# zram이 스왑 대상일 때: pswpin/pswpout이 zram 압축/해제 횟수
# pgmajfault가 높으면 → 메모리 부족, 스왑 thrashing 의심
# === OOM 시나리오: ramfs 메모리 고갈 재현 ===
# 주의: 테스트 환경에서만 실행!
mount -t ramfs ramfs /mnt/test
# ramfs는 크기 제한이 없으므로 아래 명령은 OOM을 유발
# dd if=/dev/zero of=/mnt/test/oom bs=1M count=999999
# → OOM killer 발동 → dmesg에 기록
# === cgroup v2에서 shmem(tmpfs) 메모리 추적 ===
# cgroup v2 memory controller
cat /sys/fs/cgroup/user.slice/memory.stat | grep shmem
# shmem 12345678 ← 이 cgroup의 tmpfs 사용량 (바이트)
# shmem_pmdmapped 0
# 특정 컨테이너의 tmpfs 사용량 모니터링
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.stat | grep shmem
- 1. BRD가 없음:
CONFIG_BLK_DEV_RAM=m이면modprobe brd필요.lsmod | grep brd로 확인. - 2. initramfs 부팅 실패:
/init파일이 없거나 실행 권한이 없는 경우.rdinit=/bin/sh로 셸 진입 후ls -la /init확인. - 3. zram swap 성능 저하: CPU 바운드 워크로드에서 압축 오버헤드.
perf top으로lzo1x_compressCPU 점유율 확인 →lz4로 전환. - 4. ramfs OOM: 크기 제한 없어 메모리 전체 소진 → OOM killer 발동.
tmpfs로 전환하고size=옵션 설정. - 5. zram writeback 실패: backing device 공간 부족 또는 I/O 에러.
bd_stat확인 후writeback_limit조정.
커널 버전별 변화
| 커널 버전 | 변경 사항 | 영향 | 주요 커밋/참고 |
|---|---|---|---|
| 2.4 | ramfs 도입 | 페이지 캐시 기반 메모리 FS | Linus Torvalds, VFS 예제 코드 |
| 2.4.4 | tmpfs(shmem) 도입 | 크기 제한 + 스왑 가능한 메모리 FS | Christoph Rohland |
| 2.5 | initramfs 도입 | cpio 기반, BRD 불필요 | Al Viro, usr/gen_init_cpio.c |
| 2.6.33 | brd.c 리팩토링 | 기존 rd.c를 현대적 블록 레이어 API로 전환 | Nick Piggin, commit 2e2e7da |
| 3.14 | zram staging 졸업 | drivers/block/zram/ 공식 포함 | Minchan Kim, Nitin Gupta |
| 4.7 | zram: same_page 최적화 | 0으로 채워진 페이지 메모리 할당 없이 저장 | commit 43209ea (same_filled) |
| 4.14 | zram: writeback 도입 | 유휴 페이지를 backing device로 이동 | Minchan Kim, CONFIG_ZRAM_WRITEBACK |
| 5.1 | zram: idle 페이지 마킹 | writeback 대상 세밀 제어 | commit cab7a7e |
| 5.6 | BRD: XArray 전환 | Radix Tree에서 XArray로 마이그레이션 | Matthew Wilcox |
| 5.18 | tmpfs: noswap 옵션 | tmpfs 개별 스왑 비활성화 가능 | commit 2c6efe9 |
| 6.1 | zram: multi-comp 기반 마련 | ZRAM_PRIMARY_COMP / ZRAM_SECONDARY_COMP | Sergey Senozhatsky |
| 6.2 | zram: 다중 압축(recompress) | 기본(빠른) + 보조(높은 압축) 이중 전략 | CONFIG_ZRAM_MULTI_COMP |
| 6.8 | zram: per-entry 압축 알고리즘 | 페이지별로 다른 압축 알고리즘 적용 가능 | recompress 고도화 |
| 6.19.11 | BRD 현재 상태 | 여전히 .submit_bio 직접 경로와 XArray 백킹 사용 | drivers/block/brd.c stable 소스 확인 |
| Deprecated/변경 API | 이전 API | 대체 API | 변경 버전 |
|---|---|---|---|
| BRD 페이지 관리 | radix_tree_insert/lookup | xa_store/xa_load (XArray) | 5.6 |
| zram 압축 인터페이스 | zcomp_strm_find/release | zcomp_stream_get/put (per-CPU) | 4.x |
| 블록 디바이스 할당 | alloc_disk() + blk_init_queue() | blk_alloc_disk() | 5.15 |
| BIO 페이지 반복 | bio_for_each_segment() | 동일 (유지) | — |
| initrd 로드 | rd_load_image() | 동일 (레거시 유지) | — |
| shmem folio | shmem_getpage() | shmem_get_folio() | 5.18+ |
- BRD: 최신 stable에서도
submit_bio직접 경로와 XArray 페이지 저장소를 유지합니다. - rootfs/initramfs: rootfs는 기본적으로 tmpfs이며, 필요하면
rootfstype=ramfs로 강제할 수 있습니다. - zram:
algorithm_params,writeback_limit,recomp_algorithm,recompress인터페이스가 현재 sysfs 핵심 축입니다. - tmpfs:
noswap,inode64, quota 등 마운트 옵션이 운영 관점에서 더 중요해졌습니다.
보안 고려사항
RAM 디스크는 메모리 기반이라 물리적 디스크와 다른 보안 특성을 가집니다. 전원 꺼짐 시 데이터가 사라지는 점은 보안에 유리하지만, 메모리 상의 데이터는 cold boot attack이나 메모리 덤프(Dump)로 노출될 수 있습니다.
| 보안 주제 | 위험 | 대응 | 관련 CONFIG |
|---|---|---|---|
| tmpfs 민감 데이터 | /tmp에 저장된 비밀번호/키 파일이 메모리에 평문 존재 | memfd_secret() 사용 (5.14+), 파일 삭제 후 메모리 즉시 해제 | CONFIG_SECRETMEM |
| ramfs OOM 공격 | 크기 제한 없는 ramfs에 대량 쓰기 → 시스템 다운 | 프로덕션에서 ramfs 미사용, tmpfs + size= 제한 | — |
| initramfs 변조 | initramfs에 악성 /init 삽입 → 부팅 시 루트 권한 획득 | IMA/EVM 서명 검증(Signature Verification), Secure Boot 연동 | CONFIG_IMA, CONFIG_EVM |
| zram 데이터 잔존 | swapoff 후 압축 데이터가 zsmalloc에 잔존 | echo 1 > reset으로 전체 초기화, DISCARD on swapoff | — |
| Cold boot attack | 전원 차단 직후 DRAM에서 데이터 추출 | Full memory encryption (AMD SME/SEV, Intel TME) | CONFIG_AMD_MEM_ENCRYPT |
| /dev/shm 권한 | 공유 메모리 영역을 통한 프로세스(Process) 간 데이터 유출 | 적절한 권한 설정 (1777), namespace 격리(Isolation) | — |
# === initramfs 서명 검증 (IMA/EVM) ===
# IMA 정책으로 initramfs 내 파일 무결성 검증
# /etc/ima/ima-policy:
# measure func=KEXEC_INITRAMFS_CHECK
# appraise func=KEXEC_INITRAMFS_CHECK appraise_type=imasig
# initramfs에 IMA 서명 추가
evmctl ima_sign -k /etc/keys/privkey_ima.pem /boot/initramfs.img
# Secure Boot 체인: UEFI → shim → GRUB → vmlinuz (서명) → initramfs (IMA)
# === zram 데이터 안전 삭제 ===
# swapoff만으로는 zsmalloc 메모리가 즉시 해제되지 않을 수 있음
swapoff /dev/zram0
# 완전 초기화 (메모리 + 메타데이터 전부 해제)
echo 1 > /sys/block/zram0/reset
# 민감 환경: zram 사용 후 반드시 reset 수행
# reset → zs_destroy_pool() → 모든 zspage __free_page()
# === tmpfs에서 민감 데이터 처리 ===
# memfd_secret: 커널도 접근 불가한 비밀 메모리 (5.14+)
# 프로그래밍 방식:
# fd = memfd_secret(0);
# ftruncate(fd, 4096);
# ptr = mmap(NULL, 4096, PROT_READ|PROT_WRITE,
# MAP_SHARED, fd, 0);
# → 이 메모리는 /proc/pid/mem으로도 읽을 수 없음
# tmpfs 파일 안전 삭제
shred -u /tmp/secret_file # 0으로 덮어쓴 후 삭제
| CONFIG 보안 옵션 | 기본값 | 설명 |
|---|---|---|
CONFIG_SECRETMEM | y | memfd_secret() 시스템 콜 활성화 (커널 접근 불가 메모리) |
CONFIG_IMA | n/y | Integrity Measurement Architecture — 파일 무결성(Integrity) 측정 |
CONFIG_EVM | n | Extended Verification Module — 파일 메타데이터 무결성 |
CONFIG_AMD_MEM_ENCRYPT | y (AMD) | AMD Secure Memory Encryption — DRAM 암호화(Encryption) |
CONFIG_INTEL_TDX_GUEST | n | Intel Trust Domain Extensions — 메모리 격리 |
CONFIG_INIT_ON_FREE_DEFAULT_ON | n | 페이지 해제 시 0으로 초기화 (성능 영향) |
CONFIG_PAGE_POISONING | n | 해제된 페이지에 독 패턴 기록 (디버그용) |
# === tmpfs 보안 강화 마운트 예시 ===
# /tmp: noexec,nosuid,nodev — 실행 파일/setuid/디바이스 파일 차단
mount -t tmpfs -o size=1g,mode=1777,noexec,nosuid,nodev tmpfs /tmp
# /dev/shm: 공유 메모리 크기 제한
mount -t tmpfs -o size=512m,noexec,nosuid,nodev tmpfs /dev/shm
# fstab 설정
# tmpfs /tmp tmpfs defaults,size=1g,noexec,nosuid,nodev,mode=1777 0 0
# tmpfs /dev/shm tmpfs defaults,size=512m,noexec,nosuid,nodev 0 0
# tmpfs /run tmpfs defaults,size=256m,noexec,nosuid,nodev,mode=755 0 0
/* init/initramfs.c — initramfs 서명 검증 (IMA 연동) */
/*
* CONFIG_IMA_APPRAISE_SIGNED_INIT 활성화 시
* initramfs의 모든 파일이 IMA 서명을 가져야 부팅 가능
*
* 검증 흐름:
* 1. unpack_to_rootfs() → 각 파일을 rootfs에 생성
* 2. 파일 접근 시 → ima_file_check() → 서명 검증
* 3. 서명 불일치 → -EACCES → 부팅 실패
*
* 서명 포함 방법:
* - evmctl ima_sign --key /path/to/key 파일
* - security.ima xattr에 서명 저장
* - cpio 아카이브에 xattr 포함하여 패키징
*/
noexec,nosuid 옵션 추가,
② /dev/shm의 크기를 제한(size=), ③ zram swap 사용 후 반드시 reset 수행,
④ initramfs는 Secure Boot + IMA 서명으로 무결성 보장을 권장합니다.
현대 이미지 생성 도구 — dracut-ng, mkosi, composefs
initramfs/루트 이미지 생성 생태계는 2023~2026년에 걸쳐 세 가지 흐름으로 재편되었습니다. dracut-ng는 전통 dracut의 유지보수 후속, mkosi는 OS 이미지 빌드 파이프라인 통합 도구, composefs는 불변 루트를 위한 재현 가능한 오버레이(Overlay) 파일시스템입니다. 본 섹션은 각 도구의 위치와 상호 관계, 선택 기준을 정리합니다.
dracut-ng
GitHub dracut-ng/dracut-ng 저장소는 2023년 dracut 원 개발자 은퇴 이후 커뮤니티 유지 포크로 출발하여, Fedora 40 / RHEL 10 / Debian trixie가 공식 패키지로 수용했습니다. dracut 명령 인터페이스와 모듈 구조는 호환되며 주요 개선은 다음과 같습니다.
- UKI 기본 지원:
dracut --uefi가 내부에서ukify(systemd)를 호출해 PE 바이너리 생성. - 모듈 재정렬: 레거시 모듈(
btrfs-progs내부 스크립트 등) 제거로 이미지 크기 축소, zstd 기본 압축. - systemd-in-initrd:
-systemd경로가 기본, 레거시 쉘 기반 init은--no-systemd로 옵션화. - 재현 가능 빌드:
SOURCE_DATE_EPOCH반영, 해시(Hash) 안정성 확보(dracut --reproducible).
# dracut-ng: UKI 생성(서명 포함)
$ sudo dracut --force --uefi \
--uefi-stub /usr/lib/systemd/boot/efi/linuxx64.efi.stub \
--kernel-image /boot/vmlinuz-$(uname -r) \
--kernel-cmdline "root=UUID=... ro" \
--kver $(uname -r) \
/boot/efi/EFI/Linux/fedora-$(uname -r).efi
# 재현 가능 빌드(CI/이미지 서명 파이프라인용)
$ sudo dracut --reproducible --no-hostonly -f initramfs.img
# 현재 이미지에 포함된 모듈 리스트
$ lsinitrd /boot/initramfs-$(uname -r).img | less
mkosi — OS 이미지 빌드 파이프라인
mkosi는 systemd 프로젝트가 유지하는 이미지 빌드 도구로, 초기 부팅 이미지(initrd)·UKI·루트 파일시스템·디스크 이미지·OCI 이미지를 동일한 설정 파일(mkosi.conf)로 일괄 생산합니다. systemd v253 이후 개발·배포·릴리스 파이프라인(Fedora bootc, SUSE MicroOS)과 연동됩니다.
# mkosi.conf 최소 예시: Fedora 41 + UKI + dm-verity
[Distribution]
Distribution=fedora
Release=41
[Output]
Format=uki
Output=my-system.efi
# 또는 Format=disk 로 서명된 GPT 이미지
[Content]
Packages=systemd systemd-udev kernel-core dracut
KernelCommandLine=console=ttyS0 root=PARTLABEL=root-x86-64
[Validation]
SecureBoot=yes
SecureBootKey=mkosi.key
SecureBootCertificate=mkosi.crt
Verity=yes
# 빌드 → UKI + verity 루트 + 서명까지 한 번에
$ mkosi build
$ mkosi qemu # 결과물을 QEMU에서 즉시 부팅 테스트
$ mkosi boot # systemd-nspawn으로 컨테이너 부팅
composefs — 재현 가능한 불변 루트
composefs는 EROFS(읽기 전용)에 fs-verity를 결합하고, 실제 파일 내용은 OSTree/CAS 오브젝트 스토어에서 공유하도록 overlayfs를 통해 계층화하는 파일시스템입니다. Linux 6.7에서 오버레이 redirect_dir=origin과 fs-verity 통합 기능이 병합되며 메인라인에 진입했고, Fedora CoreOS / bootc / Red Hat Image Mode의 기본 루트 형식으로 채택되고 있습니다.
/*
* composefs 레이아웃
*
* ┌────────────────────────┐ ← overlayfs 상위(tmpfs / writable)
* │ /etc, /var 쓰기 가능 │
* └────────────┬───────────┘
* │
* ┌────────────▼───────────┐ ← EROFS + fs-verity (불변 메타데이터)
* │ /usr 트리 레이아웃 │ inode와 dentry만 보관
* └────────────┬───────────┘
* │ "lowerdir 리다이렉트"
* ┌────────────▼───────────┐
* │ /sysroot/ostree/repo/ │ ← CAS: SHA-256 내용 주소 객체 저장
* │ objects/XX/YYYY... │
* └────────────────────────┘
*/
/usr 이미지 전체의 해시 하나(EROFS + fs-verity)만 UKI 또는 dm-verity 블록에 기록하면 무결성 검증이 O(1), ② 같은 파일을 여러 부트 엔트리가 CAS로 공유하여 디스크 사용량 감소, ③ rollback/snapshot이 OSTree 커밋 단위로 단순화.
선택 가이드
| 시나리오 | 권장 도구 | 이유 |
|---|---|---|
| 기존 distro의 initramfs 유지·확장 | dracut-ng | 모듈/hostonly 모델 호환, 패키지 관리자 연동 |
| Secure Boot UKI + TPM 봉인을 자동화 | dracut-ng --uefi 또는 mkosi | ukify 통합, pcrsig 자동 생성 |
| CI에서 OS 이미지/디스크/UKI를 한 번에 | mkosi | 선언형 파이프라인, QEMU/nspawn 즉시 부팅 테스트 |
| 불변 루트 / 엣지 배포 / Image Mode | composefs + OSTree | 해시 기반 무결성, CAS 공유, 원자적(Atomic) rollback |
| 임베디드(Buildroot/Yocto) | Buildroot/Yocto 네이티브 | 의존성 트리/레이어 모델 유지, dracut-ng 미대체 |
| zram만 쓰는 디스크리스 노드 | busybox + cpio 수동 구성 | 최소 크기, dracut-ng가 과잉 |
세 도구는 경쟁 관계가 아니라 레이어입니다. 실제 배포 파이프라인은 mkosi가 dracut-ng를 호출해 initramfs를 만들고, 루트 파일시스템을 composefs로 구성한 뒤, ukify로 UKI에 서명하는 식으로 결합됩니다. 자세한 UKI 빌드 흐름은 UEFI → UKI 문서를 참고하세요.
참고자료
- initrd/initramfs 사용 가이드 — Using the initial RAM disk (kernel.org)
- ramfs, rootfs, initramfs 설명 — ramfs, rootfs and initramfs (kernel.org)
- tmpfs 파일시스템 문서 — Tmpfs (kernel.org)
- zram 압축 블록 장치(Block Device) — zram: Compressed RAM-based block devices (kernel.org)
- initramfs 버퍼 포맷 — initramfs buffer format (docs.kernel.org)
- x86 부트 프로토콜 — Linux x86 Boot Protocol (kernel.org)
- initramfs 언팩 소스 — init/initramfs.c (Bootlin Elixir)
- initrd 마운트 소스 — init/do_mounts_rd.c (Bootlin Elixir)
- tmpfs(shmem) 구현 소스 — mm/shmem.c (Bootlin Elixir)
- zram 드라이버 소스 — drivers/block/zram/ (Bootlin Elixir)
- initramfs와 initrd 비교 — Initramfs arrives (LWN.net)
- zram 메모리 압축(Memory Compaction) 최적화 — Improving zram performance (LWN.net)
- tmpfs(5) 맨 페이지 — tmpfs file system (man7.org)
- mkinitramfs(8) 맨 페이지 — mkinitramfs (man7.org)
- cpio(1) 맨 페이지 — cpio archive format (man7.org)
- Daniel P. Bovet, Marco Cesati — Understanding the Linux Kernel, 3rd Edition, O'Reilly (파일시스템 및 메모리 기반 FS 상세)