Compound 페이지

<kernel v5.0>

Compound 페이지

  • 버디 시스템에서 order 단위로 free 페이지를 관리하고, 할당 받아 사용하는 페이지들은 order를 제거한 상태에서 사용한다. 그런데 order 단위로 묶어 관리하는 페이지가 있는데 이들을 compound 페이지라고 한다.
  • 용도
    • 슬랩 캐시
    • Huge 페이지
      • 일반적인 high order 페이지와 다른 점은 TLB 성능을 극대화하기 위해 설계되었다.
      • 성능을 향상시키기 위해 리눅스는 대량의 페이지(high order 페이지)를 할당 받는 경우 PMD 레벨에서 huge TLB를 사용하여 더 빠른 access를 사용할 수 있게 매핑을 한다.
  • 2015년 12월 kernel v.4.6-rc1 에서 CONFIG_PAGEFLAGS_EXTENDED 옵션과 PG_compound, PG_tail 이 없어지고 PG_head 만 남겨지게 되었다.

 

prep_compound_page()

/mm/page_alloc.c

void prep_compound_page(struct page *page, unsigned long order)
{
        int i;
        int nr_pages = 1 << order;

        set_compound_page_dtor(page, COMPOUND_PAGE_DTOR);
        set_compound_order(page, order);
        __SetPageHead(page);
        for (i = 1; i < nr_pages; i++) {
                struct page *p = page + i;
                set_page_count(p, 0);
                p->mapping = TAIL_MAPPING;
                set_compound_head(p, page);
        }
}

compound 페이지를 준비한다.

  • 코드 라인 6에서 compound 파괴자 id에 COMPOUND_PAGE_DTOR를 대입한다.
    • 파괴자 id는 다음과 같다.
      • NULL_COMPOUND_DTOR
      • COMPOUND_PAGE_DTOR
      • HUGETLB_PAGE_DTOR
      • TRANSHUGE_PAGE_DTOR
  • 코드 라인 7에서 두 번째 페이지에 order를 설정한다.
  • 코드 라인 8에서 헤더 페이지의 플래그에 PG_Head 비트를 설정한다.
  • 코드 라인 9~14에서 나머지 페이지의 참조 카운터에 0을 대입하고, 나머지 페이지들이 헤드 페이지를 가리키게 한 후 플래그에 PG_Tail 비트를 설정한다.

 

다음 그림은 prep_compound_page() 함수에 의해 compound 페이지가 준비되는 모습을 보여준다.

  • 두 번째 페이지에 compound 정보가 존재한다.

 

page_count()

include/linux/mm.h

static inline int page_count(struct page *page)
{
        return atomic_read(&compound_head(page)->_count);
}

요청 페이지의 _count 값을 알아온다. 만일 compound page인 경우 선두 페이지에서 _count 값을 알아온다.

 

compound_order()

include/linux/mm.h

static inline int compound_order(struct page *page)
{
        if (!PageHead(page))
                return 0;
        return page[1].compound_order;
}

compound 페이지이면 compound_order를 알아오고 compound 페이지가 아닌 경우 0 order를 반환한다

  • 첫 번째 페이지의 Head 플래그가 설정된 경우 compound 페이지를 의미한다.
  • 두 번째 페이지 구조체에서 compound_order를 알아온다.

 

다음 그림은 compound 페이지로부터 order 값을 알아오는 과정을 보여준다.

compound_order-1a

 

 

 

Swap -3- (Swap 영역 할당/해제)

<kernel v5.0>

swap 엔트리 할당/해제

swapon으로 지정된 swap 파일이나 swap 블록 디바이스가 swap 영역으로 지정되면 swap_map[] 이라는 1바이트 배열을 사용하여 swap 엔트리들의 할당을 관리한다.

  • swap_map[offset]에 사용되는 offset 인덱스는 swap 영역의 offset 페이지를 의미한다.
    • swap_map[0]은 swap 영역의 0번 페이지를 의미한다.
  • swap_map[]에 사용되는 값
    • 0
      • free 상태에서 사용되는 값
    • 1~SWAP_MAP_MAX(0x3e)
      • in-use 상태에서 사용되는 값으로 swap 엔트리의 참조 카운터가 저장된다.
      • 초과시 SWAP_MAP_CONTINUED(0x80) 플래그가 추가되고 이의 관리를 위해 별도의 swap_map[] 페이지가 생성된다.
    • SWAP_MAP_BAD (0x3f)
      • bad 페이지로 사용되는 값
    • SWAP_HAS_CACHE (0x40)
      • cache 페이지 (추가 플래그)
    • SWAP_MAP_SHMEM (0xbf)
  • swap 엔트리들은 중간에 per-cpu swap 슬롯 캐시에 충전되어 사용된다.

 

swap 엔트리들 할당

get_swap_pages()

mm/swapfile.c

int get_swap_pages(int n_goal, swp_entry_t swp_entries[], int entry_size)
{
        unsigned long size = swap_entry_size(entry_size);
        struct swap_info_struct *si, *next;
        long avail_pgs;
        int n_ret = 0;
        int node;

        /* Only single cluster request supported */
        WARN_ON_ONCE(n_goal > 1 && size == SWAPFILE_CLUSTER);

        avail_pgs = atomic_long_read(&nr_swap_pages) / size;
        if (avail_pgs <= 0)
                goto noswap;

        if (n_goal > SWAP_BATCH)
                n_goal = SWAP_BATCH;

        if (n_goal > avail_pgs)
                n_goal = avail_pgs;

        atomic_long_sub(n_goal * size, &nr_swap_pages);

        spin_lock(&swap_avail_lock);

start_over:
        node = numa_node_id();
        plist_for_each_entry_safe(si, next, &swap_avail_heads[node], avail_lists[node]) {
                /* requeue si to after same-priority siblings */
                plist_requeue(&si->avail_lists[node], &swap_avail_heads[node]);
                spin_unlock(&swap_avail_lock);
                spin_lock(&si->lock);
                if (!si->highest_bit || !(si->flags & SWP_WRITEOK)) {
                        spin_lock(&swap_avail_lock);
                        if (plist_node_empty(&si->avail_lists[node])) {
                                spin_unlock(&si->lock);
                                goto nextsi;
                        }
                        WARN(!si->highest_bit,
                             "swap_info %d in list but !highest_bit\n",
                             si->type);
                        WARN(!(si->flags & SWP_WRITEOK),
                             "swap_info %d in list but !SWP_WRITEOK\n",
                             si->type);
                        __del_from_avail_list(si);
                        spin_unlock(&si->lock);
                        goto nextsi;
                }
                if (size == SWAPFILE_CLUSTER) {
                        if (!(si->flags & SWP_FS))
                                n_ret = swap_alloc_cluster(si, swp_entries);
                } else
                        n_ret = scan_swap_map_slots(si, SWAP_HAS_CACHE,
                                                    n_goal, swp_entries);
                spin_unlock(&si->lock);
                if (n_ret || size == SWAPFILE_CLUSTER)
                        goto check_out;
                pr_debug("scan_swap_map of si %d failed to find offset\n",
                        si->type);

                spin_lock(&swap_avail_lock);
nextsi:
                /*
                 * if we got here, it's likely that si was almost full before,
                 * and since scan_swap_map() can drop the si->lock, multiple
                 * callers probably all tried to get a page from the same si
                 * and it filled up before we could get one; or, the si filled
                 * up between us dropping swap_avail_lock and taking si->lock.
                 * Since we dropped the swap_avail_lock, the swap_avail_head
                 * list may have been modified; so if next is still in the
                 * swap_avail_head list then try it, otherwise start over
                 * if we have not gotten any slots.
                 */
                if (plist_node_empty(&next->avail_lists[node]))
                        goto start_over;
        }

        spin_unlock(&swap_avail_lock);

check_out:
        if (n_ret < n_goal)
                atomic_long_add((long)(n_goal - n_ret) * size,
                                &nr_swap_pages);
noswap:
        return n_ret;
}

swap 엔트리들을 준비하고 그 수를 반환한다. (THP swap 엔트리도 1건으로 반환한다)

  • 코드 라인 3에서 THP swap을 지원하는 커널인 경우 @entry_size를 size에 대입한다. 지원하지 않는 경우 size는 항상 1이다.
    • THP swap을 지원하는 경우 엔트리 크기로 HPAGE_PMD_NR을 사용한다.
      • 예) 4K 페이지를 사용하는 경우 pmd 사이즈가 2M이고 HPAGE_PMD_NR=512이다.
  • 코드 라인 10에서 클러스터 방식에서는 @n_goal에서 1개만 요청가능하다.
  • 코드 라인 12~14에서 남은 swap 페이지를 size로 나눈 수를 avail_pgs에 대입하고 그 수가 0 이하이면 noswap 레이블로 이동한다.
  • 코드 라인 16~20에서 @n_goal이 SWAP_BATCH(64) 또는 avail_pgs를 초과하지 않도록 제한한다.
  • 코드 라인 22에서 남은 swap 페이지에서 @n_goal * size 만큼 뺀다.
  • 코드 라인 26~28에서 start_over: 레이블이다. 전역 swap_avail_heads[node] priority 리스트에 등록된 각 swap 영역을 순회한다.
    • swap 영역: swap_info_struct 노드
  • 코드 라인 30에서 si->avail_lists[node] 노드들을 전역 swap_avail_heads[node] priority 리스트의 끝에 다시 추가한다.
  • 코드 라인 33~48에서 si->highest_bit가 설정되지 않았거나 write 불가능한 swap 영역인 경우 경고 메시지를 출력하고 si->avail_lists[node] 노드들을 swap_avail_heads[node[ priority 리스트에서 제거하고 nextsi: 레이블로 이동한다.
  • 코드 라인 49~51에서 THP swap 요청 시 파일 시스템을 사용하지 않은 swap 영역인 경우 THP 클러스터용 swap 엔트리들을 준비하고 swp_entries에 대입한다.
  • 코드 라인 52~54에서 THP swap 요청이 아닌 경우 @n_goal 만큼 swap 엔트리들을 준비하고 swp_entries에 대입한다.
  • 코드 라인 56~57에서 swap 엔트리가 준비되었거나, THP 클러스터 요청인 경우 check_out: 레이블로 이동한다.
  • 코드 라인 58~59에서 swap 엔트리가 준비되지 못한 경우 “scan_swap_map of si %d failed to find offset\n” 디버그 메시지를 출력한다.
  • 코드 라인 62~76에서 nextsi: 레이블이다. 다음 swap 영역을 계속 진행한다.
  • 코드 라인 80~83에서 check_out: 레이블이다. 처리가 다 완료되었다. 목표(@n_goal) 이하의 swap 엔트리가 준비된 경우 남은 swap 페이지 수를 그 차이만큼 추가하여 갱신한다.
  • 코드 라인 84~85에서 noswap: 레이블이다. 준비된 swap 엔트리 수를 반환한다.

 

swap_map을 스캔하여 1 개의 swap 엔트리 할당

scan_swap_map()

mm/swapfile.c

static unsigned long scan_swap_map(struct swap_info_struct *si,
                                   unsigned char usage)
{
        swp_entry_t entry;
        int n_ret;

        n_ret = scan_swap_map_slots(si, usage, 1, &entry);

        if (n_ret)
                return swp_offset(entry);
        else
                return 0;

}

swap 영역에서 1개의 free swap 엔트리를 스캔한 후 offset 값을 반환한다. 실패 시 0을 반환한다.

 

swap_map을 스캔하여 swap 엔트리들 할당

scan_swap_map_slots()

mm/swapfile.c -1/3-

static int scan_swap_map_slots(struct swap_info_struct *si,
                               unsigned char usage, int nr,
                               swp_entry_t slots[])
{
        struct swap_cluster_info *ci;
        unsigned long offset;
        unsigned long scan_base;
        unsigned long last_in_cluster = 0;
        int latency_ration = LATENCY_LIMIT;
        int n_ret = 0;

        if (nr > SWAP_BATCH)
                nr = SWAP_BATCH;

        /*
         * We try to cluster swap pages by allocating them sequentially
         * in swap.  Once we've allocated SWAPFILE_CLUSTER pages this
         * way, however, we resort to first-free allocation, starting
         * a new cluster.  This prevents us from scattering swap pages
         * all over the entire swap partition, so that we reduce
         * overall disk seek times between swap pages.  -- sct
         * But we do now try to find an empty cluster.  -Andrea
         * And we let swap pages go all over an SSD partition.  Hugh
         */

        si->flags += SWP_SCANNING;
        scan_base = offset = si->cluster_next;

        /* SSD algorithm */
        if (si->cluster_info) {
                if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base))
                        goto checks;
                else
                        goto scan;
        }

        if (unlikely(!si->cluster_nr--)) {
                if (si->pages - si->inuse_pages < SWAPFILE_CLUSTER) {
                        si->cluster_nr = SWAPFILE_CLUSTER - 1;
                        goto checks;
                }

                spin_unlock(&si->lock);

                /*
                 * If seek is expensive, start searching for new cluster from
                 * start of partition, to minimize the span of allocated swap.
                 * If seek is cheap, that is the SWP_SOLIDSTATE si->cluster_info
                 * case, just handled by scan_swap_map_try_ssd_cluster() above.
                 */
                scan_base = offset = si->lowest_bit;
                last_in_cluster = offset + SWAPFILE_CLUSTER - 1;

                /* Locate the first empty (unaligned) cluster */
                for (; last_in_cluster <= si->highest_bit; offset++) {
                        if (si->swap_map[offset])
                                last_in_cluster = offset + SWAPFILE_CLUSTER;
                        else if (offset == last_in_cluster) {
                                spin_lock(&si->lock);
                                offset -= SWAPFILE_CLUSTER - 1;
                                si->cluster_next = offset;
                                si->cluster_nr = SWAPFILE_CLUSTER - 1;
                                goto checks;
                        }
                        if (unlikely(--latency_ration < 0)) {
                                cond_resched();
                                latency_ration = LATENCY_LIMIT;
                        }
                }

                offset = scan_base;
                spin_lock(&si->lock);
                si->cluster_nr = SWAPFILE_CLUSTER - 1;
        }

swap 영역에서 @nr 수 만큼 free swap 엔트리를 스캔하여 @slots[] 배열에 저장해온다. 이 때 스캔해온 free swap 엔트리 수를 반환한다.

  • 코드 라인 12~13에서 한 번에 스캔할 최대 수를 SWAP_BATCH(64)개로 제한한다.
  • 코드 라인 26에서 swap 영역에 스캐닝이 완료될 때 까지 스캐닝 중이라고 SWP_SCANNING 플래그를 추가한다.
  • 코드 라인 27에서 스캔 시작점은 si->cluster_next 페이지부터이다.
  • 코드 라인 30~35에서 SSD 클러스터 방식을 사용하는 경우이다.

 

2) 빈 클러스터(256개 free swap 페이지) 스캔 (non-SSD)
  • 코드 라인 37에서 non-SSD 클러스터 방식을 사용하는 경우이다. 클러스터 번호를 1 감소시키는데 이미 0인 경우의 처리이다.
  • 코드 라인 38~41에서 swap 영역의 남은 free swap 페이지가 SWAPFILE_CLUSTER(256)보다 적은 경우 클러스터 번호를 255로 변경하고 checks 레이블로 이동한다.
  • 코드 라인 43~55에서 swap 영역에서 락을 잡은채로 첫 번째 empty 클러스터를 찾는다. 즉 lowest_bit ~ highest_bit까지 256개의 연속된 free 페이지가 발견되면 checks: 레이블로 이동한다.
    • non-ssd 방식이므로 256개의 free swap 페이지의 시작이 클러스터 단위로 정렬되어 있지 않아도 된다.
  • 코드 라인 56~57에서 offset에 해당하는 swap 페이지가 이미 swap되어 사용 중인 경우 다음 클러스터의 페이지를 진행하기 위해 진행중인 offset 페이지 += 256으로 증가시킨다.
  • 코드 라인 58~64에서 순회 중인 클러스터의 마지막 페이지에 도달한 경우 offset을 다시 해당 클러스터의 가장 첫 페이지로 되돌리고, cluster_next에 순회중인 offset을 지정하고, 클러스터 번호를 255로 변경하고 checks 레이블로 이동한다.
  • 코드 라인 65~68에서 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
  • 코드 라인 71~73에서 첫 빈 클러스터를 찾지 못한 경우이다. offset을 시작점으로 다시 되돌리고, 클러스터 번호를 255로 변경한다.

 

mm/swapfile.c -2/3-

checks:
        if (si->cluster_info) {
                while (scan_swap_map_ssd_cluster_conflict(si, offset)) {
                /* take a break if we already got some slots */
                        if (n_ret)
                                goto done;
                        if (!scan_swap_map_try_ssd_cluster(si, &offset,
                                                        &scan_base))
                                goto scan;
                }
        }
        if (!(si->flags & SWP_WRITEOK))
                goto no_page;
        if (!si->highest_bit)
                goto no_page;
        if (offset > si->highest_bit)
                scan_base = offset = si->lowest_bit;

        ci = lock_cluster(si, offset);
        /* reuse swap entry of cache-only swap if not busy. */
        if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                int swap_was_freed;
                unlock_cluster(ci);
                spin_unlock(&si->lock);
                swap_was_freed = __try_to_reclaim_swap(si, offset, TTRS_ANYWAY);
                spin_lock(&si->lock);
                /* entry was freed successfully, try to use this again */
                if (swap_was_freed)
                        goto checks;
                goto scan; /* check next one */
        }

        if (si->swap_map[offset]) {
                unlock_cluster(ci);
                if (!n_ret)
                        goto scan;
                else
                        goto done;
        }
        si->swap_map[offset] = usage;
        inc_cluster_info_page(si, si->cluster_info, offset);
        unlock_cluster(ci);

        swap_range_alloc(si, offset, 1);
        si->cluster_next = offset + 1;
        slots[n_ret++] = swp_entry(si->type, offset);

        /* got enough slots or reach max slots? */
        if ((n_ret == nr) || (offset >= si->highest_bit))
                goto done;

        /* search for next available slot */

        /* time to take a break? */
        if (unlikely(--latency_ration < 0)) {
                if (n_ret)
                        goto done;
                spin_unlock(&si->lock);
                cond_resched();
                spin_lock(&si->lock);
                latency_ration = LATENCY_LIMIT;
        }

        /* try to get more slots in cluster */
        if (si->cluster_info) {
                if (scan_swap_map_try_ssd_cluster(si, &offset, &scan_base))
                        goto checks;
                else
                        goto done;
        }
        /* non-ssd case */
        ++offset;

        /* non-ssd case, still more slots in cluster? */
        if (si->cluster_nr && !si->swap_map[offset]) {
                --si->cluster_nr;
                goto checks;
        }

done:
        si->flags -= SWP_SCANNING;
        return n_ret;
case 1) 현재 페이지(si->cluster_next) 할당 체크 (non-SSD)
  • 코드 라인 1에서 checks: 레이블이다.
  • 코드 라인 2~11에서 SSD 클러스터 방식을 사용하는 경우이다.
  • 코드 라인 12~13에서 swap 영역에 SWP_WRITEOK 플래그가 없으면 no_page 레이블로 이동한다.
  • 코드 라인 14~15에서 swap 영역에 가장 큰 페이지 번호인 highest_bit가 설정되지 않은 경우에도 no_page 레이블로 이동한다.
  • 코드 라인 16~17에서 offset 페이지가 가장 큰 페이지 번호를 초과한 경우에는 다시 scan_base와 offset을 가장 낮은 페이지 번호인 lowest_bit로 변경한다.
  • 코드 라인 19에서 offset에 해당하는 클러스터를 lock 하고 가져온다.
  • 코드 라인 21~31에서 전체 swap 페이지가 사용 가능한 free swap 영역의 절반을 초과한 경우이면서 offset에 SWAP_HAS_CACHE 값으로 설정된 경우 해당 swap 캐시를 제거한다. 제거가 성공한 경우 checks 레이블로 이동하고, 그렇지 않은 경우 다음을 위해 scan 레이블로 이동한다.
  • 코드 라인 33~39에서 offset 페이지에 대한 swap_map[]이 이미 사용 중인 경우이다. n_ret 값이 있으면 done 레이블로 이동하고, 없으면 다음을 위해 scan 레이블로 이동한다.
  • 코드 라인 40~42에서 offset 페이지에 대한 swap_map[]이 빈 경우이다. free swap 상태이므로 할당을 위해 참조 카운터 @usage 값을 대입하고, 클러스터를 사용 중으로 표기한다. 그런 후 클러스터 lock을 푼다.
  • 코드 라인 44에서 swap 영역(lowest_bit, highest_bit)을 1 페이지만큼 추가 갱신한다. 만일 swap 영역내의 모든 페이지가 할당 완료된 경우 swap_avail_heads에서 swap 영역을 제거한다.
  • 코드 라인 45~46에서 다음 클러스터를 위해 현재 offset 페이지 + 1을 한다. 출력 인자 @slots[]에 offset에 해당하는 swap 엔트리를 저장한다.
  • 코드 라인 49~50에서 요청한 수만큼 슬롯을 채웠거나 영역의 끝까지 진행을 한 경우 done 레이블로 이동한다.
  • 코드 라인 55~62에서 인터럽트 레이튼시를 줄이는 방법이다. swap 영역에 대해 오랫동안 락을 잡고 있지 않기 위해 LATENCY_LIMIT(256) 페이지 마다 잠시 lock을 풀었다가 다시 획득한다.
  • 코드 라인 65~70에서 SSD 클러스터 방식을 사용하는 경우이다.
  • 코드 라인 72~78에서 SSD 클러스터 방식이 아닌 경우의 동작이다. 다음 페이지를 위해 offset을 증가시키고 슬롯이 더 필요한 경우 클러스터 번호를 감소시키고 checks 레이블로 이동한다.
  • 코드 라인 80~82에서 done: 레이블이다. 스캐닝일 수행하는 동안 설정한 SWP_SCANNING 플래그를 swap 영역에서 제거하고 슬롯에 준비한 swap 엔트리 수를 반환한다.

 

mm/swapfile.c -3/3-

scan:
        spin_unlock(&si->lock);
        while (++offset <= si->highest_bit) {
                if (!si->swap_map[offset]) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (unlikely(--latency_ration < 0)) {
                        cond_resched();
                        latency_ration = LATENCY_LIMIT;
                }
        }
        offset = si->lowest_bit;
        while (offset < scan_base) {
                if (!si->swap_map[offset]) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (vm_swap_full() && si->swap_map[offset] == SWAP_HAS_CACHE) {
                        spin_lock(&si->lock);
                        goto checks;
                }
                if (unlikely(--latency_ration < 0)) {
                        cond_resched();
                        latency_ration = LATENCY_LIMIT;
                }
                offset++;
        }
        spin_lock(&si->lock);

no_page:
        si->flags -= SWP_SCANNING;
        return n_ret;
}
3) 현재 페이지 ~ 영역 끝까지 스캔 (non-SSD)
  • 코드 라인 1~16에서 scan: 레이블이다. swap 영역의 free 할당 가능한 마지막 페이지까지 순회하며 offset을 증가하며 다음 두 경우에 한하여 checks 레이블로 이동한다. 또한 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
    • swap_map[offset]이 free 상태로 할당 가능한 상태이다.
    • swap 영역이 50% 이상 가득 차고 offset에 해당하는 swap 영역은 free 상태고 swap 캐시만 존재하는 경우이다.

 

4) 시작 페이지 ~ 현재 페이지까지 스캔 (non-SSD)
  • 코드 라인 17~32에서 offset을 swap 영역의 free 할당 가능한 첫 페이지부터 scan_base까지 순회하며 다음 두 경우에 한하여 checks 레이블로 이동한다. 또한 순회중에 LATENCY_LIMIT(256) 페이지 마다 preemption point를 수행한다.
    • swap_map[offset]이 free 상태로 할당 가능한 상태이다.
    • swap 영역이 50% 이상 가득 차고 offset에 해당하는 swap 영역은 free 상태고 swap 캐시만 존재하는 경우이다.

 

  • 코드 라인 35~37에서 no_page: 레이블이다. 스캐닝일 수행하는 동안 설정한 SWP_SCANNING 플래그를 swap 영역에서 제거하고 슬롯에 준비한 swap 엔트리 수를 반환한다.

 

다음 4개의 그림은 1개의 free swap 엔트리를 찾는 순서이다.

 

첫 번째, si->cluster_next가 현재 위치의 페이지로 할당 가능한 상태인지 체크한다.

 

두 번째, 현재 위치의 페이지로 할당 불가능한 경우 다음으로 swap 영역 전체에서 256개의 빈 swap 페이지가 있는지 확인한다.

 

세 번째, 빈 클러스터가 없는 경우 현재 위치에서 swap 영역 끝까지 스캔한다.

 

네 번째, 마지막으로 swap 영역 처음부터 현재 위치까지 스캔한다.

 

swap 엔트리들을 swap 영역으로 반환

swapcache_free_entries()

mm/swapfile.c

void swapcache_free_entries(swp_entry_t *entries, int n)
{
        struct swap_info_struct *p, *prev;
        int i;

        if (n <= 0)
                return;

        prev = NULL;
        p = NULL;

        /*
         * Sort swap entries by swap device, so each lock is only taken once.
         * nr_swapfiles isn't absolutely correct, but the overhead of sort() is
         * so low that it isn't necessary to optimize further.
         */
        if (nr_swapfiles > 1)
                sort(entries, n, sizeof(entries[0]), swp_entry_cmp, NULL);
        for (i = 0; i < n; ++i) {
                p = swap_info_get_cont(entries[i], prev);
                if (p)
                        swap_entry_free(p, entries[i]);
                prev = p;
        }
        if (p)
                spin_unlock(&p->lock);
}

@n개의 swap 엔트리 @entries를 free 상태로 변경한다.

  • 코드 라인 6~7에서 @n이 0개 이하이면 함수를 빠져나간다.
  • 코드 라인 17~18에서 요청한 swap 엔트리들의 swap 영역이 섞여 있으면 이를 접근하기 위해 lock을 잡아야 하는데 이의 빈번함을 피하기 위해 먼저 swap 엔트리의 소팅을 수행 한다.
  • 코드 라인 19~24에서 @n개를 순회하며 swap 엔트리를 free 한다. 만일 swap 영역이 바뀌는 경우 unlock하고 다시 새로운 swap 영역을 lock 한다.

 

