[Linux] Page Fault

최승혁·2022년 8월 23일
1

프로세스 선형 주소 공간의 페이지가 항상 메모리에 상주할 필요는 없다. 예를 들어, 공간이 vm_area_messages 내에서 예약되었기 때문에 프로세스를 대신하여 수행된 할당이 즉시 충족되지 않는다. 상주하지 않는 페이지의 다른 예로는 백업 저장소로 스왑 아웃된 페이지 또는 읽기 전용 페이지를 작성하는 페이지가 있다.

대부분의 운영 체제와 마찬가지로 리눅스는 상주하지 않는 페이지를 처리하기 위한 fetch 정책으로 Deman Fetch 정책을 가지고 있다. 이것은 운영 체제가 페이지를 추적하고 할당하는 페이지 폴트 예외를 발생시킬 때 하드웨어가 페이지를 백업 스토리지에서 가져오는 것이다. 백업 스토리지의 특성은 어떤 종류의 페이지 prefetch 정책이 페이지 fault를 덜 초래한다는 것을 의미하지만 리눅스는 이 점에서 상당히 프리미티브하다.

스왑 영역에서 페이지를 호출하면 이후 최대 2page_cluster 수의 페이지가 swapin_readahead()로 읽혀 스왑 캐시에 배치된다. 안타깝게도 곧 사용될 것 같은 페이지들이 스왑 영역에 인접하게 될 가능성만 있을 뿐이며, 이는 형편없는 prefetching 정책이다. 리눅스는 프로그램 동작에 관계없이 prefetching 정책의 혜택을 받을 수 있다.

페이지 폴트에는 major fault와 minor fault의 두 가지 유형이 있다. 주요 페이지 결함은 비용이 많이 드는 작업인 디스크에서 데이터를 읽어야 할 때 발생한다. 그렇지 않은 결함을 마이너 또는 소프트 페이지 결함이라고 한다. Linux는 이러한 페이지 폴트 수에 대한 통계를 각각 task_down→down_flttask_down→min_flt 필드로 유지한다.

리눅스의 페이지 폴트 핸들러는 표에 나열된 여러 가지 유형의 페이지 장애를 인식하고 이에 대해 작동해야 한다.

ExceptionTypeAction
Region valid but page not allocatedMinorAllocate a page frame from the physical page allocator
Region not valid but is beside an expandable region like the stackMinorExpand the region and allocate a page
Page swapped out but present in swap cacheMinorRe-establish the page in the process page tables and drop a reference to the swap cache
Page swapped out to backing storageMajorFind where the page with information stored in the PTE and read it from disk
Page write when marked read-onlyMinorIf the page is a COW page, make a copy of it, mark it writable and assign it to the process. If it is in fact a bad write, send a SIGSEGV signal
Region is invalid or process has no permissions to accessErrorSend a SEGSEGV signal to the process
Fault occurred in the kernel portion address spaceMinorIf the fault occurred in the vmalloc area of the address space, the current process page tables are updated against the master page table held by init_mm. This is the only valid kernel page fault that may occur
Fault occurred in the userspace region while in kernel modeErrorIf a fault occurs, it means a kernel system did not copy from userspace properly and caused a page fault. This is a kernel bug which is treated quite severely.

Reasons For Page Faulting

각 아키텍처는 페이지 폴트를 처리하기 위한 아키텍처별 함수를 등록한다. 이 함수의 이름은 임의이지만, do_page_fault()가 일반적인 선택이다.

do_page_fault() 함수는 장애의 주소, 단순히 페이지를 찾을 수 없거나 보호 오류인지, 읽기 또는 쓰기 오류인지, 사용자 또는 커널 공간의 오류인지 등 정보를 제공한다. 어떤 유형의 결함이 발생했는지, 그리고 아키텍처 독립 코드에 의해 어떻게 처리되어야 하는지를 결정하는 책임이 있다. 아래 그림은 대략적으로 이 기능이 무엇을 하는지를 보여준다.

handle_mm_fault()는 백업 스토리지에서 COW를 수행하는 등의 페이지 폴트를 위한 아키텍처 독립적인 최상위 함수이다. 1을 반환하면 minor 결함, 2는 major 결함, 0은 SIGBUS 오류를 전송하고 다른 값은 메모리 부족 핸들러를 호출합니다.

Handling a Page Fault

예외 처리기가 폴트가 유효한 메모리 영역에서 유효한 페이지 결함이라고 결정하면, 호출 그래프가 아래 그림에 표시된 아키텍처 독립 함수 handle_mm_fault()가 호출 된다. 필요한 PTE가 아직 존재하지 않으면 할당하고 handle_pte_fault()를 호출한다.

PTE의 특성에 따라 아래 그림에 표시된 핸들러 기능 중 하나가 사용된다. 결정의 첫 번째 단계는 PTE가 존재하지 않는 것으로 표시되었는지 또는 pte_present()pte_none()에 의해 확인되는 PTE가 할당되었는지 확인하는 것이다. 할당된 PTE가 없으면(pte_none()true로 반환됨), do_no_page()가 호출되어 요구 할당을 처리한다. 그렇지 않으면 디스크로 스왑 아웃된 페이지이며 do_swap_page()는 요구 페이징을 수행한다.

두 번째 옵션은 페이지를 쓰는 경우이다. PTE가 쓰기 보호되어 있으면 페이지가 COW(Copy-On-Write) 페이지이므로 do_wp_page()가 호출된다. COW 페이지는 여러 프로세스(일반적으로 부모 및 자식) 간에 공유되는 페이지이며, 쓰기 프로세스에 대한 개인 사본이 작성될 때까지 계속된다. COW 페이지는 개별 PTE가 아닌 경우에도 해당 영역에 대한 VMA가 쓰기 가능한 것으로 표시되기 때문에 인식된다. COW 페이지가 아닌 경우, 페이지는 쓰여진 대로 dirty 상태로 표시됩니다.

마지막 옵션은 페이지를 읽었고 페이지가 존재하지만 오류가 여전히 발생하는 경우이다. 이 문제는 3단계 페이지 테이블이 없는 일부 아키텍처에서 발생할 수 있다. 이 경우, PTE는 단순히 새로 생성되고 young으로 표시된다.

Demand Allocation

프로세스가 페이지에 처음 접근할 때, 페이지를 할당하고 do_no_page() 함수를 통해 데이터를 채워야 한다. 상위 VMA(vma→vm_ops)와 연결된 vm_operations_messagesnopage() 함수를 제공하는 경우 do_no_page() 함수가 호출된다. 이것은 페이지를 할당하고 액세스 시 데이터를 제공해야 하는 메모리 매핑 디바이스나 백업 저장소에서 데이터를 가져와야 하는 매핑 파일에 중요하다. 먼저 결함 페이지가 익명인 경우에 대해 살펴보자.

anonymous pages

vm_area_filename→vm_ops 필드가 채워지지 않았거나 nopage() 함수가 제공되지 않은 경우 do_anonymous_page() 함수를 호출하여 익명 액세스를 처리합니다. 처리해야 할 케이스는 처음 읽고 처음 쓰는 두 가지뿐이다. 익명 페이지이므로 데이터가 존재하지 않아 첫 번째 읽기는 쉽다. 이 경우 0의 페이지인 시스템 전체의 empty_zero_pagePTE에 대해 매핑되고 PTE는 쓰기 보호된다. 쓰기 보호는 프로세스가 페이지에 쓸 때 또 다른 페이지 오류가 발생하도록 설정된다. x86에서 전역 0 채우기 페이지는 mem_init() 함수에 0으로 표시된다.

페이지에 대한 첫 번째 쓰기일 경우, 자유 페이지를 할당하기 위해 alloc_page()이 호출되며 clear_user_highpage()에 의해 0이 채워진다. 페이지가 성공적으로 할당되었다고 가정하면 mm_structRSS(Resident Set Size) 필드가 증가한다. 캐시 일관성을 보장하기 위해 일부 아키텍처에서 사용자 공간 프로세스에 페이지를 삽입할 때 필요에 따라 flush_page_to_ram()이 호출됩니다. 그런 다음 페이지가 LRU 목록에 삽입되므로 나중에 페이지 회수 코드에 의해 회수될 수 있다. 마지막으로 프로세스의 페이지 테이블 항목이 새 매핑을 위해 업데이트 된다.

file/device backed pages

