Ryotta's Linux 7.0 MM

메모리 관리 서브시스템 완전 분석

🧮 zsmalloc

관련 소스: mm/zsmalloc.c, mm/zpdesc.h, include/linux/zsmalloc.h
관련 문서: Swap / zswap · SLUB 할당자 · Buddy Allocator

개요 (Overview)

zsmalloc은 리눅스 커널의 압축 메모리 전용 할당자입니다. zswap, zram 등에서 압축된 페이지 프레임을 저장할 때 사용하며, 일반적인 slab 할당자(SLUB)와 달리 여러 물리 페이지에 걸쳐 분산된 객체를 하나의 논리적 단위로 관리하는 것이 핵심 특징입니다.

일반 slab 할당자는 하나의 객체가 반드시 하나의 페이지 안에 완전히 들어가야 하지만, zsmalloc은 여러 페이지에 걸쳐 분산된 객체도 하나의 zspage로 묶어 할당/해제할 수 있습니다. 이를 통해 압축 데이터의 크기가 페이지 경계를 넘나들 때도 효율적인 메모리 관리가 가능합니다.

운영 관점에서는 zsmalloc을 "압축된 짐 상자를 여러 칸짜리 선반에 끼워 넣고, 손잡이(handle)만 들고 위치를 찾아가는 창고"처럼 보면 이해가 쉽습니다. zswap이나 zram은 압축 결과물만 보관하고, 실제 물리 페이지 묶음과 빈 슬롯 관리, compaction, migration 같은 저장소 운영은 zsmalloc이 맡습니다.

소스 파일:
  mm/zsmalloc.c        ← 핵심 할당 로직 (2258줄)
  mm/zpdesc.h          ← zpdesc 구조체 (struct page 오버레이)
  include/linux/zsmalloc.h  ← 외부 API 선언

빠른 점검 명령

# zsmalloc 모듈 로드 상태 확인
lsmod | grep zsmalloc

# zsmalloc 통계 (CONFIG_ZSMALLOC_STAT 활성화 시)
cat /sys/kernel/debug/zsmalloc/*/classes

# zsmalloc 풀에서 사용 중인 페이지 수
cat /sys/kernel/debug/zsmalloc/*/classes | awk '{sum += $14} END {print "total pages:", sum}'

# zswap에서 사용하는 zsmalloc 풀 확인
cat /sys/kernel/mm/zswap/mm_stat

# zsmalloc 관련 커널 설정 확인
grep CONFIG_ZSMALLOC /boot/config-$(uname -r)

# 현재 시스템의 Zswap / Zswapped 메모리량 확인
grep -E 'Zswap|Zswapped' /proc/meminfo

# zspage compaction 상태 확인 (kern.msgbuf 또는 dmesg)
dmesg | grep -i "zsmalloc\|zspage"

# 현재 시스템의 zsmalloc pool 수
ls /sys/kernel/debug/zsmalloc/ 2>/dev/null || echo "debugfs not available"

# zram과 zsmalloc 연결 확인
cat /sys/block/zram0/mm_stat 2>/dev/null

# 클래스별 통계를 표 형태로 보기 좋게 정렬
cat /sys/kernel/debug/zsmalloc/*/classes 2>/dev/null | column -t

핵심 자료구조

struct zpdesc — 페이지 설명자 (struct page 오버레이)

zpdesc는 struct page의 필드를 재사용하여 zsmalloc이 관리하는 물리 페이지를 설명합니다.

/* mm/zpdesc.h:32-50 */
struct zpdesc {
    unsigned long flags;           /* 페이지 플래그 (PG_private: 첫 번째 페이지 식별) */
    struct list_head lru;          /* 페이지 마이그레이션용 LRU */
    unsigned long movable_ops;     /* 마이그레이션 가능 오퍼레이션 */
    union {
        struct zpdesc *next;       /* 다음 zpdesc (zspage 내 연결) */
        unsigned long handle;      /* huge zspage의 할당 핸들 */
    };
    struct zspage *zspage;         /* 소속 zspage 포인터 */
    unsigned int first_obj_offset; /* 첫 번째 객체의 페이지 내 오프셋 (하위 24비트) */
    atomic_t _refcount;            /* 참조 카운트 */
};
/* struct page와 정확히 같은 크기/오프셋 매핑 (static_assert로 검증) */

