Linux Kernel Memory

최승혁·2022년 8월 18일
0

다양한 메모리 주소

  • User Virtual Address

사용자 공간에서 접근하는 주소, 사용자 프로세스가 접근하는 주소는 모두 가상 주소이다.

  • Physical Address

프로세서와 메모리 사이에서 사용하는 물리적인 주소

  • Bus Address

주변 장치와 메모리가 서로 데이터를 주고 받기 위해서 사용하는 주소이다. 실제로 PCI와 같은 경우 memory-mapped IO를 수행하여 데이터를 읽거나 쓴다.

  • Kernel Logical Address

커널은 자신이 사용할 일정한 크기의 메모리 영역을 일대일로 매핑해 놓는다. (단, 시작 주소는 정해져있고 그 주소에 대한 offset으로 일대일 매핑한다.) 따라서 커널은 항상 일정한 영역의 Physical Address를 커널 Logical Address로 매핑하여 Kernel Logical Address에 접근할 수 있다. 주의할 점은 kmalloc은, kernel logical address 안에서만 메모리를 할당할 수 있다.

  • Kernel Virtual Address

Kernel Virtual Address는 Kernel Logical Address와는 다르게 일대일 매핑이 되어있지는 않다. 그때그때 필요한 메모리 영역을 매핑하기 때문이다. Kernel Logical/Virtual Address에서 사용하는 메커니즘은 동일하며, 페이징을 사용한다. 다만 Kernel Logical Address는 사용을 위해 미리 준비된 영역의 주소이고, Kernel Virtual Address는 사용이 필요할 때 매핑해서 사용하는 영역의 주소이다. 모든 Kernel Logical Address는 Kernel Virtual Address이지만 역은 참이 아니다. Kernel Logical Address를 갖지 않는 Kernel Virtual Address를 high memory라 한다.

High Memory와 Low memory

커널 관련 문서에 종종 등장하는 표현인 high memory와 low memory는 kernel virtual address와 kernel logical address를 의미한다.

  • Low Memory: logical address가 존재하는 메모리 공간

  • High Memory: logical address를 갖고 있지 않는 메모리 공간

둘의 차이점은 Low Memory는 가상 주소를 물리 주소로 변환하는 매핑이 이미 존재하나, High memory는 별도의 매핑을 해야만 접근할 수 있다는 점이다. 보통 사용자 공간의 메모리에 접근할 때 High Memory에 매핑해야만 커널에서 사용자 공간에 접근할 수 있다.

Paging과 Physical Address

현대적인 운영체제는 메모리 관리 기법으로 페이징을 사용한다. 페이징은 메모리를 페이지라는 작은단위로 나누어서 관리하는 기법이고, 페이지 하나의 사이즈는 매우 아키텍처에 의존적이지만 보통 4KB or 8KB이다.

페이징 기법을 사용하는 경우 주소는 두 부분으로 나눌 수 있따. 하나는 페이지 번호(PFN, page frame number)이고, 나머지는 페이지 내의 offset이다. 예를 들어서 32bit 아키텍처에서 PAGE_SIZE == 4KB인 경우, 상위 20비트는 PFN 하위 12비트는 offset이다.

Memory Map과 struct page

옛날에는 High Memory가 존재하지 않았었다. 그래서 logical address와 physical address가 일대일로 매핑되어서 어떤 페이지를 가리킬 때 logical address를 바로 사용할 수 있었따. 하지만 high memory가 생기면서 이게 불가능하게 되었고, 따라서 커널 내에서 logical address를 그대로 사용하기 보다 struct page에 대한 포인터를 사용하게 되었다.

struct page는 페이징 기법에서 페이지 하나를 나타내기 위한 구조체이며 해당 페이지에 대한 모든 정보가 들어있다.

  • void *virtual
#if defined(WANT_PAGE_VIRTUAL)
	void *virtual;	// Kernel virtual address (NULL if not kmapped, ie. high mem)

#endif // WANT_PAGE_VIRTUAL

매핑이 존재하는 경우 가상 주소이며, 아닌 경우 NULL이다. WANT_PAGE_VIRTUAL이 정의된 경우에만 page 구조체에 추가된다.

Virtual Memory Area (VMA)

위에서는 주소를 특성에 따라 나누었다. 하지만 실제로 프로세스의 메모리 영역들을 생각해보면 text, data, heap, stack 등등 특성에 따라 메모리 영역을 나눌 수 있다. 이렇듯 메모리 영역은 특성에 따라 분류할 수 있으며 이것을 VMA라 한다.

VMA은 struct vm_area_struct에 자료구조로 정의되어 있으며, 아래와 같이 구현되어 있다.

struct vm_area_strcut

unsigned long vm_start; // start address within vm_mm
unsigned long vm_end;	// The first byte after our end address within vm_mm
unsigned long vm_flags;

unsigned long vm_pgoff;	// offset

