[커널 19차] 3주차

2022.06.04 15:04

리턴 조회 수:215

가상 메모리


컴퓨터의 RAM을 물리 주소로 직접 접근한다면 여러가지 문제가 생긴다:

  1. 물리 메모리보다 더 많은 메모리를 사용할 수 없다.
  2. 한 프로세스가 다른 프로세스의 메모리에 임의로 접근할 수 있다.
  3. 메모리가 1GB정도 있다고 생각하고 프로그램을 짰는데 실제로 512MB밖에 없다면 프로세스가 메모리에 접근할 때 없는 주소일 수 있다.

등등 다양한 제약때문에 현대적인 운영체제들은 프로세스에서 물리 주소가 아닌, 가상의 주소로 접근할 수 있는 다양한 매커니즘을 제공한다. (Paging, Segmentation)
 

UbZuuBqYZ9SNURTMhdHkm9KmCoyjRrB192J9T9aL

 

가상 주소는 어차피 실제 물리 주소가 아니기 때문에 한 프로세스와 다른 프로세스가 같은 주소를 사용하더라도 다른 물리 주소를 가리키면 되므로 문제가 되지 않는다.

 

예를 들어 fork() 수행 이후에 부모 프로세스와 자식 프로세스에서 포인터 값을 출력하면, 해당 포인터의 값은 가상 주소이기 때문에 같지만 실제로는 다른 물리 주소를 가리키기 때문에 부모 프로세스에서 포인터가 가리키는 변수의 값을 바꿔도 그 결과가 자식에게 반영되지 않는다.

 

그리고 가상 주소는 말 그대로 ‘가상’이기 때문에 물리 메모리의 크기와 관계 없이 32비트 프로세서에서는 최대 2^32 = 4GB, 64비트 프로세서에서는 최대 2^64 = 16EB를 사용할 수 있다. (물론 실제로 4GB/16EB 전체를 사용하지는 않을 수 있다. 가상 주소니까 사용하는 만큼만 물리 주소와 연결해서 사용할 수 있다.)
 

 

물리 메모리 자료구조

NUMA (Non-Uniform Memory Access) 개요
NUMA는 1990년대 즈음에 인기를 얻기 시작한 구조로, 메모리 버스의 병목 현상을 해결하기 위한 방법 중 하나이다.

NUMA에서는 CPU 번호와 메모리의 주소 범위에 따라서 접근 지연 시간이 수십 퍼센트 정도 차이가 날 수 있다.

반대로 접근 속도가 균일한 구조는 UMA(Uniform Memory Access)라고 한다.
 

Cg89WJwSy9u5dyM4VTStcHUxshnhHauF2KyE1PPW

현대적인 프로세서들은 메모리와 CPU 사이에 메모리보다 빠른L1, L2, L3 캐시를 둔다.

이때 캐시는 느린 메모리의 지연시간을 줄여줄 뿐만 아니라, 캐시에 데이터가 있다면 캐시에서 가져오면 되므로 메모리의 트래픽을 덜어주는 역할을 한다.

 

 

p--zfdasPxDw6pZ9ObF9Dy3jTAQ7cuIcV8xVpZ2e

 

하지만 캐시의 크기는 매우 작기 때문에, 캐시를 잘 활용하더라도 주기적으로 메모리에서 데이터를 불러와야 한다.

그런데 CPU의 개수가 수십개, 수백, 수천개로 늘어나다보면 메모리의 트래픽이 매우 증가해 메모리가 큰 병목이 될 수 있다.

 

qVF3ezWbZzS6emc1lvkngkuBgFccp8ihz3WfS1nQ

 

따라서 NUMA는 메모리 버스에 계층 구조를 도입해서 CPU에 가까운 (local) 메모리와 먼(remote) 메모리를 구분한다.

이때 CPU와 이에 가까운 메모리(CPU에 대해서 접근 속도가 같은 메모리)의 집합을 NUMA 노드라고 한다.

 

4ThcnahBnKeQ9hYZ2kvpTmXhCG_Mqg7OZbbmR52P

 

4C4VQqSWROgLFu2GewuJuCfZQR5EGEXYOlyzr0rO

 

NUMA에서는 메모리의 접근을 최대한 로컬 노드 내에서 해결하여 메모리 대역폭으로 인한 병목 현상을 완화한다.

이때 이러한 특성 때문에 NUMA 이후의 운영체제들은 최대한 메모리 할당을 로컬 노드에서 해결하거나, 서로 다른 노드간의 태스크 마이그레이션을 지양하고, 메모리가 부족하면 원격 노드에서 할당할지 로컬 노드에서 reclaim 후 할당할지 등을 고려한다.

 

 

Node (pglist_data)

리눅스에서 노드는 위에서 말한 NUMA 노드를 관리하기 위한 자료구조(struct pglist_data)이다. 여기서 노드는 접근 속도가 같은 물리 메모리들의 집합이다.

 

리눅스는 UMA와 NUMA 구조를 모두 지원하기 때문에, UMA도 모든 물리메모리가 하나의 노드 안에 존재한다고 취급한다. UMA 구조는 단일 노드를 contig_page_data 변수에 저장하고 있다.


Zone

노드가 접근 시간에 따라 메모리를 나눈 것이라면, 존은 노드 내에서 주소 범위에 따라서 노드를 나눈 것이다. (따라서 존은 노드마다 1개 이상 존재한다.) 리눅스에는 다양한 종류의 존이 있는데 세 가지만 알아보자.

 

ZONE_DMA는 아주 오래전에 사용하던 ISA 디바이스를 지원하기 위한 용도로, 이러한 디바이스들은 16bit-addressable한 주소들만 접근할 수 있으므로 물리주소가 하위 16MB 이하인 메모리들을 묶어서 하나의 존으로 관리한다.