파일 또는 디바이스에 백업되는 경우 VMAs vm_operations_struct 내에서 nopage() 함수가 제공된다. 백업 파일의 경우 filemap_nopage() 함수는 페이지를 할당하고 디스크에서 페이지 크기의 데이터를 읽기 위한 nopage() 함수이다. shmfs에서 제공하는 것과 같은 가상 파일에 의해 지원되는 페이지는 shmem_nopage() 함수를 사용한다. 각 장치 드라이버는 사용할 수 있는 유효한 struct page를 반환하는 한 여기서 내부가 중요하지 않은 다른 nopage()를 제공한다.

페이지를 반환할 때 페이지가 성공적으로 할당되었는지 확인하고, 할당되지 않은 경우 해당 오류가 반환되었는지 확인한다. 그런 다음 조기 COW 중단이 발생해야 하는지 여부를 확인한다. 폴트가 페이지에 쓰기이고 VM_SHARED 플래그가 관리 VMA에 포함되지 않은 경우 조기 COW 중단이 발생합니다. 조기 중단은 nopage() 함수에 의해 반환되는 페이지로 참조 수를 줄이기 전에 새 페이지를 할당하고 데이터를 복사하는 경우입니다.

두 경우 모두 pte_none()을 사용하여 검사하여 사용하려는 페이지 테이블에 이미 PTE가 없는지 확인합니다. SMP의 경우 동일한 페이지에 대해 거의 동시에 두 개의 고장이 발생할 수 있으며, 고장 기간 내내 스핀 록이 유지되지 않기 때문에 이 점검을 마지막 순간에 수행해야 합니다. 경쟁이 없으면 PTE가 할당되고 통계가 업데이트되며 캐시 일관성을 위한 아키텍처 후크가 호출됩니다.

Demand paging

페이지를 백업 스토리지로 스왑 아웃할 때 do_swap_page() 함수는 페이지를 다시 읽는다. 이를 찾는 데 필요한 정보는 PTE 자체 내에 저장된다. PTE 내의 정보는 스왑 중인 페이지를 찾기에 충분하다. 여러 프로세스 간에 페이지를 공유할 수 있기 때문에 페이지를 항상 즉시 교환할 수 있는 것은 아니다. 대신 페이지가 스왑 아웃되면 스왑 캐시 내에 배치됩니다.

공유 페이지를 공유하는 각 프로세스의 PTEstruct page를 매핑할 수 있는 방법이 없기 때문에 공유 페이지를 즉시 스왑 아웃할 수 없다. 모든 프로세스의 페이지 테이블을 검색하는 것은 단순히 너무 비싸다.

스왑 캐시가 있으면 장애가 발생해도 스왑 캐시에 계속 존재할 수 있다. 이 경우 페이지에 대한 참조 카운트가 증가하여 프로세스 페이지 테이블 내에 다시 배치되고 마이너 페이지 폴트로 등록된다.

페이지가 디스크에만 있는 경우 swapin_readahead()이 요청된 페이지와 페이지 뒤에 있는 여러 페이지를 호출한다. 읽을 페이지 수는 <mm/swap.c>에 정의된 변수 page_cluster에 의해 결정된다. RAM이 16MB 미만인 low 메모리 시스템에서는 2 또는 3으로 초기화된다. 스왑 엔트리가 잘못되었거나 비어 있지 않은 경우 읽은 페이지 수는 2page_cluster이다. 이 작업은 탐색이 가장 비용이 많이 드는 작업이라는 전제 하에 수행되므로 탐색이 완료되면 다음 페이지도 읽어야 한다.

COW Pages

옛날에는 프로세스가 분기될 때 자식에게 전체 부모 주소 공간이 중복되었다. 이 작업은 백업 스토리지에서 프로세스의 상당 부분을 스왑 인해야 하기 때문에 매우 비용이 많이 드는 작업이었다. 이러한 상당한 오버헤드를 방지하기 위해 COW(Copy-On-Write)라는 기술이 사용된다.

포크가 진행되는 동안, 두 프로세스의 PTE는 읽기 전용으로 만들어져서 쓰기가 발생할 때 페이지 오류가 발생한다. Linux는 PTE가 쓰기 보호되어 있더라도 제어 VMA는 해당 영역을 쓰기 가능 상태로 표시하기 때문에 COW 페이지를 인식한다. do_wp_page() 함수를 사용하여 페이지를 복사하고 쓰기 프로세스에 할당하여 처리한다. 필요한 경우 새 스왑 슬롯이 페이지에 예약된다. 이 방법을 사용하면 포크 중에 페이지 테이블 항목만 복사해야 한다.

