사용자 공간에서 접근하는 주소, 사용자 프로세스가 접근하는 주소는 모두 가상 주소이다.
프로세서와 메모리 사이에서 사용하는 물리적인 주소
주변 장치와 메모리가 서로 데이터를 주고 받기 위해서 사용하는 주소이다. 실제로 PCI와 같은 경우 memory-mapped IO를 수행하여 데이터를 읽거나 쓴다.
커널은 자신이 사용할 일정한 크기의 메모리 영역을 일대일로 매핑해 놓는다. (단, 시작 주소는 정해져있고 그 주소에 대한 offset으로 일대일 매핑한다.) 따라서 커널은 항상 일정한 영역의 Physical Address를 커널 Logical Address로 매핑하여 Kernel Logical Address에 접근할 수 있다. 주의할 점은 kmalloc은, kernel logical 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는 kernel virtual address와 kernel logical address를 의미한다.
Low Memory: logical address가 존재하는 메모리 공간
High Memory: logical address를 갖고 있지 않는 메모리 공간
둘의 차이점은 Low Memory는 가상 주소를 물리 주소로 변환하는 매핑이 이미 존재하나, High memory는 별도의 매핑을 해야만 접근할 수 있다는 점이다. 보통 사용자 공간의 메모리에 접근할 때 High Memory에 매핑해야만 커널에서 사용자 공간에 접근할 수 있다.
현대적인 운영체제는 메모리 관리 기법으로 페이징을 사용한다. 페이징은 메모리를 페이지라는 작은단위로 나누어서 관리하는 기법이고, 페이지 하나의 사이즈는 매우 아키텍처에 의존적이지만 보통 4KB or 8KB이다.
페이징 기법을 사용하는 경우 주소는 두 부분으로 나눌 수 있따. 하나는 페이지 번호(PFN, page frame number)이고, 나머지는 페이지 내의 offset이다. 예를 들어서 32bit 아키텍처에서 PAGE_SIZE == 4KB인 경우, 상위 20비트는 PFN 하위 12비트는 offset이다.
옛날에는 High Memory가 존재하지 않았었다. 그래서 logical address와 physical address가 일대일로 매핑되어서 어떤 페이지를 가리킬 때 logical address를 바로 사용할 수 있었따. 하지만 high memory가 생기면서 이게 불가능하게 되었고, 따라서 커널 내에서 logical address를 그대로 사용하기 보다 struct page에 대한 포인터를 사용하게 되었다.
struct page는 페이징 기법에서 페이지 하나를 나타내기 위한 구조체이며 해당 페이지에 대한 모든 정보가 들어있다.
#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 구조체에 추가된다.
위에서는 주소를 특성에 따라 나누었다. 하지만 실제로 프로세스의 메모리 영역들을 생각해보면 text, data, heap, stack 등등 특성에 따라 메모리 영역을 나눌 수 있다. 이렇듯 메모리 영역은 특성에 따라 분류할 수 있으며 이것을 VMA라 한다.
VMA은 struct vm_area_struct에 자료구조로 정의되어 있으며, 아래와 같이 구현되어 있다.
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로부터 접근할 수 있다.)
그리고 각 프로세스는 프로세스 별로 고유한 주소 공간을 갖는다. 그리고 프로세스는 자신이 접근하는 가상 주소를 가진이 가진 페이지 테이블로부터 물리 주소로 변환한다. 다시 말해 가상 주소는 유일하지 않다. 서로 다른 프로세스 사이에서는 가상 주소가 같을 수도 있다. 가상 주소는 유일하지 않되 이는 물리적으로 유일한 물리 주소로 변환된다. 프로세스는 즉, "프로세스 별로 고유한 (가상의) 주소 공간을 갖는다."
리눅스에서는 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 연산으로 묶을 수 있다.
flags
: 매핑할 메모리의 유형과 그 동작에 대한 요소를 명시하며, OR 연산으로 묶을 수 있다.
#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); // 매핑된 메모리 영역을 해제합니다.
}