Published on

[ Pintos ] 0.Pintos: code explanation

Authors
  • avatar
    Name
    유사공대생
    Twitter

내부 코드

내부 코드 이해를 위해 적어두려고 한다.

디렉토리

thread.c

스레드 관리 및 스케줄링을 수행하는 스레드 시스템을 구현하는 운영 체제 커널의 일부이다. 이 코드는 C 언어로 작성되었으며, 스레드의 생성, 스케줄링, 상태 관리 및 스레드 간 동기화를 다룬다.

이해가 안되는 부분들만 정리해두려고 한다.

thread_init(void)

void
thread_init (void) {
	ASSERT (intr_get_level () == INTR_OFF);

	/* Reload the temporal gdt for the kernel
	 * This gdt does not include the user context.
	 * The kernel will rebuild the gdt with user context, in gdt_init (). */
	struct desc_ptr gdt_ds = {
		.size = sizeof (gdt) - 1,
		.address = (uint64_t) gdt
	};
	lgdt (&gdt_ds);

	/* Init the globla thread context */
	lock_init (&tid_lock);
	list_init (&ready_list);
	list_init (&sleep_list);
	list_init (&destruction_req);

	/* Set up a thread structure for the running thread. */
	initial_thread = running_thread ();
	init_thread (initial_thread, "main", PRI_DEFAULT);
	initial_thread->status = THREAD_RUNNING;
	initial_thread->tid = allocate_tid ();
}

thread 시스템을 초기화하는 함수이다. 이 함수는 운영체제 커널이 시작될 때 호출된다.

ASSERT (intr_get_level () == INTR_OFF);

  • ASSERT 매크로를 사용하여 함수의 입력 매개변수와 관련된 조건을 검사한다.

  • 현재 인터럽트가 비활성화되어 있는지 확인한다. 스레드가 시스템을 초기화할 때 인터럽트가 비활성화되어야 하는데, 이를 확인하여 안전성을 보장한다.

struct desc_ptr gdt_ds = { ... };

  • GDT(Global Descriptor Table)의 정보를 담는 구조체 gdt_ds를 초기화한다.

  • GDT는 메모리 세그먼트와 권한 정보를 저장하는 테이블로, 스레드 및 프로세스 컨텍스트 전환 시 사용된다.

  • gdt_ds_size에 GDT의 크기를 설정하고, gdt_ds.address에 GDT의 시작 주소를 설정한다.

lgdt(&gdt_ds);

  • lgdt 함수를 사용하여 GDT를 설정한다.

  • GDT를 재설정함으로써, 현재 커널 스레드의 컨텍스트 정보를 GDT에 저장한다.

lock_init (&tid_lock);

스레드 식별자(tid)를 할당하기 위한 락(lock)인 tid_lock을 초기화한다. 스레드가 고유한 식별자를 안전하게 할당하기 위해 사용된다.

idle(void *idlestarted UNUSED)

Idle 스레드는 다른 실행가능한 스레드가 없을 때 실행되며, 시스템이 아무런 작업을 수행하지 않을 때 CPU를 놀리지 않고 효율적으로 사용하기 위한 목적으로 만들어진 특수한 스레드이다.

프로그램을 여러개 돌다 보면 메모리 공간이라던가 정리가 안 되고 계속 돌게 된다. 사용자 프로세스가 돌지 않을 때 작동하게 되고, 쓰레드 종료된 부분들을 뒷정리하거나 근처로 메모리 영역을 모아준다.

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

for (;;) { ... }:

  • 무한루프를 돌게 하여 Idle 스레드를 계속 실행시킨다.
  • Idle 스레드는 다른 실행 가능한 스레드가 없을 때 실행되므로, 기다리는 동안 다른 스레드가 실행되도록 허용한다.

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

  • 인터럽트를 활성화하고 hlt 명령을 사용하여 CPU를 대기 상태로 전환한다.
  • sti 명령은 인터럽트를 활성화하고, hlt 명령은 CPU를 대기 상태로 만든다.
  • 이렇게 함으로써 Idle 스레드는 인터럽트가 발생하기 전까지 CPU를 놀리지 않고 절전 모드로 들어간다. CPU가 필요한 경우 인터럽트가 발생하면서 시스템이 깨어난다.

**init_thread(struct thread t, const char name, int priority)

/* Does basic initialization of T as a blocked thread named
   NAME. */
