| 가상 메모리, Page fault handling

[정리]

1. 주제

- Page fault는 왜 발생하는 것이며, page fault 발생시 kernel은 어떻게 동작 하는가

2. 전체 내용 요약

1) 이론 필기 내용 요약

A. 가상 메모리

B. Page table (Multi-level page table)

C. User / Kenrel 가상주소 범위 확인

2) mmap을 통한 page fault handling 추적

3) read syscall을 이용한 page fault handling 추적

4) mmap vs. read 비교

 

[1)이론 필기 내용 요약]

A. 가상 메모리

$ free -h

free를 통해 전체 system에서 사용중인 memory에 대해 대략적으로 확인 가능함

자세한 내용은 '실습과 그림으로 배우는 리눅스 구조' 책에서 메모리 파트에 자세하게 설명 되어 있음

total, used, free, shared, buff/chace, available에 대한 의미를 파악 해야 함

total = used + free + buff/cache

used는 anonymous page를 나타내며 programming으로 사용되는 것, heap, stack등 같은 메모리를 뜻 함

buff/cache는 disk상의 내용이 메모리상에 유지될때를 말하며, bufferd cache,  page cache를 말함

available는 free + buff/cache의 합이며, process가 처음에 최초로 실행이 된다면, 최대치로 사용할 수 있는 메모리의 양을 뜻 함

free에 보여지는 buff/cache에 대해 /proc/meminfo를 통해 분리해 볼 수 있음

buffers : inode block과 같은 파일 정보를 나타냄

cached : data block의 파일 내용을 나타냄

앞에서 언급한 메모리 영역들은 task 할당시 사용되며, 하나의 task가 생성되어 사용할 수 있는 가상 메모리 크기는  ulimit를 통해 확인이 가능하며, unlimited로 초기에 설정되어 있음

vsz는 현재 실행중인 task가 모든 메모리에 파일 내용을 로딩했을 경우 필요한 가상 메모리의 크기를 나타내며, 최대 vsz크기는 unlimited로 설정되어 있음

unlimited의 최대 크기는 2bit OS이라면 2^32, 64bit OS이라면 2^64로 결정 됨

 

B. Page table (Multi-level page table)

참조 :

http://jake.dothome.co.kr/pt64/ 

https://www.kernel.org/doc/gorman/html/understand/understand006.html

$ cat /boot/config-5.3.0 | grep PGTABLE_LEVELS

사용중인 kernel의 page table level을 확인 하는 방법이며, 위의 결과에선 4단계 multi-level page table을 사용 중

최신 kernel에서는 level 5까지 지원하는 것으로 보임

<용어>

pgd (page global directory)

pud (page upper directory)

pmd (page middle directory)

pagetable

pte (page table entry)

 

multi level page table을 사용함에 있어 64bit를 9 / 9 / 9 / 9 / 12 로 총 48bit에 대해 사용 함

처음 초기화시 pgd만 존재하며, 만약 pgd level에서 pud entry가 존재하지 않아 page fault가 발생하는 경우,

pud ~ pte까지 모든 요소에 대해 모두 메모리에 loading하게 됨

 

C. User / Kenrel 가상주소 범위 확인 (추가 정리 필요)

__do_page_fault 에서 address가 kernel영역인지 user 영역인지 확인하게 됨

그 부분을 확인시 TASK_SIZE_MAX와 비교해 판단 함

 

memory 영역 참조 자료: https://wogh8732.tistory.com/397

user와 kernel은 어떻게 메모리 영역을 나눠 사용 하는지에 대해 파악이 필요함 

여기에서 ZONE이라는 용어가 나오며 해당 자료들에 대해 조사 필요

 

[2) mmap을 통한 page fault handling 추적]

실습 코드 파일 

- hello_mmap.c : mmap을 통해 page fault 발생 시킬 예제

- mmap_test.txt : hello_mmap.c에 의해 데이터 변경될 테스트 파일

<hello_mmap.c>

#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>

#define READ_SIZE 16

