- 하나의 프로세스는 하나의 페이지 테이블을 갖는다.
- 하나의 프로세스는 하나의(자신만의) 주소공간을 갖는다. 그 프로세스의 스레드들은 이 주소공간을 공유한다. 즉, 프로세스가 소유하고 있는 메모리영역만 접근 가능하다.
- 페이지가 메모리에 있으면, 그 페이지에 대한 맵핑정보는 페이지테이블에 에 존재한다. 없으면, 없다.( 페이지 테이블의 valid bit로 이를 표현함). 그러니까 페이지 테이블은 현재 메인 메모리에 존재하는 페이지에 대한 정보들을 가지고 있는 것이다.
- 페이지폴트: 페이지 테이블에 해당 페이지에 대한 정보가 없다(invalid) = 메인 메모리에 해당 페이지에 존재하지 않는다. 이 때 디스크에서 해당 페이지를 찾아서 비어있는 프레임(free page)에 가져온다. 또는 페이지교체 알고리즘을 통해 특정페이지와 swap한다. -> 이에 대한 결과를 페이지 테이블/TLB에 반영해준다.
- switch_mm()는 주소공간을 교체한다. 주소공간 교체: context_switch는 프로세스를 교체하는 것이기 떄문에, 주소 공간 역시 현재 프로세스의 주소공간에서 다음 프로세스의 주소공간으로 교체해야한다. 그래야만 다음 프로세스에서 자신의 주소공간 내에 있는 메모리영역에 접근하려 할 때 MMU가 이를 허용해주기 때문이다.(MMU는 특정 프로세스가 접근 가능한 메모리 영역을 알고있다.) 즉, 주소 공간을 교체한다는 뜻은 MMU가 접근을 허용해주는 영역을 바꾼다는 뜻이다. 다시 정리하면, switch_mm()은 주소공간을 prev의 주소공간에서 next의 주소공간으로 바꿔준다.
==========================================================================
리눅스 컨텍스트 스위치 과정
linux-2.4.20/kernel/sched.c
해당 버전의 리눅스 커널 소스는 https://www.kernel.org/pub/linux/kernel/v2.4/ 에서 받을 수 있다.
1. 알아야 할 사항
- 프로세스, 스레드, 태스크?
- 프로세스: 프로세스란, 독립적인 메모리 공간을 사용중인 실행중인 프로그램(실행 단위)을 의미한다. 각각의 프로세는 자신만의 주소변환맵핑테이블을 가지고 있다. 이로 인해 다른 프로세스의 물리적 메모리공간에 접근할 수 없으며, 자신만의 독립적인 메모리공간을 사용할 수 있는 것이다.
- 스레드: 스레드란, 프로세스에 속해있는 작은 실행 단위를 의미한다. 하나의 프로세스에는 여러개의 스레드가 속할 수 있으며, 이 스레드들은 메모리 공간을 공유한다. 즉, 하나의 프로세스에 속해있는 스레드들은 같은 주소 공간(address space)을 사용한다.
- 태스크: 리눅스에서는 프로세스, 스레드 구분이 없이 모두 태스크로 처리한다. 같은 프로세스에 속한 스레드들은 단지 해당 태스크(스레드)들이 가진 메모리영역이 같을 뿐이다. 즉, 리눅스에서는 스레드 = 태스크 로 정의할 수 있다. 개념적으로는 위의 프로세스, 스레드 개념과 동일하다.
- 태스크 구조체 task_struct
- 리눅스에서 하나의 태스크(스레드)는 하나의 task_struct 구조체로 구현되어있다. 각 태스크가 가진 메모리정보는 task_struct 구조체 안의 mm_struct 구조체에 기록되어 있다. 또한, 스레드는 자신만의 스택과 TCB를 가지고 있다. fork()로 프로세스를 생성하거나 pthread_create()로 스레드를 생성하면, 커널이 동적으로 하나의 task_struct를 할당해준다.
- 스택은 커널 스택, 유저 스택 두가지로 나뉘게 된다. 물리적 메모리를 두 부분으로 나눴을 때, 유저스택은 유저영역에 존재하며, TCB와 커널스택은 커널영역에 존재한다.
2. Context-switch 란?
context switch란 현재 CPU가 처리중인(바라보는) 태스크를 다른 태스크로 바꾸는 작업을 말한다.
3. context_switch진행 과정의 이해
- 태스크 A에서 태스크 B로 context_switch가 일어나는 과정은 다음과 같다.
- 태스크 A는 유저모드에서 커널모드로 변환하면서 커널로 진입한다.
- 커널에 있는 태스크 A는 커널에 있는 태스크B로 context_switch한다.
- 태스크 B는 커널모드에서 유저모드로 변환하면서 커널에서 빠져나온다.
이번에 알아볼 linux 커널 2.4 sched.c의 context_switch는 2번 째 진행과정을 다룬다.
위의 과정을 이해하는데 있어서 가장 중요한 개념은 context_switch는 반드시 커널안에서 진행된다는 것이다. 즉, context_switch 의 대상인 두 태스크는 항상 커널모드 상태로 커널 영역의 코드를 읽고 있는 상태이다.
간혹 context switch에 대해 잘 못 이해하여 "context_switch는 태스크 A에서 태스크 B로 context_switch를 할 때, 태스크 A가 스케쥴러 태스크 C를 호출하고 이 스케쥴러 태스크 C가 태스크 A에서 태스크 B로 context_switch 해 주는 것" 라고 생각하지만 전혀 그렇지 않다. 스케쥴러 태스크는 존재하지 않고, 태스크 A에서 태스크 B로의 진행과정은 전부 태스크A 안에서 진행된다. 이 점을 이해하는 것이 이번 내용의 핵심이다.
context_switch는 커널 안에서만 일어난다는 것은, 현재 실행중인 태스크 A가 태스크 B로 context_switch 하기 위해서는 유저 영역에서 실행중이던 먼저 태스크 A가 커널로 진입해야 한다는 것을 의미한다. 커널로 진입한다는 위해서는 현재 유저영역에서 진행 중이던 상태를 저장해야 한다. 이 값들은 태스크 A의 커널 영역 스택에 저장되게 된다. 이 때 저장하는 정보는 Stack Pointer, IP(Instruction pointer: 흔히 알고 있는 PC), 레지스터, 플래그 등이 있다. 그리고 CPU 역시 유저모드에서 커널모드로 상태를 전환한다. 정리하자면, 유저 영역에서 실행 중이던 태스크 A가 context_switch를 하기 위해서는 커널로 진입해야 하며, 커널로 진입 시 유저 영역에서 실행 중이던 태스크 A의 정보는 태스크 A의 커널 스택에 저장된다.
태스크 B가 종료되고 리턴 되어 태스크 A로 돌아 올땐, 커널 스택에 저장해두었던 유저 영역의 실행 정보들을 다시 복구 하여, 기존의 유저 영역에서 실행되던 태스크 A를 이어서 실행하게 된다.
태스크 A가 커널로 진입하고 나서 태스크 B 로 context_switch 하기 위해서, 태스크 A의 커널 영역 정보들을 태스크 A의 TCB에 저장 한다. 그래야만 나중에 태스크 A가 다시 스케쥴링되어 실행되었을 때, TCB의 값들을 다시 꺼내서 태스크 A가 수행중이었던 과정을 이어서 진행할 수 있다. 이 때, TCB에 저장되는 정보는 Stack Pointer, IP 등이 있다.
context-switch 할 때 태스크의 유저 영역 정보는 저장, 복구하지 않는다. 왜냐하면 태스크의 유저 영역 정보는 이미 커널 진입 시 커널 스택에 저장해두었기 때문이다.
전체 과정을 다시 한번 정리하자면, 태스크 A에서 다른 태스크로 context-switch 하기 위해 태스크의 유저 영역 정보를 커널 스택에 저장하고 커널로 진입한다. 이 때, CPU 역시 커널모드로 바뀐다. (system call) 그리고 스케줄러 루틴에서 다음으로 실행 할 태스크를 선택한다.(스케줄링) 만약 스케줄링에서 선택 된 태스크가 B라면, 태스크 A의 커널 영역정보를 태스크 A의 TCB에 save하고, 태스크 B의 커널 영역정보를 태스크의 B의 TCB에서 load한다.(context_switch) 마지막으로, 선택 된 태스크는 커널 스택에 저장되어 있는 유저 영역 정보를 꺼냄으로써 커널에서 유저 함수로 돌아가게 된다.(return)
밑에서 다룰 context_switch() 함수는 커널 영역에서 context_switch되는 과정을 다룬다.
4. 코드 분석
A. linux-2.4.20/kernel/sched.c
asmlinkage void schedule(void)
{
struct task_struct *prev, *next
...
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (!mm) {
BUG_ON(next->active_mm);
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
BUG_ON(next->active_mm != mm);
switch_mm(oldmm, mm, next, this_cpu);
}
if (!prev->mm) {
prev->active_mm = NULL;
mmdrop(oldmm);
}
}
switch_to(prev, next, prev);
...
}
struct task_struct *prev, *next
task_struct는 태스크를 나타내는 구조체이다. prev는 현재 실행중이던 태스크이며, next는 스케줄링 알고리즘에서 선택되어 다음 실행될 태스크이다.
struct mm_struct *mm = next->mm;
mm_strcut(memory management struct)는 메모리를 나타내는 구조체이다. mm은 next태스크가 가지고 있는 메모리 영역을 가리키게 된다.
struct mm_struct *oldmm = prev->active_mm;
oldmm은 prev의 active_mm을 가리키게 된다.
active_mm은 mm와 같은 mm_strcut구조체이다. 둘 다 mm_struct구조체에 속해있다.
mm은 해당 태스크가 가지고 있는 메모리영역을 가리킨다.
active_mm은 해당 태스크가 실제로 사용중인 메모리영역을 가리킨다.
둘의 차이는 태스크가 커널태스크이냐 아니냐에 따라 나타난다.
만약, 태스크가 유저레벨의 태스크라면(즉, 통상적인 사용자의 프로그램) mm과 active_mm은 가리키는 값이 같다.
하지만, 태스크가 커널영역에서만 실행되는 커널스레드라면, 커널스레드는 유저레벨에 점유하고 있는 메모리영역이 없기 때문에, mm은 NULL이 된다. 그리고 active_mm은 앞에서 실행된 다른 태스크의 메모리영역을 가리킨다.
왜냐하면 커널 스레드는 커널영역의 메모리만 참조하면 되고, 커널영역의 메모리는 모든 태스크에서 접근할 수 있는 공유영역이기 때문이다. 그렇기 때문에 커널 스레드는 아무 메모리영역을 빌려도 된다.
결론적으로 태스크의 mm이 NULL일 경우 커널 태스크라고 할 수 있다.
if (!mm) {
mm이 NULL인 경우. 즉, next태스크가 '커널 스레드'인 경우.
next->active_mm = oldmm;
next태스크의 active_mm을 prev태스크의 active_mm으로 한다. next 태스크가 커널 스레드이기 때문에, 실제로 사용할 메모리영역을 이 전 태스크의 메모리영역과 공유하는 것이다. 커널 스레드는 어차피 커널영역만 사용할 것이기 때문에 문제없다.
atomic_inc(&oldmm->mm_count);
mm_count: 해당 mm_struct구조체가 얼마나 참조되었는지 횟수를 나타낸다.
즉, oldmm이 가리키는 메모리영역의 참조 횟수를 1 증가시킨다.
enter_lazy_tlb(oldmm, next, this_cpu);
SMP(symmetric multiprocessing) 일경우, 현재 cpu의 tlb상태를 lazy로 바꾸는 것인데, 잘 모르겠으므로 패스.
} else {
next태스크가 커널 스레드가 '아닌' 경우
switch_mm(oldmm, mm, next, this_cpu);
CPU(정확히 MMU)가 바라보는 메모리 영역을 oldmm에서 mm으로 바꾼다.
일단 패스.
if (!prev->mm) {
prev가 커널스레드이면,
prev->active_mm = NULL;
prev의 사용이 끝났으므로, 실제 메모리 점유영역을 나타내는 active_mm을 리셋한다.
mmdrop(oldmm);
oldmm 메모리영역의 참조 횟수를 1줄이고(atomic_dec_and_test(&mm->mm_count)), 만약 참조 횟수가 0이 되면 oldmm이 가리키는 메모리영역과 페이지테이블을 drop시킨다.
어떤 의미를 가지는지 잘 모르겠다.
switch_to(prev, next, prev);
prev에서 next로 context-switch한다.
B. linux-2.4.20/include/asm-i386/system.h
#define switch_to(prev,next,last) do { \
asm volatile("pushl %%esi\n\t" \
"pushl %%edi\n\t" \
"pushl %%ebp\n\t" \
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %3,%%esp\n\t" /* restore ESP */ \
"movl $1f,%1\n\t" /* save EIP */ \
"pushl %4\n\t" /* restore EIP */ \
"jmp __switch_to\n" \
"1:\t" \
"popl %%ebp\n\t" \
"popl %%edi\n\t" \
"popl %%esi\n\t" \
:"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
"=b" (last) \
:"m" (next->thread.esp),"m" (next->thread.eip), \
"a" (prev), "d" (next), \
"b" (prev)); \
} while (0)
pushl %%esi\n\t"
/*
* This just switches the register state and the
* stack.
*/
switch_to(prev, next, prev);
참고
http://stackoverflow.com/questions/12630214/context-switch-internals
http://ljhh.tistory.com/m/entry/Task-Process-Thread
https://kldp.org/node/73308 커널스택, 유저스택
'옛날' 카테고리의 다른 글
Load balancing 알고리즘 비교&분석 (0) | 2016.12.19 |
---|---|
동시 파일 전송 프로그램 만들기(소켓 프로그래밍) (0) | 2016.12.18 |
댓글