[TIL][Project 1] 이해하기: interrupt frame, idle, 그리고 busy waiting

2023. 11. 24. 23:33· 공부기록/OS
목차
  1. 오늘의 할 일
  2. Project 1 이해하기
  3. 해결해야 하는 테스트들 미리보기
  4. gitbook 탐색
  5. 스래드 생성 :: thread_create 
  6. 현재 project 1 상태 : Busy Wating 
  7. 오늘의 소감

 

 

오늘의 할 일

Project 1 이해하기

프로세스와 thread에 대해 조금 더 공부하고 정리하기. (이전 판 운영체제 서적 열람 github 페이지 발견(링크))

 

 

Project 1 이해하기

코드라는 이름의 망망대해에 둥둥 떠다니는 기분이었지만, 우리에겐 깃북이 있다!

 

해결해야 하는 테스트들 미리보기

project 1에서 해결해야하는 테스트케이스들의 이름을 살펴보았다. 크게 묶어 3가지로 정리할 수 있다. 

Alram, Priority, 그리고 MLFQS. 각각 무엇을 수행하는 것일까? 깃북에 각각 무엇을 하면 되는지 있어서 살펴보기로 했다. 그 전에 introduction부터. 

gitbook 탐색

Project 1 threads / Introduction 

gitbook (project 1 introduction) 에서 이렇게 말했다. 

최소한으로 기능하는 thread system을 줄게, 너희(학습자)들은 동기화 문제를 더 명료하게 이해해가면서 이 시스템의 기능을 확장해보렴. 이번 프로젝트의 무대는 thread 디렉토리이고, devices 폴더에서 곁가지로 일을 좀 할 수 있다. 그런데, project 1을 이해하기 위해서는 먼저 동기화( Synchronization.) 항목을 읽어보는 걸 권장한다.

 

동기화 항목을 따로 정리해가면서 읽기 시작했는데, 개념으로만 훑고 지나간 semaphore등 이걸 직접 한땀한땀 코드로 구현할 거란 게 두근거린다.

이 프로젝트의 첫걸음은 바로 스레드 생성과 완료completion, 각 스레드 간 전환을 위한 간단한 스케쥴러와 스레드 완료thread completion, 동기화 기본 요소(세마포어, lock, condition variables, 최적화 배리어(optimization barriers)가 이미 구현된 초기 스레드 시스템을 이해 해야 한다. 

 



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()
.

스래드 생성 :: thread_create 

pintOS에서 thread를 생성할 때 스케쥴링 할 새 컨텍스트가 만들어지고, 이 context에서 실행할 함수를 thread_create()의 인자로 전달한다. thread_create라는 함수가 하는 일은 함수(혹은 프로그램)가 실행될 어떤 환경을 조성하는 것이다. CPU 레지스터(Program Couter, PC 등)의 상태와 주소 공간 등의 정보가 포함된 환경을. 그리고 그 환경에서 실행될 함수를 전달받는다는 말인데, thread_create() 에 인자로서 전달받은 함수는 예약된 스레드가 실행될 때 thread_create() 가 생성한 컨텍스트 위에서 실행되고, 이 함수가 종료하면 thread가 종료한다. 아! 그래서 main() 처럼 작동한단 말이구나. 

 

thread.c에 있는 함수를 가져와서 살펴보면, page를 할당받고, thread를 초기화하고, 커널 스레드를 호출하고 레지스터에서 argument를 할당하는 것 같다.

tid_t
thread_create (const char *name, int priority,
		thread_func *function, void *aux) {
	struct thread *t;
	tid_t tid;

	ASSERT (function != NULL);

	/* Allocate thread. */
	t = palloc_get_page (PAL_ZERO);
	if (t == NULL)
		return TID_ERROR;

	/* Initialize thread. */
	init_thread (t, name, priority);
	tid = t->tid = allocate_tid ();

	/* Call the kernel_thread if it scheduled.
	 * Note) rdi is 1st argument, and rsi is 2nd argument. */
	t->tf.rip = (uintptr_t) kernel_thread;
	t->tf.R.rdi = (uint64_t) function;
	t->tf.R.rsi = (uint64_t) aux;
	t->tf.ds = SEL_KDSEG;
	t->tf.es = SEL_KDSEG;
	t->tf.ss = SEL_KDSEG;
	t->tf.cs = SEL_KCSEG;
	t->tf.eflags = FLAG_IF;

	/* Add to run queue. */
	thread_unblock (t);

	return tid;
}

 

intr_frame tf 

thread 구조체 안의 tf, 이것이 thread가 생성(제공)하는 컨텍스트에서 필요한 레지스터 정보를 갖고 있는 것 같아서, thread 구조체를 들여다보았다. process 내에서 생성된다는 thread 를 실제 눈으로 확인하니 굉장히 신기하고, 이 구조체 내에 선언된 것들이 각각 어떻게 쓰일지 궁금해졌다. 하지만 지금은 조금 더 파헤쳐볼 때 였다. 

// include/threads/thread.h
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. */

	/* Shared between thread.c and synch.c. */
	struct list_elem elem;              /* List element. */

#ifdef USERPROG
	/* Owned by userprog/process.c. */
	uint64_t *pml4;                     /* Page map level 4 */
#endif
#ifdef VM
	/* Table for whole virtual memory owned by thread. */
	struct supplemental_page_table spt;
#endif

	/* Owned by thread.c. */
	struct intr_frame tf;               /* Information for switching */
	unsigned magic;                     /* Detects stack overflow. */
};