struct file *vm_file;	// file we map to, vma에 해당하는 파일에 대한 포인터
void * vm_private_data;	// was vm_pte, 디바이스 드라이버가 데이터를 저장하는 변수
const strcut vm_opreations_struct *vm_ops; 	// 프로세스가 vma에 접근할 떄 호출할 함수

void (*open)(struct vm_area_struct * area);		// 레퍼런스 카운트 증가
void (*close)(struct vm_area_struct * area);	// 페이지 레퍼런스 카운트 감소

vm_fault_t (*fault)(struct vm_fault *vmf);	// 주소는 유효하나 메모리 상에 없는 상태에 호출되는 함수, page fault

프로세스 관점에서의 메모리 공간

각 프로세스는 소유한 메모리에 대한 것을 struct mm_struct 구조체로 관리한다. 여기엔 vma, 페이지 테이블 등 프로세스와 관련된 모든 정보가 들어간다. (mm_struct는 task_struct로부터 접근할 수 있다.)

그리고 각 프로세스는 프로세스 별로 고유한 주소 공간을 갖는다. 그리고 프로세스는 자신이 접근하는 가상 주소를 가진이 가진 페이지 테이블로부터 물리 주소로 변환한다. 다시 말해 가상 주소는 유일하지 않다. 서로 다른 프로세스 사이에서는 가상 주소가 같을 수도 있다. 가상 주소는 유일하지 않되 이는 물리적으로 유일한 물리 주소로 변환된다. 프로세스는 즉, "프로세스 별로 고유한 (가상의) 주소 공간을 갖는다."

메모리 맵 파일

  • 장점
    • 버퍼나 파일 처리를 위한 추가적인 자료구조가 필요 없다.
    • 대용량의 데이터를 처리할 때 매우 효율적이다. 파일의 크기가 크더라도 필요한 부분만 페이지로 불러와 작업
    • 전통적인 파일 입출력 API보다 빠르다.
  • 단점
    • 항상 페이지 크기의 정수배만 가능하기 때문에 내부 단편화 문제
    • 페이지 폴트로 인해 페이지를 새로 불러와야 할 때 오버헤드 발생

mmap()

리눅스에서는 mmap 시스템 콜을 사용하여 객체를 메모리에 매핑할 수 있다. mmap 시스템콜은 호출이 성공하면 매핑된 메모리 주소를 반환한다. 실패시 MAP_FAILED를 반환하고 errno를 적절한 값으로 설정한다.

#include <sys/mman.h>

void* mmap(void*, addr,	// 매핑될 주소
          size_t len,
          int prot,		// 메모리 보호 정책
          int flags,	// 매핑 유형과 그 동작에 관한 요소 명시
          int fd,
          off_t offset);	// 해당 offset 위치에서 len 바이트만큼 메모리에 매핑하도록 요청
  • 메모리 보호 정책

    : 메모리를 보호하기 위한 정책으로 다음 중 하나 이상을 OR 연산으로 묶을 수 있다.

    • PROT_NONE: 접근이 불가능한 페이지
    • PROT_READ: 읽기가 가능한 페이지
    • PROT_WRITE: 쓰기가 가능한 페이지
    • PROT_EXEC: 실행이 가능한 페이지
  • flags

    : 매핑할 메모리의 유형과 그 동작에 대한 요소를 명시하며, OR 연산으로 묶을 수 있다.

    • MAP_FIXED: mmap() 시스템콜의 addr 매개변수를 원하는 메모리 주소 공간을 알려준다는 목적을 넘어 해당 주소의 메모리 공간이 아니면 호출이 실패하게끔 확고히 커널에 요청
    • MAP_PRIVATE: 매핑된 메모리 공간의 쓰기가 발생하더라도 실제 파일과 해당 메모리 공간을 공유하고 있는 다른 프로세스에 반영하지 않는다. 원본을 훼손하지 않고, 수정된 복사본이 생김
    • MAP_SHARED: 같은 파일을 메모리에 매핑한 모든 프로세스와 매핑된 메모리 영역을 공유한다. 당연히 다른 프로세스에 의해 수정되었따면 해당 영역에 읽기를 할 때 반영된다.

예제

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
 
int main(int argc, char* argv[]) {
    struct stat sb;
    off_t len;
    char* p;
    int fd;
    
    if (argc < 2) {
        fprintf(stderr, "usage: %s [file] \n", argv[0]);
        return 1;
    }
    
    // 이하 에러 처리문 생략합니다.
    fd = open(argv[1], O_RDONLY);
    
    fstat(fd, &sb);
    if (!S_ISREG(sb.st_mode)) return 1;
    
    p = mmap(0, sb.st_size, PROT_READ, MAP_SHARED, fd, 0);
    
    for (len = 0; len < sb.st_size; ++len)
        putchar(p[len]);
    
    close(fd);
    munmap(p, sb.st_size); // 매핑된 메모리 영역을 해제합니다.
}
[참고자료]
profile
그냥 기록하는 블로그

0개의 댓글