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), 메모리 압력 대응, 보안 설정, 성능 튜닝까지 전 영역을 깊이 있게 다룹니다.

전제 조건: VFS (가상 파일시스템)메모리 관리(Memory Management) 개요 문서를 먼저 읽으세요. 페이지(Page) 할당, VMA(Virtual Memory Area), 스왑(swap)의 기본 개념을 이해하고 있어야 합니다.
일상 비유: tmpfs는 화이트보드와 비슷합니다. 디스크에 기록하는 일반 파일시스템이 노트에 펜으로 쓰는 것이라면, tmpfs는 화이트보드에 마커로 쓰는 것입니다. 매우 빠르지만 전원을 끄면(재부팅하면) 내용이 사라집니다. 다만 화이트보드가 꽉 차면 사진을 찍어 보관할 수 있듯이(스왑), tmpfs도 메모리가 부족하면 데이터를 스왑 공간으로 내보낼 수 있습니다.

핵심 요약

  • shmem — 커널 내부 이름. mm/shmem.c에 구현된 RAM 기반 파일시스템 핵심 코드
  • tmpfs — 사용자 공간에서 마운트하는 이름. mount -t tmpfs로 사용
  • 스왑 가능 — 일반 RAM 페이지처럼 메모리 부족 시 스왑 아웃 가능 (ramfs와의 핵심 차이)
  • 크기 제한 — 마운트 시 size= 옵션으로 최대 크기를 제한하여 메모리 고갈 방지
  • 다중 사용처/tmp, /dev/shm, /run, memfd, DRM GEM 등 커널 전반에 활용

단계별 이해

  1. VFS 계층
    shmem은 VFS의 file_system_type으로 등록됩니다. super_operations, inode_operations, file_operations를 구현하여 일반 파일시스템처럼 동작합니다.
  2. 페이지 캐시(Page Cache) 활용
    파일 데이터는 페이지 캐시(page cache)에 저장됩니다. 디스크 블록 장치(Block Device)가 없으므로 페이지 캐시 자체가 유일한 저장소입니다.
  3. 스왑 백엔드
    메모리 압력이 발생하면 shmem 페이지는 스왑 공간으로 내보내집니다. 다시 접근하면 스왑에서 읽어와 페이지 캐시에 복원합니다.
  4. 사용자 공간 인터페이스
    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의 차이

특성ramfstmpfs (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) 인터페이스를 완전히 구현하는 파일시스템입니다. 일반적인 디스크 파일시스템과 달리 블록 장치 대신 페이지 캐시와 스왑 공간을 백엔드로 사용합니다.

사용자 공간 (User Space) mount -t tmpfs shm_open() shmget()/shmat() memfd_create() DRM GEM (GPU) VFS (Virtual File System) file_operations / inode_operations / super_operations shmem (mm/shmem.c) shmem_inode_info / shmem_get_folio / shmem_writepage / shmem_file_setup 페이지 캐시 (Page Cache) folio / address_space / XArray 스왑 백엔드 (Swap) swap_entry_t / swap cache 읽기/쓰기 메모리 압력 물리 RAM (Physical Memory) 스왑 장치 (Disk / zram) 스왑 인/아웃 THP (2MB huge pages)

VFS 인터페이스

shmem은 struct file_system_type으로 등록되어 VFS 계층과 통합됩니다. shmem_fs_typetmpfs라는 이름으로 등록되며, 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_mntshmem_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=neverTHP 정책: never, always, within_size, advise
noexecoff실행 파일 실행 금지
nosuidoffsetuid/setgid 비트 무시
nodevoff디바이스 파일 사용 금지
inode64off64비트 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 정책 등 모든 동작을 체계적으로 파악할 수 있습니다.

SHMEM_I() 매크로: VFS 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) 경로, swaplistswapoff 시 전체 복원 경로에서 사용됩니다. 두 리스트 모두 필요할 때만 등록되어 오버헤드(Overhead)를 최소화합니다.
  • vfs_inode를 마지막에 배치하는 것은 커널의 임베딩 패턴(Embedding Pattern)입니다. VFS가 inode 포인터(Pointer)를 전달하면, container_of()로 역산하여 shmem 전용 메타데이터에 접근합니다.

shmem_get_folio() 흐름

shmem_get_folio()은 shmem 파일의 페이지에 접근하는 핵심 함수입니다. 페이지 캐시를 먼저 검색하고, 없으면 스왑에서 복원하거나 새로 할당합니다.