struct zspage — zsmalloc 할당 단위

여러 물리 페이지(zpdesc)로 구성된 논리적 할당 단위입니다.

/* mm/zsmalloc.c:261-274 */
struct zspage {
    struct {
        unsigned int huge:HUGE_BITS;       /* 1: 하나의 zpdesc에 하나의 객체 (huge class) */
        unsigned int fullness:FULLNESS_BITS;/* 현재 fullness 그룹 인덱스 */
        unsigned int class:CLASS_BITS + 1;  /* 소속 size_class 인덱스 */
        unsigned int magic:MAGIC_VAL_BITS;  /* ZSPAGE_MAGIC (0x58) 검증용 */
    };
    unsigned int inuse;                     /* 현재 사용 중인 객체 수 */
    unsigned int freeobj;                   /* freelist의 첫 번째 객체 인덱스 */
    struct zpdesc *first_zpdesc;            /* 첫 번째 zpdesc 포인터 */
    struct list_head list;                  /* size_class의 fullness_list 연결용 */
    struct zs_pool *pool;                   /* 소속 풀 포인터 */
    struct zspage_lock zsl;                 /* zspage별 락 */
};

struct zs_pool — zsmalloc 풀

zsmalloc 할당자의 최상위 관리 구조체입니다.

/* mm/zsmalloc.c:199-220 */
struct zs_pool {
    const char *name;                       /* 풀 이름 (예: "zswap") */
    struct size_class *size_class[ZS_SIZE_CLASSES]; /* 크기별 클래스 배열 */
    atomic_long_t pages_allocated;          /* 할당된 총 페이지 수 */
    struct zs_pool_stats stats;             /* compaction 통계 */
    struct shrinker *shrinker;              /* 메모리 회수용 shrinker */
    rwlock_t lock;                          /* 마이그레이션/compaction 보호 */
    atomic_t compaction_in_progress;        /* compaction 진행 중 플래그 */
#ifdef CONFIG_COMPACTION
    struct work_struct free_work;           /* 지연 해제 워크큐 */
#endif
};

struct size_class — 크기별 클래스

동일한 크기의 객체를 관리하는 zspage 그룹입니다.

/* mm/zsmalloc.c:160-174 */
struct size_class {
    spinlock_t lock;                        /* 클래스 내 동시성 제어 */
    struct list_head fullness_list[NR_FULLNESS_GROUPS]; /* fullness별 freelist */
    int size;                               /* 객체 크기 (ZS_ALIGN 배수) */
    int objs_per_zspage;                    /* zspage당 최대 객체 수 */
    int pages_per_zspage;                   /* zspage를 구성하는 물리 페이지 수 */
    unsigned int index;                     /* 클래스 인덱스 */
    struct zs_size_stat stats;              /* 할당/사용 통계 */
};

struct link_free — free list 노드

zspage 내부의 빈 객체에 삽입되어 singly linked list를 구성합니다.

/* mm/zsmalloc.c:182-194 */
struct link_free {
    union {
        unsigned long next;     /* 다음 빈 객체의 인덱스 (OBJ_TAG_BITS 시프트) */
        unsigned long handle;   /* 할당된 객체의 핸들 */
    };
};

Handle 인코딩 방식

객체 위치는 (PFN, obj_idx) 쌍으로 하나의 unsigned long 핸들에 인코딩됩니다.

/* mm/zsmalloc.c:56-92 */
#define _PFN_BITS               (MAX_POSSIBLE_PHYSMEM_BITS - PAGE_SHIFT)

