본문 바로가기

정글캠프-WIL/서브아이템

Pintos - 어떻게 부팅해서 운영체제가 되는가? [2]

https://gyumingomin.tistory.com/85

 

Pintos - 어떻게 부팅해서 운영체제가 되는가? [1]

Pintos는 어떻게 부팅해서 운영체제가 되는가?start.S에서 main(), 그리고 thread_exit() 까지의 흐름 정리 [1] Pintos를 보면서 가장 인상 깊었던 지점은 main()이 시작점이 아니라는 점이다.보통 프로그램에

gyumingomin.tistory.com

먼저 읽고 오시길 추천드립니다.

 

start.S에서 main() 그리고 thread_exit()까지의 흐름 정리 [2]

이전에서 start.S의 어셈블리어 흐름을 조사했다면(물론 어셈블리어 전부를 다 보진 못했지만), 이제는 실제로 OS가 어떻게 초기화되는지 명확히 하고 넘어가고자 한다.
start.S -> main() -> thread_exit() 흐름을 따라가며, Pintos가 어떻게 "실제로 동작하는 운영체제"가 되는지 정리

init.c의 main()

앞에서 보았듯 start.S에서 CPU를 "실행 가능 상태"로 만드는 부트 어셈블리의 역할이 끝났다.
       start.S
          ↓
    long mode 진입
          ↓
임시 페이지 테이블 준비
          ↓
       GDT 로드
          ↓
    커널 스택 설정

 

1. bss_init(): 커널의 전역 상태를 0으로 정리하는 첫 작업

이 함수는 .bss 영역을 0으로 채움 (.bss는 초기값이 없는 전역 변수와 static 변수가 놓이는 공간)

2. read_command_line() / parse_options(argv)

로더가 메모리를 남겨 둔 커맨드 라인 인자를 읽어 argv 형태로 해석후, -q, -mlfqs, -ul 같은 옵션을 파싱해 이후 초기화 정책을 결정
이 순서가 앞쪽에 있는 이유는 스케줄러를 어떤 방식으로 쓸지, 종료 시 전원을 끌지, 유저 메모리를 얼마나 줄지 같은 정책은 나머지 커널 초기화보다 먼저 정해져야 하기 때문
parse_options : 어떤 커널로 부팅할지 먼저 결정

3. thread_init() : 현재 실행 중인 흐름을 main 스레드로 바꿈

현재 실행 중인 흐름을 main 스레드로 변경
ready_list, tid_list, destruction request list를 ㅊ기화하고, 지금까지 흘러오던 부팅 흐름 자체를 main 스레드로 등록
이 순간부터 pintos는 하나의 흐름이 그냥 실행되는 코드가 아니라 스레드라는 추상화 위에서 돌아가는 운영체제가 됨

 

4. console_init()

콘솔 락은 스레드와 동기화 primitive 위에서 동작하므로, 반드시 thread_init()뒤에 와야 함
즉, 콘솔도 그냥 출력 장치가 아니라 동시성 환경에서 안전하게 쓰이도록 재정의 되는 셈

5. palloc_init() / malloc_init() / paging_init()

메모리 초기화
palloc_init()은 페이지 단위 물리 메모리 할당기를 준비
malloc_init()은 작은 크기 요청을 처리할 블록 할당기를 준비
paging_init()은 커널이 사용할 정식 페이지 테이블을 만듬
운영체제가 메모리를 쓰기 전에 먼저 메모리를 어떻게 해석하고 나눌 것인지 규칙부터 만든다는 것

더보기

(다음 주 차에 공부할 예정)

tss_init() / gdt_init()

`USERPROG`가 켜져 있다면 다음 단계는 `tss_init()`과 `gdt_init()`

TSS가 "Task 전환 전체"를 담당하는 오래된 구조라기보다, Pintos에서는 "유저 모드에서 인터럽트나 시스템 콜이 들어왔을 때 사용할 커널 스택 주소를 알려주는 역할"을 한다는 점

즉, 유저 프로그램을 돌리기 전 먼저 준비해야 하는 것은 "유저 코드를 실행하는 방법"보다 "문제가 생겼을 때 커널로 안전하게 복귀하는 방법"

6. intr_init() 부터 input_init()

인터럽트를 켜기 전에 먼저 인터럽트 핸들러를 등록하고, 인터럽트 디스크립터 테이블을 설정해서 정책 준비

7. thread_start()

idle thread를 생성한 뒤 intr_enable을 호출해 여기서 부터 진짜 운영체제가 됨
이 시점부터는 더 이상 하나의 부팅 흐름이 순서대로만 흘러가지 않게 됨
타이머 인터럽트가 들어오고, ready queue가 의미를 가지며, 필요하면 현재 실행 중인 스레드가 CPU를 빼앗길 수도 있음

 

 

8. serial_init_queue() / timer_calibrate()

스케줄러가 살아난 뒤에 시리얼 큐 , 타이머 보정, 파일 시스템, VM 같은 상위 기능들이 붙음

9. run_action

부팅이 끝난 뒤, 실제 일을 시작
Boot complete가 출력된 뒤 Pintos는 run_actions(argv)를 호출
여기서 run test-name이면 스레드 테스트를 돌리고, 유저 프로그램 모드에서는 첫 유저 프로세스인 initd를 만든 뒤 기다림
부팅과 실행이 분리되어 있는 상태
즉, 지금까지의 모든 과정은 "일을 하기 위한 준비"였고, 실제로 커널이 무엇을 할지는 run_actions()에서 비로소 시작

10. power_off() 또는 thread_exit()

마지막에 -q 옵션이 있으면 power_off()를 호출하고, 아니면 thread_exit()으로 빠짐
일반 사용자 프로그램이라면main()이 끝나면 그냥 return 하고 프로세스가 종료
하지만 Pintos의 main()은 이미 하나의 커널 스레드로 끝도 함수 반환이 아니라 스레드 종료로 처리
thread_exit()은 현재 스레드를 즉시 free하지 않고 자기 자신의 커널 스택 위에서 아직 실행 중이므로 THREAD_DYING으로 상태를 바꾸고 스케줄링한 뒤, 나중에 다른 문맥에서 안전하게 정리함

정리

Pintos의 부팅 흐름은 단순한 초기화 목록이 아니라
start.S 에서는 CPU가 커널을 실행할 수 있는 환경을 만들고,
init.c 에서는 커널이 운영체제로 구성되며
thread_start() 이후에는 진짜 인터럽트와 스케줄링이 살아있는 시스템이 되며
마지막에 main()은 함수가 아니라 하나의 스레드로서 생을 마침