shmem_get_folio() filemap_get_folio() 페이지 캐시 검색 캐시 히트? folio 반환 XArray에 swap_entry 존재? 아니오 스왑? shmem_swapin_folio() shmem_alloc_folio() 아니오 shmem_add_to_page_cache() XArray 삽입 folio 반환
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 다이어그램으로 나타냅니다.

사용자 공간: mmap 접근 / read / write VFS → shmem_fault() shmem_get_folio() shmem_get_folio_gfp() GFP 플래그 + sgp_type 전달 filemap_get_folio(mapping, index) 캐시 히트 → 반환 swap entry? 미스 shmem_swapin_folio() swap_read_folio() → 페이지 캐시 삽입 shmem_alloc_and_add_folio() 아니오 shmem_alloc_folio(gfp, mpol) shmem_add_to_page_cache() folio 반환 alloc_pages_mpol() 호출
호출 체인 핵심 포인트: 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_type 열거형: 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 고유의 할당/스왑인 경로를 사용합니다.

사용자 공간: mmap 영역 접근 → 페이지 폴트 handle_mm_fault() → do_fault() shmem_fault(vmf) vm_ops->fault inode = file_inode(vmf->vma->vm_file) huge fault? (PMD 정렬 + THP 활성) shmem_get_folio(order=HPAGE) shmem_get_folio(inode, index, &folio, sgp) SGP_READ / SGP_CACHE / SGP_WRITE 아니오 sgp_type 의미 SGP_READ: 없으면 NULL SGP_CACHE: 할당+캐시 SGP_WRITE: 할당+dirty 성공? vmf->page = folio_page() VM_FAULT_LOCKED 반환 VM_FAULT_SIGBUS / VM_FAULT_OOM ENOMEM / ENOSPC / EIO 아니오 finish_fault(): PTE 설정 → TLB flush 사용자 공간으로 복귀
/* 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_READread() 시스템 콜 경로에서만 사용되며, 페이지가 없으면 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 할당을 시도합니다.
shmem_fault() vs filemap_fault(): 일반 파일시스템(ext4, XFS)은 filemap_fault()를 사용하여 디스크에서 페이지를 읽어옵니다. shmem은 디스크 백엔드가 없으므로 자체 shmem_fault()를 구현합니다. 주요 차이점: ① readahead 없음 (디스크 I/O 불필요), ② 스왑에서 복원 경로 존재, ③ THP를 huge_fault로 직접 지원.

페이지 할당과 스왑 연동

페이지 할당 경로

shmem의 페이지 할당은 shmem_alloc_folio()를 통해 이루어집니다. NUMA 정책, THP 여부, GFP 플래그에 따라 적절한 할당 경로를 선택합니다.

shmem_alloc_folio() NUMA 정책 확인 (mpol) THP 할당 시도 (2MB) 4KB 기본 페이지 할당 페이지 캐시 (address_space XArray) folio / swap_entry_t 혼합 저장 kswapd / 직접 회수 shmem_writepage() 스왑 캐시 / 스왑 장치 XArray에 swap_entry_t 기록 (folio 자리 대체) 메모리 압력 shmem_swapin_folio(): 스왑에서 복원 → 페이지 캐시 삽입

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()가 핵심 역할을 합니다.

메모리 압력 (Memory Pressure) kswapd / 직접 회수 shrink_folio_list() shmem_writepage() ① folio_alloc_swap() 스왑 슬롯 할당 ② shmem_replace_folio() XArray: folio → swap_entry_t ③ swap_writepage() 디스크/zswap/zram 기록 XArray 상태 변환 변경 전: slot[index] = folio * (실제 메모리 페이지 참조) 변경 후: slot[index] = swp_entry_t (스왑 장치 오프셋 인코딩) info->alloced-- (RAM 페이지 감소) folio 해제 → 물리 메모리 반환 info->swapped++ (스왑 페이지 증가) alloced + swapped = 일정 복원: shmem_swapin_folio() swap_read_folio() → XArray에 folio 복원 → swapped-- / alloced++
/* 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 낭비를 줄입니다.
스왑 없는 시스템에서의 위험: 스왑이 비활성화된 상태에서 tmpfs 사용량이 물리 메모리(Physical Memory)를 초과하면, 페이지를 회수할 방법이 없어 OOM killer가 작동합니다. 프로덕션 환경에서 tmpfs를 사용할 때는 size= 옵션으로 최대 크기를 제한하거나 스왑을 활성화하세요.

shmem_unuse() — swapoff 시 shmem 복원 경로

swapoff 명령을 실행하면 스왑 장치의 모든 페이지를 RAM으로 복원해야 합니다. shmem 페이지의 경우 전역 shmem_swaplist를 순회하며 shmem_unuse()로 각 inode의 스왑 엔트리를 복원합니다. 이 과정은 시간이 오래 걸릴 수 있으며, 충분한 RAM이 없으면 실패합니다.

swapoff /dev/sdX (또는 swapoff -a) sys_swapoff() → try_to_unuse() shmem_swaplist 순회 list_for_each_entry(info, &shmem_swaplist, swaplist) shmem_unuse(info, swap_entry) 해당 inode의 XArray에서 swap_entry 검색 ① XArray 스캔 xa_for_each(): swap_entry 위치 탐색 ② 스왑에서 읽기 shmem_swapin_folio() ③ XArray 복원 swap_entry → folio * info->swapped-- / alloced++ swapped==0 → swaplist에서 제거 스왑 장치 해제 완료
/* 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 성능 주의: 대용량 tmpfs 데이터가 스왑에 있을 때 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 미스를 줄이고 대용량 파일 접근 성능을 크게 향상시킬 수 있습니다.

tmpfs THP 할당 결정 흐름 huge= 마운트 옵션 확인 never (기본) always within_size advise force 4KB 페이지만 사용 항상 2MB 시도 i_size 범위 내만 2MB MADV_HUGEPAGE 시 2MB compound folio 할당 시도 → 실패 시 4KB 폴백 (order-0) compaction / reclaim 후 재시도 가능 /sys/kernel/mm/transparent_hugepage/shmem_enabled: always within_size advise never deny force

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
주의: THP를 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) 사이의 균형을 맞출 수 있습니다.

shmem Folio 크기 스펙트럼 4KB order-0 기본 16KB order-2 4 페이지 64KB order-4 16 페이지 256KB order-6 64 페이지 2MB (PMD) order-9 512 페이지 compound folio 낮은 단편화 높은 TLB 효율 mTHP: 최적 균형 shmem_alloc_and_add_folio() order 결정 로직 ① huge=always → order-9(2MB) 시도 → 실패 시 중간 order 폴백 ② huge=within_size → i_size 기반 최적 order 선택 ③ 최종 폴백: order-0 (4KB) — 항상 성공 보장
# 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
mTHP 실용 가이드: 64KB folio(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 위에 파일을 생성하는 방식으로 동작합니다.

프로세스 A (생산자) fd = shm_open("/buf", O_CREAT|O_RDWR, 0666); ftruncate(fd, 4096); ptr = mmap(fd, ...) 프로세스 B (소비자) fd = shm_open("/buf", O_RDONLY, 0); ptr = mmap(fd, ...) 데이터 읽기 /dev/shm (tmpfs) /dev/shm/buf (shmem inode) shm_open + mmap shm_open + mmap 공유 물리 페이지 (RAM) 동일한 folio를 두 프로세스가 매핑 프로세스 A 가상 주소 공간 프로세스 B 가상 주소 공간

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)
APIshmget/shmat/shmdt/shmctlshm_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 백업 저장소를 활용합니다.

shmem_file_setup() mm/shmem.c | kern_mount(shm_mnt) 위에 파일 생성 DRM GEM 객체 i915, amdgpu, nouveau memfd_create() 익명 공유 메모리 + seals System V SHM ipc/shm.c newseg() splice / pipe zero-copy 전송 shmem inode + folio 페이지 캐시 저장 스왑 백엔드 swap_entry_t 관리 mmap(): vm_area_struct → shmem_vm_ops → shmem_fault()

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)을 생성합니다. 함수의 내부 동작을 단계별로 분석합니다.

memfd_create() ipc/shm.c newseg() drm_gem_object_init() __shmem_file_setup() shmem_file_setup(name, size, flags) 또는 shmem_file_setup_with_mnt() ① 크기 검증 size > SHMEM_MAX_BYTES → -EINVAL ② shmem_get_inode() shmem_inode_info + inode 할당 ③ d_alloc_anon() 익명 dentry 생성 ④ alloc_file_pseudo(shm_mnt, dentry, name, flags) struct file 할당 + f_op = shmem_file_operations 설정 shm_mnt 커널 내부 마운트 struct file * 반환 memfd_create 후처리 fd_install(fd, file) MFD_ALLOW_SEALING → seals 초기화 SysV SHM 후처리 shp->shm_file = file ipc_addid() → shmid 발급 DRM GEM 후처리 obj->filp = file GPU 페이지 요청 시 shmem_read_folio()
/* 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()로 사용자 공간에 전달되거나, 커널 내부에서 직접 사용됩니다.
memfd vs SysV SHM vs DRM GEM 비교: 세 서브시스템 모두 shmem_file_setup()으로 shmem 파일을 생성하지만, 이후 사용 방식이 다릅니다. memfd는 fd를 SCM_RIGHTS로 다른 프로세스(Process)에 전달하고, SysV SHMshmid 정수 키(Key)로 프로세스 간 공유하며, DRM GEM은 GPU 드라이버(Driver)가 내부적으로 버퍼(Buffer) 메모리를 관리합니다. shmem이 이 모든 것의 공통 기반인 이유는, RAM 백업 + 스왑 가능 + 페이지 캐시 통합이라는 세 가지 특성을 동시에 제공하기 때문입니다.

tmpfs와 메모리 압력

tmpfs의 가장 중요한 특성 중 하나는 메모리 압력(memory pressure) 시 일반 페이지 캐시처럼 회수(reclaim)될 수 있다는 점입니다. ramfs와 달리 shmem 페이지는 LRU 목록에 포함되어 kswapd의 관리를 받습니다.

shmem 메모리 회수 흐름 메모리 압력 발생 (low watermark) kswapd (비동기) 직접 회수 (동기) shrink_folio_list() LRU 스캔 shmem folio? shmem_writepage() 일반 writeback 경로 아니오 스왑 공간에 기록 + 해제 스왑 없으면 OOM killer
위험: 스왑 공간이 없거나 부족한 상태에서 tmpfs가 대량의 메모리를 사용하면 OOM(Out Of Memory) killer가 호출될 수 있습니다. 프로덕션 환경에서는 반드시 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과 memory cgroup 연동 컨테이너 A (memory.max=2G) 프로세스 메모리 tmpfs (/tmp) 사용 memory.stat shmem 524288 | file 1048576 | anon 786432 컨테이너 B (memory.max=1G) 프로세스 메모리 /dev/shm 사용 memory.stat shmem 262144 | file 0 | anon 524288 memory cgroup 한도 초과 시 회수 경로 mem_cgroup_reclaim() → shrink_lruvec() → shmem 페이지 대상 포함 스왑 가능 shmem_writepage() → 스왑 아웃 스왑 불가 noswap 또는 스왑 공간 부족 OOM kill cgroup 내 프로세스 종료 shmem 페이지 충전(charge): mem_cgroup_charge() → folio->memcg_data 설정
/* 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 함정: Docker의 --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 + 메모리 부족 = OOM: noswap이 설정된 tmpfs의 페이지는 메모리 압력에서도 회수할 수 없습니다. 이는 mlock()과 유사한 효과를 가지며, 크기 제한 없이 사용하면 시스템 전체의 OOM을 유발할 수 있습니다. 반드시 size= 옵션과 함께 사용하세요.

커널 설정 (CONFIG_SHMEM, CONFIG_TMPFS)

관련 커널 설정 옵션

설정기본값설명
CONFIG_SHMEMyshmem 핵심 구현 활성화. 비활성화 시 ramfs로 대체
CONFIG_TMPFSytmpfs 사용자 공간 마운트 지원
CONFIG_TMPFS_POSIX_ACLytmpfs에서 POSIX ACL 지원
CONFIG_TMPFS_XATTRytmpfs에서 확장 속성(xattr) 지원
CONFIG_TMPFS_INODE64y64비트 inode 번호 기본 활성화
CONFIG_TRANSPARENT_HUGEPAGEyTHP 지원 (tmpfs 포함)
CONFIG_SWAPy스왑 서브시스템 활성화 (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 프로그램을 배치하는 것을 방지해야 합니다.

tmpfs 보안 계층 마운트 옵션 보안 noexec nosuid nodev size= (크기 제한) 파일/디렉터리 퍼미션 mode=1777 (sticky bit) / uid= / gid= / POSIX ACL 커널 보안 모듈 (LSM) SELinux / AppArmor / seccomp (tmpfs 접근 제어) 네임스페이스 / cgroup mount namespace 격리 / memory cgroup 메모리 제한 심층 방어

보안 강화 마운트 예시

# /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 성능 비교

워크로드tmpfsext4 (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
핵심 포인트: tmpfs는 디스크 I/O가 없으므로 순수 메모리 대역폭(Bandwidth)에 가까운 성능을 제공합니다. 빌드 시스템(Build System)의 임시 파일, 데이터베이스 임시 테이블스페이스, 컴파일 캐시(Cache) 등에 활용하면 큰 성능 이점을 얻을 수 있습니다.

주요 튜닝 포인트

# 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/shmPOSIX 공유 메모리 (shm_open)RAM 50%
/run런타임 데이터 (PID 파일, 소켓(Socket))RAM 20% 또는 800MB
/run/lock잠금(Lock) 파일5MB
/sys/fs/cgroupcgroup 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 기반 특성에 맞게 최적화되어 있습니다.

shmem (VFS 구현) super_operations alloc_inode destroy_inode statfs / evict_inode inode_operations setattr / getattr tmpfile (O_TMPFILE) fileattr_set/get file_operations read_iter / write_iter (generic) splice_read / splice_write mmap / fallocate / llseek address_space_operations writepage → shmem_writepage write_begin / write_end dirty_folio / migrate_folio vm_operations_struct fault → shmem_fault huge_fault → shmem_huge_fault shmem_dir_inode_operations create / link / unlink / mkdir / rmdir / rename / symlink / mknod tmpfile (O_TMPFILE 지원) / get_offset_ctx
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() 호출 시 즉시 메모리를 소비합니다.

shmem_fallocate() 내부 동작 shmem_fallocate(file, mode, ...) PUNCH_HOLE? COLLAPSE? INSERT? PUNCH_HOLE shmem_undo_range() 지정 범위 folio 해제 + swap 해제 사전 할당 (기본 모드) for (index = start; index < end; index++) shmem_get_folio(SGP_FALLOC) → 실제 RAM 할당 기본 COLLAPSE_RANGE / INSERT_RANGE COLLAPSE: 범위 제거 → 뒤쪽 데이터 앞으로 INSERT: 범위 삽입 → 뒤쪽 데이터 뒤로 shmem_undo_range() 상세 ① XArray에서 folio/swap_entry 제거 ② folio→folio_put(), swap→free_swap_and_cache() ③ info->alloced-- / info->swapped-- SGP_FALLOC 특수 동작 ① folio를 할당하지만 i_size는 변경하지 않음 ② info->fallocend = end (예약 끝 위치 기록) ③ 이 folio는 i_size 밖이므로 truncate 시 해제됨 hole punch → 즉시 RAM 반환 | 사전 할당 → 즉시 RAM 소비 (lazy 아님)
/* 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 + udev 디바이스 노드 관리 흐름 커널 device_add() devtmpfs kdevtmpfs 스레드 /dev/sda 노드 생성 uevent (netlink) kobject_uevent() udevd (유저스페이스) rules 적용 /dev (최종) /dev/sda (devtmpfs) /dev/disk/by-id/... (udev symlink) 퍼미션: udev rule 소유자: udev rule 노드 즉시 생성 symlink/퍼미션/소유자 보정 동시
# 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]
devtmpfs vs udev 역할 분담: devtmpfs는 커널이 디바이스 감지 시 기본 노드(/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 라이브 마이그레이션 흐름 소스 호스트 (Source) VM 프로세스 shmem mmap 영역 마이그레이션 스레드 페이지 전송 shmem tmpfs (VM 메모리 백업) memfd_create() 기반 UFFD_FEATURE_MISSING_SHMEM 등록 대상 호스트 (Destination) VM 프로세스 실행 중 (폴트 대기) UFFD 핸들러 페이지 수신+설치 shmem tmpfs (빈 상태로 시작) 폴트 발생 시 소스에서 요청 ioctl(UFFDIO_COPY) → shmem 페이지 설치 네트워크 포스트카피 페이지 폴트 처리 흐름 (대상 호스트) ① 페이지 폴트 VM이 미설치 페이지 접근 ② UFFD 이벤트 커널 → UFFD 핸들러 전달 ③ 소스에서 수신 네트워크로 페이지 데이터 수신 ④ UFFDIO_COPY → shmem 페이지 설치 shmem_mfill_atomic_pte() → XArray에 folio 삽입 ⑤ VM 프로세스 재개 PTE 설정 완료 → 프로세스 깨움 → 접근 성공
/* 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_COPY ioctl의 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
성능 문제 체크리스트:
  1. /proc/meminfoShmem 값이 예상보다 크지 않은지 확인
  2. df -h --type=tmpfs로 각 마운트의 실제 사용량 파악
  3. vmstat에서 si/so(swap in/out) 값이 높으면 tmpfs 스왑 아웃 의심
  4. 컨테이너 환경이면 memory.stat의 shmem 확인
  5. THP 관련이면 /proc/vmstatthp_file_* 카운터 확인

참고자료

다음 학습: