Context Switching은 현재 실행 중인 프로세스(스레드)의 상태 정보를 보관하고, 대기중인 프로세스(스레드)의 상태 정보를 읽어서 실행하는 작업을 말한다.
이번 포스팅은 PintOS code 를 살펴보며 Context Switching이 어떤식으로 이뤄지는지 살펴보고자 한다.
참고사항
KAIST PintOS 베이스로 작성된 글입니다. 하지만 project 1 정답코드는 포함되어있지 않습니다.
목차
1.Thread 메모리 구조
2. Thread 구조체
3. context switching
4. PintOS Context Switching 과정
위 그림은 thread 메모리 구조를 도식화 한 것이다. 각 thread는 독립적인 kernel space 와 user space를 갖는다.
특히 kernel space 의 경우 Context Switching 가 밀접하게 연관 되어 있다. 그 이유는 Context Switching에 동작과정을 살펴보면 알 수 있다.
현재 실행 중인 프로세스(스레드)의 상태 정보를 보관하고, 대기중인 프로세스(스레드)의 상태 정보를 읽어서 실행하는 작업을 말한다.
여기서 현재 실행 중인 thread의 상태 정보를 보관 하는 곳이 바로 kernel space이다!
kernel space 에서 주목할 공간은 kernel stack이다.
thread의 정보는 kernel stack 가장 낮은 주소값에 저장되어, 높은 주소에서 낮은 주소로 grow 하는 kernel stack과 충돌을 방지한다.
충돌할 경우 thread 정보중 가장 높은 주소값에 저장된 magic이 변경되면서 thread가 stack over flow가 발생함을 알 수 있다.
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. */
int origin_priority;
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
int64_t wake_time;
/* Owned by thread.c. */
struct intr_frame tf; /* Information for switching */
unsigned magic; /* Detects stack overflow. */
};
위는 pintOS thread 구조체 이며, 우리가 중점적으로 살펴볼 interrupt frame(intr_frame) 이다.
struct intr_frame {
/* Pushed by intr_entry in intr-stubs.S.
These are the interrupted task's saved registers. */
struct gp_registers R;
uint16_t es;
uint16_t __pad1;
uint32_t __pad2;
uint16_t ds;
uint16_t __pad3;
uint32_t __pad4;
/* Pushed by intrNN_stub in intr-stubs.S. */
uint64_t vec_no; /* Interrupt vector number. */
/* Sometimes pushed by the CPU,
otherwise for consistency pushed as 0 by intrNN_stub.
The CPU puts it just under `eip', but we move it here. */
uint64_t error_code;
/* Pushed by the CPU.
These are the interrupted task's saved registers. */
uintptr_t rip;
uint16_t cs;
uint16_t __pad5;
uint32_t __pad6;
uint64_t eflags;
uintptr_t rsp;
uint16_t ss;
uint16_t __pad7;
uint32_t __pad8;
} __attribute__((packed));
struct gp_registers {
uint64_t r15;
uint64_t r14;
uint64_t r13;
uint64_t r12;
uint64_t r11;
uint64_t r10;
uint64_t r9;
uint64_t r8;
uint64_t rsi;
uint64_t rdi;
uint64_t rbp;
uint64_t rdx;
uint64_t rcx;
uint64_t rbx;
uint64_t rax;
} __attribute__((packed));
Interrupt frame : 인터럽트가 발생했을때 현재 thread의 context를 저장하는 구조체
위 코드를 살펴보면 intr_frame 구조체 멤버들이 CPU 레지스터의 대응되는 변수명을 갖고있음을 알 수 있다. 즉 thread 구조체 안에 CPU 레지스터값(context)를 저장 할 수 있다!
interrupt frame 구조체가 thread 의 context를 의미하고, kernel space에 해당 값을 저장 하고 읽는 과정이 context switching 이라고 할 수 있겠다.
현재 실행 중인 프로세스(스레드)의 상태 정보를 보관하고, 대기중인 프로세스(스레드)의 상태 정보를 읽어서 실행하는 작업을 말한다.
context switching 과정을 살펴보도록 하자.
OS의 scheduling으로 인해 context switching이 일어나는 상황이다. 현재 실행중인 thread를 current thread, context switching 되어 실행될 thread가 next thread 이다. (각 thread의 그림은 thread의 kernel stack 도식화 하였다.)
CPU는 현재 thread(current thread)를 실행중이다. 즉 CPU 레지스터 값이 실행 중인 thread의 context를 의미한다.
CPU에서 현재 thread의 context를 current thread의 interrupt frame의 저장한다.
다음 실행될 thread(next thread)의 context(next thread 구조체의 저장된 interrupt frame)를 CPU로 옮긴다.
과정을 마치면 CPU는 자연스럽게 next thread를 실행하게 된다.
먼저 Context Switching 일어나는 시나리오를 설정하고. 코드를 살펴보도록 하자.
1)timer 가 timer interrupt를 호출한다.
2)thread ticks가 정해진 TIME SLICE 이상이 되어 context switching이 발생한다.
static void
timer_interrupt (struct intr_frame *args UNUSED) {
ticks++;
thread_tick();
thread_wake(ticks);
}
void
thread_tick (void) {
struct thread *t = thread_current ();
/* Update statistics. */
if (t == idle_thread)
idle_ticks++;
else
kernel_ticks++;
/* Enforce preemption. */
if (++thread_ticks >= TIME_SLICE)
intr_yield_on_return ();
}
void
intr_yield_on_return (void) {
ASSERT (intr_context ());
yield_on_return = true;
}
void
intr_handler (struct intr_frame *frame) {
bool external;
intr_handler_func *handler;
/* External interrupts are special.
We only handle one at a time (so interrupts must be off)
and they need to be acknowledged on the PIC (see below).
An external interrupt handler cannot sleep. */
external = frame->vec_no >= 0x20 && frame->vec_no < 0x30;
if (external) {
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (!intr_context ());
in_external_intr = true;
yield_on_return = false;
}
/* Invoke the interrupt's handler. */
handler = intr_handlers[frame->vec_no];
if (handler != NULL)
handler (frame);
else if (frame->vec_no == 0x27 || frame->vec_no == 0x2f) {
/* There is no handler, but this interrupt can trigger
spuriously due to a hardware fault or hardware race
condition. Ignore it. */
} else {
/* No handler and not spurious. Invoke the unexpected
interrupt handler. */
intr_dump_frame (frame);
PANIC ("Unexpected interrupt");
}
/* Complete the processing of an external interrupt. */
if (external) {
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (intr_context ());
in_external_intr = false;
pic_end_of_interrupt (frame->vec_no);
if (yield_on_return)
thread_yield ();
}
}
interrupt handler 에서 timer_intterupt는 external 이다.
if (external) {
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (intr_context ());
in_external_intr = false;
pic_end_of_interrupt (frame->vec_no);
if (yield_on_return)
thread_yield ();
external && yield_on_return 임으로 thread_yield()실행
thread_yield()가 schedule()함수 실행
static void
schedule (void) {
struct thread *curr = running_thread ();
struct thread *next = next_thread_to_run ();
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (curr->status != THREAD_RUNNING);
ASSERT (is_thread (next));
/* Mark us as running. */
next->status = THREAD_RUNNING;
/* Start new time slice. */
thread_ticks = 0;
#ifdef USERPROG
/* Activate the new address space. */
process_activate (next);
#endif
if (curr != next) {
/* If the thread we switched from is dying, destroy its struct
thread. This must happen late so that thread_exit() doesn't
pull out the rug under itself.
We just queuing the page free reqeust here because the page is
currently used by the stack.
The real destruction logic will be called at the beginning of the
schedule(). */
if (curr && curr->status == THREAD_DYING && curr != initial_thread) {
ASSERT (curr != next);
list_push_back (&destruction_req, &curr->elem);
}
/* Before switching the thread, we first save the information
* of current running. */
thread_launch (next);
}
}
static void
thread_launch (struct thread *th) {
uint64_t tf_cur = (uint64_t) &running_thread ()->tf;
uint64_t tf = (uint64_t) &th->tf;
ASSERT (intr_get_level () == INTR_OFF);
/* The main switching logic.
* We first restore the whole execution context into the intr_frame
* and then switching to the next thread by calling do_iret.
* Note that, we SHOULD NOT use any stack from here
* until switching is done. */
__asm __volatile (
/* Store registers that will be used. */
"push %%rax\n" //tf_cur
"push %%rbx\n"
"push %%rcx\n"
/* Fetch input once */
"movq %0, %%rax\n" //%0은 첫번쨰 인자 tf_cur
"movq %1, %%rcx\n" //%1은 두번째 인자 tf
"movq %%r15, 0(%%rax)\n"
"movq %%r14, 8(%%rax)\n"
"movq %%r13, 16(%%rax)\n"
"movq %%r12, 24(%%rax)\n"
"movq %%r11, 32(%%rax)\n"
"movq %%r10, 40(%%rax)\n"
"movq %%r9, 48(%%rax)\n"
"movq %%r8, 56(%%rax)\n"
"movq %%rsi, 64(%%rax)\n"
"movq %%rdi, 72(%%rax)\n"
"movq %%rbp, 80(%%rax)\n"
"movq %%rdx, 88(%%rax)\n"
"pop %%rbx\n" // Saved rcx
"movq %%rbx, 96(%%rax)\n"
"pop %%rbx\n" // Saved rbx
"movq %%rbx, 104(%%rax)\n"
"pop %%rbx\n" // Saved rax
"movq %%rbx, 112(%%rax)\n"
"addq $120, %%rax\n"
"movw %%es, (%%rax)\n"
"movw %%ds, 8(%%rax)\n"
"addq $32, %%rax\n"
"call __next\n" // read the current rip.
"__next:\n"
"pop %%rbx\n"
"addq $(out_iret - __next), %%rbx\n"
"movq %%rbx, 0(%%rax)\n" // rip
"movw %%cs, 8(%%rax)\n" // cs
"pushfq\n"
"popq %%rbx\n"
"mov %%rbx, 16(%%rax)\n" // eflags
"mov %%rsp, 24(%%rax)\n" // rsp
"movw %%ss, 32(%%rax)\n"
"mov %%rcx, %%rdi\n"
"call do_iret\n"
"out_iret:\n"
: : "g"(tf_cur), "g" (tf) : "memory"
);
}
현재 thread context를 tf_cur에 저장한다.
do_iret함수를 호출한다.
void
do_iret (struct intr_frame *tf) {
__asm __volatile(
"movq %0, %%rsp\n"
"movq 0(%%rsp),%%r15\n"
"movq 8(%%rsp),%%r14\n"
"movq 16(%%rsp),%%r13\n"
"movq 24(%%rsp),%%r12\n"
"movq 32(%%rsp),%%r11\n"
"movq 40(%%rsp),%%r10\n"
"movq 48(%%rsp),%%r9\n"
"movq 56(%%rsp),%%r8\n"
"movq 64(%%rsp),%%rsi\n"
"movq 72(%%rsp),%%rdi\n"
"movq 80(%%rsp),%%rbp\n"
"movq 88(%%rsp),%%rdx\n"
"movq 96(%%rsp),%%rcx\n"
"movq 104(%%rsp),%%rbx\n"
"movq 112(%%rsp),%%rax\n"
"addq $120,%%rsp\n"
"movw 8(%%rsp),%%ds\n"
"movw (%%rsp),%%es\n"
"addq $32, %%rsp\n"
"iretq"
: : "g" ((uint64_t) tf) : "memory");
}