2025.05.10
오늘한 내용 : OS - gitbook1장 - Project1: Threads 정리
WEEK 09 : 정글 끝까지(PintOS) - Threads
timer_sleep()
을 busy‐wait 대신 블록/언블록 방식으로 재구현devices/timer.c
void timer_sleep(int64_t ticks)
(wake_tick, cur_thread)
추가 후 thread_block()
timer_interrupt()
current_ticks++
→ 슬립 리스트 순회 → 기상 시점 도달 스레드 thread_unblock()
threads/thread.c
및 threads/thread.h
thread.h
struct thread
에 int priority
, int base_priority
, struct list donations
, struct lock *waiting_lock
필드 추가thread_unblock()
/ thread_yield()
/ thread_create()
등에서list_insert_ordered(&ready_list, &t->elem, cmp_priority, NULL)
사용next_thread_to_run()
ready_list.front
에서 우선순위 높은 스레드 반환threads/synch.c
(lock_acquire()
, lock_release()
)lock_acquire()
holder
가 있으면cur->waiting_lock = lock
donate_priority(cur, lock->holder)
(재귀적 중첩 기부 허용)lock_release()
sema_up(&lock->semaphore)
remove_donations_for_lock()
)current_thread->priority = max(base_priority, highest_donation())
threads/thread.c
void thread_set_priority(int new_priority)
cur->base_priority = new_priority
→ refresh_priority(cur)
→ 양보 조건 시 thread_yield()
int thread_get_priority(void)
return current_thread->priority;
mlfqs
모드)threads/thread.c
, threads/thread.h
, devices/timer.c
thread_mlfqs
옵션 분기thread_get_nice()
/ thread_set_nice(int)
thread_get_recent_cpu()
/ thread_get_load_avg()
recent_cpu
, load_avg
갱신 로직 (timer_interrupt()
, 1초·4틱 주기)priority
계산 공식 반영 (update_priority()
)timer_interrupt()
→ 슬립 처리 + recent_cpu++
+ load_avg
/recent_cpu
/priority
갱신schedule()
→ next_thread_to_run()
→ 우선순위 기반 문맥 전환이 네 단계와 주요 함수들을 중심으로 수정·확장하면 Project 1 요구사항을 모두 충족할 수 있습니다.
threads/
: 프로젝트 1부터 수정할 기본 커널 소스 코드이다.userprog/
: 프로젝트 2부터 수정할 사용자 프로그램 로더 소스 코드이다.vm/
: 거의 비어 있는 디렉터리이다. 프로젝트 3에서 가상 메모리를 구현하는 곳이다.filesys/
: 기본 파일 시스템 소스 코드이다. 프로젝트 2부터 사용하지만 프로젝트 4 전까지는 수정하지 않는다.devices/
: 키보드, 타이머, 디스크 등의 I/O 장치 인터페이스 소스 코드이다. 프로젝트 1에서 타이머 구현만 수정하면 된다.lib/
: 표준 C 라이브러리의 하위 집합을 구현한 디렉터리이다. 커널과 사용자 프로그램 모두에 컴파일된다.include/lib/kernel/
: 커널 전용 C 라이브러리 일부가 포함된 디렉터리이다. 비트맵, 이중 연결 리스트, 해시 테이블 등의 구현이 포함된다.include/lib/user/
: 사용자 프로그램 전용 C 라이브러리 일부가 포함된 디렉터리이다.tests/
: 각 프로젝트 테스트 코드가 위치한 디렉터리이다. 테스트 실행 전에는 원본으로 복원된다.examples/
: 프로젝트 2부터 참조할 예제 사용자 프로그램 모음이다.include/
: 헤더 파일(.h
) 소스 코드가 위치한 디렉터리이다.threads/
: 프로젝트 1부터 수정할 기본 커널 소스 코드이다.devices/
: 키보드, 타이머, 디스크 등의 I/O 장치 인터페이스 소스 코드이다. 프로젝트 1에서 타이머 구현만 수정하면 된다.tests/
: 프로젝트 1 테스트 코드가 위치한 디렉터리이다. 테스트 실행 전에는 원본으로 복원된다.userprog/
: 프로젝트 2부터 수정할 사용자 프로그램 로더 소스 코드이다.filesys/
: 프로젝트 2부터 사용하는 기본 파일 시스템 소스 코드이다. (프로젝트 4 전까지는 수정하지 않는다)examples/
: 프로젝트 2부터 참조할 예제 사용자 프로그램 모음이다.vm/
: 프로젝트 3에서 가상 메모리를 구현할 거의 비어 있는 디렉터리이다.tests/
: 프로젝트 3 테스트 코드가 위치한 디렉터리이다. 테스트 실행 전에는 원본으로 복원된다.filesys/
: 프로젝트 4에서 수정할 파일 시스템 소스 코드이다.tests/
: 프로젝트 4 테스트 코드가 위치한 디렉터리이다. 테스트 실행 전에는 원본으로 복원된다.lib/
: 표준 C 라이브러리의 하위 집합을 구현한 디렉터리이다. 커널과 사용자 프로그램 모두에 컴파일된다.include/lib/kernel/
: 커널 전용 C 라이브러리 일부가 포함된 디렉터리이다. 비트맵, 이중 연결 리스트, 해시 테이블 등의 구현이 포함된다.include/lib/user/
: 사용자 프로그램 전용 C 라이브러리 일부가 포함된 디렉터리이다.include/
: 헤더 파일(.h
) 소스 코드가 위치한 디렉터리이다.동기화(synchronization) 먼저 읽고 오기
thread_create()
→ 스레드 함수 실행 → 함수 반환 시 자동 종료thread_yield()
, 동기화 블록)thread_create()
동작 흐름thread_launch()
호출 → 함수 시작 지점으로 점프sema_down()
, lock_acquire()
등 블록 시)thread_yield()
호출시)idle()
스레드 실행thread_launch()
iret
실행 → 새로운 스레드로 제어 이동break schedule
step
로 한 단계씩 추적struct thread
주소·상태 확인do_iret()
이후 다른 스레드가 실행됨 관찰int buf[1000];
) 사용 시 오버플로우 가능palloc
)malloc
)thread_create()
→thread_launch()
흐름 파악thread_launch()
, 타이머 인터럽트, 동기화 블록, thread_yield()
에서 발생bootstrap()
으로 진입. (수정 불필요)start.S
위치 지정.main()
) 코드. 초기화 순서 확인 및 추가 코드 삽입 가능.struct thread
정의.malloc()
/free()
구현.intr_enable()
/intr_disable()
함수.printf()
사용).printf()
사용).ctype.h
, inttypes.h
, limits.h
, stdarg.h
, stdbool.h
,stddef.h
, stdint.h
, stdio.c/.h
, stdlib.c/.h
, string.c/.h
printf()
등 콘솔 출력 구현.struct thread
)에 새 필드를 추가하고, 스케줄러(schedule()
, next_thread_to_run()
)를 확장합니다.thread_launch()
)는 그대로 두되, 우선순위나 MLFQS 같은 고급 스케줄링 기능을 여기에 구현합니다.thread_create()
, thread_exit()
)도 필요에 따라 수정합니다.struct thread
에 우선순위, 티켓, 계층별 큐 포인터 등 프로젝트 요구사항에 맞는 필드를 선언합니다.timer_sleep(int64_t ticks)
구현을 busy-wait에서 슬립 리스트+인터럽트 방식으로 바꿉니다.timer_interrupt()
핸들러에 “깨어날 스레드 체크 → thread_unblock()
” 로직을 추가합니다.intr_disable()
/intr_enable()
범위를 조절해야 할 수 있습니다.작업 내용 | 수정 파일 |
---|---|
스레드 구조체·스케줄러 확장 | threads/thread.cthreads/thread.h |
timer_sleep() 재구현 | devices/timer.c |
조건부 우선순위 기부 등 동기화 확장 | threads/synch.c / synch.h (선택) |
(필요 시) 인터럽트 동기화 범위 조정 | threads/interrupt.c |
기본 환경 점검
threads/
디렉터리에서 make
→ pintos run alarm-multiple
실행스레드 구조체와 스케줄러 살펴보기
threads/thread.h
의 struct thread
threads/thread.c
의 thread_init()
, schedule()
, next_thread_to_run()
읽기schedule()
에 breakpoint 걸고 문맥 전환 흐름 파악Alarm Clock (timer_sleep) 재구현
devices/timer.c
에서 timer_sleep()
을 슬립 리스트 + timer_interrupt()
내 thread_unblock()
방식으로 교체timer_msleep/usleep/nsleep()
은 건드리지 않아도 자동 호출됨우선순위 필드 추가 및 초기화
struct thread
에 int priority
필드 추가thread_create()
와 thread_init()
에서 기본 우선순위(예: PRI_DEFAULT
) 설정준비 큐 우선순위 순 정렬
next_thread_to_run()
또는 schedule()
에서 ready list 를 우선순위 비교 함수로 뽑아오도록 수정thread_yield()
, thread_unblock()
등에서 리스트 삽입 시 list_insert_ordered()
사용Lock에 우선순위 기부(priority donation) 적용
threads/synch.c
의 lock_acquire()
/lock_release()
에 다음 로직 추가동기화 테스트와 디버깅
threads
디렉터리에서 make check
→ 실패한 테스트 확인threads/alarm-priority
, threads/priority-donate
등 관련 테스트 집중 실행코드 정리 및 최종 검토
printf()
또는 디버깅용 인터럽트 비활성화 제거아카이브 생성 및 제출
cd threads
TEAM=<번호> make archive
team<번호>.tar.gz
파일을 제출threads/synch.c
내부에서 이미 인터럽트 비활성화를 이용해 원자성(atomicity)을 보장while (...) thread_yield();
같은 루프는 CPU 낭비요약
timer_sleep(int64_t ticks)
ticks
타이머 틱만큼 대기시킨다.TIMER_FREQ
).void timer_sleep(int64_t ticks) {
int64_t start = timer_ticks();
while (timer_ticks() < start + ticks)
thread_yield();
}
timer_sleep()
동작 방식timer_ticks()
로 현재 틱 수를 읽고thread_yield()
로 CPU를 양보(wake_tick, 스레드)
를 등록 후 thread_block()
current_tick
증가 직후wake_tick ≤ current_tick
인 스레드를 thread_unblock()
wake_up_tick
라는 추가 변수를 기준으로 시간이 되었는지를 슬립 리스트 안에서 조건 검사하는 방식thread_yield()
thread_unblock()
PRI_MIN
(0) ~ PRI_MAX
(63)PRI_DEFAULT
(31)void thread_set_priority(int new_priority);
// 현재 스레드 우선순위 설정
// 더 이상 최고가 아니면 즉시 yield
int thread_get_priority(void);
// 기부 포함된 실제 우선순위 반환
시스템에서 실행되는 작업들의 평균 응답 시간을 줄이기 위해, 4.4BSD 스케줄러와 유사한 다단계 피드백 큐 스케줄러를 구현하라.
우선순위 스케줄러와 마찬가지로, 고급 스케줄러도 스레드 우선순위를 기준으로 실행 대상을 결정한다. 다만, 우선순위 기부(priority donation) 기능은 포함하지 않는다.
코드 작성 시 부팅 시점에 스케줄링 정책을 선택할 수 있도록 해야 한다. 기본값은 우선순위 스케줄러가 동작하지만,
-mlfqs
옵션을 주면 4.4BSD 스타일 고급 스케줄러가 대신 동작해야 한다.
mlfqs
모드에서는 스레드가 스스로 우선순위를 설정하거나thread_set_priority()
를 호출해도 무시된다. 대신 스케줄러가 nice 값과 recent_cpu를 기반으로 동적으로 우선순위를 계산한다.
recent_cpu
를 1 증가시킨다.timer_ticks() % TIMER_FREQ == 0
)recent_cpu
를 지수 가중 이동 평균으로 갱신load_avg
(시스템 부하 평균)를 갱신```markdown
priority = PRI_MAX - (recent_cpu/4) - (nice*2)
recent_cpu = (2load_avg)/(2load_avg + 1) * recent_cpu + nice
load_avg = (59/60)*load_avg + (1/60)*ready_threads
- 정수만 지원 → **17.14 고정소수점** 사용
- 곱셈: `((int64_t)x*y)/f`, 나눗셈: `((int64_t)x*f)/y`
```
항목 | 빈도 | 시점 |
---|---|---|
recent_cpu | 매 틱(실행 스레드) | timer interrupt |
recent_cpu | 1초마다 | timer_ticks() % TIMER_FREQ == 0 |
load_avg | 1초마다 | timer_ticks() % TIMER_FREQ == 0 |
priority | 매 4틱 또는 1초마다 | recent_cpu / nice 변경 시 |
이렇게 구현하면,
thread_get_nice()
, thread_set_nice()
thread_get_recent_cpu()
, thread_get_load_avg()
thread_tick()
, thread_update_priority()
)mlfqs
플래그에 따라 스케줄러 분기 처리Arithmetic | C |
---|---|
Convert n to fixed point | n * f |
Convert x to integer (rounding toward zero) | x / f |
Convert x to integer (rounding to nearest) | (x + f / 2) / f if x >= 0 |
(x - f / 2) / f if x <= 0 | |
Add x and y | x + y |
Subtract y from x | x - y |
Add x and n | x + n * f |
Subtract n from x | x - n * f |
Multiply x by y | ((int64_t) x) * y / f |
Multiply x by n | x * n |
Divide x by y | ((int64_t) x) * f / y |
Divide x by n | x / n |
devices/timer.c
(29행)include/threads/fixed-point.h
(10행, 새 파일)include/threads/synch.h
(4행)include/threads/thread.h
(21행)threads/synch.c
(143행)threads/thread.c
(135행)targets.mk
에 .c
파일을 dir_SRC
변수에 추가warning: no previous prototype for func
경고는?static
으로 변경TIMER_FREQ
번/초 (기본 100Hz, devices/timer.h
)TIME_SLICE
틱/슬라이스 (기본 4, threads/thread.c
)make
후 pintos run <test-name>
또는 make check
참조전체 테스트 한 번에 돌리기
cd threads
make check
threads/
디렉터리에서 make check
를 실행하면 제공된 모든 테스트가 순차적으로 실행됩니다.개별 테스트 실행하기
pintos -- run <test-name>
예를 들어
pintos -- run alarm-priority
pintos -- run priority-sema
처럼 원하는 테스트 이름을 지정해서 돌릴 수 있습니다.
결과 확인
- make check
나 개별 pintos run
명령 후 출력되는 로그를 보고, 실패한 테스트 이름과 백트레이스를 확인하세요.
참고: 테스트 이름은 threads/tests/ 디렉터리 안에 있는 소스 파일 이름과 대응됩니다.
pass()
테스트 실패 시 백트레이스 혼동?fail()
→ debug_panic()
호출debug_panic()
은 NO_RETURN, 복귀 코드가 없어 백트레이스가 옆 함수를 가리킴schedule()
후 인터럽트 재활성화 시점은?intr_enable()
호출thread_yield()
, sema_down()
, idle()
등 경로에서 복원yield()
하면?thread_unblock()
후 우선순위 상승 가능thread_yield()
호출로 선점해야 함thread_set_priority()
와 기부 상호작용은?base_priority
를 설정하고 priority = max(base_priority, donated_max)
base_priority
로 복원printf()
호출 → 출력이 중첩추가 테스트 실패 시
- 실패한 테스트 소스 확인
- 고정소수점 연산(overflow) 점검
- 타이머 인터럽트 핸들러 과부하 여부 확인