swap_info_get_cont()

mm/swapfile.c

static struct swap_info_struct *swap_info_get_cont(swp_entry_t entry,
                                        struct swap_info_struct *q)
{
        struct swap_info_struct *p;

        p = _swap_info_get(entry);

        if (p != q) {
                if (q != NULL)
                        spin_unlock(&q->lock);
                if (p != NULL)
                        spin_lock(&p->lock);
        }
        return p;
}

swap 영역이 바뀌면 기존 swap 영역 @q를 unlock하고 새로운 swap 영역 p의 lock을 획득한다.

 

swap 엔트리 할당 해제

swap_entry_free()

mm/swapfile.c

static void swap_entry_free(struct swap_info_struct *p, swp_entry_t entry)
{
        struct swap_cluster_info *ci;
        unsigned long offset = swp_offset(entry);
        unsigned char count;

        ci = lock_cluster(p, offset);
        count = p->swap_map[offset];
        VM_BUG_ON(count != SWAP_HAS_CACHE);
        p->swap_map[offset] = 0;
        dec_cluster_info_page(p, p->cluster_info, offset);
        unlock_cluster(ci);

        mem_cgroup_uncharge_swap(entry, 1);
        swap_range_free(p, offset, 1);
}

swap 엔트리를 할당 해제한다.

  • 코드 라인 7에서 offset 페이지가 소속된 클러스터의 락을 획득한다.
  • 코드 라인 7~10에서 offset 페이지에 대한 swap_map[]의 값을 count로 알아온 후 0으로 클리어하여 free 상태로 변경 한다.
  • 코드 라인 11에서 offset 페이지가 소속된 클러스터의 사용 카운터를 1 감소시킨다.
  • 코드 라인 12에서 클러스터 락을 푼다.
  • 코드 라인 14에서 memcg를 통해 swap 엔트리의 회수를 보고한다.
  • 코드 라인 15에서 swap 엔트리의 할당 해제로 인해 할당 가능한 범위를 조정한다.

 

swap 영역 할당/해제 후 사용 가능 영역 조정

swap_range_alloc()

mm/swapfile.c

static void swap_range_alloc(struct swap_info_struct *si, unsigned long offset,
                             unsigned int nr_entries)
{
        unsigned int end = offset + nr_entries - 1;

        if (offset == si->lowest_bit)
                si->lowest_bit += nr_entries;
        if (end == si->highest_bit)
                si->highest_bit -= nr_entries;
        si->inuse_pages += nr_entries;
        if (si->inuse_pages == si->pages) {
                si->lowest_bit = si->max;
                si->highest_bit = 0;
                del_from_avail_list(si);
        }
}

swap 영역의 @offset 부터 @nr_entries 만큼 할당을 통한 할당 가능 범위를 갱신한다.

  • 코드 라인 6~9에서 offset ~ nr_entries -1 범위를 할당할 때 최소 페이지 또는 최대 페이지를 필요 시 갱신한다.
  • 코드 라인 10에서 si->inuse_pages를 할당 한 페이지 수 만큼 추가한다.
  • 코드 라인 11~15에서 swap 영역을 다 사용한 경우 이 swap 영역을 swap_avail_heads 리스트에서 제거한다.

 

swap_range_free()

mm/swapfile.c

static void swap_range_free(struct swap_info_struct *si, unsigned long offset,
                            unsigned int nr_entries)
{
        unsigned long end = offset + nr_entries - 1;
        void (*swap_slot_free_notify)(struct block_device *, unsigned long);

        if (offset < si->lowest_bit)
                si->lowest_bit = offset;
        if (end > si->highest_bit) {
                bool was_full = !si->highest_bit;

                si->highest_bit = end;
                if (was_full && (si->flags & SWP_WRITEOK))
                        add_to_avail_list(si);
        }
        atomic_long_add(nr_entries, &nr_swap_pages);
        si->inuse_pages -= nr_entries;
        if (si->flags & SWP_BLKDEV)
                swap_slot_free_notify =
                        si->bdev->bd_disk->fops->swap_slot_free_notify;
        else
                swap_slot_free_notify = NULL;
        while (offset <= end) {
                frontswap_invalidate_page(si->type, offset);
                if (swap_slot_free_notify)
                        swap_slot_free_notify(si->bdev, offset);
                offset++;
        }
}

swap 영역의 @offset 부터 @nr_entries 만큼 할당 해제를 통한 할당 가능 범위를 갱신한다.

  • 코드 라인 4~12에서 offset ~ nr_entries -1 범위를 할당 해제할 때 최소 페이지 또는 최대 페이지를 필요 시 갱신한다.
  • 코드 라인 13~14에서 새로 사용 가능한 영역이 생긴 경우이므로 이 swap 영역을 &swap_avail_heads 리스트에 추가한다.
  • 코드 라인 17에서 si->inuse_pages를 할당 해제한 페이지 수 만큼 감소시킨다.
  • 코드 라인 18~28에서 offset ~ nr_entries -1 범위에 대해 frontswap에서 페이지를 invalidate 한다. 그리고 블럭 디바이스를 사용한 swap 영역인 경우 (*swap_slot_free_notify) 후크 함수를 호출하여 swap 영역이 free 되었음을 통지한다.

 

swap_avail_heads[] 리스트

swap_avail_heads[] 리스트는 노드별로 운영되며, 사용 가능한 swap 영역이 등록되어 있다.

 

swapfile_init()

mm/swapfile.c”

static int __init swapfile_init(void)
{
        int nid;

        swap_avail_heads = kmalloc_array(nr_node_ids, sizeof(struct plist_head),
                                         GFP_KERNEL);
        if (!swap_avail_heads) {
                pr_emerg("Not enough memory for swap heads, swap is disabled\n");
                return -ENOMEM;
        }

        for_each_node(nid)
                plist_head_init(&swap_avail_heads[nid]);

        return 0;
}
subsys_initcall(swapfile_init);

swap_avail_heads[] 리스트를 노드 수 만큼 할당한 후 초기화한다.

  • 이 리스트에는 swapon시 사용될 swap 영역이 등록된다.

 

add_to_avail_list()

mm/swapfile.c

static void add_to_avail_list(struct swap_info_struct *p)
{
        int nid;

        spin_lock(&swap_avail_lock);
        for_each_node(nid) {
                WARN_ON(!plist_node_empty(&p->avail_lists[nid]));
                plist_add(&p->avail_lists[nid], &swap_avail_heads[nid]);
        }
        spin_unlock(&swap_avail_lock);
}

요청한 swap 영역을 swap_avail_heads[]을 모든 노드에 추가한다.

 

del_from_avail_list()

mm/swapfile.c

static void del_from_avail_list(struct swap_info_struct *p)
{
        spin_lock(&swap_avail_lock);
        __del_from_avail_list(p);
        spin_unlock(&swap_avail_lock);
}

요청한 swap 영역을 swap_avail_heads[] 리스트에서 제거한다.

 

__del_from_avail_list()

mm/swapfile.c

static void __del_from_avail_list(struct swap_info_struct *p)
{
        int nid;

        for_each_node(nid)
                plist_del(&p->avail_lists[nid], &swap_avail_heads[nid]);
}

요청한 swap 영역을 모든 노드의 swap_avail_heads[] 리스트에서 제거한다.

 


클러스터를 사용한 swap 페이지 할당

SSD 및 persistent 메모리를 사용한 블럭 디바이스를 swap 영역으로 사용할 때 가능한 방법이다. 메모리 부족 시 여러 cpu에서 동시에 swap을 시도하게 되는데 각 cpu들이 클러스터별로 동작을 하게 설계되어 있다. 그 외에도 각 cpu가 각자의 cpu에 해당하는 swap_cluster_info 구조체에 접근할 때 false cache line share 현상으로 성능이 저하되는 것을 최대한 줄이기 위해 free 클러스터 리스트에 각 클러스터들을 그냥 순번대로 등록하지 않고 최소한 캐시 라인 크기 이상으로 떨어뜨리면 이러한 문제를 해결할 수 있다.

 

구조체

swap_cluster_info 구조체

include/linux/swap.h

/*
 * We use this to track usage of a cluster. A cluster is a block of swap disk
 * space with SWAPFILE_CLUSTER pages long and naturally aligns in disk. All
 * free clusters are organized into a list. We fetch an entry from the list to
 * get a free cluster.
 *
 * The data field stores next cluster if the cluster is free or cluster usage
 * counter otherwise. The flags field determines if a cluster is free. This is
 * protected by swap_info_struct.lock.
 */
struct swap_cluster_info {
        spinlock_t lock;        /*
                                 * Protect swap_cluster_info fields
                                 * and swap_info_struct->swap_map
                                 * elements correspond to the swap
                                 * cluster
                                 */
        unsigned int data:24;
        unsigned int flags:8;
};

하나의 클러스터 상태 및 다음 free 클러스터 번호를 가리키기 위해 사용되는 자료 구조이다.

  • lock
    • 클러스터 락
  • data:24
    • free 클러스터 상태
      • 다음 free 클러스터 번호
  • flags:8
    • CLUSTER_FLAG_FREE (1)
      • free 클러스터
    • CLUSTER_FLAG_NEXT_NULL (2)
      • 마지막 free 클러스터
    • CLUSTER_FLAG_HUGE (4)
      • thp용 클러스터

 

percpu_cluster 구조체

include/linux/swap.h

/*
 * We assign a cluster to each CPU, so each CPU can allocate swap entry from
 * its own cluster and swapout sequentially. The purpose is to optimize swapout
 * throughput.
 */
struct percpu_cluster {
        struct swap_cluster_info index; /* Current cluster index */
        unsigned int next; /* Likely next allocation offset */
};

per-cpu에 사용되는 클러스터 자료 구조이다.

  • index
    • 현재 클러스터를 담고 있다.
  • next
    • 높은 확률로 다음 할당 페이지 offset을 담고 있다.

 

swap_cluster_list 구조체

include/linux/swap.h

struct swap_cluster_list {
        struct swap_cluster_info head;
        struct swap_cluster_info tail;
};

다음 swap 클러스터 리스트에 사용된다.

  • si->free_clusters 리스트
  • si->discard_clusters 리스트

 

다음 그림은 각 cpu가 클러스터를 하나씩 지정하여 사용하는 모습을 보여준다.

  • 지정되지 않는 경우 해당 cpu의 si->cluster 값에 null을 사용한다.

 

다음 그림은 free swap 클러스터들을 연결한 상태의 free swap 클러스터 리스트를 보여준다.

 

SWAP_CLUSTER_COLS

mm/swapfile.c

#define SWAP_CLUSTER_COLS                                               \
        max_t(unsigned int, SWAP_CLUSTER_INFO_COLS, SWAP_CLUSTER_SPACE_COLS)

free 클러스터 리스트를 처음 구성할 때 false cache line share 현상을 감소시키기 위해 swap_info_cluster 구조체를 건너뛰어야 할 간격을 산출한다.

 

mm/swapfile.c

#define SWAP_CLUSTER_INFO_COLS                                          \
        DIV_ROUND_UP(L1_CACHE_BYTES, sizeof(struct swap_cluster_info))
#define SWAP_CLUSTER_SPACE_COLS                                         \
        DIV_ROUND_UP(SWAP_ADDRESS_SPACE_PAGES, SWAPFILE_CLUSTER)

 

다음과 같이 swap_cluster_info 구조체들이 free_clusters 리스트에 구성될 때 SWAP_CLUSTER_COLS 번호 간격으로 등록되는 모습을 보여준다.

  • 모든 클러스터를 다음과 같은 순서대로 등록한다.
  • 0, 64, 128, 192, ….
  • 1, 65, 129, 193, ….
  • 2, 66, 130, 194, ….

 

클러스터 하나 통째 할당 (SSD 아니더라도 사용 가능)

swap_alloc_cluster()

mm/swapfile.c

static int swap_alloc_cluster(struct swap_info_struct *si, swp_entry_t *slot)
{
        unsigned long idx;
        struct swap_cluster_info *ci;
        unsigned long offset, i;
        unsigned char *map;

        /*
         * Should not even be attempting cluster allocations when huge
         * page swap is disabled.  Warn and fail the allocation.
         */
        if (!IS_ENABLED(CONFIG_THP_SWAP)) {
                VM_WARN_ON_ONCE(1);
                return 0;
        }

        if (cluster_list_empty(&si->free_clusters))
                return 0;

        idx = cluster_list_first(&si->free_clusters);
        offset = idx * SWAPFILE_CLUSTER;
        ci = lock_cluster(si, offset);
        alloc_cluster(si, idx);
        cluster_set_count_flag(ci, SWAPFILE_CLUSTER, CLUSTER_FLAG_HUGE);

        map = si->swap_map + offset;
        for (i = 0; i < SWAPFILE_CLUSTER; i++)
                map[i] = SWAP_HAS_CACHE;
        unlock_cluster(ci);
        swap_range_alloc(si, offset, SWAPFILE_CLUSTER);
        *slot = swp_entry(si->type, offset);

        return 1;
}

thp swap을 위해 사용될 free swap 클러스터(256 페이지)를 통째로 할당할 swap 엔트리를 출력 인자 @slot에 저장한다. 성공하는 경우 1을 반환한다.

  • 코드 라인 12~15에서 thp swap을 지원하지 않는 커널의 경우 실패로 0을 반환한다.
  • 코드 라인 17~18에서 free 클러스터 리스트에 남은 free 클러스터가 없는 경우 실패로 0을 반환한다.
  • 코드 라인 20~24에서 free 클러스터 리스트의 첫 클러스터 번호(idx)에 해당하는 클러스터를 huge 할당 상태로 변경한다.
  • 코드 라인 26~28에서 해당 클러스터에 포함된 페이지들에 대한 할당 상태를 SWAP_HAS_CACHE로 변경한다.
  • 코드 라인 30에서 해당 클러스터를 할당 상태로 바꿈에 따라 lowest_bit 및 highest_bit 범위의 변경 시 갱신한다.
  • 코드 라인 31~33에서 출력 인자 @slot에 swap 엔트리를 저장하고, 성공 1을 반환한다.

 

scan_swap_map_ssd_cluster_conflict()

mm/swapfile.c

/*
 * It's possible scan_swap_map() uses a free cluster in the middle of free
 * cluster list. Avoiding such abuse to avoid list corruption.
 */
static bool
scan_swap_map_ssd_cluster_conflict(struct swap_info_struct *si,
        unsigned long offset)
{
        struct percpu_cluster *percpu_cluster;
        bool conflict;

        offset /= SWAPFILE_CLUSTER;
        conflict = !cluster_list_empty(&si->free_clusters) &&
                offset != cluster_list_first(&si->free_clusters) &&
                cluster_is_free(&si->cluster_info[offset]);

        if (!conflict)
                return false;

        percpu_cluster = this_cpu_ptr(si->percpu_cluster);
        cluster_set_null(&percpu_cluster->index);
        return true;
}

요청한 @offset 페이지가 free 클러스터의 첫 free 클러스터인지 확인한다. 만일 그렇지 않은 경우 conflict 상황이므로 true를 반환한다.

  • 코드 라인 8~11에서 scan_swap_map()은 사용 가능한 클러스터 리스트 중간에 있는 free 클러스터를 사용할 수 있다. 이러한 목록 손상을 피하기는 방법이다. 만일 요청한 @offset 페이지가 free 클러스터의 처음이 아닌 경우 conflict 상황이다.
  • 코드 라인 13~14에서 conflict 상황이 아니면 정상적으로 false를 반환한다.
  • 코드 라인 16~18에서 conflict 상황인 경우 현재 cpu의 percpu_cluster를 null로 설정하고 true를 반환한다.

 

SSD 클러스터 단위 swap 맵 할당

scan_swap_map_try_ssd_cluster()

mm/swapfile.c

/*
 * Try to get a swap entry from current cpu's swap entry pool (a cluster). This
 * might involve allocating a new cluster for current CPU too.
 */
static bool scan_swap_map_try_ssd_cluster(struct swap_info_struct *si,
        unsigned long *offset, unsigned long *scan_base)
{
        struct percpu_cluster *cluster;
        struct swap_cluster_info *ci;
        bool found_free;
        unsigned long tmp, max;

new_cluster:
        cluster = this_cpu_ptr(si->percpu_cluster);
        if (cluster_is_null(&cluster->index)) {
                if (!cluster_list_empty(&si->free_clusters)) {
                        cluster->index = si->free_clusters.head;
                        cluster->next = cluster_next(&cluster->index) *
                                        SWAPFILE_CLUSTER;
                } else if (!cluster_list_empty(&si->discard_clusters)) {
                        /*
                         * we don't have free cluster but have some clusters in
                         * discarding, do discard now and reclaim them
                         */
                        swap_do_scheduled_discard(si);
                        *scan_base = *offset = si->cluster_next;
                        goto new_cluster;
                } else
                        return false;
        }

        found_free = false;

        /*
         * Other CPUs can use our cluster if they can't find a free cluster,
         * check if there is still free entry in the cluster
         */
        tmp = cluster->next;
        max = min_t(unsigned long, si->max,
                    (cluster_next(&cluster->index) + 1) * SWAPFILE_CLUSTER);
        if (tmp >= max) {
                cluster_set_null(&cluster->index);
                goto new_cluster;
        }
        ci = lock_cluster(si, tmp);
        while (tmp < max) {
                if (!si->swap_map[tmp]) {
                        found_free = true;
                        break;
                }
                tmp++;
        }
        unlock_cluster(ci);
        if (!found_free) {
                cluster_set_null(&cluster->index);
                goto new_cluster;
        }
        cluster->next = tmp + 1;
        *offset = tmp;
        *scan_base = tmp;
        return found_free;
}

현재 cpu에 지정된 클러스터에서 free swap 엔트리를 찾아 출력 인자 @offset과 @scan_base에 저장한다. 실패하는 경우 false를 반환한다.

  • 코드 라인 9에서 새 클러스터를 다시 찾아야 할 때 이동될 new_cluster: 레이블이다.
  • 코드 라인 10~26에서 현재 cpu에 지정된 클러스터가 없는 경우 다음 중 하나를 수행한다.
    • free 클러스터 리스트에서 준비한다.
    • free 클러스터 리스트에 등록된 free 클러스터가 없으면 discard 클러스터 리스트에 있는 클러스터들을 블럭 디바이스에 discard 요청하고 free 클러스터에 옮긴다. 그런 후 다시 new_cluster: 레이블로 이동하여 다시 시도한다.
    • free 클러스터 리스트 및 discard 클러스터 리스트에 등록된 클러스터가 없는 경우 사용할 수 있는 클러스터가 없어 false를 반환한다.
  • 코드 라인 34~40에서 클러스터 내 찾을 next 페이지를 tmp에 대입하고, 다음 클러스터의 마지막 페이지를 벗어난 경우 현재 cpu가 이 클러스터에 null을 대입하여 사용을 포기하게 한다. 그리고 다시 새로운 클러스터를 찾으러 new_cluster 레이블로 이동한다. 클러스터의 락을 획득한 상태가 아니므로 경쟁 상황에서 이미 다른 cpu가 이 클러스터를 사용했을 수 있으므로 이 클러스터를 포기한다.
  • 코드 라인 41~53에서 tmp 페이지가 소속한 클러스터 락을 획득하 상태에서 tmp ~ max 페이지까지 순회하며 swap_map[]에 free swap 페이지가 있는지 찾아 발견되면 found_free에 true를 대입하고 루프를 탈출한다. 만일 이 클러스터에 free swap 페이지가 하나도 발견되지 않으면 이 클러스터를 포기하고, 다시 새로운 클러스터를 찾으러 new_cluster: 레이블로 이동한다.
  • 코드 라인 54~57에서 찾은 페이지를 @offset과 @scan_base에 저장하고 성공 결과인 true를 반환한다.

 

클러스터 사용 증가

inc_cluster_info_page()

mm/swapfile.c

/*
 * The cluster corresponding to page_nr will be used. The cluster will be
 * removed from free cluster list and its usage counter will be increased.
 */
static void inc_cluster_info_page(struct swap_info_struct *p,
        struct swap_cluster_info *cluster_info, unsigned long page_nr)
{
        unsigned long idx = page_nr / SWAPFILE_CLUSTER;

        if (!cluster_info)
                return;
        if (cluster_is_free(&cluster_info[idx]))
                alloc_cluster(p, idx);

        VM_BUG_ON(cluster_count(&cluster_info[idx]) >= SWAPFILE_CLUSTER);
        cluster_set_count(&cluster_info[idx],
                cluster_count(&cluster_info[idx]) + 1);
}

@page_nr에 해당하는 클러스터의 사용 카운터를 증가시킨다. 처음 사용되는 경우 free 클러스터를 할당 상태로 변경한다.

  • 코드 라인 6~7에서 SSD 클러스터 방식을 사용하지 않는 경우 아무것도 하지 않고 함수를 빠져나간다.
  • 코드 라인 8~9에서 @idx 클러스터가 free 상태이면 free 클러스터에서 제거하고 할당 상태로 변경한다.
  • 코드 라인 11에서 @idx 클러스터의 사용 카운터가 256 이상이 되면 버그다.
  • 코드 라인 12~13에서 @idx 클러스터의 사용 카운터를 1 증가 시킨다.

 

 

alloc_cluster()

mm/swapfile.c

static void alloc_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info;

        VM_BUG_ON(cluster_list_first(&si->free_clusters) != idx);
        cluster_list_del_first(&si->free_clusters, ci);
        cluster_set_count_flag(ci + idx, 0, 0);
}

@idx 클러스터를 free 클러스터에서 제거하고, 사용 카운터를 0으로 설정한다.

 

 

클러스터 사용 감소

dec_cluster_info_page()

mm/swapfile.c

/*
 * The cluster corresponding to page_nr decreases one usage. If the usage
 * counter becomes 0, which means no page in the cluster is in using, we can
 * optionally discard the cluster and add it to free cluster list.
 */
static void dec_cluster_info_page(struct swap_info_struct *p,
        struct swap_cluster_info *cluster_info, unsigned long page_nr)
{
        unsigned long idx = page_nr / SWAPFILE_CLUSTER;

        if (!cluster_info)
                return;

        VM_BUG_ON(cluster_count(&cluster_info[idx]) == 0);
        cluster_set_count(&cluster_info[idx],
                cluster_count(&cluster_info[idx]) - 1);

        if (cluster_count(&cluster_info[idx]) == 0)
                free_cluster(p, idx);
}

@page_nr에 해당하는 클러스터의 사용 카운터를 감소시킨다. 사용 카운터가 0이 되면 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

  • 코드 라인 6~7에서 SSD 클러스터 방식을 사용하지 않는 경우 아무것도 하지 않고 함수를 빠져나간다.
  • 코드 라인 9에서 @idx 클러스터의 사용 카운터가 0인 경우 버그다.
  • 코드 라인 10~11에서 @idx 클러스터의 사용 카운터를 1 감소 시킨다.
  • 코드 라인 13~14에서 @idx 클러스터의 사용 카운터가 0이되면 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

free_cluster()

mm/swapfile.c

static void free_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info + idx;

        VM_BUG_ON(cluster_count(ci) != 0);
        /*
         * If the swap is discardable, prepare discard the cluster
         * instead of free it immediately. The cluster will be freed
         * after discard.
         */
        if ((si->flags & (SWP_WRITEOK | SWP_PAGE_DISCARD)) ==
            (SWP_WRITEOK | SWP_PAGE_DISCARD)) {
                swap_cluster_schedule_discard(si, idx);
                return;
        }

        __free_cluster(si, idx);
}

@idx 클러스터를 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

  • 코드 라인 11~15에서 discard가 허용된 기록 가능한 swap 영역인 경우 @idx 클러스트에 대해 워커 스레드를 통해  discard 후 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.
  • 코드 라인 17에서 그 외의 경우 @idx 클러스터를 곧바로 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

 

discard 정책

SSD 장치가 discard 옵션을 사용하여 마운트되었거나 트림(trim) 작업을 지원하는 경우 swapon을 사용하여 swap 영역을 지정할 때 discard 옵션을 추가하여 swap discard 기능을 사용할 수 있다. 이러한 경우 일부 SSD 장치의 성능을 향상시킬 수도 있다. swapon의 discard 옵션은 다음 두 가지를 지정하거나, 지정하지 않으면 두 가지 정책을 다 사용한다.

  • –discard=once
    • 전체 swap 영역에 대해 한 번만 discard 작업을 수행한다.
  • –discard=pages
    • 사용 가능한 swap 페이지를 재사용하기 전에 비동기적으로 discard 한다.

 

__free_cluster()

mm/swapfile.c

static void __free_cluster(struct swap_info_struct *si, unsigned long idx)
{
        struct swap_cluster_info *ci = si->cluster_info;

        cluster_set_flag(ci + idx, CLUSTER_FLAG_FREE);
        cluster_list_add_tail(&si->free_clusters, ci, idx);
}

@idx 클러스터를 free 상태로 변경하고 free 클러스터 리스트에 추가한다.

 

스케줄드 discard 클러스터

swap_cluster_schedule_discard()

mm/swapfile.c

/* Add a cluster to discard list and schedule it to do discard */
static void swap_cluster_schedule_discard(struct swap_info_struct *si,
                unsigned int idx)
{
        /*
         * If scan_swap_map() can't find a free cluster, it will check
         * si->swap_map directly. To make sure the discarding cluster isn't
         * taken by scan_swap_map(), mark the swap entries bad (occupied). It
         * will be cleared after discard
         */
        memset(si->swap_map + idx * SWAPFILE_CLUSTER,
                        SWAP_MAP_BAD, SWAPFILE_CLUSTER);

        cluster_list_add_tail(&si->discard_clusters, si->cluster_info, idx);

        schedule_work(&si->discard_work);
}

스케줄 워크를 동작시켜 @idx 클러스터를 discard 요청 후 free 클러스터에 추가하게 한다.

  • 코드 라인 11~12에서 @idx 클러스터의 모든 페이지에 해당하는 swap_map[]을 bad 페이지로 마크한다.
  • 코드 라인 14에서 이 클러스터를 discard 클러스터에 등록한다.
  • 코드 라인 16에서 스케줄 워크를 동작시켜 swap_discard_work() 함수를 호출한다. 이 함수에서는 블럭 디바이스에 discard를 요청 후 free 상태로 변경하고 free 클러스터 리스트에 추가하는 작업을 수행한다.

 