그리고 ISA 디바이스에서 사용할 페이지를 할당할 때 GFP_DMA를 사용하면 DMA 존에서 메모리를 할당한다.


ZONE_NORMAL은 16MB 이상의 메모리가 위치하는 곳으로, 최대 ~896MB 까지의 물리 메모리가 존재하는 존이다.

ZONE_NORMAL에 존재하는 메모리는 항상 커널이 접근할 수 있다. (다시 말해 이 ZONE_NORMAL의 메모리는 항상 커널의 페이지 테이블에 매핑되어있다.)

여기서 잠깐 32비트 리눅스에서 가상 주소 공간과 물리 주소 공간이 어떻게 나누어지는지 잠시 살펴보자.


rKkBOFtXORSHSIaVa63Sb8eSrRvXgfBx7cwajIEW

 

OKJoxQbiNtLcwpKvfwbMcGStK9EVNA7aqomzJsxm

우선 32비트 가상 주소 공간 중 상위 1GB는 커널이, 하위 3GB는 사용자 프로세스가 사용하는 공간이다.

사용자 프로세스는 하위 3GB 안에서 독립적인 주소 공간을 가지며, 커널 주소 공간은 사용자 프로세스가 무엇이냐에 관계 없이 모두 같다.

그리고 커널의 페이지 테이블은 (사용자 공간은 어떤 프로세스에 따라 다르므로) 커널의 주소 공간인 상위 1GB만을 매핑한다.

 

하지만 현실적으로 1GB 전체를 매핑할 수는 없는데, 왜냐하면 가상 주소 공간의 일부는 vmalloc, kmap, fixmap, mem_map등에 필요하기 때문이다.

따라서 x86_32에서 현실적으로 모든 물리 메모리가 커널의 페이지 테이블에서 매핑될 수 없으므로 ZONE_NORMAL의 크기는 최대 896MB이다.

 

ZONE_HIGHMEM은 커널이 직접 접근할 수 없는 물리 메모리가 존재하는 존이다. ZONE_HIGHMEM 상의 메모리는 사용자 프로세스가 사용하거나, 커널이 kmap, vmap과 같은 특수한 API로 매핑을 한 후에 사용할 수 있다.

 

 

 

Page Frame

 페이지 프레임(Page Frame)은 물리 메모리의 최소 단위를 뜻하고 페이지 프레임은 struct page라는 이름의 구조체에 의해 관리된다. 시스템 내에 모든 물리 메모리는 리눅스에 의해 접근이 가능해야 하기 때문에 모든 페이지 프레임은 하나의 페이지 구조체를 갖고 있다. 페이지 구조체는 mem_map이라는 전역 배열을 통해 볼 수 있으며 이는 시스템이 부팅되는 순간에 물리 메모리에 위치한다.

 

 위에 나와있는 용어들과 함께 정리해 보자면, 페이지 프레임이 존을 구성하고 하나 이상의 zone이 노드를 구성한다. 

 

 

Internal & External Fragmentation

내부 단편화: 요청된 할당 크기보다 더 큰 메모리 블럭을 할당해서 할당된 블럭 내의 메모리가 낭비되는 문제.

슬랩은 이 문제를 페이지 프레임 내부를 잘개 쪼갠 후 할당해서 해결하고자 한다.

 

 

외부 단편화: 가용 메모리의 총 양은 많지만 메모리 블럭이 쪼개져서 큰 메모리 블럭을 할당하기가 어려워지는 문제. 버디할당자는 메모리가 해제된 후 메모리 블럭을 최대한 합쳐서 이 문제를 해결하고자 한다.

 

 

Buddy & Slab

 모든 프로세스가 페이지 프레임만큼 메모리 할당을 요청하면 얼마나 좋을까?

내외부적으로 메모리가 단편화되는 일은 없을 것이다.

하지만 이는 항상 가능한 일이 아니기에 리눅스 개발자들은 단편화를 막을 두 가지 방법을 고안해냈다.

외부 단편화를 최소화 하기 위한 버디 할당자(Buddy allocator)와 내부 단편화를 최소화 하기 위한 슬랩 할당자(Slab allocator)가 그 주인공이다.

 

 struct zone 소스 코드를 보면 free_area 라는 구조체를 MAX_ORDER(11)의 갯수만큼 가지고 있다.

이는 zone 안에 있는 페이지 프레임을 관리하기 위한 구조체인데 예를 들어 free_area[0]에는 2의 0승, 1개의 페이지 프레임이 double linked list로 관리되고 있으며, free_area[3]에는 2의 2승인 4개의 연속된 페이지 프레임이 관리된다.

 

 현재는 lazy buddy라는 버디를 사용하고 있지만, 커널 2.6.19버전 이전에는 비트맵을 사용한 버디를 채택했다.

비트맵 버디를 간략하게 설명하자면 사용 중인 페이지 프레임의 비트맵 상태를 1로 바꿔 인접해있는 두 페이지 프레임의 비트맵 값을 이용해 상위 free_area의 order에서 관리한다.(더 자세한 내용은 p103, p104를 참고해 주세요.)

 

 비트맵 버디 할당자에는 큰 문제점이 있었다. 페이지 프레임이 할당/해제될 때마다 비트맵 값을 바꿔줘야 할 뿐더러 상위 오더도 항상 바꿔줘야 했었다.

페이지 프레임이 반복적으로 할당/해제를 할 때는 오버헤드가 크게 발생했다. 이를 방지하기 위해 Lazy buddy는 페이지 프레임이 해제가 되어도 병합 작업을 최대한 미룬다(결정은 watermark 값을 보고 판단한다).

만약 아직 물리 메모리가 많이 남았다면 최대한 병합을 미루고, 그렇지 않다면 페이지 프레임을 병합시켜서 공간을 만든다.

 

 버디 할당자는 페이지 프레임 크기보다 큰 공간을 할당할 때의 메모리 단편화 최소화 방법이고 슬랩은 그 반대이다.

페이지 프레임이 4KB일 때, 한 프로세스가 64byte를 요청하면 꽤나 낭패다. 이를 도와주는 것이 슬랩이다.
 

 

Ad3sVUo1phMeXAsWJxMIGp8Ji5PR4eEdB0hnuZtk

 슬랩 할당자는 페이지 프레임을 캐시처럼 사용한다고 보면 된다.

일단 자주 사용하는 구조체를 위해 페이지 프레임을 다수 준비한다. 그리고 페이지 프레임(슬랩)을 구조체의 크기만큼 나눈다(슬랩 오브젝트).

각 캐시에는 한 개 이상의 페이지 프레임이 존재하며 만약 캐시가 더 많은 구조체를 필요로 할 시 페이지 프레임을 더 할당받는다.

 

 슬랩 할당자는 사용자가 쓰고 있던 구조체를 해제했을 때 구태여 해제해서 버디 할당자로 보내는 대신에 갖고 있다가 같은 크기의 구조체가 쓰일 때 다시 할당되는 캐시의 역할을 해낸다.

이러한 캐시들은 kmem_cache라는 자료 구조로 관리되며 kmem_cache 구조체의 메타 데이터는 cache_cache에 저장된다.
 

 

SLUB & SLOB

SLAB의 가장 큰 문제점은 우선 무겁다는 점이다.

최대한 캐시 친화적으로 할당하기 위해서 슬랩 캐시마다 per-cpu queue가 존재하며, NUMA 노드가 많을 때 메모리 사용량이 매우 많아집니다. (공간 복잡도가 O(N^2))

 

SLOB
그래서 2005년 즈음에는 좀더 lightweight한 슬랩 할당자인 SLOB이 나왔습니다.

하지만 SLOB은 address-ordered first fit allocator라서 메모리 오버헤드는 적지만 성능이 좀 달립니다.

임베디드용으로 탄생한 슬랩 할당자입니다.

 

SLUB
2006-7년 즈음에는 SLUB (The Unqueued Slab Allocator)이 탄생했고 SLAB보다 메모리 오버헤드가 적으면서도 SLOB처럼 성능에 큰 문제가 없는 슬랩 할당자가 탄생했습니다.
v2.6.23부터는 디폴트로 SLUB을 사용하며 많은 컴퓨터 시스템들이 SLUB을 사용합니다. 적어도 제 PC, 스마트폰은 SLUB을 사용합니다.

 

현재 커널에는 SLAB, SLUB, SLOB 3개의 슬랩 할당자가 있습니다.

여담이지만, 어떤 하나의 서브시스템에 대해서 3가지 다른 구현이 존재한다는 것은 그닥 즐거운 일이 아닙니다.

새로운 feature를 추가할 때마다 3가지 구현을 따로 해야하며 관리하는 비용이 너무 큽니다. 아마 이런 일이 왠만하면 다시 일어나지 않을 겁니다.

 

지금 SLAB과 SLOB을 버리지 못하는 이유는 SLUB이 모든 상황에서 다른 두 할당자보다 뛰어나다는 데이터가 없기 때문입니다.

 


프로세스 주소 공간: mm_struct & vm_area_struct

*여기서 언급하는 ‘프로세스’는 ‘태스크(task_struct)’입니다.

 

 

앞에서 언급했듯 프로세스는 독자적인 가상 주소 공간을 갖는다.

32비트 리눅스에서는 하위 3GB를 프로세스가 사용하며, x86_64의 경우에는 하위 128TB 또는 하위 64PB를 프로세스가 사용한다.

 

프로세스의 가상 주소 공간은 프로세스별로 존재한다.

따라서 커널은 프로세스에 대한 가상 메모리 정보를 관리해야되는데 이는 mm_struct로 관리된다.

mm_struct에는 프로세스의 코드/데이터/힙/arg/스택 등의 영역의 시작과 끝 범위, 프로세스가 사용하는 최상위 페이지 테이블 (pgd), 그리고 가상 메모리를 공간을 이루는 ‘region (vma)’ 등이 기록되어있다.

 

 

w9Q0_Ka2KiCT9MMEGsFITK3Sd4_yP11LvHkgF0ml

 

 

mm_struct는 프로세스에 대한 가상 메모리를 나타낸다고 했다.

그런데 프로세스는 다양한 종류의 가상 메모리(스택, 힙, 열린 파일, 데이터 등)를 갖기 때문에 mm_struct만으로 모든 영역을 나타내기는 어려움이 있다.

 

따라서 mm_struct는 하나의 가상 메모리 공간을 vm_area_struct로 나타낸다.

하나의 가상 메모리 공간 (vm_area_struct)는 같은 파일, 같은 권한의 연속된 가상 메모리 영역을 관리한다.

vma는 시간이 지남에 따라서 여러 개로 나뉘거나 다시 하나로 합쳐질 수 있다.
 

EyeZG1Az3Oh34c5INBUVP9LsE3XqeIw0jua0oBuK

 

이 때 mm_struct에서 vm_area_struct는 효율적인 관리를 위해 red-black tree(mm_rb)와 linked list (mmap) 두 가지 방법으로 동시에 관리된다.

 

이 때 가장 최근에 사용한 vm_area_struct는 mmap_cache에 저장되며, 찾는 vm_area_struct가 mmap_cache와 다른 경우 mm_rb에서 탐색한다.

vm_area_struct에 접근할 때는 mmap_sem 세마포어를 획득해야 하며, 수정을 해야한다면 page_table_lock 스핀락까지 획득해야한다.

 

vm_area_struct는 파일을 참조할 수도 있고 아닐 수도 있다.