tf란 바로 switching을 위한 정보를 담고 있는 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));

인터럽트 프레임이라는 것이 무엇일까? 이것을 이해하기 위해선 Interrupt에 대해 알아야 했다. 운영체제 세 가지 이야기 책에서 힌트를 얻을 수 있었는데, 이 개념은 CPU를 가상화와 연관되어 있었다. 와! 가상화!

 

interrupt 와 cpu 가상화 

책을 읽어보니, CPU는 하나(혹은 제한적)인데 해야 하는 일(process)는 많고, 이걸 '완전히' 동시에 실행하기에는 사실상 불가능하다. 그러나 우리는 윈도우나 맥을 사용할 때 크롬 여러 탭을 띄워두며 vs code를 사용하기도 하고 잘만 쓰고 있다. 이게 어떻게 된 일일까? 정답은 바로 돌려막기였다.

CPU의 가상화 방법
한 프로세스를 잠깐 실행시키고, 다른 프로세스를 또 잠깐 실행시키고 하면서 CPU시간을 나누어 씀으로써 '여러 작업이 동시에 실행되는 것처럼 보이도록', 가상화를 구현한 것이다.(와, 어떻게 이런 생각을 할 수 있었던 걸까?)

 

이 가상화 방법을 사용하려면, 두 가지 이슈에 직면하는데 이 중 하나가 바로 '제어 문제' 였고, 내가 마주한 interrupt라는 개념이 여기에서 필요했다. (다른 하나는 '성능 저하' 라고 한다. cpu시간을 나눌 때 오버헤드를 주면 말짱 도루묵이 되나 보다!) 

 

운영체제는 어떤 프로세스가 CPU를 사용해야 할 때 process에게 사용권한을 넘겨주지만, CPU의 통제권은 OS가 확실하게 갖고 있어야 한다. 왜냐하면 제어권까지 통째로 주어버린다면 하나의 process가 계속해서 그 CPU를 잡고 있을 것이고(실행을 계속하고) process가 접근하면 안 되는 정보 등에 접근해 문제를 일으킬 수 있기 때문이다. 따라서 이 문제들을 예방하면서, 어떻게 process를 통제할 것인지에 대해 많은 내용이 있었는데, 이것에 대해선 따로 포스팅을 해야겠다. 

 

중요한 건, OS는, 그리고 pintOS는, 타이머 인터럽트(Timer Interrupt)를 이용해 CPU의 제어권을 확보한다!

CPU를 잡고 있는 process에게, 수 밀리 초 간격으로 인터럽트를 발생시켜(=프로세스의 실행을 방해interrupt!)) 현재 수행 중인 process를 중단시키고 OS 내부의 interrupt Handler 가 실행된다. 이 핸들러는 운영체제에게 CPU 제어권을 넘겨주고, 운영체제는 스케쥴링된 다른 프로세스를 실행시킨다.

 