swap_discard_work()

mm/swapfile.c

static void swap_discard_work(struct work_struct *work)
{
        struct swap_info_struct *si;

        si = container_of(work, struct swap_info_struct, discard_work);

        spin_lock(&si->lock);
        swap_do_scheduled_discard(si);
        spin_unlock(&si->lock);
}

discard 클러스터 리스트의 클러스터들을 discard 요청 후 free 클러스터로 옮긴다.

 

swap_do_scheduled_discard()

mm/swapfile.c

/*
 * Doing discard actually. After a cluster discard is finished, the cluster
 * will be added to free cluster list. caller should hold si->lock.
*/
static void swap_do_scheduled_discard(struct swap_info_struct *si)
{
        struct swap_cluster_info *info, *ci;
        unsigned int idx;

        info = si->cluster_info;

        while (!cluster_list_empty(&si->discard_clusters)) {
                idx = cluster_list_del_first(&si->discard_clusters, info);
                spin_unlock(&si->lock);

                discard_swap_cluster(si, idx * SWAPFILE_CLUSTER,
                                SWAPFILE_CLUSTER);

                spin_lock(&si->lock);
                ci = lock_cluster(si, idx * SWAPFILE_CLUSTER);
                __free_cluster(si, idx);
                memset(si->swap_map + idx * SWAPFILE_CLUSTER,
                                0, SWAPFILE_CLUSTER);
                unlock_cluster(ci);
        }
}

discard 클러스터 리스트의 클러스터들을 discard 요청 후 free 클러스터로 옮긴다.

  • 코드 라인 8~9에서 discard 클러스터 리스트의 모든 클러스터를 순회하며 discard 클러스터 리스트에서 제거한다.
  • 코드 라인 12~13에서 블록 디바이스에 각 클러스터에 포함된 페이지들을 모두 discard 요청한다.
  • 코드 라인 15~20에서 cluster 락을 획득한 채로 순회 중인 클러스터를 free 상태로 변경하고 free 클러스터 리스트의 마지막에 추가한다.

 

discard_swap_cluster()

mm/swapfile.c

/*
 * swap allocation tell device that a cluster of swap can now be discarded,
 * to allow the swap device to optimize its wear-levelling.
 */
static void discard_swap_cluster(struct swap_info_struct *si,
                                 pgoff_t start_page, pgoff_t nr_pages)
{
        struct swap_extent *se = si->curr_swap_extent;
        int found_extent = 0;

        while (nr_pages) {
                if (se->start_page <= start_page &&
                    start_page < se->start_page + se->nr_pages) {
                        pgoff_t offset = start_page - se->start_page;
                        sector_t start_block = se->start_block + offset;
                        sector_t nr_blocks = se->nr_pages - offset;

                        if (nr_blocks > nr_pages)
                                nr_blocks = nr_pages;
                        start_page += nr_blocks;
                        nr_pages -= nr_blocks;

                        if (!found_extent++)
                                si->curr_swap_extent = se;

                        start_block <<= PAGE_SHIFT - 9;
                        nr_blocks <<= PAGE_SHIFT - 9;
                        if (blkdev_issue_discard(si->bdev, start_block,
                                    nr_blocks, GFP_NOIO, 0))
                                break;
                }

                se = list_next_entry(se, list);
        }
}

discard를 지원하는 SSD 스타일의 swap 장치에 @start_page 부터 @nr_pages만큼 discard 요청한다.

 


Per-cpu Swap 슬롯 캐시

swap 영역의 swap_map[]을 통해 swap 엔트리를 할당한다. 이 때마다 swap 영역의 락이 필요하다. 이의 성능 향상을 위해 swap 엔트리 할당을 위해 앞단에 per-cpu swap 슬롯 캐시를 사용하여 swap 영역의 잠금 없이 빠르게 할당/해제할 수 있게 하였다.

 

swap_slots_cache 구조체

include/linux/swap_slots.h

struct swap_slots_cache {
        bool            lock_initialized;
        struct mutex    alloc_lock; /* protects slots, nr, cur */
        swp_entry_t     *slots;
        int             nr;
        int             cur;
        spinlock_t      free_lock;  /* protects slots_ret, n_ret */
        swp_entry_t     *slots_ret;
        int             n_ret;
};

swap 슬롯 캐시 하나에는 할당용과 회수용 swap 엔트리 배열을 가진다.

  • lock_initialized
    • 락 초기화 여부를 표시한다.
  • alloc_lock
    • swap 엔트리 할당용 swap 슬롯 캐시 락
  • *slots
    • swap 엔트리 할당용 swap 엔트리 배열
  • nr
    • swap 엔트리 할당용 swap 엔트리 수
  • cur
    • swap 엔트리 할당용 현재 swap 엔트리 번호
  • free_lock
    • swap 엔트리 회수용 swap 슬롯 캐시 락
  • *slots_ret
    • swap 엔트리 회수용 swap 엔트리 배열
  • n_ret
    • swap 엔트리 회수용 swap 엔트리 수

 

다음 그림은 swap 슬롯 캐시의 두 swap 엔트리 배열의 용도를 보여준다.

 

swap 슬롯 캐시에서 swap 엔트리 할당

get_swap_page()

mm/swap_slots.c

swp_entry_t get_swap_page(struct page *page)
{
        swp_entry_t entry, *pentry;
        struct swap_slots_cache *cache;

        entry.val = 0;

        if (PageTransHuge(page)) {
                if (IS_ENABLED(CONFIG_THP_SWAP))
                        get_swap_pages(1, &entry, HPAGE_PMD_NR);
                goto out;
        }

        /*
         * Preemption is allowed here, because we may sleep
         * in refill_swap_slots_cache().  But it is safe, because
         * accesses to the per-CPU data structure are protected by the
         * mutex cache->alloc_lock.
         *
         * The alloc path here does not touch cache->slots_ret
         * so cache->free_lock is not taken.
         */
        cache = raw_cpu_ptr(&swp_slots);

        if (likely(check_cache_active() && cache->slots)) {
                mutex_lock(&cache->alloc_lock);
                if (cache->slots) {
repeat:
                        if (cache->nr) {
                                pentry = &cache->slots[cache->cur++];
                                entry = *pentry;
                                pentry->val = 0;
                                cache->nr--;
                        } else {
                                if (refill_swap_slots_cache(cache))
                                        goto repeat;
                        }
                }
                mutex_unlock(&cache->alloc_lock);
                if (entry.val)
                        goto out;
        }

        get_swap_pages(1, &entry, 1);
out:
        if (mem_cgroup_try_charge_swap(page, entry)) {
                put_swap_page(page, entry);
                entry.val = 0;
        }
        return entry;
}

swap 슬롯 캐시를 통해 요청 페이지에 사용할 swap 엔트리를 얻어온다. 실패 시 null을 반환한다.

  • 코드 라인 8~12에서 thp의 경우 thp swap이 지원될 때에만 HPAGE_PMD_NR 단위로 swap 엔트리를 확보한다.
    • 현재 x86_64 아키텍처가 thp  swap이 지원되고 있다.
  • 코드 라인 23~42에서 thp가 아닌 경우이다. swap 슬롯 캐시가 활성화된 경우 swap 슬롯 캐시를 통해 swap 엔트리를 얻어온다. swap 슬롯 캐시가 부족하면 리필한다.
  • 코드 라인 44에서 swap 슬롯 캐시가 활성화되지 않은 경우에는 직접 swap 엔트리를 얻어온다.
  • 코드 라인 45~49에서 out: 레이블이다. memcg를 통해 메모리 + swap 용량이 메모리 한계를 초과하는 경우 swap 엔트리를 drop 한다.
    • memory.memsw.limit_in_bytes
  • 코드 라인 50에서 swap 엔트리를 반환한다.

 

swap 슬롯 캐시 충전

refill_swap_slots_cache()

mm/swap_slots.c

/* called with swap slot cache's alloc lock held */
static int refill_swap_slots_cache(struct swap_slots_cache *cache)
{
        if (!use_swap_slot_cache || cache->nr)
                return 0;

        cache->cur = 0;
        if (swap_slot_cache_active)
                cache->nr = get_swap_pages(SWAP_SLOTS_CACHE_SIZE,
                                           cache->slots, 1);

        return cache->nr;
}

슬롯 캐시에 swap 영역에서 검색해온 swap 엔트리들을 충전(refill)하고 그 수를 반환한다.

  • 코드 라인 4~5에서 swap 슬롯 캐시를 사용하지 않거나 캐시에 swap 엔트리들이 이미 있는 경우 0을 반환한다.
  • 코드 라인 7~10에서 슬롯 캐시가 활성화된 경우 swap_map을 검색하여 swap 엔트리들을 SWAP_SLOTS_CACHE_SIZE(64) 만큼 가져온다.
  • 코드 라인 12에서 가져온 swap 엔트리 수를 반환한다.

 

swap 슬롯 캐시로 swap 엔트리 회수

free_swap_slot()

mm/swap_slots.c

int free_swap_slot(swp_entry_t entry)
{
        struct swap_slots_cache *cache;

        cache = raw_cpu_ptr(&swp_slots);
        if (likely(use_swap_slot_cache && cache->slots_ret)) {
                spin_lock_irq(&cache->free_lock);
                /* Swap slots cache may be deactivated before acquiring lock */
                if (!use_swap_slot_cache || !cache->slots_ret) {
                        spin_unlock_irq(&cache->free_lock);
                        goto direct_free;
                }
                if (cache->n_ret >= SWAP_SLOTS_CACHE_SIZE) {
                        /*
                         * Return slots to global pool.
                         * The current swap_map value is SWAP_HAS_CACHE.
                         * Set it to 0 to indicate it is available for
                         * allocation in global pool
                         */
                        swapcache_free_entries(cache->slots_ret, cache->n_ret);
                        cache->n_ret = 0;
                }
                cache->slots_ret[cache->n_ret++] = entry;
                spin_unlock_irq(&cache->free_lock);
        } else {
direct_free:
                swapcache_free_entries(&entry, 1);
        }

        return 0;
}

swap 슬롯 캐시를 통해 swap 엔트리 하나를 회수한다. (put_swap_page() 함수에서 호출된다)

  • 코드 라인 5에서 현재 cpu에 대한 swap 슬롯 캐시를 알아온다.
  • 코드 라인 6~12에서 swap 슬롯 캐시를 사용할 수 있는 경우이다. swap 슬롯 캐시의 lock을 획득한 채로 다시 한 번 swap 슬롯 캐시를 사용 가능한지 체크한다. 사용 불가능한 경우 락을 풀고 direct_free 레이블로 이동하여 slot 캐시로 반환하지 않고 직접 swap 영역에 반환한다.
  • 코드 라인 13~22에서 슬롯 캐시가 관리하는 최대 수를 초과하는 경우 기존 swap 슬롯 캐시에 존재하는 swap 엔트리들을 한꺼번에 각 글로벌 swap 영역으로 회수한다.
  • 코드 라인 23~24에서 swap 슬롯 캐시로 swap 엔트리 하나를 회수한다.
  • 코드 라인 25~28에서 direct_free: 레이블이다. swap 슬롯 캐시가 아닌 글로벌 swap 영역으로 직접 swap 엔트리 하나를 회수한다.

 

참고

 

Swap -2- (Swapin & Swapout)

<kernel v5.0>

Swapin & Swapout

유저 프로세스가 swap되어 언매핑된 빈 가상 주소에 접근하는 경우 swap 영역에서 swap 페이지를 로드하여 복구해야 한다. 이 때 swap 캐시를 거쳐 swap 영역에서 swap된 페이지를 로드하는 과정을 swap-in 이라고 하고, 반대로 저장하는 과정을 swap-out이라고 한다.

 

Swap readahead

swap-in 과정에서 swap되어 빈 영역에 접근하는 경우 fault 에러가 발생한다. 이 때 fault된 가상 주소로 페이지 테이블에 저장된 swap 엔트리를 알아온 후 이를 키로 관련 swap 영역에서 swap 페이지를 로드하는데 주변 페이지들을 미리 로드하여 처리 성능을 올릴 수 있다. 이러한 방법을 readahead라고 하는데 swap 과정에 사용되는 readahead는 다음과 같이 두 가지 방법이 사용되고 있다.

  • vma 기반 swap readahead
  • cluster 기반 swap readahead (for SSD)

 

readahead 기능으로 같이 읽혀온 페이지들에 swap 캐시에 있을때 해당 페이지에 PG_reclaim (플래그 사용을 절약하기 위해 readahead 시에는 reclaim이 아니라 PG_readahead 용도로 사용된다.) 플래그가 붙는다.

 

VMA 기반 swap readahead

유저 프로세스가 swap 된 페이지에 접근하여 fault가 발생하면, swap 캐시에서 페이지를 찾아보고, swap 캐시에서 발견하지 못하면 swap 영역으로 부터 vma 영역내의 fault된 페이지 주변을 조금 더 읽어와서 주변 페이지에 접근하여 fault될 때 이미 읽어온 페이지가 swap 캐시 영역에서 찾을 수 있어 이를 빠르게 anon 페이지로 변환 및 매핑할 수 있다.

 

다음 그림은 swap된 페이지가 swap 영역에서 로드될 때 fault 페이지의 vma내 주변 페이지 일부를 미리 swap 캐시에 로드하는 과정을 보여준다.

 

클러스터 기반 swap readahead

VMA 기반과 다르게 fault된 swap 페이지의 주변이 아니라 swap 영역의 주변 페이지를 더 읽어온다.

 

다음 그림은 swap된 페이지가 swap 영역에서 로드될 때 swap 영역의 주변 페이지 일부를 미리 swap 캐시에 로드하는 과정을 보여준다.

 

Swap 관련 페이지 플래그

  • PG_swapbacked
    • swap 영역을 가졌는지 여부이다.
      • 이 플래그를 가지면 swap이 가능한 일반 anon 페이지이다.
      • 이 플래그가 없으면 swap이 불가능한 clean anon 페이지이다.
  • PG_swapcached
    • swap 되어 swap 캐시 영역에 존재하는 상태이다. 유저가 swap된 가상 주소 페이지에 접근 시 fault 핸들러를 통해 swap 영역보다 먼저 swap 캐시를 찾는다.
      • swap 영역에 기록되었는지 여부는 이 플래그로 알 수 없고 page_swapped() 함수를 통해서 알아낼 수 있다.
    • swap-in이 진행될 때 swap 영역에서 읽을 때 성능 향상을 위해 주변 페이지도 읽어  swap 캐시 영역에 로드한다.
    • swap-out이 진행할 때 이 swap 캐시를 swap 영역에 저장한다.
  • PG_writeback
    • swap 영역에 기록(sync 또는 async) 하는 동안 설정된다.
    • pageout() 에서 swap writeback 후크 함수인 swap_writepage() 함수에서 설정되고, writeback이 완료되면 클리어된다.
  • PG_reclaim (2가지 용도)
    • swap-out 시 PageReclaim()으로 사용된다.
      • reclaim을 위해 swap 영역에 기록하는 중에 설정되고, 이 플래그가 제거될 때 회수할 수 있다.
      • pageout() 에서 writeback 직전에 설정되고, writeback이 완료되면 클리어된다.
    • swap-in 시 PageReadahead()으로 사용된다.
      • readahead로 미리 읽어온 swap 캐시 페이지에 설정된다.
  • PG_dirty
    • swap 영역에 기록하기 위해 설정되며, 이 플래그를 보고 pageout()이 호출된다.
    • add_to_swap_cache() 함수에서 설정되고, pageout() 에서 writeback 직전에 클리어된다.
  • PG_workingset
    • 페이지가 작업중임을 알리기 위한 플래그이다.
      • file 페이지가 inactive lru 리스트에서 refault될 때 그 fault 간격을 메모리 크기와 비교하여 메모리 크기보다 작은 fault 간격인 경우 이를 체크하여 thrashing을 감지하는데 사용하기 위해 workingset 플래그를 사용한다.
      • 자주 사용되는 페이지가 여러 번 fualt되어 캐시를 교체하느라 성능 저하되는 현상을 막는 솔루션이다.
      • 참고: mm: workingset: tell cache transitions from workingset thrashing (2018, v4.20-rc1)
    • file 페이지가 처음 access되면 inactive lru 리스트의 선두에서 출발한다. 그리고 두 번의 access를 감지하면 active lru로 승격한다. 그러나 anon 페이지는 처음 access되면 active lru 리스트의 선두에서 출발하므로 처음부터 workingset으로 설정한다.

 

다음 그림은 swap 관련 페이지 플래그의 변화를 보여준다.

 


Swap 초기화

swap_init_sysfs()

mm/swap_state.c

static int __init swap_init_sysfs(void)
{
        int err;
        struct kobject *swap_kobj;

        swap_kobj = kobject_create_and_add("swap", mm_kobj);
        if (!swap_kobj) {
                pr_err("failed to create swap kobject\n");
                return -ENOMEM;
        }
        err = sysfs_create_group(swap_kobj, &swap_attr_group);
        if (err) {
                pr_err("failed to register swap group\n");
                goto delete_obj;
        }
        return 0;

delete_obj:
        kobject_put(swap_kobj);
        return err;
}
subsys_initcall(swap_init_sysfs);

swap 시스템을 위해 /sys/kernel/mm/swap 디렉토리를 생성하고 관련 속성(vma_ra_enabled) 파일을 생성한다.

 

vma_ra_enabled 속성

mm/swap_state.c

static ssize_t vma_ra_enabled_show(struct kobject *kobj,
                                     struct kobj_attribute *attr, char *buf)
{
        return sprintf(buf, "%s\n", enable_vma_readahead ? "true" : "false");
}
static ssize_t vma_ra_enabled_store(struct kobject *kobj,
                                      struct kobj_attribute *attr,
                                      const char *buf, size_t count)
{
        if (!strncmp(buf, "true", 4) || !strncmp(buf, "1", 1))
                enable_vma_readahead = true;
        else if (!strncmp(buf, "false", 5) || !strncmp(buf, "0", 1))
                enable_vma_readahead = false;
        else
                return -EINVAL;

        return count;
}
static struct kobj_attribute vma_ra_enabled_attr =
        __ATTR(vma_ra_enabled, 0644, vma_ra_enabled_show,
               vma_ra_enabled_store);

static struct attribute *swap_attrs[] = {
        &vma_ra_enabled_attr.attr,
        NULL,
};

static struct attribute_group swap_attr_group = {
        .attrs = swap_attrs,
};

vma 기반 readahead 기능을 enable 하는 속성 파일이다.

  • “/sys/kernel/mm/swap/vma_ra_enabled” 속성의 디폴트 값은 true이다.
    • swap_vma_readahead -> vma_ra_enabled 속성으로 이름이 바뀌었다.
  • swap_use_vma_readahead() 함수를 통해 이 속성의 설정 여부를 알아온다.

 


Swap-out

normal anon 페이지가 swap 영역에 기록된 후 페이지가 free되는 순서는 다음과 같다.

  • normal anon 페이지 → swapcache → unmap → write out → free 페이지
    • add_to_swap()
    • try_to_unmap()
    • pageout()
    • free_unref_page_commit()

 

다음 그림은 swap-out 과정을 보여준다.

 

Swap 영역에 추가

add_to_swap()

mm/swap_state.c

/**
 * add_to_swap - allocate swap space for a page
 * @page: page we want to move to swap
 *
 * Allocate swap space for the page and add the page to the
 * swap cache.  Caller needs to hold the page lock.
 */
int add_to_swap(struct page *page)
{
        swp_entry_t entry;
        int err;

        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(!PageUptodate(page), page);

        entry = get_swap_page(page);
        if (!entry.val)
                return 0;

        /*
         * XArray node allocations from PF_MEMALLOC contexts could
         * completely exhaust the page allocator. __GFP_NOMEMALLOC
         * stops emergency reserves from being allocated.
         *
         * TODO: this could cause a theoretical memory reclaim
         * deadlock in the swap out path.
         */
        /*
         * Add it to the swap cache.
         */
        err = add_to_swap_cache(page, entry,
                        __GFP_HIGH|__GFP_NOMEMALLOC|__GFP_NOWARN);
        if (err)
                /*
                 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
                 * clear SWAP_HAS_CACHE flag.
                 */
                goto fail;
        /*
         * Normally the page will be dirtied in unmap because its pte should be
         * dirty. A special case is MADV_FREE page. The page'e pte could have
         * dirty bit cleared but the page's SwapBacked bit is still set because
         * clearing the dirty bit and SwapBacked bit has no lock protected. For
         * such page, unmap will not set dirty bit for it, so page reclaim will
         * not write the page out. This can cause data corruption when the page
         * is swap in later. Always setting the dirty bit for the page solves
         * the problem.
         */
        set_page_dirty(page);

        return 1;

fail:
        put_swap_page(page, entry);
        return 0;
}

swap 엔트리를 할당한 후 이 값을 키로 anon 페이지를 swap 캐시 및 swap 영역에 저장한다. 성공 시 1을 반환한다.

  • 코드 라인 6~7에서 swap-out을 하기 전에 PG_lock과 PG_uptodate가 반드시 설정되어 있어야 한다.
  • 코드 라인 9~11에서 swap할 anon 페이지에 사용할 swap 엔트리를 얻어온다.
  • 코드 라인 24~31에서 swap 엔트리를 키로 swap할 anon 페이지를 swap 캐시에 추가한다.
  • 코드 라인 42~44에서 페이지에 dirty 설정을 한다. 그 후 성공하였으므로 1을 반환한다.
    • address_space에 매핑된 페이지의 경우 드라이버를 통해 dirty 설정을 하고, 페이지에도 PG_dirty 플래그를 설정한다.
    • dirty된 페이지는 reclaim 과정에서 pageout() 함수가 호출되어 swap 영역에 저장되며, 완료된 후에 PG_dirty 플래그가 클리어된다.
  • 코드 라인 46~48에서 fail: 레이블이다. 실패한 경우이므로 0을 반환한다.

 

Swap 캐시에 추가

add_to_swap_cache()

mm/swap_state.c

/*
 * add_to_swap_cache resembles add_to_page_cache_locked on swapper_space,
 * but sets SwapCache flag and private instead of mapping and index.
 */
int add_to_swap_cache(struct page *page, swp_entry_t entry, gfp_t gfp)
{
        struct address_space *address_space = swap_address_space(entry);
        pgoff_t idx = swp_offset(entry);
        XA_STATE_ORDER(xas, &address_space->i_pages, idx, compound_order(page));
        unsigned long i, nr = 1UL << compound_order(page);

        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(PageSwapCache(page), page);
        VM_BUG_ON_PAGE(!PageSwapBacked(page), page);

        page_ref_add(page, nr);
        SetPageSwapCache(page);

        do {
                xas_lock_irq(&xas);
                xas_create_range(&xas);
                if (xas_error(&xas))
                        goto unlock;
                for (i = 0; i < nr; i++) {
                        VM_BUG_ON_PAGE(xas.xa_index != idx + i, page);
                        set_page_private(page + i, entry.val + i);
                        xas_store(&xas, page + i);
                        xas_next(&xas);
                }
                address_space->nrpages += nr;
                __mod_node_page_state(page_pgdat(page), NR_FILE_PAGES, nr);
                ADD_CACHE_INFO(add_total, nr);
unlock:
                xas_unlock_irq(&xas);
        } while (xas_nomem(&xas, gfp));

        if (!xas_error(&xas))
                return 0;

        ClearPageSwapCache(page);
        page_ref_sub(page, nr);
        return xas_error(&xas);
}

swap 엔트리 정보를 키로 anon 페이지를 swap 캐시에 추가한다. 성공 시 0을 반환한다.

  • 코드 라인 3에서 swap 엔트리를 사용하여 swap용 address_space 포인터를 알아온다.
  • 코드 라인 4에서 swap 엔트리로 offset 부분만을 읽어 idx에 대입한다.
  • 코드 라인 5에서 xarray operation state를 선언한다.
    • 작업할 xarray는  &address_space->i_pages이고, 초기 인덱스(idx) 및 엔트리의 order를 지정한다.
    • The XArray data structure (2018) | LWN.net
  • 코드 라인 6에서 nr에 compound 페이지의 수 만큼 대입한다.
    • 일반 페이지는 1이 대입되지만, thp의 경우 compound 구성된 페이지들의 수를 대입한다.
  • 코드 라인 8~10에서 xarray로 관리되는 swap 캐시 영역에 추가할 페이지는 PG_locked, PG_swapbacked 플래그 설정이 반드시 있어야 하고, PG_swapcache 플래그는 없어야 한다.
  • 코드 라인 12~13에서  swap 캐시 영역에 추가할 페이지의 참조 카운터를 nr 만큼 증가시키고, PG_swapcache 플래그를 설정한다.
    • thp swap이 지원되는 경우 head 페이지의 참조 카운터를 nr 만큼 증가시킨다.
  • 코드 라인 15~19에서 idx 부터 nr 페이지 수 범위의 xarray가 생성되도록 미리 준비한다.
  • 코드 라인 20~25에서 페이지 수 만큼 순회하며 p->private에 swap 엔트리를 저장하고,  페이지를 xarray에 저장한다.
  • 코드 라인 26~27에서 nr 페이지 수만큼 다음 카운터들을 증가시킨다.
    • address_space가 관리하는 전체 페이지 수
    • NR_FILE_PAGES 카운터
    • swap_cache_info->add_total 카운터
  • 코드 라인 29~31에서 unlock: 레이블이다. xa_node 할당 실패 시 다시 할당하고 반복한다.
  • 코드 라인 33~34에서 할당이 성공한 경우 0을 반환한다.
  • 코드 라인 36~38에서 할당이 실패한 경우 PG_swapcache를 클리어하고, 참조 카운터를 nr 만큼 다시 내린 후 에러 코드를 반환한다.

 


Swap-in

다음과 같은 순서로 swap된 페이지가 복구된다.

  • 새 페이지 할당 → swapcache로 변경 → swap 영역(파일/파티션)에서 읽기 → 매핑

 

다음 그림은 swap-in 과정을 보여준다.

 

swapin_readahead()

mm/swap_state.c

/**
 * swapin_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vmf: fault information
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * It's a main entry function for swap readahead. By the configuration,
 * it will read ahead blocks by cluster-based(ie, physical disk based)
 * or vma-based(ie, virtual address based on faulty address) readahead.
 */
