리눅스 커널 심층 분석 개정 3판 : 15장. 프로세스 주소 공간
Linux Kernel Development Third Edition : Chapter 15. The Process Address Space
Notion에서 보기
- The Process Address Space
=> 이번 챕터에서는 커널이 어떻게 process address space*를 관리하는지 다룸
*Process address space: 시스템 상에서 유저 스페이스 프로세스 각각에 주어진 메모리 / 리눅스는 가상 메모리 운영체제이므로 각 프로세스의 주소 공간이 물리적 메모리의 전체 공간보다 클 수 있기 때문에 커널이 이를 관리해야 한다.
- Address Spaces = 주소 공간
- 프로세스가 접근할 수 있는 가상 메모리 + 가상 메모리 내에서 프로세스가 사용할 수 있는 주소
- 보통 각 프로세스마다 flat* address space (32/64 bit) 가지고 있음
*flat : 연속된 범위의 주소 공간 vs segmented : 분리된 범위의 주소 공간
현대 가상 메모리 운영체제는 대부분 flat 사용
- 프로세스의 address space의 memory address는 완전히 독립적
(다른 프로세스의 주소 공간의 메모리 주소와 완전히 별개)
- 각자의 address space의 같은 주소에 서로 다른 데이터를 저장할 수 있음
- Thread = 다른 프로세스와 공유할 수 있는 address space
- Memory address = address space안에 있는 값 (주소 공간 내부의 특정 바이트)
- 주소 공간 중에 프로세스가 접근할 수 없는 메모리 주소 영역이 있음
접근할 수 있는 (유효한) 주소 영역 = memory area (메모리 영역)
- 프로세스는 커널을 통해 메모리 영역을 주소 공간에 동적으로 추가, 제거 가능
(즉, 프로세스가 접근할 수 있는 공간을 커널이 할당)
- 프로세스가 메모리 영역에 대해 수행할 수 있는 권한 지정됨 (readable, writable, executable)
접근 권한 없는 메모리 영역에 접근하면 ‘segment fault’
- 메모리 영역이 가지고 있을 수 있는 것:
- Text section: 실행 파일 코드
- Data section: 실행 파일의 초기값이 있는 전역 변수
- Zero page (=bss section): 초기값이 없는 전역 변수가 들어 있는 제로 페이지
- 공유 라이브러리 각각을 위한 추가적인 text, data, bss section
- 메모리 할당 파일 영역
- 공유 메모리 segment
- 메모리 영역은 서로 겹치지 않음
- The Memory Descriptor = 메모리 서술자
: 커널이 프로세스의 address space를 표현하는 자료 구조
프로세스 주소 공간 관련 모든 정보를 담고 있음
- struct mm_struct - <linux/mm_types.h>
- mm_struct에 담긴 정보: 메모리 영역 리스트, vma 레드 블랙 트리, 최근 사용한 메모리 영역, 첫번째 주소 공간 구멍, 전체 페이지 목록, 주소 공간 사용자, 주 사용 횟수, 메모리 영역 개수, 메모리 영역 세마포어, 페이지 테이블 락, 전체 mm_struct 리스트, 코드/데이터/힙의 시작주소와 마지막 주소, 스택의 시작 주소, 실행 인자의 시작와 끝, 환경 설정 값의 시작과 끝, 할당된 페이지, 전체 페이지 개수, 잠긴 페이지 개수, 저장된 auxv, 느슨한 TLB 전환 마스크, 아키텍처 특정 데이터, 상태 플래그, 스레드 코어 덤프 대기자, 비동기 입출력 리스트 락, 비동기 입출력 리스트
**mm_users = 해당 주소 공간을 사용하는 프로세스의 개수
**mm_count = 해당 구조체의 주 참조 횟수 / 사용하고 있으면 1 아니면 0
->0이면 (해당 주소 공간을 사용하는 스레드가 없으면 메모리 해제)
->mm_users에 프로세스가 추가되면 mm_count도 1 증가
**mmap = 주소 공간에 있는 모든 메모리 영역을 나타내는 연결 리스트
->모든 항목을 간단하고 효율적으로 탐색 가능
**mm_rb = 주소 공간에 있는 모든 메모리 영역을 나타내는 레드블랙 트리
->특정 항목 탐색 빠름
**mm_list = 모든 mm_struct를 이중 연결 리스트로 연결
->리스트의 첫번째 항목 = init 프로세스의 주소 공간을 나타내는 init_mm 메모리 서술자
->mmlist_lock (defined in kernel/fork.c)을 통해 동시 접근 제한
- Allocating a Memory Descriptor
- 특정 task의 memory descriptor는 태스크의 프로세스 descriptor*의 mm field에 저장
*task_struct (=process descriptor)
current -> mm : 현재 프로세스의 메모리 descriptor
- fork() -> copy_mm() 함수가 부모 프로세스의 메모리 서술자를 자식 프로세스로 복사
- allocate_mm() 매크로 (in kernel/fork.c)를 통해 mm_struct 구조체는 mm_cachep 슬랩 캐시에서 할당됨
- 모든 프로세스는 독자적인 프로세스 주소 공간 mm_struct를 받는다.
- thread: 프로세스 중에서 clone()함수에 CLONE_VM flag를 지정하여 자식 프로세스와 주소 공간을 공유하는 것 (특정 자원을 공유하는 프로세스)
- CLONE_VM이 지정 되었다는 것이 무슨 뜻인가?
- CLONE_VM이 지정된 경우 allocate_mm() 함수 호출 X,
/ 프로세스의 mm 항목이 부모 프로세스의 메모리 서술자 (mm_struct)를 가리키게 설정
자식 프로세스->mm = 부모 프로세스 ->mm;
/ address space 부모 자식 공유
- Destroying a Memory Descriptor
- 주소 공간 사용하던 프로세스 종료 -> exit_mm() in kernel/exit.c 호출
- housekeeping 수행, 통계 값 갱신
- mmput() 호출하여 mm_users (in mm_struct) 감소시킴
/ mm_users = 0 -> mmdrop() 호출 -> mm_count 감소시킴
/ mm_count = 0 -> free_mm() 매크로 호출 -> kmem_cache_free() 함수 실행 -> mm_struct를 mm_cachep 슬랩 캐시로 반납
- The mm_struct and Kernel Threads
- Thread는 address space가 없음 = memory descriptor (mm_struct) 없음
- 스레드의 프로세스 서술자 (task_struct)의 mm = NULL / user context가 없는 프로세스
- 스레드는 사용자 공간에 있는 페이지를 가지지 않음
- 스레드 자신의 메모리 서술자, 페이지 테이블이 없음
- 스레드가 커널 메모리 접근을 위한 페이지 테이블 필요하면 스레드는 이전에 실행한 프로세스의 메모리 서술자 이용
**프로세스가 스케줄링될 때마다 task_struct의 mm 항목이 참조하는 주소 공간이 로드됨
-> task_struct의 active_mm항목 갱신하여 새로운 주소 공간을 가리키게 함
-> 스레드가 스케줄링되면 커널은 mm = NULL 을 확인하고 사용중인 이전 프로세스의 주소 공간을 그대로 둠.
스레드의 메모리 서술자의 active_mm 항목을 이전 프로세스의 메모리 서술자가 가리키던 곳으로 갱신
- 스레드는 부모 메모리 서술자를 그대로 사용해서 메모리 서술자가 없다고 하지 않았나?
-> 스레드는 이전 프로세스의 페이지 테이블이 필요할 때 사용 가능
- Virtual Memory Areas (VMA - 리눅스 커널 메모리 영역)
- vm_area_struct = *메모리 영역을 표현하는 구조체 in <linux/mm_types.h>
: address space의 연속된 구간에 해당하는 단일 메모리 영역 표현
*메모리 영역 / memory area: process address space에서 프로세스가 접근할 수 있는 유효한 메모리 주소 영역
- 커널은 각 메모리 영역을 독립적 메모리 객체로 간주
- 각 메모리 영역 별도의 권한, 관련 연산을 할 수 있음
- VMA 구조가 서로 다른 type의 메모리 영역을 표현할 수 있음 (객체지향 접근법)
- vm_area_struct가 가지고 있는 정보:
해당 메모리 영역이 속한 mm_struct, vma 시작 및 종료 지점, vma 리스트, 접근 권한, 플래그, 트리상의 vma 노드, address_space->i_mmap, i_mmap_nonlinear과 연결되는 링크, anon_vma, 익명 vma객체, 관련 연산, 파일 내의 오프셋, 할당된 파일, 내부 처리용 데이터
**vm_start = 해당 메모리 영역의 시작 주소
**vm_end = 해당 메모리 영역의 마지막 주소 바로 다음 바이트
=> vm_end – vm_start = 메모리 영역의 바이트 길이 / 메모리 영역간 중첩 X
**vm_mm = vma에 해당하는 mm_struct (VMA별로 고유한 mm_struct / 메모리 서술자를 가지고 있음)
->서로 다른 프로세스가 같은 파일을 각자의 주소 공간 할당할 경우 각자 별도의 vm_area_struct를 통해 각자의 메모리 공간을 식별함.
- VMA Flags
: vm_flags - <linux/mm.h>
메모리 영역에 대해 커널이 책임지는 동작 + 메모리 영역에 들어 있는 개별 페이지와 메모리 영역 전체에 대한 정보를 나타내는 비트 플래그
**VM_READ, VM_WRITE, VM_EXEC = 해당 메모리 영역에 있는 페이지에 대한 읽기, 쓰기, 실행 권한 표시
**VM_SHARED = 메모리 영역이 여러 프로세스가 공유하는 할당인지 표시
VM_SHARED = 1 -> shared mapping (공유 할당)
VM_SHARED = 0 -> private mapping (개별 할당)
**VM_IO = 메모리 영역이 장치 입출력 공간으로 할당된 것인지 표시
디바이스 드라이버가 입출력 공간에 mmap() 호출 -> VM_IO = 1
->메모리 영역이 프로세스의 코어 덤프*에 들어가지 않도록 함
*코어 덤프 = 메모리 덤프 : 컴퓨터 프로그램이 특정 시점에 작업 중이던 메모리 상태를 기록한 것으로 보통 프로그램이 비정상적으로 종료했을 때 만들어짐.
**VM_RESERVED = 메모리 영역이 swap되지 않게 함
**VM_SEQ_READ = 커널에게 응용프로그램이 해당 영역에 순차적인 읽기 연산을 한다는 것을 알림
->커널은 해당 파일 read-ahead (미리 읽기)를 증가시킴
** VM_RAND_READ = 커널에게 응용프로그램이 해당 영역에 비연속적 읽기 연산 수행을 알림
->커널은 해당 파일 미리 읽는 정보 줄임
->madvise()호출 -> MADV_SEQUENTIAL, MADV-RANDOM 플래그 설정
- VMA Operations
**vm_ops in vm_area_strcuct: 해당 메모리 영역을 조작하기 위해 커널이 호출할 수 있는 동작 구조체 테이블을 가리킴
- vm_operations_struct : 특정 객체 인스턴스에 대해 구체적인 동작 작성
**void open(struct vm_area_struct *area) : *area로 가리킨 메모리 영역을 주소 공간에 추가하기 위해 호출하는 함수
**void close(struct vm_area_struct *area) : *area로 가리킨 메모리 영역을 주소 공간에서 제거하기 위해 호출하는 함수
** int fault(struct vm_area_sruct *area, struct vm_fault *vmf) : 물리적 메모리에 적재되어 있지 않은 페이지에 접근할 경우 page fault handler를 호출하는 함수
**int page_mkwrite(struct vm_area_sruct *area, struct vm_fault *vmf) : 읽기 전용 페이지를 쓰기 가능으로 변경 하려는 경우 page fault handler 호출
** int access(struct vm_area_struct *vma, unsigned long address, void buf, int len, int write) : get_user_pages() 호출 실패시 access_process_vm()*에서 호출
*get_user_pages(): pin user pages in memory
- Lists and Trees of Memory Areas
- mm_struct의 mmap, mm_rb로 메모리 영역 (vm_area_struct) 접근
- mmap: 모든 메모리 영역 객체를 하나의 연결 리스트로 보관
vm_next in vm_area_struct로 리스트 연결
메모리 주소 오름차순으로 정렬
연결리스트의 마지막 구조체 = NULL
mmap 포인터가 연결리스트이 첫번째 메모리 영역 구조체 (vm_area_strcut)를 가리킴
- 주소 공간의 모든 메모리 영역을 탐색해야 하는 경우 사용
- mm_rb: 모든 메모리 영역 객체를 레드블랙 트리로 연결
mm_rb -> 루트 노드
vm_rb in vm_area_struct로 각 메모리 영역 연결
- 레드블랙 트리: 균형 이진 트리, 모든 노드의 값은 왼쪽 자식 노드의 값보다 크고, 오른쪽 자식 노드의 값보다 작다. 빨강 노드의 자식 노드는 검정 노드이고, 트리의 루트에서 리프 노드로 가는 모든 경로는 같은 개수의 검정 노드가 있다. 루트 노드는 항상 빨강 노드이다.
- 주소 공간의 특정 메모리 영역 찾아야 하는 경우 사용
- Memory Areas in Real Life
- 프로세스 – 주소 공간 ⊃ 메모리 영역 (텍스트 영역, 데이터 영역, bss 영역, 스택 영역)
- 각 프로세스마다 mm_struct를 가짐 <- task struct에서 참조
- 스레드는 mm_struct를 공유함
- 각 메모리 영역마다 vm_area_struct를 가짐
- /proc/<pid>/maps 파일 : 프로세스 주소 공간의 메모리 영역을 출력
=> 출력: 시작-끝 주소 | 권한 rwxp | 오프셋 | major:minor | inode | 파일
- pmap(1) 유틸리티 : 프로세스 메모리 영역을 정리해서 보여줌
-> ex)
전체 메모리 영역 1340KB 차지 – 그 중 쓰기 가능한 영역 40KB
shared 영역 0KB / 쓰기 불가능한 영역 1300KB - 사본을 하나만 둠
->여러 프로세스가 접근해도 안전
-> 물리적 메모리 40KB만 사용하고 나머지는 공유 메모리 영역 사용
=> 공간 절약
- Manipulating Memory Areas (by kernel)
: mmap() 함수의 기본 / 커널이 제공 / <linux/mm.h>
- find_vma()
: 해당 메모리 주소가 있는 VMA를 찾는 함수
- struct vm_area_struct * find_vma(struct mm_struct *mm, unsigned long addr);
: VMA의 vm_end 항목이 addr를 포함하거나 보다 큰 주소로 시작하는 첫번째 VMA를 찾음
- 없으면 NULL반환
/ 있으면 해당 메모리 영역을 나타내는 vm_area_struct 포인터 반환
- 반환 결과 mmap_cache field (in mm_struct)에 캐시
->캐시 적중률 높은 편 / 캐시에 없으면 mm에 속한 메모리 영역에서 대상 탐색 (mm_rb 활용)
** 먼저 memory descriptor (mm_struct *mm)의 mmap_cache (캐시)에 vma가 있는지 확인
->입력한 주소값 (addr)이 캐시된 vma안에 있으면 이를 반환
-> 없으면 래드블랙 트리 탐색 (mm_rb)
-> 메모리 영역에도 없으면 NULL 반환
- find_vma_prev()
: addr 이전의 마지막 vma 반환 / mm/mmap.c / linux/mm.h에 선언
- struct vm_area_struct * find_vma_prev(struct mm_struct *mm, unsigned long addr, **struct vm_area_struct pprev)
: pprev = addr 앞 쪽의 픔 포인터 저장
- find_vma_intersection()
: 지정한 주소 범위와 중첩되는 첫번째 vma 반환 / linux/mm.h에 정의
**struct mm_strucr *mm = 탐색할 주소 공간
**start_addr ~ end_addr = 범위의 시작과 끝
Fine_vma가 유효한 vma를 반환한 경우 해당 vma가 지정한 주소 범위의 끝 부분 (end_addr)을 지나서 시작하지 않는 경우에만 fine_vma와 같은 vma 반환
- Creating an Address Interval: mmap(), do_mmap()
- do_mmap(): 커널이 연속된 주소 범위를 새로 만들 때 사용
- 기존 주소 범위와 인접
- 권한이 같으면 하나로 병합 (기존 VMA 확장)
- 병합이 불가능한 경우, vm_area_cachep 슬랩 캐시에서 새로운 vm_area_struct를 할당하고 vma_link() 함수를 이용하여 새로운 메모리 영역을 주소 공간의 연결리스트와 레드블랙 리스트에 추가하고, mm_struct의 total_vm 항목을 갱신한다.
- 새로 생성된 주소 범위의 초기 주소 값 반환 or 음수값 반환
- linux/mm.h
** *file에 할당 offset부터 len만큼
*file 지정 X = anonymous mapping (익명 할당)
*file, offset 지정 = file-backed mapping (파일 지정 할당)
** addr = (option) 빈 범위 탐색 시작 주소
** prot = 메모리 영역에 해당하는 페이지의 접근 권한 지정 (read, write, exec, none)
페이지 보호 플래그
PROT_READ, PROT_WRITE, PROT_EXEC, PROTE_NONE(페이지 접근 X)
** flag = VMA에 할당된 메모리의 형식 지정, 동작 변경
- mmap() 시스템 콜
: do_map() 결과를 사용자 공간에 제공하는 시스템 콜
**페이지 단위 오프셋 사용
- Removing an Address Interval: munmap(), do_munmap()
- do_munmap()
: 지정한 프로세스 주소 공간에서 주소 범위 제거
in <linux/mm.h>
int do_munmap(struct mm_struct *mm, unsigned long start, size_t len)
** *mm인자는 start 주소부터 len만큼의 주소 공간 범위를 제거할 주소 공간으로 지정함
- mm이 가리킨 주소 공간 범위 제거에 성공하면 0 반환
- 실패하면 음수의 오류 코드 반환
- munmap()
: 사용자 공간의 프로세스가 자신의 주소 공간에서 특정 주소 범위 제거
int munmap(void *start, size_t length)
- 시스템 콜 in <mm/mmap.c>
- Page Tables
- 응용프로그램은 물리적 주소가 할당된 가상 메모리 사용
CPU는 직접적인 물리적 주소 사용
- 응용프로그램이 가상 메모리 주소에 접근시 물리적 주소로 변환 필요 using page table
- Page table
: 가상 주소를 여러 조각으로 나눔
각 조각 = 테이블의 인덱스
테이블은 다른 테이블이나 물리적 페이지를 가리킴.
- 3-level Page table of Linux
- 1-level (최상위) 페이지 테이블: 페이지 전역 디렉토리 (PGD / page global directory)
- 2-level 페이지 테이블: 페이지 중간 디렉토리 (PMD / page middle directory)
- 3-level 페이지 테이블: 페이지 테이블 (PTE) – 각 엔트리는 물리적 페이지를 가리킴
- TLB (translation lookaside buffer/변환 참조 버퍼):
가상 주소 -> 물리적 주소 변환을 빠르게 수행할 수 있도록 돕는 버퍼
가상 물리적 주소 변환 정보를 하드웨어적으로 캐시하는 역할
** 먼저 변환하려는 가상 주소 TLB 캐시 확인
- 있으면 바로 물리적 주소 변환
- 없으면 페이지 테이블 참조하여 물리적 주소 찾기
- 부모 – 자식 페이지 테이블 공유 방식 :
fork() 시스템콜 호출시 페이지 테이블 복사 오버헤드 X
공유하는 프로세스 중 한쪽이 특정 페이지 테이블 항목 수정하려 하면 복본 만들어 공유 해제, 따로 씀
- Huge page
: 일반적으로 리눅스에서 page의 크기는 4KB인데 대용량 메모리를 장착한 시스템에서는 효율적인 page table관리를 위해 2M~256M 정도로 더 큰 사이즈의 page로 관리한다.
- Huge page 사용 장점
- Huge page는 swap되지 않기 때문에 그로 인한 오버헤드가 발생하지 않는다.
- 메모리를 크게 쪼갰기 때문에 전체적인 huge page의 개수가 적다
- TLB에 더 많은 page 엔트리를 보관할 수 있다.
- Page table의 엔트리 개수가 줄어들어 page table가 차지하는 메모리 공간이 절약된다.
- 즉, 페이지 개수가 적기 때문에 관리가 쉬워지고, 페이지 테이블에 필요한 메모리 공간이 절약되지만 페이지가 크기 때문에 페이지가 작아서 발생하는 페이지 부재 현상이 적게 발생한다.