관련 소스:mm/shmem.c,include/linux/shmem_fs.h
관련 문서: 메모리 관리 개요 · Folio / Page Cache · Swap / zswap · Memory Cgroup
tmpfs는 리눅스 커널의 가상 메모리 기반 파일 시스템으로, 모든 데이터가 물리 메모리에 저장되며 시스템 종료 시 사라집니다. shmem(shared memory)는 tmpfs의 내부 구현체로, SysV IPC 공유 메모리, POSIX 공유 메모리, mmap된 익명 파일 등을 구현합니다. tmpfs는 swap을 사용할 수 있어 외부 메모리로 쫓겨날 수 있으며, 이를 통해 물리 메모리보다 큰 가상 파일을 생성할 수 있습니다.
커널 내부에서 shmem은 파일 캐시(folio/page cache)와 swap 캐시를 연결하는 중간 다리 역할을 합니다. 페이지가 메모리에 있으면 file cache로, swap으로 쫓겨나면 swap entry로 관리됩니다. 이를 통해 anonymous shared memory와 file-backed memory를 동일한 인터페이스로 처리합니다.
운영 관점에서는 tmpfs를 "디스크 파일처럼 보이지만 실제로는 메모리 압박에 따라 swap까지 왕복하는 파일 객체"로 이해하면 편합니다. memfd_create(), /dev/shm, 브라우저/DB의 공유 메모리 세그먼트, 컨테이너 내부의 tmpfs 마운트가 모두 같은 shmem 코드 경로 위에서 동작하므로, page cache, reclaim, memcg, THP 설정이 함께 얽힙니다.
/* 주요 소스 파일 */
mm/shmem.c // shmem 핵심 구현
include/linux/shmem_fs.h // 핵심 구조체 정의
mm/shmem_quota.c // quota 지원
# tmpfs 마운트 확인
mount | grep tmpfs
# tmpfs 사용량 확인
df -h /dev/shm
df -h /tmp
# shmem 메모리 사용량 확인
cat /proc/meminfo | grep -i shmem
# 시스템 전체 shmem 페이지 수
cat /proc/vmstat | grep nr_shmem
# hugepage 사용 중인 shmem 페이지
cat /proc/vmstat | grep nr_shmem_thps
# 특정 프로세스의 shmem 사용량
pmap -x <PID> | grep shmem
# tmpfs 파일 시스템 정보
stat -f /dev/shm
# slabinfo에서 shmem 관련 slab 확인
cat /proc/slabinfo | grep shmem_inode_cache
# NUMA 노드별 shmem 할당 정책 확인
cat /proc/<PID>/numa_maps | grep shmem
# shmem THP 정책 확인
cat /sys/kernel/mm/transparent_hugepage/shmem_enabled
# tmpfs 마운트 옵션 확인
findmnt -no TARGET,OPTIONS /dev/shm
# 프로세스별 shared/tmpfs 매핑 크기 확인
grep -E 'Shared_Clean|Shared_Dirty|Anonymous|ShmemPmdMapped' /proc/<PID>/smaps
shmem inode의 핵심 확장 구조체. 일반 inode보다 큰 메모리 할당이 필요합니다.
/* include/linux/shmem_fs.h:36-59 */
struct shmem_inode_info {
spinlock_t lock;
unsigned int seals; // shmem seal 비트
unsigned long flags;
unsigned long alloced; // 파일에 할당된 데이터 페이지 수
unsigned long swapped; // swap으로 넘긴 페이지 수의 소계
union {
struct offset_ctx dir_offsets; // 안정적인 디렉터리 오프셋
struct {
struct list_head shrinklist; // 축소 가능한 huge folio inode 목록
struct list_head swaplist; // swap 위 inode 체인
};
};
struct timespec64 i_crtime; // 파일 생성 시간
struct shared_policy policy; // NUMA 메모리 할당 정책
struct simple_xattrs xattrs; // xattr 목록
pgoff_t fallocend; // 가장 높은 fallocate end index
unsigned int fsflags; // FS_IOC_[SG]ETFLAGS용
atomic_t stop_eviction; // inode 작업 중 hold
#ifdef CONFIG_TMPFS_QUOTA
struct dquot __rcu *i_dquot[MAXQUOTAS];
#endif
struct inode vfs_inode;
};
핵심 필드 설명:
alloced: file cache에 존재하는 페이지 수 (swap에 있는 것 포함)swapped: swap으로 쫓겨난 페이지 수. alloced == nrpages + swapped 관계shrinklist: 메모리 부족 시 huge page를 분할(fsplit)하기 위한 inode 목록swaplist: shmem_unuse()가 swap off 시 inode를 찾기 위한 체인superblock 수준의 shmem 메타데이터 관리 구조체.
/* include/linux/shmem_fs.h:73-92 */
struct shmem_sb_info {
unsigned long max_blocks; // 블록 수 제한
struct percpu_counter used_blocks; // 할당된 블록 수
unsigned long max_inodes; // inode 수 제한
unsigned long free_ispace; // 남은 inode 공간
raw_spinlock_t stat_lock; // sb_info 변경 직렬화
umode_t mode; // 루트 디렉토리 마운트 모드
unsigned char huge; // hugepage 사용 여부
kuid_t uid; // 루트 디렉토리 UID
kgid_t gid; // 루트 디렉토리 GID
bool full_inums; // ino uint/ino_t 선택
bool noswap; // swap 사용 불가 플래그
ino_t next_ino; // 다음 사용할 inode 번호
ino_t __percpu *ino_batch; // per-cpu inode 번호 배치
struct mempolicy *mpol; // 기본 NUMA 정책
spinlock_t shrinklist_lock; // shrinklist 보호
struct list_head shrinklist; // 축소 가능한 inode 목록
unsigned long shrinklist_len; // shrinklist 길이
struct shmem_quota_limits qlimits; // quota 제한
};
핵심 플래그:
SHMEM_F_NORESERVE (BIT(0)): 전체 객체 사전 할당 비활성화SHMEM_F_LOCKED (BIT(1)): swap 사용 불가 (SysV SHM_LOCK용)SHMEM_F_MAPPING_FROZEN (BIT(2)): grow/shrink/hole punch 불가 (folio pinning용)fallocate 작업 중 hole punch 대기 상태를 담는 구조체.
/* mm/shmem.c:105-111 */
struct shmem_falloc {
wait_queue_head_t *waitq; // hole punch 대기 큐
pgoff_t start; // 현재 fallocate 중인 범위 시작
pgoff_t next; // 다음 fallocate할 페이지 오프셋
pgoff_t nr_falloced; // 새로 fallocate된 페이지 수
pgoff_t nr_unswapped; // writeout이 swap을 거부한 횟수
};
페이지 캐시에서 folio를 찾거나, swap에서 읽어오거나, 새롭게 할당합니다.
/* mm/shmem.c:2467-2644 */
static int shmem_get_folio_gfp(struct inode *inode, pgoff_t index,
loff_t write_end, struct folio **foliop, enum sgp_type sgp,
gfp_t gfp, struct vm_fault *vmf, vm_fault_t *fault_type)
/* mm/shmem.c:2491-2557 */
folio = filemap_get_entry(inode->i_mapping, index);
if (folio && vma && userfaultfd_minor(vma)) {
if (!xa_is_value(folio))
folio_put(folio);
*fault_type = handle_userfault(vmf, VM_UFFD_MINOR);
return 0;
}
if (xa_is_value(folio)) {
error = shmem_swapin_folio(inode, index, &folio,
sgp, gfp, vma, fault_type);
if (error == -EEXIST)
goto repeat;
*foliop = folio;
return error;
}
if (folio) {
folio_lock(folio);
/* folio가 truncate되었거나 swap out되었는지 확인 */
if (unlikely(folio->mapping != inode->i_mapping)) {
folio_unlock(folio);
folio_put(folio);
goto repeat;
}
if (sgp == SGP_WRITE)
folio_mark_accessed(folio);
if (folio_test_uptodate(folio))
goto out;
/* fallocate된 folio */
if (sgp != SGP_READ)
goto clear;
folio_unlock(folio);
folio_put(folio);
}
/*
* SGP_READ: hole이면 NULL folio와 0을 반환하고 호출자가 zero 처리.
* SGP_NOALLOC: hole이면 NULL folio와 -ENOENT를 반환하고 호출자가 실패 처리.
*/
*foliop = NULL;
if (sgp == SGP_READ)
return 0;
if (sgp == SGP_NOALLOC)
return -ENOENT;
/* 빠른 cache/swap 조회에서 찾지 못했으므로 새 folio를 할당한다. */
if (vma && userfaultfd_missing(vma)) {
*fault_type = handle_userfault(vmf, VM_UFFD_MISSING);
return 0;
}
/* anonymous shmem과 tmpfs에 허용된 hugepage order를 찾는다. */
orders = shmem_allowable_huge_orders(inode, vma, index, write_end, false);
if (orders > 0) {
gfp_t huge_gfp;
huge_gfp = vma_thp_gfp_mask(vma);
huge_gfp = limit_gfp_mask(huge_gfp, gfp);
folio = shmem_alloc_and_add_folio(vmf, huge_gfp,
inode, index, fault_mm, orders);
if (!IS_ERR(folio)) {
if (folio_test_pmd_mappable(folio))
count_vm_event(THP_FILE_ALLOC);
count_mthp_stat(folio_order(folio), MTHP_STAT_SHMEM_ALLOC);
goto alloced;
}
if (PTR_ERR(folio) == -EEXIST)
goto repeat;
}
분기 로직:
1. filemap_get_entry()로 페이지 캐시 탐색
2. xa_is_value(folio) → swap entry → shmem_swapin_folio() 호출
3. folio 존재 + locked → uptodate 확인 후 반환
4. 없음 → SGP_READ/SGP_NOALLOC이면 반환, 그 외 할당
5. hugepage 허용 시 shmem_alloc_and_add_folio() (large folio)
6. 일반 할당 시 shmem_alloc_and_add_folio() (order 0)
swap entry를 실제 folio로 읽어옵니다.
/* mm/shmem.c:2293-2456 */
static int shmem_swapin_folio(struct inode *inode, pgoff_t index,
struct folio **foliop, enum sgp_type sgp,
gfp_t gfp, struct vm_area_struct *vma,
vm_fault_t *fault_type)
/* mm/shmem.c:2335-2359 */
folio = swap_cache_get_folio(swap);
if (!folio) {
if (data_race(si->flags & SWP_SYNCHRONOUS_IO)) {
/* swap cache와 readahead를 건너뛴 direct swapin */
folio = shmem_swap_alloc_folio(inode, vma, index,
index_entry, order, gfp);
if (IS_ERR(folio)) {
error = PTR_ERR(folio);
folio = NULL;
goto failed;
}
} else {
/* cached swapin은 order 0 folio만 지원 */
folio = shmem_swapin_cluster(swap, gfp, info, index);
if (!folio) {
error = -ENOMEM;
goto failed;
}
}
if (fault_type) {
*fault_type |= VM_FAULT_MAJOR;
count_vm_event(PGMAJFAULT);
count_memcg_event_mm(fault_mm, PGMAJFAULT);
}
} else {
swap_update_readahead(folio, NULL, 0);
}
/* mm/shmem.c:2425-2437 */
error = shmem_add_to_page_cache(folio, mapping, index,
swp_to_radix_entry(swap), gfp);
if (error)
goto failed;
shmem_recalc_inode(inode, 0, -nr_pages);
if (sgp == SGP_WRITE)
folio_mark_accessed(folio);
folio_put_swap(folio, NULL);
swap_cache_del_folio(folio);
folio_mark_dirty(folio);
흐름:
1. radix_to_swp_entry()로 swap entry 변환
2. shmem_confirm_swap()으로 swap entry 유효성 확인
3. swap_cache_get_folio()로 swap cache에서 기존 folio 탐색
4. 없으면 SWP_SYNCHRONOUS_IO 플래그에 따라:
- 직렬 IO: shmem_swap_alloc_folio() (대기 없이 직접 할당)
- 비직렬: shmem_swapin_cluster() (readahead 포함)
5. swap entry 분할 필요 시 shmem_split_large_entry() 호출
6. shmem_add_to_page_cache()로 페이지 캐시에 추가
7. folio_put_swap() + swap_cache_del_folio()로 swap cache에서 제거
새 folio를 할당하고 페이지 캐시에 추가합니다.
/* mm/shmem.c:1936-2038 */
static struct folio *shmem_alloc_and_add_folio(struct vm_fault *vmf,
gfp_t gfp, struct inode *inode, pgoff_t index,
struct mm_struct *fault_mm, unsigned long orders)
/* mm/shmem.c:1978-2031 */
__folio_set_locked(folio);
__folio_set_swapbacked(folio);
gfp &= GFP_RECLAIM_MASK;
error = mem_cgroup_charge(folio, fault_mm, gfp);
if (error) {
if (xa_find(&mapping->i_pages, &index,
index + pages - 1, XA_PRESENT)) {
error = -EEXIST;
} else if (pages > 1) {
if (pages == HPAGE_PMD_NR) {
count_vm_event(THP_FILE_FALLBACK);
count_vm_event(THP_FILE_FALLBACK_CHARGE);
}
count_mthp_stat(folio_order(folio), MTHP_STAT_SHMEM_FALLBACK);
count_mthp_stat(folio_order(folio), MTHP_STAT_SHMEM_FALLBACK_CHARGE);
}
goto unlock;
}
error = shmem_add_to_page_cache(folio, mapping, index, NULL, gfp);
if (error)
goto unlock;
error = shmem_inode_acct_blocks(inode, pages);
if (error) {
struct shmem_sb_info *sbinfo = SHMEM_SB(inode->i_sb);
long freed;
/*
* 파일시스템에서 i_size 밖 large folio를 조금 쪼개서
* 공간을 되찾아 본다.
*/
shmem_unused_huge_shrink(sbinfo, NULL, pages);
/*
* freed page를 반영하려고 shmem_recalc_inode()와 비슷한 정리를 한다.
* 다만 현재 folio는 아직 cache에 있으므로 완전히 균형이 맞지는 않는다.
*/
spin_lock(&info->lock);
freed = pages + info->alloced - info->swapped -
READ_ONCE(mapping->nrpages);
if (freed > 0)
info->alloced -= freed;
spin_unlock(&info->lock);
if (freed > 0)
shmem_inode_unacct_blocks(inode, freed);
error = shmem_inode_acct_blocks(inode, pages);
if (error) {
filemap_remove_folio(folio);
goto unlock;
}
}
shmem_recalc_inode(inode, pages, 0);
folio_add_lru(folio);
흐름:
1. shmem_suitable_orders()로 사용 가능한 huge page order 확인
2. shmem_alloc_folio()로 NUMA 정책 적용하여 할당
3. mem_cgroup_charge()로 memcg 충전
4. shmem_add_to_page_cache()로 XArray에 추가
5. shmem_inode_acct_blocks()로 블록 할당량 확인
6. shmem_recalc_inode()로 inode 통계 갱신
folio를 swap으로 내보냅니다.
/* mm/shmem.c:1590-1737 */
int shmem_writeout(struct folio *folio, struct swap_iocb **plug,
struct list_head *folio_list)
/* mm/shmem.c:1601-1642 */
if ((info->flags & SHMEM_F_LOCKED) || sbinfo->noswap)
goto redirty;
if (!total_swap_pages)
goto redirty;
if (folio_test_large(folio)) {
index = shmem_fallocend(inode,
DIV_ROUND_UP(i_size_read(inode), PAGE_SIZE));
if ((index > folio->index && index < folio_next_index(folio)) ||
!IS_ENABLED(CONFIG_THP_SWAP))
split = true;
}
if (split) {
int order;
try_split:
order = folio_order(folio);
/* subpage가 여전히 dirty 상태가 되도록 보장 */
folio_test_set_dirty(folio);
if (split_folio_to_list(folio, folio_list))
goto redirty;
#ifdef CONFIG_TRANSPARENT_HUGEPAGE
if (order >= HPAGE_PMD_ORDER) {
count_memcg_folio_events(folio, THP_SWPOUT_FALLBACK, 1);
count_vm_event(THP_SWPOUT_FALLBACK);
}
#endif
count_mthp_stat(order, MTHP_STAT_SWPOUT_FALLBACK);
folio_clear_dirty(folio);
}
/* mm/shmem.c:1679-1703 */
if (!folio_alloc_swap(folio)) {
bool first_swapped = shmem_recalc_inode(inode, 0, nr_pages);
/*
* 아직 swaplist에 없으면 지금 추가해서 shmem_unuse()가
* swap된 inode를 다시 찾을 수 있게 한다.
*/
if (first_swapped) {
spin_lock(&shmem_swaplist_lock);
if (list_empty(&info->swaplist))
list_add(&info->swaplist, &shmem_swaplist);
spin_unlock(&shmem_swaplist_lock);
}
folio_dup_swap(folio, NULL);
shmem_delete_from_page_cache(folio, swp_to_radix_entry(folio->swap));
BUG_ON(folio_mapped(folio));
error = swap_writeout(folio, plug);
if (error != AOP_WRITEPAGE_ACTIVATE)
return error;
}
흐름:
1. SHMEM_F_LOCKED 또는 noswap이면 즉시 redirty
2. large folio인 경우 split_folio_to_list()로 분할
3. folio_alloc_swap()으로 swap 할당
4. shmem_delete_from_page_cache()로 캐시에서 제거
5. swap_writeout()으로 실제 swap 디스크에 기록
6. 실패 시 캐시에 다시 추가
hole punch 진행 중 발생한 페이지 폴트를 대기시킵니다.
/* mm/shmem.c:2708-2747 */
static vm_fault_t shmem_falloc_wait(struct vm_fault *vmf, struct inode *inode)
/* mm/shmem.c:2714-2741 */
spin_lock(&inode->i_lock);
shmem_falloc = inode->i_private;
if (shmem_falloc &&
shmem_falloc->waitq &&
vmf->pgoff >= shmem_falloc->start &&
vmf->pgoff < shmem_falloc->next) {
wait_queue_head_t *shmem_falloc_waitq;
DEFINE_WAIT_FUNC(shmem_fault_wait, synchronous_wake_function);
ret = VM_FAULT_NOPAGE;
fpin = maybe_unlock_mmap_for_io(vmf, NULL);
shmem_falloc_waitq = shmem_falloc->waitq;
prepare_to_wait(shmem_falloc_waitq, &shmem_fault_wait,
TASK_UNINTERRUPTIBLE);
spin_unlock(&inode->i_lock);
schedule();
/*
* shmem_falloc_waitq는 hole-punch task의 stack을 가리킨다.
* 여기 도달할 때쯤 무효가 될 수 있지만, 그 경우 finish_wait()는
* 역참조하지 않는다. 다만 wake_up_all()과의 경합 때문에 i_lock은 필요하다.
*/
spin_lock(&inode->i_lock);
finish_wait(shmem_falloc_waitq, &shmem_fault_wait);
}
spin_unlock(&inode->i_lock);
동작:
1. inode->i_private가 shmem_falloc이면
2. waitq에서 TASK_UNINTERRUPTIBLE로 대기
3. hole punch 완료 후 wake_up으로 깨어남
VMA fault 핸들러로, hole punch 대기와 shmem_get_folio_gfp() 호출을 연결합니다.
/* mm/shmem.c:2749-2777 */
static vm_fault_t shmem_fault(struct vm_fault *vmf)
/* mm/shmem.c:2761-2776 */
if (unlikely(inode->i_private)) {
ret = shmem_falloc_wait(vmf, inode);
if (ret)
return ret;
}
err = shmem_get_folio_gfp(inode, vmf->pgoff, 0, &folio, SGP_CACHE,
gfp, vmf, &ret);
if (err)
return vmf_error(err);
if (folio) {
vmf->page = folio_file_page(folio, vmf->pgoff);
ret |= VM_FAULT_LOCKED;
}
return ret;
역할:
1. inode->i_private가 살아 있으면 punch/fallocate 경합을 먼저 해소
2. SGP_CACHE로 캐시 조회, swapin, 새 folio 할당을 통합 처리
3. 최종적으로 vmf->page에 fault 대상 subpage를 연결하고 VM_FAULT_LOCKED를 반환
shmem_fault (VMA 폴트 진입점)
├── shmem_falloc_wait (hole punch 대기)
└── shmem_get_folio_gfp
├── filemap_get_entry → 페이지 캐시 탐색
├── shmem_swapin_folio → swap에서 읽기
│ ├── swap_cache_get_folio → swap cache 히트
│ ├── shmem_swap_alloc_folio → 새 folio 할당
│ ├── shmem_split_large_entry → large swap entry 분할
│ └── shmem_add_to_page_cache → 캐시에 추가
├── shmem_alloc_and_add_folio → 새 folio 할당
│ ├── shmem_alloc_folio → 메모리 할당
│ ├── mem_cgroup_charge → memcg 충전
│ └── shmem_add_to_page_cache → 캐시에 추가
└── shmem_recalc_inode → 통계 갱신
shmem_writeout (swap 내보내기)
├── split_folio_to_list → large folio 분할
├── folio_alloc_swap → swap 할당
├── shmem_delete_from_page_cache → 캐시 제거
└── swap_writeout → swap 디스크 기록
shmem_undo_range (truncate/punch hole)
├── find_lock_entries → 대상 folio 검색
├── truncate_inode_folio → folio 제거
└── shmem_free_swap → swap entry 해제
shmem_unuse (swap off)
├── shmem_unuse_inode → inode 내 swap 탐색
│ ├── shmem_find_swap_entries → swap entry 찾기
│ └── shmem_unuse_swap_entries → swap → 캐시 복원
└── shmem_unuse_inode 반복
shmem_fault → shmem_get_folio_gfp → shmem_writeout → swap_writeout
/* include/linux/shmem_fs.h:166-171 */
enum sgp_type {
SGP_READ, // i_size를 넘지 않고 페이지를 할당하지 않음
SGP_NOALLOC, // 비슷하지만 hole에서는 실패하거나 fallocated 페이지를 사용
SGP_CACHE, // i_size를 넘지 않으며 필요하면 페이지를 할당함
SGP_WRITE, // i_size를 넘을 수 있고 !Uptodate 페이지도 할당함
SGP_FALLOC, // SGP_WRITE와 비슷하지만 기존 페이지를 Uptodate로 만듦
};
| SGP 타입 | i_size 초과 | 페이지 할당 | 사용 시점 |
|---|---|---|---|
| SGP_READ | 불가 | 불가 | 읽기만, hole은 NULL 반환 |
| SGP_NOALLOC | 불가 | 불가 | hole이면 -ENOENT 반환 |
| SGP_CACHE | 불가 | 가능 | 일반 읽기/쓰기 (페이지 폴트) |
| SGP_WRITE | 가능 | 가능 | 쓰기 시, new folio에 referenced 설정 |
| SGP_FALLOC | 가능 | 가능 | fallocate, existing folio 초기화 |
| 모드 | 동작 | 마운트 옵션 |
|---|---|---|
| SHMEM_HUGE_NEVER (0) | huge page 사용 안함 | `huge=never` |
| SHMEM_HUGE_ALWAYS (1) | 항상 huge page 시도 | `huge=always` |
| SHMEM_HUGE_WITHIN_SIZE (2) | i_size 내에 있을 때만 | `huge=within_size` |
| SHMEM_HUGE_ADVISE (3) | madvise(MADV_HUGEPAGE) 시에만 | `huge=advise` |
| SHMEM_HUGE_DENY (-1) | 전체 비활성화 (긴급용) | sysfs에서 설정 |
| SHMEM_HUGE_FORCE (-2) | 전체 강제 활성화 (테스트용) | sysfs에서 설정 |
| 모드 | 동작 | 반환값 |
|---|---|---|
| 기본 (0) | 할당 + zeroing | 0 |
| FALLOC_FL_KEEP_SIZE | i_size 변경 없이 할당 | 0 |
| FALLOC_FL_PUNCH_HOLE | hole punch + truncate | 0 |
| SHMEM_F_MAPPING_FROZEN | 모든 변경 불가 | -EPERM |
| F_SEAL_GROW | 확장 불가 | -EPERM |
| F_SEAL_SHRINK | 축소 불가 | -EPERM |
| F_SEAL_WRITE | 쓰기 불가 | -EPERM |
| 조건 | 경로 | 설명 |
|---|---|---|
| SWP_SYNCHRONOUS_IO | shmem_swap_alloc_folio | 대기 없이 직접 할당 |
| cached swap | swap_cache_get_folio | 기존 swap cache 히트 |
| !SWP_SYNCHRONOUS_IO | shmem_swapin_cluster | readahead 포함 |
| uffd armed | order 0 fallback | per-page fault 보장 |
| zswap 비활성화 | order 0 fallback | zswap 고려 없음 |