struct page *swapin_readahead(swp_entry_t entry, gfp_t gfp_mask,
                                struct vm_fault *vmf)
{
        return swap_use_vma_readahead() ?
                        swap_vma_readahead(entry, gfp_mask, vmf) :
                        swap_cluster_readahead(entry, gfp_mask, vmf);
}

swap 엔트리에 대한 swapin을 수행하여 페이지를 읽어들인다.

  • “/sys/kernel/mm/swap/vma_ra_enabled” 속성의 사용 시 vma based readahead를 사용하고, 그렇지 않은 경우 ssd 등에서 사용하는 클러스터 기반의 readahead 방식을 사용한다.

 


VMA 기반 readhead 방식으로 swap-in

vma 내에서 fault된 swap 페이지를 위해 fault된 가상 주소 그 전후로 산출된 readahead 페이지 수 만큼 swap-in을 하는 방식이다. (SSD only)

  • readahead할 페이지들은 vma 경계 또는 pte 테이블 한 개의 범위를 초과할 수 없다.

 

swap_vma_readahead()

mm/swap_state.c

static struct page *swap_vma_readahead(swp_entry_t fentry, gfp_t gfp_mask,
                                       struct vm_fault *vmf)
{
        struct blk_plug plug;
        struct vm_area_struct *vma = vmf->vma;
        struct page *page;
        pte_t *pte, pentry;
        swp_entry_t entry;
        unsigned int i;
        bool page_allocated;
        struct vma_swap_readahead ra_info = {0,};

        swap_ra_info(vmf, &ra_info);
        if (ra_info.win == 1)
                goto skip;

        blk_start_plug(&plug);
        for (i = 0, pte = ra_info.ptes; i < ra_info.nr_pte;
             i++, pte++) {
                pentry = *pte;
                if (pte_none(pentry))
                        continue;
                if (pte_present(pentry))
                        continue;
                entry = pte_to_swp_entry(pentry);
                if (unlikely(non_swap_entry(entry)))
                        continue;
                page = __read_swap_cache_async(entry, gfp_mask, vma,
                                               vmf->address, &page_allocated);
                if (!page)
                        continue;
                if (page_allocated) {
                        swap_readpage(page, false);
                        if (i != ra_info.offset) {
                                SetPageReadahead(page);
                                count_vm_event(SWAP_RA);
                        }
                }
                put_page(page);
        }
        blk_finish_plug(&plug);
        lru_add_drain();
skip:
        return read_swap_cache_async(fentry, gfp_mask, vma, vmf->address,
                                     ra_info.win == 1);
}

swap 엔트리에 대해 vma 기반 swap readahead를 수행한다.

  • 코드 라인 11~15에서 swap용 readahead 정보를 구성한다. 만일 swapin할 페이지가 최소 값 1이면 곧바로 skip 레이블로 이동한다.
  • 코드 라인 17에서 blk_plug를 초기화하고, blk_finish_plug()가 끝나기 전까지 블럭 디바이스에 submit 을 유보하게 한다.
  • 코드 라인 18~27에서 pte 엔트리 수 만큼 순회하며 swap 엔트리 정보가 기록된 pte가 아니면 스킵한다.
  • 코드 라인 28~31에서 swap 캐시 영역에서 페이지를 찾아온다.
  • 코드 라인 32~38에서 새로 할당된 페이지이면 swap 영역으로 부터 비동기로 bio 요청을 하여 페이지를 읽어온다. 그리고 할당한 페이지가 요청한 offset 페이지가 아니면 PG_reclaim(swap-in시 readahead 기능) 플래그를 설정하고, SWAP_RA 카운터를 증가시킨다.
  • 코드 라인 41에서 blk_start_plug() 함수와 짝이되는 함수를 통해 블럭 디바이스의 submit 실행을 지금부터 가능하게 한다.
  • 코드 라인 42에서 per-cpu lru 캐시들을 lru로 되돌린다.
  • 코드 라인 43~45에서 skip: 레이블이다. 다시 한 번 swap 캐시 영역에서 페이지를 찾아온다. 단 한 페이지만(win=1) 처리할 때 swap 캐시에서 싱크 모드로 페이지를 읽어온다.

 


swapin 시 readahed를 위한 페이지 수 산출

swapin_nr_pages()

mm/swap_state.c

static unsigned long swapin_nr_pages(unsigned long offset)
{
        static unsigned long prev_offset;
        unsigned int hits, pages, max_pages;
        static atomic_t last_readahead_pages;

        max_pages = 1 << READ_ONCE(page_cluster);
        if (max_pages <= 1)
                return 1;

        hits = atomic_xchg(&swapin_readahead_hits, 0);
        pages = __swapin_nr_pages(prev_offset, offset, hits, max_pages,
                                  atomic_read(&last_readahead_pages));
        if (!hits)
                prev_offset = offset;
        atomic_set(&last_readahead_pages, pages);

        return pages;
}

@offset 페이지에 대해 swapin 시 readahead할 페이지 수를 산출한다.

  • 코드 라인 3~5에서 최근 readahead 산출 시 사용했던 offset 값이 prev_offset에, 그리고 최근 산출된 readahead 페이지 수가 last_readahead_pages에 저장되어 있다.
  • 코드 라인 7~9에서 최대 페이지 제한으로 1 << page_cluster 값을 지정한다. 만일 그 값이 1이면 추가 산출할 필요 없이 가장 작은 수인 1을 반환한다.
  • 코드 라인 11에서 swap-in시 readahead 히트 페이지 수를 알아온다.
  • 코드 라인 12~13에서 최근 offset(prev_offset), @offset, readahead 히트 페이지(hits), 최대 페이지 수 제한(@max_pages) 및 최근 산출되었었던 readahead 페이지 수(last_readahead_pages) 값을 사용하여 적절한 readahead 페이지 수를 산출한다.
  • 코드 라인 14~15에서 readahead 히트 페이지 수가 0인 경우 offset을 prev_offset에 기억해둔다.
  • 코드 라인 16~18에서 산출한 readahead 페이지 수를 last_readahead_pages에 기억하고 반환한다.

 

__swapin_nr_pages()

mm/swap_state.c

static unsigned int __swapin_nr_pages(unsigned long prev_offset,
                                      unsigned long offset,
                                      int hits,
                                      int max_pages,
                                      int prev_win)
{
        unsigned int pages, last_ra;

        /*
         * This heuristic has been found to work well on both sequential and
         * random loads, swapping to hard disk or to SSD: please don't ask
         * what the "+ 2" means, it just happens to work well, that's all.
         */
        pages = hits + 2;
        if (pages == 2) {
                /*
                 * We can have no readahead hits to judge by: but must not get
                 * stuck here forever, so check for an adjacent offset instead
                 * (and don't even bother to check whether swap type is same).
                 */
                if (offset != prev_offset + 1 && offset != prev_offset - 1)
                        pages = 1;
        } else {
                unsigned int roundup = 4;
                while (roundup < pages)
                        roundup <<= 1;
                pages = roundup;
        }

        if (pages > max_pages)
                pages = max_pages;

        /* Don't shrink readahead too fast */
        last_ra = prev_win / 2;
        if (pages < last_ra)
                pages = last_ra;

        return pages;
}

최근 offset(@prev_offset), @offset, readahead 히트페이지(@hits), 최대 페이지 수 제한(@max_pages) 및 최근 결정된 readahead 페이지 수(@prev_win) 값을 사용하여 적절한 readahead 페이지 수를 산출하여 반환한다.

  • 코드 라인 14~22에서 최근 readahead 히트 페이지가 없는 경우 2개 페이지로 지정한다. 단 offset이 최근 offset과 +- 1 차이를 벗어나면 1개 페이지로 지정한다.
  • 코드 라인 23~28에서 최근 readahead 히트 페이지가 존재하는 경우 4부터 시작하여 두 배씩 증가하는 값(4, 8, 16, …) 중 하나로 결정하는데 증가 값이 (히트 페이지 + 2)를 초과한 수 증 작은 값이어야 한다.
    • 예) hits=10
      • pages = 16
  • 코드 라인 30~31에서 산출한 페이지 수가 최대 페이지 수를 초과하지 않도록 제한한다.
  • 코드 라인 34~36에서 산출한 페이지 수가 급격히 작아지지 않도록, 최근 readahead한 페이지의 절반 이하로 내려가지 않도록 제한한다.
  • 코드 라인 38에서 산출한 readahead 페이지 수를 반환한다.

 


swap 캐시에서 페이지를 읽어오기

read_swap_cache_async()

mm/swap_state.c

/*
 * Locate a page of swap in physical memory, reserving swap cache space
 * and reading the disk if it is not already cached.
 * A failure return means that either the page allocation failed or that
 * the swap entry is no longer in use.
 */
struct page *read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
                struct vm_area_struct *vma, unsigned long addr, bool do_poll)
{
        bool page_was_allocated;
        struct page *retpage = __read_swap_cache_async(entry, gfp_mask,
                        vma, addr, &page_was_allocated);

        if (page_was_allocated)
                swap_readpage(retpage, do_poll);

        return retpage;
}

swap 캐시 영역에서 swap 엔트리에 해당하는 페이지를 읽어온다. swap 캐시에서 찾을 수 없으면 swap 캐시를 할당하여 등록한 후 swap 영역에서 블럭 디바이스를 비동기로 읽어오도록 요청한다. 만일 @do_poll을 true로 요청한 경우 swap 캐시로 읽어올 때까지 기다린다.(sync)

  • 코드 라인 5~6에서 swap 캐시 영역에서 페이지를 찾아온다. 만일 swap 캐시에서 발견할 수 없으면 swap 영역에서 읽어올 때 필요한 새 swap 캐시 페이지를 미리 준비해둔다. 이러한 경우 page_was_allocated 값에 true가 담겨온다.
  • 코드 라인 8~9에서 새 swap 캐시 페이지가 할당된 경우 swap 영역에서 읽어오도록 bio 요청을 한다.
  • 코드 라인 11에서 읽어온 페이지를 반환한다.

 

__read_swap_cache_async()

mm/swap_state.c

struct page *__read_swap_cache_async(swp_entry_t entry, gfp_t gfp_mask,
                        struct vm_area_struct *vma, unsigned long addr,
                        bool *new_page_allocated)
{
        struct page *found_page, *new_page = NULL;
        struct address_space *swapper_space = swap_address_space(entry);
        int err;
        *new_page_allocated = false;

        do {
                /*
                 * First check the swap cache.  Since this is normally
                 * called after lookup_swap_cache() failed, re-calling
                 * that would confuse statistics.
                 */
                found_page = find_get_page(swapper_space, swp_offset(entry));
                if (found_page)
                        break;

                /*
                 * Just skip read ahead for unused swap slot.
                 * During swap_off when swap_slot_cache is disabled,
                 * we have to handle the race between putting
                 * swap entry in swap cache and marking swap slot
                 * as SWAP_HAS_CACHE.  That's done in later part of code or
                 * else swap_off will be aborted if we return NULL.
                 */
                if (!__swp_swapcount(entry) && swap_slot_cache_enabled)
                        break;

                /*
                 * Get a new page to read into from swap.
                 */
                if (!new_page) {
                        new_page = alloc_page_vma(gfp_mask, vma, addr);
                        if (!new_page)
                                break;          /* Out of memory */
                }

                /*
                 * Swap entry may have been freed since our caller observed it.
                 */
                err = swapcache_prepare(entry);
                if (err == -EEXIST) {
                        /*
                         * We might race against get_swap_page() and stumble
                         * across a SWAP_HAS_CACHE swap_map entry whose page
                         * has not been brought into the swapcache yet.
                         */
                        cond_resched();
                        continue;
                } else if (err)         /* swp entry is obsolete ? */
                        break;

                /* May fail (-ENOMEM) if XArray node allocation failed. */
                __SetPageLocked(new_page);
                __SetPageSwapBacked(new_page);
                err = add_to_swap_cache(new_page, entry, gfp_mask & GFP_KERNEL);
                if (likely(!err)) {
                        /* Initiate read into locked page */
                        SetPageWorkingset(new_page);
                        lru_cache_add_anon(new_page);
                        *new_page_allocated = true;
                        return new_page;
                }
                __ClearPageLocked(new_page);
                /*
                 * add_to_swap_cache() doesn't return -EEXIST, so we can safely
                 * clear SWAP_HAS_CACHE flag.
                 */
                put_swap_page(new_page, entry);
        } while (err != -ENOMEM);

        if (new_page)
                put_page(new_page);
        return found_page;
}

swap 캐시 영역에서 페이지를 찾아온다. 만일 swap 캐시에서 발견할 수 없으면 swap 영역에서 읽어오기 위해 새 swap 캐시를 할당하여 준비한 후 반환한다. 새 swap 캐시를 준비한 경우 출력 인자 @new_page_allocated에 true를 저장한다.

  • 코드 라인 16~18에서 swap 캐시 영역에서 swap 엔트리의 offset을 사용하여 swap 캐시 페이지를 찾아온다.
  • 코드 라인 28~29에서 swapoff되어 더 이상 swap 엔트리가 유효하지 않은 경우 null 페이지를 반환하러 루프를 벗어난다.
  • 코드 라인 34~38에서 swap 영역에서 읽어올 페이지 데이터를 저장하기 위해 새 swap 캐시 페이지를 할당한다.
  • 코드 라인 43~65에서 추가할 새 swap 캐시 페이지의 PG_locked, PG_swapbacked 플래그를 먼저 설정하고 swap 캐시에 추가한다. 추가가 완료하면 새 swap 캐시 페이지를 반환한다.
  • 코드 라인 66~72에서 swap 캐시에 추가가 실패하는 경우 새 swap 캐시 페이지의 참조 카운터를 감소시키고 메모리가 부족하지 않는 한 다시 반복한다.
  • 코드 라인 75에서 swap 캐시에서 찾은 페이지를 반환한다.

 

swap 영역에서 읽어 swap 캐시에 저장하기

swap_readpage()

mm/page_io.c

int swap_readpage(struct page *page, bool synchronous)
{
        struct bio *bio;
        int ret = 0;
        struct swap_info_struct *sis = page_swap_info(page);
        blk_qc_t qc;
        struct gendisk *disk;

        VM_BUG_ON_PAGE(!PageSwapCache(page) && !synchronous, page);
        VM_BUG_ON_PAGE(!PageLocked(page), page);
        VM_BUG_ON_PAGE(PageUptodate(page), page);
        if (frontswap_load(page) == 0) {
                SetPageUptodate(page);
                unlock_page(page);
                goto out;
        }

        if (sis->flags & SWP_FS) {
                struct file *swap_file = sis->swap_file;
                struct address_space *mapping = swap_file->f_mapping;

                ret = mapping->a_ops->readpage(swap_file, page);
                if (!ret)
                        count_vm_event(PSWPIN);
                return ret;
        }

        ret = bdev_read_page(sis->bdev, swap_page_sector(page), page);
        if (!ret) {
                if (trylock_page(page)) {
                        swap_slot_free_notify(page);
                        unlock_page(page);
                }

                count_vm_event(PSWPIN);
                return 0;
        }

        ret = 0;
        bio = get_swap_bio(GFP_KERNEL, page, end_swap_bio_read);
        if (bio == NULL) {
                unlock_page(page);
                ret = -ENOMEM;
                goto out;
        }
        disk = bio->bi_disk;
        /*
         * Keep this task valid during swap readpage because the oom killer may
         * attempt to access it in the page fault retry time check.
         */
        get_task_struct(current);
        bio->bi_private = current;
        bio_set_op_attrs(bio, REQ_OP_READ, 0);
        if (synchronous)
                bio->bi_opf |= REQ_HIPRI;
        count_vm_event(PSWPIN);
        bio_get(bio);
        qc = submit_bio(bio);
        while (synchronous) {
                set_current_state(TASK_UNINTERRUPTIBLE);
                if (!READ_ONCE(bio->bi_private))
                        break;

                if (!blk_poll(disk->queue, qc, true))
                        io_schedule();
        }
        __set_current_state(TASK_RUNNING);
        bio_put(bio);

out:
        return ret;
}

swap 영역에서 읽어온 데이터를 swap 캐시 페이지에 저장하도록 bio 요청 한다. 요청 시 @synchronous가 1인 경우 동기 요청한다. 결과는 성공한 경우 0을 반환한다.

  • 코드 라인 5에서 swap 캐시 페이지의 private 멤버에 저장된 swap 엔트리 정보로 swap_info_struct 정보를 알아온다.
  • 코드 라인 9~11에서 페이지에 PG_swapcache, PG_locked 플래그가 반드시 설정되어 있어야 하고, PG_uptodate는 클리어된 상태여야 한다.
  • 코드 라인 12~16에서 front swap을 지원하는 경우 front swap 로드 후 PG_uptodate를 설정하고 성공을 반환한다.
  • 코드 라인 18~26에서 파일 시스템을 통해 사용되는 swap 영역인 경우 해당 드라이버의 (*readpage) 후크 함수를 통해 페이지를 읽어오고 PSWPIN 카운터를 증가시키고 결과를 반환한다.
  • 코드 라인 28~37에서 블럭 디바이스를 통해 사용되는 swap 영역인 경우 블럭 디바이스를 통해 페이지를 읽어오고 PSWPIN을 증가시키고 결과를 반환한다.
  • 코드 라인 40~56에서 bio를 통해 swap 영역을 읽어오도록 요청 준비를 한 PSWPIN 카운터를 증가시킨다.
  • 코드 라인 57~68에서 bio를 통해 요청을 한다. 그런 후 @synchronous가 설정된 경우 완료될 때까지 대기한다.
  • 코드 라인 70~71에서 out: 레이블에서 곧바로 결과를 반환한다.

 


VMA 기반 Swap readahead 정보 구성

 

다음 그림은 vma 기반 swap readahead 과정을 보여준다.

 

vma_swap_readahead 구조체

include/linux/swap.h

struct vma_swap_readahead {
        unsigned short win;
        unsigned short offset;
        unsigned short nr_pte;
#ifdef CONFIG_64BIT
        pte_t *ptes;
#else
        pte_t ptes[SWAP_RA_PTE_CACHE_SIZE];
#endif
};

pmd 사이즈 범위내에서, 즉 1개의 pte 페이지 테이블내에서 pte

  •  win
    • swapin 시 readahead할 산출된 페이지 수
  • offset
    • fault pfn을  기준으로 readahead할 시작 pfn offset
  • nr_pte
    • swapin 시 readahead할 pte 엔트리 수 (vma 및 pmd 단위 경계로 win 값과 다를 수 있다)
  • *ptes
    • fault 페이지 pte + offset에 해당하는 pte 주소

 

swap_ra_info()

mm/swap_state.c

static void swap_ra_info(struct vm_fault *vmf,
                        struct vma_swap_readahead *ra_info)
{
        struct vm_area_struct *vma = vmf->vma;
        unsigned long ra_val;
        swp_entry_t entry;
        unsigned long faddr, pfn, fpfn;
        unsigned long start, end;
        pte_t *pte, *orig_pte;
        unsigned int max_win, hits, prev_win, win, left;
#ifndef CONFIG_64BIT
        pte_t *tpte;
#endif

        max_win = 1 << min_t(unsigned int, READ_ONCE(page_cluster),
                             SWAP_RA_ORDER_CEILING);
        if (max_win == 1) {
                ra_info->win = 1;
                return;
        }

        faddr = vmf->address;
        orig_pte = pte = pte_offset_map(vmf->pmd, faddr);
        entry = pte_to_swp_entry(*pte);
        if ((unlikely(non_swap_entry(entry)))) {
                pte_unmap(orig_pte);
                return;
        }

        fpfn = PFN_DOWN(faddr);
        ra_val = GET_SWAP_RA_VAL(vma);
        pfn = PFN_DOWN(SWAP_RA_ADDR(ra_val));
        prev_win = SWAP_RA_WIN(ra_val);
        hits = SWAP_RA_HITS(ra_val);
        ra_info->win = win = __swapin_nr_pages(pfn, fpfn, hits,
                                               max_win, prev_win);
        atomic_long_set(&vma->swap_readahead_info,
                        SWAP_RA_VAL(faddr, win, 0));

        if (win == 1) {
                pte_unmap(orig_pte);
                return;
        }

        /* Copy the PTEs because the page table may be unmapped */
        if (fpfn == pfn + 1)
                swap_ra_clamp_pfn(vma, faddr, fpfn, fpfn + win, &start, &end);
        else if (pfn == fpfn + 1)
                swap_ra_clamp_pfn(vma, faddr, fpfn - win + 1, fpfn + 1,
                                  &start, &end);
        else {
                left = (win - 1) / 2;
                swap_ra_clamp_pfn(vma, faddr, fpfn - left, fpfn + win - left,
                                  &start, &end);
        }
        ra_info->nr_pte = end - start;
        ra_info->offset = fpfn - start;
        pte -= ra_info->offset;
#ifdef CONFIG_64BIT
        ra_info->ptes = pte;
#else
        tpte = ra_info->ptes;
        for (pfn = start; pfn != end; pfn++)
                *tpte++ = *pte++;
#endif
        pte_unmap(orig_pte);
}

swap용 readahead 정보를 구성한다.

  • 코드 라인 4에서 폴트 핸들러가 전달해준 vmf의 vma를 활용한다.
  • 코드 라인 15~20에서 최대 readahead 페이지 수를 구한 후 max_win에 대입한다. 이 값이 1일 때 추가 산출할 필요 없으므로 ra_info->win에 1을 대입하고 함수를 빠져나간다.
    • 1 << page_cluster와 SWAP_RA_ORDER_CEILING 중 작은 수
      • page_cluster의 초기 값은 2~3(메모리가 16M 이하인 경우 2이고, 그 외의 경우 3)이고, “/proc/sys/vm/page-cluster” 값을 통해 조정된다.
      • SWAP_RA_ORDER_CEILING 값은 64비트 시스템에서 5이고, 32비트 시스템에서 3이다.
      • arm64 디폴트 설정에서 max_win 값은 2^3=8이다.
  • 코드 라인 22~28에서 fault 주소로 페이지 테이블에서 orig_pte 값을 알아오고, 이 값으로 swap 엔트리를 구한다. swap 엔트리가 아닌 경우 pte를 언맵하고 함수를 빠져나간다.
  • 코드 라인 30에서 fault 주소에 해당하는 pfn 값을 구한다.
  • 코드 라인 31~34에서 vma에 지정된 ra_val 값을 알아오고, 이 값에서 pfn, prev_win과 hits 값을 알아온다.
    • ra_val 값에는 세 가지 값이 담겨 있다.
      • PAGE_SHIFT 비트 수를 초과하는 비트들에 pfn  값을 담는다.
      • PAGE_SHIFT 비트 수의 상위 절반에 win 값을 담는다.
      • PAGE_SHIFT 비트 수의 하위 절반에 hits 값을 담는다.
    • 예) ra_val=0b111_101010_100001
      • SWAP_RA_ADDR(ra_val)=0b111_000000_000000
        • PFN_DOWN() 하면 0b111
      • SWAP_RA_WIN(ra_val)=0b101010
      • SWAP_RA_HITS(ra_val)=0b100001
    • 지정되지 않은 경우 prev_win=0, hists=4부터 시작한다.
  • 코드 라인 35~36에서 pfn, 폴트 pfn, hits, max_win, prev_win 값을 사용하여 readahead할 페이지 수를 산출한다.
  • 코드 라인 37~38에서 vma->swap_readahead_info 값에 faddr, win, hits=0 값을 사용하여 ra_val 값을 만들어 저장한다.
  • 코드 라인 40~43에서 만일 win 값이 1인 경우 pte 수와 ptes를 수정할 필요 없으므로 기존 pte 매핑을 언맵하고 함수를 빠져나간다.
  • 코드 라인 46~55에서 다음 세 가지 조건으로 시작과 끝 pfn을 산출한다.
    • 지난번에 사용한 페이지 다음에서 fault가 발생한 경우
    • 지난번에 사용한 페이지 이전에서 fault가 발생한 경우
    • 그 외의 경우
  • 코드 라인 56~65에서 출력 인자인 @ra_info에 pte 정보들을 대입한다.
  • 코드 라인 66에서 오리지널 pte는 매핑 해제한다.

 

다음 그림은 지난 번에 사용했던 페이지와 fault 페이지의 위치에 따라 swap 캐시에 읽어올 readahead 페이지 수를 산출하는 과정을 보여준다.

 

ra_val 값 관련 매크로

mm/swap_state.c

#define SWAP_RA_WIN_SHIFT       (PAGE_SHIFT / 2)
#define SWAP_RA_HITS_MASK       ((1UL << SWAP_RA_WIN_SHIFT) - 1)
#define SWAP_RA_HITS_MAX        SWAP_RA_HITS_MASK
#define SWAP_RA_WIN_MASK        (~PAGE_MASK & ~SWAP_RA_HITS_MASK)

#define SWAP_RA_HITS(v)         ((v) & SWAP_RA_HITS_MASK)
#define SWAP_RA_WIN(v)          (((v) & SWAP_RA_WIN_MASK) >> SWAP_RA_WIN_SHIFT)
#define SWAP_RA_ADDR(v)         ((v) & PAGE_MASK)

#define SWAP_RA_VAL(addr, win, hits)                            \
        (((addr) & PAGE_MASK) |                                 \
         (((win) << SWAP_RA_WIN_SHIFT) & SWAP_RA_WIN_MASK) |    \
         ((hits) & SWAP_RA_HITS_MASK))

 

다음 그림은 swap_ra 값에서 addr, win, hits 값을 가져오는 세 매크로들을 보여준다.

 

다음 그림은 SWAP_RA_VAL() 매크로를 사용하여 addr, win, hits 인자로 swap_ra 값을 만드는 과정을 보여준다.

 

swap_ra_clamp_pfn()

mm/swap_state.c

static inline void swap_ra_clamp_pfn(struct vm_area_struct *vma,
                                     unsigned long faddr,
                                     unsigned long lpfn,
                                     unsigned long rpfn,
                                     unsigned long *start,
                                     unsigned long *end)
{
        *start = max3(lpfn, PFN_DOWN(vma->vm_start),
                      PFN_DOWN(faddr & PMD_MASK));
        *end = min3(rpfn, PFN_DOWN(vma->vm_end),
                    PFN_DOWN((faddr & PMD_MASK) + PMD_SIZE));
}

 