예를 들어 코드 영역은 실행 파일이 존재하기 때문에 파일을 참조하며 (file-backed), 스택은 이에 해당하는 파일이 존재하지 않으므로 익명(anonymous)이다.
 

Vregxz7hQWDAk9ngKulYa7DviePYXdjFgyVQGtcP

 

vm_area_struct가 파일을 참조하는 경우에는 vm_file 필드와 vm_offset 필드로, 이 vma가 어떤 파일의 몇 번째 오프셋에서 시작하는지를 나타낸다. 

그리고 VMA에 대한 권한은 vm_flags로 나타낸다. (코드 영역을 쓰기가 불가능하고, 데이터 영역은 실행이 불가능하는 등).


페이징: 가상 주소 -> 물리 주소 변환

실제 물리 메모리의 크기와 관계 없이 프로세스가 가상 주소 공간 전체를 사용할 수 있도록 하는 기술을 페이징이라고 한다.

이 때 메모리는 페이지 단위로 관리된다.

 

페이징을 사용하면 실제 물리 주소가 아닌 가상 주소로 메모리에 접근하기 때문에 가상 주소를 물리 주소로 변환하는 과정이 필요하다.

이때 변환 과정에서 메모리 접근이 유효한지, 권한이 없는 메모리에 접근하는 것이 아닌지 등을 체크해서 프로세스간 접근 보호 매커니즘을 제공할 수 있다. (물론 페이지 단위로)

 

참고로 이 주소 변환은 운영체제가 소프트웨어적으로 처리하는 것이 아니라 CPU가 하드웨어적으로 처리한다.

CPU의 MMU(Memory Management Unit)가 이 주소 변환을 담당한다.

 

만약 CPU가 페이지 테이블을 탐색하면서 페이지가 메모리 상에 존재하지 않거나, 권한이 없는 등의 문제가 발생하면 Page Fault를 발생시켜 운영체제가 이를 처리할 수 있도록 한다.
 

 

One-Level Paging

페이징을 사용하여 가상 주소를 사용하므로, 우리는 가상 주소를 물리 주소로 변환할 방법이 필요하다.

여기서 잠깐 상상력을 발휘해보자. 가상 주소를 물리 주소로 변환하는 데에 가장 적절한 자료구조가 뭘까? (정답은 없다.)

 

가장 적절한 자료구조는 모르겠지만 가장 간단한 구현은 그냥 일차원 배열로 저장하는 것이다.

예를 들어서 페이지 크기가 8KB고 16비트 프로세서라서 최대 64KB의 메모리를 사용한다고 가정해보자.

그러면 최대 8개의 페이지를 사용할 수 있으므로 프로세스별로 길이가 8인 포인터 배열을 저장하면 가상 주소를 물리 주소로 변환할 수 있다.

 

6oxOidA8sYaQNF8T3H6H3XAHUldk3FmtKlmQWwAW

앞에서 상상력을 발휘해보자고 했지만 실제로 PDP-11이라는 아주 오래된 아키텍처가 유사한 방식으로 페이징을 구현했다.

포인터 배열은 아니지만 8쌍의 레지스터가 8개의 페이지의 물리 주소를 가리키는 데에 사용되었다.

아주 원시적인 페이지 테이블이라고 할 수 있다.

 

위의 사진에서 간단하게 보여주듯, PDP-11에서는 어떤 가상 주소에 접근할 때 8개의 엔트리를(레지스터들) 가진 페이지 테이블에서 찾은 후 해당 엔트리가 가리키는 물리 주소를 찾는다.

이 때 페이지 크기가 8KB이므로 16비트 주소 중 하위 13비트는 페이지 내에서의 오프셋을 나타내며, 나머지 3비트는 페이지 테이블에서의 인덱스로 사용된다.

 


Multi-Level Paging

하지만 위에서 설명한 방식을 32비트/64비트 프로세서에서 그대로 사용하면 페이지 테이블의 크기가 걷잡을 수 없이 커진다.

예를 들어서 페이지 크기가 4KB인 32비트 프로세서에서는 2^20개의 페이지가 존재하고, 페이지마다 4바이트 엔트리가 필요하므로 총 4MB의 페이지 테이블이 프로세스별로 필요하다.

그런데 프로세스가 가상 주소 공간 4GB 전체를 사용하지는 않으므로 아주 큰 공간이 낭비된다.

 

따라서 현대적인 프로세서들은 앞서 살펴본 1단계 페이징이 아닌 다단계 페이징을 사용한다. 
32비트 인텔 CPU의 경우에는 일반적으로 2단계 페이징을, 64비트 프로세서는 4단계 또는 5단계 페이징을 지원한다.
 

AiAqGpb25loxNlqj6FX_J1VG-Kav3rWnv8Cp5_9A

 

위 사진에서는 32비트 인텔 CPU의 2단계 페이징을 보여준다.

만약 32비트에서도 1단계 페이징을 사용했다면 비트 31-12가 페이지 테이블의 인덱스로 사용되었겠지만, 여기선 페이지 테이블을 두 단계로 나누어서 비트 31-22를 1단계 페이지 테이블의 인덱스, 비트 21-12를 2단계 페이지 테이블의 인덱스로 사용한다. (나머지 비트 11-0은 페이지 내에서의 오프셋)

 

KSpgdPX7JjoxnpgZLIeJILucS0ISEt5Ry6JsIear

64비트 인텔 프로세서에서의 4단계 페이징도 크게 다르지 않다.

차이점은 페이지 테이블의 단계 수가 늘어났고, 32비트에서는 페이지 테이블당 2^10 = 1024개의 엔트리가 존재했지만 64비트에서는 2^9 = 512개로 바뀌었다.

 

OgTkpyVxRgs4wqNIvV6YzqST_ddiIX4is1Dv-QkX

 

위 사진은 4단계 페이징에서 페이지 테이블을 어떻게 탐색하는지 잘 보여준다.

첫 번째 (최상위) 페이지 테이블은 PGD이며, pgd_t는 첫 번째 페이지 테이블의 엔트리를 나타낸다.

그 후로 PUD, PMD, PTE 순으로 페이지 테이블이 존재한다.

 

여기서 한 가지 짚고 넘어갈 점은, 아키텍처 지원하는 페이징 단계에 관계 없이 리눅스에서는 모두 4단계 페이징을 사용하는 것처럼 취급한다.

단, 아키텍처에 따라서 페이지 테이블 몇 개가 무시된다.

예를 들어서 2단계 페이징에서는 PUD, PMD가 무시되고 PGD, PTE만 사용한다.
 

zJ_Q1RrbAQqIwwSEcfK4_TlzGeJOFEYEQU9FDf6q

 

follow_page_pte() 함수는 커널에서 페이지 테이블을 탐색하여 PTE 테이블 엔트리(pte_t 타입)가 가리키는 페이지를 찾아서 반환한다.

 


주소 변환의 비용

현대적인 운영체제들은 모두 가상 메모리 기법을 사용한다. 가상 메모리 기법은 프로그래머가 물리 메모리를 신경쓰지 않도록 해주는 매우 편리한 기법이지만 주소 변환을 해야한다는 단점이 있다.

 

페이지 테이블이 RAM에 존재한다는 점을 고려했을 때 페이지 테이블의 탐색은 수백 나노초가 걸린다.

DRAM의 latency가 약 50~100ns니까 4단계 페이징에서는 주소 변환을 하는 데에 200~400ns가 걸린다.

물론 페이지 테이블 엔트리도 CPU 캐시의 덕을 보기 때문에 조금 더 줄어들 수는 있지만, 그럼에도 변환하는 비용이 저렴하지 않다.

 

따라서 프로세서에는 보통  TLB (Translation Lookaside Buffer)라는 캐시가 존재한다.

TLB는 앞에서 살펴본 L1/L2/L3 캐시와 유사하게 “어떤 가상 주소가 어떤 물리 주소로 변환되는지”를 저장해둔다.

 


가상으로 연속된 메모리 할당

slab, buddy는 모두 물리적으로 연속된 메모리를 할당한다.

구체적으로는 direct mapping된 영역의 메모리만을 할당한다. 사용하는 주소는 자체는 가상 주소이지만 물리적으로 연속되어있다.

이와 다르게 vmalloc()/vfree() 함수를 사용하면, 물리적으로는 연속되어있지 않지만 가상으로 연속된 메모리를 할당할 수 있다.
 

 

 

[진행 내용 및 질의 응답 내용]

 

NUMA 구조

ToaTwbwDcgneKjoCdKd6sgUEmcm3UceqBdGumwYh

 

ZONE

lVbuNTtGsyf_-k3zUbRCuUyO0C0WAIfVMEIGjvTM

  • 관련 링크 : Zones
  • 어플리케이션은 하이메모리부터, 커널은 로우 메모리부터 할당 시작
  • ZONE은 물리로 나눈거라 그걸 어떻게 같은 주소공간에 맵핑하는지는 다른 문제

 

ZONE_MOVABLE

  • 내가 페이지를 옮길 수 있다
  • 다른 곳으로 옮기더라도 문제가 없이 쓸 수 있다는 이런 거에 대한 메모리 속성
  • ZONE_MOVABLE is used for the allocation of pages that may be reclaimed or moved by the page migration subsystem.  Note that allocations like PTEs-from-HighMem still use the HighMem zone if it exists, and the Normal zone if it does not. 
  • (ZONE_MOVABLE은 page migration subsystem들에 의해 옮겨지거나 reclaim질 수 있는 페이지들의 할당을 위해 쓰인다. PTEs-from HighMem 같은 할당들은 만약 있으면 HighMem zone을 이용하고, 없으면 Normal zone을 쓴다는 점을 기억해야 한다. )
  • ONE_MOVABLE  => ZONE_MOVABLE
  • CMA memory part of a kernel zone essentially behaves like memory in  ZONE_MOVABLE and similar considerations apply, especially when combining CMA with ZONE_MOVABLE. 
  • (kernel zone의 CMA memory part는 기본적으로 ZONE_MOVABLE 안의 메모리처럼 행동하고, 특히 CMA를 ZONE_MOVABLE과 합칠 때 비슷한 considerations가 적용된다)
  • Without ZONE_MOVABLE, there is absolutely no guarantee whether a memory block can be offlined successfully. 
  • ( ZONE_MOVABLE 없이는 memory block이 성공적으로 offlined될 수 있을 지 없을지 보장할 수 없다)
  • 물리적으로 연속적인 메모리 할당의 성공률을 높이기 위해서 만약 외부 단편화가 심하면 사용중인 메모리를 다른 곳으로 옮겨서 물리적으로 연속적인 메모리의 확보가 쉬워지도록 부트 파라미터로 movable zone크기 정해줄 수 있음 
     

 

 

Q. CMA는 커널에서도 사용가능한 메모리 영역인건가요? 

A.

yes, ex. framebuffer

 

Q. ZONE_HighMem은 왜 따로 나누어 놓았나요?

A.
가상 주소공간 4GB, 커널 1GB 
-> 커널에서 가상 주소공간 다 접근은 불가능, 한계가 있음

 

Q. 그럼 32bit와 64bit는 다른가요?

A.​
64bit는 주소공간이 차고 넘쳐서 커널에서 모든 주소공간에 접근 가능하여 highmem이 필요 없습니다

 

Q. zone이라는게 직접 physical로 접근 가능한데, page table 개념이 필요 없는건가요?

