본문 바로가기

Linux Kernel

[Linux Kernel] 주소와 메모리 공간

반응형

이 글은 LDD3을 공부하면서 정리하였고 필요한 내용을 그때그때 추가했다. 사용자 프로세스 관점에서 바라보는 주소 공간과 커널 관점에서 바라보는 것은 매우 다르기 때문에 이를 이해할 필요가 있다. 이 글을 읽으려면 페이징, 가상 파일 시스템, 그리고 Memory-Mapped IO를 어느 정도 이해하고 있어야 한다.

 

처음엔 커널에서 왜 무작정 userspace의 주소에 접근하면 안되는지 이해하지 못했고, 왜 그러면 안되는지도 몰랐는데 이걸 공부하고 나니까 이해가 됐다. 역시 Greg 아저씨 짱짱맨... ㄹㅇ... 발끝이라도 따라가고 싶다.

 

이 글에선 커널과 사용자 프로세스의 관점에서 주소와 메모리 공간이 어떻게 이루어져있는지 살펴볼 것이다. 다음 글에선 이번 글에서 다룬 내용을 바탕으로 디바이스 드라이버에서 어떻게 mmap 시스템 호출을 구현하는지 알아볼 것이다.

다양한 유형의 주소

리눅스 커널 안에선 다양한 종류의 메모리 주소가 존재한다. 주소는 그냥 주소 아닌가? 싶지만, 메모리 주소 간에도 서로 다른 특징이 존재한다.

User Virtual Address

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

Physical Address

프로세서와 메모리 사이세서 사용하는 ㅡ 실제로 물리적으로 존재하는 주소다.

Bus Address

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

Kernel Logical Address

커널은 자신이 사용할 일정한 크기의 메모리 영역을 일대일로 매핑해놓는다. (단, 시작 주소는 정해져있고 그 주소에 대한 offset으로 일대일로 매핑한다) 따라서 커널은 항상 일정한 영역의 Physical Address를 Kernel 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 Virtual Address가 Kernel Logical Address는 아니다. Kernel Logical Address를 갖지 않는 Kernel Virtual Address를 "High Memory"라고 부른다.

High Memory와 Low Memory에 관하여

커널 관련 문서를 읽다보면 high memory, low memory라는 표현이 자주 보이는데, 이는 Kernel Virtual Address와 Kernel Logical Address를 의미한다. LDD3에 따르면 정의는 다음과 같다.

 

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

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

 

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

 

kernel virtual/logical address를 나타낸 그림이다.

Paging과 Phyiscal Address

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

 

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

Memory Map과 struct page

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

 

struct page는 페이징 기법에서 페이지 하나를 나타내기 위한 구조체이며 해당 페이지에 대한 모든 정보가 들어있다. include/linux/mm_types.h에 정의되어있긴 한데, 매우 복잡하므로 주요 필드만 한 번 살펴보자.

atomic_t   _refcount

      /* Usage count. *DO NOT USE DIRECTLY*. See page_ref.h */                  
      atomic_t _refcount;    

현재 페이지를 사용하는 횟수를 나타낸다. 0이 되면 어디에서도 사용하지 않으므로 free list로 돌아간다. 책 기준에서는 count라고 정의되어있었는데, 이후에 직접 참조하지 말라고 바꾼 것 같다.

void *virtual

      /*                                                                        
       * On machines where all RAM is mapped into kernel address space,         
       * we can simply calculate the virtual address. On machines with          
       * highmem some memory is mapped into kernel virtual memory               
       * dynamically, so we need a place to store that address.                 
       * Note that this field could be 16 bits on x86 ... ;)                    
       *                                                                        
       * Architectures with slow multiplication can define                      
       * WANT_PAGE_VIRTUAL in asm/page.h                                        
       */                                                                       
#if defined(WANT_PAGE_VIRTUAL)                                                  
      void *virtual;                /* Kernel virtual address (NULL if          
                                 not kmapped, ie. highmem) */                   
#endif /* WANT_PAGE_VIRTUAL */

매핑이 존재하는 경우 가상 주소이며, 아닌 경우 NULL이다. WANT_PAGE_VIRTUAL이 정의된 경우(가상 주소를 단순하게 변환하기 어려운 경우)에만 page 구조체에 추가된다. 

unsigned long flags

      unsigned long flags;          /* Atomic flags, some possibly              
                               * updated asynchronously */  

말 그대로 page의 상태를 나타내기 위한 플래그이며, 실제 값은 include/linux/page-flags.h에 enum으로 정의되어있다. struct page는 훨씬 더 복잡하지만.. 우리가 memory management쪽의 메인테이너가 될 건 아니므로 전부 이해할 필요는 없다.

페이지 관련 API

커널은 모든 물리 메모리상의 페이지에 대한 정보를 mem_map 일차원 배열에 저장한다. mem_map에 직접적으로 접근하는 건 좋은 방법이 아니다. 예를 들어 (NUMA, Non-Uniform Memory Access) 환경인 경우에는 mem_map이 일차원 배열이 아닐 수 있다. 따라서 mem_map에 직접 접근하는 대신 커널은 관련 API를 제공한다.

struct page *virt_to_page(void *kaddr)

가상 주소를 page로 변환한다.

struct page *pfn_to_page(int pfn)

앞서 말한 PFN을 page로 변환한다.

void *page_address(struct page *page)

page를 가상 주소로 변환한다. 단, 가상 주소로 매핑이 되어있어야 한다.

kmap

앞에서 살펴본 page_address가 실제 매핑된 주소를 가져오는 API라면, 매핑이 존재하지 않는 영역에 새로 매핑을 해야할 수도 있다. 이때 kmap과 kunmap을 사용한다. kmap, kunmap은 include/linux/highmem.h에 존재한다.

/**                                                                             
 * kmap - Map a page for long term usage                                        
 * @page:   Pointer to the page to be mapped                                    
 *                                                                              
 * Returns: The virtual address of the mapping                                  
 *                                                                              
 * Can only be invoked from preemptible task context because on 32bit           
 * systems with CONFIG_HIGHMEM enabled this function might sleep.               
 *                                                                              
 * For systems with CONFIG_HIGHMEM=n and for pages in the low memory area       
 * this returns the virtual address of the direct kernel mapping.               
 *                                                                              
 * The returned virtual address is globally visible and valid up to the         
 * point where it is unmapped via kunmap(). The pointer can be handed to        
 * other contexts.                                                              
 *                                                                              
 * For highmem pages on 32bit systems this can be slow as the mapping space     
 * is limited and protected by a global lock. In case that there is no          
 * mapping slot available the function blocks until a slot is released via      
 * kunmap().                                                                    
 */                                                                             
static inline void *kmap(struct page *page);                                    
                                                                               

kmap은 struct page에 대한 포인터를 받고, 이에를 매핑해서 해당하는 가상 주소를 리턴한다. 주의할 점은 주석에도 나와있지만 preemtible task context - 선점 가능한 프로세스 컨텍스트에서만 호출할 수 있다.

kunmap

/**                                                                             
 * kunmap - Unmap the virtual address mapped by kmap()                          
 * @addr:   Virtual address to be unmapped                                      
 *                                                                              
 * Counterpart to kmap(). A NOOP for CONFIG_HIGHMEM=n and for mappings of       
 * pages in the low memory area.                                                
 */                                                                             
static inline void kunmap(struct page *page);

kmap에서 매핑했던 것을 되돌린다. kmap_atomic / kunmap_atomic도 있긴 한데 deprecated이므로 패스.

Virtual Memory Area (VMA)

앞에서 우리는 주소를 주소 자체를 특성에 따라서 나눌 수 있다고 했다. 하지만 실제로 프로세스의 메모리 영역들을 생각해보면 text, data, heap, stack 등등 특성에 따라서 메모리 영역을 나눌 수 있다. 이렇듯 메모리 영역은 특성에 따라서 분류할 수 있으며 이것을 VMA (가상 메모리 공간, Virtual Memory Area)라고 한다.

 

procfs를 통해 간단하게 vma 들을 확인할 수 있다. /proc/<pid 번호>/maps를 치면 된다. self는 자기 자신이라 아래 사진에선 cat의 vma가 나왔다. 형식은 다음과 같다.

 

start-end perm offset major:minor inode image

start-end: 시작과 끝

perm: r (read), w (write), x (execute), p or s (private to process, or shared)

offset: 주소가 시작되는 offset

major:minor : 불러온 파일을 들고있는 디바이스의 major, minor 번호

inode: 불러온 파일에 대한 inode

image: 불러온 파일 이름

 

모든 vma가 그런 것은 아니지만, 메모리 영역이 어떤 파일의 내용을 담고 있는 경우도 있다. 예를 cat이란 프로그램을 실행하여 프로세스가 되면 코드 영역은 /usr/bin/cat으로부터 가져왔을 것이다.

 

자 이제 vma에 대한 개념이 어느 정도 잡혔다. 그럼 이에 해당하는 struct vm_area_struct (include/linux/mm_types.h)를 살펴보자. 위에서 /proc/self/maps로 살펴본 항목과 크게 다르지 않다. 물론.. page 구조체처럼 엄청 복잡하므로 중요한 부분만 보자.

struct vm_area_struct

unsigned long vm_start, unsigned long vm_end

      unsigned long vm_start;       /* Our 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_flags;       /* Flags, see mm.h. */                      

unsigned long vm_pgoff, struct file *vm_file, void *vm_private_data

      /* Information about our backing store: */                                
      unsigned long vm_pgoff;       /* Offset (within vm_file) in PAGE_SIZE     
                                 units */                                       
      struct file * vm_file;        /* File we map to (can be NULL). */         
      void * vm_private_data;       /* was vm_pte (shared mem) */  

vm_file은 vma에 해당하는 파일에 대한 포인터를 가리키고, vm_pgoff는 파일 내의 offset이다. vm_private_data는 디바이스 드라이버가 데이터를 저장하는 변수이다.

strcut vm_operations_struct *vm_ops

      /* Function pointers to deal with this struct. */                         
      const struct vm_operations_struct *vm_ops;

프로세스가 vma에 접근할 때 (이 vma에 해당하는 페이지를 사용할 때) 호출될 함수를 struct vm_operations_struct로 정의할 수 있다. 이것도 함수가 많은데 몇개만 살펴보자면 다음과 같다.

 void (*open)(struct vm_area_struct * area)

페이지의 레퍼런스 카운트가 증가할 때 호출되는 함수이다.

 void (*close)(struct vm_area_struct * area)

페이지의 레퍼런스 카운트가 감소할 때 호출되는 함수이다. 주의하자. file_operations의 release와는 다르다. release는 레퍼런스 카운트가 0이 될때 호출되고, close는 레퍼런스 카운트가 감소할 때마다 호출된다. 

vm_fault_t (*fault)(struct vm_fault *vmf)

그리고 nopage라는 함수도 있었는데 접근한 페이지의 주소가 유효하나 메모리 상에 없는 상태(page fault)에 호출되는 함수이다. https://lwn.net/Articles/258113/ 를 보면 fault로 바뀌었단걸 알 수 있다. page fault가 발생할 때 사용된다.

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

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

 

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

 

 

Linux Device Drivers book - Bootlin

A must-have book for people creating device drivers for the Linux kernel! Now available in a single PDF file. Linux Device Drivers from Jonathan Corbet, Alessandro Rubini and Greg Kroah-Hartmann, is the book anyone interested in writing Linux device driver

bootlin.com

 

Difference between Kernel Virtual Address and Kernel Logical Address?

I am not able to exactly difference between kernel logical address and virtual address. In Linux device driver book it says that all logical address are kernel virtual address, and virtual address ...

stackoverflow.com

 

What are high memory and low memory on Linux?

I'm interested in the difference between Highmem and Lowmem: Why is there such a differentiation? What do we gain by doing so? What features does each have?

unix.stackexchange.com

 

the only overhead incurred by fork is page table duplication and process id creation

The only overhead incurred by fork() is the duplication of the parent’s page tables and the creation of a unique process descriptor for the child. In Linux, fork() is implemented through the use o...

stackoverflow.com