Start_Kernel (Week 15)
Start_Kernel (Week 15)

Start_Kernel (Week 15)

카테고리
⚙️ Start Kernel
작성자
박용성박용성
작성일
2024년 08월 18일
태그
C
Linux
Slug
start-kernel-15
floatFirstTOC: right

🖥️ 전체 코드

/** * struct folio - Represents a contiguous set of bytes. * @flags: Identical to the page flags. * @lru: Least Recently Used list; tracks how recently this folio was used. * @mlock_count: Number of times this folio has been pinned by mlock(). * @mapping: The file this page belongs to, or refers to the anon_vma for * anonymous memory. * @index: Offset within the file, in units of pages. For anonymous memory, * this is the index from the beginning of the mmap. * @private: Filesystem per-folio data (see folio_attach_private()). * @swap: Used for swp_entry_t if folio_test_swapcache(). * @_mapcount: Do not access this member directly. Use folio_mapcount() to * find out how many times this folio is mapped by userspace. * @_refcount: Do not access this member directly. Use folio_ref_count() * to find how many references there are to this folio. * @memcg_data: Memory Control Group data. * @virtual: Virtual address in the kernel direct map. * @_last_cpupid: IDs of last CPU and last process that accessed the folio. * @_entire_mapcount: Do not use directly, call folio_entire_mapcount(). * @_large_mapcount: Do not use directly, call folio_mapcount(). * @_nr_pages_mapped: Do not use outside of rmap and debug code. * @_pincount: Do not use directly, call folio_maybe_dma_pinned(). * @_folio_nr_pages: Do not use directly, call folio_nr_pages(). * @_hugetlb_subpool: Do not use directly, use accessor in hugetlb.h. * @_hugetlb_cgroup: Do not use directly, use accessor in hugetlb_cgroup.h. * @_hugetlb_cgroup_rsvd: Do not use directly, use accessor in hugetlb_cgroup.h. * @_hugetlb_hwpoison: Do not use directly, call raw_hwp_list_head(). * @_deferred_list: Folios to be split under memory pressure. * * A folio is a physically, virtually and logically contiguous set * of bytes. It is a power-of-two in size, and it is aligned to that * same power-of-two. It is at least as large as %PAGE_SIZE. If it is * in the page cache, it is at a file offset which is a multiple of that * power-of-two. It may be mapped into userspace at an address which is * at an arbitrary page offset, but its kernel virtual address is aligned * to its size. */ struct folio { /* private: don't document the anon union */ union { struct { /* public: */ unsigned long flags; union { struct list_head lru; /* private: avoid cluttering the output */ struct { void *__filler; /* public: */ unsigned int mlock_count; /* private: */ }; /* public: */ }; struct address_space *mapping; pgoff_t index; union { void *private; swp_entry_t swap; }; atomic_t _mapcount; atomic_t _refcount; #ifdef CONFIG_SLAB_OBJ_EXT unsigned long memcg_data; #endif #if defined(WANT_PAGE_VIRTUAL) void *virtual; #endif #ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid; #endif /* private: the union with struct page is transitional */ }; struct page page; }; union { struct { unsigned long _flags_1; unsigned long _head_1; /* public: */ atomic_t _large_mapcount; atomic_t _entire_mapcount; atomic_t _nr_pages_mapped; atomic_t _pincount; #ifdef CONFIG_64BIT unsigned int _folio_nr_pages; #endif /* private: the union with struct page is transitional */ }; struct page __page_1; }; union { struct { unsigned long _flags_2; unsigned long _head_2; /* public: */ void *_hugetlb_subpool; void *_hugetlb_cgroup; void *_hugetlb_cgroup_rsvd; void *_hugetlb_hwpoison; /* private: the union with struct page is transitional */ }; struct { unsigned long _flags_2a; unsigned long _head_2a; /* public: */ struct list_head _deferred_list; /* private: the union with struct page is transitional */ }; struct page __page_2; }; }; #define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct page, pg) == offsetof(struct folio, fl)) FOLIO_MATCH(flags, flags); FOLIO_MATCH(lru, lru); FOLIO_MATCH(mapping, mapping); FOLIO_MATCH(compound_head, lru); FOLIO_MATCH(index, index); FOLIO_MATCH(private, private); FOLIO_MATCH(_mapcount, _mapcount); FOLIO_MATCH(_refcount, _refcount); #ifdef CONFIG_MEMCG FOLIO_MATCH(memcg_data, memcg_data); #endif #if defined(WANT_PAGE_VIRTUAL) FOLIO_MATCH(virtual, virtual); #endif #ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS FOLIO_MATCH(_last_cpupid, _last_cpupid); #endif #undef FOLIO_MATCH #define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct folio, fl) == \ offsetof(struct page, pg) + sizeof(struct page)) FOLIO_MATCH(flags, _flags_1); FOLIO_MATCH(compound_head, _head_1); #undef FOLIO_MATCH #define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct folio, fl) == \ offsetof(struct page, pg) + 2 * sizeof(struct page)) FOLIO_MATCH(flags, _flags_2); FOLIO_MATCH(compound_head, _head_2); FOLIO_MATCH(flags, _flags_2a); FOLIO_MATCH(compound_head, _head_2a); #undef FOLIO_MATCH
 

💡 folio 구조체가 생겨난 배경

리눅스 커널은 메모리를 기본적으로 4KB 크기의 페이지 단위로 관리한다. 그러나 특정 작업에서는 더 큰 메모리 블록을 다루어야 할 때가 많다. 예를 들어 파일 시스템이나 메모리 집약적인 작업에서는 4KB 페이지가 아닌 더 큰 크기의 메모리 블록을 처리하는 것이 효율적이다. 이를 위해 리눅스 커널은 Compound PageTransparent Huge Pages (THP)라는 개념을 도입했다.

1️⃣ Compound Page

  • 여러 페이지를 하나의 큰 메모리 블록처럼 묶어주는 구조, 이때 첫 번째 페이지는 Head Page로, 나머지 페이지들은 Tail Page로 관리된다.
  • 이런 Compound Page를 사용할 때마다 해당 페이지가 Head Page인지 Tail Page인지를 매번 확인해야 하므로 코드의 복잡성을 증가시킨다.
    • notion image

      💡 Compound Page 구조

      notion image
    • Head Page:
      • flags 필드에 PG_head 플래그가 설정되어 있어서 이 페이지가 Compound Page의 head임을 표시한다.
      • Head Page는 전체 Compound Page의 메타데이터를 관리한다.
    • Tail Pages:
      • 첫 번째 Tail Page는 중요한 메타데이터를 포함하고 있다:
        • compound_head: 이 Tail Page가 속한 Compound Page의 head를 가리킨다.
        • 이후 각 필드는 Compound Page의 다양한 속성을 나타낸다.
      • 그 이후의 Tail Pages는 점차적으로 더 적은 정보를 가지고 있다.

2️⃣ THP (Transparent Huge Pages)

  • 시스템이 메모리를 일정한 크기(일반적으로 2MB)로 나누어 사용할 수 있도록 하는 기능
  • 이는 큰 메모리 할당에서 TLB 미스와 같은 문제를 줄이기 위해 설계되었지만, 역시 복잡한 메모리 관리 로직이 필요하다.
 
이러한 복잡성을 해결하기 위해 folio 구조체가 도입되었다. 다음은 개발자가 이 구조체를 도입한 근거이다:
1.3. 보다 빠른 메모리 관리를 위한 메모리 폴리오스 인프라 시스템의 메모리를 관리하기 위해 사용 가능한 RAM은 페이지라고 하는 작은 단위로 나뉩니다. 이러한 페이지의 크기는 아키텍처에 따라 다르지만 x86 시스템에서는 KB입니다. 수십 GB의 최신 시스템에서 이러한 작은 페이지 크기는 관리하기 어려운 방대한 양의 페이지와 같습니다. 이 문제를 해결하기 위해 리눅스 커널은 하나 이상의 물리적 페이지를 포함할 수 있는 페이지 구조인 복합 페이지의 개념을 개발했습니다. 그러나 이러한 복합 페이지의 작동 방식은 명확하지 않으며 버그가 발생하기 쉬운 API를 가지고 있어 커널 전체에 오버헤드를 발생시킵니다.
 

🔍 folio 구조체

📌 folio 구조

notion image
512KB의 Compound Page를 기존의 방식대로 구현하면 총 128개의 페이지가 필요하지만 folio 방식을 사용하면 하나의 단위로 묶어 관리한다.
  • Head Page 정보:
    • folio 의 첫 번째 union 구조체는 Head 페이지에 해당하는 정보를 포함한다. (기본적인 페이지 관리 정보)
  • Tail Page 정보:
    • 추가적인 두 개의 union 구조체는 나머지 Tail 페이지에 관련된 정보를 관리한다.
    • 두 번째 union 구조체는 모든 Tail 페이지를 포함할 수 있도록 설계되었기 때문에 각각의 페이지마다 구조체를 할당할 필요가 없다.
    • 세 번째 union 구조체는 나머지 추가적인 Tail 페이지 정보와 특수한 경우를 처리한다.
 

folio 는 코드상에서 대강 다음과 같이 정의된다.
struct folio { union { struct { unsigned long flags; struct list_head lru; struct address_space *mapping; pgoff_t index; void *private; atomic_t _mapcount; atomic_t _refcount; #ifdef CONFIG_MEMCG unsigned long memcg_data; #endif }; struct page page; }; };

💡 참고: C언어에서의 union

구조체와 비슷하지만 다른 점이라면 메모리 공간을 공유할 수 있는 자료형이라는 점이다. 모든 멤버 변수가 같은 메모리 공간을 공유하기 때문에, 한 멤버 변수를 설정하면 다른 멤버 변수가 바뀔 수 있다.
#include <stdio.h> union Data { int i; char str[20]; }; int main() { union Data data; data.i = 10; printf("data.i: %d\n", data.i); printf("data.str: %s\n", data.str); snprintf(data.str, 20, "Hello, World!"); printf("str 설정 후\n"); printf("data.i: %d\n", data.i); printf("data.str: %s\n", data.str); printf("\nunion Data 사이즈: %zu bytes\n", sizeof(data)); return 0; }
data.i: 10 data.str: str 설정 후 data.i: 1819043144 data.str: Hello, World! union Data 사이즈: 20 bytes
notion image

  • 첫 번째 멤버는 여러 개의 필드를 포함하는 무명 구조체다.
  • 두 번째 멤버는 struct page 이다.
 
이 두 멤버는 같은 메모리 공간을 차지하므로 folio->flagsfolio->page.flags 와 같은 메모리 위치를 참조한다. 왜냐면 flags 필드는 struct page 의 첫 번째 필드이기 때문이다.
#include <stdio.h> // struct page 정의 struct page { unsigned long flags; }; // struct folio 정의 struct folio { union { struct { unsigned long flags; }; struct page page; }; }; int main() { // struct folio 변수 선언 struct folio my_folio; // folio의 flags 필드에 값 할당 my_folio.flags = 0x1234; // folio의 flags와 page의 flags 출력 printf("my_folio.flags = 0x%lx\n", my_folio.flags); printf("my_folio.page.flags = 0x%lx\n", my_folio.page.flags); // page 구조체의 flags 필드에 값 변경 my_folio.page.flags = 0x5678; // folio의 flags와 page의 flags 출력 printf("\nmy_folio.page.flags값 변경 후:\n"); printf("my_folio.flags = 0x%lx\n", my_folio.flags); printf("my_folio.page.flags = 0x%lx\n", my_folio.page.flags); return 0; }
my_folio.flags = 0x1234 my_folio.page.flags = 0x1234 my_folio.page.flags값 변경 후: my_folio.flags = 0x5678 my_folio.page.flags = 0x5678
notion image

⚙️ Compound Page vs folio

Compound Page
Folio
구성 요소
Head PageTail Pages로 구성
하나의 통합된 Folio 구조체로 구성
Head, Tail
Head와 Tail 페이지를 구분하여 관리
모든 페이지를 하나의 Folio로 통합하여 관리
메타데이터
Head Page에 주요 메타데이터 저장, Tail Pages는 추가적인 정보 저장
모든 메타데이터를 Folio 구조체 내에서 통합 관리
사용되는 페이지 구조체 개수
페이지 수에 따라 다름 (예: 512KB Compound Page = 128개의 struct page)
크기에 관계없이 3개의 페이지 구조체로 관리 (예: 512KB 페이지도 동일)
접근하는 방식
특정 페이지에 대한 접근 시 Head 또는 Tail을 구분하여 처리
Folio를 통해 모든 페이지에 접근, Head와 Tail 구분 불필요
복잡성
Head와 Tail 구분을 위해 코드 복잡성 증가
통합된 인터페이스로 단순화
성능
Head와 Tail 구분에 따른 오버헤드 발생 가능
단순화로 성능 최적화

1️⃣ 단순화

folio 는 여러 페이지를 하나의 연속된 메모리 블록으로 간주해 이 단위가 항상 Head Page를 가리킨다는 것을 보장하므로 그저 여러 페이지를 묶어 하나의 큰 페이지로 취급하는 Compound Page와 달리 매번 Head Page인지 Tail Page인지 확인할 필요가 없다.

2️⃣ 단순화에 따른 일관성

기존의 메모리 관리 함수들은 일부는 단일 페이지를, 일부는 Compound Page의 Head Page(전체 블록의 대표 페이지, 첫 번째 페이지를 말한다. 나머지 페이지들은 Tail Page로 간주되어 각각 Head Page를 참조한다.)를 인자로 받았다.
예를 들어 Compound Page에서 Tail Page를 처리할 때 해당 페이지의 compound_head 필드를 통해 Head Page를 찾고, 실제 작업은 Head Page를 기준으로 수행된다.
// include/linux/page-flags.h line 245 static inline unsigned long _compound_head(const struct page *page) { unsigned long head = READ_ONCE(page->compound_head); if (unlikely(head & 1)) return head - 1; return (unsigned long)page_fixed_fake_head(page); }
 
이는 함수 호출자가 단일 페이지인지, Compound Page의 일부인지 알 수 없고 실수를 유발할 가능성이 높아진다. 허나 folio 는 페이지를 모두 하나의 논리적 단위로 다루며 항상 Head Page로 간주되므로 여러 개로 묶여 있더라도 일관된 인자를 사용 가능하다.

3️⃣ 일관성에 따른 성능 향상

파일 시스템이나 큰 메모리 블록이 필요한 활동 등에서 큰 페이지 단위로 메모리를 처리할 수 있어 효율성을 높일 수 있다.
 

💡 Compound Page와 folio의 cache 방식

  1. Legacy (디스크에서 데이터를 처리하는 기본 단위는 512B)
    1. notion image
  1. folio
notion image
 

🔍 코드 분석

💡 참고: struct page 분석

시스템의 각 물리적 페이지는 연관된 struct page 를 가진다. 이는 현재 페이지가 어떤 용도로 사용되고 있는지 추적하기 위함이다. 어떤 태스크가 페이지를 사용하고 있는지 직접 추적할 방법은 없지만 pagecahce 페이지의 경우 rmap 구조를 통해 누가 이 페이지를 매핑하고 있는지 알 수 있다.

1️⃣ 전반적 구조

struct page { unsigned long flags; union { struct { /* Page cache and anonymous pages */ }; struct { /* page_pool used by netstack */ }; struct { /* Tail pages of compound page */ }; struct { /* ZONE_DEVICE pages */ }; struct rcu_head rcu_head; }; union { /* 4-byte union */ }; atomic_t _refcount; #ifdef CONFIG_MEMCG unsigned long memcg_data; #endif #if defined(WANT_PAGE_VIRTUAL) void *virtual; #endif #ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid; #endif #ifdef CONFIG_KMSAN struct page *kmsan_shadow; struct page *kmsan_origin; #endif } _struct_page_alignment;
 

2️⃣ flags 필드

unsigned long flags;
페이지의 상태를 나타내는 다양한 플래그들을 저장한다. 페이지가 사용 중인지, 수정되었는지, 잠겼는지 등의 정보를 포함한다. 아토믹하게 업데이트되며 비동기적으로 변경 가능하다.
 

3️⃣ 1번째 union

페이지가 어떻게 사용되고 있는지에 따라 필요한 정보를 저장할 수 있도록 여러 구조체가 포함된다. 예를 들어 페이지가 파일 시스템 캐시나 익명 메모리로 사용될 때, 네트워크 스택에서 사용될 때 등이 있다.
  1. Page cache 및 익명 페이지
    1. struct { /* 페이지 캐시 및 익명 페이지 */ /** * @lru: 페이지 아웃 리스트, 예: lruvec->lru_lock으로 보호되는 * active_list. 때때로 페이지 소유자가 일반 리스트로 사용함. */ union { struct list_head lru; // 가장 오랫동안 참조되지 않은 페이지를 교체 /* 또는, Unevictable "LRU 리스트" 슬롯 */ struct { /* 항상 짝수, PageTail을 부정하기 위해?? */ void *__filler; /* 페이지 또는 folio의 mlock 수를 계산 */ unsigned int mlock_count; // mclock 함수에 의해 메모리에 고정된 횟수 }; /* 또는, 프리 페이지 */ // 페이지가 버디 시스템이나 PCP에서 관리되는 경우, 해당 리스트에서의 위치 struct list_head buddy_list; struct list_head pcp_list; }; /* 페이지의 매핑 플래그는 page-flags.h 참조 */ struct address_space *mapping; // 이 페이지가 속한 파일의 주소 공간 union { pgoff_t index; /* (파일 내에서)매핑 내에서의 오프셋 */ unsigned long share; /* fsdax의 공유 수 */ }; /** * @private: 매핑 전용 불투명 데이터. * 보통 PagePrivate인 경우 buffer_head에 사용됨. * PageSwapCache인 경우 swp_entry_t에 사용됨. * PageBuddy인 경우 버디 시스템의 순서를 나타냄. */ unsigned long private; // 페이지가 특정 페이지와 관련된 비공개 데이터 저장하는 데 사용 };
      페이지가 파일 시스템의 캐시나 익명 메모리로 사용될 때 사용된다. 페이지가 LRU 리스트에 포함된 위치를 나타내는 정보, 페이지가 속한 파일의 매핑 정보, 페이지의 오프셋 등이 포함된다.
  1. 네트워크 스택에 사용되는 page_pool
    1. struct { /* netstack에 의해 사용되는 page_pool */ /** * @pp_magic: 비 page_pool 할당 페이지의 재활용을 방지하기 위한 매직 값. */ unsigned long pp_magic; struct page_pool *pp; unsigned long _pp_mapping_pad; unsigned long dma_addr; atomic_long_t pp_ref_count; };
      페이지가 네트워크 스택에서 사용되는 경우 사용된다. 페이지가 할당된 DMA 주소나 페이지 풀의 참조 카운트가 포함된다.
  1. Compound Page의 Tail Page
    1. struct { unsigned long compound_head; /* 첫 번째 비트가 설정됨 */ };
      비트 값으로 이 페이지가 Tail Page인지 아닌지 나타낸다.
  1. ZONE_DEVICE 페이지
    1. struct { /* ZONE_DEVICE 페이지 */ /** @pgmap: 호스팅 장치 페이지 맵을 가리킴. */ struct dev_pagemap *pgmap; void *zone_device_data; /* * ZONE_DEVICE 전용 페이지는 매핑된 것으로 계산되므로 * 다음 3개의 워드는 페이지가 장치 전용 메모리로 * 마이그레이션되는 동안 소스 익명 또는 페이지 캐시 * 페이지에서 매핑, 인덱스, 전용 필드를 보유함. * ZONE_DEVICE MEMORY_DEVICE_FS_DAX 페이지도 * pmem 지원 DAX 파일이 매핑될 때 매핑, 인덱스 및 * 전용 필드를 사용함. */ };
      페이지가 장치 전용 메모리에 매핑된 경우 사용된다. 장치 페이지 맵에 대한 포인터와 장치 전용 데이터가 포함된다.

4️⃣ 2번째 union

두 번째 union 은 페이지가 사용자 공간에 매핑한 횟수나 페이지의 유형을 나타내는 필드가 포함된다.
union { /* 이 유니온은 4바이트 크기임. */ /* * 페이지가 사용자 공간에 매핑될 수 있는 경우, * 이 페이지가 페이지 테이블에서 참조된 횟수를 인코딩함. */ atomic_t _mapcount; /* * 페이지가 PageSlab이 아니거나 사용자 공간에 매핑될 수 없는 경우, * 여기 저장된 값은 이 페이지가 무엇에 사용되는지를 * 결정하는 데 도움이 될 수 있음. 현재 이곳에 저장된 * 페이지 유형 목록은 page-flags.h를 참조. */ unsigned int page_type; };

5️⃣ 참조 카운트

atomic_t _refcount;
페이지에 대한 참조 횟수. 직접 사용하지 않으며, 참조 카운트와 관련된 작업을 수행할 때는 page_ref.h 에 들어있는 함수를 사용해야 한다. 페이지가 해제될 때 값이 0이 된다.
 

1️⃣ 구성 요소

/** * struct folio - 연속된 바이트 집합을 나타낸다. * @flags: 페이지 플래그와 동일한 역할을 한다. * @lru: 이중 연결 리스트(LRU); 이 folio가 얼마나 최근에 사용되었는지를 추적한다 * @mlock_count: 이 folio가 mlock()에 의해 고정된 횟수를 기록한다. mlock은 페이지를 메모리에서 해제되지 않도록 고정되는 데 사용한다. * @mapping: 이 folio가 속한 파일 또는 익명 메모리의 anon_vma를 가리킨다. 익명 메모리는 파일과 연결되지 않은 메모리를 의미한다. * @index: 파일 내에서 이 folio의 오프셋을 페이지 단위로 나타낸다. 익명 메모리의 경우, mmap의 시작부터의 인덱스를 나타낸다. * @private: 파일 시스템 별로 folio와 관련된 데이터를 저장하기 위해 사용된다. * @swap: folio_test_swapcache()일 경우, swp_entry_t로 사용된다. * @_mapcount: folio_mapcount()를 사용해 이 folio가 사용자 공간에서 매핑된 횟수를 확인한다. 이 멤버에 직접 접근하지 않는다. * @_refcount: folio_ref_count()를 사용해 이 folio에 대한 참조 횟수를 확인한다. 이 멤버에 직접 접근하지 않는다. * @memcg_data: 메모리 제어 그룹과 관련된 데이터를 저장한다. * @virtual: 커널의 다이렉트 맵에서의 가상 주소를 나타낸다. * @_last_cpupid: 이 folio에 마지막으로 접근한 CPU와 프로세스의 ID를 나타낸다. * @_entire_mapcount: folio_entire_mapcount()를 호출한다. 직접 사용하지 않는다. * @_nr_pages_mapped: folio_mapcount()를 호출한다. 직접 사용하지 않는다. * @_pincount: DMA와 같은 용도로 페이지가 고정된 횟수를 나타낸다. (데이터 전송 끝나기 전까지 고정) 직접 접근하지 않고 folio_maybe_dma_pinned()을 사용한다. * @_folio_nr_pages: folio 내 페이지의 수를 나타낸다. 직접 접근하지 않고 folio_nr_pages()를 호출한다. * @_hugetlb_subpool: hugetlb.h의 접근자를 사용한다. 직접 사용하지 않는다. * @_hugetlb_cgroup: hugetlb_cgroup.h의 접근자를 사용한다. 직접 사용하지 않는다. * @_hugetlb_cgroup_rsvd: hugetlb_cgroup.h의 접근자를 사용한다. 직접 사용하지 않는다. * @_hugetlb_hwpoison: raw_hwp_list_head()를 호출한다. 직접 사용하지 않는다. * @_deferred_list: 메모리 압박 시 분할해야 할 folios를 추적한다. * * folio는 물리적으로, 가상적으로, 논리적으로 연속된 바이트 집합을 나타낸다. * folio의 크기는 2의 제곱수이며, 그 크기에 맞게 정렬된다. 최소한 %PAGE_SIZE 이상이다. * 페이지 캐시에 있는 경우, 해당 크기의 제곱수 배수로 파일 오프셋이 지정된다. * folio는 임의의 페이지 오프셋에서 사용자 공간에 매핑될 수 있지만, * 커널 가상 주소는 folio의 크기에 맞게 정렬된다. */

2️⃣ 전반적 구조

struct folio { union { struct { /* ... */ }; struct page page; }; union { struct { /* ... */ }; struct page __page_1; }; union { struct { /* ... */ }; struct page __page_2; }; };
세 개의 union 을 포함하고 있으며, 각각의 union 에는 struct page 와 이를 확장하는 데이터 필드들이 포함된다.
 

3️⃣ 1번째 union

union { struct { unsigned long flags; union { struct list_head lru; struct { void *__filler; unsigned int mlock_count; }; }; struct address_space *mapping; pgoff_t index; union { void *private; swp_entry_t swap; }; atomic_t _mapcount; atomic_t _refcount; #ifdef CONFIG_MEMCG unsigned long memcg_data; #endif #if defined(WANT_PAGE_VIRTUAL) void *virtual; #endif #ifdef LAST_CPUPID_NOT_IN_PAGE_FLAGS int _last_cpupid; #endif }; struct page page; };
첫 번째 unionstruct page 와 호환성을 유지하면서 folio 의 기본적인 속성을 정의한다. struct page 는 리눅스 커널에서 메모리 페이지를 관리하기 위한 기본 단위이기 때문에, folio 는 이를 확장해 더 많은 기능을 제공하기 위해 설계되었다.
 

💡 구조체 멤버가 struct page 와 비슷해 보이는데?

메모리 레이아웃의 일관성 유지를 위해 비슷한 멤버가 존재한다. struct pagefolio 의 성능 차이는 주로 관련 API와 메모리 관리 코드에서 드러난다.
// mm/page_alloc.c line 4530 /* * This is the 'heart' of the zoned buddy allocator. */ struct page *__alloc_pages(gfp_t gfp, unsigned int order, int preferred_nid, nodemask_t *nodemask) { struct page *page; unsigned int alloc_flags = ALLOC_WMARK_LOW; gfp_t alloc_gfp; /* The gfp_t that was actually used for allocation */ struct alloc_context ac = { }; /* * There are several places where we assume that the order value is sane * so bail out early if the request is out of bound. */ if (WARN_ON_ONCE_GFP(order > MAX_PAGE_ORDER, gfp)) return NULL; gfp &= gfp_allowed_mask; /* * Apply scoped allocation constraints. This is mainly about GFP_NOFS * resp. GFP_NOIO which has to be inherited for all allocation requests * from a particular context which has been marked by * memalloc_no{fs,io}_{save,restore}. And PF_MEMALLOC_PIN which ensures * movable zones are not used during allocation. */ gfp = current_gfp_context(gfp); alloc_gfp = gfp; if (!prepare_alloc_pages(gfp, order, preferred_nid, nodemask, &ac, &alloc_gfp, &alloc_flags)) return NULL; /* * Forbid the first pass from falling back to types that fragment * memory until all local zones are considered. */ alloc_flags |= alloc_flags_nofragment(ac.preferred_zoneref->zone, gfp); /* First allocation attempt */ page = get_page_from_freelist(alloc_gfp, order, alloc_flags, &ac); if (likely(page)) goto out; alloc_gfp = gfp; ac.spread_dirty_pages = false; /* * Restore the original nodemask if it was potentially replaced with * &cpuset_current_mems_allowed to optimize the fast-path attempt. */ ac.nodemask = nodemask; page = __alloc_pages_slowpath(alloc_gfp, order, &ac); out: if (memcg_kmem_online() && (gfp & __GFP_ACCOUNT) && page && unlikely(__memcg_kmem_charge_page(page, gfp, order) != 0)) { __free_pages(page, order); page = NULL; } trace_mm_page_alloc(page, order, alloc_gfp, ac.migratetype); kmsan_alloc_page(page, order, alloc_gfp); return page; }
// mm/page_alloc.c line 4598 struct folio *__folio_alloc(gfp_t gfp, unsigned int order, int preferred_nid, nodemask_t *nodemask) { struct page *page = __alloc_pages(gfp | __GFP_COMP, order, preferred_nid, nodemask); return page_rmappable_folio(page); }

 4️⃣ 2번째 union

union { struct { unsigned long _flags_1; // folio의 추가 상태 플래그 저장 unsigned long _head_1; // folio의 추가 헤드 정보 저장 unsigned long _folio_avail; // folio 가용성 나타냄 atomic_t _entire_mapcount; // folio의 전체 페이지 수 추적 atomic_t _nr_pages_mapped; // folio의 매핑된 페이지 수 추적 atomic_t _pincount; // 페이지가 DMA와 같은 용도로 고정된 횟수 추적 #ifdef CONFIG_64BIT unsigned int _folio_nr_pages; // 64비트 시스템에서만, folio 내의 페이지 개수 나타냄 #endif }; struct page __page_1; };
두 번째 unionstruct page 의 레이아웃을 기반으로 folio 가 추가적인 메타데이터를 추적할 수 있도록 확장된 필드다.union 은 여러 페이지를 논리적으로 관리하기 위해 추가적인 상태와 카운터들을 관리한다.
 

 5️⃣ 3번째 union

union { struct { unsigned long _flags_2; // folio의 추가 상태 플래그 저장 unsigned long _head_2; // folio의 추가 헤드 정보 저장 // 메모리와 관련된 서브풀, 제어 그룹, 예약 메모리, 하드웨어 오류 정보를 저장 void *_hugetlb_subpool; void *_hugetlb_cgroup; void *_hugetlb_cgroup_rsvd; void *_hugetlb_hwpoison; }; struct { // 페이지와 관련된 또 다른 상태와 헤드 정보 나타냄 unsigned long _flags_2a; unsigned long _head_2a; struct list_head _deferred_list; // 메모리 압박 시 분할해야 할 folios 추적 }; struct page __page_2; };
세 번째 unionhugetlb 페이지와 같은 특수한 경우를 처리하기 위한 추가적인 필드를 제공한다.
 

 6️⃣ 매크로 정의 FOLIO_MATCH

매크로 정의는 struct pagestruct folio 간의 호환성을 유지하기 위해 사용된다. 이 매크로는 두 구조체의 필드가 동일한 메모리 오프셋에 있는지, 또는 특정 크기만큼 떨어져 있는지를 확인해 구조체 간의 일관성을 보장한다.

6️⃣-1. 첫 번째 FOLIO_MATCH

#define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct page, pg) == offsetof(struct folio, fl))
첫 번째 매크로는 struct pagestruct folio 내에서 동일한 이름의 필드가 같은 메모리 오프셋에 위치하는지 확인한다.
static_assert 로 컴파일 시간에 조건을 확인해 false라면 컴파일 오류를 발생시킨다.
FOLIO_MATCH(flags, flags); FOLIO_MATCH(lru, lru); FOLIO_MATCH(mapping, mapping); FOLIO_MATCH(index, index); FOLIO_MATCH(private, private); FOLIO_MATCH(_mapcount, _mapcount); FOLIO_MATCH(_refcount, _refcount);
flagslru 등등, 필드가 두 구조체에서 같은 위치에 있는지 확인한다.
 

6️⃣-2. 두 번째 FOLIO_MATCH

#define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct folio, fl) == \ offsetof(struct page, pg) + sizeof(struct page))
두 번째 매크로는 struct page 의 크기만큼 떨어진 위치에 struct folio 의 특정 필드가 위치하는지 확인합니다.
FOLIO_MATCH(flags, _flags_1); FOLIO_MATCH(compound_head, _head_1);
예를 들어 flags 필드의 경우, struct folio_flags_1 필드가 struct pageflags 필드가 위치한 곳에서 sizeof(struct page) 만큼 떨어진 곳에 있어야 한다. 이는 foliopage 의 확장된 구조임을 나타낸다.
 

6️⃣-3. 세 번째 FOLIO_MATCH

#define FOLIO_MATCH(pg, fl) \ static_assert(offsetof(struct folio, fl) == \ offsetof(struct page, pg) + 2 * sizeof(struct page))
세 번째 매크로는 struct page 의 두 배 크기만큼 떨어진 위치에 struct folio 의 특정 필드가 위치하는지 확인합니다.
FOLIO_MATCH(flags, _flags_2); FOLIO_MATCH(compound_head, _head_2); FOLIO_MATCH(flags, _flags_2a); FOLIO_MATCH(compound_head, _head_2a);
이는 folio 가 여러 페이지를 다루는 구조체로서, page 의 데이터를 기반으로 확장된 데이터를 저장하기 위한 추가적인 메모리 공간을 필요로 한다는 것을 의미한다.
 

💡 왜 이런 체크가 필요한가? (ChatGPT 질문)

  1. 메모리 레이아웃 일관성: struct foliostruct page 를 확장하여 더 큰 메모리 블록을 관리하기 위해 설계되었습니다. 이때, foliopage의 일부 필드들은 같은 역할을 하지만, folio는 추가적인 필드를 통해 확장된 기능을 제공합니다. 예를 들어, flags, mapping, index와 같은 필드는 struct pagestruct folio 모두에 존재하지만, folio에서는 추가적인 필드가 더 있어야 합니다.
  1. 기존 코드와의 호환성: struct folio가 도입되기 전에는 struct page가 메모리 관리의 기본 단위였습니다. folio는 이를 확장하는 개념이므로, 커널 내의 기존 코드가 page에서 folio로 확장될 때 문제가 발생하지 않도록 해야 합니다. 즉, foliopage의 역할을 그대로 수행할 수 있으면서도 추가적인 기능을 제공해야 하므로, 동일한 필드가 동일한 위치에 있는지 확인하는 것이 중요합니다.
  1. 구조체 연속성 보장: foliopage를 기반으로 하는데, 때로는 pagefolio로 캐스팅하거나 그 반대의 작업이 필요합니다. 이 경우 구조체가 메모리 내에서 동일한 위치에 동일한 필드를 가지고 있는지 확인하는 것이 매우 중요합니다. 그렇지 않으면 캐스팅 과정에서 데이터가 손상되거나 잘못된 위치를 참조하게 되어, 시스템 오류가 발생할 수 있습니다.
 

📎 Reference

댓글

guest