벌써 핀토스 주간이라는게 믿기지 않습니다.
그간 배운 내용들을 토대로 저레벨 관련 프로젝트 진행에 본격적으로 들어온 느낌입니다.
1주차라 그런건진 모르겠으나, 오히려 여기서 숨이 조금 트이는 느낌입니다.
조 매칭 운이 좋아 특히나 좋은 팀원분들을 뵌 덕분인지도 모르겠습니다.
덕분에 블로그에 글 작성할 여유가 생겨 이렇게 작성해봅니다.
혹여나 이후 정글 분들께서 이 글을 보신다면, 꼭 당부드릴게 있습니다.
허리 조심하셔야됩니다. 자세 조심하셔야합니다. 스트레칭 꼬박꼬박하여야합니다.
한순간입니다. 저도 허리 걱정하기까지 한 5년 남았다 생각했는데
그게 5주로 줄어들고, 5일로 줄어들줄은 몰랐습니다.
아래는 alarm 테스트에 대한 공략글이 적혀있으며,
글 특성상 해당 코드에 대한 스포일러가 포함되어있습니다. 이 점 유의하여주시기 바랍니다.
처음 OS에 들어왔을 땐 정말 막막하게 됩니다.. 이게 다 뭐지... 싶고,
개인적으론 그런 면 때문에 오히려 alarm 테스트 파트가 가장 고됐던 것 같습니다.
처음 alarm 과제의 경우,
timer_sleep()의 함수를 기존의 busy waiting으로 되어있는 세팅에서 어떠한 효율적인 방안으로 변경하라 였습니다.
구성조차 어떤 상태인지 몰라, 지금 세팅이 뭘 말하는지조차 이해하기 어려웠습니다.
다만, 코드를 보면 해당 내용이 무슨 뜻인지 금방 알 수 있습니다.
/* Suspends execution for approximately TICKS timer ticks. */
void
timer_sleep (int64_t ticks) {
int64_t start = timer_ticks ();
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks)
thread_yield ();
}
다음은 devices 내 timer.c의 timer_sleep 함수입니다.
ticks를 인자로 받아, 해당 ticks 도달 전까지 thread_yeild() 를 실행시키는 함수입니다.
그럼 thread_yeild()는 또 뭘까요 바로 까보겠습니다.
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_current() 현재 실행 중인 스레드를 받아오는 함수일 것입니다(실제로 그렇습니다)
그리고 old_level이라는 enum을 설정합니다. 이것또한 까보면(또는 바로 밑 함수로도 알 수 있지만)
인터룹트 가능 여부를 세팅하는 변수가 됩니다. 특히 이 intr_disable 류는, 인터럽트를 허용/거부 모드로 바꾸고,
기존의 모드가 무엇이었든(겹치더라도) 이를 반환합니다. 즉 old_level은 말 그대로, 기존의 인터럽트 허용 여부 값이고
해당 작업이 종료될때, intr_set_level에서 old_level, 기존 값으로 다시 세팅하여 마감합니다.
해당 두 코드 사이엔, idle_thread라는게 보입니다. 이는 cpu가 비었을 때를 대비해, 임의로 넣어두는 일종의 더미 스레드입니다.
더미 스레드가 아닐 때, list_push_back()가 실행됩니다. ready_list에 curr->elem을 넣으라는 명령어 같습니다. 여기서 elem은 앞으로 계속 두고두고 쓰이니 꼭 숙지하면 좋습니다. 해당 스레드가 속한 prev 포인터와 next 포인터를 모드 담고 있는 스트럭쳐입니다. 따라서 이중 연결 리스트 생성의 간편함을 보장해주는 스트럭쳐입니다.
그리고 최종적으론 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 ();
}
이젠 do_schedule()입니다. 마트료시카 같지만, 어쩔 수 없습니다. 경고문은 예상치 못한 접근 방지용으로 세운, 그러니까 저희가 말도 안되는 실수를 했을 때 이상하게 작동하지 않게 해줄 최소한의 방어벽입니다. while 문을 보시면 리스트가 존재하지 않을 때까지, 어떤 victim을 설정합니다. list_entry() 이것도 중요합니다. 해당 elem을 조회하여, 그 elem 속한 스트럭쳐의 시작 주소값, 즉 포인터를 추적하는 매크로입니다. 그 밑엔 페이지에 대한 free를 시켜줍니다. 일단.. 지금 중요한 건 아닌거 같으니 지나가겠습니다.
현재의 스레드를 위에 설정된 status로 바꿔줍니다. 저흰 위에 있는 ready 상태로 세팅해줍니다. 이제 스케줄을 열어보겠습니다. 그래도 이제 진짜 다왔습니다.
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);
}
thread_launch (next);
}
}
현재 러닝 중인 스레드와, 그 다음에 실행할 스레드를 선택합니다. 그리고 next_thread_to_run()에선 그 다음에 실행될 스레드를 가져옵니다. 그리고 그 위치가 ready_list입니다. 그럼 여기서 중요한 정보를 얻을 수 있습니다.
yield가 실행되면, ready 리스트에 들어갑니다. 근데 스케줄이 실행되면, 레디에서 꺼내옵니다.
어? sleep을 걸었는데, 무슨 불침번 서게 하는거도 아니고, 수시로 깨운다는 말이 됩니다.
비효율입니다. sleep이 한 100틱 정도 걸렸다고 해도, 미친듯이 근무나가실 시간입니다를 시전 당하게 됩니다.
해당 코드는 스레드를 상대로 고문을 시키고 있었습니다. 저희는 이를 바로 잡아줄 겁니다.
elem에서 힌트를 얻을 수 있습니다. 이 elem은 앞 뒤의 포인터를 설정할 수 있지만, 정작 본인이 '어디에' 소속되는지는 중요하지 않습니다. 꼭 ready list에 보내야 할까요? 그건 아닌거 같습니다. 오히려 이게 pintos에서 의도한 바였을 확률이 높습니다. ready 상태인 애들만 ready에 넣어서, 꼭 필요한 애만 꺼내 낭비를 줄이고, 격리될 sleep 상태인 애들은 어떤 격리 리스트에 넣어서 따로 보관할 수 있게 됩니다. 저희는 저 push_back이라는 함수도 알았습니다. 클릭해서 가보시면 아시겠지만 insert도 있고 insert_sort도 있고 함수 구성만큼은 특대 맥가이버 키트보다도 든든합니다. 저희는 이 스레드를 논리적으로 격리할 뿐입니다. 즉, 얘를 깨울 땐 status만 ready로 돌려주고, elem의 앞 뒤를 ready list에만 연결해주면 해결입니다.
그럼 다음 고려 사항입니다. 어떻게 깨우지? 깨우는 기준은 간단합니다. 현재의 틱이, 해당 슬립 틱보다 높으면 됩니다. 그럼 그걸 어떻게 알지? 어디다 넣어놓지? 근데 이거 지금 보니까? 스레드 마다 고유의 슬립 틱이 주어집니다. 즉, 기존 스레드 스트럭쳐 안에 넣어주면 됩니다! 그럼 격리 리스트는 슬립 상태가 된 스레드들의 elem으로 구성되고, 이는 list_entry로 추적하여 해당 스레드의 포인터를 즉시 획득할 수 있습니다. 그렇다면 그 wake_up_tick 필드에도 접근 할 수 있겠고, 바로 조건 여부를 확인할 수 있습니다.
최종완성코드
https://github.com/WEEK09-11-PintOS/W09-11-PintOS/tree/alarm_clock/zeka0228
완성본은 해당 링크에서 확인하실 수 있습니다. 이곳 저곳 건드리는 바람에 조금 방대해져서 링크가 오히려 효율적일 것 같습니다.
/* Timer interrupt handler. */
static void
timer_interrupt (struct intr_frame *args UNUSED) {
ticks++;
thread_tick();
Sleep_list_out();
}
void Sleep_list_in(struct list_elem *SL_elem, int64_t UBticks){
struct thread *st;
struct list_elem *cur = list_begin(&Sleep_list);
ASSERT (intr_get_level () == INTR_OFF);
thread_current()->UB_ticks = UBticks;
while (cur != list_end(&Sleep_list)){
st = list_entry(cur, struct thread, SL_elem);
if(UBticks < st->UB_ticks){
break;
}
cur = list_next(cur);
}
list_insert(cur, SL_elem);
return;
}
void Sleep_list_out(void){
struct thread *st;
struct list_elem *cur = list_begin(&Sleep_list), *next;
ASSERT (intr_get_level () == INTR_OFF);
while (cur != list_end(&Sleep_list)){
st = list_entry(cur, struct thread, SL_elem);
if (st->UB_ticks > ticks)
break;
next = list_next(cur);
list_remove(cur);
cur = next;
thread_unblock(st);
}
return;
}
저의 경우, 전역 변수 tick이 관리되는 타이버 인터럽터에 sleep_list를 조회하여 조건이 달성된 스레드는 ready_list로 옮기는 함수를 넣어뒀습니다. 즉 tick++ 될때마다 이 작업이 시행되고, 매번 시행된다는 단점이 있겠지만 한번에 ready로 옮겨지는 양이 적어 오버헤드가 적을 것이라는 접근으로 구현했습니다.
개인적으론 개념보다는, 이 pintOS가 어떤 구성으로 이루어져 있는지, 감을 잡고 들어가라고 만들어둔 튜토리얼 문제라고 생각했습니다. 뒤에 나올 과제는 이보다 더 많은 개념과 파일을 건드리게 됩니다. 익숙해지는게 좋습니다.
이상입니다 다음에 다른 글에서 뵙겠습니다.
감사합니다. 그리고 명심하십쇼. 척추수술 1700만원!
'CS' 카테고리의 다른 글
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(2) - 레지스터의 종류 (2) | 2025.08.10 |
|---|---|
| [CSAPP] 3챕터 - 프로그램의 기계수준 표현(1) - 인스트럭션 셋 아키텍처(ISA) (5) | 2025.07.30 |
| [CSAPP] 1챕터 - 컴퓨터 시스템에게로 떠나는 여행 (3) - 운영체제 (5) | 2025.07.29 |
| [CSAPP] 1챕터 - 컴퓨터 시스템에게로 떠나는 여행 (2) - 하드웨어의 물리 구조와 저장장치 (4) | 2025.07.28 |
| [CSAPP] 1챕터 - 컴퓨터 시스템에게로 떠나는 여행 (1) - 프로그램 번역 과정 (3) | 2025.07.28 |