Skip to content
Kyoseung Koo edited this page Apr 28, 2015 · 5 revisions

3. 테스크 관리

8. 문맥 교환

문맥 교환이란?

수행 중이던 task에게 할당되어 있는 타임 슬라이스가 모두 소진 or 특정 사건을 기다리기 위해 잠들어야 하는 경우 리눅스 커널이 새로이 수행할 task를 선택하여 CPU 자원을 배정해주는 과정에서 task의 동작을 먹추고 다른 task로 전환하는 과정문맥 교환이라고 함.

thread_struct

이러한 문맥 교환시 저장하는 사항들은 task가 문맥교환 되는 시점에 어디까지 수행했는지, 현재 CPU의 레지스터 값(H/W Context)은 얼마인지 등의 정보를 저장하게 된다. 이 때 이런 값들은 thread_struct에 저장되어지게 된다. 실제로 저장되는 값들을 살펴보면

  • task가 어떤 명령어까지 수행하였는지, 다음에 수행해야 할 명령어가 어디인지 저장하는 pc 레지스터 (program counter register, intel CPU의 경우 eip register)
  • task를 중지할 때의 스택의 사용 위치를 저장하는 sp 레지스터(stack pointer register)
  • task가 실행 중에 이용한 CPU의 범용 레지스터 값들을 저장하는 장소
  • CPU의 상태를 나타내는 eflags
  • 세그먼트를 관리하는 cs, ds 등의 레지스터 내용

등의 정보를 저장하게 되어있다. 실제로 리눅서 커널에서의 이런 thread_struct는 아키텍쳐 마다 다르게 정의되어 있다. 이 이유에 대해서는 아마 CPU나 프로세서 마다 다르게 설계되어 있기 때문이라고 생각된다.

./arch/$(ARCH)/include/asm/processor.h

대부분 위의 경로에서 확인할 수 있다.

9. 태스크와 시그널

시그널

태스크에게 비동기적인 사건의 발생을 알리는 메커니즘시그널이라고 한다. 이러한 signal의 역할에서 필요한 기능들을 간추려 보면

  • 다른 task에게 signal을 보낼 수 있어야 한다.
  • 자신에게 signal이 오면 그 signal을 수신할 수 있어야 한다.
  • 자신에게 signal이 오면 그 signal을 처리할 수 있는 함수를 지정할 수 있어야 한다.

이러한 signal들은 구조체로 정의되어 큐에 등록되는 방식으로 작용된다. 이는 task_struct 에 저장되고, signal들의 공유가 필요한 매커니즘은 signal 필드에, 특별한 task에게만 시그널을 보내야 하는 경우에는 pending 필드에 저장하여 사용된다.

./include/asm-$(ARCH)/signal.h

리눅스에서 지원하는 signal 목록은 위의 위치에서 확인할 수 있다.

시그널 핸들러

각각의 task에서는 특정 signal이 발생했을 때 수행될 함수, 시그널 핸들러를 설정할 수 있다. 이는 task_struct 구조체의 sighand 필드에 저장된다.

task에서 특정 signal을 받지 않도록 설정할 수도 있는데 이는 task_struct 구조체의 blocked 필드에 signal 번호에 해당하는 비트를 저장함으로써 설정할 수 있다. 다만 SIGKILL, SIGSTOP*이라는 시그널은 받지 않도록 설정할 수 없다.

시그널을 보내는 과정

signal은 다음과 같은 방식으로 task에 보내지게 된다.

  1. 해당 task의 task_struct를 찾아낸다.
  2. 보내려는 signal의 번호를 통해 siginfo 자료 구조를 초기화 한다.
  3. signal의 매커니즘에 따라 signal이나 pending 필드에 저장한다. 이 때 blocked 필드에 등록된 signal인지 검사한다.

또한 이런 signal들은 다음과 같은 방식으로 실행되게 된다.

  1. 수신한 signal의 처리는 task가 커널 수준에서 사용자 수준 실행 상태로 전이할 때 이루어진다.
  2. 커널은 signal이나 pending 필드의 count를 검사한다.
  3. count의 값에 따라 두가지 경우로 나뉘게 된다.
  • 0이 아니라면 어떤 signal이 대기중인지 검사하고, blocked 필드에 등록되지 않은 signal이라면 수행한다. 만약 task가 명시적으로 핸들러를 등록하지 않을 경우 디폴트 액션(SIGKILL, SIGSTOP, SIGIGN)을 취하게 된다.
  • 0이라면 넘어가겠죠~~

