본문 바로가기

Programming/Linux_Kernel

Device Driver - memory mapping

원문 : http://vincenthanna.springnote.com/pages/2267520?print=1


Device Driver - memory mapping

가상 주소와 MMU 

간단한 시스템은 물리 주소만으로도 동작할 수 있다. 그러나 다중프로세스를 지원하고 각 프로세스에 대해 메모리 공간을 보호해야 하는 운영체제는 물리 주소만으로 구현하기는 어렵다.

 

MMU는 프로세서에 전달되는 주소를 다른 주소로 변환한다. 그래서 프로세서가 메모리에 접근하는 주소가 메모리에 직접 전달되는 것이 아니라 먼저 MMU에 전달되고, MMU는 변환 테이블을 참고해 이 주소를 실제 물리 주소로 변환해 전달한다. 이때 프로세서가 MMU에 가상 주소를 전달하면, MMU가 이 가상 주소를 해석하여 나온 물리 주소를 실제 메모리에 전달한다.

 

MMU가 프로세서에서 전달된 가상 주소를 물리 주소로 변환하려면 변환 테이블이 있어야 한다. 그런데 이 테이블에 저장되는 정보가 사용하는 메모리와 주소를 32비트 주소 체계를 사용하는 시스템보다 메모리가 9배나 더 필요하다. 그래서 MMU는 주소를 1:1 대응시키지 않고, 페이지단위로 처리한다. 보통 1 페이지의 크기는 4kbyte로 리눅스에서는 PAGE_SIZE라는 매크로로 관리한다. 32비트 주소 공간을 모두 사용하면 4gbyte크기의 메모리를 사용할 수 있지만 ,이런 시스템은 드물며 실제로 필요한 MMU테이블의 크기는 작다. 256mbytes의 메모리를 관리하려면 256kbyte정도의 MMU table이면 된다 .이렇게 MMU테이블을 이용해 CPU에 필요한 주소가 실제 주소로 변환되는데, 이때 CPU가 요구하는 주소를 가상 주소라 하고, MMU를 통해 접근되는 주소를 물리 주소라고 한다. 

 

MMU에는 MMU테이블을 유지하기 위한 별도의 관리 메모리가 없다. MMU는 보통 프로세서에 내장되고 시스템 메모리를 같이 사용한다. 그래서 프로세서가 처음 부팅되면 리눅스는 시스템 메모리의 일부분을 MMU테이블에 할당하고, 관리할 정보를 MMU테이블에 기록한다. 이 과정에서는 MMU가 동작하지 않는다. 리눅스 커널은 MMU테이블에 관련된 정보를 메모리에 모두 기록한 후에 MMU테이블에 해당하는 메모리 위치를 MMU에 알려주고 MMU를 동작시킨다.

 

MMU 테이블은 MMU라는 하드웨어에 의존적이므로 리눅스 커널은 여러 아키텍처와 호환되도록 VM이라는 가상 메모리 관리 시스템을 사용한다. VM에서 관리하는 정보는 항상 MMU테이블에 적용된다. 리눅스 커널이 동작하는 초기 VM은 커널이 동작하는데 필요한 정보만 담고 있다가 프로세스가 생성되면 프로세스 동작에 필요한 메모리 관리 정보를 생성하고 MMU테이블을 갱신한다. 프로세스간의 메모리 참조는 원칙적으로는 불가능하다.하지만 예외적으로 프로세스가 커널 모드로 진입했을 경우에는 가능하다. 프로세스가 시스템 호출을 통해 커널 모드로 진입하면 프로세스 메모리 공간은 커널메모리 공간으로 바뀐다. 커널 모드 상태의 프로세스는 수퍼바이저 권한을 갖기 때문에 시스템 내의 모든 메모리 공간에 접근할 수 있다. 그래서 이 경우에는 다른 프로세스의 메모리 공간에 접근할 수 있는데, 이때 직접 접근은 안 되고 VM에서 제공하는 특정 함수를 통해 접근할 수 있다.

 

이렇게 MMU라는 장치가 있으면 주소 영역이 같은 프로세스가 수행되더라도 MMU에 의해 실제로 접근되는 물리적인 메모리 주소가 달라지기 때문에 프로세스마다 독립된 메모리 주소를 가질 수 있다. 또한 물리적으로 존재하지 않는 경우에는 보조 기억 장치로 구현하는 가상 메모리 시스템을 만들 수 있다.

 

리눅스에서 관리하는 가상 메모리 테이블에는 커널모드에서 동작하는 메모리 공간을 관리하는 커널 MMU메모리 테이블과 운영체제에서 수행되는 각 프로세스의 사용자 모드 상태의 메모리 공간을 관리하는 MMU메모리테이블이 있다. 또한 가상 메모리 공간은 이에 상응하는 물리적인 메모리 공간을 할당받는다. 

 

 

 