다음 그림은 한 개의 pte 페이지 테이블에서 가져올 pte 엔트리들에 대한 시작 주소와 끝 주소를 알아내는 과정을 보여준다.

 


클러스터 기반 readhead 방식으로 swap-in

swap_cluster_readahead()

mm/swap_state.c

/**
 * swap_cluster_readahead - swap in pages in hope we need them soon
 * @entry: swap entry of this memory
 * @gfp_mask: memory allocation flags
 * @vmf: fault information
 *
 * Returns the struct page for entry and addr, after queueing swapin.
 *
 * Primitive swap readahead code. We simply read an aligned block of
 * (1 << page_cluster) entries in the swap area. This method is chosen
 * because it doesn't cost us any seek time.  We also make sure to queue
 * the 'original' request together with the readahead ones...
 *
 * This has been extended to use the NUMA policies from the mm triggering
 * the readahead.
 *
 * Caller must hold down_read on the vma->vm_mm if vmf->vma is not NULL.
 */
struct page *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
                                struct vm_fault *vmf)
{
        struct page *page;
        unsigned long entry_offset = swp_offset(entry);
        unsigned long offset = entry_offset;
        unsigned long start_offset, end_offset;
        unsigned long mask;
        struct swap_info_struct *si = swp_swap_info(entry);
        struct blk_plug plug;
        bool do_poll = true, page_allocated;
        struct vm_area_struct *vma = vmf->vma;
        unsigned long addr = vmf->address;

        mask = swapin_nr_pages(offset) - 1;
        if (!mask)
                goto skip;

        do_poll = false;
        /* Read a page_cluster sized and aligned cluster around offset. */
        start_offset = offset & ~mask;
        end_offset = offset | mask;
        if (!start_offset)      /* First page is swap header. */
                start_offset++;
        if (end_offset >= si->max)
                end_offset = si->max - 1;

        blk_start_plug(&plug);
        for (offset = start_offset; offset <= end_offset ; offset++) {
                /* Ok, do the async read-ahead now */
                page = __read_swap_cache_async(
                        swp_entry(swp_type(entry), offset),
                        gfp_mask, vma, addr, &page_allocated);
                if (!page)
                        continue;
                if (page_allocated) {
                        swap_readpage(page, false);
                        if (offset != entry_offset) {
                                SetPageReadahead(page);
                                count_vm_event(SWAP_RA);
                        }
                }
                put_page(page);
        }
        blk_finish_plug(&plug);

        lru_add_drain();        /* Push any new pages onto the LRU now */
skip:
        return read_swap_cache_async(entry, gfp_mask, vma, addr, do_poll);
}

swap 엔트리에 대해 클러스터 기반 swap readahead를 수행한다.

  • 코드 라인 9에서 swap 엔트리로 swap 정보를 알아온다.
  • 코드 라인 12에서 폴트 핸들러가 전달해준 vmf의 vma를 활용한다.
  • 코드 라인 15~22에서 fault pnf에 대한 상대 offset pfn이 시작 pfn인데 이 값으로 swapin할 페이지 수 단위로 align한 시작 offset과 끝 offset을 구한다.
    • 예) offset=0x3, mask=0xf
      • start_offset=0x0
      • end_offset=0xf
    • 예) offset=0xffff_ffff_ffff_fffd(-3), mask=0xf
      • start_offset=0xffff_ffff_ffff_fff0(-16)
      • end_offset=0xffff_ffff_ffff_ffff(-1)
  • 코드 라인 23~26에서 시작 offset이 0이면 swap 헤더이므로 그 다음을 사용하도록 증가시키고, 끝 offset이 최대값 미만이 되도록 제한한다.
  • 코드 라인 28에서 blk_plug를 초기화하고, blk_finish_plug()가 끝나기 전까지 블럭 디바이스에 submit 을 유보하게 한다.
  • 코드 라인 29~35에서 pte 엔트리 수 만큼 순회하며 swap 캐시 영역에서 swap 엔트리를 사용하여 페이지를 찾아온다.
  • 코드 라인 36~42에서 새로 할당된 페이지이면 swap 영역으로 부터 비동기로 bio 요청을 하여 페이지를 읽어온다. 그리고 할당한 페이지가 요청한 offset 페이지가 아니면 PG_reclaim(swap-in시 readahead 기능) 플래그를 설정하고, SWAP_RA 카운터를 증가시킨다.
  • 코드 라인 45에서 blk_start_plug() 함수와 짝이되는 함수를 통해 블럭 디바이스의 submit 실행을 지금부터 가능하게 한다.
  • 코드 라인 47에서 per-cpu lru 캐시들을 lru로 되돌린다.
  • 코드 라인 48~49에서 skip: 레이블이다. 다시 한 번 swap 캐시 영역에서 페이지를 async 모드로 찾아온다.

 


Swap 캐시 페이지 찾기

fault 핸들러에서 fault된 pte 엔트리 값에 swap 엔트리가 기록되어 있는 경우 do_swap_page() 함수를 호출하여 swap 캐시 영역에서 찾아 anon 페이지로 매핑해주는데 이 때 swap 캐시 영역을 찾는 함수를 알아본다.

 

lookup_swap_cache()

mm/swap_state.c

/*
 * Lookup a swap entry in the swap cache. A found page will be returned
 * unlocked and with its refcount incremented - we rely on the kernel
 * lock getting page table operations atomic even if we drop the page
 * lock before returning.
 */
struct page *lookup_swap_cache(swp_entry_t entry, struct vm_area_struct *vma,
                               unsigned long addr)
{
        struct page *page;

        page = find_get_page(swap_address_space(entry), swp_offset(entry));

        INC_CACHE_INFO(find_total);
        if (page) {
                bool vma_ra = swap_use_vma_readahead();
                bool readahead;

                INC_CACHE_INFO(find_success);
                /*
                 * At the moment, we don't support PG_readahead for anon THP
                 * so let's bail out rather than confusing the readahead stat.
                 */
                if (unlikely(PageTransCompound(page)))
                        return page;

                readahead = TestClearPageReadahead(page);
                if (vma && vma_ra) {
                        unsigned long ra_val;
                        int win, hits;

                        ra_val = GET_SWAP_RA_VAL(vma);
                        win = SWAP_RA_WIN(ra_val);
                        hits = SWAP_RA_HITS(ra_val);
                        if (readahead)
                                hits = min_t(int, hits + 1, SWAP_RA_HITS_MAX);
                        atomic_long_set(&vma->swap_readahead_info,
                                        SWAP_RA_VAL(addr, win, hits));
                }

                if (readahead) {
                        count_vm_event(SWAP_RA_HIT);
                        if (!vma || !vma_ra)
                                atomic_inc(&swapin_readahead_hits);
                }
        }

        return page;
}

swap 엔트리 값으로 swap 캐시 페이지를 찾아온다.

  • 코드 라인 6에서 swap 엔트리에 매핑된 address_space와 swap 엔트리의 offset 값으로 페이지를 찾아온다.
  • 코드 라인 8에서 swap 캐시 stat의 find_total 카운터를 증가시킨다.
  • 코드 라인 9~10에서 캐시된 페이지를 찾은 경우 vma 기반 readahead가 enable 되었는지 여부를 vam_ra에 대입한다.
  • 코드 라인 13에서 swap 캐시 stat의 find_success 카운터를 증가시킨다.
  • 코드 라인 18~19에서 낮은 확률로 thp 또는 hugetlbfs 페이지인 경우 anon THP에 대한 readahead를 지원하지 않아 그냥 해당 페이지를 반환한다.
  • 코드 라인 21에서 페이지의 PG_reclaim(swap-in시 readahed 플래그로 동작) 플래그 값을 readahead에 대입한 후 클리어한다.
  • 코드 라인 22~33에서 vma 기반 readahead를 사용하는 경우 vma에 저장한 ra_val 값을 갱신한다. 이 때 readahead 플래그가 있었던 페이지인 경우 ra_val내의 hits 값은 증가시킨다.
  • 코드 라인 35~39에서 readahead 플래그가 있었던 페이지인 경우 SWAP_RA_HIT 카운터를 증가시킨다. 만일 vma가 지정되지 않았거나 vma 기반 readahead를 사용하지 않는 경우 swapin_readahead_hits 카운터를 증가시킨다.

 


swap operations

 

address_space_operations 구조체

mm/swap_state.c

/*
 * swapper_space is a fiction, retained to simplify the path through
 * vmscan's shrink_page_list.
 */
static const struct address_space_operations swap_aops = {
        .writepage      = swap_writepage,
        .set_page_dirty = swap_set_page_dirty,
#ifdef CONFIG_MIGRATION
        .migratepage    = migrate_page,
#endif
};

 

swap 영역에 기록

anon 페이지를 swap 캐시에 저장한 후 dirty를 설정하고 빠져나온다음 매핑을 해제한다. 그런 후 pageout()을 통한 swap 캐시를 swap 영역에 기록할 수 있다.

 

swap_writepage()

mm/page_io.c

/*
 * We may have stale swap cache pages in memory: notice
 * them here and get rid of the unnecessary final write.
 */
int swap_writepage(struct page *page, struct writeback_control *wbc)
{
        int ret = 0;

        if (try_to_free_swap(page)) {
                unlock_page(page);
                goto out;
        }
        if (frontswap_store(page) == 0) {
                set_page_writeback(page);
                unlock_page(page);
                end_page_writeback(page);
                goto out;
        }
        ret = __swap_writepage(page, wbc, end_swap_bio_write);
out:
        return ret;
}

dirty 상태의 swap 캐시를 swap 영역에 기록한다.

  • 코드 라인 5~8에서 다음의 경우가 아니면 swap 캐시에서 이 페이지를 제거하고 out: 레이블로 이동한다.
    • 이미 swap 캐시에서 제거되었다. (PG_swapcache 플래그가 없는 상태)
    • writeback이 완료되었다. (PG_writeback이 없는 상태)
    • 이미 swap 영역에 저장한 상태이다. (swap_map[]에 비트가 설정된 상태)
  • 코드 라인 9~14에서 frontswap을 지원하는 경우의 처리이다.
  • 코드 라인 15~17에서 페이지를 swap 영역에 기록 요청하고 결과를 반환한다. sync/async 기록이 완료되면 end_swap_bio_write() 함수를 호출하여 writeback이 완료되었음을 페이지 플래그를 변경한다.

 

swap_set_page_dirty()

mm/page_io.c

int swap_set_page_dirty(struct page *page)
{
        struct swap_info_struct *sis = page_swap_info(page);

        if (sis->flags & SWP_FS) {
                struct address_space *mapping = sis->swap_file->f_mapping;

                VM_BUG_ON_PAGE(!PageSwapCache(page), page);
                return mapping->a_ops->set_page_dirty(page);
        } else {
                return __set_page_dirty_no_writeback(page);
        }
}

swap 페이지에 dirty 표식을 한다. 새롭게 dirty로 변경된 경우 1을 반환한다.

  • 코드 라인 5~9에서 SWP_FS가 설정된 swap 영역인 경우 해당 드라이버가 제공하는 set_page_dirty() 함수를 사용하여 dirty 표식을 한다.
    • SWP_FS는 sunrpc, nfs, xfs, btrfs 등에서 사용된다.
  • 코드 라인 10~12에서 그 외의 일반 swap 영역을 사용하는 경우 swap 캐시 페이지에 dirty 설정을 한다.

 

__set_page_dirty_no_writeback()

mm/page-writeback.c

/*
 * For address_spaces which do not use buffers nor write back.
 */
int __set_page_dirty_no_writeback(struct page *page)
{
        if (!PageDirty(page))
                return !TestSetPageDirty(page);
        return 0;
}

해당 페이지에 dirty 플래그를 설정한다. 새롭게 dirty 설정한 경우 1을 반환한다.

 

참고

 

Swap -1- (Basic, 초기화)

<kernel v5.0>

Swap

유저 프로세스에서 사용한 스택 및 anonymous 메모리 할당(malloc 등) 요청 시 커널은 anon 페이지를 할당하여 관리한다. 메모리 부족 시 swap 영역에 저장할 수 있다.

 

swap 매커니즘은 다음과 같이 3단계 구성을 통해 swap 된다.

  • Swap 캐시(swapcache)
  • Frontswap
  • Swap 영역(Swap Backing Store)

 

다음 그림은 3단계 swap 컴포넌트들을 보여준다.

 

clean anon 페이지

swap 가능한 anon 페이지와 다르게 swap 영역을 가지지 않아 swap 할 수 없는 특별한 anon 페이지가 clean anon 페이지이다. 이러한 clean anon 페이지는 swap 할 필요없다.

  • PG_swapbacked 가 없는 anon 페이지가 clean anon 페이지 상태이다.
  • lazy free 페이지로 매핑을 바로 해제 하지 않기 위해 사용되는데, 이들은 madvise API를 통해 사용된다.

 

FRONTSWAP

Back-End에 존재하는 swap 영역과 달리 Front-End에 만든 swap 시스템이다.

  • 여기에 사용되는 swap 장치는 swap할 페이지에 대해 동기 저장할 수 있는 고속이어야 한다.
    • back-end swap 장치는 보통 비동기 저장한다.
  • tmem(Transcendent Memory)을 사용하며 현재 Xen에서 제공된다.
    • DRAM 메모리 또는 persistent 메모리
  • 참고: Frontswap (2012) | Kernel.org

 

 

Transcendent Memory

부족한 RAM 개선을 위한 새로운 메모리 관리 기술중 하나이다.  리눅스 커널에서 동기적으로 접근할 수 있을정도로 빠른 메모리로 직접 주소 지정되지 않고, 간접 주소 지정 방식으로 호출되며, 영역은 가변 사이즈로 구성되는 영구적 또는 일시적 메모리를 의미하는 메모리 클래스이다. 이 메모리에 저장된 데이터는 경고없이 사라질 수 있다.

 

Cleancache

page 캐시 중 clean page 캐시만을 저장할 수 있는 캐시이다. swap 캐시도 swap 영역에 저장된 경우 이 또한 clean page 캐시이므로 이 cleancache를 사용할 수 있다.

  • 현재 리눅스 커널은 xen에서 제공하는 tmem을 사용한다.

 

 

swap 영역(Swap Backing Store)

swap 영역은 다음과 같이 swap 파일 및 swap 디스크(블럭 디바이스)에 지정하여 사용할 수 있다.

  • swap 파일 
  • swap 디스크

 

다음과 같은 방법으로도 swap 장치를 구성할 수 있다.

  • Swap over NFS, sunrpc 등 네트워크를 통해 마운트된 파일 시스템에 위치한 swap 파일
  • 기타 로컬 RAM의 일부를 사용하여 압축하여 저장하는 zram을 사용하여 IO 요청을 줄인다.
    • 디폴트로 cpu를 사용하여 압축하고, 서버 등에서는 HW를 사용한 압축도 가능하다.

 

다음 그림과 같이 swap 영역을 지정하는 다양한 방법이 있다.

 

Swap Priority

다음 그림과 같이 swap 영역은 여러 개를 지정할 수 있다. priority를 부여할 때 높은 번호 priority가 높은 우선 순위로 먼저 사용되고, priority를 부여하지 않은 경우 디폴트로 swapon을 통해 활성화한 순서대로 사용된다.

  • default priority 값은 -2부터 순서대로 낮아진다.
  • swap 영역 1개의 크기는 아키텍처마다 조금씩 다르다.
    • ARM32
      • 실제 가상 주소 공간(최대 3G)이내에서 사용 가능하나 보통 2G까지 사용한다.
    • ARM64
      • 실제 가상 주소 공간(커널 설정마다 다름)이내에서 사용 가능하다.

 

swap 캐시

anon 페이지가 swap 영역으로 나가고(swap-out) 들어올(swap-in) 때 일시적으로 swap 영역의 내용이 RAM에 존재하는 경우이다. 예를 들어 유저 프로세스가 사용하던 anon 페이지가 메모리 부족으로 인해 swap 영역에 저장이 되었다고 가정한다. 이 때 유저 프로세스가 swap된 페이지에 접근을 시도할 때 fault 가 발생하고, fault 핸들러가 swap 된 페이지임을 확인하면 swap-in을 시도한다. 그런 후 swap 캐시가 발견되면 swap 영역에서 로드를 하지 않고 곧바로 swap 캐시를 찾아 anon 페이지로 변경하여 사용한다.

  • swap 캐시 상태는 PG_swapcache 플래그로 표현된다.

 

swap 캐시별로 사용하는 address_space는 xarray(기존 radix tree)로 관리되며 이에 접근하기 위해 사용되는 rock 경합에 대한 개선을 위해 각각의 address_space의 크기를 64M로 제한하였다.

 

XArray 자료 구조 사용

swap 캐시는 기존 커널에서 radix tree를 사용하여 관리하였지만, 커널 v4.20-rc1부터 XArray 자료 구조로 관리하고 있다.

  • address_space->i_pages가 radix tree를 가리켰었는데 지금은 xarray를 가리킨다.

 

다음 그림은 여러 개의 swap 영역을 사용할 때 64M 단위로 관리되는 swap 캐시의 운용되는 모습을 보여준다.

 

Swap 슬롯 캐시

swap 영역의 swap_map[]을 통해 swap 엔트리를 할당하는데, 이 때마다 swap 영역의 락이 필요하다. swap 영역을 빈번하게 접근할 때 이의 성능이 저하되는 것을 개선하기 위해 swap 엔트리 할당을 위해 앞단에 per-cpu swap 슬롯 캐시를 사용하여 swap 영역의 잠금 없이 빠르게 할당/해제할 수 있게 하였다.

 

다음 그림은 anon  페이지를 swap 영역에 저장할 때 swap 엔트리의 빠른 할당 및 회수에  사용되는 각각 64개로 구성된 swap 슬롯 캐시를 보여준다.

 

Swap 엔트리

swap 엔트리는 swap 페이지를 검색할 수 있는 최소한의 정보를 담고 있다.

  • VMA 영역내의 anonymous 페이지가 swap이 되면 swap 엔트리 정보를 구성하여 해당 pte 엔트리에 기록해둔다.
  • ARM에서 swap 엔트리의 구성은 다음과 같다.
    • offset
      • swap 페이지 offset
    • type
      • swap 영역 구분
    • 하위 두 비트=0b00
      • ARM 및 ARM64 아키텍처에서 언매핑 상태로, 이 페이지에 접근하는 경우 fault 에러가 발생한다.
  • 참고: Swap 엔트리 | 문c

 

다음 그림은 유저 프로세스의 가상 주소 공간에 swap 상태의 페이지와 사용 중인 상태의 anonymous 페이지들의 관리 상태를 swap 공정 관점으로 보여준다.

  • 1개의 swap 영역(type=0)의 첫 번째 헤더 페이지를 제외한 그 다음 페이지(offset=1)를 swap 엔트리로 할당한 경우
    • ARM64용 swap 엔트리 값 0x100이 페이지 테이블에 매핑되고, swap 캐시의 xarray에는 엔트리 값을 키로 페이지가 저장된다.

 

Swap 영역 관리

swap 영역은 swap_info_struct의 멤버인 swap_map이 바이트 단위로 관리되어 사용된다. 이 때 각 바이트 값은 1 페이지의 swap 여부에 대응된다. 이 값에는 페이지의 참조 카운터가 기록된다. 이 swap_map은 다음과 같이 두 가지 관리 방식이 사용된다.

  • Legacy swap 맵 관리
    • Legacy HDDswap 파일에서 사용되는 일반적인 방식이다.  swap 페이지의 할당/해제 관리에 사용되는 swap_map을 SMP 시스템에서 사용하기 위해서 swap을 시도하는 cpu가 swap 영역(swap_info_struct)에 대한 spin-lock을 잡고 swap_map의 선두부터  빈 자리를 검색하여 사용하는 방식이다. 이 방식은  swap할 페이지마다 spin-lock을 획득하고 사용하므로 lock contention이 매우 큰 단점이 있다.
  • per-cpu 클러스터 단위 swap 맵 관리

 

다음 그림과 같이 swap 페이지의 swap 여부는 swap_map[] 배열에 각 swap 페이지가 1바이트 정보로 기록되어 관리된다.

 

다음 그림은 per-cpu 클러스터 단위로 swap_map을 관리하는 모습을 보여준다.

 

swap 유틸리티

mkswap

swap 영역을 파일 또는 파티션에 할당하여 생성한다.

 

$ mkswap -h

Usage:
 mkswap [options] device [size]

Set up a Linux swap area.

Options:
 -c, --check               check bad blocks before creating the swap area
 -f, --force               allow swap size area be larger than device
 -p, --pagesize SIZE       specify page size in bytes
 -L, --label LABEL         specify label
 -v, --swapversion NUM     specify swap-space version number
 -U, --uuid UUID           specify the uuid to use
 -V, --version             output version information and exit
 -h, --help                display this help and exit

 

예) 10M(10240K) 사이즈의 /abc 파일명을 생성한 후 swap 파일로 지정한다.

  • $ fallocate –length 10485760 /abc
    • 또는 dd if=/dev/zero of=/abc bs=1M count=10
  • $ mkswap /abc

 

예) hdc3 파티션을 swap 영역으로 생성한다.

  • $ mkswap /dev/hdc3

 

swapon

swap 영역을 파일 또는 파티션에 지정하여 enable 한다.

 

$ swapon -h

Usage:
 swapon [options] [<spec>]

Enable devices and files for paging and swapping.

Options:
 -a, --all                enable all swaps from /etc/fstab
 -d, --discard[=<policy>] enable swap discards, if supported by device
 -e, --ifexists           silently skip devices that do not exist
 -f, --fixpgsz            reinitialize the swap space if necessary
 -o, --options <list>     comma-separated list of swap options
 -p, --priority <prio>    specify the priority of the swap device
 -s, --summary            display summary about used swap devices (DEPRECATED)
     --show[=<columns>]   display summary in definable table
     --noheadings         don't print table heading (with --show)
     --raw                use the raw output format (with --show)
     --bytes              display swap size in bytes in --show output
 -v, --verbose            verbose mode

 -h, --help     display this help and exit
 -V, --version  output version information and exit

The <spec> parameter:
 -L <label>             synonym for LABEL=<label>
 -U <uuid>              synonym for UUID=<uuid>
 LABEL=<label>          specifies device by swap area label
 UUID=<uuid>            specifies device by swap area UUID
 PARTLABEL=<label>      specifies device by partition label
 PARTUUID=<uuid>        specifies device by partition UUID
 <device>               name of device to be used
 <file>                 name of file to be used

Available discard policy types (for --discard):
 once    : only single-time area discards are issued
 pages   : freed pages are discarded before they are reused
If no policy is selected, both discard types are enabled (default).

Available columns (for --show):
 NAME   device file or partition path
 TYPE   type of the device
 SIZE   size of the swap area
 USED   bytes in use
 PRIO   swap priority
 UUID   swap uuid
 LABEL  swap label

For more details see swapon(8).

 

예) /abc 파일명의 swap 파일에서 swap을 활성화한다.

  • $ swapon /abc

 

예) hdc3 파티션의 swap을 활성화한다.

  • $ swapon /dev/hdc3

 

swapoff

swap 영역으로 지정된 파일 또는 파티션의 swap 기능을 disable 한다.

 

$ swapoff -h

Usage:
 swapoff [options] [<spec>]

Disable devices and files for paging and swapping.

Options:
 -a, --all              disable all swaps from /proc/swaps
 -v, --verbose          verbose mode

 -h, --help     display this help and exit
 -V, --version  output version information and exit

The <spec> parameter:
 -L <label>             LABEL of device to be used
 -U <uuid>              UUID of device to be used
 LABEL=<label>          LABEL of device to be used
 UUID=<uuid>            UUID of device to be used
 <device>               name of device to be used
 <file>                 name of file to be used

For more details see swapoff(8).

 

예) /abc 파일명의 swap 파일에서 swap을 비활성화한다.

  • $ swapoff /abc

 

예) hdc3 파티션의 swap을 비활성화한다.

  • $ swapoff /dev/hdc3

 

Swap Extent

swap 영역의 페이지 범위를 swap 장치의 블럭에 매핑할 때 사용하는 자료 구조이다. swap 영역이 블럭 디바이스에 직접 사용된 경우에는 swap 영역의 각 페이지가 블럭 디바이스의 각 블럭에 연속적으로 1:1로 동일하게 사용되므로 하나의 매핑만 필요하다. 그러나 swap 영역으로 swap 파일을 이용하는 경우에는 swap 파일의 각 페이지와 블럭 디바이스의 실제 사용된 블럭이 동일하게 연속하지 않으므로 여러 개의 매핑이 필요하다. 그렇다고 하나의 페이지와 하나의 블럭을 각각 매핑하면 이러한 매핑으로 너무 많이 소모된다. 따라서 범위로 묶어서 매핑을 하는데 이 때 사용하는 구조체가 swap_extent이다.

  • 하나의 swap_extent에는 시작 페이지 번호와 페이지 수 그리고 매핑할 시작 블럭 번호가 포함된다.
  • 리스트로 관리하던 swap extent를 커널 v5.3-rc1에서 rbtree로 변경하였다.

 

다음 그림은 swap 파일이 위치한 블럭 디바이스에 3개의 연속된 블럭들로 나뉘어 배치된 경우에 필요한 3개의 swap_extent를 보여준다.

 


Swapon

swap 영역을 파일 또는 블럭 디바이스에 지정하여 활성화한다.

 

sys_swapon()

mm/swapfile.c -1/3-

SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
{
        struct swap_info_struct *p;
        struct filename *name;
        struct file *swap_file = NULL;
        struct address_space *mapping;
        int prio;
        int error;
        union swap_header *swap_header;
        int nr_extents;
        sector_t span;
        unsigned long maxpages;
        unsigned char *swap_map = NULL;
        struct swap_cluster_info *cluster_info = NULL;
        unsigned long *frontswap_map = NULL;
        struct page *page = NULL;
        struct inode *inode = NULL;
        bool inced_nr_rotate_swap = false;

        if (swap_flags & ~SWAP_FLAGS_VALID)
                return -EINVAL;

        if (!capable(CAP_SYS_ADMIN))
                return -EPERM;

        if (!swap_avail_heads)
                return -ENOMEM;

        p = alloc_swap_info();
        if (IS_ERR(p))
                return PTR_ERR(p);

        INIT_WORK(&p->discard_work, swap_discard_work);

        name = getname(specialfile);
        if (IS_ERR(name)) {
                error = PTR_ERR(name);
                name = NULL;
                goto bad_swap;
        }
        swap_file = file_open_name(name, O_RDWR|O_LARGEFILE, 0);
        if (IS_ERR(swap_file)) {
                error = PTR_ERR(swap_file);
                swap_file = NULL;
                goto bad_swap;
        }

        p->swap_file = swap_file;
        mapping = swap_file->f_mapping;
        inode = mapping->host;

        /* If S_ISREG(inode->i_mode) will do inode_lock(inode); */
        error = claim_swapfile(p, inode);
        if (unlikely(error))
                goto bad_swap;

        /*
         * Read the swap header.
         */
        if (!mapping->a_ops->readpage) {
                error = -EINVAL;
                goto bad_swap;
        }
        page = read_mapping_page(mapping, 0, swap_file);
        if (IS_ERR(page)) {
                error = PTR_ERR(page);
                goto bad_swap;
        }
        swap_header = kmap(page);

        maxpages = read_swap_header(p, swap_header, inode);
        if (unlikely(!maxpages)) {
                error = -EINVAL;
                goto bad_swap;
        }

        /* OK, set up the swap map and apply the bad block list */
        swap_map = vzalloc(maxpages);
        if (!swap_map) {
                error = -ENOMEM;
                goto bad_swap;
        }        

swap용 블럭 디바이스 또는 파일을 @type 번호의 swap 영역에 활성화한다. 성공 시 0을 반환한다.

  • 코드 라인 20~21에서 잘못된 swap 플래그가 지정된 경우 -EINVAL 에러를 반환한다. 허용되는 플래그들은 다음과 같다.
    • SWAP_FLAG_PREFER (0x8000)
    • SWAP_FLAG_PRIO_MASK (0x7fff)
    • SWAP_FLAG_DISCARD (0x10000)
    • SWAP_FLAG_DISCARD_ONCE (0x20000)
    • SWAP_FLAG_DISCARD_PAGES (0x40000
  • 코드 라인 23~24에서 CAP_SYS_ADMIN 권한이 없는 경우 -EPERM 에러를 반환한다.
  • 코드 라인 26~27에서 swap_avail_heads 리스트가 초기화되지 않은 경우 -ENOMEM 에러를 반환한다.
  • 코드 라인 29~31에서 swap  영역을 구성하기 위해 swap_info_struct를 할당하고 초기화한다.
  • 코드 라인 33에서 워커 스레드에서 swap_discard_work 함수를 호출할 수 있도록 워크를 초기화한다.
    • SSD를 사용하는 디스크에서 discard 기능을 사용할 수 있다.
  • 코드 라인 35~48에서 swapon할 디바이스 또는 파일을 오픈한 후 swap 정보에 지정한다.
  • 코드 라인 49~55에서 swap파일의 address_space와 inode 정보로 다음과 같이 수행한다.
    • 블럭 디바이스인 경우 swap 정보의 멤버 bdev에 inode를 포함한 블럭 디바이스를 지정한다. 그리고 멤버 flag에 SWP_BLKDEV 플래그를 추가한다.
    • 파일인 경우 swap 정보의 멤버 bdev에 inode->i_sb->s_bdev를 지정한다. 그리고 이 파일이 이미 swapfile로 사용중이면 -EBUSY 에러를 반환한다.
  • 코드 라인 60~63에서 swap 파일 시스템의 (*readpage) 후크가 지정되지 않은 경우 -INVAL 에러를 반환한다.
  • 코드 라인 64~68에서 swap 파일의 헤더를 읽기 위해 인덱스 0에 대한  대한 페이지 캐시를 읽어온다.
  • 코드 라인 69에서 읽어온 페이지를 swap_header로 사용하기 위해 kmap을 사용하여 잠시 매핑해둔다.
    • arm64에서는 highmem을 사용하지 않기 때문에 이미 매핑되어 있다.
  • 코드 라인 71~75에서 swap 헤더를 파싱하여 swap 정보에 그 시작과 끝 위치를 기록하고, 실제 swap 영역에 해당하는 페이지 수를 알아온다.
  • 코드 라인 78~82에서 실제 swap 영역에 해당하는 페이지 수에 해당하는 바이트를 할당하여 swap_map에 할당한다.

 

mm/swapfile.c -2/3-

.       if (bdi_cap_stable_pages_required(inode_to_bdi(inode)))
                p->flags |= SWP_STABLE_WRITES;

        if (bdi_cap_synchronous_io(inode_to_bdi(inode)))
                p->flags |= SWP_SYNCHRONOUS_IO;

        if (p->bdev && blk_queue_nonrot(bdev_get_queue(p->bdev))) {
                int cpu;
                unsigned long ci, nr_cluster;

                p->flags |= SWP_SOLIDSTATE;
                /*
                 * select a random position to start with to help wear leveling
                 * SSD
                 */
                p->cluster_next = 1 + (prandom_u32() % p->highest_bit);
                nr_cluster = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);

                cluster_info = kvcalloc(nr_cluster, sizeof(*cluster_info),
                                        GFP_KERNEL);
                if (!cluster_info) {
                        error = -ENOMEM;
                        goto bad_swap;
                }

                for (ci = 0; ci < nr_cluster; ci++)
                        spin_lock_init(&((cluster_info + ci)->lock));

                p->percpu_cluster = alloc_percpu(struct percpu_cluster);
                if (!p->percpu_cluster) {
                        error = -ENOMEM;
                        goto bad_swap;
                }
                for_each_possible_cpu(cpu) {
                        struct percpu_cluster *cluster;
                        cluster = per_cpu_ptr(p->percpu_cluster, cpu);
                        cluster_set_null(&cluster->index);
                }
        } else {
                atomic_inc(&nr_rotate_swap);
                inced_nr_rotate_swap = true;
        }

        error = swap_cgroup_swapon(p->type, maxpages);
        if (error)
                goto bad_swap;

        nr_extents = setup_swap_map_and_extents(p, swap_header, swap_map,
                cluster_info, maxpages, &span);
        if (unlikely(nr_extents < 0)) {
                error = nr_extents;
                goto bad_swap;
        }
        /* frontswap enabled? set up bit-per-page map for frontswap */
        if (IS_ENABLED(CONFIG_FRONTSWAP))
                frontswap_map = kvcalloc(BITS_TO_LONGS(maxpages),
                                         sizeof(long),
                                         GFP_KERNEL);

        if (p->bdev &&(swap_flags & SWAP_FLAG_DISCARD) && swap_discardable(p)) {
                /*
                 * When discard is enabled for swap with no particular
                 * policy flagged, we set all swap discard flags here in
                 * order to sustain backward compatibility with older
                 * swapon(8) releases.
                 */
                p->flags |= (SWP_DISCARDABLE | SWP_AREA_DISCARD |
                             SWP_PAGE_DISCARD);

                /*
                 * By flagging sys_swapon, a sysadmin can tell us to
                 * either do single-time area discards only, or to just
                 * perform discards for released swap page-clusters.
                 * Now it's time to adjust the p->flags accordingly.
                 */
                if (swap_flags & SWAP_FLAG_DISCARD_ONCE)
                        p->flags &= ~SWP_PAGE_DISCARD;
                else if (swap_flags & SWAP_FLAG_DISCARD_PAGES)
                        p->flags &= ~SWP_AREA_DISCARD;

                /* issue a swapon-time discard if it's still required */
                if (p->flags & SWP_AREA_DISCARD) {
                        int err = discard_swap(p);
                        if (unlikely(err))
                                pr_err("swapon: discard_swap(%p): %d\n",
                                        p, err);
                }
        }
  • 코드 라인 1~2에서 swap 기록을 안정적으로 할 수 있는 장치인 경우 SWP_STABLE_WRITES 플래그를 추가한다.
  • 코드 라인 4~5에서 swap 기록이 빠른 장치(zram, pmem 등)인 경우 비동기로 처리할 필요 없다. 이 때 SWP_SYNCHRONOUS_IO 플래그를 추가한다.
  • 코드 라인 7~11에서 SSD 처럼 non-rotational 블럭 장치인 경우 SWP_SOLIDSTATE 플래그를 추가한다.
  • 코드 라인 16에서 다음 사용할 클러스터 위치를 swap 영역내에서 랜덤하게 선택한다.
  • 코드 라인 17에서 swap 가용 페이지 수를 사용하여 클러스터의 수를 결정한다.
    • 1개의 클러스터는 SWAPFILE_CLUSTER(256) 수 만큼 페이지를 관리한다.
    • 현재 x86_64 아키텍처만 THP_SWAP을 지원하고 이 때 256 페이지 대신 HPAGE_PMD_NR 수 만큼 페이지를 관리한다.
  • 코드 라인 19~27에서 결정된 클러스터 수만큼 cluster_info를  할당하고 초기화한다.
  • 코드 라인 29~38에서 swap 정보의 멤버 percpu_cluster에 per-cpu percpu_cluster 구조체를 할당하여 지정하고 초기화한다.
  • 코드 라인 39~42에서 SSD가 아닌 장치인 경우 nr_rotate_swap을 증가시키고 inced_nr_rotate_swap을 true로 지정한다.
    • nr_rotate_swap 값이 0이 아니면 vma 기반 readahead를 사용하지 않는다.
  • 코드 라인 44~46에서 cgroup용 swapon을 위해 swap_cgroup 배열들을 할당하고 준비한다.
  • 코드 라인 48~53에서 swap 맵과 swap_extent를 할당하고 준비한다.
  • 코드 라인 55~58에서 frontswap을 지원하는 커널인 경우 frontswap용 맵을 할당한다.
    • 맵의 각 비트는 1 페이지에 대응한다.
  • 코드 라인 60~88에서 SWAP_FLAG_DISCARD 요청을 처리한다.

 

mm/swapfile.c -3/3-

        error = init_swap_address_space(p->type, maxpages);
        if (error)
                goto bad_swap;

        mutex_lock(&swapon_mutex);
        prio = -1;
        if (swap_flags & SWAP_FLAG_PREFER)
                prio =
                  (swap_flags & SWAP_FLAG_PRIO_MASK) >> SWAP_FLAG_PRIO_SHIFT;
        enable_swap_info(p, prio, swap_map, cluster_info, frontswap_map);

        pr_info("Adding %uk swap on %s.  Priority:%d extents:%d across:%lluk %s%s%s%s%s\n",
                p->pages<<(PAGE_SHIFT-10), name->name, p->prio,
                nr_extents, (unsigned long long)span<<(PAGE_SHIFT-10),
                (p->flags & SWP_SOLIDSTATE) ? "SS" : "",
                (p->flags & SWP_DISCARDABLE) ? "D" : "",
                (p->flags & SWP_AREA_DISCARD) ? "s" : "",
                (p->flags & SWP_PAGE_DISCARD) ? "c" : "",
                (frontswap_map) ? "FS" : "");

        mutex_unlock(&swapon_mutex);
        atomic_inc(&proc_poll_event);
        wake_up_interruptible(&proc_poll_wait);

        if (S_ISREG(inode->i_mode))
                inode->i_flags |= S_SWAPFILE;
        error = 0;
        goto out;
bad_swap:
        free_percpu(p->percpu_cluster);
        p->percpu_cluster = NULL;
        if (inode && S_ISBLK(inode->i_mode) && p->bdev) {
                set_blocksize(p->bdev, p->old_block_size);
                blkdev_put(p->bdev, FMODE_READ | FMODE_WRITE | FMODE_EXCL);
        }
        destroy_swap_extents(p);
        swap_cgroup_swapoff(p->type);
        spin_lock(&swap_lock);
        p->swap_file = NULL;
        p->flags = 0;
        spin_unlock(&swap_lock);
        vfree(swap_map);
        kvfree(cluster_info);
        kvfree(frontswap_map);
        if (inced_nr_rotate_swap)
                atomic_dec(&nr_rotate_swap);
        if (swap_file) {
                if (inode && S_ISREG(inode->i_mode)) {
                        inode_unlock(inode);
                        inode = NULL;
                }
                filp_close(swap_file, NULL);
        }
out:
        if (page && !IS_ERR(page)) {
                kunmap(page);
                put_page(page);
        }
        if (name)
                putname(name);
        if (inode && S_ISREG(inode->i_mode))
                inode_unlock(inode);
        if (!error)
                enable_swap_slots_cache();
        return error;
}
  • 코드 라인 1~3에서 @type에 대한 swap 영역을 초기화한다.
    • swapper_spaces[type]에 swap 영역 크기를 SWAP_ADDRESS_SPACE_PAGES(2^14=16K pages=64M) 단위 수로 나누어 address_space 배열을 할당하여 준비한다.
  • 코드라인 6~9에서 SWAP_FLAG_PREFER 플래그가 요청된 경우 플래그에 priority 값이 추가되어 있다. 이 경우 priority 값만 분리하여 prio에 대입한다. 그 외의 경우 -1이다.
  • 코드 라인 10~19에서 swap 영역을 활성화하고, 메시지를 춮력한다.
    • “Adding <페이지수> swap on <파일/블럭 디바이스명>. Priority:<prio> extents:<extent 매핑수> across:<span 크기> [SS][D][s][c][FS]”
      • SS: SSD
      • D: discardable
      • s: swap 영역 discard
      • c: swap 페이지 discard
      • FS: FrontSwap 맵
  • 코드 라인 25~26에서 swap 파일을 사용하는 swap 영역인 경우 inode에 S_SWAPFILE 플래그를 추가한다.
  • 코드 라인 27~28에서 에러 없이 성공적으로 처리하려 out 레이블로 이동한다.
  • 코드 라인 29~53에서 bad_swap: 레이블이다. swap 영역을 활성화하지 못하는 경우 할당했었던 메모리를 회수한다.
  • 코드 라인 54~65에서 out: 레이블이다. swap 영역의 초기화가 성공한 경우 swap 슬롯 캐시를 활성화한다.

 

swap_info_struct 할당후 초기화

alloc_swap_info()

mm/swapfile.c

static struct swap_info_struct *alloc_swap_info(void)
{
        struct swap_info_struct *p;
        unsigned int type;
        int i;
        int size = sizeof(*p) + nr_node_ids * sizeof(struct plist_node);

        p = kvzalloc(size, GFP_KERNEL);
        if (!p)
                return ERR_PTR(-ENOMEM);

        spin_lock(&swap_lock);
        for (type = 0; type < nr_swapfiles; type++) {
                if (!(swap_info[type]->flags & SWP_USED))
                        break;
        }
        if (type >= MAX_SWAPFILES) {
                spin_unlock(&swap_lock);
                kvfree(p);
                return ERR_PTR(-EPERM);
        }
        if (type >= nr_swapfiles) {
                p->type = type;
                swap_info[type] = p;
                /*
                 * Write swap_info[type] before nr_swapfiles, in case a
                 * racing procfs swap_start() or swap_next() is reading them.
                 * (We never shrink nr_swapfiles, we never free this entry.)
                 */
                smp_wmb();
                nr_swapfiles++;
        } else {
                kvfree(p);
                p = swap_info[type];
                /*
                 * Do not memset this entry: a racing procfs swap_next()
                 * would be relying on p->type to remain valid.
                 */
        }
        INIT_LIST_HEAD(&p->first_swap_extent.list);
        plist_node_init(&p->list, 0);
        for_each_node(i)
                plist_node_init(&p->avail_lists[i], 0);
        p->flags = SWP_USED;
        spin_unlock(&swap_lock);
        spin_lock_init(&p->lock);
        spin_lock_init(&p->cont_lock);

        return p;
}

swap 영역 정보를 할당한다. (할당한 swap_info_struct 포인터를 반환한다.)

  • 코드 라인 6~10에서 swap_info_struct 구조체와 연결하여 노드 수 만큼의 plist_node 구조체 배열을 할당한다.
  • 코드 라인 13~16에서 swap 파일 수만큼 swap_info[] 배열에 빈 자리가 있는지 찾아본다.
  • 코드 라인 17~21에서 생성한 swap 파일 수가 이미 MAX_SWAPFILES 수 이상인 경우 할당을 취소하고 -EPERM 에러를 반환한다.
  • 코드 라인 22~31에서 swap_info[] 배열에 빈 자리가 없으면 마지막에 할당한 메모리를 지정하고, swap 파일 수를 증가시킨다.
  • 코드 라인 32~39에서 swap_info[] 배열에 빈 자리가 있으면 이미 할당한 메모리는 취소하고, 기존 할당한 메모리를 사용한다.
  • 코드 라인 40~47에서 할당한 swap_info_struct 배열의 관련 멤버들을 초기화하고, SWP_USED 플래그를 설정한다.

 


Swap 헤더

swap_header 구조체

include/linux/swap.h

/*
 * Magic header for a swap area. The first part of the union is
 * what the swap magic looks like for the old (limited to 128MB)
 * swap area format, the second part of the union adds - in the
 * old reserved area - some extra information. Note that the first
 * kilobyte is reserved for boot loader or disk label stuff...
 *
 * Having the magic at the end of the PAGE_SIZE makes detecting swap
 * areas somewhat tricky on machines that support multiple page sizes.
 * For 2.5 we'll probably want to move the magic to just beyond the
 * bootbits...
 */
union swap_header {
        struct {
                char reserved[PAGE_SIZE - 10];
                char magic[10];                 /* SWAP-SPACE or SWAPSPACE2 */
        } magic;
        struct {
                char            bootbits[1024]; /* Space for disklabel etc. */
                __u32           version;
                __u32           last_page;
                __u32           nr_badpages;
                unsigned char   sws_uuid[16];
                unsigned char   sws_volume[16];
                __u32           padding[117];
                __u32           badpages[1];
        } info;
};

 

다음 그림은 swap 파일의 헤더 구성을 보여준다.

read_swap_header()

mm/swapfile.c

static unsigned long read_swap_header(struct swap_info_struct *p,
                                        union swap_header *swap_header,
                                        struct inode *inode)
{
        int i;
        unsigned long maxpages;
        unsigned long swapfilepages;
        unsigned long last_page;

        if (memcmp("SWAPSPACE2", swap_header->magic.magic, 10)) {
                pr_err("Unable to find swap-space signature\n");
                return 0;
        }

        /* swap partition endianess hack... */
        if (swab32(swap_header->info.version) == 1) {
                swab32s(&swap_header->info.version);
                swab32s(&swap_header->info.last_page);
                swab32s(&swap_header->info.nr_badpages);
                if (swap_header->info.nr_badpages > MAX_SWAP_BADPAGES)
                        return 0;
                for (i = 0; i < swap_header->info.nr_badpages; i++)
                        swab32s(&swap_header->info.badpages[i]);
        }
        /* Check the swap header's sub-version */
        if (swap_header->info.version != 1) {
                pr_warn("Unable to handle swap header version %d\n",
                        swap_header->info.version);
                return 0;
        }

        p->lowest_bit  = 1;
        p->cluster_next = 1;
        p->cluster_nr = 0;

        maxpages = max_swapfile_size();
        last_page = swap_header->info.last_page;
        if (!last_page) {
                pr_warn("Empty swap-file\n");
                return 0;
        }
        if (last_page > maxpages) {
                pr_warn("Truncating oversized swap area, only using %luk out of %luk\n",
                        maxpages << (PAGE_SHIFT - 10),
                        last_page << (PAGE_SHIFT - 10));
        }
        if (maxpages > last_page) {
                maxpages = last_page + 1;
                /* p->max is an unsigned int: don't overflow it */
                if ((unsigned int)maxpages == 0)
                        maxpages = UINT_MAX;
        }
        p->highest_bit = maxpages - 1;

        if (!maxpages)
                return 0;
        swapfilepages = i_size_read(inode) >> PAGE_SHIFT;
        if (swapfilepages && maxpages > swapfilepages) {
                pr_warn("Swap area shorter than signature indicates\n");
                return 0;
        }
        if (swap_header->info.nr_badpages && S_ISREG(inode->i_mode))
                return 0;
        if (swap_header->info.nr_badpages > MAX_SWAP_BADPAGES)
                return 0;

        return maxpages;
}

swap 헤더를 파싱하여 swap 정보에 그 시작과 끝 위치를 알아온다.

  • 코드 라인 10~13에서 페이지의 마지막 10바이트에 “SWAPSPACE2” 라는 매직 문자열을 확인하고, 없는 경우 에러 메시지를 출력하고 0을 반환한다.
    • “Unable to find swap-space signature\n”
  • 코드 라인 16~30에서 버전이 1로 확인되는 경우 모든 unsigned int 값들을 바이트 swap 하여 읽는다. 만일 버전이 1이 아닌 경우 다음과 같은 경고 메시지를 출력하고 0을 반환한다.
    • “Unable to handle swap header version %d\n”
  • 코드 라인 32~34에서 클러스터 수를 0으로하고, 다음 클러스터 번호는 1로 지정하여 초기화한다. 그리고 swap 시작(lowestbit) 페이지로 첫 페이지인 1을 지정한다.
  • 코드 라인 36에서 아키텍처가 지원하는 swap offset 페이지 한계를 알아와서 maxpages에 대입한다.
  • 코드 라인 37~41에서 swap 헤더에 기록된 last_page 수가 0인 경우 “Empty swap-file” 메시지를 출력하고 0을 반환한다.
  • 코드 라인 42~46에서 last_page가 maxpages를 초과하는 경우 경고 메시지를 출력한다.
  • 코드 라인 47~52에서 maxpages가 last_page보다 큰 경우 maxpages는 last_page+1을 대입한다.
  • 코드 라인 53에서 swap 끝(highestbit) 페이지로 maxpages-1을 지정한다.
  • 코드 라인 57~61에서 swap 파일 또는 블럭 디바이스의 페이지 수를 알아와서 maxpages보다 작은 경우 다음과 같은 경고 메시지를 출력하고 0을 반환한다.
    • “Swap area shorter than signature indicates\n”
  • 코드 라인 62~63에서 swap 파일의 경우 배드 페이지가 존재하는 경우 0을 반환한다.
  • 코드 라인 64~65에서 배드 페이지 수가 MAX_SWAP_BADPAGES를 초과하는 경우 0을 반환한다.
  • 코드 라인 67에서 maxpages를 반환한다.

 

다음 그림은 swap 파일 또는 블럭 디바이스에서 swap 헤더를 읽고 swap 정보에 시작(lowest_bit)과 끝(highest_bit) 위치를 처음 지정하는 모습을 보여준다.

 


Cgroup용 swap

swap_cgroup_swapon()

mm/swap_cgroup.c

int swap_cgroup_swapon(int type, unsigned long max_pages)
{
        void *array;
        unsigned long array_size;
        unsigned long length;
        struct swap_cgroup_ctrl *ctrl;

        if (!do_swap_account)
                return 0;

        length = DIV_ROUND_UP(max_pages, SC_PER_PAGE);
        array_size = length * sizeof(void *);

        array = vzalloc(array_size);
        if (!array)
                goto nomem;

        ctrl = &swap_cgroup_ctrl[type];
        mutex_lock(&swap_cgroup_mutex);
        ctrl->length = length;
        ctrl->map = array;
        spin_lock_init(&ctrl->lock);
        if (swap_cgroup_prepare(type)) {
                /* memory shortage */
                ctrl->map = NULL;
                ctrl->length = 0;
                mutex_unlock(&swap_cgroup_mutex);
                vfree(array);
                goto nomem;
        }
        mutex_unlock(&swap_cgroup_mutex);

        return 0;
nomem:
        pr_info("couldn't allocate enough memory for swap_cgroup\n");
        pr_info("swap_cgroup can be disabled by swapaccount=0 boot option\n");
        return -ENOMEM;
}

swap cgroup을 할당하고 준비한다. 성공 시 0을 반환한다.

  • 코드 라인 8~9에서 memcg swap을 지원하는 커널이 아니면 함수를 빠져나간다.
    • CONFIG_MEMCG_SWAP & CONFIG_MEMCG_SWAP_ENABLED 커널 옵션을 사용해야 한다.
  • 코드 라인 11~16에서 swap 영역에 필요한 페이지 수만큼 필요한 swap_cgroup 구조체를 수를 산출하고 그 수 만큼 페이지 포인터 배열을 할당한다.
  • 코드 라인 18~22에서 전역 swap_cgroup_ctrl[] 배열에서 @type에 해당하는 swap_cgroup_ctrl에 할당한 페이지 포인터 배열을 연결하고, length에 산출한 swap_cgroup 구조체 수를 담는다.
  • 코드 라인 23~30에서 swap_cgroup_ctrl에 할당하여 준비한 페이지 포인터 배열에 swap_cgroup 구조체 배열용으로 사용할 페이지들을 할당하고 연결한다.
  • 코드 라인 33에서 정상 할당이 완료되면 0을 반환한다.
  • 코드 라인 34~37에서 nomem: 레이블이다. swap cgroup을 할당하기에 메모리가 부족하다고 메시지 출력을 한다. 그리고 메모리 부족 시 커널 옵션으로 “swapaccount=0″을 사용하면 swap cgroup을 disable 할 수 있다고 메시지 출력을 한다.

 

다음 그림은 swap cgroup을 할당하고 준비하는 과정을 보여준다.

 

swap_cgroup_prepare()

mm/swap_cgroup.c

/*
 * allocate buffer for swap_cgroup.
 */
static int swap_cgroup_prepare(int type)
{
        struct page *page;
        struct swap_cgroup_ctrl *ctrl;
        unsigned long idx, max;

        ctrl = &swap_cgroup_ctrl[type];

        for (idx = 0; idx < ctrl->length; idx++) {
                page = alloc_page(GFP_KERNEL | __GFP_ZERO);
                if (!page)
                        goto not_enough_page;
                ctrl->map[idx] = page;

                if (!(idx % SWAP_CLUSTER_MAX))
                        cond_resched();
        }
        return 0;
not_enough_page:
        max = idx;
        for (idx = 0; idx < max; idx++)
                __free_page(ctrl->map[idx]);

        return -ENOMEM;
}