4. 메모리 관리

1. 메모리 관리 기법과 가상 메모리

가상 메모리

컴퓨터가 개발되는 초창기부터 사용자는 시스템에 물리적으로 존재하는 것보다 많은 양의 메모리를 필요로 해 왔다. 이런 메모리 부족 현상을 극복하기 대부분의 시스템이서 사용되는 방법이 가상 메모리를 사용하는 방법이다.

가상 메모리는 실제 시스템에 존재하는 물리 메모리의 크기와 관계없이 가상적인 주소 공간을 사용자 task에게 제공한다. 32비트 CPU의 경우 2^32 = 4GB, 2^64 = 16EB의 가상 메모리 주소 공간을 제공한다.

주의할 점은 물리적으로 이 가상 메모리를 모두 사용자 task에게 제공하지 않는다는 것이다.

2. 물리 메모리 관리 자료 구조

복수 개의 CPU를 가지고 있는 컴퓨터 시스템 중 모든 CPU가 메모리와 입출력 버스 등을 공유하는 구조를 **SMP(Symmetric Multiprocessing)**라 부른다. 이러한 구조에서는 병목 현상이 발생하기 쉽기 때문에 **NUMA(Non-Uniform Memory Access)**라는 CPU들을 몇 개의 그룹으로 나누고 각각의 그룹에게 별도의 지역 메모리를 주는 구조를 통해 관리하게 되었다. 이전의 기존 시스템은 UMA이라 부른다.

Node

리눅스에서는 접근 속도가 같은 메모리의 집합뱅크라고 부른다. UMA상에서는 한 개가, NUMA상에서는 복수개의 뱅크가 존재하게 되는데, 이러한 뱅크는 Node의 구조를 가지고 있다. 이러한 Node를 통해 리눅스는 하드웨어 시스템에 관계없이 노드라는 일관된 자료구조를 통해 전체 물리메모리를 접근할 수 있게 된다.

이러한 노드는 pg_data_t라는 구조체를 통해 표현된다. 이 구조체 안에는

  • 해당 노드에 속해있는 메모리의 실제 양
  • 해당 물리메모리가 메모리 맵의 몇 번지에 위치하고 있는지를 나타내늘 변수
  • zone 구조체를 담기 위한 배열과 변수 등이 선언되어 있다.

Node의 구조는 다음의 파일에서 확인할 수 있다.

./include/linux/mmzone.h

Zone

node의 일부분을 따로 관리할 수 있도록 만들어 놓은 것Zone이다. 이는 일부 ISA/PCI 버스 기반 디바이스의 경우 정상적인 동작을 위해서는 반드시 물리메모리 중 16MB 이하 부분을 할당해 줘야 하는 경우나 커널이 실제 메모리 어드레스에 매핑되지 못했을 경우 발생하는 일에 대응하기 위해서 고안되었다.

32bit 아키텍쳐 아래에서 대부분 Node에서의 Zone은 다음과 같이 구성되어 있다.

  • 16MB 이하 부분
    • ZONE_DMA, ZONE_DMA32
    • 일부 ISA/PCI 기반 디바이스가 필요로 하는 메모리 범위
  • 그 이상부터 896MB까지
    • ZONE_NORMAL
    • 커널에 의해 직접 매핑된 메모리 범위
  • 그 이상의 나머지들
    • ZONE_HIGHMEM
    • 커널에 의해 매핑되지 않은 가용 메모리

물론 이 zone들은 필요하지 않다면 한개의 zone으로 존재할 수 있다.

zone 구조체에는

  • 해당 zone에 속해있는 물리 메모리의 시작주소와 크기
  • 버디 할당자가 사용할 구조체를 담는 변수 등이 존재하게 된다.

Zone의 구조 역시 Node 구조체가 선언된 파일에서 zone이라는 이름의 구조체로 확인할 수 있다.

Page frame

각각의 Zone에 속해 있는 물리 메모리들의 최소 단위페이지 프레임이라고 한다.

페이지 프레임을 구성하는 page 구조체는 다음 파일에 정의되어 있다.

./include/linux/mm_types.h

3. Buddy와 Slab