쉽게 이해해보려고 생각했다. 하나의 교실을 생각해보자. process는 학생들, 그리고 os는 선생님이다. CPU는 교탁이고 토론이든 뭐든 이번 수업에서는 모든 학생들이 발표를 '거의 동시에' 하게끔 하는 게 목표다. 최대한 모든 학생들이 발표해야 하므로, 선생(os)은 어떤 학생(process)이 일정 시간 발언하면 다음! 하고 끊고 다른 학생들에게 교탁 자리를 내어준다고 생각했다. 

 

다시, intr_frame

다시 돌아와서, intr_frame은 그래서 무엇을 하는 구조인가? 

인터럽트가 걸릴 때, CPU의 당장의 상태를 저장한다. 즉, CPU에서 현재 돌아가고 있는 process(PintOS에선, thread)를 스탑시키고 당장 어떤 context에서 돌아가고 있었는지를 해당 thread 내부의 intr_frame이라는 구조체 안에 싹 저장하는 것이다. 왜? 나중에 다시 이 thread의 차레가 돌아왔을 때, 이 정보를 기반으로 context를 복원해야 하니까! 좀 더 정돈된 용어로 말하자면, '인터럽트 핸들러나 예외 처리기 등이 실행을 완료한 후, (해당 process가 cpu에 다시 올라왔을 ) 원래의 작업으로 제어를 올바르게 반환하기 위해 필요한 정보를 저장한다.' 

 

여기까지 온 김에 하나하나 살펴보았다. 

  • struct gp_registers R;: 범용 레지스터(General Purpose Registers)의 상태를 저장. 내부 구조는 아래에.
    • 범용 레지스터란? integer data와 pointer를 저장하기 위해 사용되는 16개의 레지스터(여기엔 15개지만?). x86-64 cpu는 %r로 시작하며, 일반적으로 산술연산 등에 사용되고 일반적으로 rax에는  함수의 리턴 값을 저장한다는 등의 암묵적인 룰이 있는데, 특정 용도로만 사용하도록 약속된 레지스터로는 rsp(stack pointer 스텍 포인터)가 있다. CSAPP을 읽을 때 본 레지스터들이, 구조체 형태로 값이 저장되어 실제 사용되고 있었다. 눈으로 확인하니 두근거렸다.

/* 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));

 

  • uint16_t es / uint16_t ds :  세그먼트 레지스터(segment registers)
    • 실제 메모리 주소와 관련된 레지스터들
    • es: extra segment(메모리 주소지정을 다룰 때 사용함)
    • ds: data segment (데이터 세그먼트 시작 주소를 저장, 데이터의 위치를 알아낼 때 사용함)
  • uint64_t vec_no : 인터럽트 벡터 번호(interrupt vector number) | 인터럽트 구분용 코드
  • uint64_t error_code : 인터럽트나 예외(error?) 가 발생했을 때 CPU가 푸시한다고 함
  • uintptr_t rip & uint16_t cs
    • 인터럽트가 발생했을 때 실행 중이던 코드의 위치를 나타낸다. (둘이 함께!)
    • rip: 인터럽트가 발생했을 때의 명령 포인터(Instruction Pointer)의 값
    • cs: 코드 세그먼트 레지스터(Code Segment register)의 값
  • uintptr_t rsp;, uint16_t ss
    • 인터럽트가 발생했을 때 사용 중이던 스택의 위치와 세그먼트를 보관하고 나타낸다.
    • sp: 스택 포인터(Stack Pointer)의 값
    • ss: 스택 세그먼트 레지스터(Stack Segment register)의 
  • uint64_t eflags: CPU의 플래그 관련 값, 현재 실행 상태까지 저장. 
  • __pad 시리즈: padding으로, 구조체가 특정 정렬(alignment)을 유지하도록 하기 위해 메모리에 추가됨. 이러한 패딩은 (아마도) x86-64 아키텍처의 메모리 관리를 위해 필요한 게 아니었나 싶음. 

다른 팀이 공유해준  [OS] pintos의 interrupt frame 이란 + do_iret 함수가 하는 일 글까지 보니 이런 거구나 하는 감이 왔다. 

 

do_iret? 

내친김에 do_iret 함수까지 봤다. 함수는 아래에 있는데, 이게 어떻게 쓰이는지는 thread_launch 에 적혀 있었다.

/* 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. */

 

 

 thread_launch는 현재 실행 중인 스레드의 상태를 저장하고 전환하려는 스레드의 상태를 복원한다. .

더보기

thread_launch 에 대한 이야기 (블랙박스) 