/*
 * 할당된 객체의 헤더는 OBJ_ALLOCATED_TAG를 가져야 한다.
 * 최하위 비트에 상태 비트를 넣어도 되는 이유는
 * handle이 정렬된 주소라서 하위 비트 여유가 있기 때문이다.
 */
#define OBJ_ALLOCATED_TAG       1

#define OBJ_TAG_BITS            1
#define OBJ_TAG_MASK            OBJ_ALLOCATED_TAG

#define OBJ_INDEX_BITS          (BITS_PER_LONG - _PFN_BITS)
#define OBJ_INDEX_MASK          ((_AC(1, UL) << OBJ_INDEX_BITS) - 1)

enum fullness_group — zspage 점유율 구간

fullness_list는 size_class 안에서 같은 크기 객체를 담는 zspage를 점유율별로 나눠 관리합니다. compaction은 이 구간을 기준으로 가장 비어 있는 zspage와 가장 차 있는 zspage를 골라 객체를 옮깁니다.

/* mm/zsmalloc.c:134-140 */
enum fullness_group {
	ZS_INUSE_RATIO_0,
	ZS_INUSE_RATIO_10,
	/* 8개의 fullness 그룹이 중간에 더 있다 */
	ZS_INUSE_RATIO_99       = 10,
	ZS_INUSE_RATIO_100,
	NR_FULLNESS_GROUPS,
};

핵심 함수

zs_malloc — 객체 할당

/* mm/zsmalloc.c:1297-1354 */
unsigned long zs_malloc(struct zs_pool *pool, size_t size, gfp_t gfp, const int nid);

역할: 풀에서 지정된 크기의 객체를 할당합니다.

분기 로직:

1. size == 0ERR_PTR(-EINVAL) 반환

2. size > ZS_MAX_ALLOC_SIZEERR_PTR(-ENOSPC) 반환

3. handle 캐시에서 할당 실패 → ERR_PTR(-ENOMEM) 반환

4. find_get_zspage()로 기존 zspage 탐색

- zspage 존재 → obj_malloc() + fix_fullness_group() 후 반환

- zspage 없음 → alloc_zspage()로 새로 생성 → obj_malloc() + insert_zspage()

zs_free — 객체 해제

/* mm/zsmalloc.c:1384-1417 */
void zs_free(struct zs_pool *pool, unsigned long handle);

역할: 할당된 객체를 풀에 반환합니다.

분기 로직:

1. handleERR_PTR 또는 NULL → 즉시 반환

2. obj_free()로 freelist에 삽입

3. fix_fullness_group()로 fullness 그룹 갱신

4. fullness == ZS_INUSE_RATIO_0free_zspage()로 zspage 전체 해제

zs_compact — 풀 컴팩션

/* mm/zsmalloc.c:1938-1964 */
unsigned long zs_compact(struct zs_pool *pool);

역할: 각 크기 클래스의 zspage를 압축하여 빈 페이지를 회수합니다.

분기 로직:

1. compaction_in_progress가 이미 1이면 중복 실행 방지 (0 반환)

2. 큰 크기 클래스부터 순회하며 __zs_compact() 호출

3. __zs_compact() 내부:

- isolate_dst_zspage()로 가장 많이 찬 zspage를 대상으로 선택

- isolate_src_zspage()로 가장 적게 찬 zspage를 소스로 선택

- migrate_zspage()로 객체를 복사하여 대상 zspage에 이동

- 소스 zspage가 비면 free_zspage()로 해제

zs_shrinker_scan — 메모리 압박 시 compaction 진입

/* mm/zsmalloc.c:1972-1985 */
static unsigned long zs_shrinker_scan(struct shrinker *shrinker,
		struct shrink_control *sc);

역할: shrinker 인터페이스를 통해 메모리 압박 시 zsmalloc 풀에서 회수 가능한 페이지를 찾고 zs_compact()를 호출합니다.

분기 로직:

1. shrinker->private_data에서 대상 zs_pool 획득

2. zs_compact(pool) 실행

3. 회수한 페이지가 있으면 그 수를 반환