swap_cgroup_ctrl에 할당하여 준비한 각 페이지 포인터 배열에 swap_cgroup 구조체 배열용으로 사용할 페이지들을 할당하고 연결한다.

  • 코드 라인 7에서 @type에 해당하는 swap_cgroup_ctrl을 지정한다.
  • 코드 라인 9~17에서 ctrl->length 만큼 순회하며 swap_cgroup 구조체 배열용 페이지를 할당하여 ctrl->map[idx]에 연결한다.
  • 코드 라인 18에서 성공시 0을 반환한다.
  • 코드 라인 19~24에서 not_enough_page: 레이블이다. 메모리 부족 시 할당한 페이지들을 할당 해제하고 -ENOMEM 에러를 반환한다.

 


swap 맵 초기화

setup_swap_map_and_extents()

mm/swapfile.c

static int setup_swap_map_and_extents(struct swap_info_struct *p,
                                        union swap_header *swap_header,
                                        unsigned char *swap_map,
                                        struct swap_cluster_info *cluster_info,
                                        unsigned long maxpages,
                                        sector_t *span)
{
        unsigned int j, k;
        unsigned int nr_good_pages;
        int nr_extents;
        unsigned long nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
        unsigned long col = p->cluster_next / SWAPFILE_CLUSTER % SWAP_CLUSTER_COLS;
        unsigned long i, idx;

        nr_good_pages = maxpages - 1;   /* omit header page */

        cluster_list_init(&p->free_clusters);
        cluster_list_init(&p->discard_clusters);

        for (i = 0; i < swap_header->info.nr_badpages; i++) {
                unsigned int page_nr = swap_header->info.badpages[i];
                if (page_nr == 0 || page_nr > swap_header->info.last_page)
                        return -EINVAL;
                if (page_nr < maxpages) {
                        swap_map[page_nr] = SWAP_MAP_BAD;
                        nr_good_pages--;
                        /*
                         * Haven't marked the cluster free yet, no list
                         * operation involved
                         */
                        inc_cluster_info_page(p, cluster_info, page_nr);
                }
        }

        /* Haven't marked the cluster free yet, no list operation involved */
        for (i = maxpages; i < round_up(maxpages, SWAPFILE_CLUSTER); i++)
                inc_cluster_info_page(p, cluster_info, i);

        if (nr_good_pages) {
                swap_map[0] = SWAP_MAP_BAD;
                /*
                 * Not mark the cluster free yet, no list
                 * operation involved
                 */
                inc_cluster_info_page(p, cluster_info, 0);
                p->max = maxpages;
                p->pages = nr_good_pages;
                nr_extents = setup_swap_extents(p, span);
                if (nr_extents < 0)
                        return nr_extents;
                nr_good_pages = p->pages;
        }
        if (!nr_good_pages) {
                pr_warn("Empty swap-file\n");
                return -EINVAL;
        }

        if (!cluster_info)
                return nr_extents;

        /*
         * Reduce false cache line sharing between cluster_info and
         * sharing same address space.
         */
        for (k = 0; k < SWAP_CLUSTER_COLS; k++) {
                j = (k + col) % SWAP_CLUSTER_COLS;
                for (i = 0; i < DIV_ROUND_UP(nr_clusters, SWAP_CLUSTER_COLS); i++) {
                        idx = i * SWAP_CLUSTER_COLS + j;
                        if (idx >= nr_clusters)
                                continue;
                        if (cluster_count(&cluster_info[idx]))
                                continue;
                        cluster_set_flag(&cluster_info[idx], CLUSTER_FLAG_FREE);
                        cluster_list_add_tail(&p->free_clusters, cluster_info,
                                              idx);
                }
        }
        return nr_extents;
}

swap 맵과 swap_extent를 할당하고 준비하며 할당한 swap_extent 수를 반환한다.

  • 코드 라인 11에서 swap 최대 페이지 수로 필요한 클러스터 수를 산출한다.
  • 코드 라인 12에서 다음에 진행할 클러스터 번호를 알아온다.
    • swap 파일은 최대 64M 단위로 최대 클러스터 번호는
  • 코드 라인 15에서 good 페이지 수를 산출할 때 배드 페이지들을 빼기 전에 먼저 헤더 페이지로 1 페이지를 사용하므로 swap 최대 페이지 수에서 1을 뺀다.
  • 코드 라인 17~18에서 free_clusters와 discard_clusters 리스트들을 클리어한다.
  • 코드 라인 20~33에서 헤더 페이지에 기록된 배드 페이지 수만큼 순회하며 배드 페이지 번호를 알아와서 그에 해당하는 swap_map[]에 SWAP_MAP_BAD 마킹을 하고, good 페이지 수를 감소시킨다. 마킹한 배드 페이지가 있는 클러스터도 사용중으로 설정한다.
  • 코드 라인 36~37에서 swap 영역을 클러스터 단위로 관리하는데 끝 부분이 정렬되지 않고 남는 영역의 페이지들도 모두 사용중인 클러스터로 설정한다.
  • 코드 라인 39~52에서 good 페이지가 존재하는 경우 swap 영역의 첫 페이지는 SWAP_MAP_BAD로 설정하고, 첫 클러스터를 사용중으로 설정한다. 그리고 swap extent를 구성한다.
  • 코드 라인 53~56에서 good 페이지가 하나도 없으면 빈 swap 파일이라고 경고 메시지를 출력하고 -EINVAL 에러를 반환한다.
  • 코드 라인 58~59에서 @cluster_info가 지정되지 않은 경우 swap extent 수를 반환한다.
  • 코드 라인 65~77에서 false cache line sharing을 줄이기 위해 cluster_info와 address_space를 각 cpu들이 따로 접근하도록 떨어뜨렸다. 사용 가능한 free 클러스터들을 fre_clusters 리스트에 추가하고, cluster_info 정보의 플래그에 CLUSTER_FLAG_FREE를 추가한다.
  • 코드 라인 78에서 swap extent 수를 반환한다.

 

다음 그림은 swap 영역의 헤더 페이지를 분석한 정보로 swap_map을 구성하는 모습을 보여준다.

 

다음 그림은 SSD를 사용 시 클러스터 구성을 위해 free 클러스터 리스트를 준비하는 과정을 보여준다.

  • 헤더 페이지, BAD 페이지가 속한 클러스터는 사용 중으로 만들어 free 클러스터 리스트에서 제거한다.
  • free 클러스터를 추가할 때 클러스터를 순서대로 넣지 않고 64개씩 분리하여 추가하는 것을 보여준다.

 

 


Swap Extents

swap 영역을 블럭 디바이스에 범위를 매핑할 때 사용한다. swap 영역의 종류에 따라 다음 3가지 방법으로 swap extent를 준비한다.

  • 블럭 디바이스를 사용하는 경우 swap 영역과 블럭 디바이스는 한 번에 전부 1개의 swap extent를 사용하여 매핑한다.
  • 마운트된 파일 시스템에서 (*swap_activate)가 지원되는 swap 파일이 위치한 swap 영역도 한 번에 전부를 매핑하므로  1개의 swap extent만 필요하다.
    • nfs, xfs, btrfs, sunrpc, …
  • generic swap 파일
    • swap 영역으로 swap 파일을 사용하는 경우이다. 이 때에는 블럭 디바이스의 빈 공간이 여러 군데에 fragment된 경우 이므로 여러 개의 swap_extent가 필요하게 된다.

 

setup_swap_extents()

mm/swapfile.c

/*
 * A `swap extent' is a simple thing which maps a contiguous range of pages
 * onto a contiguous range of disk blocks.  An ordered list of swap extents
 * is built at swapon time and is then used at swap_writepage/swap_readpage
 * time for locating where on disk a page belongs.
 *
 * If the swapfile is an S_ISBLK block device, a single extent is installed.
 * This is done so that the main operating code can treat S_ISBLK and S_ISREG
 * swap files identically.
 *
 * Whether the swapdev is an S_ISREG file or an S_ISBLK blockdev, the swap
 * extent list operates in PAGE_SIZE disk blocks.  Both S_ISREG and S_ISBLK
 * swapfiles are handled *identically* after swapon time.
 *
 * For S_ISREG swapfiles, setup_swap_extents() will walk all the file's blocks
 * and will parse them into an ordered extent list, in PAGE_SIZE chunks.  If
 * some stray blocks are found which do not fall within the PAGE_SIZE alignment
 * requirements, they are simply tossed out - we will never use those blocks
 * for swapping.
 *
 * For S_ISREG swapfiles we set S_SWAPFILE across the life of the swapon.  This
 * prevents root from shooting her foot off by ftruncating an in-use swapfile,
 * which will scribble on the fs.
 *
 * The amount of disk space which a single swap extent represents varies.
 * Typically it is in the 1-4 megabyte range.  So we can have hundreds of
 * extents in the list.  To avoid much list walking, we cache the previous
 * search location in `curr_swap_extent', and start new searches from there.
 * This is extremely effective.  The average number of iterations in
 * map_swap_page() has been measured at about 0.3 per page.  - akpm.
 */
static int setup_swap_extents(struct swap_info_struct *sis, sector_t *span)
{
        struct file *swap_file = sis->swap_file;
        struct address_space *mapping = swap_file->f_mapping;
        struct inode *inode = mapping->host;
        int ret;

        if (S_ISBLK(inode->i_mode)) {
                ret = add_swap_extent(sis, 0, sis->max, 0);
                *span = sis->pages;
                return ret;
        }

        if (mapping->a_ops->swap_activate) {
                ret = mapping->a_ops->swap_activate(sis, swap_file, span);
                if (ret >= 0)
                        sis->flags |= SWP_ACTIVATED;
                if (!ret) {
                        sis->flags |= SWP_FS;
                        ret = add_swap_extent(sis, 0, sis->max, 0);
                        *span = sis->pages;
                }
                return ret;
        }

        return generic_swapfile_activate(sis, swap_file, span);
}

swap extents를 준비한다. 출력 인자 @span에 페이지 수를 지정한다. 결과가 0인 경우 새로 활성화된 경우이다.

  • 코드 라인 8~12에서 swap 영역이 블럭 디바이스인 경우 swap 영역 전체(0 ~ sis->max 페이지)를 지정한 블럭 디바이스에 한 번에 매핑할 수 있다. 그렇게 하기 위해 0번 블럭부터 전체를 매핑하도록 1개의 swap_extent를 추가한다.
  • 코드 라인 14~24에서 매핑된 오퍼레이션의 (*swap_activate) 후크가 지원되는 경우 호출한 후 이미 활성화된 경우 SWP_ACTIVATED 플래그를 추가한다. 또한 새로 활성화된 경우 SWP_FS 플래그를 설정하고, swap 영역 전체(0 ~ sis->max 페이지)를 한 번에 매핑하도록 1개의 swap_extent를 구성하여 추가한다.
  • 코드 라인 26에서 swap 영역이 generic한 swap 파일인 경우 1개 이상의 매핑을 위해 swap_extent 들을 추가하고 활성화한다.

 

generic_swapfile_activate()

mm/page_io.c

int generic_swapfile_activate(struct swap_info_struct *sis,
                                struct file *swap_file,
                                sector_t *span)
{
        struct address_space *mapping = swap_file->f_mapping;
        struct inode *inode = mapping->host;
        unsigned blocks_per_page;
        unsigned long page_no;
        unsigned blkbits;
        sector_t probe_block;
        sector_t last_block;
        sector_t lowest_block = -1;
        sector_t highest_block = 0;
        int nr_extents = 0;
        int ret;

        blkbits = inode->i_blkbits;
        blocks_per_page = PAGE_SIZE >> blkbits;

        /*
         * Map all the blocks into the extent list.  This code doesn't try
         * to be very smart.
         */
        probe_block = 0;
        page_no = 0;
        last_block = i_size_read(inode) >> blkbits;
        while ((probe_block + blocks_per_page) <= last_block &&
                        page_no < sis->max) {
                unsigned block_in_page;
                sector_t first_block;

                cond_resched();

                first_block = bmap(inode, probe_block);
                if (first_block == 0)
                        goto bad_bmap;

                /*
                 * It must be PAGE_SIZE aligned on-disk
                 */
                if (first_block & (blocks_per_page - 1)) {
                        probe_block++;
                        goto reprobe;
                }

                for (block_in_page = 1; block_in_page < blocks_per_page;
                                        block_in_page++) {
                        sector_t block;

                        block = bmap(inode, probe_block + block_in_page);
                        if (block == 0)
                                goto bad_bmap;
                        if (block != first_block + block_in_page) {
                                /* Discontiguity */
                                probe_block++;
                                goto reprobe;
                        }
                }

                first_block >>= (PAGE_SHIFT - blkbits);
                if (page_no) {  /* exclude the header page */
                        if (first_block < lowest_block)
                                lowest_block = first_block;
                        if (first_block > highest_block)
                                highest_block = first_block;
                }

                /*
                 * We found a PAGE_SIZE-length, PAGE_SIZE-aligned run of blocks
                 */
                ret = add_swap_extent(sis, page_no, 1, first_block);
                if (ret < 0)
                        goto out;
                nr_extents += ret;
                page_no++;
                probe_block += blocks_per_page;
reprobe:
                continue;
        }
        ret = nr_extents;
        *span = 1 + highest_block - lowest_block;
        if (page_no == 0)
                page_no = 1;    /* force Empty message */
        sis->max = page_no;
        sis->pages = page_no - 1;
        sis->highest_bit = page_no - 1;
out:
        return ret;
bad_bmap:
        pr_err("swapon: swapfile has holes\n");
        ret = -EINVAL;
        goto out;
}

swap 영역이 generic한 swap 파일에서 이를 활성화한다. 성공 시 추가한 extent 수를 반환한다.

  • 코드 라인 17~18에서 한 개의 페이지에 들어갈 수 있는 블럭 비트 수를 구해 blocks_per_page에 대입한다.
    • 블럭 크기는 512byte 이다. 그러나 여기서 말하는 블럭은 IO 단위로 처리 가능한 가상 블럭을 의미한다. swap 파일이 ext2, ext3, ext4 파일시스템에서 운영되는 경우 가상 블럭 사이즈는 1K, 2K, 4K, 8K를 지원하고, 디폴트로 4K를 사용한다.
      • 예) PAGE_SIZE(4096) >> inode->iblkbits(12) = 1 블럭
  • 코드 라인 24~26에서 파일의 시작 페이지(page_no)를 0부터 끝 페이지(sis->max) 까지 순회를 위해 준비한다. 이 때 probe_block도 0부터 시작하고, last_block에는 파일의 끝 블럭 번호를 대입한다.
  • 코드 라인 27~28에서 probe_block 부터 blocks_per_page 단위로 증가하며 last_block까지 순회한다.
    • swap 파일이 ext2, ext3, ext4 파일시스템에서 운영되는 경우 블럭 사이즈로 디폴트 설정을 사용하면 1페이지가 1블럭과 동일하다. 따라서 blocks_per_page의 경우 1이다.
  • 코드 라인 34~36에서 swap 파일의 probe_block 페이지에 대한 디스크 블럭 번호를 알아와서 first_block에 대입한다.
  • 코드 라인 41~44에서 알아온 디스크 블럭 번호(first_block)가 blocks_per_page 단위로 정렬되지 않은 경우 정렬될 때까지 swap 파일에 대한 블럭 번호(probe_block)를 증가시키고 reprobe 레이블로 이동한다.
  • 코드 라인 46~58에서 페이지 내에 2개 이상의 블럭이 있는 경우 블럭내 두 번째 페이지부터 블럭내 끝 페이지까지 순회한다. probe_block + 순회중인 페이지 순번을 더한 번호로 이에 해당하는 블럭 디바이스의 블럭 번호와 동일하게 연동되는지 확인한다. 만일 일치하지 않으면 reprobe 레이블로 이동한다.
  • 코드 라인 60~66에서 헤더 페이지를 제외하고 알아온 블럭 디바이스의 번호(first_block)로 가장 작은 lowest_block과 가장 큰 highest_block을 갱신한다.
  • 코드 라인 71~74에서 swap 파일의 page_no에 해당하는 1개 페이지를 알아온 블럭 디바이스 번호(first_block)에 매핑한다. 매핑 시 실제 swap extent가 추가되면 1이 반환된다. 추가되지 않고 기존 swap extent에 merge되면 0이 반환된다. 이렇게 반환된 수를 nr_extents에 합산한다.
  • 코드 라인 75~76에서 다음 페이지를 처리하러 계속 진행한다. probe_block은 페이지 단위로 정렬되어야 하므로 blocks_per_page 만큼 증가시킨다.
  • 코드 라인 77~79에서 reprobe: 레이블이다. while 루프를 계속 진행한다.
  • 코드 라인 80에서 반환할 값으로 추가한 extent 수를 대입한다.
  • 코드 라인 81에서 출력 인자 @span에는 가장 작은 블록부터 가장 큰 블록까지의 수를 대입한다.
  • 코드 라인 82~83에서 page_no가 한 번도 증가되지 않은 경우 빈 페이지인 경우이다. page_no에 1을 대입한다.
  • 코드 라인 84~86에서 max, pages, highest_bit등을 갱신한다.
  • 코드 라인 87~88에서 out: 레이블이다. 추가한 extent 수를 반환한다.
  • 코드 라인 89~92에서 bad_bmap: 레이블이다. 다음 에러 메시지를 출력하고 -EINVAL 에러를 반환한다.
    • “swapon: swapfile has holes\n”

 

swap_extent 구조체

include/linux/swap.h

/*
 * A swap extent maps a range of a swapfile's PAGE_SIZE pages onto a range of
 * disk blocks.  A list of swap extents maps the entire swapfile.  (Where the
 * term `swapfile' refers to either a blockdevice or an IS_REG file.  Apart
 * from setup, they're handled identically.
 *
 * We always assume that blocks are of size PAGE_SIZE.
 */
struct swap_extent {
        struct list_head list;
        pgoff_t start_page;
        pgoff_t nr_pages;
        sector_t start_block;
};

swap 영역을 블럭 디바이스에 범위를 매핑할 때 사용한다.

  •  list
    • 다음과 같이 두 가지 사용 방법으로 나뉜다.
      • swap_info_struct에 내장된 swap_extent의 list인 경우 리스트 헤드로 사용된다.
      • 헤드에 추가되는 swap_extent의 list는 추가할 때 사용하는 노드로 사용된다.
  • start_page
    • swap 영역에서 매핑할 시작 페이지이다.
  • nr_pages
    • swap 영역에서 위의 start_page부터 연속 매핑할 페이지 수이다.
  • start_block
    • swap 영역의 start_page 부터 nr_pages 만큼 블럭 디바이스의 start_block 부터 매핑된다.

 

add_swap_extent()

mm/swapfile.c

/*
 * Add a block range (and the corresponding page range) into this swapdev's
 * extent list.  The extent list is kept sorted in page order.
 *
 * This function rather assumes that it is called in ascending page order.
 */
int
add_swap_extent(struct swap_info_struct *sis, unsigned long start_page,
                unsigned long nr_pages, sector_t start_block)
{
        struct swap_extent *se;
        struct swap_extent *new_se;
        struct list_head *lh;

        if (start_page == 0) {
                se = &sis->first_swap_extent;
                sis->curr_swap_extent = se;
                se->start_page = 0;
                se->nr_pages = nr_pages;
                se->start_block = start_block;
                return 1;
        } else {
                lh = sis->first_swap_extent.list.prev;  /* Highest extent */
                se = list_entry(lh, struct swap_extent, list);
                BUG_ON(se->start_page + se->nr_pages != start_page);
                if (se->start_block + se->nr_pages == start_block) {
                        /* Merge it */
                        se->nr_pages += nr_pages;
                        return 0;
                }
        }

        /*
         * No merge.  Insert a new extent, preserving ordering.
         */
        new_se = kmalloc(sizeof(*se), GFP_KERNEL);
        if (new_se == NULL)
                return -ENOMEM;
        new_se->start_page = start_page;
        new_se->nr_pages = nr_pages;
        new_se->start_block = start_block;

        list_add_tail(&new_se->list, &sis->first_swap_extent.list);
        return 1;
}
EXPORT_SYMBOL_GPL(add_swap_extent);

swap 영역의 @start_page부터 @nr_pages를 블럭 디바이스의 @start_block에 매핑한다. 만일 새 swap extent가 할당된 경우 1을 반환한다.

  • 코드 라인 9~15에서 swap 영역을 처음 매핑하러 시도할 때 0번 페이지부터 시작하는데 이 때 @nr_pages 만큼 @start_block에 매핑한다. 매핑할 때 사용되는 swap_extent는 swap_info_struct 구조체 내부에 기본으로 사용되는 first_swap_extent를 사용한다. 내장된 swap extent를 사용했어도 추가되었다는 의미로 1을 반환한다.
    • 블럭 디바이스를 swap 영역으로 사용 시 swap_extent는 하나만 사용되므로, 한 번의 매핑을 위해 이 조건 한 번만 호출된다.
  • 코드 라인 16~25에서 연속된 블럭을 사용할 수 있는 경우 기존 매핑을 merge하여 사용되는 케이스이다. merge 되었으므로 swap extent가 추가되지 않아 0을 반환한다.
  • 코드 라인 30~38에서 방금 전에 매핑한 블럭 디바이스 번호가 연속되지 않는 경우 새로운 swap extent를 할당하고 새로운 매핑 정보를 기록한다. 그리고 sis->first_swap_extent.list에 추가한 후 1을 반환한다.

 

다음 그림은 swap 파일이 마운트된 블럭 디바이스에 흩어지지(fragment) 않고 연속되어 사용되는 경우 swap extent를 1개만 사용하는 모습을 보여준다.

 

다음 그림은 swap 파일이 마운트된 블럭 디바이스에 3번 흩어져서(fragment) 사용되는 경우 swap extent를 3개 사용한 모습을 보여준다.

 

destroy_swap_extents()

mm/swapfile.c

/*
 * Free all of a swapdev's extent information
 */
static void destroy_swap_extents(struct swap_info_struct *sis)
{
        while (!list_empty(&sis->first_swap_extent.list)) {
                struct swap_extent *se;

                se = list_first_entry(&sis->first_swap_extent.list,
                                struct swap_extent, list);
                list_del(&se->list);
                kfree(se);
        }

        if (sis->flags & SWP_ACTIVATED) {
                struct file *swap_file = sis->swap_file;
                struct address_space *mapping = swap_file->f_mapping;

                sis->flags &= ~SWP_ACTIVATED;
                if (mapping->a_ops->swap_deactivate)
                        mapping->a_ops->swap_deactivate(swap_file);
        }
}

모든 swap extent들을 리스트에서 제거하고 할당 해제한다.

 


swap 엔트리용 address_space 관리

swap용 address_space 생성과 소멸

address_space 구조체는 swapon/swapoff 명령에 의해 생성과 소멸된다.

  • swapon -> sys_swapon() -> init_swap_address_space()
  • swapoff -> sys_swapoff() -> exit_swap_address_space()

 

init_swap_address_space()

mm/swap_state.c

int init_swap_address_space(unsigned int type, unsigned long nr_pages)
{
        struct address_space *spaces, *space;
        unsigned int i, nr;

        nr = DIV_ROUND_UP(nr_pages, SWAP_ADDRESS_SPACE_PAGES);
        spaces = kvcalloc(nr, sizeof(struct address_space), GFP_KERNEL);
        if (!spaces)
                return -ENOMEM;
        for (i = 0; i < nr; i++) {
                space = spaces + i;
                xa_init_flags(&space->i_pages, XA_FLAGS_LOCK_IRQ);
                atomic_set(&space->i_mmap_writable, 0);
                space->a_ops = &swap_aops;
                /* swap cache doesn't use writeback related tags */
                mapping_set_no_writeback_tags(space);
        }
        nr_swapper_spaces[type] = nr;
        rcu_assign_pointer(swapper_spaces[type], spaces);

        return 0;
}

swap용 address_space 구조체를 할당하여 준비한다.

  • swapon에 의해 호출되어 nr_pages를 SWAP_ADDRESS_SPACE_PAGES(2^14) 단위로 절상한 수만큼 address_space 구조체를 할당하여 초기화한 후 swapper_space[@type]에 지정한다.
  • swap 영역에 사용되는 swap 캐시를 하나의 radix tree로 관리하였었는데, rock 사용 빈도를 줄여 성능을 올리기 위해 swap 캐시를 관리하는 address_space마다 최대 64M만을 관리하도록 나누어 배치하였다. 따라서 swapper_space[] 배열을 사용하는 것이 아니라 swapper_space[][] 이중 배열로 사용하는 것으로 변경되었다.

 

다음 그림은 swapon 시 swap용 address_space 구조체 배열이 생성되는 과정을 보여준다.

 

exit_swap_address_space()

mm/swap_state.c

void exit_swap_address_space(unsigned int type)
{
        struct address_space *spaces;

        spaces = swapper_spaces[type];
        nr_swapper_spaces[type] = 0;
        rcu_assign_pointer(swapper_spaces[type], NULL);
        synchronize_rcu();
        kvfree(spaces);
}

swapoff에 의해 호출되어 swapper_space[@type]에 저장된 address_space 배열을 할당 해제한다.

 

swap 엔트리로 address_space 찾기

swap_address_space()

include/linux/swap.h

#define swap_address_space(entry)                           \
        (&swapper_spaces[swp_type(entry)][swp_offset(entry) \
                >> SWAP_ADDRESS_SPACE_SHIFT])

swap 엔트리에 해당하는 address_space 포인터를 반환한다.

  • swap 엔트리의 type과 offset으로 지정된 address_space 포인터를 반환한다.

 

/* linux/mm/swap_state.c */
/* One swap address space for each 64M swap space */
#define SWAP_ADDRESS_SPACE_SHIFT        14
#define SWAP_ADDRESS_SPACE_PAGES        (1 << SWAP_ADDRESS_SPACE_SHIFT)

1 개의 address_space가 관리하는 swap 크기는 64M이다.

 

다음 그림은 swap_address_space() 함수에서 swap 엔트리로 address_space를 알아오는 과정을 보여준다.

 

swapper_spaces[]

mm/swap_state.c

struct address_space *swapper_spaces[MAX_SWAPFILES] __read_mostly;

MAX_SWAPFILES(커널 옵션에 따라 27~32, ARM64 디폴트=29) 수 만큼의 파일과 연결되며 각각은 address_space 배열이 지정된다.

  • 이 배열은 static하게 1차원 배열로 생성되지만, 실제 운영은 다음과 같이 2차원 배열로 사용한다.
    • address_space[type][offset]

 

