본문 바로가기
SW사관학교 정글/PintOS

[PintOS] Threads 다시 보다가 문맥교환(Context Switching) 이해하기

by 대범하게 2022. 12. 7.
반응형

Gitbook Project 1: Understanding Threads를 다시 읽어보았다. 

https://casys-kaist.github.io/pintos-kaist/project1/introduction.html

내가 이해했던 부분을 적은 것이기 때문에 흐름이 오락가락 할 수 있다. 

 

When a thread is created, you are creating a new context to be scheduled. You provide a function to be run in this context as an argument to thread_create(). The first time the thread is scheduled and runs, it starts from the beginning of that function and executes in that context. When the function returns, the thread terminates. Each thread, therefore, acts like a mini-program running inside Pintos, with the function passed to thread_create() acting like main().

 

스레드가 생성될 때, 스케줄링의 대상이 되는 새로운 문맥(context)이 생성된다. (문맥이라는 말이 모호하지만 뒤에서 설명)

만약 이 문맥에서 어떤 함수를 실행하고자 하는 경우, thread_create()의 인자로 실행하고자 하는 함수를 넣으면 된다.

스레드가 처음 스케줄링되고 실행될 때, 스레드는 해당 문맥에서 함수의 맨 처음부터 실행한다. 

함수가 리턴될 때 스레드도 종료된다. 그러므로 각각의 스레드는 Pintos 내부에서 실행되는 미니 프로그램 같이 동작한다고 생각하면 된다.

마치 프로그램을 실행하면 main()함수가 실행되는 것처럼, thread_create()가 실행되면 스레드에 전달된 함수가 실행된다.

 

thread_creat()의 세번째 인자를 보면 function이 들어가는 것을 볼 수 있다. 

tid_t thread_create(const char *name, int priority, thread_func *function, void *aux)

 

At any given time, exactly one thread runs and the rest, if any, become inactive. The scheduler decides which thread to run next. (If no thread is ready to run at any given time, then the special idle thread, implemented inidle(), runs.) Synchronization primitives can force context switches when one thread needs to wait for another thread to do something.

 

언제나 한 번에는 하나의 스레드만이 실행되고 나머지들은 (만약 있다면) 모두 비활성화된다. 스케줄러는 다음 실행할 스레드를 결정한다. 만약 다음으로 실행할 스레드가 없다면 idle 스레드라는 특별한 스레드를 실행한다. (idle 스레드는 idle()로 실행된다.) 동기화 함수는 하나의 스레드가 다른 스레드가 뭔가를 하는 것을 기다려야 할 때 문맥교환(context switch)를 강제 진행한다. 

 

The mechanics of a context switch are in thread_launch() in threads/thread.c (You don't have to understand it.) It saves the state of the currently running thread and restores the state of the thread we're switching to.

 

문맥 교환이 일어나는 방식threads/trhead.cthread_launch()에 정의되어 있다. (이 방식을 이해할 필요는 없다.)

문맥 교환현재 실행중인 스레드의 상태를 저장하고, 스위칭 할 즉, 다음으로 실행할 스레드의 상태를 복원한다.

 

- thread_launch()의 주석에 따르면, "Before switching the thread, we first save the information of current running."

즉, thread_launch()는 실행 중이던 스레드 정보를 저장하고, 다음 스레드의 정보로 교체하는 함수

 

- thread_launch()가 호출되기까지의 과정:

 thread_yield()에서 do_schedule()함수 호출 => do_schedule()에서 schedule()함수 호출 =>schedule()함수에서 thread_launch() 함수 호출


💡문맥 교환(Context Switching)

이 방식을 이해할 필요는 없다고 적혀있지만 이해해보자.

(* eax 레지스터: 어큐물레이터 레지스터(Accumulator Registr)라고도 부르며, 산술연산을 수행하기 위해 사용되거나 함수의 리턴 값을 전달하기 위해 사용된다.)

 

자자, 프로세스 A가 CPU의 eax 레지스터에 2라는 값을 넣고 연산을 수행하다가 프로세스 B로 문맥교환이 됐어.

그래서 프로세스 B는 eax 레지스터에 10이라는 값을 넣고 연산을 수행하고 있어.

근데 다시 B에서 A로 문맥교환이 된다고 하면, 프로세스 A가 제대로 수행할려면 CPU의 eax 값은 2여야 하겠지?

 

그니깐 핵심은 문맥 교환이 되는 시점에 어디까지 수행했는지, 그리고 현재 CPU의 레지스터 값이 얼마인지를 저장해둬야해.

 

CPU의 레지스터 값이 얼마인지 저장해야할 공간

Linux에서는 task 구조체 안의 struct thread_struct이고,

Pintos에서는 thread 구조체 안의 tf 이다. (struct thread에 멤버로 tf가 들어가 있다.)

struct intr_frame tf; /* Information for switching */

 

어디까지 실행했는지를 기억하기 위해 intr_frame 내에 저장하고 있다. 

gp_registers 구조체와 intr_frame 구조체는 다음과 같은 멤버들을 가지고 있다. 

/* Interrupt stack frame. */
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));