// thread_launch(next) /* schedule 함수 내부 */

/* Switching the thread by activating the new thread's page
   tables, and, if the previous thread is dying, destroying it.

   At this function's invocation, we just switched from thread
   PREV, the new thread is already running, and interrupts are
   still disabled.

   It's not safe to call printf() until the thread switch is
   complete.  In practice that means that printf()s should be
   added at the end of the function. */
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"
			"push %%rbx\n"
			"push %%rcx\n"
            
            /* Fetch input once */
			"movq %0, %%rax\n"
			"movq %1, %%rcx\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"
			);
            
}

 

현재 실행중인 스레드의 인터럽트 프레임 구조체인 tf를 저장하고, 인자로 받은 다음 스레드(호출시 next, 내부에서는 th)의 인터럽트 프레임 구조체인 tf를 저장한다.  _asm __volatile 에서 현재 실행 중인 스레드의 상태를 현제 스레드(tf_cur)에 저장한 뒤에 call __ next 명령어로 현재의 rip(프로그램 카운터, pc)를 읽는다. (어떻게 읽을까?) out_iret 으로 어떤 주소를 저장하고 do_iret함수를 call 하는 것 같은데, 정확히는 모르겠다. 다만 rip, cs, eflags와 같은 것들의 값을 계산하는 것 같다. 아직은 여전히 블랙박스다. 하지만 굳이 알 필요는 없을 것 같은 부분들이라서, c언어보다 더 밑단에서 어떻게 메모리에 값을 넣는지 본 것으로 만족하고 넘어갔다. 

 

요점은 이거다. thread_launch에서는

(1) 현재 thread의 CPU 레지스터 상태를 tf_cur에 저장하여 컨텍스트를 저장하고

(2) do_iret 함수를 이곳에서 호출함으로써 새 thread의 context로 전환하는 context switching이 발생한다는 것.  

do_iret 함수는 무엇을 하지? (인자로 받은)  다음 스레드의 tf에서 저장되어있는 정보를 CPU레지스터로 복사해 컨텍스트를 복원 한다.

 

thread_launch 에서 context switching이 일어나는데, do_iret이라는 함수를 호출함으로서 다음 스레드로 스위칭된다고 한다. 깃북은 이걸 건들지 말라고 해서 보기만 해야 한다! 

/* Use iretq to launch the thread */
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");
}

 

do_iret 함수는 무엇을 하지? 

(인자로 받은)  다음 스레드의 tf에서 저장되어있는 정보를 CPU레지스터로 복사해 컨텍스트를 복원 한다.

 

thread_create() 에 대한 한 문단을 보다가 여기까지 왔다... 


 

 

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 in idle(), runs.) Synchronization primitives can force context switches when one thread needs to wait for another thread to do something.

 

cpu에서 특정 시간에 실행할 수 있는 thread는 오직 하나의 thread 뿐이고, 스케쥴러가 다음에 실행될 스레드를 결정한다. 그런데 만약 다음에 실행될 thread가 없다면-그리고 돌고 있는 스레드가 오직 유일하다면, 그건 idle 이란 이름의 스페셜한 thread가 돈다(runs)고 한다. 그럼 이 idle thread란 무엇일까?

 

Idle thread

idle 이란 게 특정 상태를 말하는 것 같았다. gitbook에 검색해보니, timer_sleep에 이런 글이 있었다. 

Suspends execution of the calling thread until time has advanced by at least x timer ticks. Unless the system is otherwise idle, the thread need not wake up after exactly x ticks. Just put it on the ready queue after they have waited for the right amount of time.

. 

시스템이 idle 할 때, 즉 실행해야 하는 다른 작업이 없을 때에는 지정된 시간을 기다린 후에, 경과 시간이 지남을 확인하고  ready queue에 추가될 수 있다고 한다. idle thread인 상태에서는 sleep 하지 말아야 한다고도 한다. 

 

/* Idle thread.  Executes when no other thread is ready to run.

   The idle thread is initially put on the ready list by
   thread_start().  It will be scheduled once initially, at which
   point it initializes idle_thread, "up"s the semaphore passed
   to it to enable thread_start() to continue, and immediately
   blocks.  After that, the idle thread never appears in the
   ready list.  It is returned by next_thread_to_run() as a
   special case when the ready list is empty. */