다음 그림은 swap 캐시의 address_space가 관리하는 xarray와 swap opearations를 보여준다.

 


구조체

swap_info_struct 구조체

/*
 * The in-memory structure used to track swap areas.
 */
struct swap_info_struct {
        unsigned long   flags;          /* SWP_USED etc: see above */
        signed short    prio;           /* swap priority of this type */
        struct plist_node list;         /* entry in swap_active_head */
        signed char     type;           /* strange name for an index */
        unsigned int    max;            /* extent of the swap_map */
        unsigned char *swap_map;        /* vmalloc'ed array of usage counts */
        struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
        struct swap_cluster_list free_clusters; /* free clusters list */
        unsigned int lowest_bit;        /* index of first free in swap_map */
        unsigned int highest_bit;       /* index of last free in swap_map */
        unsigned int pages;             /* total of usable pages of swap */
        unsigned int inuse_pages;       /* number of those currently in use */
        unsigned int cluster_next;      /* likely index for next allocation */
        unsigned int cluster_nr;        /* countdown to next cluster search */
        struct percpu_cluster __percpu *percpu_cluster; /* per cpu's swap location */
        struct swap_extent *curr_swap_extent;
        struct swap_extent first_swap_extent;
        struct block_device *bdev;      /* swap device or bdev of swap file */
        struct file *swap_file;         /* seldom referenced */
        unsigned int old_block_size;    /* seldom referenced */
#ifdef CONFIG_FRONTSWAP
        unsigned long *frontswap_map;   /* frontswap in-use, one bit per page */
        atomic_t frontswap_pages;       /* frontswap pages in-use counter */
#endif
        spinlock_t lock;                /*
                                         * protect map scan related fields like
                                         * swap_map, lowest_bit, highest_bit,
                                         * inuse_pages, cluster_next,
                                         * cluster_nr, lowest_alloc,
                                         * highest_alloc, free/discard cluster
                                         * list. other fields are only changed
                                         * at swapon/swapoff, so are protected
                                         * by swap_lock. changing flags need
                                         * hold this lock and swap_lock. If
                                         * both locks need hold, hold swap_lock
                                         * first.
                                         */
        spinlock_t cont_lock;           /*
                                         * protect swap count continuation page
                                         * list.
                                         */
        struct work_struct discard_work; /* discard worker */
        struct swap_cluster_list discard_clusters; /* discard clusters list */
        struct plist_node avail_lists[0]; /*
                                           * entries in swap_avail_heads, one
                                           * entry per node.
                                           * Must be last as the number of the
                                           * array is nr_node_ids, which is not
                                           * a fixed value so have to allocate
                                           * dynamically.
                                           * And it has to be an array so that
                                           * plist_for_each_* can work.
                                           */
};

swap 영역마다 하나씩 사용된다.

  • flags
    • swap 영역에 사용되는 플래그 값들이다. (아래 참조)
  • prio
    • 영역의 사용에 대한 순서를 정하기 위한 우선 순위 값이다.
    • default 값은 -2부터 생성되는 순서대로 감소된다.
    • 사용자가 지정하는 경우 양수를 사용할 수 있다.
  • list
    • swap_active_head 리스트에 사용되는 노드이다.
  • type
    • swap 영역에 대한 인덱스 번호이다. (0~)
  • max
    • swap 영역의 전체 페이지 수 (bad 페이지 포함)
  • *swam_map
    • swap 영역에 대한 swap 맵으로 1페이지당 1바이트의 값을 사용된다.
    • 예) 0=free 상태, 1~0x3e: 사용 상태(usage counter), 0x3f: bad 상태
  • *cluster_info
    • swap 영역에서 사용하는 모든 클러스터들을 가리킨다.
  • free_clusters
    • 사용 가능한  free 클러스터들을 담는 리스트이다.
  • lowest_bit
    • swap 영역에서 가장 낮은 free 페이지의 offset 값이다.
  • highest_bit
    • swap 영역에서 가장 높은 free 페이지의 offset 값이다.
  • pages
    • swap 영역의 전체 사용가능한 페이지 수 (bad 페이지 제외)
  • inuse_pages
    • swap 영역에서 할당되어 사용중인 페이지 수
  • cluster_next
    • 높은 확률로 다음 할당 시 사용할 페이지 offset를 가리킨다.
  • cluster_nr
    • 다음 클러스터를 검색하기 위한 countdown 값
  • *percpu_cluster
    • cpu별 현재 지정된 클러스터를 가리킨다.
    • 해당 cpu에 지정된 클러스터가 없는 경우 그 cpu의 값은 null을 가진다.
  • *curr_swap_extent
    • 현재 사용중인 swap extent를 가리킨다.
  • first_swap_extent
    • swap 영역에 내장된 첫 swap extent이다.
  • *bdev
    • swap 영역에 지정된 블럭 디바이스 또는 swap 파일의 블럭 디바이스
  • *swap_file
    • swap 파일을 가리킨다.
  • old_block_size
    • swap 블럭 디바이스의 사이즈
  • *frontswap_map
    • frontswap의 in-use 상태를 비트로 표기하고, 1 비트는 1 페이지에 해당한다.
  • frontswap_pages
    • fronswap의 in-use 카운터
  • lock
    • swap 영역에 대한 lock이다.
  • cont_lock
    • swap_map[]의 usage 카운터가 0x3e를 초과하는 경우에 swap 영역의 맵 관리는 swap count continuation 모드로 관리되는데 이 때 사용되는 swap count continuation 페이지 리스트에 접근할 때 사용하는 lock 이다.
  • discard_work
    • discard를 지원하는 SSD에서 사용할 워크이다.
  • discard_clusters
    • discard할 클러스터들이 담긴 리스트이다.
    • discard 처리 후 free 클러스터 리스트로 옮긴다.
  • avail_lists[0]
    • 노드별로 관리되는 swap_avail_heads[] 리스트의 노드로 사용된다.

 

플래그에 사용되는 값들이다.

enum {
        SWP_USED        = (1 << 0),     /* is slot in swap_info[] used? */
        SWP_WRITEOK     = (1 << 1),     /* ok to write to this swap?    */
        SWP_DISCARDABLE = (1 << 2),     /* blkdev support discard */
        SWP_DISCARDING  = (1 << 3),     /* now discarding a free cluster */
        SWP_SOLIDSTATE  = (1 << 4),     /* blkdev seeks are cheap */
        SWP_CONTINUED   = (1 << 5),     /* swap_map has count continuation */
        SWP_BLKDEV      = (1 << 6),     /* its a block device */
        SWP_ACTIVATED   = (1 << 7),     /* set after swap_activate success */
        SWP_FS          = (1 << 8),     /* swap file goes through fs */
        SWP_AREA_DISCARD = (1 << 9),    /* single-time swap area discards */
        SWP_PAGE_DISCARD = (1 << 10),   /* freed swap page-cluster discards */
        SWP_STABLE_WRITES = (1 << 11),  /* no overwrite PG_writeback pages */
        SWP_SYNCHRONOUS_IO = (1 << 12), /* synchronous IO is efficient */
                                        /* add others here before... */
        SWP_SCANNING    = (1 << 13),    /* refcount in scan_swap_map */
};
  • SWP_USED
    • swapon되어 사용 중인 swap 영역이다.
  • SWP_WRITEOK
    • swap 영역에 write 가능한 상태이다.
  • SWP_DISCARDABLE
    • swap 영역이 discard를 지원한다. (SSD)
  • SWP_DISCARDING
    • swap 영역에 discard 작업을 수행 중이다.
  • SWP_SOLIDSTATE
    • swap 영역이 SSD이다.
  • SWP_CONTINUED
    • swap 영역에서 usage 카운터가 0x3e를 초과하여 사용 중이다.
  • SWP_BLKDEV
    • swap 영역이 블럭 디바이스이다.
  • SWP_ACTIVATED
    • swap 영역이 swap_activate 후크를 성공한 경우에 설정된다.
  • SWP_FS
    • 파일 시스템을 통한 swap 파일이다.
  • SWP_AREA_DISCARD
    • 한 번만 discard를 지원한다.
  • SWP_PAGE_DISCARD
    • 클러스터 단위로 자유롭게 discard를 사용할 수 있다.
  • SWP_STABLE_WRITES
    • 체크섬등을 사용한 무결성 저장 장치에 부여한다.
    • SCSI Data Integrity Block Device
  • SWP_SYNCHRONOUS_IO
    • 동기 저장이 가능한 swap 영역이다. (RAM 등)
  • SWP_SCANNING
    • swap 영역을 검색중일 때 설정된다.

 

참고

 

 

VMPressure

<kernel v5.0>

VMPressure

Memory Control Ggroup을 통해 스캔한 페이지와 회수한 페이지 비율을 분석하여 메모리 압박률을 산출하고, 이에 대응하는 스레졸드별 3 가지 이벤트 레벨로 memcg에 등록한 vmpressure 리스너들에 통지할 수 있게 하였다.  vmpressure 리스너들은 eventfd를 사용하여 이러한 이벤트를 수신할 수 있다.

 

이벤트 레벨

  • low
    • memcg로 지정한 메모리 압박이 적은 편이다.
  • medium
    • memcg로 지정한 메모리 압박이 많은 편이다.
  • critical
    • memcg로 지정한 메모리 압박이 심해 곧 OOM killer가 동작할 예정이다.

 

vmpressure_win

mm/vmpressure.c

/*
 * The window size (vmpressure_win) is the number of scanned pages before
 * we try to analyze scanned/reclaimed ratio. So the window is used as a
 * rate-limit tunable for the "low" level notification, and also for
 * averaging the ratio for medium/critical levels. Using small window
 * sizes can cause lot of false positives, but too big window size will
 * delay the notifications.
 *
 * As the vmscan reclaimer logic works with chunks which are multiple of
 * SWAP_CLUSTER_MAX, it makes sense to use it for the window size as well.
 *
 * TODO: Make the window size depend on machine size, as we do for vmstat
 * thresholds. Currently we set it to 512 pages (2MB for 4KB pages).
 */
static const unsigned long vmpressure_win = SWAP_CLUSTER_MAX * 16;
  • SWAP_CLUSTER_MAX(32) * 16 = 512 페이지로 설정되어 있다.
  • 이 윈도우 사이즈는 scanned/reclaim 비율을 분석을 시도하기 전에 사용하는 scanned 페이지 수이다.
  • low 레벨 notification에 사용되고 medium/critical 레벨의 평균 비율을 위해서도 사용된다.

 

vmpressure_level_med & vmpressure_level_critical

mm/vmpressure.c

/*
 * These thresholds are used when we account memory pressure through
 * scanned/reclaimed ratio. The current values were chosen empirically. In
 * essence, they are percents: the higher the value, the more number
 * unsuccessful reclaims there were.
 */
static const unsigned int vmpressure_level_med = 60;
static const unsigned int vmpressure_level_critical = 95;
  • vmpressure_level_med
    • scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 medium 레벨의 스레졸드 값
  • vmpressure_level_critical
    • scanned/reclaimed 비율로 메모리 pressure 계량시 사용되는 critical 레벨의 스레졸드 값

 

vmpressure_prio()

mm/vmpressure.c

/**
 * vmpressure_prio() - Account memory pressure through reclaimer priority level
 * @gfp:        reclaimer's gfp mask
 * @memcg:      cgroup memory controller handle
 * @prio:       reclaimer's priority
 *
 * This function should be called from the reclaim path every time when
 * the vmscan's reclaiming priority (scanning depth) changes.
 *
 * This function does not return any value.
 */
void vmpressure_prio(gfp_t gfp, struct mem_cgroup *memcg, int prio)
{
        /*
         * We only use prio for accounting critical level. For more info
         * see comment for vmpressure_level_critical_prio variable above.
         */
        if (prio > vmpressure_level_critical_prio)
                return;

        /*
         * OK, the prio is below the threshold, updating vmpressure
         * information before shrinker dives into long shrinking of long
         * range vmscan. Passing scanned = vmpressure_win, reclaimed = 0
         * to the vmpressure() basically means that we signal 'critical'
         * level.
         */
        vmpressure(gfp, memcg, true, vmpressure_win, 0);
}

우선 순위가 높아져 스캔 depth가 깊어지는 경우 vmpressure 정보를 갱신한다.

  • 코드 라인 7~8에서 요청 우선 순위가 vmpressure_level_critical_prio(3)보다 낮아 함수를 빠져나간다.
    • prio는 낮을 수록 우선 순위가 높다.
  • 코드 라인 17에서 스레졸드 이하로 prio가 떨어진 경우, 즉 우선 순위가 높아진 경우 shrinker가 오랫 동안 스캔하기 전에 vmpressure 정보를 업데이트한다.

 

vmpressure()

mm/vmpressure.c

/**
 * vmpressure() - Account memory pressure through scanned/reclaimed ratio
 * @gfp:        reclaimer's gfp mask
 * @memcg:      cgroup memory controller handle
 * @tree:       legacy subtree mode
 * @scanned:    number of pages scanned
 * @reclaimed:  number of pages reclaimed
 *
 * This function should be called from the vmscan reclaim path to account
 * "instantaneous" memory pressure (scanned/reclaimed ratio). The raw
 * pressure index is then further refined and averaged over time.
 *
 * If @tree is set, vmpressure is in traditional userspace reporting
 * mode: @memcg is considered the pressure root and userspace is
 * notified of the entire subtree's reclaim efficiency.
 *
 * If @tree is not set, reclaim efficiency is recorded for @memcg, and
 * only in-kernel users are notified.
 *
 * This function does not return any value.
 */
void vmpressure(gfp_t gfp, struct mem_cgroup *memcg, bool tree,
                unsigned long scanned, unsigned long reclaimed)
{
        struct vmpressure *vmpr = memcg_to_vmpressure(memcg);

        /*
         * Here we only want to account pressure that userland is able to
         * help us with. For example, suppose that DMA zone is under
         * pressure; if we notify userland about that kind of pressure,
         * then it will be mostly a waste as it will trigger unnecessary
         * freeing of memory by userland (since userland is more likely to
         * have HIGHMEM/MOVABLE pages instead of the DMA fallback). That
         * is why we include only movable, highmem and FS/IO pages.
         * Indirect reclaim (kswapd) sets sc->gfp_mask to GFP_KERNEL, so
         * we account it too.
         */
        if (!(gfp & (__GFP_HIGHMEM | __GFP_MOVABLE | __GFP_IO | __GFP_FS)))
                return;

        /*
         * If we got here with no pages scanned, then that is an indicator
         * that reclaimer was unable to find any shrinkable LRUs at the
         * current scanning depth. But it does not mean that we should
         * report the critical pressure, yet. If the scanning priority
         * (scanning depth) goes too high (deep), we will be notified
         * through vmpressure_prio(). But so far, keep calm.
         */
        if (!scanned)
                return;

        if (tree) {
                spin_lock(&vmpr->sr_lock);
                scanned = vmpr->tree_scanned += scanned;
                vmpr->tree_reclaimed += reclaimed;
                spin_unlock(&vmpr->sr_lock);

                if (scanned < vmpressure_win)
                        return;
                schedule_work(&vmpr->work);
        } else {
                enum vmpressure_levels level;

                /* For now, no users for root-level efficiency */
                if (!memcg || memcg == root_mem_cgroup)
                        return;

                spin_lock(&vmpr->sr_lock);
                scanned = vmpr->scanned += scanned;
                reclaimed = vmpr->reclaimed += reclaimed;
                if (scanned < vmpressure_win) {
                        spin_unlock(&vmpr->sr_lock);
                        return;
                }
                vmpr->scanned = vmpr->reclaimed = 0;
                spin_unlock(&vmpr->sr_lock);

                level = vmpressure_calc_level(scanned, reclaimed);

                if (level > VMPRESSURE_LOW) {
                        /*
                         * Let the socket buffer allocator know that
                         * we are having trouble reclaiming LRU pages.
                         *
                         * For hysteresis keep the pressure state
                         * asserted for a second in which subsequent
                         * pressure events can occur.
                         */
                        memcg->socket_pressure = jiffies + HZ;
                }
        }
}

scaned 및 reclaimed 비율로 메모리 pressure를 계량한다.

  • 코드 라인 4에서 요청한 memcg의 vmpressure 정보를 반환한다.
  • 코드 라인 17~18에서 highmem, movable, FS, IO 플래그 요청이 하나도 없는 경우 pressure 계량을 하지 않는다.
  • 코드 라인 28~29에서 인수 scanned가 0인 경우 함수를 중단한다.
  • 코드 라인 31~39에서 기존 tree 방식의 presssure를 계량한다. tree_scanned와 tree_reclaimed 각각 그 만큼 증가시키고 vmpr->work에 등록한 작업을 실행시킨다. 만일 vmpr->scanned가 vmpressure_win 보다 작은 경우 함수를 중단한다.
    • vmpressure_work_fn()
  • 코드 라인 40~45에서 @tree가 0이면 커널 내부 사용자에게 통지하기 위해 @memcg를 위한 회수 효율성이 기록된다. memcg가 지정되지 않은 경우 함수를 중단한다.
  • 코드 라인 47~55에서 scanned와 reclaimed 각각 그 만큼 증가시키고 만일 scanned가 vmpressure_win 보다 작은 경우 함수를 중단한다. 중단하지 않은 경우 vmpr의 scanned와 reclaimed는 0으로 리셋한다.
  • 코드 라인 57~69에서 산출된 vmpressure 레벨이 VMPRESSURE_LOW를 초과하면 memcg의 socket_pressure를 현재 시각보다 1초 뒤인 틱 값을 설정한다.

 

다음 그림은 vmpressure() 함수가 처리되는 과정을 보여준다.

 


워크 큐에서 vmpressure에 따른 이벤트 통지

vmpressure_work_fn()

mm/vmpressure.c

static void vmpressure_work_fn(struct work_struct *work)
{
        struct vmpressure *vmpr = work_to_vmpressure(work);
        unsigned long scanned;
        unsigned long reclaimed;
        enum vmpressure_levels level;
        bool ancestor = false;
        bool signalled = false;

        spin_lock(&vmpr->sr_lock);
        /*
         * Several contexts might be calling vmpressure(), so it is
         * possible that the work was rescheduled again before the old
         * work context cleared the counters. In that case we will run
         * just after the old work returns, but then scanned might be zero
         * here. No need for any locks here since we don't care if
         * vmpr->reclaimed is in sync.
         */
        scanned = vmpr->tree_scanned;
        if (!scanned) {
                spin_unlock(&vmpr->sr_lock);
                return;
        }

        reclaimed = vmpr->tree_reclaimed;
        vmpr->tree_scanned = 0;
        vmpr->tree_reclaimed = 0;
        spin_unlock(&vmpr->sr_lock);

        level = vmpressure_calc_level(scanned, reclaimed);

        do {
                if (vmpressure_event(vmpr, level, ancestor, signalled))
                        signalled = true;
                ancestor = true;
        } while ((vmpr = vmpressure_parent(vmpr)));
}

메모리 압박 레벨을 산출하고 레벨 및 모드 조건을 만족시키는 vmpressure 리스너에 이벤트를 전송한다.

  • 코드 라인 10~28에서 tree_scanned 값과 tree_reclaimed 값을 가져오고 리셋한다.
  • 코드 라인 30에서 scanned 값과 reclaimed 값으로 레벨을 산출한다.
  • 코드 라인 32~36에서 하이라키로 구성된 memcg의 vmpressure 값을 최상위 루트까지 순회하며 조건을 만족시키는 vmpressure 리스너에 이벤트를 통지한다.

 

memcg_to_vmpressure()

mm/memcontrol.c

/* Some nice accessors for the vmpressure. */
struct vmpressure *memcg_to_vmpressure(struct mem_cgroup *memcg)
{
        if (!memcg)
                memcg = root_mem_cgroup;
        return &memcg->vmpressure;   
}

요청한 memcg의 vmpressure 정보를 반환한다. memcg가 지정되지 않은 경우 root memcg의 vmpressure를 반환한다.

 

vmpressure 이벤트 통지

vmpressure_event()

mm/vmpressure.c

static bool vmpressure_event(struct vmpressure *vmpr,
                             const enum vmpressure_levels level,
                             bool ancestor, bool signalled)
{
        struct vmpressure_event *ev;
        bool ret = false;

        mutex_lock(&vmpr->events_lock);
        list_for_each_entry(ev, &vmpr->events, node) {
                if (ancestor && ev->mode == VMPRESSURE_LOCAL)
                        continue;
                if (signalled && ev->mode == VMPRESSURE_NO_PASSTHROUGH)
                        continue;
                if (level < ev->level)
                        continue;
                eventfd_signal(ev->efd, 1);
                ret = true;
        }
        mutex_unlock(&vmpr->events_lock);

        return ret;
}

vmpressure에 등록된 이벤트들을 대상으로 요청 @level 이하로 등록한 vmpressure 리스터 application에 eventfd 시그널을 통지한다.

  • 통지 대상이 아닌 경우는 다음과 같다.
    • @ancestor=1일 때, local 모드는 제외한다.
    • @signalled=1일 때, no_passthrough 모드는 제외한다.
    • @level보다 큰 레벨로 등록한 경우는 제외한다.

 

다음 그림은 memcg에 등록한 vmpressure 리스너에 이벤트를 통지하는 조건들을 보여준다.

 


vmpressure 레벨 산출

vmpressure_calc_level()

mm/vmpressure.c

static enum vmpressure_levels vmpressure_calc_level(unsigned long scanned,
                                                    unsigned long reclaimed)
{
        unsigned long scale = scanned + reclaimed;
        unsigned long pressure = 0;

        /*
         * reclaimed can be greater than scanned for things such as reclaimed
         * slab pages. shrink_node() just adds reclaimed pages without a
         * related increment to scanned pages.
         */
        if (reclaimed >= scanned)
                goto out;
        /*
         * We calculate the ratio (in percents) of how many pages were
         * scanned vs. reclaimed in a given time frame (window). Note that
         * time is in VM reclaimer's "ticks", i.e. number of pages
         * scanned. This makes it possible to set desired reaction time
         * and serves as a ratelimit.
         */
        pressure = scale - (reclaimed * scale / scanned);
        pressure = pressure * 100 / scale;

out:
        pr_debug("%s: %3lu  (s: %lu  r: %lu)\n", __func__, pressure,
                 scanned, reclaimed);

        return vmpressure_level(pressure);
}

scanned, reclaimed 비율에 따라 pressure 값을 산출하고, 이에 따른 레벨을 반환한다.

 

다음과 예와 같이 scanned 페이지 수와 reclaimed 페이지 수에 대한 pressure 값과 레벨을 확인해보자.

  • scanned=5, reclaimed=0
    • pressure=100%, level=critical
  • scanned=5, reclaimed=1
    • pressure=66%, level=medium
  • scanned=5, reclaimed=2
    • pressure=57%, level=low
  • scanned=5, reclaimed=3
    • pressure=37%, level=low
  • scanned=5, reclaimed=4
    • pressure=11%, level=low
  • scanned=5, reclaimed=5
    • pressure=0%, level=low

 

다음 그림은 scanned, reclaimed 비율에 따른 pressure 값을 산출하고, 이에 따른 레벨을 결정하는 과정을 보여준다.

 

vmpressure_level()

mm/vmpressure.c

static enum vmpressure_levels vmpressure_level(unsigned long pressure)
{
        if (pressure >= vmpressure_level_critical)
                return VMPRESSURE_CRITICAL;
        else if (pressure >= vmpressure_level_med)
                return VMPRESSURE_MEDIUM;
        return VMPRESSURE_LOW;
}

@pressure에 따른 레벨을 반환한다.

  • critical
    • 디폴트 값 95% 이상
  • med
    • 디폴트 값 60% 이상
  • low
    • 그 외

 


이벤트 수신 프로그램 데모

cgroup_event_listener

tools/cgroup 위치에서 make를 실행하면 다음 소스를 빌드하여 cgroup_event_listener 파일이 생성된다.

tools/cgroup/cgroup_event_listener.c

#include <assert.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>

#include <sys/eventfd.h>

#define USAGE_STR "Usage: cgroup_event_listener <path-to-control-file> <args>"

int main(int argc, char **argv)
{
        int efd = -1;
        int cfd = -1;
        int event_control = -1;
        char event_control_path[PATH_MAX];
        char line[LINE_MAX];
        int ret;

        if (argc != 3)
                errx(1, "%s", USAGE_STR);

        cfd = open(argv[1], O_RDONLY);
        if (cfd == -1)
                err(1, "Cannot open %s", argv[1]);

        ret = snprintf(event_control_path, PATH_MAX, "%s/cgroup.event_control",
                        dirname(argv[1]));
        if (ret >= PATH_MAX)
                errx(1, "Path to cgroup.event_control is too long");

        event_control = open(event_control_path, O_WRONLY);
        if (event_control == -1)
                err(1, "Cannot open %s", event_control_path);

        efd = eventfd(0, 0);
        if (efd == -1)
                err(1, "eventfd() failed");

        ret = snprintf(line, LINE_MAX, "%d %d %s", efd, cfd, argv[2]);
        if (ret >= LINE_MAX)
                errx(1, "Arguments string is too long");

        ret = write(event_control, line, strlen(line) + 1);
        if (ret == -1)
                err(1, "Cannot write to cgroup.event_control");

        while (1) {
                uint64_t result;

                ret = read(efd, &result, sizeof(result));
                if (ret == -1) {
                        if (errno == EINTR)
                                continue;
                        err(1, "Cannot read from eventfd");
                }
                assert(ret == sizeof(result));

                ret = access(event_control_path, W_OK);
                if ((ret == -1) && (errno == ENOENT)) {
                        puts("The cgroup seems to have removed.");
                        break;
                }

                if (ret == -1)
                        err(1, "cgroup.event_control is not accessible any more");

                printf("%s %s: crossed\n", argv[1], argv[2]);
        }

        return 0;
}

 

사용 방법

다음과 같이 pressure 레벨이 medium일 때 이벤트를 수신할 수 있게 한다.

# cd /sys/fs/cgroup/memory/
# mkdir foo
# cd foo
# cgroup_event_listener memory.pressure_level medium &
# echo 8000000 > memory.limit_in_bytes
# echo 8000000 > memory.memsw.limit_in_bytes
# echo $$ > tasks
# dd if=/dev/zero | read x

 

참고