리눅스에서는 메모리를 물리 메모리의 최소 관리 단위인 page frame 단위로 할당하도론 결정되었다. 따라서 대부분 4KB가 최소 할당단위가 된다. 하지만 2KB나 64KB같이 이러한 단위보다 작거나 큰 메모리를 요청한다면 문제가 있기 때문에 리눅스에서는 메모리 할당자를 도입하게 되었는데, 큰 단위는 버디 할당자로, 작은 단위는 슬랩 할당자를 이용해 관리하게 되었다.

버디 할당자(Buddy Allocator)

버디 할당자버디 메모리 할당이라는 알고리즘에 의해 메모리를 할당하게 된다. 이러한 버디 할당자는 메모리 관리의 부하가 적으며 외부 단편화를 줄일 수 있다는 장점을 제공한다. 리눅스에서는 zone 구조체이 있는 free_area[] 배열을 통해 구축된다.

zone 구조체의 free_area 배열은 index가 1인 경우 2^1, 2일 경우 2^2... 10일 경우 2^10이 할당의 단위임을 뜻한다. 이러한 free_area 구조체에서의 free_list 변수를 통해 자신에게 할당된 free page frame을 list로써 관리하게 된다.

따라서 실제로 리눅스에서의 구조는 free_area[order].free_list로써 비어있는 메모리를 찾은 뒤 할당한다고 볼 수 있다. 다만 해당 page frame 크기의 free_list가 비어 있을 경우 그보다 큰 order의 free_list를 쪼갬으로써 할당하게 된다. 쪼개진 free_list는 낮은 order의 free_area로 속해지게 된다.

Lazy Buddy

리눅스 커널 2.6.19버전 이후 Lazy buddy라고 불리는 새로운 버디가 도입되었고 사용되고 있다.

Lazy buddy는 기존 버디에서 한 페이지 프레임을 할당/해제 반복하는 경우 (예를들어 페이지 프레임을 할당해주기 전에 큰 페이지를 쪼개고 쪼갠 뒤 사용 후 다시 붙이고 붙이고 하는 작업이 반복되는 경우 등) 많은 오버헤드가 발생하는 문제를 해결하기 위해 고안되었다고 할 수 있다. Lazy buddy할당되었던 페이지 프레임을 구지 합치지 말고 곧 다시 할당 될 테니 되도록 합치는 작업을 뒤로 미루는 방식이다.

실제 zone 구조체와 page 구조체가 작성되어 있는 mmzone.h 파일을 살펴보면 수정된 free_area 구조체를 볼 수 있는데 여기서 nr_free 변수는 자신이 관리하는 zone 내에서 비사용중인 페이지 프레임의 개수이다. 이를 통해 zone에 가용 메모리가 충분한 경우 해제된 페이지의 병합을 최대한 뒤로 미룬다.

현재 할당되어 있는 buddy의 정보는 /proc/buddyinfo 에서 확인할 수 있다.

슬랩 할당자(Slab Allocator)

작은 메모리 단위를 할당하기 위해 최소 page frame 크기의 메모리를 할당해주는 것은 매우 비효율적이기 때문에 미리 페이지 프레임 한 개를 할당받은 뒤 이를 작은 메모리 단위로 분할해 놓고 요청된 작은 메모리 단위에게 떼어 주는 방식을 통해 이들의 집합으모 메모리를 관리하는 정책슬랩 할당자라고 한다.

리눅스 커널에는 page frame으로부터 메모리를 받은 cache들이 정의되어 있고, 각각의 cache는 포함되는 slab의 메모리 크기에 따라 여러개가 존재한다. slab은 구성하고 있는 객체들의 상태에 따라 Full, Free, Partial의 상태로 구분된다. 슬랩 할당자에게 메모리 공간의 할당 요청이 들어온다면 가장 적합한 크기의 캐시를 찾아가서, partial 슬랩으로부터 객체를 할당해주게 된다.

이러한 cache들을 효율적으로 관리하기 위해 keme_cache라는 구조가 정의되어 있다. 이 keme_cache는 ./include/linux/slab_def.h에 정의되어 있다.

현재 시스템의 슬랩 할당자와 관련된 정보는 /proc/slabinfo 에서 확인할 수 있다.

slab은 일반적으로 무난한 성능을 보여주어서 사용되었지만, 제한적인 임베디드 환경에서는 slob을, 많은 수의 CPU와 메모리 노드로 구성된 서버 환경에서는 slub을 사용할 수 있다고 한다. (게다가 2.6.23버전 이후로는 x86 시스템에서 slub이 기본 채택)

Clone this wiki locally