struct intr_frame
{
	/* Pushed by intr_entry in intr-stubs.S.
	   These are the interrupted task's saved registers. */
	struct gp_registers R; /* 스레드가 실행 중에 이용한 CPU의 범용 레지스터 값들 */
	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; /* CPU의 레지스터 정보 */
	uint16_t cs;   /* 세그먼트 관리 */
	uint16_t __pad5;
	uint32_t __pad6;
	uint64_t eflags; /* CPU 상태를 나타내는 정보 */
	uintptr_t rsp;	 /* 스택 포인터, 스택의 어느 부분을 사용하고 있었는지에 대해 저장 */
	uint16_t ss;
	uint16_t __pad7;
	uint32_t __pad8;
} __attribute__((packed));

 

자자, 프로세스 A에서 프로세스 B로 문맥교환이 일어나는 과정을 살펴보자.

[실제로 문맥교환이 일어나는 과정]

먼저 프로세스 A가 실행 중이며

1. 문맥 교환을 하기 위해서 유저 모드에서 커널 모드로 전환되면서

프로세스 A의 커널 스택에 CPU 레지스터 정보가 저장된다.

 

2. CPU 레지스터 정보가 프로세스 A의 커널 스택에 저장된 후,

2-1) 다음으로 실행할 프로세스가 B로 결정되면,

2-2) 커널 스택에 저장된 후 지금까지 사용했던 CPU 레지스터 정보

2-3) 프로세스 A의 스레드 구조체 tf에 저장한다. 

 

3. 실행될 프로세스 B의 스레드 구조체 tf에서 CPU 레지스터 정보를 복원한다. 

 

4. 커널 수준의 작업이 모두 완료된 후, 다시 유저 모드로 복귀할 때

프로세스 B의 커널 스택에서 CPU 레지스터 정보를 복원한다. 


Using the GDB debugger, slowly trace through a context switch to see what happens (see GDB). You can set a breakpoint on schedule() to start out, and then single-step from there. Be sure to keep track of each thread's address and state, and what procedures are on the call stack for each thread. You will notice that when one thread executes iret in do_iret(), another thread starts running.

 

GDB 디버거를 사용해서 문맥교환에서 어떤 일이 일어나는지를 천천히 추적해보세요.

schedule() 함수에 breadkpoint를 설정하는 것부터 시작해서 한 단계씩 나아가보세요.

각각의 스레드 주소와 상태, 그리고 각각의 스레드에 대해 어떤 프로시저가 콜 스택에 있는지를 주의깊에 추적해보세요.

하나의 스레드가 iretdo_iret()을 실행할 때, 또 다른 스레드가 실행을 시작한다는 것을 알아채게 될 것입니다.

 

Warning: In Pintos, each thread is assigned a small, fixed-size execution stack just under 4 kB in size. The kernel tries to detect stack overflow, but it cannot do so perfectly. You may cause bizarre problems, such as mysterious kernel panics, if you declare large data structures as non-static local variables, e.g. int buf[1000];. Alternatives to stack allocation include the page allocator and the block allocator (see Memory Allocation.)

 

경고: Pintos에서 각각의 스레드에는 작은 고정 사이즈 (4kB 이하의) 실행 스택이 할당된다. 커널이 스택 오버플로우를 잡아내려고 노력은 하겠지만 완벽하게 잡아내지 못한다. 그래서 int buf[1000];와 같이 큰 자료구조를 non-static 지역변수로 선언한다면 kernel panic과 같은 이상한 문제들을 만들어낼 수 있다. 스택에 할당하는 대신 page allocator와 block allocator를 사용해보세요.

반응형