static void
init_thread (struct thread *t, const char *name, int priority) {
	ASSERT (t != NULL);
	ASSERT (PRI_MIN <= priority && priority <= PRI_MAX);
	ASSERT (name != NULL);

	memset (t, 0, sizeof *t);
	t->status = THREAD_BLOCKED;
	strlcpy (t->name, name, sizeof t->name);
	t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);
	t->priority = priority;
	t->magic = THREAD_MAGIC;
}

memset (t, 0, sizeof *t);

  • t가 가리키는 스레드 구조체를 0으로 초기화한다. 이를 통해 구조체의 모든 필드를 초기화한다.

t->status = THREAD_BLOCKED;

  • 스레드의 상태를 THREAD_BLOCKED로 설정한다. 이것은 스레드가 처음에는 실행가능한 상태가 아니라 블록된 상태임을 나타낸다.

t->tf.rsp = (uint64_t) t + PGSIZE - sizeof (void *);

  • 스레드의 스택 포인터(rsp)를 설정한다. 스택 포인터를 스레드의 메모리 영역 끝으로 설정하고, 스택의 크기에서 void * 타입의 크기를 뺀 값으로 설정한다.

t->magic = THREAD_MAGIC;

  • 스레드의 magic 필드를 THREAD_MAGIC 상수로 설정한다. 이 값은 스레드의 유효성을 검사하는데 사용된다.

THREAD_MAGIC은 스레드 관련 코드에서 사용되는 값이다. 이 값은 스레드 구조체의 유효성을 확인하고 스택 오버플로우를 감지하는 데 사용된다.

일반적으로 스레드 구조체의 첫 번째 필드나 마지막 필드에 저장되며, 스레드가 생성될 때 무작위로 할당된 값이다. 스레드가 제대로 초기화되고 올바른 스레드인지 확인하기 위해 THREAD_MAGIC을 사용한다. 만약 스레드의 이 값이 기대하는 값과 다르다면, 스레드가 손상되었거나 초기화되지 않은 것으로 간주할 수 있다.

thread_tick(void)

thread_tick 함수는 타이머 인터럽트 핸들러에서 주기적으로 호출되며, 현재 스레드의 상태를 확인하고 상태를 업데이트한 후, CPU 양보를 통해 스레드 스케줄러에게 CPU를 양보할지 여부를 결정한다.

/* Called by the timer interrupt handler at each timer tick.
   Thus, this function runs in an external interrupt context. */
void
thread_tick (void) {
	struct thread *t = thread_current ();

	/* Update statistics. */
	if (t == idle_thread)
		idle_ticks++;
#ifdef USERPROG
	else if (t->pml4 != NULL)
		user_ticks++;
#endif
	else
		kernel_ticks++;

	/* Enforce preemption. */
	if (++thread_ticks >= TIME_SLICE)
		intr_yield_on_return ();
}

#ifdef USERPROG

  • 유저 프로그램을 지원하는 환경에서만 그 밑 코드를 실행한다.

else if (t->pml4 != NULL)

  • 현재 스레드의 페이지 디렉토리(pml4)가 NULL이 아닌지 확인한다. 페이지 디렉토리가 NULL이 아니면, 이 스레드는 유저 프로그램에서 실행 중인 스레드이다. 따라서 user_ticks 변수를 증가시킨다.

else

  • 위 idle 스레드이거나, 사용자 프로그램 스레드가 아닐 경우이다. 이 경우는 현재 스레드가 커널 모드에서 실행중인 스레드이다. 그래서 kermel_ticks 변수를 증가시킨다.

if(++thread_ticks >= TIME_SLICE) intr_yield_on_return()

  • thread_ticks 변수를 증가시킨 후, TIME_SLICE 시간에 도달했는지 확인한다. `TIME_SLICE는 스레드가 CPU를 점유한 후 얼마나 오랜 시간이 지나야 다른 스레드에게 CPU를 양보할지를 나타내는 값이다.
  • 만냑 threads_tick 값이 TIME_SLICE에 도달한 경우, intr_yield_on_return` 함수를 호출하여 스레드 스케줄러에게 CPU 양보를 요청한다. 이렇게 하면 스레드 스케줄러는 다음에 실행될 스레드를 선택하고, 현재 스레드는 다른 스레드로 교체될 수 있게 된다.