Copying To/From Userspace

주소 지정된 페이지가 상주하는지 여부를 빠르게 확인할 방법이 없기 때문에 프로세스 주소 공간의 메모리에 직접 액세스하는 것은 안전하지 않다. Linux는 MMU를 사용하여 주소가 잘못될 때 예외를 발생시키고 페이지 오류 예외 처리기가 예외를 포착하여 수정하도록 한다. x86의 경우 어셈블러는 __copy_user()에 의해 제공되어 주소가 완전히 쓸모없는 예외를 트랩한다. fixup 코드의 위치는 search_exception_table() 함수를 호출할 때 찾을 수 있다. 리눅스는 사용자 주소 공간에서 데이터를 안전하게 복사하기 위한 충분한 API(주로 매크로)를 제공한다.

Accessing Process Address Space API

  • unsigned long copy_from_user(void *to, const void *from, unsigned long n)

    Copies n bytes from the user address(from) to the kernel address space(to)

  • unsigned long copy_to_user(void *to, const void *from, unsigned long n)
    Copies n bytes from the kernel address(from) to the user address space(to)

  • void copy_user_page(void *to, void *from, unsigned long address)
    This copies data to an anonymous or COW page in userspace. Ports are responsible for avoiding D-cache alises. It can do this by using a kernel virtual address that would use the same cache lines as the virtual address.

  • void clear_user_page(void *page, unsigned long address)
    Similar to copy_user_page() except it is for zeroing a page

  • void get_user(void *to, void *from)
    Copies an integer value from userspace (from) to kernel space (to)

  • void put_user(void *from, void *to)
    Copies an integer value from kernel space (from) to userspace (to)

  • long strncpy_from_user(char *dst, const char *src, long count)
    Copies a null terminated string of at most count bytes long from userspace (src) to kernel space (dst)

  • long strlen_user(const char *s, long n)
    Returns the length, upper bound by n, of the userspace string including the terminating NULL

  • int access_ok(int type, unsigned long addr, unsigned long size)
    Returns non-zero if the userspace block of memory is valid and zero otherwise

모든 매크로가 어셈블러 함수에 매핑되는데, 이 함수는 모두 유사한 구현 패턴을 따르므로 설명을 위해 x86에서 copy_from_user()가 구현되는 방식을 추적한다.

컴파일 시간에 복사본의 크기를 알 수 있는 경우 copy_from_user() 호출 __constant_copy_from_user() 또는 _generic_copy_from_user() 호출이 사용된다. 크기가 알려진 경우, 1, 2, 4바이트 속도로 데이터를 복사하기 위한 다양한 어셈블러 최적화가 있으며, 그렇지 않으면 두 복사 함수 사이의 구별은 중요하지 않다.

일반 복사 함수는 결국 <asm-i386/uaccess.h>에서 __copy_user_zeroing() 함수를 호출한다. 여기에는 세 가지 중요한 부분이 있다. 첫 번째 부분은 사용자 공간에서 바이트의 크기 수를 실제로 복사하기 위한 어셈블러이다. 상주하지 않는 페이지가 있으면 페이지 장애가 발생하고 주소가 유효한 경우 정상적으로 스왑 인 된다. 두 번째 부분은 "fixup" 코드이고 세 번째 부분은 첫 번째 부분의 명령어를 두 번째 부분의 fixup 코드에 매핑하는 __ex_table이다.

이러한 쌍은 복사 명령의 위치와 수정 코드의 위치를 링커에 의해 커널 예외 핸들 테이블로 복사한다. 잘못된 주소를 읽으면 함수 do_page_fault()가 통과하고, search_exception_table()을 호출하여 잘못된 읽기가 발생한 EIP를 찾아 나머지 커널 공간에 0을 복사하고 레지스터를 복구하고 반환하는 수정 코드로 이동한다. 이러한 방식으로 커널은 비싼 검사 없이 사용자 공간에 안전하게 접근할 수 있고 MMU 하드웨어가 예외를 처리하도록 할 수 있다.

사용자 공간에 액세스하는 다른 모든 기능은 유사한 패턴을 따른다.

profile
그냥 기록하는 블로그

0개의 댓글