static void
idle (void *idle_started_ UNUSED) {
	struct semaphore *idle_started = idle_started_;

	idle_thread = thread_current ();
	sema_up (idle_started);

	for (;;) {
		/* Let someone else run. */
		intr_disable ();
		thread_block ();

		/* Re-enable interrupts and wait for the next one.

		   The `sti' instruction disables interrupts until the
		   completion of the next instruction, so these two
		   instructions are executed atomically.  This atomicity is
		   important; otherwise, an interrupt could be handled
		   between re-enabling interrupts and waiting for the next
		   one to occur, wasting as much as one clock tick worth of
		   time.

		   See [IA32-v2a] "HLT", [IA32-v2b] "STI", and [IA32-v3a]
		   7.11.1 "HLT Instruction". */
		asm volatile ("sti; hlt" : : : "memory");
	}
}

이 thread는 main이 스케쥴러를 시작하려 호출하는 thread_start() 가 생성한다. 이 특별한 스레드는 thraed_start() 에서 대기 리스트에 초기에 넣어진다고 한다.이것이 스케쥴링되고 처음에 초기화할 때, semaphore의 숫자를 늘리는 "up" 을 해서 이 특별한 스레드가 시작되었음을 알린다고 한다. 

 

intr_disable ()  , thread_block ()

 

idle 함수 내부를 살펴보면, 인터럽트를 일시적으로 비활성화하고, thread block 함수를 호출해서 block 한다. 그렇게 함으로써 ready list에 다시 나타나지 않는데, 주석에 따르면 ready list가 비어있을 때 next_thread_to_run 이 idle thread를 반환한다고 한다.

 

asm volatile ("sti; hlt" : : : "memory");

이건 인터럽트와 관련된 부분 같은데, 인터럽트를 위에서 inter_disable() 처리하여 인터럽트의 방해를 막았던 걸 다시 푸는 것 같다. 정확히는 인터럽트를 다시 가능케 만들고(re-enable interrupt) 다음 인터럽트를 기다린다. 그런데 왜 이런 방식으로 했을까? 궁금하다. 

 

왜 idle thread라는 것이 필요할까? 

찾아보니 다른 thread가 대기하고 있지 않을 때, 즉 할 일이 없을 때에도 idle thread를 이용하여 CPU를 돌리는(공회전?) 것이 전원을 껐다 켜는 것보다 시간 소모와 전력 소모를 덜 하기 때문이라고 한다. 즉,  CPU의 절전을 위해서라고 한다. 

 

왜 idle thread를 사용하는가에 대한 stack overflow의 답변 링크. 

idle thread에 대한 어떤 페이지 링크를 남긴다. (나중에 봐야지) 

 

 

추가로 유의할 점! 각 스레드에 4kb 미만의 작은 실행 스택이 주어진다. (너무 많이 선언하면 곤란해진다.) 


동기화 

In the Pintos projects, the only class of problem best solved by disabling interrupts is coordinating data shared between a kernel thread and an interrupt handler. Because interrupt handlers can't sleep, they can't acquire locks. This means that data shared between kernel threads and an interrupt handler must be protected within a kernel thread by turning off interrupts.

인터럽트를 끄면 동시성이 사라진다고 한다. 즉! 동시에 하나의 자원을 두고 다투는 경쟁 조건 상태가 발생하지 않는단 뜻이다. 깃북에서는 이 인터럽트 비활성화라는 방법을 커널 스레드와 인터럽트 핸들러 간의 데이터 공유시에만 쓰라고 하는데, 지금은 잘 모르겠다. 

 

이 부분은 내일 좀 더 읽기로 한다.


현재 project 1 상태 : Busy Wating 

...it spins in a loop checking the current time and calling thread_yield() until enough time has gone by. 

현재 상태는 바로 threads가 계속해서 현재 시간을 확인하고 지정된 시간이 경과할 때까지 양보하는 상황. 즉 busy waiting 하는 방식으로 구현되어 있다. busy wating?

바쁘게 기다리기?

이것이 왜 문제가 되는 걸까?  아래 코드를 보자. 

// devices/timer.c
/* Suspends execution for approximately TICKS timer ticks. */
/* TODO */
void
timer_sleep (int64_t ticks) {
	int64_t start = timer_ticks ();

	ASSERT (intr_get_level () == INTR_ON);
	while (timer_elapsed (start) < ticks)
		thread_yield ();
}