4. 회수하지 못하면 SHRINK_STOP 반환

zs_obj_read_begin / zs_obj_read_end — 객체 읽기

/* mm/zsmalloc.c:1037-1085 */
void *zs_obj_read_begin(struct zs_pool *pool, unsigned long handle,
                        size_t mem_len, void *local_copy);
/* mm/zsmalloc.c:1087-1112 */
void zs_obj_read_end(struct zs_pool *pool, unsigned long handle,
                     size_t mem_len, void *handle_mem);

역할: 객체 데이터를 안전하게 읽기 위해 매핑/언매핑합니다.

분기 로직:

1. pool->lock read_lock으로 핸들→zpdesc 변환 보호

2. zspage_read_lock()으로 마이그레이션 중단 보장

3. 객체가 한 페이지에 완전히 포함 → kmap_local_zpdesc()로 직접 매핑

4. 객체가 두 페이지에 걸침 → local_copy에 복사하여 반환

zs_page_migrate — 페이지 마이그레이션

/* mm/zsmalloc.c:1686-1777 */
static int zs_page_migrate(struct page *newpage, struct page *page,
                           enum migrate_mode mode);

역할: zsmalloc 페이지를 다른 물리 위치로 이동합니다 (compaction, NUMA balancing).

분기 로직:

1. zpdesc->zspage == NULL → 이미 해제된 페이지, 마이그레이션 불가

2. pool->lock write_lock + class->lock spin_lock 획득

3. zspage_write_trylock() 실패 → -EINVAL 반환

4. 새 페이지에 전체 데이터 복사 → handle 업데이트 → replace_sub_page() 호출

호출 흐름

할당 흐름

zs_malloc(pool, size, gfp, nid)
├── cache_alloc_handle()              ← handle 캐시에서 할당
├── get_size_class_index(size)        ← 크기 → 클래스 인덱스 변환
├── [class->lock]
├── find_get_zspage(class)            ← fullness_list에서 빈 zspage 탐색
│   └── (ZS_INUSE_RATIO_99 → 0 순서)
├── [zspage 발견 시]
│   ├── obj_malloc(pool, zspage, handle)
│   │   ├── get_freeobj()             ← freelist head
│   │   ├── link->handle = handle     ← handle 기록
│   │   ├── location_to_obj()         ← (zpdesc, obj_idx) → obj 인코딩
│   │   └── record_obj(handle, obj)   ← handle에 obj 저장
│   └── fix_fullness_group()          ← fullness 그룹 갱신
├── [zspage 미발견 시]
│   ├── alloc_zspage(pool, class, gfp, nid)
│   │   ├── cache_alloc_zspage()      ← zspage 구조체 할당
│   │   ├── alloc_zpdesc() × N        ← 물리 페이지 할당 (N = pages_per_zspage)
│   │   ├── create_page_chain()       ← zpdesc들 연결
│   │   └── init_zspage()             ← freelist 초기화 (link_free 연결)
│   ├── obj_malloc()
│   ├── insert_zspage(class, zspage, newfg)
│   └── SetZsPageMovable()            ← 마이그레이션 가능 플래그 설정
└── [class->lock 해제] → handle 반환

해제 흐름

zs_free(pool, handle)
├── [pool->lock read_lock]
├── handle_to_obj(handle)             ← handle → obj 디코딩
├── obj_to_zpdesc(obj)                ← obj → zpdesc 변환
├── [class->lock]
├── class_stat_sub(ZS_OBJS_INUSE)     ← 사용 중인 객체 수 감소
├── obj_free(class_size, obj)
│   ├── obj_to_location()             ← obj → (zpdesc, obj_idx)
│   ├── link->next = freeobj << BITS  ← freelist에 삽입
│   └── mod_zspage_inuse(zspage, -1)  ← inuse 카운트 감소
├── fix_fullness_group()              ← fullness 그룹 갱신
├── [fullness == ZS_INUSE_RATIO_0]
│   └── free_zspage(pool, class, zspage)
│       ├── trylock_zspage()          ← 모든 zpdesc 잠금 시도
│       │   └── [잠금 실패 시] kick_deferred_free()
│       ├── remove_zspage()           ← fullness_list에서 제거
│       └── __free_zspage()
│           ├── reset_zpdesc() × N    ← zpdesc 초기화
│           ├── free_zpdesc() × N     ← 물리 페이지 해제
│           └── cache_free_zspage()   ← zspage 구조체 해제
└── cache_free_handle()               ← handle 해제