int main(int argc, char **argv)
{
	int fd, flag = PROT_WRITE | PROT_READ;
	struct stat sb;
	char *addr;


	if ((fd = open(argv[1], O_RDWR|O_CREAT)) < 0) {
		printf("File Open Error\n");
		return -1;
	}

	if (fstat(fd, &sb) < 0) {
		printf("fstat error\n");
		return -1;
		
	}
	addr =  mmap(NULL, READ_SIZE, flag, MAP_SHARED, fd, 0);
	if (addr == MAP_FAILED)
		printf("mmap error\n");

	printf("%s\n", addr);
	memset(addr, 0x00, READ_SIZE);
	munmap(addr, READ_SIZE);
	close(fd);
}
<mmap_test.txt>

$ vim mmap_test.txt
111111111122222222223333333333

https://github.com/hackndev/tools/blob/master/devmem2 와 와 위의 mmap과 비교 한번 해보기

 

$ gcc  -g -pg -o hello_mmap hello_mmap.c
$ ./hello_mmap mmap_test.txt

기존에 1로 적혀 있던 부분들에 대해 hello_mmap.c에서 설정한 READ_SIZE 만큼의 크기가 0으로 memset 된 결과를 확인 할 수 있음

이 변환 과정이 동작 중 page fault를 유발할 것이며, page fault 실험을 아래처럼 진행 할 수 있음

$ sudo uftrace record -d mmap.uftrace.data -K 30 ./hello_mmap mmap_test.txt
$ cd mmap.uftrace.data 
$ uftrace replay

puts()는 memset 과정에서 page에 접근 하는 부분을 나타내고 있으며, 결과에서 보이듯이 page fault handle -> ... -> submit_bio()까지 이어져 결국 disk layer까지 연결됨을 확인 가능 함

 

mmap에서 받은 addr에 대해 memset을 통해 data write 수행시 puts()가 불리게 되며,

기존에 이미 echo 3 을 통해 disk cache flush가 되었으므로, va-pa 연결이 끊겨져있음,

따라서 puts() 수행시 page fault가 발생하고 지난번에 배운것 처럼 page fault handler 로직이 호출됨

 

우리가 만든 mmap_test.txt 파일이 ext4 fs위에서 생성되었기 때문에 __do_fault()내에서 vma->vm_ops->fault()가 ext4_filemap_fault()와 연결되는 것임. 만약 다른 fs이라면 vma에 다른 함수가 연결될 것임

 

위의 호출 과정 정리시 아래와 같음

    1) arch/x86/mm/fault.c 의 do_page_fault() 함수 부터 시작

    2) arch/x86/mm/fault.c 의 do_user_addr_fault()

    3) arch/x86/mm/fault.c 의 find_vma()

    4) mm/memory.c 의 handle_mm_fault()

    5) mm/memory.c 의 __handle_mm_fault()

    6) mm/memory.c 의 handle_pte_fault()

    7) mm/memory.c 의 do_fault()

    8) mm/memory.c 의 do_read_fault()

    9) mm/memory.c 의 __do_fault()

    10) mm/memory.c 의 vma->vm_ops->fault(vmf);

    11) fs/ext4/inode.c 의 ext4_filemap_fault()

 

[3) read syscall을 이용한 page fault handling 추적]

$ vim read.c
$ cat read.c
#include <stdio.h>
#include <stdlib.h>

#define SIZE 16

void main()
{
    FILE *fp = fopen("mmap_test.txt","r");
    char buf[BUFSIZ];

    if (fp) {
        fread(buf, SIZE, 1, fp);
        printf("%s", buf);
        fclose(fp);
    }
}
$ sudo uftrace record -d read.uftrace.data -K 30 ./read
$ uftrace replay

mmap을 했을 경우 puts()를 통해 진행되고 read의 경우 syscall에 의해서 동작하게 됨 서로 다른 call path를 가지게 됨

 

[4) mmap vs. read 비교]

작은 size 크기를 1번 읽고 그만 한다면 성능상 큰 차이는 없겠지만, 많은 양의 데이터를 연속적으로 읽는다고 했을시 caching으로 인해 mmap이 조금 더 성능이 좋음

+ Recent posts