리눅스는 실제로 MMU를 효율적으로 처리하기 위해 커널 내부에서 관리하는 데이터 구조는 3단계 페이지 테이블이다.(PGD, PMD, PTE, OFFSET)

PGD는 1단계 페이지 테이블로 이는 가장 상위에 존재한다. pgd_t구조체인 각 엔트리는 좀더 세분화된 다음 2단계인 PMD의 엔트리를 가리키고 이 PMD는 pmd_t라는 엔트리를 가진다. 그리고 이 엔트리는 다시 3단계인 PTE를 가리킨다. 이 중 PMD는 대부분의 리눅스 시스템에 형식적으로만 존재한다. 커널 컴파일 단계에서 최적화 처리로 이 PMD를 사용하지 않으면 코드에서 제거된다. 시스템에 따라서는 이 PMD를 사용하는 경우가 있는데, 이때는 컴파일 단계에서 제거되지 않는다. 

 

물리 주소 공간을 커널 주소 공간으로 매핑

 

네트워크 필터와 같은 논리적인 디바이스 드라이버를 제외한 대부분의 디바이스 드라이버는 하드웨어를 제어한다. 그런데 디바이스 드라이버에서 하드웨어를 제어하려면 하드웨어에 관련된 I/O주소가 필요하다. i386계열의 프로세스는 in,out어셈블러 명령으로 제어할 수 있는 I/O주소 영역과 일반 메모리 처리 명령으로 접근할 수 있는 I/O주소 영역으로 나뉜다. in,out명령으로 제어하는 I/O주소 공간은 inb(), outb()와 같은 매크로 함수로 접근하며, 메모리 처리 명령으로 접근하는 주소 공간은 일반 함수 포인터로 접근할 수 있다. 일반 함수 포인터로 접근 가능한 예갸 비디오 버퍼 영역이다. 비디오 버퍼가 있는 0xB0000주소 영역에 데이터를 써 넣으려면 다음과 같은 형식으로 처리한다. 

 

char* videoptr;

videoptr = (cha*) 0x000B0000;

videoptr = 'A';

위 코드는 TEXT모드의 화면에 'A'라는 문자를 출력하는데 이렇게 메모리 접근 명령으로 처리하는 것을 memory-mapped I/O라 한다. i386계열 이외의 프로세스는 대부분 memory-mapped I/O방식으로 처리한다.

 

 

 

 

 

사용자 가상 주소 : 사용자 영역 프로그램이 보는 일반적인 주소

물리 주소 : 실제 메모리 주소
버스 주소 : 버스 주소는 프로세서가 사용하는 물리 주소와 동일하나 그럴필요는 없다. 
커널 논리 주소 : 일반적인 커널 주소 영역임. 대다수 아키텍처는 논리 주소와 연관된 물리 주소는 단지 상수값 차이만 있다. 

아래 매크로는 커널 논리 주소와 연관된 물리 주소를 반환한다.

#define __pa(x)((unsigned long)(x) - PAGE_OFFSET)

0x80000000 -> CONFIG_PAGE_OFFSET -> __PAGE_OFFSET -> PAGE_OFFSET 

 

커널 가상 주소  : 

    커널 논리 주소와는 달리 물리메모리와 1대1매핑이 아닐수도 있다. 

 

가상 메모리 영역(VMA)

    가상 메모리 영역은 프로세스 주소 영역의 독립적인 영역을 관리하기 위해 사용하는 커널 자료 구조이다. 

    "독자적인 속성으로 무장한 메모리 객체"

 

/proc/*/maps에서 각각의 필드는 이미지 이름을 제외하고 struct vm_area_struct에 있는 필드에 대응한다.

 

vm_area_struct : 

    사용자 영역 프로세스가 mmap을 호출하여 디바이스 메모리를 프로세스 주소 영역으로 사상할 때 시스템은 이 사상을 표현하는 새로운 VMA를 생성하는 방법으로 반응한다. 

 

mmap 디바이스 연산 

    디바이스를 사상한다는 말은 사용자 영역 주소 범위를 디바이스 메모리와 연결시킴을 의미한다. 

    mmap을 구현하기 위해서 드라이버는 단지 주소 범위를 위한 적절한 페이지 테이블을 구축하고 필요하다면 vma->vm_ops를 새로운 연산 집합으로 교체하는 작업만 하면 된다.

    드라이버가 단순하면서도 디바이스의 메모리를 사용자 주소 영역으로 선형 사상을 하고 싶다면 remap_pfn_range를 호출하는 작업 이외에는 특별히 할 일이 없다.

 

 

가상 메모리 영역

가상 메모리 영역(VMA)는 프로세스 주소 영역의 독립적인 영역을 관리하기 위해 사용하는 커널 자료 구조이다.  VMA는 독자적인 속성으로 무장한 메모리 객체.
vm_area_struct
사용자 영역 프로세스가 mmap을 호출해서 디바이스 메모리를 프로세스 주소 영역으로 사상할 때 시스템은 이 사상을 표현하는 새로운 VMA를 생성하는 방법으로 반응한다.
unsigned long vm_pgoff 페이지에서 파일 영역의 오프셋을 가리킨다. 파일이나 디바이스를 사상하면, 이 값은 이 영역에 사상된 첫 페이지의 파일 크기가 된다.
unsigned long vm_flags 이 영역을 기술하는 플래그의 집합이다. VM_IO는 VMA를 메모리 사상 I/O영역으로 취급하게 표시한다. VM_RESERVED는 메모리 관리 시스템에게 이 VMA를 스왑아웃하지 않도록 요청한다. VM_RESERVED는 대부분 디바이스 사상에 필요한 플래그이다.

프로세스 메모리 사상
메모리 관리 퍼즐의 마지막 조각은 프로세스 메모리 사상 구조체로서, 모든 다른 자료구조체를 함께 포함하고 있다. 시스템에서 각 프로세스는(몇몇 커널 영역 보조 스레드를 제외하면) (<linux/sched.h>에 정의되어 있는) struct mm_struct 를 포함한다. 이 구조체는 프로세스의 가상 메모리 영역, 페이지 테이블, 메모리 관리 기록 정보를 위한 다양한 비트와 세마포어(mmap_sem), 스핀락(page_table_lock)도 포함하고 있다. 이 구조체를 가리키는 포인터는 task구조체에서 발견할 수 있다. 아주 드물게 드라이버가 이 구조체에 접근할 필요가 있으며, 일반적으로 current->mm을 사용한다. 메모리 관리 구조체는 프로세스 사이에 공유될 수 있다는 사실에 주의한다. 예를 들어 리눅스는 이러한 방식으로 스레드 동작을 구현한다.

 

mmap 디바이스 연산

드라이버와 관련된 범위에서 메모리 사상은 사용자 프로그램에 디바이스 메모리를 직접 접근하도록 제공하기 위해 구현할 수 있다.
디바이스를 사상한다는 말은 사용자 영역 주소 범위를 디바이스 메모리와 연결시킴을 의미한다. 프로그램이 할당받은 주소 영역에 읽거나 쓸 때마다 실제로는 디바이스에 접근한다.
사상은 PAGE_SIZE단위로 이루어져야 한다. 따라서 사상된 영역은 반드시 PAGE_SIZE의 배수여야 하며, 물리 메모리는 PAGE_SIZE의 배수인 주소에서 시작해야만 한다.
시스템 호출은 다음과 같이 선언한다.

mmap( caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);

다른 한편으로 커널 파일 연산 자료 구조체에서는 다음과 같이 선언한다.
int (*mmap) (struct file* filp, struct vm_area_struct* vma);

mmap을 구현하기 위해서 드라이버는 단지 주소 범위를 위한 적절한 테이블을 구축하고 필요하다면 vma->vm_ops 를 새로운 연산 집합으로 교체하는 작업만 하면 된다.
페 이지 테이블을 구축하는 두 가지 방법이 존재한다. remap_pfn_range로 불리는 함수를 사용해서 한방에 해결하거나, nopage VMA메소드를 사용해서 필요할 때 페이지를 만들어내는 방법이 있다. 각 메소드는 장단점이 있다.

 

 

 

nopage를 사용한 메모리 사상

비록 전부는 아니더라도, remap_pfn_range가 대다수 드라이버를 위한 mmap구현 과정에서 제대로 동작하지만, 종종 좀 더 유연할 필요가 있다. 이러한 상황에서는 nopage VMA메소드를 사용한 구현 방식이 필요하다.
nopage방식은 PAGE_SIZE단위로 매핑을 처리한다.

nopage 방식은 응용 프로그램에서 mmap()함수를 호출하여 프로세스에서 사용할 수 있는 주소를 먼저 요구한다. nopage방식은 remap_pfn_range()함수를 이용하는 방법처럼 응용프로그램이 mmap()함수를 호출하면 디바이스 드라이버의  file_operation구조체에 정의된 mmap함수가 호출된다.
그러나 mmap()함수가 요청된 영역의 매핑을 remap_pfn_range()함수를 이용해 처리하는 것과는 달리 nopage방식에서는 mmap()함수가 remap_pfn_range()함수를 수행하지 않는다. 그래서 커널이 해당  영역을 매핑하지 않기 때문에 응용 프로그램이 mmap()을 통해 주소에 접근하면 해당 메모리 주소를 유효하지 않은 영역으로 인식하여 page fault 가 발생한다.
커널은 page fault가 발생하면 다비아스 드라이버로 매핑하기 위해 해당 주소 공간이 예약된 주소 영역인지를 확인하고, 예약된 영역이면 vma->vm_ops->nopage에 선언된 함수를 호출한다. 이 함수는 페이지 폴트가 발생한 가상 주소 페이지의 물리 주소 페이지를 디바이스 드라이버에 요청한다. 여기서 vma는 vm_area_struct 구조체 변수고, vm_ops는 vm_operation_struct 구조체 변수다. 디바이스 드라이버에 선언된 nopage()함수는 요청된 영역에 대한 검사를 수행하고 성공적으로 수행되어 해당 영역에 해당하는 물리 주소를 반환하면 응용프로그램은 해당 영역의 페이지 폴트에서 벗어난다.

nopage 방식은 물리적인 I/O메모리 공간을 응용프로그램의 프로세스 공간에 사용하기보다는 주로 디바이스 드라이버에 의해 할당된 메모리 공간을 공유하기 위해 사용한다.다 비이스 드라이버와 메모리를 공유하는 대표적인 경우가 DMA버퍼 공간이다. 사운드 장치처럼 최근에 많이 사용되는 멀티미디어 장치들은 많은 데이터를 하드웨어에 전달해야 하기 때문에 DMA방식을 지원한다. 이 DMA에 사용되는 메모리 공간은 kmalloc()이나 __get_free_page()를 이용해 할당한다.  이렇게 할당된 메모리는 응용프로그램에서 직접 다루어야 하므로 mmap이 필요한데 이때 RAM공간에 대한 매핑은 주로 nopage방식을 사용한다. 그리고 nopage방식으로 다룰 때 가장 편리한 것이 바로 vmalloc()함수로 할당한 공간이다.


함수 형식
struct page* xxx_nopage(struct vm_area_struct* vma, unsigned long addr, int* type);

이 함수는 addr매개변수에 전달된 가상 주소에 대응하는 물리주소 페이지를 반환하도록 작성돼야 한다. 그리고 vm_area_struct구조체 변수 vma와 매핑영역의 선두 주소인 addr그리고 page fault처리 타입을 반환하기 위한 type이 매개변수로 전달된다.

unsigned long addr
프로세스 메모리 공간에 매핑할 주소공간이 넘어오는데 이 주소는 프로세스 메모리 공간에 유효한 가상 주소다. nopage()함수는 addr에 대응하는 물리 주소 페이지를 반환해야 하며 이 반환값은 struct page*타입이어야 한다.
addr은 PAGE_SIZE aligned 되서 넘어온다. nopage함수는 이렇게 전달된 addr값이 가장 먼저 매핑 영역에서 벗어나는지를 검사해야 한다.

int* type
page fault의 처리 종류를 반환할 수 있는 주소를 전달하는데 보통 디바이스 드라이버는 VM_FAULT_MINOR값을 반환한다 VM_FAULT_MINOR값은 고정된 페이지 테이블의 매핑처리를 의미한다.

함수 구현 예 : vmalloc()함수로 mapping_memory변수에 MAPPING_SIZE만큼 할당한 메모리를 nopage방식으로 응용 프로그램의 프로세스 공간에 매핑한다.

struct page* xxx_nopage(struct vm_area_struct* vma, unsigned long addr, int* type) {
struct page* page;
unsigned long offset;
void* page_ptr;

//매핑 대상이 되는 메모리의 유효성을 검사한다.
if (mapping_memory == NULL) return NOPAGE_SIGBUS;

//매핑 대상의 영역에서 벗어나는지를 검사한다.
offset = addr - vma->vm_start;
if (offset >= MAPPING_SIZE ) return NOPAGE_SIGBUS;

//페이지의 물리 주소를 구한다.
page_ptr = mapping_memory + offset;
page = vmalloc_to_page (page_ptr);

//페이지 사용수를 증가시킨다. 이러지 않으면 해당 페이지가 매핑되더라도 프로세스가 종료되어 매핑 영역을
 //해제할 때 문제가 발생한다. 특정 페이지의 사용수를 증가시킬때는 get_page()를 사용한다.
get_page(page);
if (type) *type = VM_FAULT_MINOR;
return page;
}

nopage()함수에서는 보통 다음 세 가지를 구현해야 한다.
- 요구한 addr에 대한 영역 검사
- 요구한 addr에 대한 물리 주소 페이지 획득
- 구한 물리 주소 페이지의 참조수 증가

 

 

 

 

 

 


'Programming > Linux_Kernel' 카테고리의 다른 글

linux 에서 Movi Nand, MMC Control 흐름도  (0) 2010.05.14
dd, tail 명령어  (0) 2010.05.14
Kprobes를 이용한 커널 디버깅  (0) 2010.04.27
flush_dcache_page와 kmap_atomic  (0) 2010.04.26
L1, L2 cache  (0) 2010.04.26