A.​
커널에서도 필요합니다.
커널에서도 똑같이 참조해서 접근하는 건 맞는데, 모든 물리 메모리에 접근할 수 없으니까 접근 가능과 불가능한 애들 구분해야 함
커널과 사용자 프로세스랑 관계 없이 가상주소가 있으면 page table 참조해서 데이터 읽거나 쓰거나 하는데(그거 자체는 똑같은데), 커널에서는 모든 물리주소에 접근할 수 없어서 구분한것
1:1 로 mapping하면 좋은데 주소공간이 모자라니까 그럴 수 없음


Q. 어떤 코드는 중요하니까 896mb 이하, 어떤 코드는 안 중요해서 896mb 이상? 어떻게 구분?

A.​
어플리케이션에서 필요하는 건 high memory부터 할당(언제든지 갈아치워질 수 있음)
low memory에서 할당(없어지면 안되는 것)

 

Q. 0~3gb 유저공간 3~4gb 커널공간

A.​
3/1, 2/2 
후자의 경우 적은 메모리가 어플리케이션에 사용될 거고, 스왑 일어날 가능성도 있음


Q. mem_map은 어디에 위치해있나요?

A.
커널 공간에 메모리의 크기에 비례한만큼 있습니다.
ZONE_NORMAL에 위치해있습니다.
시스템의 메모리는 page frame이라는 고정된 chunk들로 이루어져 있다.각각의 physical page frame들은 struct page라는 구조체로 표현되며, 이 모든 구조체들은 mem_map이라는 배열에 저장된다.(mem_map은 보통 ZONE_NORMAL 시작부분이나 지정된 커널 로딩 영역 뒤에 저장된다)
출처: https://codecat.tistory.com/entry/Virtual-Linux-Manager-정리-1 [code cat:티스토리]

 

Q. 왜 order의 최대 크기가 11일까요? 왜 페이지 할당자에서는 4MB 이상의 메모리를 할당할 수 없도록 되어있을까요?

A.

The kernel memory allocator divides physically contiguous memory blocks into "zones", where each zone is a power of two number of pages.  

This option selects the largest power of two that the kernel keeps in the memory allocator.  

If you need to allocate very large blocks of physically contiguous memory, then you may need to increase this value.

This config option is actually maximum order plus one.

For example, a value of 11 means that the largest free memory block is 2^10 pages. We make sure that we can allocate upto a HugePage size for each configuration.


Hence we have :
MAX_ORDER = (PMD_SHIFT - PAGE_SHIFT) + 1 => PAGE_SHIFT - 2
However for 4K, we choose a higher default value, 11 as opposed to 10, giving us 4M allocations matching the default size used by generic code.


( kernel memory allocator는 물리적으로 인접한 memory block들을 zone으로 나눈다.

각각의 zone은 2^n(n은 page 숫자)이다. 이 옵션은 kernel이 memory allocator 안에 유지하고 있는 가장 큰 2^n을 선택한다.

만약 물리적으로 인접한 메모리에서 엄청 큰 block들을 할당해야한다면, 이 값을 증가 시켜야할 수도 있다.

이 config option은 사실 최대 order에 1을 더한 값이다.

예를 들어, 11이라는 값은 가장 큰 free memory block이 2^10 페이지임을 의미한다.

각 configuration에 대해 HugePage size까지 할당할 수 있다는 점을 확실히 하고 넘어가자. 


그래서 다음과 같은 식이 성립한다:
MAX_ORDER = (PMD_SHIFT - PAGE_SHIFT) + 1 => PAGE_SHIFT - 2
하지만 4K의 경우, 더 큰 default value(10이 아니라 11)를 선택하고, 이건 generic code에 의해 사용되는 default size가 매치되는(?) 4M allocations를 준다(???) )

 

Q. buddy free_area 구조체에서 bitmap에서 map이 왜 포인터인가요?
A.

어드레스 포인터, 지금은 저렇게 안씀. 옛날 방식

 

Q. ARM도 highmem은 똑같이 쓰겠죠?
A. 네

 

Q. kmem_cache와 cache의 연관성?
A.

하나의 cache에 kmem_cache이 있다.

slab memory 할당자 관련 자료

 

Q. cache 사이즈 설정
A.

cache 생성한 다음에 kmem_cache alloc으로 그 cache에서 할당
kmalloc() 쓰면 커널 내부적으로 알아서 사이즈를 맞춰서 할당하는 것으로 알고 있습니다

 

Q. 혹시 kmem_cache 와 kmalloc(을 page  사이즈 보다 작게 생성할떄 ) 성능 비교해보신분들 계실까요?(kmem_cache => slab alloc 사용 , kmalloc => slob 사용 케이스)
A.

slab allocator가 성능 이점이 더 있을 거 같긴한데 얼마만큼의 이점이 있을지
상황마다 다를듯함
cache_cache는 cache를 할당하기 위한 cache

 

Q. kernel thread에서는 왜 mm이 없는지
A.

kernel threads runs only in kernel address space.

They don't have access to user space virtual memory and they only use kernel space memory address after PAGE_OFFSET.

So (struct task_struct *)->mm field in the process descriptor is NULL.

You would require to dynamically allocate memory within your kernel thread if required.


관련 내용 링크


커널 스레드에는 프로세스 주소 공간이 없으므로 관련 메모리 설명자가 없습니다.

따라서 커널 스레드 프로세스 설명자의 mm 필드는 NULL입니다.

이것은 거의 사용자 컨텍스트가 없는 커널 스레드 프로세스의 정의입니다.


커널 스레드는 사용자 공간 메모리(누구에게 액세스할까요?)에 액세스하지 않기 때문에 이러한 주소 공간 부족은 문제가 되지 않습니다.

커널 스레드는 사용자 공간에 페이지가 없기 때문에 자체 메모리 설명자와 페이지 테이블을 가질 자격이 없습니다(페이지 테이블은 이 장의 뒷부분에서 설명합니다).