//timer.h
/* Number of timer interrupts per second. */
#define TIMER_FREQ 100

/* devices/timer.c */
int64_t timer_elapsed (int64_t then) {
  return timer_ticks () - then;
}

 

tick?

현재 스레드를 인자로 받은 ticks (시간) 동안 잠재운다. 여기서의 tick은 pintOS 내부에서 시간 측정에 사용되는 값으로, HW에 전원이 들어온 이후 1ms(밀리세컨드, 1/1000초)에 1씩 증가하는 것으로 기본 설정되어 있다. 

 

timer_elapsed

현재 ticks값에서 인자로 답은 값을 뺀 값을 반환한다. 여기선 timer sleep이 호출된 시점에서 몇 틱이 지났는지 확인한다. 

 

이 결과가 몇 틱을 자야 하는지, timer sleep의 인자로 받은 값보다 작으면 yield 함수를 호출, ready list에 있는 다른 thread에게 CPU를 양보하고 readly list의 맨 뒤로 이동한다. 

 

이게 왜 문제일까? 

바로 sleep, 즉 방해받지 않고 아무것도 하지 않아야 하는 thread들이 불필요하게 계속해서 'ready'상태로 만들었다는 점이다. ready상태와, sleep하는 상태, 즉, block 상태는 엄연히 다른데 현재 구현에서는 block되어야 하는 thread가 ready list에 삽입됨으로써 ready가 되어 문제였다.

 

이걸 이해하기 위해 마침 저번에 공부했던 process 의 상태에 대해 다시 봐야할 거 같다. 


오늘의 소감

Pintos를 직접 보니, 어떻게 해야 하는지 막막해 코치님께 물어보러 갔다.

Q 정말 막막한데 어떻게 하면 좋을까요?

A 바로 이럴 때 동료학습을 해라.

조언대로 강의실 전체를 팀으로 삼아 돌아다니며 듣다 보니 뭔말인지 알 것도 같다.

함께 공부하는 사람들이 있으니 든든하고, 자리를 비운 팀원을 위해 이해한 바를 정리해 전달하자.

 

'공부기록 > OS' 카테고리의 다른 글

[TIL / WIL] [Project 1] MLFQS + Advanced Scheduler in PintOS  (0) 2023.12.04
[TIL][Project 1] Priority Donation  (0) 2023.12.04
[TIL][Project 1] Priority Scheduling , Synchronization + pintOS  (0) 2023.11.27
[TIL][Project 1] process & thread in PintOS + Alarm Clock  (0) 2023.11.25
[TIL][Thur] PintOS Threads Keywords  (0) 2023.11.23
  1. 오늘의 할 일
  2. Project 1 이해하기
  3. 해결해야 하는 테스트들 미리보기
  4. gitbook 탐색
  5. 스래드 생성 :: thread_create 
  6. 현재 project 1 상태 : Busy Wating 
  7. 오늘의 소감
'공부기록/OS' 카테고리의 다른 글
  • [TIL][Project 1] Priority Donation
  • [TIL][Project 1] Priority Scheduling , Synchronization + pintOS
  • [TIL][Project 1] process & thread in PintOS + Alarm Clock
  • [TIL][Thur] PintOS Threads Keywords
J융
J융
Recording of development
J융
Develop day by day
J융
전체
오늘
어제
  • 분류 전체보기 (67)
    • 공부기록 (63)
      • CS (8)
      • OS (15)
      • Algorithm (19)
      • Web (3)
      • HTML&CSS (6)
      • Electron (1)
      • JavaScript (5)
      • Network (0)
      • C (2)
      • Python (3)
      • Git (1)
    • 개발일기 (3)
      • Alice기록 (0)
      • Krafton Jungle 기록 (3)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • JS기초
  • vm
  • 크래프톤정글
  • 비전공자개발자
  • CG
  • 부트캠프
  • 정글공부키워드
  • 앨리스트랙
  • Web
  • 기술면접대비
  • 개발일기
  • fe
  • cs지식
  • pintos
  • 알고리즘
  • #cs기초
  • 수강후기
  • os
  • cs기초
  • 엘리스AI트랙

최근 댓글

최근 글

hELLO · Designed By 정상우.v4.2.2
J융
[TIL][Project 1] 이해하기: interrupt frame, idle, 그리고 busy waiting
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.