Compaction 흐름

zs_compact(pool)
├── [compaction_in_progress 확인]
├── [큰 크기 클래스 → 작은 크기 클래스 순회]
└── __zs_compact(pool, class)
    ├── [pool->lock write_lock + class->lock]
    ├── while (zs_can_compact(class))
    │   ├── isolate_dst_zspage()      ← 가장 찬 zspage 선택
    │   ├── isolate_src_zspage()      ← 가장 빈 zspage 선택
    │   ├── migrate_zspage()
    │   │   └── while (소스에 객체 남음)
    │   │       ├── find_alloced_obj()  ← 할당된 객체 찾기
    │   │       ├── obj_malloc()        ← 대상에 새 위치 할당
    │   │       ├── zs_object_copy()    ← 데이터 복사
    │   │       └── obj_free()          ← 소스 위치 해제
    │   ├── [소스 zspage 비면] free_zspage()
    │   └── [대상 zspage 차면] putback_zspage() + 새 대상 선택
    └── [반복 종료]

메모리 압박에서 compaction으로 들어가는 흐름

메모리 압박
└── shrink_slab()/shrinker 호출
    └── zs_shrinker_scan(shrinker, sc)
        ├── pool = shrinker->private_data
        ├── pages_freed = zs_compact(pool)
        └── [pages_freed == 0] SHRINK_STOP

조건별 비교

size_class 결정 기준

조건동작비고
`size <= ZS_MIN_ALLOC_SIZE`class index = 0최소 32바이트
`size > ZS_MAX_ALLOC_SIZE`할당 실패 (ENOSPC)PAGE_SIZE 초과 불가
`is_power_of_2(class_size)`pages_per_zspage = 1낭비 0
`class_size`가 2의 거듭제곱 아님`calculate_zspage_chain_size()`최소 낭비 chain 선택

fullness 그룹 분류

fullness 그룹inuse 비율설명
ZS_INUSE_RATIO_00%완전히 빈 zspage (해제 가능)
ZS_INUSE_RATIO_101~10%거의 빈 zspage (compaction 대상)
.........
ZS_INUSE_RATIO_9990~99%거의 찬 zspage
ZS_INUSE_RATIO_100100%완전히 찬 zspage

lock 계층

범위획득 순서
`pool->lock`풀 전체 마이그레이션 보호1번째
`class->lock`크기 클래스 내 zspage 할당/해제2번째
`zspage->zsl`개별 zspage 읽기/쓰기 접근3번째
`zpdesc` (folio lock)물리 페이지 잠금4번째

huge vs non-huge zspage

속성non-hugehuge
pages_per_zspage> 1 또는 objs > 1= 1이고 objs = 1
handle 저장 위치첫 번째 객체의 link_freezpdesc->handle
freelist 구조link_free singly linked list단일 객체 (freelist 불필요)
`ZsHugePage()`falsetrue

관련 문서

  • Swap / zswap — zswap이 zsmalloc을 사용하여 압축된 페이지 저장
  • SLUB 할당자 — 일반 slab 할당자와의 비교
  • Buddy Allocator — zsmalloc이 buddy에서 물리 페이지를 할당하는 방식
  • Compaction — zsmalloc compaction과 커널 compaction의 관계
  • Folio / Page Cache — zpdesc가 struct page를 오버레이하는 방식
  • SVG 다이어그램

    zsmalloc 구조 계층도

    zsmalloc 구조 계층도

    할당/해제 호출 흐름

    할당/해제 호출 흐름