그럼에도 불구하고 커널 스레드는 커널 메모리에 액세스하기 위해서라도 페이지 테이블과 같은 일부 데이터가 필요합니다.

커널 스레드에 필요한 데이터를 제공하기 위해 메모리 설명자(Memory descriptor)와 페이지 테이블에서 메모리를 낭비하거나 커널 스레드가 실행되기 시작할 때마다 새 주소 공간으로 전환하기 위해 프로세서 주기를 낭비하지 않고 커널 스레드는 이전에 실행된 작업의 메모리 설명자를 사용합니다.

프로세스가 스케쥴 될 때마다 프로세스의 mm 필드가 참조하는 프로세스 주소 공간이 로드됩니다.

그런 다음 프로세스 설명자의 active_mm 필드가 새 주소 공간을 참조하도록 업데이트됩니다.

커널 스레드에는 주소 공간이 없고 mm은 NULL입니다.

따라서 커널 스레드가 예약될 때 커널은 mm가 NULL임을 인지하고 이전 프로세스의 주소 공간을 로드한 상태로 유지합니다.

그런 다음 커널은 이전 프로세스의 메모리 설명자를 참조하도록 커널 스레드의 프로세스 설명자의 active_mm 필드를 업데이트합니다.

그런 다음 커널 스레드는 필요에 따라 이전 프로세스의 페이지 테이블을 사용할 수 있습니다.

커널 스레드는 사용자 공간 메모리에 액세스하지 않기 때문에 모든 프로세스에서 동일한 커널 메모리와 관련된 주소 공간의 정보만 사용합니다.

 

Q. 커널 스레드는 프로세스의 mm을 active_mm으로 빌려서 사용한다 정도로 이해하면 될까요 ?

A.  아니요
active_mm은 전에 실행했던 프로세스의 mm의 값을 가지고 있다고 합니다(왜인지는 모르겠습니다)


https://docs.kernel.org/vm/active_mm.html


we have "real address spaces" and "anonymous address spaces".

The difference is that an anonymous address space doesn't care about the user-level page tables at all, so when we do a context switch into a anonymous address space we just leave the previous address space active.


address space는 "real address space"와 "anonymous address space"가 있다.

anonymous address space는 사용자 페이지 테이블을 전혀 신경쓰지 않는다. 따라서 anonymous address space로 문맥 전환을 할 때는 이전에 쓰던 address space를 그대로 쓸 수 있다.

 


The obvious use for a "anonymous address space" is any thread that doesn't need any user mappings - all kernel threads basically fall into  this category, but even "real" threads can temporarily say that for some amount of time they are not going to be interested in user space, and that the scheduler might as well try to avoid wasting time on switching the VM state around. Currently only the old-style bdflush sync does that.


“anonymous address space”는 사용자 공간을 사용하지 않는 스레드들을 의미하며 기본적으로 커널 스레드는 이에 해당한다. 하지만 “real” 스레드도 임시적으로 사용자 공간을 사용하지 않을 때 “anonymous address space”라고 할 수 있으며 스케줄러는 문맥 전환을 피하려고 한다. (현재로서 - (1999년 기준) - old-style bdflush만 그러한 방식으로 동작한다.)

 


"tsk->mm" points to the "real address space". For an anonymous process, tsk->mm will be NULL, for the logical reason that an anonymous process really doesn't _have_ a real address space at all.
"tsk->mm"은 "real address space"를 가리키며 anonymous process는 tsk->mm이 널이다. (anonymous process는 real address space를 갖지 않으므로)


however, we obviously need to keep track of which address space we "stole" for such an anonymous user. For that, we have "tsk->active_mm", which shows what the currently active address space is.

The rule is that for a process with a real address space (ie tsk->mm is non-NULL) the active_mm obviously always has to be the same as the real one.


하지만 우리는 anonymous user가 real 스레드로부터 “훔친” address space가 무엇인지 알아야 하며 그것이 바로 tsk->active_mm(현재 사용중인 주소 공간)에 저장된다. real address space를 갖는 프로세스는 tsk->mm이 널이어서는 안되며 active_mm은 항상 mm과 같아야 한다.

 

What’s an anonymous process in Linux?


https://www.kernel.org/doc/gorman/html/understand/understand007.html#sec:%20Process%20Address%20Space%20Descriptor


unique mm_struct is not needed for kernel threads as they will never page fault or access the userspace portion.

The only exception is page faulting within the vmalloc space.

The page fault handling code treats this as a special case and updates the current page table with information in the the master page table.

As a mm_struct is not needed for kernel threads, the task_struct→mm field for kernel threads is always NULL.

For some tasks such as the boot idle task, the mm_struct is never setup but for kernel threads, a call to daemonize() will call exit_mm() to decrement the usage counter.


커널 스레드에게는 유니크한 mm_struct가 필요하지 않다. (사용자 공간을 접근하거나 페이지 폴트가 나지 않으므로)

따라서 커널 스레드는 task_struct->mm이 항상 널이다.

아이들 태스크 같은 경우에는 mm_struct가 전혀 없지만 커널 스레드는 damonize()를 호출하면 exit_mmap() 함수가 mm_struct의 카운터를 감소시킨다.

 


As TLB flushes are extremely expensive, especially with architectures such as the PPC, a technique called lazy TLB is employed which avoids unnecessary TLB flushes by processes which do not access the userspace page tables as the kernel portion of the address space is always visible.

The call to switch_mm(), which results in a TLB flush, is avoided by “borrowing” the mm_struct used by the previous task and placing it in task_struct→active_mm. This technique has made large improvements to context switches times.


TLB 플러시는 매우 비싸기 때문에 파워피씨 같은 아키텍처는 lazy tlb flushing 기술을 사용한다.

