shmem/tmpfs — 공유 메모리 파일시스템(Filesystem)
shmem(공유 메모리 파일시스템)은 리눅스 커널에서 RAM을 백업 저장소로 사용하는 가상 파일시스템(VFS)입니다.
사용자 공간(User Space)에서는 tmpfs로 마운트(Mount)하여 접근하며, 커널 내부에서는 POSIX 공유 메모리,
System V 공유 메모리, DRM GEM 객체, memfd_create() 등 다양한 서브시스템의 기반으로 활용됩니다.
이 문서는 mm/shmem.c의 내부 구현부터 스왑(Swap) 연동, THP(Transparent Huge Pages),
메모리 압력 대응, 보안 설정, 성능 튜닝까지 전 영역을 깊이 있게 다룹니다.
핵심 요약
- shmem — 커널 내부 이름.
mm/shmem.c에 구현된 RAM 기반 파일시스템 핵심 코드 - tmpfs — 사용자 공간에서 마운트하는 이름.
mount -t tmpfs로 사용 - 스왑 가능 — 일반 RAM 페이지처럼 메모리 부족 시 스왑 아웃 가능 (ramfs와의 핵심 차이)
- 크기 제한 — 마운트 시
size=옵션으로 최대 크기를 제한하여 메모리 고갈 방지 - 다중 사용처 —
/tmp,/dev/shm,/run, memfd, DRM GEM 등 커널 전반에 활용
단계별 이해
- VFS 계층
shmem은 VFS의file_system_type으로 등록됩니다.super_operations,inode_operations,file_operations를 구현하여 일반 파일시스템처럼 동작합니다. - 페이지 캐시(Page Cache) 활용
파일 데이터는 페이지 캐시(page cache)에 저장됩니다. 디스크 블록 장치(Block Device)가 없으므로 페이지 캐시 자체가 유일한 저장소입니다. - 스왑 백엔드
메모리 압력이 발생하면 shmem 페이지는 스왑 공간으로 내보내집니다. 다시 접근하면 스왑에서 읽어와 페이지 캐시에 복원합니다. - 사용자 공간 인터페이스
mount -t tmpfs,shm_open(),shmget()/shmat(),memfd_create()등 다양한 경로로 접근합니다.
shmem/tmpfs 개요
shmem/tmpfs란?
shmem(shared memory filesystem)은 리눅스 커널의 mm/shmem.c에 구현된
RAM 기반 가상 파일시스템입니다. 물리 디스크 없이 시스템 메모리(RAM)를 백업 저장소로 사용하며,
필요 시 스왑 공간을 활용할 수 있습니다.
사용자 공간에서는 tmpfs라는 파일시스템 타입으로 마운트하여 접근합니다.
커널 내부에서는 shmem이라는 이름으로 다양한 서브시스템이 이를 활용합니다.
ramfs와 tmpfs의 차이
| 특성 | ramfs | tmpfs (shmem) |
|---|---|---|
| 크기 제한 | 없음 (메모리 전부 사용 가능) | size= 옵션으로 제한 가능 |
| 스왑 가능 | 불가 | 가능 (메모리 압력 시 스왑 아웃) |
| 메모리 회수(Memory Reclaim) | 불가 (페이지 해제 불가) | 가능 (LRU 기반 회수) |
| 구현 파일 | fs/ramfs/ | mm/shmem.c |
| 주요 용도 | initramfs | /tmp, /dev/shm, /run |
shmem의 역할 범위
shmem은 단순한 /tmp 마운트 포인트를 넘어, 커널 전반에서 핵심적인 역할을 수행합니다.
/* shmem이 사용되는 주요 경로 */
사용자 공간:
mount -t tmpfs → /tmp, /run, /dev/shm
shm_open() / shm_unlink() → POSIX 공유 메모리
shmget() / shmat() → System V 공유 메모리
memfd_create() → 익명 파일 기반 공유 메모리
커널 내부:
DRM GEM (i915, amdgpu) → GPU 버퍼 객체 백업 저장소
IPC 서브시스템 → SysV SHM 세그먼트
zero-copy 전송 → splice/vmsplice 파이프라인
shmem 아키텍처
shmem은 VFS(Virtual File System) 인터페이스를 완전히 구현하는 파일시스템입니다. 일반적인 디스크 파일시스템과 달리 블록 장치 대신 페이지 캐시와 스왑 공간을 백엔드로 사용합니다.
VFS 인터페이스
shmem은 struct file_system_type으로 등록되어 VFS 계층과 통합됩니다.
shmem_fs_type은 tmpfs라는 이름으로 등록되며,
shmem_init()에서 커널 부팅 시 초기화됩니다.
static struct file_system_type shmem_fs_type = {
.owner = THIS_MODULE,
.name = "tmpfs",
.init_fs_context = shmem_init_fs_context,
.parameters = shmem_fs_parameters,
.kill_sb = kill_litter_super,
.fs_flags = FS_USERNS_MOUNT,
};
int __init shmem_init(void)
{
int error;
shmem_init_inodecache();
error = register_filesystem(&shmem_fs_type);
if (error) {
pr_err("Could not register tmpfs\\n");
goto out2;
}
shm_mnt = kern_mount(&shmem_fs_type);
if (IS_ERR(shm_mnt)) {
error = PTR_ERR(shm_mnt);
goto out1;
}
return 0;
out1:
unregister_filesystem(&shmem_fs_type);
out2:
shmem_destroy_inodecache();
shm_mnt = ERR_PTR(error);
return error;
}
코드 설명
-
1-7행
shmem_fs_type구조체(Struct) 정의.name = "tmpfs"로 사용자 공간에서mount -t tmpfs로 마운트 가능합니다.FS_USERNS_MOUNT플래그로 비특권 사용자 네임스페이스(Namespace)에서도 마운트 허용합니다. -
9-10행
shmem_init()는__init매크로(Macro)가 붙은 부팅 시 일회성 함수입니다. -
14행
register_filesystem()으로 VFS에 tmpfs를 등록합니다. -
20행
kern_mount()로 커널 내부 사용을 위한 shmem 인스턴스를 마운트합니다. 이shm_mnt는shmem_file_setup()등에서 사용됩니다.
tmpfs 마운트와 옵션
기본 마운트
# 기본 마운트: 물리 RAM의 50%를 최대 크기로 사용
mount -t tmpfs tmpfs /mnt/tmp
# 크기 제한 설정 (2GB)
mount -t tmpfs -o size=2G tmpfs /mnt/tmp
# /etc/fstab 항목
tmpfs /tmp tmpfs defaults,size=1G,mode=1777 0 0
주요 마운트 옵션
| 옵션 | 기본값 | 설명 |
|---|---|---|
size= | 물리 RAM 50% | 최대 사용 가능 바이트 수. 접미사 k/m/g/% 지원 |
nr_inodes= | 물리 RAM 페이지 수 / 2 | 최대 inode(파일/디렉터리) 수 |
mode= | 1777 | 루트 디렉터리 퍼미션 |
uid= | 0 | 루트 디렉터리 소유자 UID |
gid= | 0 | 루트 디렉터리 소유자 GID |
huge= | never | THP 정책: never, always, within_size, advise |
noexec | off | 실행 파일 실행 금지 |
nosuid | off | setuid/setgid 비트 무시 |
nodev | off | 디바이스 파일 사용 금지 |
inode64 | off | 64비트 inode 번호 사용 |
마운트 옵션 파싱 코드
static const struct fs_parameter_spec shmem_fs_parameters[] = {
fsparam_u32("mode", Opt_mode),
fsparam_string("size", Opt_size),
fsparam_string("nr_inodes", Opt_nr_inodes),
fsparam_u32("uid", Opt_uid),
fsparam_u32("gid", Opt_gid),
fsparam_enum("huge", Opt_huge, shmem_param_enums),
fsparam_flag("noswap", Opt_noswap),
{}
};
런타임 재마운트
# 크기를 4GB로 변경 (마운트 해제 없이)
mount -o remount,size=4G /tmp
# 현재 tmpfs 사용량 확인
df -h /tmp
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 4.0G 128M 3.9G 4% /tmp
# 전체 tmpfs 마운트 목록
mount | grep tmpfs
shmem 내부 구현
shmem_inode_info 구조체
shmem의 inode별 메타데이터는 struct shmem_inode_info에 저장됩니다.
이 구조체는 struct inode에 임베딩되며, SHMEM_I() 매크로로 접근합니다.
struct shmem_inode_info {
spinlock_t lock;
unsigned int seals; /* F_SEAL_* 플래그 */
unsigned long flags;
unsigned long alloced; /* 할당된 데이터 페이지 수 */
unsigned long swapped; /* 스왑 아웃된 페이지 수 */
pgoff_t fallocend; /* fallocate 예약 끝 */
struct list_head shrinklist; /* 슈퍼블록 shrink 목록 */
struct list_head swaplist; /* 스왑 가능 inode 목록 */
struct shared_policy policy; /* NUMA 정책 */
struct simple_xattrs xattrs; /* 확장 속성 목록 */
atomic_t stop_eviction; /* 삭제 방지 카운터 */
struct inode vfs_inode; /* VFS inode (임베딩) */
};
코드 설명
-
3행
seals: memfd에서 사용하는 씰(seal) 플래그.F_SEAL_SHRINK,F_SEAL_GROW,F_SEAL_WRITE,F_SEAL_SEAL등으로 파일 수정을 제한합니다. -
5-6행
alloced: RAM에 있는 페이지 수.swapped: 스왑에 내보낸 페이지 수. 두 값의 합이 파일의 총 데이터 페이지 수입니다. - 8-9행 메모리 회수 경로에서 사용하는 연결 리스트(Linked List). shrinklist는 슈퍼블록(Superblock)의 회수 대상 목록, swaplist는 스왑 가능 inode 목록입니다.
-
13행
VFS inode가 구조체 끝에 임베딩됩니다.
container_of()로shmem_inode_info에서inode로, 또는 그 반대로 변환합니다.
shmem_inode_info 필드별 해설
struct shmem_inode_info의 각 필드가 커널 내부에서 어떻게 활용되는지 상세히 살펴봅니다.
이 구조체를 이해하면 shmem의 메모리 회수(Reclaim), 스왑(Swap), fallocate, NUMA 정책 등
모든 동작을 체계적으로 파악할 수 있습니다.
inode에서 shmem_inode_info를 얻을 때
container_of(inode, struct shmem_inode_info, vfs_inode)를 사용합니다.
vfs_inode가 구조체 끝에 위치하므로, kmem_cache_alloc()으로 할당한
메모리에서 역방향 오프셋(Offset)으로 shmem 전용 필드에 접근합니다.
/* include/linux/shmem_fs.h — 필드별 상세 */
struct shmem_inode_info {
spinlock_t lock;
/* ① inode 단위 잠금: alloced/swapped 카운터 변경,
* XArray 조작, fallocate 범위 보호에 사용.
* IRQ 컨텍스트에서는 사용하지 않으므로 spin_lock() 사용 */
unsigned int seals;
/* ② F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE |
* F_SEAL_FUTURE_WRITE | F_SEAL_SEAL
* memfd_create()로 생성한 파일에만 유효.
* fcntl(F_ADD_SEALS)로 설정, 한번 설정하면 해제 불가 */
unsigned long flags;
/* ③ SHMEM_PAGEIN: 최근 page-in 발생 표시
* SHMEM_TRUNCATE: truncate 진행 중 표시
* 비트 플래그로 상태 머신 역할 */
unsigned long alloced;
/* ④ RAM에 상주하는 데이터 페이지 수.
* shmem_alloc_and_add_folio()에서 증가,
* shmem_writepage()에서 감소.
* i_blocks 계산에 사용: i_blocks = alloced * BLOCKS_PER_PAGE */
unsigned long swapped;
/* ⑤ 스왑 장치에 내보낸 페이지 수.
* shmem_writepage()에서 증가, shmem_swapin_folio()에서 감소.
* alloced + swapped = 파일의 총 논리 페이지 수.
* /proc/meminfo의 Shmem 통계에 alloced만 반영 */
pgoff_t fallocend;
/* ⑥ fallocate(FALLOC_FL_KEEP_SIZE) 예약 범위의 끝 오프셋.
* shmem_get_folio()에서 이 범위 내 접근이면
* SGP_FALLOC 모드로 처리 (데이터 없이 공간만 예약) */
struct list_head shrinklist;
/* ⑦ 슈퍼블록의 shmem_sb_info.shrinklist에 연결.
* 메모리 압력 시 shmem_unused_huge_shrink()가
* 이 목록을 순회하며 미사용 THP를 분할(split)합니다*/
struct list_head swaplist;
/* ⑧ 전역 shmem_swaplist에 연결.
* 스왑된 페이지가 있는 inode만 등록.
* swapoff 시 모든 shmem inode를 순회하여
* swap entry를 복원할 때 사용 */
struct shared_policy policy;
/* ⑨ NUMA 메모리 정책 (mbind/set_mempolicy).
* VMA별로 다른 정책을 가질 수 있으며,
* shmem_alloc_folio()에서 이 정책을 참조하여
* 특정 NUMA 노드에 페이지를 할당 */
struct simple_xattrs xattrs;
/* ⑩ 확장 속성(xattr) 목록.
* security.* (SELinux/AppArmor 레이블),
* trusted.*, user.* 네임스페이스 지원.
* RB-tree로 관리 */
atomic_t stop_eviction;
/* ⑪ 0이 아니면 inode eviction 방지.
* shmem_unuse_inode()가 swapoff 중
* inode가 삭제되지 않도록 보호 */
struct inode vfs_inode;
/* ⑫ VFS inode 임베딩. 반드시 마지막 필드.
* shmem_alloc_inode()에서 전체 구조체를 할당하고
* &info->vfs_inode를 VFS에 반환 */
};
코드 설명
-
①②
lock은 inode 메타데이터 보호용 스핀락(Spinlock)입니다.seals는 memfd 전용으로,F_SEAL_FUTURE_WRITE(커널 5.1+)는 기존 mmap은 허용하되 새로운 쓰기 mmap을 차단합니다. -
④⑤
alloced + swapped합계가 파일 크기를 결정합니다. 스왑 아웃 시 alloced 감소 + swapped 증가가lock보호 하에 원자적으로 수행되어 일관성을 유지합니다. -
⑦⑧
shrinklist는 THP 회수(Shrink) 경로,swaplist는swapoff시 전체 복원 경로에서 사용됩니다. 두 리스트 모두 필요할 때만 등록되어 오버헤드(Overhead)를 최소화합니다. -
⑫
vfs_inode를 마지막에 배치하는 것은 커널의 임베딩 패턴(Embedding Pattern)입니다. VFS가inode포인터(Pointer)를 전달하면,container_of()로 역산하여 shmem 전용 메타데이터에 접근합니다.
shmem_get_folio() 흐름
shmem_get_folio()은 shmem 파일의 페이지에 접근하는 핵심 함수입니다.
페이지 캐시를 먼저 검색하고, 없으면 스왑에서 복원하거나 새로 할당합니다.
static int shmem_get_folio_gfp(struct inode *inode,
pgoff_t index, struct folio **foliop,
enum sgp_type sgp, gfp_t gfp,
struct vm_fault *vmf)
{
struct address_space *mapping = inode->i_mapping;
struct shmem_inode_info *info = SHMEM_I(inode);
struct folio *folio;
int error;
/* 1단계: 페이지 캐시 검색 */
folio = filemap_get_folio(mapping, index);
if (!IS_ERR(folio))
goto out;
/* 2단계: 스왑 엔트리 확인 */
folio = shmem_swapin_folio(inode, index, ...);
if (!IS_ERR_OR_NULL(folio))
goto out;
/* 3단계: 새 folio 할당 */
folio = shmem_alloc_and_add_folio(vmf, gfp,
inode, index, ...);
out:
*foliop = folio;
return error;
}
shmem_get_folio() 호출 체인(Call Chain)
tmpfs 페이지 폴트(Page Fault) 시 호출되는 전체 함수 체인을 추적합니다.
shmem_fault()에서 시작하여 shmem_get_folio() →
shmem_get_folio_gfp() → 스왑캐시(Swapcache) 조회 또는
shmem_alloc_and_add_folio() → shmem_alloc_folio()로 이어지는
경로를 SVG 다이어그램으로 나타냅니다.
shmem_get_folio()는 단순 래퍼(Wrapper)로
shmem_get_folio_gfp()에 기본 GFP 플래그를 전달합니다. 실제 로직은
shmem_get_folio_gfp()에 집중되어 있으며, 3단계 폴백(Fallback) 구조를 가집니다:
① 페이지 캐시 히트(Page Cache Hit) → ② 스왑 복원(Swap-in) → ③ 새 페이지 할당(Allocation).
shmem_get_folio_gfp() 구현 분석
shmem_get_folio_gfp()는 tmpfs 페이지 폴트의 핵심 경로입니다.
약 200줄에 달하는 실제 구현을 핵심 로직 중심으로 단순화하여 분석합니다.
이 함수가 sgp_type 열거형(Enum)에 따라 동작을 분기하는 방식에 주목하세요.
/* mm/shmem.c — 핵심 로직 추출 (약 30줄 단순화) */
static int shmem_get_folio_gfp(struct inode *inode,
pgoff_t index, struct folio **foliop,
enum sgp_type sgp, gfp_t gfp,
struct vm_fault *vmf)
{
struct shmem_inode_info *info = SHMEM_I(inode);
struct folio *folio;
int error;
bool alloced;
repeat:
/* 1단계: 페이지 캐시(XArray)에서 folio 검색 */
folio = filemap_get_folio(inode->i_mapping, index);
if (!IS_ERR(folio)) {
/* THP split이 필요한 경우 처리 */
if (folio_test_large(folio) &&
sgp == SGP_WRITE)
shmem_split_large_entry(...);
goto out;
}
/* 2단계: XArray에 swap entry가 있는지 확인 */
folio = shmem_swapin_folio(inode, index,
gfp, info, ...);
if (!IS_ERR_OR_NULL(folio))
goto out;
/* SGP_READ: 읽기 전용이면 할당하지 않음 (hole 반환) */
if (sgp == SGP_READ) {
*foliop = NULL;
return 0;
}
/* 3단계: 새 folio 할당 후 페이지 캐시에 추가 */
folio = shmem_alloc_and_add_folio(vmf, gfp,
inode, index, info, ...);
if (IS_ERR(folio)) {
error = PTR_ERR(folio);
if (error == -EEXIST)
goto repeat; /* 경쟁 조건: 재시도 */
return error;
}
alloced = true;
/* 새 할당된 folio 초기화: zero-fill */
folio_zero_range(folio, 0, folio_size(folio));
out:
*foliop = folio;
if (alloced) {
info->alloced += folio_nr_pages(folio);
shmem_recalc_inode(inode, ...);
}
return 0;
}
코드 설명
-
repeat 레이블
경쟁 조건(Race Condition) 처리를 위한 재시도 루프입니다. 두 스레드(Thread)가 동시에 같은 인덱스에 folio를 할당하려 하면,
-EEXIST를 받은 쪽이repeat로 돌아가 이미 삽입된 folio를 가져옵니다. -
1단계
filemap_get_folio()는 XArray에서 O(log N) 검색을 수행합니다. 히트(Hit)하면 folio 참조 카운트(Reference Count)를 증가시키고 즉시 반환합니다. -
2단계
shmem_swapin_folio()는 XArray 엔트리가xa_is_value()로 스왑 엔트리인지 확인합니다. 스왑 엔트리면swap_read_folio()로 디스크에서 읽어와 페이지 캐시에 복원합니다. -
SGP_READ 분기
SGP_READ는 페이지가 없을 때 할당하지 않고 NULL을 반환합니다. 읽기 전용 접근에서 불필요한 zero-fill 페이지 생성을 방지하여 메모리를 절약합니다.SGP_WRITE일 때만 실제 할당이 일어납니다. - folio_zero_range 보안상 새로 할당된 페이지는 반드시 0으로 초기화(Zero-fill)됩니다. 이전 프로세스(Process)의 데이터가 노출되는 것을 방지합니다.
-
alloced 카운터
새 할당 시
info->alloced를 folio 크기만큼 증가시킵니다. THP folio는 512페이지(2MB)이므로 한 번에 512가 증가합니다.shmem_recalc_inode()는i_blocks를 갱신합니다.
SGP_READ(읽기, 할당 안 함),
SGP_WRITE(쓰기, 할당 필요), SGP_FALLOC(fallocate 예약, 데이터 없음),
SGP_CACHE(캐시 준비)로 구분됩니다. 이 타입에 따라 페이지 할당 여부와
초기화 방식이 결정되므로, shmem 동작을 이해하는 핵심 키(Key)입니다.
shmem_fault() 상세 분석
사용자 공간에서 tmpfs 파일을 mmap()한 뒤 해당 주소에 접근하면
페이지 폴트(Page Fault)가 발생하고, VFS의 filemap_fault() 대신
shmem_fault()가 호출됩니다. 이 함수는 shmem 전용 페이지 폴트 핸들러로,
일반 파일시스템의 readahead 경로 대신 shmem 고유의 할당/스왑인 경로를 사용합니다.
/* mm/shmem.c — shmem_fault() 핵심 구현 */
static vm_fault_t shmem_fault(struct vm_fault *vmf)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
gfp_t gfp = mapping_gfp_mask(inode->i_mapping);
struct folio *folio = NULL;
int err;
/*
* sgp_type 결정:
* 쓰기 폴트 → SGP_WRITE (즉시 dirty 마크)
* 읽기 폴트 → SGP_CACHE (캐시에만 삽입)
*/
enum sgp_type sgp = (vmf->flags & FAULT_FLAG_WRITE)
? SGP_WRITE : SGP_CACHE;
err = shmem_get_folio_gfp(inode,
vmf->pgoff, &folio, sgp, gfp, vmf);
if (err)
return vmf_error(err);
/* folio가 NULL이면 hole 접근 (SGP_READ 모드) */
if (!folio)
return VM_FAULT_SIGBUS;
vmf->page = folio_file_page(folio,
vmf->pgoff);
return VM_FAULT_LOCKED;
}
/* shmem_huge_fault — THP 페이지 폴트 핸들러 */
static vm_fault_t shmem_huge_fault(
struct vm_fault *vmf,
unsigned int order)
{
struct inode *inode = file_inode(vmf->vma->vm_file);
/* THP 비활성화 시 폴백 */
if (!shmem_is_huge(inode, vmf->pgoff, ...))
return VM_FAULT_FALLBACK;
/* order 파라미터로 2MB PMD / 더 큰 folio 시도 */
return shmem_fault(vmf); /* 내부에서 order 반영 */
}
코드 설명
-
sgp_type 결정
FAULT_FLAG_WRITE가 설정되면SGP_WRITE를 사용하여 folio를 즉시 dirty로 마크합니다. 읽기 폴트 시에는SGP_CACHE로 clean 상태로 캐시에 삽입합니다.SGP_READ는read()시스템 콜 경로에서만 사용되며, 페이지가 없으면 NULL을 반환합니다. -
shmem_get_folio_gfp
실제 페이지 할당/스왑인/캐시 조회를 수행하는 핵심 함수입니다.
vmf포인터를 전달하여 폴트 컨텍스트(Context) 정보(VMA, 주소 등)를 활용합니다. -
VM_FAULT_LOCKED
반환 시 folio가 잠긴(locked) 상태임을 나타냅니다. 호출자(
finish_fault())가 PTE를 설정한 후 folio를 언락합니다. -
shmem_huge_fault
vm_ops->huge_fault에 등록됩니다. PMD 수준(2MB) 폴트 시 호출되며, THP 설정(huge=마운트 옵션)에 따라VM_FAULT_FALLBACK을 반환하여 4KB 폴트로 폴백하거나, 2MB folio 할당을 시도합니다.
filemap_fault()를 사용하여 디스크에서 페이지를 읽어옵니다.
shmem은 디스크 백엔드가 없으므로 자체 shmem_fault()를 구현합니다.
주요 차이점: ① readahead 없음 (디스크 I/O 불필요), ② 스왑에서 복원 경로 존재,
③ THP를 huge_fault로 직접 지원.
페이지 할당과 스왑 연동
페이지 할당 경로
shmem의 페이지 할당은 shmem_alloc_folio()를 통해 이루어집니다.
NUMA 정책, THP 여부, GFP 플래그에 따라 적절한 할당 경로를 선택합니다.
shmem_writepage() - 스왑 아웃
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct folio *folio = page_folio(page);
struct shmem_inode_info *info;
struct address_space *mapping;
swp_entry_t swap;
/* 스왑 공간에 슬롯 할당 */
swap = folio_alloc_swap(folio);
if (!swap.val)
goto redirty;
/* XArray에서 folio를 swap_entry로 교체 */
xa_lock_irq(&mapping->i_pages);
if (shmem_replace_folio(&folio, swap, mapping))
goto free_swap;
xa_unlock_irq(&mapping->i_pages);
info->swapped++;
info->alloced--;
/* 스왑 장치에 기록 */
swap_writepage(&folio->page, wbc);
return 0;
}
코드 설명
-
10행
folio_alloc_swap()로 스왑 공간에서 빈 슬롯을 할당받습니다. 실패하면 페이지를 dirty 상태로 유지합니다. -
15-17행
XArray를 잠근 상태에서 folio 엔트리를
swap_entry_t로 원자적(Atomic)으로 교체합니다. 이후shmem_get_folio()에서 이 swap 엔트리를 찾아 복원합니다. -
20-21행
swapped카운터 증가,alloced카운터 감소. 이 두 카운터의 합은 파일의 총 논리 페이지 수를 유지합니다.
swap-backed tmpfs 내부 동작
일반 파일시스템(ext4, XFS 등)은 디스크에 writeback하여 페이지를 회수합니다.
그러나 tmpfs는 디스크 백엔드(Backend)가 없으므로, 페이지를 회수하려면
반드시 스왑 장치(Swap Device)에 기록해야 합니다.
이 과정에서 shmem_writepage()가 핵심 역할을 합니다.
/* shmem_writepage() 핵심 경로 — 스왑 아웃 상세 */
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct folio *folio = page_folio(page);
struct address_space *mapping = folio->mapping;
struct inode *inode = mapping->host;
struct shmem_inode_info *info = SHMEM_I(inode);
swp_entry_t swap;
/* 스왑 비활성화 상태면 dirty 유지 (회수 불가) */
if (!total_swap_pages)
goto redirty;
/* 스왑 슬롯 할당 — 실패 시 OOM 위험 */
swap = folio_alloc_swap(folio);
if (!swap.val)
goto redirty;
/* XArray 잠금 후 원자적 교체 */
xa_lock_irq(&mapping->i_pages);
if (shmem_replace_folio(&folio, swap, mapping,
index))
goto free_swap;
/* 카운터 갱신 (lock 보호) */
spin_lock(&info->lock);
info->swapped++;
info->alloced--;
spin_unlock(&info->lock);
xa_unlock_irq(&mapping->i_pages);
/* swaplist에 등록 (최초 스왑 시) */
if (list_empty(&info->swaplist))
list_add(&info->swaplist, &shmem_swaplist);
/* 실제 스왑 장치에 기록 */
swap_writepage(&folio->page, wbc);
return 0;
redirty:
folio_mark_dirty(folio);
return AOP_WRITEPAGE_ACTIVATE;
}
코드 설명
-
total_swap_pages 확인
스왑이 비활성화된 시스템(
swapoff -a)에서는 tmpfs 페이지를 회수할 수 없습니다.redirty로 분기하여AOP_WRITEPAGE_ACTIVATE를 반환하면, 회수 경로(vmscan)가 이 페이지를 건너뜁니다. -
folio_alloc_swap
스왑 공간에서 folio 크기에 맞는 연속 슬롯을 할당합니다. THP folio(2MB)는 512개 연속 슬롯이 필요하여 할당 실패 확률이 높습니다. 실패 시
redirty로 돌아가 나중에 재시도합니다. -
shmem_replace_folio
XArray의 해당 인덱스(Index)에서 folio 포인터(Pointer)를
swp_entry_to_pte(swap)으로 인코딩된 값으로 교체합니다.xa_lock_irq보호 하에 수행되어shmem_get_folio()와의 경쟁을 방지합니다. -
swaplist 등록
스왑된 페이지가 있는 inode는 전역
shmem_swaplist에 등록됩니다.swapoff실행 시 이 리스트를 순회하며 모든 스왑 엔트리를 RAM으로 복원(shmem_unuse())합니다. - AOP_WRITEPAGE_ACTIVATE 이 반환값은 "페이지를 active LRU 리스트로 이동하라"는 뜻입니다. 스왑 불가 상태에서 반복적인 회수 시도를 방지하여 CPU 낭비를 줄입니다.
size= 옵션으로 최대 크기를 제한하거나 스왑을 활성화하세요.
shmem_unuse() — swapoff 시 shmem 복원 경로
swapoff 명령을 실행하면 스왑 장치의 모든 페이지를 RAM으로 복원해야 합니다.
shmem 페이지의 경우 전역 shmem_swaplist를 순회하며
shmem_unuse()로 각 inode의 스왑 엔트리를 복원합니다.
이 과정은 시간이 오래 걸릴 수 있으며, 충분한 RAM이 없으면 실패합니다.
/* mm/shmem.c — shmem_unuse() 핵심 경로 */
int shmem_unuse(unsigned int type)
{
struct shmem_inode_info *info, *next;
/* 전역 shmem_swaplist 순회 */
mutex_lock(&shmem_swaplist_mutex);
list_for_each_entry_safe(info, next,
&shmem_swaplist, swaplist) {
if (!info->swapped)
continue;
/* 해당 inode의 XArray에서 스왑 엔트리 검색 */
error = shmem_unuse_inode(info, type);
if (error)
break; /* ENOMEM: RAM 부족 → swapoff 실패 */
/* 모든 스왑 엔트리 복원 완료 시 리스트에서 제거 */
if (!info->swapped)
list_del_init(&info->swaplist);
}
mutex_unlock(&shmem_swaplist_mutex);
return error;
}
/* shmem_unuse_inode — 단일 inode의 스왑 복원 */
static int shmem_unuse_inode(
struct shmem_inode_info *info,
unsigned int type)
{
struct address_space *mapping = info->vfs_inode.i_mapping;
pgoff_t index;
struct folio *folio;
/* XArray 전체를 스캔하여 해당 스왑 타입의 엔트리 검색 */
xa_for_each(&mapping->i_pages, index, folio) {
if (!xa_is_value(folio))
continue; /* 실제 folio → 건너뛰기 */
swp_entry_t entry = radix_to_swp_entry(folio);
if (swp_type(entry) != type)
continue; /* 다른 스왑 장치의 엔트리 */
/* 스왑에서 읽어와 페이지 캐시에 복원 */
error = shmem_swapin_folio(
&info->vfs_inode, index,
&folio, ...);
if (error)
return error;
folio_unlock(folio);
folio_put(folio);
}
return 0;
}
코드 설명
-
shmem_swaplist
shmem_writepage()에서 최초 스왑 시 등록되는 전역 연결 리스트(Linked List)입니다. 스왑된 페이지가 있는 모든 shmem inode가 이 리스트에 포함됩니다.swapoff시 이 리스트를 순회하여 해당 스왑 장치의 엔트리만 복원합니다. -
xa_is_value()
XArray 엔트리가 실제 folio 포인터인지 인코딩된 swap_entry인지 구분합니다.
xa_is_value()가 true이면 스왑 엔트리로,radix_to_swp_entry()로 디코딩합니다. -
ENOMEM 실패
shmem_swapin_folio()가 RAM 할당에 실패하면swapoff명령 자체가 실패합니다. 이 때swapoff는-ENOMEM을 반환하고, 스왑 장치는 계속 활성 상태를 유지합니다. 충분한 여유 RAM이 있어야swapoff가 성공합니다.
swapoff는
매우 오래 걸릴 수 있습니다. 각 inode의 XArray를 전체 스캔하고, 스왑 장치에서 개별적으로
읽어와야 하기 때문입니다. 수 GB의 tmpfs 데이터가 스왑에 있으면 수분 이상 소요될 수 있습니다.
프로덕션에서는 swapoff 전에 swapon --show로 사용량을 확인하세요.
shmem NUMA 정책
shmem은 NUMA(Non-Uniform Memory Access) 시스템에서 페이지 할당 위치를 제어하는
정책(Policy)을 지원합니다. set_mempolicy(), mbind(),
numactl 등을 통해 tmpfs 파일의 메모리를 특정 노드에 배치할 수 있습니다.
/* shmem NUMA 정책 적용 경로 */
/* 1. vm_ops에 등록된 NUMA 정책 핸들러 */
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy, /* mbind() 처리 */
.get_policy = shmem_get_policy, /* get_mempolicy() 처리 */
#endif
};
/* 2. 페이지 할당 시 NUMA 정책 적용 */
static struct folio *shmem_alloc_folio(
gfp_t gfp, struct shmem_inode_info *info,
pgoff_t index)
{
struct mempolicy *mpol;
int nid;
/* inode의 shared_policy에서 해당 인덱스의 정책 조회 */
mpol = mpol_shared_policy_lookup(
&info->policy, index);
/* 정책에 따라 적절한 NUMA 노드에서 할당 */
folio = folio_alloc_mpol(gfp, order, mpol, ...);
mpol_cond_put(mpol);
return folio;
}
# NUMA 시스템에서 tmpfs NUMA 정책 활용
# 특정 노드에 tmpfs 데이터 바인딩
numactl --membind=0 cp large_file /tmp/fast_data
# mbind()로 영역별 NUMA 정책 설정
# (프로그램 내에서 mmap한 tmpfs 영역에 적용)
# NUMA 통계 확인
numastat -m | grep Shmem
# Node 0 Node 1
# Shmem 512.00 256.00
# shmem inode별 NUMA 정책 확인
# /proc//numa_maps에서 tmpfs 매핑 확인
grep shmem /proc/self/numa_maps
Transparent Huge Pages in tmpfs
리눅스 커널은 tmpfs에서 Transparent Huge Pages(THP)를 지원합니다. 2MB 크기의 huge page를 사용하면 TLB 미스를 줄이고 대용량 파일 접근 성능을 크게 향상시킬 수 있습니다.
THP 활성화 방법
# 마운트 시 THP 활성화
mount -t tmpfs -o size=4G,huge=always tmpfs /mnt/huge_tmp
# sysfs를 통한 전역 설정
echo always > /sys/kernel/mm/transparent_hugepage/shmem_enabled
# madvise 기반 (advise 모드일 때)
# 프로그램에서 madvise(addr, len, MADV_HUGEPAGE) 호출 필요
# 현재 설정 확인
cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
# always within_size advise [never] deny force
THP 통계 확인
# shmem THP 관련 통계
grep -i shmem /proc/vmstat
# nr_shmem 1234
# nr_shmem_hugepages 56
# nr_shmem_pmdmapped 48
# AnonHugePages vs ShmemHugePages
grep -E "Shmem|Huge" /proc/meminfo
# Shmem: 512000 kB
# ShmemHugePages: 114688 kB
# ShmemPmdMapped: 98304 kB
always로 설정하면 내부 단편화(Fragmentation)로 인해
실제 메모리 사용량이 증가할 수 있습니다. 예를 들어 5KB 파일에 2MB 페이지가 할당되면
2043KB가 낭비됩니다. 프로덕션 환경에서는 within_size 또는 advise를 권장합니다.
대용량 Folio와 Multi-size THP
리눅스 6.8+부터 shmem은 2MB PMD 크기뿐 아니라 다양한 order의 folio를 할당할 수 있습니다. 이를 Multi-size THP (mTHP)라고 하며, 16KB(order-2), 32KB(order-3), 64KB(order-4) 등 중간 크기의 folio를 통해 내부 단편화(Fragmentation)와 TLB 효율(Efficiency) 사이의 균형을 맞출 수 있습니다.
# mTHP sysfs 설정 (커널 6.8+)
ls /sys/kernel/mm/transparent_hugepage/hugepages-*/
# hugepages-16kB hugepages-32kB hugepages-64kB
# hugepages-128kB hugepages-256kB hugepages-512kB
# hugepages-1024kB hugepages-2048kB
# 특정 크기의 mTHP 활성화 (shmem 전용)
echo always > /sys/kernel/mm/transparent_hugepage/hugepages-64kB/shmem_enabled
# mTHP 통계 확인
cat /proc/vmstat | grep thp_file
# thp_file_alloc 1234
# thp_file_fallback 56
# thp_file_fallback_charge 12
hugepages-64kB/shmem_enabled=always)는
많은 워크로드에서 좋은 절충점입니다. 2MB THP에 비해 내부 단편화가 32배 적으면서도
TLB 커버리지를 16배 향상시킵니다. 특히 ARM64 시스템에서는 하드웨어가
64KB contiguous PTE를 지원하므로 추가적인 TLB 이점이 있습니다.
POSIX 공유 메모리 (shm_open, /dev/shm)
POSIX 공유 메모리는 shm_open()/shm_unlink() API를 통해
프로세스(Process) 간 공유 메모리 세그먼트를 생성합니다. 내부적으로 /dev/shm에 마운트된
tmpfs 위에 파일을 생성하는 방식으로 동작합니다.
POSIX 공유 메모리 예제
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define SHM_NAME "/my_shared_buf"
#define SHM_SIZE 4096
/* 생산자: 공유 메모리 생성 및 데이터 쓰기 */
int producer(void)
{
int fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
if (fd < 0) { perror("shm_open"); return 1; }
ftruncate(fd, SHM_SIZE);
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd);
strcpy(ptr, "Hello from producer!");
printf("생산자: 데이터 기록 완료\\n");
munmap(ptr, SHM_SIZE);
return 0;
}
/* 소비자: 공유 메모리 읽기 */
int consumer(void)
{
int fd = shm_open(SHM_NAME, O_RDONLY, 0);
if (fd < 0) { perror("shm_open"); return 1; }
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ, MAP_SHARED, fd, 0);
close(fd);
printf("소비자: %s\\n", ptr);
munmap(ptr, SHM_SIZE);
shm_unlink(SHM_NAME); /* 정리 */
return 0;
}
System V 공유 메모리 (shmget, shmat)
System V IPC의 공유 메모리는 shmget()/shmat()/shmdt()/shmctl()
API를 사용합니다. 커널 내부에서는 shmem 파일시스템 위에 익명 파일을 생성하여 구현됩니다.
System V vs POSIX 공유 메모리
| 특성 | System V (shmget) | POSIX (shm_open) |
|---|---|---|
| 식별자 | 정수 키 (key_t) | 문자열 이름 (/name) |
| API | shmget/shmat/shmdt/shmctl | shm_open/mmap/munmap/shm_unlink |
| 파일시스템 | 커널 내부 shmem (보이지 않음) | /dev/shm/ (보임) |
| 크기 조정 | 생성 시 고정 | ftruncate()로 변경 가능 |
| 수명 | 명시적 IPC_RMID 또는 재부팅 | shm_unlink() 또는 재부팅 |
| 큰 페이지 | SHM_HUGETLB 플래그 | tmpfs huge= 옵션 의존 |
| 권장 여부 | 레거시 (새 코드에서 비권장) | 권장 (POSIX 표준) |
System V 공유 메모리 사용 예제
#include <sys/ipc.h>
#include <sys/shm.h>
#include <stdio.h>
#include <string.h>
#define SHM_KEY 0x1234
#define SHM_SIZE 4096
int main(void)
{
/* 공유 메모리 세그먼트 생성/획득 */
int shmid = shmget(SHM_KEY, SHM_SIZE,
IPC_CREAT | 0666);
if (shmid < 0) {
perror("shmget");
return 1;
}
/* 현재 프로세스 주소 공간에 연결 */
char *ptr = shmat(shmid, NULL, 0);
if (ptr == (char *)-1) {
perror("shmat");
return 1;
}
strcpy(ptr, "SysV SHM 데이터");
printf("데이터: %s\\n", ptr);
/* 분리 및 삭제 */
shmdt(ptr);
shmctl(shmid, IPC_RMID, NULL);
return 0;
}
System V SHM 커널 내부 경로
/* ipc/shm.c: shmget() 시스템 콜 처리 */
static int newseg(struct ipc_namespace *ns,
struct ipc_params *params)
{
struct shmid_kernel *shp;
struct file *file;
size_t size = params->u.size;
/* shmem 파일 생성 (핵심!) */
file = shmem_kernel_file_setup("SYSV", size, 0);
if (IS_ERR(file))
return PTR_ERR(file);
shp->shm_file = file; /* shmem 파일과 연결 */
...
}
shmem_file_setup과 커널 내부 사용
shmem_file_setup()은 커널 내부에서 shmem 기반 익명 파일을 생성하는 핵심 함수입니다.
DRM GEM 객체, memfd, System V SHM 등 다양한 서브시스템이 이 함수를 통해
shmem 백업 저장소를 활용합니다.
memfd_create() 시스템 콜(System Call)
memfd_create()는 이름 없는 shmem 파일을 생성하는 현대적인 API입니다.
파일 디스크립터(File Descriptor)만 반환하며, 파일시스템에 보이지 않아 이름 충돌이 없습니다.
씰(seal) 메커니즘으로 파일 내용의 불변성을 보장할 수 있어,
IPC와 버퍼(Buffer) 공유에 이상적입니다.
#include <sys/mman.h>
#include <linux/memfd.h>
#include <unistd.h>
#include <stdio.h>
int main(void)
{
/* 익명 shmem 파일 생성 */
int fd = memfd_create("my_buffer", MFD_ALLOW_SEALING);
if (fd < 0) { perror("memfd_create"); return 1; }
/* 크기 설정 */
ftruncate(fd, 4096);
/* 데이터 기록 */
char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
sprintf(ptr, "memfd 공유 데이터");
/* 씰 적용: 크기 변경/쓰기 금지 */
fcntl(fd, F_ADD_SEALS,
F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_WRITE);
/* 이제 fd를 다른 프로세스에 전달 (sendmsg SCM_RIGHTS) */
printf("memfd fd=%d, 씰 적용 완료\\n", fd);
/* 수신 측은 씰을 확인하여 데이터 무결성 보장 가능 */
int seals = fcntl(fd, F_GET_SEALS);
printf("씰 플래그: 0x%x\\n", seals);
munmap(ptr, 4096);
close(fd);
return 0;
}
DRM GEM shmem 사용
/* drivers/gpu/drm/drm_gem_shmem_helper.c */
struct drm_gem_shmem_object *drm_gem_shmem_create(
struct drm_device *dev, size_t size)
{
struct drm_gem_shmem_object *shmem;
shmem = kzalloc(sizeof(*shmem), GFP_KERNEL);
/* shmem 기반 GEM 객체 초기화 */
drm_gem_object_init(dev, &shmem->base, size);
/* 내부적으로 shmem_file_setup() 호출 */
mutex_init(&shmem->pages_lock);
mutex_init(&shmem->vmap_lock);
return shmem;
}
shmem_file_setup() 내부 경로
shmem_file_setup()은 커널 내부에서 shmem 기반 파일을 생성하는 유일한 진입점(Entry Point)입니다.
memfd_create(), System V SHM, DRM GEM 등이 모두 이 함수를 통해 익명 파일(Anonymous File)을 생성합니다.
함수의 내부 동작을 단계별로 분석합니다.
/* mm/shmem.c — shmem_file_setup() 핵심 경로 */
struct file *shmem_file_setup(const char *name,
loff_t size,
unsigned long flags)
{
return __shmem_file_setup(shm_mnt, name, size,
flags, 0);
}
static struct file *__shmem_file_setup(
struct vfsmount *mnt, const char *name,
loff_t size, unsigned long flags, unsigned int i_flags)
{
struct inode *inode;
struct file *file;
/* 크기 상한 검증 */
if (size < 0 || size > MAX_LFS_FILESIZE)
return ERR_PTR(-EINVAL);
if (shmem_acct_size(flags, size))
return ERR_PTR(-ENOMEM);
/* inode 생성 (shmem_inode_info 포함) */
inode = shmem_get_inode(mnt->mnt_sb, NULL,
S_IFREG | S_IRWXUGO, 0,
flags);
/* 파일 크기 설정 */
inode->i_size = size;
/* 익명 file 구조체 생성 */
file = alloc_file_pseudo(inode, mnt,
name, O_RDWR,
&shmem_file_operations);
return file;
}
코드 설명
-
shmem_file_setup
외부에 노출되는 API로, 전역
shm_mnt(커널 부팅 시shmem_init()에서kern_mount()로 생성)를__shmem_file_setup()에 전달합니다.memfd_create()는MFD_HUGETLB여부에 따라 별도 마운트를 사용할 수 있습니다. -
shmem_acct_size
메모리 계정(Accounting) 함수로,
VM_NORESERVE플래그가 없으면vm_committed_as에 크기를 예약합니다. 오버커밋(Overcommit) 정책(vm.overcommit_memory)에 따라-ENOMEM을 반환할 수 있습니다. -
shmem_get_inode
new_inode()로 VFS inode를 할당하고,SHMEM_I(inode)로 shmem 전용 필드를 초기화합니다.i_mapping->a_ops = &shmem_aops를 설정하여 shmem의 address_space_operations를 등록합니다. -
alloc_file_pseudo
실제 파일시스템에 보이지 않는 의사 파일(Pseudo File)을 생성합니다.
d_alloc_pseudo()로 익명 dentry를 만들고,file->f_op = &shmem_file_operations를 설정합니다. 반환된struct file *은fd_install()로 사용자 공간에 전달되거나, 커널 내부에서 직접 사용됩니다.
shmem_file_setup()으로 shmem 파일을 생성하지만, 이후 사용 방식이 다릅니다.
memfd는 fd를 SCM_RIGHTS로 다른 프로세스(Process)에 전달하고,
SysV SHM은 shmid 정수 키(Key)로 프로세스 간 공유하며,
DRM GEM은 GPU 드라이버(Driver)가 내부적으로 버퍼(Buffer) 메모리를 관리합니다.
shmem이 이 모든 것의 공통 기반인 이유는, RAM 백업 + 스왑 가능 + 페이지 캐시 통합이라는
세 가지 특성을 동시에 제공하기 때문입니다.
tmpfs와 메모리 압력
tmpfs의 가장 중요한 특성 중 하나는 메모리 압력(memory pressure) 시 일반 페이지 캐시처럼 회수(reclaim)될 수 있다는 점입니다. ramfs와 달리 shmem 페이지는 LRU 목록에 포함되어 kswapd의 관리를 받습니다.
size= 옵션으로 tmpfs 크기를 제한하고, 적절한 스왑 공간을 확보하세요.
메모리 사용량 모니터링
# shmem 전체 사용량 확인
grep Shmem /proc/meminfo
# Shmem: 512000 kB
# 개별 tmpfs 마운트 사용량
df -h --type=tmpfs
# Filesystem Size Used Avail Use% Mounted on
# tmpfs 1.0G 128M 896M 13% /tmp
# tmpfs 3.9G 0 3.9G 0% /dev/shm
# tmpfs 800M 1.2M 799M 1% /run
# cgroup v2 메모리 통계 (컨테이너 환경)
cat /sys/fs/cgroup/memory.stat | grep shmem
# shmem 131072
shmem과 memory cgroup 연동
memory cgroup(memcg) 환경에서 shmem 페이지는 해당 cgroup의 메모리 한도에 포함됩니다. 컨테이너 내부에서 tmpfs를 사용하면 그 컨테이너의 메모리 사용량으로 계산되며, cgroup 한도를 초과하면 cgroup 내부의 shmem 페이지가 우선 스왑 아웃됩니다.
/* shmem memcg 충전 경로 */
/* shmem_get_folio_gfp() 내에서 새 folio 할당 시 */
folio = shmem_alloc_folio(gfp, info, index);
/* memcg에 충전 (charge) */
error = mem_cgroup_charge(folio, current->mm, gfp);
if (error) {
/* cgroup 한도 초과 → 직접 회수 시도 후 재시도
* 그래도 실패하면 -ENOMEM 반환 */
folio_put(folio);
return error;
}
/* folio->memcg_data에 cgroup 정보 기록 */
/* 이후 이 folio는 해당 cgroup의 메모리 사용량에 포함됨 */
/* 스왑 아웃 시에도 cgroup 추적 유지 */
/* swap_entry에 memcg ID가 인코딩되어 스왑인 시 복원 */
# 컨테이너/cgroup에서 shmem 사용량 확인
# cgroup v2 메모리 통계
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.stat
# shmem 524288 ← shmem 페이지 바이트 수
# file 1048576 ← 일반 파일 캐시 (shmem 제외)
# shmem_pmdmapped 0 ← PMD 매핑된 shmem THP
# cgroup 내 shmem이 한도에 기여하는지 확인
cat /sys/fs/cgroup/user.slice/memory.current
# 이 값에 shmem 사용량이 포함됨
# Docker에서 tmpfs 크기 제한 + memory limit 조합
docker run --memory=1g --tmpfs /tmp:size=256M alpine sh
# /tmp 사용량도 1GB 메모리 한도에 포함
--tmpfs 옵션은 size=로
tmpfs 자체 크기를 제한하지만, 이 사용량은 컨테이너의 --memory 한도에도 포함됩니다.
예를 들어 --memory=1g --tmpfs /tmp:size=512M으로 설정하면,
/tmp에 500MB를 사용하면 프로세스에 남은 메모리는 약 500MB뿐입니다.
이를 인지하지 못하면 예기치 않은 OOM kill이 발생할 수 있습니다.
noswap 마운트 옵션 (커널 6.4+)
커널 6.4부터 tmpfs에 noswap 마운트 옵션이 추가되었습니다.
이 옵션을 설정하면 해당 tmpfs 인스턴스의 페이지는 절대 스왑 아웃되지 않습니다.
민감한 데이터(비밀번호, 암호화 키 등)가 스왑 장치에 기록되는 것을 방지하는 보안 기능입니다.
# noswap 마운트 (커널 6.4+)
mount -t tmpfs -o size=256M,noswap tmpfs /run/secrets
# /etc/fstab 항목
tmpfs /run/secrets tmpfs defaults,size=256M,noswap,noexec,nosuid,mode=0700 0 0
# 런타임에 noswap 활성화/비활성화
mount -o remount,noswap /tmp
mount -o remount,swap /tmp # swap 복원
/* mm/shmem.c — noswap 검사 */
static int shmem_writepage(struct page *page,
struct writeback_control *wbc)
{
struct shmem_sb_info *sbinfo;
...
sbinfo = SHMEM_SB(inode->i_sb);
/* noswap 옵션이 설정되면 스왑 아웃 거부 */
if (sbinfo->noswap)
goto redirty;
/* 이하 일반 스왑 아웃 경로 */
...
}
noswap이 설정된 tmpfs의 페이지는
메모리 압력에서도 회수할 수 없습니다. 이는 mlock()과 유사한 효과를 가지며,
크기 제한 없이 사용하면 시스템 전체의 OOM을 유발할 수 있습니다.
반드시 size= 옵션과 함께 사용하세요.
커널 설정 (CONFIG_SHMEM, CONFIG_TMPFS)
관련 커널 설정 옵션
| 설정 | 기본값 | 설명 |
|---|---|---|
CONFIG_SHMEM | y | shmem 핵심 구현 활성화. 비활성화 시 ramfs로 대체 |
CONFIG_TMPFS | y | tmpfs 사용자 공간 마운트 지원 |
CONFIG_TMPFS_POSIX_ACL | y | tmpfs에서 POSIX ACL 지원 |
CONFIG_TMPFS_XATTR | y | tmpfs에서 확장 속성(xattr) 지원 |
CONFIG_TMPFS_INODE64 | y | 64비트 inode 번호 기본 활성화 |
CONFIG_TRANSPARENT_HUGEPAGE | y | THP 지원 (tmpfs 포함) |
CONFIG_SWAP | y | 스왑 서브시스템 활성화 (shmem 스왑 아웃 필수) |
CONFIG_SHMEM 비활성화 시 동작
/* include/linux/shmem_fs.h */
#ifdef CONFIG_SHMEM
/* 전체 shmem 구현 사용 (mm/shmem.c) */
extern int shmem_init(void);
extern struct file *shmem_file_setup(const char *name,
loff_t size, unsigned long flags);
#else
/* ramfs 기반 최소 구현으로 대체 */
/* 스왑 불가, 크기 제한 불가, THP 불가 */
static inline struct file *shmem_file_setup(
const char *name, loff_t size,
unsigned long flags)
{
return ramfs_file_setup(name, size, flags);
}
#endif
CONFIG_SHMEM=n으로 설정하면 임베디드 환경에서
커널 이미지 크기를 줄일 수 있습니다. 다만 스왑, 크기 제한, THP 등 핵심 기능이
모두 비활성화되므로 일반적인 리눅스 배포판에서는 사용하지 않습니다.
보안 (noexec, nosuid, 크기 제한)
tmpfs는 사용자가 쓰기 가능한 파일시스템이므로, 보안 설정이 매우 중요합니다. 공격자가 tmpfs에 악성 바이너리를 올려 실행하거나, setuid 프로그램을 배치하는 것을 방지해야 합니다.
보안 강화 마운트 예시
# /tmp: noexec + nosuid + nodev + 크기 제한
mount -t tmpfs -o size=1G,mode=1777,noexec,nosuid,nodev tmpfs /tmp
# /dev/shm: 공유 메모리 전용, 실행 금지
mount -t tmpfs -o size=2G,mode=1777,noexec,nosuid,nodev tmpfs /dev/shm
# /run: 시스템 런타임 데이터 (적절한 크기)
mount -t tmpfs -o size=800M,mode=0755,nosuid,nodev tmpfs /run
# /etc/fstab 보안 강화 항목
tmpfs /tmp tmpfs defaults,size=1G,noexec,nosuid,nodev,mode=1777 0 0
tmpfs /dev/shm tmpfs defaults,size=2G,noexec,nosuid,nodev,mode=1777 0 0
tmpfs /run tmpfs defaults,size=800M,nosuid,nodev,mode=0755 0 0
memfd_create 씰 보안
| 씰 플래그 | 효과 |
|---|---|
F_SEAL_SEAL | 더 이상 새로운 씰을 추가할 수 없음 |
F_SEAL_SHRINK | 파일 크기 축소 불가 (ftruncate 차단) |
F_SEAL_GROW | 파일 크기 확장 불가 |
F_SEAL_WRITE | 쓰기 불가 (write, mmap PROT_WRITE 차단) |
F_SEAL_FUTURE_WRITE | 새로운 쓰기 매핑(Mapping)만 차단 (기존 매핑은 유지) |
F_SEAL_EXEC | 실행 가능 매핑 차단 (6.3+) |
성능 벤치마크와 튜닝
tmpfs vs ext4 vs xfs 성능 비교
| 워크로드 | tmpfs | ext4 (SSD) | xfs (SSD) |
|---|---|---|---|
| 순차 쓰기 (1GB) | ~8 GB/s | ~500 MB/s | ~480 MB/s |
| 순차 읽기 (1GB) | ~12 GB/s | ~550 MB/s | ~530 MB/s |
| 랜덤 4K 읽기 IOPS | ~2,000K | ~300K | ~280K |
| 파일 생성 (10K 파일) | ~50ms | ~800ms | ~600ms |
| fsync 지연(Latency) | N/A (불필요) | ~0.5ms | ~0.3ms |
주요 튜닝 포인트
# 1. THP 활성화 (대용량 파일 접근 시 TLB 미스 감소)
mount -t tmpfs -o huge=within_size,size=8G tmpfs /mnt/fast_tmp
# 2. NUMA 친화적 할당 (NUMA 시스템)
# tmpfs 파일에 NUMA 정책 적용
numactl --membind=0 dd if=/dev/zero of=/mnt/fast_tmp/data bs=1M count=1024
# 3. 스왑 우선순위 최적화 (zram 사용)
# zram을 높은 우선순위로 설정하여 tmpfs 스왑 아웃 시 성능 저하 최소화
zramctl /dev/zram0 --size 4G --algorithm zstd
mkswap /dev/zram0
swapon -p 100 /dev/zram0
# 4. vm.swappiness 조정 (tmpfs 스왑 아웃 빈도 제어)
# 낮은 값: tmpfs 페이지를 오래 RAM에 유지
sysctl vm.swappiness=10
# 5. 적절한 크기 설정 (과도한 할당 방지)
# 실제 필요한 만큼만 할당 (기본 50%는 과도할 수 있음)
mount -o remount,size=2G /tmp
실전 사용 사례
/tmp, /run, /dev/shm
현대 리눅스 배포판은 기본적으로 여러 tmpfs 마운트를 사용합니다.
| 마운트 포인트 | 용도 | 일반적인 크기 |
|---|---|---|
/tmp | 임시 파일 (빌드 출력, 세션 데이터) | RAM 50% 또는 1-4GB |
/dev/shm | POSIX 공유 메모리 (shm_open) | RAM 50% |
/run | 런타임 데이터 (PID 파일, 소켓(Socket)) | RAM 20% 또는 800MB |
/run/lock | 잠금(Lock) 파일 | 5MB |
/sys/fs/cgroup | cgroup v2 파일시스템 | 자동 |
컨테이너(Container) 환경에서의 tmpfs
# Docker: 컨테이너 내 tmpfs 마운트
docker run --tmpfs /tmp:size=512M,noexec,nosuid alpine sh
# Kubernetes: emptyDir medium=Memory
# Pod spec에서 tmpfs 볼륨 사용
# volumes:
# - name: shared-data
# emptyDir:
# medium: Memory
# sizeLimit: 256Mi
# containerd: OCI 런타임 설정에서 tmpfs
# 각 컨테이너의 /dev/shm은 별도 tmpfs로 격리
# 컨테이너 tmpfs의 memory cgroup 제한 확인
cat /sys/fs/cgroup/system.slice/docker-*.scope/memory.stat | grep shmem
GPU 버퍼 (DRM GEM)
GPU 드라이버(i915, amdgpu, nouveau 등)는 DRM GEM(Graphics Execution Manager) 객체의 백업 저장소로 shmem을 광범위하게 사용합니다. GPU가 접근하지 않는 버퍼는 RAM에서 스왑 아웃되어 메모리를 절약할 수 있습니다.
/* GPU 버퍼 할당 흐름 (i915 드라이버 예시) */
/* 1단계: shmem 기반 GEM 객체 생성 */
struct drm_i915_gem_object *obj;
obj = i915_gem_object_create_shmem(i915, size);
/* 내부: shmem_file_setup() → 페이지 캐시 위에 파일 생성 */
/* 2단계: GPU에 핀 (스왑 아웃 방지) */
i915_gem_object_pin_pages(obj);
/* shmem_get_folio()로 모든 페이지를 RAM에 고정 */
/* 3단계: GPU 렌더링 완료 후 언핀 */
i915_gem_object_unpin_pages(obj);
/* 메모리 압력 시 kswapd가 스왑 아웃 가능 */
/* 4단계: 재사용 시 스왑에서 복원 */
i915_gem_object_pin_pages(obj);
/* shmem_swapin_folio()로 스왑에서 다시 읽기 */
빌드 시스템 최적화
# 리눅스 커널 빌드 시 tmpfs 활용
mount -t tmpfs -o size=8G tmpfs /tmp/kernel-build
cd /tmp/kernel-build
tar xf linux-6.x.tar.xz
cd linux-6.x
make defconfig
make -j$(nproc)
# 디스크 I/O 병목 없이 빌드 속도 극대화
# ccache와 tmpfs 조합
export CCACHE_TEMPDIR=/tmp/ccache_tmp
mkdir -p $CCACHE_TEMPDIR
shmem operations 전체 구조
shmem은 VFS의 모든 주요 operations 구조체를 구현합니다. 각 operations는 shmem의 RAM 기반 특성에 맞게 최적화되어 있습니다.
static const struct address_space_operations shmem_aops = {
.writepage = shmem_writepage,
.dirty_folio = noop_dirty_folio,
#ifdef CONFIG_TMPFS
.write_begin = shmem_write_begin,
.write_end = shmem_write_end,
#endif
.migrate_folio = migrate_folio,
.error_remove_folio = shmem_error_remove_folio,
};
static const struct vm_operations_struct shmem_vm_ops = {
.fault = shmem_fault,
.map_pages = filemap_map_pages,
#ifdef CONFIG_NUMA
.set_policy = shmem_set_policy,
.get_policy = shmem_get_policy,
#endif
};
fallocate와 hole punch
tmpfs는 fallocate() 시스템 콜을 지원하여 공간 사전 할당과
파일 중간의 구멍(hole) 생성을 지원합니다.
#include <fcntl.h>
#include <linux/falloc.h>
#include <unistd.h>
int main(void)
{
int fd = open("/tmp/test_fallocate",
O_CREAT | O_RDWR, 0644);
/* 공간 사전 할당 (1MB) */
fallocate(fd, 0, 0, 1048576);
/* 파일 중간에 hole punch (128KB 영역 해제) */
fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE,
4096, 131072);
/* 파일 끝부분 제거 (collapse) */
fallocate(fd, FALLOC_FL_COLLAPSE_RANGE,
524288, 524288);
close(fd);
return 0;
}
코드 설명
- 11행 기본 fallocate: 1MB 분량의 페이지를 사전 할당합니다. 디스크 파일시스템과 달리 shmem에서는 실제 RAM 페이지가 할당됩니다.
-
14-15행
FALLOC_FL_PUNCH_HOLE: 파일 크기는 유지하면서 지정 범위의 페이지를 해제합니다. RAM을 즉시 반환하므로 메모리 절약에 유용합니다. -
18-19행
FALLOC_FL_COLLAPSE_RANGE: 지정 범위를 제거하고 뒤쪽 데이터를 앞으로 당깁니다. 파일 크기가 줄어듭니다.
shmem_fallocate() 커널 내부 구현
shmem_fallocate()는 tmpfs 파일에 대한 공간 사전 할당과 hole punch를
처리하는 커널 함수입니다. 디스크 파일시스템과 달리 실제 RAM 페이지를 할당하므로,
fallocate() 호출 시 즉시 메모리를 소비합니다.
/* mm/shmem.c — shmem_fallocate() 핵심 경로 */
static long shmem_fallocate(struct file *file,
int mode, loff_t offset, loff_t len)
{
struct inode *inode = file_inode(file);
struct shmem_inode_info *info = SHMEM_I(inode);
/* seals 검사: F_SEAL_WRITE → 쓰기 불가 */
if (info->seals & (F_SEAL_WRITE | F_SEAL_GROW))
return -EPERM;
if (mode & FALLOC_FL_PUNCH_HOLE) {
/* hole punch: 지정 범위의 페이지 해제
* → 즉시 메모리 반환, 파일 크기는 유지 */
shmem_undo_range(inode, offset,
offset + len - 1, true);
shmem_inode_unacct_blocks(inode, ...);
return 0;
}
if (mode & FALLOC_FL_COLLAPSE_RANGE) {
/* 범위를 제거하고 뒤쪽을 앞으로 이동 */
shmem_collapse_range(inode, offset, len);
i_size_write(inode, inode->i_size - len);
return 0;
}
/* 기본: 공간 사전 할당 */
for (index = start; index < end; index++) {
/* SGP_FALLOC: 할당만, dirty/uptodate 마크 안 함 */
error = shmem_get_folio(inode, index,
&folio, SGP_FALLOC);
if (error)
break;
folio_unlock(folio);
folio_put(folio);
cond_resched(); /* 대용량 할당 시 CPU 양보 */
}
return error;
}
FALLOC_FL_PUNCH_HOLE은 tmpfs에서 메모리를 즉시 반환하는
유일한 방법 중 하나입니다. 대용량 tmpfs 파일의 일부만 더 이상 필요하지 않을 때,
파일을 삭제하지 않고도 특정 범위의 메모리를 반환할 수 있습니다.
예를 들어 데이터베이스의 tmpfs 임시 테이블에서 처리 완료된 구간을 punch hole로 해제하면
메모리를 효율적으로 관리할 수 있습니다.
devtmpfs (/dev 자동 관리)
devtmpfs는 커널이 자동으로 디바이스 노드를 생성하는 특수한 tmpfs 변형입니다. 부팅 초기에 /dev에 마운트되어, 커널이 디바이스를 감지할 때마다 해당 디바이스 노드(/dev/sda, /dev/tty0 등)를 자동으로 생성합니다.
/* drivers/base/devtmpfs.c — devtmpfs 핵심 구현 */
/*
* devtmpfs는 kdevtmpfs 커널 스레드가 관리합니다.
* device_add() → devtmpfs_create_node()
* device_del() → devtmpfs_delete_node()
*/
static struct file_system_type dev_fs_type = {
.name = "devtmpfs",
.init_fs_context = shmem_init_fs_context, /* tmpfs 기반! */
/* CONFIG_TMPFS 비활성화 시 ramfs 기반으로 폴백 */
};
/* 디바이스 추가 시 노드 자동 생성 */
static int handle_create(const char *name,
umode_t mode, kuid_t uid,
kgid_t gid, struct device *dev)
{
struct dentry *dentry;
struct path path;
/* 필요한 중간 디렉토리 자동 생성 (/dev/bus/usb 등) */
dentry = kern_path_create(AT_FDCWD, name, &path, 0);
/* mknod로 디바이스 노드 생성 */
vfs_mknod(&nop_mnt_idmap, d_inode(path.dentry),
dentry, mode, dev->devt);
/* 소유권/퍼미션 설정 */
vfs_fchown(dentry, uid, gid);
vfs_fchmod(dentry, mode);
return 0;
}
/* kdevtmpfs 스레드: 요청 큐를 처리하는 루프 */
static int devtmpfsd(void *p)
{
while (1) {
wait_for_completion(&req_done);
/* 큐의 create/delete 요청 순차 처리 */
handle(requests->name, requests->mode, ...);
}
}
# devtmpfs 마운트 확인
mount | grep devtmpfs
# devtmpfs on /dev type devtmpfs (rw,nosuid,size=8168440k,nr_inodes=2042110,mode=755)
# devtmpfs가 자동 생성한 노드 vs udev가 보정한 노드
ls -la /dev/sda
# brw-rw---- 1 root disk 8, 0 ... /dev/sda ← udev가 퍼미션/그룹 설정
# udev 없이 devtmpfs만으로 부팅 가능 (임베디드/rescue)
# 커널 파라미터: devtmpfs.mount=1
# → 커널이 /dev를 직접 마운트 (initramfs 이전)
# devtmpfs 비활성화 (매우 드묾)
grep CONFIG_DEVTMPFS /boot/config-$(uname -r)
# CONFIG_DEVTMPFS=y
# CONFIG_DEVTMPFS_MOUNT=y ← 커널이 자동 마운트
# kdevtmpfs 커널 스레드 확인
ps aux | grep kdevtmpfs
# root 27 0.0 0.0 0 0 ? S ... [kdevtmpfs]
/dev/sda 등)를 즉시 생성합니다. udev는 이후 netlink uevent를 받아 심볼릭 링크(/dev/disk/by-id/..., /dev/disk/by-uuid/...), 퍼미션, 소유자 등을 rules에 따라 보정합니다. devtmpfs 덕분에 udev가 아직 시작되지 않은 부팅 초기에도 디바이스에 접근할 수 있으며, 임베디드 환경에서는 udev 없이 devtmpfs만으로 운영이 가능합니다.
userfaultfd와 shmem
userfaultfd(UFFD)는 페이지 폴트(Page Fault) 처리를 사용자 공간에서 수행할 수 있게 하는
메커니즘입니다. shmem/tmpfs는 UFFD를 완전히 지원하며, 이를 통해
라이브 마이그레이션(Live Migration), 포스트카피(Postcopy) 메모리 전송,
지연 복원(Lazy Restore) 등의 고급 기능을 구현할 수 있습니다.
/* userfaultfd shmem 관련 코드 경로 */
/* UFFDIO_COPY: 사용자 공간에서 shmem 페이지 설치 */
/* mm/userfaultfd.c */
static int shmem_mfill_atomic_pte(
struct mm_struct *mm,
pmd_t *pmd,
unsigned long addr,
unsigned long src_addr,
struct page **pagep,
bool wp_copy)
{
struct inode *inode = file_inode(dst_vma->vm_file);
struct folio *folio;
/* shmem folio 할당 */
folio = shmem_alloc_folio(gfp, info, pgoff);
/* 사용자 공간에서 데이터 복사 */
copy_from_user(folio_address(folio),
src_addr, PAGE_SIZE);
/* shmem XArray에 folio 삽입 */
shmem_add_to_page_cache(folio,
inode->i_mapping, pgoff, ...);
/* PTE 설정 */
__SetPageUptodate(page);
set_pte_at(mm, addr, pte, mk_pte(page, ...));
return 0;
}
/* userfaultfd 기능 플래그 */
#define UFFD_FEATURE_MISSING_SHMEM (1 << 3)
#define UFFD_FEATURE_MINOR_SHMEM (1 << 6)
/* MISSING: 미할당 페이지 폴트 처리
* MINOR: 이미 할당된 페이지의 내용 업데이트 */
코드 설명
- UFFD_FEATURE_MISSING_SHMEM shmem 매핑에서 아직 할당되지 않은 페이지에 접근 시 UFFD 이벤트를 생성합니다. QEMU 포스트카피 마이그레이션에서 사용되며, VM 메모리가 shmem 기반(memfd)일 때 필요합니다.
- UFFD_FEATURE_MINOR_SHMEM 이미 페이지 캐시에 존재하는 shmem 페이지의 내용을 업데이트할 때 사용합니다. QEMU의 "배경 마이그레이션(Background Migration)"에서 활용되며, VM이 실행 중인 상태에서 페이지 내용을 교체합니다.
-
shmem_mfill_atomic_pte
UFFDIO_COPYioctl의 shmem 전용 구현입니다. 사용자 공간 버퍼에서 데이터를 복사하고, shmem의 페이지 캐시(XArray)에 삽입한 뒤, PTE를 설정합니다. 이 과정이 원자적(Atomic)으로 수행되어 경쟁 조건(Race Condition)을 방지합니다.
- QEMU/KVM 라이브 마이그레이션: VM 메모리를
memfd_create()로 할당하고, 포스트카피 방식으로 필요한 페이지만 전송 - CRIU (Checkpoint/Restore In Userspace): 컨테이너 체크포인트 후 지연 복원 시 shmem 영역을 UFFD로 관리
- 분산 공유 메모리(DSM): 원격 노드의 shmem 페이지를 UFFD를 통해 온디맨드(On-demand) 로딩
흔한 실수와 트러블슈팅
실무에서 자주 발생하는 문제
| 증상 | 원인 | 해결 방법 |
|---|---|---|
| 시스템 OOM 발생, tmpfs가 RAM 대부분 사용 | size= 미설정 (기본 50%) |
mount -o remount,size=2G /tmp |
swapoff이 매우 오래 걸림/실패 |
대용량 shmem 데이터가 스왑에 존재 | 여유 RAM 확보 후 시도, 또는 tmpfs 파일 정리 후 swapoff |
| 컨테이너 OOM, 원인 불명 | tmpfs 사용량이 memcg 한도에 포함 | memory.stat의 shmem 확인, tmpfs size= 조정 |
/dev/shm 공간 부족 |
POSIX SHM 세그먼트 미정리 | ls -la /dev/shm/으로 확인, 오래된 세그먼트 삭제 |
| THP 활성화 후 메모리 사용량 급증 | huge=always로 내부 단편화 발생 |
huge=within_size 또는 advise로 변경 |
| tmpfs에서 실행 파일 실행 차단됨 | noexec 마운트 옵션 |
의도적 보안 설정이 아니면 remount로 해제 |
memfd_create() 후 쓰기 실패 |
F_SEAL_WRITE 씰 적용됨 |
fcntl(fd, F_GET_SEALS)로 씰 확인 |
| 재부팅 후 tmpfs 데이터 소실 | tmpfs는 휘발성(Volatile)! | 영구 저장이 필요하면 디스크 파일시스템 사용 |
디버깅 명령어 모음
# ═══ shmem 시스템 전체 상태 확인 ═══
# 1. 전체 shmem 메모리 사용량
grep -E "Shmem|ShmemHuge|ShmemPmd" /proc/meminfo
# Shmem: 1048576 kB ← 총 shmem 사용량 (RAM + 스왑)
# ShmemHugePages: 524288 kB ← THP로 할당된 shmem
# ShmemPmdMapped: 262144 kB ← PMD 매핑된 shmem THP
# 2. tmpfs 마운트별 사용량
df -h --type=tmpfs
# 3. /dev/shm 내용 확인 (POSIX SHM 세그먼트)
ls -lah /dev/shm/
# 4. System V SHM 세그먼트 확인
ipcs -m
# ------ Shared Memory Segments --------
# key shmid owner perms bytes nattch
# 0x00001234 12345 root 666 4096 2
# 5. 프로세스별 shmem 매핑 확인
grep -c "shmem" /proc/<pid>/maps
pmap <pid> | grep "/dev/shm\|/tmp\|SYSV"
# 6. shmem 관련 vmstat 카운터
grep -i shmem /proc/vmstat
# nr_shmem 262144
# nr_shmem_hugepages 128
# nr_shmem_pmdmapped 96
# 7. 스왑에 있는 shmem 페이지 확인
# (직접 확인 방법은 없으나, 간접적으로:)
cat /proc/meminfo | grep SwapCached
swapon --show
# 8. memfd 파일 확인 (procfs)
ls -la /proc/<pid>/fd/ | grep "memfd"
# lrwx------ ... 3 -> /memfd:my_buffer (deleted)
# ═══ 트러블슈팅 원라이너 ═══
# tmpfs가 시스템 메모리의 몇 %를 사용하는지 확인
awk '/^Shmem:/{s=$2} /^MemTotal:/{t=$2} END{printf "shmem: %.1f%%\n", s/t*100}' /proc/meminfo
# 가장 큰 /dev/shm 파일 찾기
du -sh /dev/shm/* 2>/dev/null | sort -rh | head -5
# 오래된 SysV SHM 세그먼트 정리 (attach 카운트 0인 것)
ipcs -m | awk '$6==0 {print $2}' | xargs -r -n1 ipcrm -m
커널 로그 분석
# tmpfs 관련 커널 로그 확인
dmesg | grep -iE "tmpfs|shmem|devtmpfs"
# [ 0.123456] shmem: enabled
# [ 0.234567] devtmpfs: initialized
# OOM killer 로그에서 shmem 확인
dmesg | grep -A 20 "Out of memory"
# oom-kill:constraint=CONSTRAINT_MEMCG
# ... Shmem:1048576kB ...
# ftrace로 shmem_fault 추적 (고급)
echo 1 > /sys/kernel/debug/tracing/events/filemap/mm_filemap_add_to_page_cache/enable
echo 'shmem' > /sys/kernel/debug/tracing/trace_options
cat /sys/kernel/debug/tracing/trace_pipe
/proc/meminfo의Shmem값이 예상보다 크지 않은지 확인df -h --type=tmpfs로 각 마운트의 실제 사용량 파악vmstat에서si/so(swap in/out) 값이 높으면 tmpfs 스왑 아웃 의심- 컨테이너 환경이면
memory.stat의 shmem 확인 - THP 관련이면
/proc/vmstat의thp_file_*카운터 확인
참고자료
- Linux Kernel Documentation: tmpfs
- mm/shmem.c 소스 코드 (Bootlin)
- include/linux/shmem_fs.h
- tmpfs(5) man page
- memfd_create(2) man page
- shm_open(3) man page
- shmget(2) man page
- LWN: Transparent huge pages for tmpfs
- LWN: Sealing memfd
- Transparent Hugepage Support
- userfaultfd(2) man page
- fallocate(2) man page
- Kernel Documentation: userfaultfd
- cgroup v2: Memory Controller (shmem accounting)
- mm/userfaultfd.c 소스 코드 (Bootlin)
- drivers/base/devtmpfs.c 소스 코드 (Bootlin)
- shm_overview(7) — POSIX 공유 메모리 개요
- LWN: Transparent huge pages in tmpfs (2012) — tmpfs에서의 THP 도입 배경
- VFS (가상 파일시스템) — shmem이 구현하는 VFS 인터페이스 상세
- Page Cache — shmem이 데이터를 저장하는 페이지 캐시 내부
- Swapping 서브시스템 — shmem 스왑 아웃 경로 상세
- 메모리 관리 개요 — 물리 메모리(Physical Memory) 할당과 회수 전체 구조