Part 1: 프로젝트 소개
이 프로젝트에서는 최소한의 기능을 갖춘 스레드 시스템을 제공한다. 우리가 해야할 것은 동기화 문제를 더 잘 이해하기 위해 이 시스템의 기능을 확장하는 것이다. 이 할당을 위해 주로 디렉토리에서 작업하고 측면의 디렉토리에서 일부 작업을 수행한다. 컴파일은 디렉토리에서 수행되어야한다. 이 프로젝트에 대한 설명을 읽기 전에 최소한 재료 동기화를 훑어봐야 한다.
기본 개념
tick
device/timer.h
에서 TIMER_FREQ
의 값이 100으로 저장되어 있는데, 이 의미는 1초당 tick이 100번 돈다는 의미이다. 즉 1tick은 100분의 1초(10ms
).timer
timer interrupt
를 날린다. 현재 TIME_SLICE
는 4 tick으로 설정되어 있다. 해당 TIME_SLICE
가 지나면 그 다음 스레드에 CPU 주도권을 넘긴다.현재 상황 : busy waiting 방식으로 구현
time_elpsed
로 시간을 체크하고 다른 스레드로 yield
한다.sleep 시켜줄 스레드 A를 대상으로 인자로 넣어준 ticks만큼 시간이 지났는지 체크한다. A가 CPU 주도권을 잡아 코드를 매 줄 실행시킬 때마다 이를 확인해 주어야 한다. 따라서 효율성 면에서 굉장히 떨어진다.
timer_sleep() 함수
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_elapsed()
)한다. 우리가 인자로 넣어준 ticks 값보다 작다면 thread_yield()
를 통해 다른 스레드에게 CPU 주도권을 양보하고, 다시 ready_list 맨뒤로 이동한다.현재 구현되어 있는 thread_yield()
ist_push_back
으로 해당 스레드를 ready_list의 맨 뒤로 넣어주고 do_schedule
로 다음 ready_list의 스레드(만약 스레드가 비어 있다면 idle 스레드)를 RUNNIG
상태로 변경하고 CPU 주도권을 넘겨준다.(thread_launch
)./* Yields the CPU. The current thread is not put to sleep and
may be scheduled again immediately at the scheduler's whim. */
void
thread_yield (void) {
struct thread *curr = thread_current ();
enum intr_level old_level;
// 외부 인터럽트를 수행중이라면 종료. 외부 인터럽트는 인터럽트 당하면 안 된다.
ASSERT (!intr_context ());
old_level = intr_disable (); // 인터럽트를 disable한다.
// 만약 현재 스레드가 idle 스레드가 아니라면 ready queue에 다시 담는다.
// idle 스레드라면 담지 않는다. 어차피 static으로 선언되어 있어, 필요할 때 불러올 수 있다.
if (curr != idle_thread)
list_push_back (&ready_list, &curr->elem);
do_schedule (THREAD_READY);
intr_set_level (old_level);
}
프로젝트1 : 개선 방안
애초에 sleep되어야 할 스레드들도 모두 READY상태로 ready list에 담겨져 있는 게 문제이다. 매번 ready list를 돌면서 자기가 CPU를 사용해야 하는 타이밍이 되었을 때마다 time_elapsed
로 시간을 체크하고 매번 다른 스레드로 yield
해 주어야 한다.
sleep 상태에 있는 프로세스들을 ready list에 넣지 않고 따로 sleep list라는 리스트에 저장시킨다.
- struct thread 개선
스레드 구조체 안에 sleep할 시 깨워줘야 하는 시각을 집어넣는다. 현재 20tick이고 앞으로 80tick 후에 깨워야 한다면, 20 + 80 = 100tick이라는 값을 저장해 줄 변수가 필요하다.
include/threads/thread.h/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. */
...
/* wakeup tick : 깨어나야 할 tick(시각) */
int64_t wakeup_tick;
...
}
- sleep_list 만들기
threads/thread.c
/* sleep 상태의 스레드들을 저장하는 리스트 */
static struct list sleep_list;
threads/thread.c/thread_init()
main() 함수에서 호출된다. 맨 처음 스레드와 관련된 구조체들과 main 스레드를 초기화해준다.
void thread_init (void) {
...
/* Init the global thread context */
lock_init (&tid_lock);
list_init (&ready_list);
list_init (&destruction_req);
list_init (&sleep_list); // sleep 스레드들을 연결해놓은 리스트를 초기화한다.
next_tick_to_awake = INT64_MAX;
}
- 스레드를 재우는 함수 thread_sleep() 구현
sleep 해줄 스레드를 sleep list에 추가하고 status를 THREAD_BLOCKED
으로 만들어준다.
이 때 idle thread를 sleep시켜준다면 CPU가 실행 상태를 유지할 수 없어 종료되므로 예외처리를 해 주어야 한다.
thread/thread.c/thread_sleep()
void thread_sleep(int64_t ticks) {
/* 현재 스레드가 idle 스레드가 아닐경우 thread의 상태를 BLOCKED로 바꾸고 깨어나야 할 ticks을 저장,
슬립 큐에 삽입하고, awake함수가 실행되어야 할 tick값을 update */
/* 현재 스레드를 슬립 큐에 삽입한 후에 스케줄한다. */
/* 해당 과정중에는 인터럽트를 받아들이지 않는다. */
struct thread *t = thread_current();
enum intr_level old_level;
ASSERT(!intr_context());
old_level = intr_disable();
if (t != idle_thread) {
t->time_to_wakeup = ticks;
if (MIN_alarm_time > ticks) {
MIN_alarm_time = ticks;
}
list_push_back(&sleep_list, &t->elem);
}
do_schedule(THREAD_BLOCKED);
intr_set_level(old_level);
}
thread/thread.c/thread_block()
스레드를 block(sleep)한다. thread_unblock()
함수에 의해서만 block이 해제된다. 무조건 인터럽트가 불가능한 상황에서 실행되어야 한다. synchronization primitives 활용해보기
void thread_block(void)
{
ASSERT(!intr_context());
ASSERT(intr_get_level() == INTR_OFF);
thread_current()->status = THREAD_BLOCKED;
schedule();
}
- 스레드를 깨우는 함수 thread_awake() 구현
sleep list에 잠자고 있는 스레드를 깨운다. 즉, sleep list에서 제거한 후 ready list에 넣어준다. status 역시 바꿔준다.
thread/thread.c/thread_awake()
sleep list를 순회하면서 인자로 받은 ticks
보다 wakeup_tick
이 작은 스레드들(깨울 시간이 된 스레드들)을 깨운 후 ready list에 넣어준다.
만약 해당 스레드가 아직 깨울 시간이 되지 않았다면 다음 스레드로 넘어간다. 이 때, 해당 스레드의 tick이 최소 tick인지를 확인한 후 next_tick_to_awake
를 업데이트한다.
void thread_awake(int64_t ticks) {
struct thread *t;
struct list_elem *now = list_begin(&sleep_list);
int64_t new_MIN = INT64_MAX;
/* sleep list를 전부 순회하며
알람시간이 다 된 스레드를 unblock 해준다. */
while (now != list_tail(&sleep_list)) {
/* list_entry return값 = thread 구조체 */
t = list_entry(now, struct thread, elem);
/* 현재 스레드가 알람 시간이 다 되었다면 깨운다. */
if (t->time_to_wakeup <= ticks) {
now = list_remove(&t->elem);
thread_unblock(t);
}
/* 현재 스레드가 아직 더 자야한다면 다음 스레드로 now를 갱신한다. */
else {
now = list_next(now);
/* 필요하다면 전체 sleep list의 MIN 값을 갱신한다. */
if (new_MIN > t->time_to_wakeup) {
new_MIN = t->time_to_wakeup;
}
}
}
MIN_alarm_time = new_MIN;
}
/* Converts pointer to list element LIST_ELEM into a pointer to
the structure that LIST_ELEM is embedded inside. Supply the
name of the outer structure STRUCT and the member name MEMBER
of the list element. See the big comment at the top of the
file for an example. */
#define list_entry(LIST_ELEM, STRUCT, MEMBER) \
((STRUCT *) ((uint8_t *) &(LIST_ELEM)->next - offsetof (STRUCT, MEMBER.next)))
#define offsetof(TYPE, MEMBER) ((size_t) &(((TYPE *) 0)->MEMBER))
- timer_sleep()과 timer_interrupt() 수정
devices/timer.c/timer_sleep( )
해당 ticks
동안 해당 스레드를 잠재운다.
void timer_sleep(int64_t ticks) {
thread_sleep(timer_ticks() + ticks);
}
devices/timer.c/timer_interrupt( )
매 tick마다 해당 tick에 깨워야 할 스레드들을 깨워줄 것이다. 이 때, 매 tick마다 깨우기보단 MIN_alarm_time
와 비교하여 깨워야 할 스레드가 sleep_list에 있을 경우에만 깨우는 것이 효율적이다.
static void
timer_interrupt(struct intr_frame *args UNUSED)
{
ticks++;
thread_tick();
if (MIN_alarm_time <= ticks) {
thread_awake(ticks);
}
}
결과
threads/build에서 pintos -- -q run alarm-multiple
명령어를 사용하면 결과를 볼 수 있다.
기존에는 idle_ticks가 0이었다. 다시 말해 CPU가 노는 시간이 없고 모두 다 sleeping하는 스레드들을 관리하느라 쉬지 않지 않고 일을 했다는 뜻, 새로 sleep_list를 만들고 block한 스레드들을 그 곳에 넣어둠으로써 CPU에게 여유를 줄 수 있다.
연관 함수
INTR_ON(1)
을, 인터럽트가 disabled 되어 있으면 INTR_OFF(0)
을 리턴한다.PintOS Project1 GIthub 주소 PintOS