이전 글
- [PintOS project] 1. Part 1: Threds - Priority Donation
https://velog.io/@takealittletime/PintOS-project-1.-Part-1-Threds-Priority-Donation
MLFQ (MultiLevel-Feedback-Queue)의 구현 이유
- 짧은 작업을 먼저 실행시켜 반환 시간을 최적화
반환시간: (작업 완료 시간) - (작업 도착 시간)- 응답 시간을 최적화 (대화형 사용자가 응답을 바로 받을 수 있도록)
응답 시간: (작업 시작 시간) - (작업 도착 시간)
MLFQ 요약
- MLFQ는 우선 순위 별로 각각의 큐를 가지고, 아래의 규칙들을 따르는 스케줄링 기법이다.
규칙 1. 우선순위 (A) > 우선순위(B) 일 경우, A가 실행, B는 실행되지 않는다.
규칙 2. 우선순위(A) = 우선순위(B), A와 B는 RR(Round-Robin) 방식으로 실행된다.
규칙 3. 작업이 시스템에 들어가면 최상위 큐에 배치된다.
규칙 4. 작업이 지정된 단계에서 배정받은 시간을 소진하면(CPU를 포기한 횟수와 상관없이), 작업의 우선순위는 감소한다. (즉, 한 단계 아래 큐로 이동한다.)
규칙 5. 일정 주기 S가 지난 후, 시스템의 모든 작업을 최상위 큐로 이동시킨다.
- Priority에 따라 여러 개의 Ready Queue를 만든다. →"Multi-Level"
- Priority를 실시간으로 조절한다. →"Feedback"
01-1. nice
NICE_MIN = -20
, NICE_DEFAULT = 0
, NICE_MAX = 20
(클수록 많이 양보)01-2. load_avg
load_avg = (59/60) * load_avg + (1/60) * ready_threads
01-3. recent_cpu
위와 같은 자료 구조를 이용해 각 스레드들의 우선순위를 결정하고, 특정 tick 마다 스레드들의 우선순위를 새로 계산한다!
→ 스레드가 무한정 대기하는 상황을 방지한다.
현재 ready_list에 P1, P2, P3 스레드가 존재한다.
load_avg (현재 실행되고 있거나 ready 상태에 있는 thread의 개수)는 3이다.
Tick = 0 일 때)
Tick = 1 ~ 4 일 때 )
Tick = 4 일 때)
priority = PRI_MAX - (recent_cpu/4) - (nice*2)
Tick = 5 ~ 8 일 때 )
Tick = 8 일 때 )
priority = PRI_MAX - (recent_cpu/4) - (nice*2)
~~ 이어서 반복)
커널에서는 정수 연산만 지원하고, 소수 연산은 지원하지 않는다.
위에서 recent_cpu
와 load_avg
는 실수값을 가지므로, 고정 소수점 연산을 따로 구현해주어야 한다.
/* include/threads/fixed_point.h */
#define F (1 << 14)
#define INT_MAX ((1 << 31) - 1)
#define INT_MIN (-(1 << 31))
// int 형 정수 ->고정 소수점 형식으로 반환
int int_to_fp(int n){
return n * F;
}
// 고정 수수점 -> 정수로 변환
int fp_to_int (int x){
return x / F;
}
// 고정 소수점 -> 반올림 -> 정수로 변환
int fp_to_int_round (int x){
if (x>=0)
return (x+F/2)/F;
else
return (x-F/2)/F;
}
// 두 고정 소수점 값 더하기
int add_fp (int x, int y){
return x + y;
}
// 두 고정 소수점 값 빼기
int sub_fp (int x, int y){
return x - y;
}
// 고정 소수점 값 + 정수 값
int add_mixed (int x, int n){
return x + n * F;
}
int sub_mixed(int x, int n) {
return x - n * F;
}
// 두 고정 소수점 값 나누기
int mult_fp (int x, int y){
return ((int64_t) x) * y / F;
}
// 고정 소수점 값 * 정수 값
int mult_mixed (int x, int n){
return x * n;
}
// 두 고정 소수점 값 나누기
int div_fp (int x, int y){
return ((int64_t) x) * F / y;
}
// 고정 소수점 값 / 정수 깂
int div_mixed (int x, int n) {
return x / n;
}
thread.c
에서 아래와 같이 헤더 파일을 추가해 사용해주도록 세팅한다./* threads/thread.c */
#include "threads/fixed_point.h"
all-list? 현재 시스템에 존재하는 모든 스레드를 저장하는 전역 리스트.
위에서 보았듯, load_avg
등 스레드의 우선순위를 실시간으로 재계산하는데 필요한 척도들을 계산할 때, 현재 시스템에 존재하는 모든 스레드들에 대한 계산이 필요해진다.
→ 이전까지 구현했던 스케줄링에서는 running중인 스레드
, ready_list의 스레드
, sleep_list의 스레드
등 현재 시스템에 있는 스레드들이 각자 따로 놀고 있기 때문에 계산하기 복잡하다.
→ all_list
로 이를 한꺼번에 관리해 계산을 편학 하자!
all_list
를 다음과 같이 선언한다.
/* threads/thread.c */
// ready_list, sleep_list 총괄 리스트
struct list all_list;
all_elem
을 선언해준다./* threads/thread.c */
struct thread {
...
/* nice값: 클수록 CPU 양보 ↑ */
int nice;
/* 해당 스레드가 최근 얼마나 많은 CPU time을 사용 했는지 */
int recent_cpu;
...
/* all_list에서 사용할 list_elem */
struct list_elem allelem;
...
};
init
함수에서 초기화를 해주어야 할 것이다./* threads/thread.c*/
void
thread_init (void) {
/* Init the globla thread context */
...
list_init(&all_list);
...
}
thread_init
함수에서 위와 같이 all_list
를 초기화 한다./* threads/thread.c */
static void
init_thread (struct thread *t, const char *name, int priority) {
...
// advanced_scheduler의 구현을 위해 아래 코드들 추가
t->nice = NICE_DEFAULT;
t->recent_cpu = RECENT_CPU_DEFAULT;
...
list_push_back(&all_list, &t->allelem);
}
init_thread()
함수에도 위와 같이 초기화 하는 내용을 추가한다. 스레드의 초기화가 끝나면 해당 스레드를 all_list
에 삽입한다.※ 주의
all_list
는 현재 실행 중 스레드, ready 중인 스레드, blocked 상태인 스레드들을 포함하는 리스트이기 때문에, 하나의 스레드가 종료되었다면 all_list
에서 삭제처리를 해주어야 한다.
이는 thread_exit()
함수에 다음과 같이 추가해준다.
/* threads/thread.c */
void
thread_exit (void) {
ASSERT (!intr_context ());
#ifdef USERPROG
process_exit ();
#endif
/* Just set our status to dying and schedule another process.
We will be destroyed during the call to schedule_tail(). */
intr_disable ();
list_remove(&thread_current()->allelem);
do_schedule (THREAD_DYING);
NOT_REACHED ();
}
/* threads/thread.c */
// mlfqs:: 스레드의 priority를 계산하는 함수
void
mlfqs_calc_priority (struct thread *t)
{
if ( t == idle_thread)
return;
t->priority = fp_to_int (add_mixed (div_mixed(t->recent_cpu, -4), PRI_MAX - t->nice * 2));
if (t->priority > PRI_MAX) t->priority = PRI_MAX;
if (t->priority < PRI_MIN) t->priority = PRI_MIN;
}
// mlfqs:: 스레드의 recent_cpu 값을 계산하는 함수
void
mlfqs_calc_recent_cpu (struct thread *t)
{
if (t == idle_thread)
return;
t->recent_cpu = add_mixed (mult_fp (div_fp (mult_mixed (load_avg,2),add_mixed(mult_mixed(load_avg, 2),1)),t->recent_cpu),t->nice);
}
// load_avg 값을 계산하는 함수
void
mlfqs_calc_load_avg ()
{
int ready_threads;
if (thread_current() == idle_thread)
ready_threads= list_size (&ready_list);
else
ready_threads = list_size (&ready_list) + 1;
load_avg = add_fp(mult_fp(div_fp(int_to_fp(59), int_to_fp(60)),load_avg), mult_mixed(div_fp(int_to_fp(1), int_to_fp(60)), ready_threads));
}
// 1 tick 마다 running 스레드의 recent_cpu 값 + 1
void
mlfqs_incr_recent_cpu ()
{
if (thread_current() != idle_thread)
thread_current()->recent_cpu = add_mixed (thread_current()->recent_cpu, 1);
}
// 4 tick 마다 모든 스레드의 recent_cpu 값 재계산
void
mlfqs_recalc_recent_cpu()
{
struct list_elem *e;
for ( e = list_begin(&all_list); e!=list_end(&all_list); e = list_next(e)) {
struct thread * t = list_entry(e,struct thread, allelem);
mlfqs_calc_recent_cpu(t);
}
}
// 4 tick 마다 모든 스레드의 load_avg 값 재계산
void
mlfqs_recalc_priority ()
{
ASSERT(intr_context());
struct list_elem *e;
bool need_yield = false;
for (e = list_begin(&all_list); e != list_end (&all_list); e = list_next(e)) {
struct thread *t = list_entry(e, struct thread, allelem);
mlfqs_calc_priority(t);
}
// priority의 변화가 일어났으므로, ready_list를 정렬하고, 재스케줄링함.
list_sort(&ready_list, cmp_thread_priority, NULL);
if (!list_empty(&ready_list) && thread_current()->priority<list_entry(list_front(&ready_list),struct thread, elem)->priority)
{
intr_yield_on_return();
}
}
priority
, recent_cpu
, load_avg
값을 특정 tick 마다 계산하도록 수정한다./* devices/timer.c */
/* Timer interrupt handler. */
static void
timer_interrupt (struct intr_frame *args UNUSED) {
ticks++;
thread_tick ();
if (thread_mlfqs) {
mlfqs_incr_recent_cpu();
if (ticks % 4 == 0){
mlfqs_recalc_priority();
if (ticks % TIMER_FREQ == 0){
mlfqs_recalc_recent_cpu();
mlfqs_calc_load_avg();
}
}
}
// 매 timer_interrupt 마다 대기 중인 스레드 깨우기
thread_awake(ticks);
}
/* threads/synch.c */
void
lock_acquire (struct lock *lock)
{
if (thread_mlfqs) {
sema_down (&lock->semaphore);
lock->holder = thread_current ();
return ;
}
...
}
void
lock_release (struct lock *lock)
{
lock->holder = NULL;
if (thread_mlfqs) {
sema_up (&lock->semaphore);
return ;
}
...
}
thread_set_priority ()
함수도 비활성화 해준다./* threads/thread.c */
void
thread_set_priority (int new_priority)
{
if (thread_mlfqs)
return;
...
}
/* threads/thread.c */
void thread_set_nice (int);
int thread_get_nice (void);
int thread_get_load_avg (void);
int thread_get_recent_cpu (void);
disable
해 준 상태에서 실행해야 한다는 것이다./* threads/thread.c */
void
thread_set_nice (int nice UNUSED)
{ // 현재 스레드의 nice 값을 새 값으로 설정
enum intr_level old_level = intr_disable ();
thread_current ()->nice = nice;
mlfqs_calculate_priority (thread_current ());
thread_test_preemption ();
intr_set_level (old_level);
}
int
thread_get_nice (void)
{ // 현재 스레드의 nice 값을 반환
enum intr_level old_level = intr_disable ();
int nice = thread_current ()-> nice;
intr_set_level (old_level);
return nice;
}
int
thread_get_load_avg (void)
{ // 현재 시스템의 load_avg * 100 값을 반환
enum intr_level old_level = intr_disable ();
int load_avg_value = fp_to_int_round (mult_mixed (load_avg, 100));
intr_set_level (old_level);
return load_avg_value;
}
int
thread_get_recent_cpu (void)
{ // 현재 스레드의 recent_cpu * 100 값을 반환
enum intr_level old_level = intr_disable ();
int recent_cpu= fp_to_int_round (mult_mixed (thread_current ()->recent_cpu, 100));
intr_set_level (old_level);
return recent_cpu;
}
threads
상에 있는 모든 테스트 케이스들에 대해 pass
를 확인할 수 있다!mlfq
의 구현 도중 스레드들의 개수를 용이하게 핸들링하기 위해 all_list
라는 자료구조를 이용한다. 이 때, '스레드가 종료되면 리스트에서 삭제해 주어야 한다'는 것을 잊으면 mlfq
와 관련된 테스트 케이스를 통과할 수가 없다.절망적인 것은, 마치 interrupt
상에서 오류가 발생한 것처럼 보여지기 때문에, 출력을 보고 디버깅을 하게 되면 애먼 곳만 수정하게 된다.
backtrace
로 이전 콜스택을 확인해서도 mlfqs_recalc_priority()
함수 부분에서 문제가 발생한 것을 확인하고, 이 부분만 내내 붙잡고 있었다. 🥲
어떠한 자료구조나 자원을 새로 정의하고 이용할 때는 해당하는 자원의 flow에 대해 한 번 더 고민하고 코드를 작성할 필요가 있는 것 같다.
처음에는 그저 막막하기만 했던 PintOS의 1주차가 지나갔다. 새벽 2시 이전에 잠 든 날이 없는 것 같다.
그만큼 애도 먹었지만, 굉장히 뿌듯하고 재밌었던 경험이 아닐 수 없다.
OS라는 것은 마치 우리가 숨을 쉬며 살지만 '공기'라는 것을 크게 인식하지 못하고 살 듯 컴퓨터를 사용하면서 어쩌면 당연하게만 느꼈던 것 같다.
이러한 OS를 직접 다뤄본다니.
사람이 숨을 쉬면서 공기를 직접 다뤄봐야겠다는 생각은 잘 안하지 않는가? 나에게는 그 정도의 생소함 이었다.
스레드, 프로세스, 우선순위, 선점, 스케줄링... 학부 때 개념론적으로만 학습하고 '아, 이런 것들이 있구나.' 하고 넘어갔을 때와 지금은 그 이해의 깊이 자체가 달라진 기분이다.
https://scientific-string-9e5.notion.site/KraftonJungle-Day61-MLFQS-13863eaa993080b693e3d6a9fde76702
[Pintos] Project 1 : Thread(스레드) - Advanced Scheduler (mlfqs)
https://poalim.tistory.com/36