lazy tlb flushing은 사용자 주소 공간에 접근하지 않는 프로세스들(커널 스레드)이 불필요하게 tlb flushing을 하지 않도록 한다. tlb flushing을 하는 switch_mm() 함수는 사용자 주소 공간에 접근하지 않는 커널 스레드가 스케줄링 되는 경우 이전의 태스크가 사용하던 mm_struct를 task_struct→active_mm으로 빌려서 최대한 피한다.

 

 

Q. 페이지 테이블 엔트리는 언제 생성이 되나요?
A.  프로세스 생성시(?)

 

Q. 페이지 폴트 핸들러는 하드웨어 인터럽트인가요?
A.  

CPU가 발생시키는 하드웨어 인터럽트입니다.
페이지 테이블 워크를 진행한 후 페이지 테이블 엔트리가 없으면 인터럽트를 일으킵니다.

 

Q. mmu
A.  

cache 역할을 하는, 항상 page table에 접근하기 속도가 느리니까 mmu를 통해서 mapping하는 게 더 빠르다
MMU = 캐시는 아니고, MMU가 주소 변환을 빨리 하기 위해서 TLB를 사용한다
TLB: mmu안에 있는 캐시
TLB가 없어도 주소 변환은 가능 다만 TLB 덕분에 성능 향상    
mmu를 끈다: 가상주소와 물리주소의 mapping 관계 포기

MMU가 없으면 물리 주소로 바로 접근해서 사용
mmu의 용도: paging -> paging한 결과를 tlb에 저장해놓으면 굳이 paging 없이 바로 찾을 수 있음(cpu cache의 역할과 같음)
MMU가 페이지 테이블을 탐색해서 변환하려면 페이지 테이블을 4번이나 참조해야하는데 (4단계 페이징에서) 그것보단 TLB에서 읽는게 더 빠르니까 TLB를 최대한 활용

 

Q. vmalloc과 vfree란?
A.  

kmalloc, buddy 할당자는 커널 가상 주소 공간에서 일대일로 매핑된 direct mapping 영역의 주소를 반환
근데 vmalloc은 물리적으로 연속되지 않은 공간을 가상 주소가 연속되도록 페이지 테이블을 수정해서 할당
*물리적으로 연속된 메모리는 접근 속도가 빠름*

 

Q. 프로세스 생성 과정 및 물리/가상메모리 매핑 과정
A.  

fork()로 프로세스 복제

-> exec() 호출 -> 기존의 mm_struct는 반환 (참조 카운트--)
-> 새로운 mm_struct 할당
-> mm_struct->pgd에 텅 빈 PGD 테이블 할당
-> 실행 파일(i.e. ELF 포맷으로 된)을 읽은 후 프로세스 실행에 필요한 vm_area_struct를 mm_struct에 삽입 (스택을 위한 vm_area_struct 포함)
-> 프로세스를 실행
-> 처음엔 페이지 테이블에 present한 페이지가 없으므로 무조건 페이지 폴트가 남
-> 페이지 폴트 핸들러가 폴트가 난 주소를 보고 어느 vm_area_struct에서 폴트가 났는지 확인
-> 접근하려는 페이지 프레임이 메모리 상에 없으므로 새로운 페이지 프레임을 버디 할당자에서 할당
-> 이 때 할당할 노드는 numa policy에 따라서 선택됨.
-> 노드를 선택하면 노드 내의 highmem->normal->dma존 순으로 사용할 수 있는 페이지 프레임을 찾아보면서 할당 (GFP_HIGHUSER_MOVABLE 플래그 사용)
-> vm_area_struct가 파일을 참조하는 경우 할당한 페이지로 파일의 내용 복사하고, 아닌 경우 0으로 초기화
-> 이제 폴트가 난 가상 주소를 버디에서 할당한 페이지의 물리 주소로 매핑해야되는데, 처음에는 PGD 테이블밖에 없으므로 PUD, PMD, PTE 테이블도 중간에 할당해준 후 초기화해야함
-> PGD 테이블 엔트리가 새로 할당한 PUD 테이블을 가리키고, PUD 테이블 엔트리가 PMD 테이블을, PMD 테이블 엔트리가 PTE 테이블을 가리키도록 함
-> 마지막으로 PTE 테이블 엔트리가 방금전에 할당한 페이지를 가리키도록 함
-> 다시 프로세스 실행 재개

 

번호 제목 글쓴이 날짜 조회 수
공지 [공지] 스터디 정리 노트 공간입니다. woos 2016.05.14 617
142 [커널 18차] 63주차 kkr 2022.08.06 94
141 [커널 17차] 99주차 ㅇㅇㅇ 2022.07.31 35
140 [커널 18차] 62주차 kkr 2022.07.30 26
139 [커널 17차] 97~98주차 ㅇㅇㅇ 2022.07.24 52
138 [커널 18차] 61주차 kkr 2022.07.23 112
137 [커널 18차] 60주차 kkr 2022.07.16 118
136 [커널 17차] 95~96주차 ㅇㅇㅇ 2022.07.10 102
135 [커널 18차] 59주차 kkr 2022.07.09 123
134 [커널 19차] 8주차 kanlee 2022.07.02 159
133 [커널 19차] 7주차 kanlee 2022.07.02 89
132 [커널 19차] 6주차 kanlee 2022.07.02 42
131 [커널 19차] 5주차 kanlee 2022.07.02 38
130 [커널 19차] 4주차 kanlee 2022.07.02 106
129 [커널 18차] 57주차 kkr 2022.06.25 127
128 [커널 17차] 94주차 ㅇㅇㅇ 2022.06.19 80
127 [커널 18차] 56주차 kkr 2022.06.18 71
126 [커널 17차] 92~93주차 ㅇㅇㅇ 2022.06.11 92
125 [커널 18차] 54주차 kkr 2022.06.04 77
» [커널 19차] 3주차 리턴 2022.06.04 215
123 [커널 18차] 53주차 kkr 2022.05.29 90
XE Login