thread.h
thread_status
enum thread_status {
THREAD_RUNNING, /* Running thread. */
THREAD_READY, /* Not running but ready to run. */
THREAD_BLOCKED, /* Waiting for an event to trigger. */
THREAD_DYING /* About to be destroyed. */
};
쓰레드는 총 4개의 상태를 가지게 된다.
1. THREAD_RUNNING : 실행 중인 쓰레드
2. THREAD_READY : 실행 가능한 쓰레드
3. THREAD_BLOCKED : block 된 쓰레드
4. THREAD_DYING : 곧 사라질 쓰레드
struct thread
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. */
};
- tid : 쓰레드를 구분하기 위한 값이다. 기본적으로 tid_t는 int형을 갖고 있고 각각의 쓰레드들은 1부터 시작해 numerically next higher한 순서로 id값을 할당 받는다.
- status : 쓰레드의 상태를 저장한다.(running, ready, block, dying)
- name : 쓰레드의 이름을 나타낸다. 일반적으로 디버깅을 할 때 사용한다.
- priority : PRI_MIN(0) ~ PRI_MAX(63)까지 우선 순위를 저장한다. 현재 제공된 핀토스는 Thread는 우선순위를 무시하지만 이 값을 사용하여 우선순위 스케줄링을 구현해야 한다.
- elem : ready_list 또는 sama_down()의 세마포어를 기다리는 리스트에 들어가는데 사용된다.
- magic : 동작 방식은 모르겠지만 스택 오버플로를 감지하는 데 사용되는 임의의 번호이다. 항상 THREAD_MAGIC로 설정되어 있어야 하며 thread_current()에서 항상 이를 확인한다.
thread.c
/* List of processes in THREAD_READY state, that is, processes
that are ready to run but not actually running. */
static struct list ready_list;
/* Idle thread. */
static struct thread *idle_thread;
/* Initial thread, the thread running init.c:main(). */
static struct thread *initial_thread;
/* Lock used by allocate_tid(). */
static struct lock tid_lock;
/* Thread destruction requests */
static struct list destruction_req;
- ready_list : 상태가 THREAD_READY인 쓰레드를 담아두는 리스트
- idle_thread : 실행되는 동안 적어도 한 개의 thread(idle_thread)가 생성되어 CPU를 차지하고 있어야한다. 만약 ready_list에 어떠한 쓰레드도 없다면 idle_thead가 실행된다.
- initial_thread : main thread라고도 불리우며 process가 시작하면 생성되고 작업을 수행한다. 작업 수행의 시작점이 되며 일반적으로 런타임 환경을 준비하거나 데이터 구조를 초기화하거나 다른 쓰레드들을 생성한다.
- tid_lock : 동일한 값을 가진 쓰레드 생성을 방지하기 위해 사용되는 lock.
- destruction_req : 상태가 THREAD_DYING인 쓰레드들이 모여있는 리스트
#define is_thread(t) ((t) != NULL && (t)->magic == THREAD_MAGIC)
#define running_thread() ((struct thread *) (pg_round_down (rrsp ())))
- is_thread(t) : 쓰레드의 값과 쓰레드의 magic값을 확인하여 유효한 쓰레드인지 확인한다.
- running_thread() : CPU의 stack pointer인 'rsp' 읽고 그것이 가리키는 곳으로 간다. 쓰레드 구조체는 항상 page의 시작점이고 stack pointer는 중간에 있기에 반내림을 한다면 현재 쓰레드를 가리키게 된다.
thread_init()
static struct thread *initial_thread;
...
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 global thread context */
lock_init (&tid_lock);
list_init (&ready_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 ();
}
inter_get_level()를 이용하여 함수가 실행되는 동안 인터럽트가 불가능한지 확인한다. 함수가 실행되는 도중 thread_current()가 호출되면 위험하기 때문이다.
사용될 tid_lock, ready_list, destruction_req를 초기화한다.
여기가 매우 중요하다고 생각되는데 initial_thread에 running_thread()를 호출하여 현재 실행되는 프로그램의 단위를 initial_thread로 명명한다.
그 다음 initial_thread에게 필요한 값을 넣어준다.
thread_start()
void
thread_start (void) {
/* Create the idle thread. */
struct semaphore idle_started;
sema_init (&idle_started, 0);
thread_create ("idle", PRI_MIN, idle, &idle_started);
/* Start preemptive thread scheduling. */
intr_enable ();
/* Wait for the idle thread to initialize idle_thread. */
sema_down (&idle_started);
}
이 함수에서 idle_started라는 세마포어를 인자로 갖는 idle 함수를 실행시키는 idle thread가 생성되게 된다.
intr_enable() 함수를 호출하여 인터럽트를 활성화 시킨다. 이는 스케줄러가 timer interrupt에서 반환 시 inter_yield_on_return()을 사용하여 실행되기 때문에 스케줄러가 활성화 된다.
sema_down을 하여 idle_thread가 block되게 한다.
thread_tick()
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 ();
}
thread_tick은 호출될 때마다 쓰레드에 부여된 ticks변수를 늘린다.
만약 thread_tick이 TIME_SLICE를 넘어간다면 스케줄러를 부르게 된다.
thread_create()
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;
}
이 함수는 새로운 스레드를 생성하고 초기화한 후, 해당 스레드를 실행 대기 상태로 전환하는 역할을 한다.
palloc_get_page()함수를 사용하여 쓰레드가 사용할 메모리를 할당받는다.
init_thread 함수를 호출하여 스레드를 초기화한다.
kernel_thread 함수를 호출하기 위해 해당 함수의 주소를 t->tf.rip에 저장한다.
또 t->tf.R.rdi와 t->tf.R.rsi에 function과 aux를 넣으므로서 kernel_thread가 실행될 때 function과 aux를 인자로 전달 받게 한다.
thread_unblock()을 호출하여 쓰레드를 준비 상태로 전환한다. 준비 상태의 스레드는 실행 대기 큐에 추가되어 다른 스레드와 경쟁하게 된다.
thread_block()
void
thread_block (void) {
ASSERT (!intr_context ());
ASSERT (intr_get_level () == INTR_OFF);
thread_current ()->status = THREAD_BLOCKED;
schedule ();
}
thread_block() 함수는 호출되었을 때 인터럽트가 활성화되어 있으면 안되기 때문에 intr_context() 함수를 사용하여 이를 확인한다.
intr_get_level() 함수를 사용하여 인터럽트가 비활성화 되어 있는지 확인한다.
쓰레드의 상태를 block 상태로 업데이트한 뒤에 scheule() 함수를 불러 다음 작업을 수행한다.
thread_unblock()
void
thread_unblock (struct thread *t) {
enum intr_level old_level;
ASSERT (is_thread (t));
old_level = intr_disable ();
ASSERT (t->status == THREAD_BLOCKED);
list_push_back (&ready_list, &t->elem);
t->status = THREAD_READY;
intr_set_level (old_level);
}
thread_unblock() 함수는 주어진 쓰레드를 언블록하고 실행 가능한 상태로 변경하여 스레드를 스케줄링에 참여할 수 있도록 한다.
is_thread()로 유효한 쓰레드인지 확인.
intr_disable()로 인터럽트 비활성화.
t->status를 THREAD_READY로 바꾸고 intr_set_level()을 이용하여 인터럽트 레벨을 복원한다.
thread_yield()
void
thread_yield (void) {
struct thread *curr = thread_current ();
enum intr_level old_level;
ASSERT (!intr_context ());
old_level = intr_disable ();
if (curr != idle_thread)
list_push_back (&ready_list, &curr->elem);
do_schedule (THREAD_READY);
intr_set_level (old_level);
}
thread_yield() 함수는 현재 실행 중인 스레드를 양보하고 스케줄링을 통해 다음으로 실행될 스레드를 선택하는 역할을 한다.
스레드 스케줄링은 인터럽트 컨텍스트에서 수행되지 않아야 하기 때문에 intr_context()를 사용하여 확인한다.
intr_disable() 함수를 호출하여 함수가 atomic하게 실행됨을 보장한다.
do_schedule(THREAD_READY)을 호출하여 현재 쓰레드를 ready list에 넣는다.
intr_set_level 함수를 호출하여 인터럽트 상태를 원래 상태로 복구한다.
next_thread_to_run()
static struct thread *
next_thread_to_run (void) {
if (list_empty (&ready_list))
return idle_thread;
else
return list_entry (list_pop_front (&ready_list), struct thread, elem);
}
next_thread_to_run() 함수는 준비 리스트에서 다음에 실행될 쓰레드를 선택하여 반환하는 역할을 한다.
스레드 스케줄러에서 사용되어 다중 스레드 환경에서 공정한 CPU 시간 분배를 위한 기능을 수행한다.
do_schedule()
static void
do_schedule(int status) {
ASSERT (intr_get_level () == INTR_OFF);
ASSERT (thread_current()->status == THREAD_RUNNING);
while (!list_empty (&destruction_req)) {
struct thread *victim =
list_entry (list_pop_front (&destruction_req), struct thread, elem);
palloc_free_page(victim);
}
thread_current ()->status = status;
schedule ();
}
실행 중인 상태에서만 실행되야 하기 때문에 현재 쓰레드의 상태가 실행 상태를 확인한다.
destruction_req에 있는 상태가 THREAD_DYING인 쓰레드를 삭제한다.
현재 실행 중인 쓰레드의 상태를 갱신한다.
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 (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);
}
}
schedule() 함수는 현재 실행 중인 쓰레드와 다음 실행할 쓰레드를 교체하고 다음 쓰레드를 실행 상태로 변경하는 함수이다.
현재 실행 중인 쓰레드와 다음 실행할 쓰레드를 가져온다. 함수 실행 중에는 인터럽트가 비활성화 되어있어야 한다.
다음 쓰레드를 실행중 상태로 바꾸고 thread_ticks 값을 0으로 초기화 한다.
다음 쓰레드로 전환한다.