Skip to content
Snippets Groups Projects
Commit 5b1638de authored by 홍 유빈's avatar 홍 유빈
Browse files

Add new file

parent 6742f0fa
No related branches found
No related tags found
No related merge requests found
### 프로젝트 1: 스레드 (Threads)
이 과제에서는 최소한의 기능을 가진 스레드 시스템을 제공합니다. 여러분의 과제는 이 시스템의 기능을 확장하여 동기화 문제를 더 깊이 이해하는 것입니다. 이 과제는 주로 `threads` 디렉토리에서 진행되며, 일부 작업은 `devices` 디렉토리에서 수행됩니다. 컴파일은 threads 디렉토리에서 이루어져야 합니다. 프로젝트 설명을 읽기 전에 `Synchronization`(동기화) 자료를 최소한 훑어보는 것이 좋습니다.
### 배경 지식
## 스레드 이해하기
첫 번째 단계는 초기 스레드 시스템 코드를 읽고 이해하는 것입니다. Pintos는 이미 스레드 생성과 종료, 스레드 간 전환을 위한 간단한 스케줄러, 그리고 동기화 프리미티브(세마포어, 락, 조건 변수, 최적화 방지 장치)를 구현하고 있습니다.
이 코드의 일부는 약간 이해하기 어려울 수 있습니다. 아직 Introduction에 설명된 대로 기본 시스템을 컴파일하고 실행해보지 않았다면, 지금 실행해보십시오. 소스 코드의 일부를 읽고 어떤 일이 발생하는지 확인할 수 있습니다. 필요하다면 `printf()`를 거의 어디에나 추가하여 코드를 다시 컴파일하고 실행하면서 어떤 일이 발생하고 어떤 순서로 실행되는지 확인할 수 있습니다. 또한 커널을 디버거로 실행하고, 흥미로운 지점에 중단점을 설정하거나, 코드를 단계별로 실행하고 데이터를 확인할 수도 있습니다.
스레드가 생성되면 스케줄링 할 새로운 실행 컨텍스트가 생성됩니다. 이 컨텍스트에서 실행할 함수는 thread_create()의 인자로 전달됩니다. 스레드가 처음 스케줄링되고 실행되면 이 함수의 시작부터 실행을 시작하며, 해당 컨텍스트 내에서 실행됩니다. 이 함수가 반환되면 스레드는 종료됩니다. 따라서 각 스레드는 Pintos 내부에서 실행되는 작은 프로그램처럼 동작하며, `thread_create()`에 전달된 함수는 이 프로그램의 `main()`처럼 작동합니다.
항상 한번에 정확히 한 스레드만 실행되며, 나머지 스레드는 비활성 상태가 됩니다. 스케줄러는 다음에 실행할 스레드를 결정합니다. (주어진 시점에 실행 준비가 된 스레드가 없다면, `idle()`에서 구현된 특수한 idle thread가 실행됩니다.) 동기화 원시는 한 스레드가 다른 스레드의 작업을 기다려야 할 때 컨텍스트 전환을 강제할 수 있습니다.
컨텍스트 전환의 동작은 `threads/thread.c``thread_launch()`에 있습니다. (이 부분을 이해할 필요는 없습니다.) 이 함수는 현재 실행 중인 스레드의 상태를 저장하고, 전환할 스레드의 상태를 복원합니다.
GDB 디버거를 사용하여 컨텍스트 전환의 동작을 천천히 추적해보십시오(GDB를 참고하세요). `schedule()`에 중단점을 설정한 후, 그 지점부터 단계별로 실행해보세요. 각 스레드의 주소와 상태를 추적하고, 각 스레드의 호출 스택에 어떤 프로시저가 있는지 확인하십시오. `do_iret()`에서 `iret` 명령어가 실행되면 다른 스레드로 전환되는 것을 확인할 수 있습니다.
Warning: Pintos에서는 각 스레드에 4kB 미만의 작은, 고정된 실행 스택이 할당됩니다. 커널은 스택 오버플로를 감지하려고 하지만 완벽하게 감지할 수는 없습니다. 예를 들어, `int buf[1000];`와 같은 큰 데이터 구조를 비정적 지역 변수로 선언하면 스택 오버플로로 인해 예기치 않은 커널 패닉 등과 같은 문제가 발생할 수 있습니다. 스택 할당 대신 페이지 할당자와 블록 할당자를 사용할 수 있습니다. (자세한 내용은 Memory Allocation을 참조하십시오.)
## 소스 파일
아래는 threads 디렉토리와 include/threads 디렉토리에 있는 파일들에 대한 간략한 개요입니다. 이 코드의 대부분은 수정할 필요가 없지만, 이 개요를 통해 어떤 코드를 살펴봐야 할지 감을 잡을 수 있기를 바랍니다.
# threads 코드
•loader.S, loader.h
커널 로더. 512바이트의 코드와 데이터로 구성되어 PC BIOS가 이를 메모리에 로드하고, 이후 디스크에서 커널을 찾아 메모리에 로드한 후 start.S의 bootstrap()으로 점프합니다. 이 코드를 수정하거나 살펴볼 필요는 없습니다. start.S는 메모리 보호 설정과 64비트 긴 모드 전환을 위한 기본 설정 코드입니다. loader와 달리 이 코드는 실제로 커널의 일부입니다.
•kernel.lds.S
커널을 링크하는 데 사용되는 링크 스크립트입니다. 커널의 로드 주소를 설정하고, start.S가 커널 이미지의 시작 부분 근처에 배치합니다. 이 코드를 수정하거나 살펴볼 필요는 없지만, 혹시 관심이 있다면 참고할 수 있도록 여기에 포함되어 있습니다.
•init.c, init.h
커널 초기화 코드로, 커널의 메인 프로그램인 main()을 포함합니다. 적어도 main()을 살펴보아 어떤 초기화가 이루어지는지 확인하십시오. 필요하다면 여기에 여러분만의 초기화 코드를 추가할 수도 있습니다.
•thread.c, thread.h
기본적인 스레드 지원을 제공합니다. 여러분의 작업 대부분은 이 파일에서 이루어질 것입니다. thread.h는 구조체 스레드를 정의하며, 이는 네 가지 프로젝트 모두에서 수정하게 될 가능성이 높습니다. 자세한 내용은 Threads 섹션을 참고하십시오.
•palloc.c, palloc.h
페이지 할당기로, 4 kB 페이지 단위로 시스템 메모리를 할당합니다. 자세한 내용은 Page Allocator를 참고하십시오.
•malloc.c, malloc.h
커널용 malloc()과 free()의 간단한 구현입니다. 자세한 내용은 Block Allocator를 참고하십시오.
•interrupt.c, interrupt.h
기본적인 인터럽트 처리 및 인터럽트를 켜고 끄는 함수들을 제공합니다.
•intr-stubs.S, intr-stubs.h
낮은 레벨 인터럽트 처리를 위한 어셈블리 코드입니다.
•synch.c, synch.h
기본 동기화 도구: 세마포어(semaphores), 락(locks), 조건 변수(condition variables), 최적화 배리어(optimization barriers). 이들은 네 가지 프로젝트 모두에서 동기화를 위해 사용해야 합니다. 자세한 내용은 Synchronization을 참고하십시오.
•mmu.c, mmu.h
x86-64 페이지 테이블 작업을 위한 함수입니다. lab1 이후에 이 파일을 자세히 살펴보게 될 것입니다.
•io.h
I/O 포트 접근을 위한 함수들입니다. 주로 devices 디렉토리의 소스 코드에서 사용되며, 여러분은 이 코드를 수정할 필요가 없습니다.
•vaddr.h, pte.h
가상 주소 및 페이지 테이블 항목을 다루기 위한 함수와 매크로입니다. 프로젝트 3에서 더 중요해질 내용이므로, 지금은 신경쓰지 않아도 괜찮습니다.
•flags.h
x86-64 플래그 레지스터의 몇 가지 비트를 정의하는 매크로입니다. 거의 신경쓰지 않아도 됩니다.
# devices 코드
기본 스레드 기반 커널에는 devices 디렉토리에 다음 파일들도 포함됩니다:
•timer.c, timer.h
시스템 타이머로, 기본적으로 초당 100번 ticks을 생성합니다. 이 프로젝트에서 이 코드를 수정해야 합니다.
•vga.c, vga.h
VGA 디스플레이 드라이버. 화면에 텍스트를 출력하는 역할을 합니다. 이 코드를 직접 볼 필요는 없습니다. printf() 함수가 VGA 디스플레이 드라이버를 호출하므로, 이 코드를 직접 호출할 이유는 거의 없습니다.
•serial.c, serial.h
직렬 포트 드라이버. printf()가 이 코드를 대신 호출하므로, 직접 호출할 필요는 없습니다. 이 코드는 직렬 입력을 처리하며, 이를 입력 계층(아래 참조)에 전달합니다.
•block.c, block.h
블록 디바이스를 위한 추상화 계층으로, 고정 크기 블록 배열로 구성된 랜덤 액세스 디스크 유사 장치입니다. Pintos는 기본적으로 두 가지 유형의 블록 디바이스: IDE 디스크와 파티션을 지원합니다. 블록 디바이스는 프로젝트 2까지는 실제로 사용되지 않습니다.
•ide.c, ide.h
최대 4개의 IDE 디스크에서 섹터를 읽고 쓰는 것을 지원합니다.
•partition.c, partition.h
디스크의 파티션 구조를 이해하여 하나의 디스크를 여러 독립적인 영역(파티션)으로 나눌 수 있도록 합니다.
•kbd.c, kbd.h
키보드 드라이버. 키 입력을 처리하고 이를 입력 계층(아래 참조)으로 전달합니다.
•input.c, input.h
입력 계층. 키보드나 직렬 드라이버에서 전달된 입력 문자를 큐에 저장합니다.
•intq.c, intq.h
인터럽트 큐로, 커널 스레드와 인터럽트 핸들러가 모두 접근하고자 하는 순환 큐를 관리합니다. 키보드와 직렬 드라이버에서 사용됩니다.
•rtc.c, rtc.h
실시간 시계 드라이버로, 커널이 현재 날짜와 시간을 확인할 수 있도록 합니다. 기본적으로, 이는 thread/init.c에서 난수 생성기의 초기 시드를 선택하기 위해 사용됩니다.
•speaker.c, speaker.h
PC 스피커에서 톤을 생성할 수 있는 드라이버입니다.
•pit.c, pit.h
8254 프로그래밍 가능 인터럽트 타이머(PIT)를 구성하는 코드입니다. 이 코드는 각 디바이스가 PIT의 출력 채널 중 하나를 사용하기 때문에 devices/timer.c와 devices/speaker.c 모두에서 사용됩니다.
# lib 코드
마지막으로 lib와 lib/kernel에는 유용한 라이브러리 루틴이 들어 있습니다. (lib/user는 프로젝트 2부터 사용자 프로그램에서 사용되지만 커널의 일부는 아닙니다.) 다음은 몇 가지 자세한 내용 입니다:
•ctype.h, inttypes.h, limits.h, stdarg.h, stdbool.h, stddef.h, stdint.h, stdio.c, stdio.h, stdlib.c, stdlib.h, string.c, string.h
표준 C 라이브러리의 하위 집합입니다.
•debug.c, debug.h
디버깅을 돕는 함수와 매크로입니다. 자세한 내용은 디버깅 도구를 참조하세요.
•random.c, random.h
의사난수 생성기(pseudorandom number generator, PRNG)입니다. 실제 난수 값의 시퀀스는 Pintos 실행마다 다르지 않습니다.
•round.h
반올림을 위한 매크로입니다.
•syscall-nr.h
시스템 호출 번호입니다. 프로젝트 2까지는 사용되지 않습니다.
•kernel/list.c, kernel/list.h
이중 연결 리스트 구현입니다. Pintos 코드 전반에 사용되며, 프로젝트 1에서 직접 사용하게 될 것입니다. 시작하기 전에 이 코드를 훑어보는 것이 좋습니다(특히 헤더 파일의 주석).
•kernel/bitmap.c, kernel/bitmap.h
비트맵 구현. 원한다면 코드에서 사용할 수 있지만, 프로젝트 1에서는 필요하지 않을 것입니다.
•kernel/hash.c, kernel/hash.h
해시 테이블 구현. 프로젝트 3에 유용할 것 같습니다.
•kernel/console.c, kernel/console.h, kernel/stdio.h
printf() 및 몇 가지 다른 함수를 구현합니다.
## 동기화
적절한 동기화는 이러한 문제를 해결하는 데 중요한 부분입니다. 모든 동기화 문제는 인터럽트를 끄면 쉽게 해결할 수 있습니다. 인터럽트가 꺼져 있는 동안에는 동시성이 없어지므로 경쟁상태(Race Condition)가 될 가능성이 없습니다. 모든 동기화 문제를 이런 방식으로 해결하고 싶지만 그렇게 하면 안됩니다. 대신 세마포어, 락(locks) 및 조건 변수를 사용하여 대부분의 동기화 문제를 해결하세요. 동기화에 대한 투어 섹션(동기화 참조)이나 어떤 상황에서 어떤 동기화 기본 요소를 사용할 수 있는지 확실하지 않은 경우 threads/synch.c의 주석을 읽어보세요.
Pintos 프로젝트에서 인터럽트를 비활성화하여 가장 잘 해결할 수 있는 유일한 문제는 커널 스레드와 인터럽트 핸들러 간에 공유되는 데이터를 조정하는 것입니다. 인터럽트 핸들러는 sleep 호출을 할수 없으므로 락을 얻을 수 없습니다. 즉, 커널 스레드와 인터럽트 핸들러 간에 공유되는 데이터는 인터럽트를 비활성화 하여 커널 스레드 내에서 보호해야 합니다.
이 프로젝트는 인터럽트 핸들러에서 약간의 스레드 상태에만 액세스하면 됩니다. 알람 클락(Alarm Clock)의 경우 타이머 인터럽트는 sleep 상태의 스레드를 깨워야 합니다. 고급 스케줄러에서 타이머 인터럽트는 몇 가지 전역 및 스레드별 변수에 액세스해야 합니다. 커널 스레드에서 이러한 변수에 액세스하는 경우 타이머 인터럽트의 간섭을 막기 위하여 인터럽트를 비활성화해야 합니다.
인터럽트를 끄는 경우 가능한 한 최소한의 코드에 대해서만 끄도록 주의하세요. 그렇지 않으면 타이머 틱이나 input 이벤트와 같은 중요한 항목을 잃을 수 있습니다. 인터럽트를 끄면 인터럽트 처리 지연 시간도 늘어나기에 머신이 느리게 느껴질 수 있습니다.
synch.c의 동기화 기본 요소 자체는 인터럽트를 비활성화하여 구현됩니다. 여기서 인터럽트가 비활성화된 상태에서 실행되는 코드 양을 늘려야 할 수도 있지만, 최소한으로 유지하도록 노력하세요.
인터럽트를 비활성화하면 디버깅에 유용할 수 있습니다. 코드 섹션이 중단되지 않도록 해야 하기 때문입니다. 프로젝트를 제출하기 전에 디버깅 코드를 제거해야 합니다. (단순히 주석으로 처리하지 마세요. 코드의 가독성이 떨어집니다.)
제출 시 바쁜 대기(busy waiting)가 없어야 합니다. thread_yield()를 호출하는 타이트 루프는 바쁜 대기의 한 형태입니다.
## 개발 제안
과거에는 많은 그룹이 과제를 여러 부분으로 나누고, 각 그룹원이 마감일 직전까지 각자의 작업을 한 다음, 그때 그룹이 다시 모여 코드를 결합하고 제출했습니다. 이는 좋지 않은 방법입니다. 이 방법은 권장하지 않습니다. 이렇게 하는 그룹은 종종 두 가지 변경 사항이 서로 충돌하여 마지막에 많은 디버깅이 필요합니다. 이렇게 한 그룹 중 일부는 컴파일이나 부팅조차 되지 않은 코드를 제출하여 테스트를 통과하지 못했습니다.
대신 git과 같은 소스 코드 제어 시스템을 사용하여 팀의 변경 사항을 일찍 자주 통합하는 것이 좋습니다. 이렇게 하면 모든 사람이 다른 사람의 코드를 완성된 시점이 아니라 작성된 대로 볼 수 있으므로 당황할 상황이 줄어듭니다. 이러한 시스템을 사용하면 변경 사항을 검토하고 변경 사항으로 인해 버그가 발생하면 작동되던 이전 버전으로 돌아갈 수도 있습니다.
이 프로젝트와 후속 프로젝트를 진행하는 동안 이해하지 못하는 버그가 발생할거라 생각해야 합니다. 그럴 때, 디버깅 도구에 대한 부록을 다시 읽어보세요. 여기에는 속도를 높이는 데 도움이 되는 유용한 디버깅 팁이 많습니다(디버깅 도구 참조). 모든 커널 패닉이나 어설션 실패 시 해결하는 데 도움이 될 백트레이스에 대한 섹션을 꼭 읽어보세요(백트레이스 참조).
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment