Skip to content

프로세스 주소 공간

15. 프로세스 주소 공간

  • 커널은 사용자 공간 프로세스의 메모리도 관리해야 하며 이를 ‘프로세스 주소 공간’이라고 부른다.
  • 프로세스는 유효한 메모리 영역에만 접근해야 하며, 이를 어길시 segment fault를 만날 것이다.

15.1 메모리 서술자 구조체 mm_struct

struct mm_struct {
struct {
atomic_t mm_users;
/*
* Fields which are often written to are placed in a separate
* cache line.
*/
struct {
/**
* @mm_count: The number of references to &struct
* mm_struct (@mm_users count as 1).
*
* Use mmgrab()/mmdrop() to modify. When this drops to
* 0, the &struct mm_struct is freed.
*/
atomic_t mm_count;
} ____cacheline_aligned_in_smp;
struct maple_tree mm_mt;
...
}
};
  • <linux/mm_types.h>에는 mm_struct 라는 메모리 서술자 구조체가 정의돼있다.

    • mm_users: 이 주소 공간을 사용하는 프로세스의 개수를 의미한다.
    • mm_count: 이 구조체의 주 참조 횟수다.
      • 9개 스레드가 주소 공간을 공유한다면? mm_users == 9, mm_count == 1
      • mm_users == 0 -> mm_count를 하나 감소시킨다.
      • mm_count == 0 -> 이 주소 공간을 참조하는 놈이 하나도 없으니 메모리를 해제한다.
    • mmap, mm_rb: 동일한 메모리 영역을 전자는 연결리스트로, 후자는 레드-블랙 트리로 나타낸 것이다.
      • 왜 같은 대상을 중복 표현해서 메모리를 낭비하는 걸까?
      • 메모리 낭비는 있겠지만, 얻을 수 있는 이점이 있기 때문이다.
      • 전후 관계를 파악하거나 모든 항목을 탐색할 때는 연결리스트가 효율적이다.
      • 특정 항목을 탐색할 때는 레드-블랙 트리가 효율적이다.
      • 이런 방식으로 같은 데이터를 두 가지 다른 접근 방식으로 사용하는 것을 ‘스레드 트리’라고 부른다.
  • 이미 3장에서 task_struct를 배울 때 mm 멤버변수로 이 구조체를 봤었다.

    • 복습하자면, current->mm은 현재 프로세스의 메모리 서술자를 뜻하며,
    • fork()copy_mm() 함수가 부모 프로세스의 메모리 서술자를 자식 프로세스로 복사하며,
    • 복사할 때 12.4절에서 배운 ‘슬랩 캐시’를 이용해 mm_cachep에서 mm_struct 구조체를 할당한다.
    • 만일 만드는게 스레드라면, 생성된 스레드의 메모리 서술자는 부모의 mm을 가리킬 것이다.
    • 그리고 커널 스레드라면, 당연히 프로세스 주소 공간이 없으므로 mm == NULL이다.
      • (+ 추가내용: 커널 스레드가 종종 프로세스 주소 공간의 페이지 테이블 일부 데이터가 필요한 경우가 있다. 메모리 서술자의 mm == NULL일 때, active_mm 항목은 이전 프로세스의 메모리 서술자가 가리키던 곳으로 갱신된다. 따라서 커널 스레드는 이전 프로세스의 페이지 테이블을 필요할 때 사용할 수 있다.)

15.2 가상 메모리 영역 구조체 vm_area_struct

  • 리눅스 커널에서 ‘가상 메모리 영역’은 VMA라고 줄여 부르며 <linux/mm_type.h>vm_area_struct 구조체로 메모리 영역을 표현한다.
// https://github.com/torvalds/linux/blob/f2e8a57ee9036c7d5443382b6c3c09b51a92ec7e/include/linux/mm_types.h#L616
struct vm_area_struct {
/* The first cache line has the info for VMA tree walking. */
union {
struct {
/* VMA covers [vm_start; vm_end) addresses within mm */
unsigned long vm_start;
unsigned long vm_end;
};
#ifdef CONFIG_PER_VMA_LOCK
struct rcu_head vm_rcu; /* Used for deferred freeing. */
#endif
};
struct mm_struct *vm_mm; /* The address space we belong to. */
pgprot_t vm_page_prot; /* Access permissions of this VMA. */
/*
* Flags, see mm.h.
* To modify use vm_flags_{init|reset|set|clear|mod} functions.
*/
union {
const vm_flags_t vm_flags;
vm_flags_t __private __vm_flags;
};
const struct vm_operations_struct *vm_ops;
...
} __randomize_layout;
  • 주요 멤버 변수를 살펴보면 아래와 같다.
    • vm_start, vm_end: 가상 메모리 영역의 시작주소와 마지막 주소를 의미하므로 이 둘의 차이가 메모리 영역의 바이트 길이가 된다. 다른 메모리 영역끼리는 중첩될 수 없다.
    • vm_mm: VMA 별로 고유한 mm_struct를 보유한다. 동일 파일을 별도의 프로세스들이 각자의 주소 공간에 할당할 경우 각자의 vm_area_struct를 통해 메모리 공간을 식별하게 된다.
    • vm_flags: 메모리 영역 내 페이지에 대한 정보(읽기, 쓰기, 실행 권한 정보 등)를 제공한다.
    • vm_ops: 메모리 영역을 조작하기 위해 커널이 호출할 수 있는 동작 구조체 vm_operations_struct를 가리킨다. (13절 VFS를 설명할 때 언급했던 ‘동작 객체’ 구조체와 비슷한 개념이다.)

15.3. 실제 메모리 영역 살펴보기

  • 간단한 프로그램을 만들고, ‘/proc’ 파일시스템과 pmap 유틸리티를 통해 특정 프로세스의 주소 공간과 메모리 영역을 살펴보자.
Terminal window
[ec2-user@ip-x-x-x-x ~]$ echo -e "int main(int argc, char *argv[]) { while(1); }" > test.c
[ec2-user@ip-x-x-x-x ~]$ gcc -o test test.c && ./test &
[1] 1024914
[ec2-user@ip-x-x-x-x ~]$ cat /proc/1024914/maps
55ef85f2b000-55ef85f5a000 r--p 00000000 103:01 2253 /usr/bin/bash
55ef86c8c000-55ef86dad000 rw-p 00000000 00:00 0 [heap]
7ffb87000000-7ffb94530000 r--p 00000000 103:01 8524092 /usr/lib/locale/locale-archive
7ffb94600000-7ffb94ed4000 r--s 00000000 103:01 9692403 /var/lib/sss/mc/passwd
7ffb94fab000-7ffb95000000 r--p 00000000 103:01 443 /usr/lib/locale/C.utf8/LC_CTYPE
7ffb95000000-7ffb95028000 r--p 00000000 103:01 8524744 /usr/lib64/libc.so.6
7ffb95230000-7ffb95231000 r--p 00000000 103:01 1600 /usr/lib/locale/C.utf8/LC_NUMERIC
7ffb95231000-7ffb95232000 r--p 00000000 103:01 1603 /usr/lib/locale/C.utf8/LC_TIME
7ffb95232000-7ffb95233000 r--p 00000000 103:01 442 /usr/lib/locale/C.utf8/LC_COLLATE
7ffb95233000-7ffb95234000 r--p 00000000 103:01 446 /usr/lib/locale/C.utf8/LC_MONETARY
7ffb95234000-7ffb95235000 r--p 00000000 103:01 8524051 /usr/lib/locale/C.utf8/LC_MESSAGES/SYS_LC_MESSAGES
7ffb95235000-7ffb95236000 r--p 00000000 103:01 1601 /usr/lib/locale/C.utf8/LC_PAPER
7ffb95236000-7ffb95237000 r--p 00000000 103:01 447 /usr/lib/locale/C.utf8/LC_NAME
7ffb95237000-7ffb9523e000 r--s 00000000 103:01 2799 /usr/lib64/gconv/gconv-modules.cache
7ffb9523e000-7ffb95240000 r--p 00000000 103:01 9455124 /usr/lib64/libnss_sss.so.2
7ffb9524e000-7ffb9525c000 r--p 00000000 103:01 8522848 /usr/lib64/libtinfo.so.6.2
7ffb95283000-7ffb95285000 r--p 00000000 103:01 8524740 /usr/lib64/ld-linux-x86-64.so.2
7ffed54c8000-7ffed54e9000 rw-p 00000000 00:00 0 [stack]
7ffed55e0000-7ffed55e4000 r--p 00000000 00:00 0 [vvar]
7ffed55e4000-7ffed55e6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]
  • /proc/<pid>/maps 파일은 프로세스 주소 공간의 메모리 영역을 출력해준다.

  • pmap 유틸리티를 사용하면 위 정보를 조금 더 가독성 있게 표현해준다.

  • 지금까지 다룬 구조체의 구조를 깔끔하게 도식화한 그림이다.

    image
  • task_struct의 mm은 각 프로세스의 메모리 서술자인 mm_struct이다.

  • mm_struct의 mmap은 가상 메모리 영역 vm_area_struct을 표현하는 연결리스트다.

  • vm_area_struct는 프로세스의 실제 메모리 영역(.txt, .data 등)을 나타낸다.

image
  • 알다시피, 커널과 애플리케이션은 가상 주소를 사용하지만, 프로세서는 물리 주소를 사용한다.
    • 따라서 프로세서와 애플리케이션이 서로 상호작용하기 위해서는 페이지 테이블을 통해 변환작업이 필요하다.
  • 리눅스 커널은 PGD(Global), PMD(Middle), PTE 세 단계의 페이지 테이블을 사용한다.
  • 페이지 테이블 구조는 아키텍처에 따라 상당히 다르며 <asm/page.h>에 정의돼있다.

참고