Notice
Recent Posts
Recent Comments
«   2025/02   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28
Tags
more
Archives
Today
Total
관리 메뉴

seven05

[PintOS] 정글 WEEK07 WIL. PROJECT 1 : THREADS 본문

SW JUNGLE 9기/Pintos

[PintOS] 정글 WEEK07 WIL. PROJECT 1 : THREADS

박상비누 2024. 10. 1. 01:05

기간: 2024.09.24~2024.09.30

정글의 가장 큰 시련이자 과제인 핀토스 과제를 시작했다. 핀토스의 첫번째 과제인 Threads를 진행하였는데 한주동안 무엇을 배웠는지 어떻게 과제를 해쳐나갔는지 WIL을 정리해보고자한다. (주로 시행착오에 가까운 이야기들)

 

1. Alarm Clock

  • 이 과제에서 첫번째로 만났던 고난은 thread.h 에 선언되어있던 thread 구조체 안에 wake_time 이라는 변수를 넣고 sleep_list를 관리할 생각이였는데 sleep_list에 쓰레드를 list_push 하는 과정에서 자꾸 커널 패닉 에러를 일으켰던 순간이다. 처음에는 기본으로 되어있는 thead 구조체를 건드려서 thread의 magic_number를 건드려서 overflow를 일으키는줄 알았으나 그런 생각으로 아예 sleeping_thread 구조체를 새로 선언해서 wake_time과 struct thread *t 를 분리 시켰지만 해결되지않았었다.(덕분에 이 과제는 쓸데없는 sleeping_thread라는 새로운 구조체가 탄생된채로 완성되었다.) 알고보니 time 인터럽트가 발생할때마다 호출되는 wakeup 함수에 디버깅을 위한 너무 많은 printf가 있었고 이를 제거하자 다른 error가 보이거나 통과되기도 했다. 이 이후부터는 디버깅을 할때 대부분 귀찮더라도 gdb를 사용하는 방향으로 바뀌었다.
  • sleep_list에 timer_sleep이 호출된 쓰레드를 담고 time_interrupt 가 발생하는 1 tick마다 깨우는 함수를 호출해서 일어날 시간이 맞는지 확인하고 깨우는 식으로 진행하였는데, sleep_list를 전부 순회하는 방식으로는 테스트를 잘 통과하지못했다 분명히 1tick마다 호출은 해야할거같지만 머신의 성능이 낮아서 그런가 고민을 했었지만 결국 sleep_list를 정렬해서 관리하고 앞에서부터 확인해서 연산을 줄이는 방법이 해결책이라고 생각하고 테스트를 통과했다. 하지만 생각하면 할수록 단순히 성능차이라고 보기는 어려운 실패 메세지였고 sleep_list를 똑바로 관리하지못해서 prev next 포인터가 잘못되어있던것이 문제라는것을 알수있었다. (gdb로 포인터를 잘찍어보자!)

2-1. Priority scheduler (정적 우선순위 + sema, cond_var, monitor)

  • Project1의 핵심 과제라고 생각되던 우선순위 스케쥴링이였다. 가장 처음에 만난 난관은 priority의 기본 테스트인 alarm-priority와 priority-change test를 통과하기 위해서 기본적인 정적 우선순위 스케쥴링을 먼저 구현했을때였다.정적 우선순위 스케줄링은 처음 쓰레드가 만들어질때 우선순위가 정해진대로만 스케줄링을 관리하면 되기때문에 실행 대기중인 쓰레드들을 관리하는 ready_list의 정렬만 쓰레드 구조체 안의 priority 값을 기준으로 정렬 해주면 된다 생각했고 이를 time_interrupt가 발생할때마다 ready_list의 제일 앞에 있는 쓰레드와 현재실행중인 쓰레드를 비교해서 thread_yield를 하거나 안하거나 결정하면 된다고 생각했다. 하지만 check_priority 함수를 만들고 나서 time_interrupt에 넣자마자 테스트를 돌리면 kernel_panic이 잔뜩 발생했는데 처음에는 위에서 겪은 printf때문인가 하고 다 찾아서 지우고 list를 관리못한 경험때문에 list가 비어있는지 체크하기도 했지만 아니였다. 천천히 뜯어본결과 intr_context() error가 눈에 보였고 좀더 찾아보니 이유를 알수있었다. thread_yield는 cpu 제어권을 넘겨주는 작업인데 이런 작업은 인터럽트 컨텍스트안에서 진행되면 안되고 쓰레드 컨텍스트일때만 진행되야 하는 작업이기에 time_interrupt 도중에 이게 호출되어버리면 kernel입장에서는 예측하지못한 행동이 발생해서 패닉이 오는것이였다. 

 

void check_priority() {
	if (list_empty(&ready_list))
        return;

    struct thread *t = list_entry(list_front(&ready_list), struct thread, elem);

    if (thread_current()->priority < t->priority) {
        if (intr_context())
            intr_yield_on_return();
        else
            thread_yield();
    }
}

 

 

위의 코드처럼 인터럽트 컨텍스트가 존재하는지를 조건으로 yield를 바로 할지 아니면 yield할 예정이라고 알릴지 정했다.

  • 정적 우선순위 스케줄링을 끝낸뒤에는 priority-sema 와 priority-convar 테스트를 통해서 동기화를 위한 semaphore 함수와 cond 함수에 대해서 이해를 하고 함수를 개선하기로 했다. 여기서 상당한 시간을 세마포어와 조건변수의 동작과정이해에 보내게 되었는데 함수에 변경해야하는 점이 waiter_list를 정렬해서 다루어야하고 unblock을 발생시키는 sema_up에서 우선순위 체크를 해준다 이외에는 크게 없었는데 cond_var와 모니터에 대한 이해가 도저히 되지않았다. java를 썻다면 이해가 쉽다고 하는데 와닿지않아서 생산자-소비자 문제의 예시로 겨우 흐름만 잡고 넘어갔다.
  • 생산자 - 소비자문제: 버퍼가 가득 차면 생산자는 기다려야 하고, 버퍼가 비어 있으면 소비자는 기다려야 한다.
struct lock buffer_lock;
struct condition not_full;
struct condition not_empty;
int buffer_count = 0;
const int MAX_BUFFER = 10;

void producer() {
    lock_acquire(&buffer_lock);

    while (buffer_count == MAX_BUFFER) {
        cond_wait(&not_full, &buffer_lock);  // 버퍼가 가득 찼으므로 생산자는 대기
    }

    // 버퍼에 아이템을 추가
    buffer_count++;

    cond_signal(&not_empty, &buffer_lock);  // 소비자에게 버퍼에 아이템이 있다고 신호
    lock_release(&buffer_lock);
}

void consumer() {
    lock_acquire(&buffer_lock);

    while (buffer_count == 0) {
        cond_wait(&not_empty, &buffer_lock);  // 버퍼가 비었으므로 소비자는 대기
    }

    // 버퍼에서 아이템을 소비
    buffer_count--;

    cond_signal(&not_full, &buffer_lock);  // 생산자에게 버퍼에 여유 공간이 생겼다고 신호
    lock_release(&buffer_lock);
}

생산자(Producer):

  • 버퍼가 가득 차면 cond_wait로 not_full 조건변수에서 대기합니다.
  • 소비자가 버퍼에서 아이템을 소비해 여유 공간이 생기면, 소비자가 cond_signal을 보내 생산자를 깨웁니다.

소비자(Consumer):

  • 버퍼가 비어 있으면 cond_wait로 not_empty 조건변수에서 대기합니다.
  • 생산자가 아이템을 버퍼에 추가하면, 생산자가 cond_signal을 보내 소비자를 깨웁니다.

이러한 방식으로 cond_wait와 cond_signal은 스레드 간의 협력을 가능하게 하며, 특정 조건이 만족될 때까지 스레드가 안전하게 대기하고, 조건이 충족되면 대기 상태에서 깨어날 수 있도록 제어합니다.

 

2-2. Priority scheduler(Donation)

  • Priority donation은 

 

(시간압박에 부딪혀서 WIL 미완성상태 일단 코치님 질문을 먼저 정리해서 개념정리를 마치기위해서 아래부터함)

 

Extra) 코치님의 질문 (과제를 진행하면서 알아두면 좋은것)

%rip, %rsp register의 의미와 CPU 동작에서의 역할

  • 일단 둘다 x86 아키텍처의 레지스터이다. 프로그램의 흐름제어 및 메모리 관리에 쓰인다.
  • %rip (Instruction Pointer): 현재 실행 중인 명령어의 메모리 위치를 가르킴, 실행한 뒤에는 다음 명령어를 실행하기 위해서 이값을 증가시킨다. 함수호출이 발생하면 새로운 위치를 가르키도록 갱신됨. 프로그램 카운터라고도 부름
  • %rsp (Stack Pointer): 함수 호출 및 복귀, 지역 변수의 할당 및 해제 등에 사용 스택으로 불리는것처럼 후입선출 구조이며 스택의 가장 위쪽에 위치한 메모리주소를 가르킨다.

assembly 명령어 JMP와 CALL의 차이점

  • JMP(JUMP): 프로그램 실행을 무조건적으로 주어진 주소로 이동시킨다. LOOP나 조건문의 분기 처리에 사용된다.
  • CALL: 현재 실행중인 위치를 스택에 저장한뒤에 주어진 장소로 이동한다.(RET 명령어를 통해서 돌아오기위해서) 함수호출을 수행하는데 쓰인다.

JMP, CALL, RET, PUSH, POP, IRET 명령에서 %rip, %rsp는 어떻게 값이 변하는가?

  • JMP는 %rip 값이 주어진 주소로 갱신된다.
  • CALL은 %rip값이 주어진 주소로 갱신된다. %rsp값은 스택에 새로운값이 차게되므로 감소된다.(스택은 거꾸로 자람)
  • RET은 %rip값이 스택에 기록되어있던 주소로 갱신된다. %rsp값은 스택이 줄어드므로 증가된다.
  • PUSH는 %rsp값이 스택이 늘어나므로 감소된다.
  • POP은 %rsp값이 스택이 줄어드므로 증가된다.
  • IRET는 인터럽트의 처리가 끝나면 이전상태로 복귀하기 위해 사용되므로 %rip값과 %rsp값이 인터럽트 이전상태로 복구된다.

interrupt 발생 시 CPU는 어떻게 동작하는가? register 값 및 연관된 memory는?

  • 현재 실행중인 작업을 중단하고 ISR(인터럽트 서비스 루틴)으로 제어흐름을 전환함. CPU는 현재 작업 상태인 레지스터 값들을 저장하기위해서 스택에 저장하고 ISR이 완료되면 나중에 IRET를 호출해서 다시 가져오고 실행중이던작업을 이어서함

interrupt를 처리한 후에는 어떻게 되는가?

  • ISR이 끝나면 스택에 저장되어있던 레지스터값들을 복구한다. 실행중이던 작업이 이어서 실행된다.

interrupt는 어떻게 enable/disable 시킬 수 있는가? enable/disable 되면 뭐가 달라지는가?

  • IF(인터럽트 플레그)가 1이면 인터럽트를 허용하고 0이면 비활성화한다.
  • CLI (Clear Interrupt Flag)명령어를 사용하면 0으로 바꾸고 STI (Set Interrupt Flag) 명령어를 사용하면 1로 바꾼다.
  • enable : CPU는 외부 인터럽트 요청을 받아들이고 처리할 수 있습니다. 예를 들어, 타이머 인터럽트입출력 인터럽트가 발생하면 CPU가 해당 요청을 처리
  • disable: CPU는 외부 인터럽트를 무시하고, 현재 실행 중인 작업에 집중합니다. 중요한 연산이나 임계 구역에서는 이 기능을 사용해 작업이 중단되지 않도록 보호

Option: NMI (Non-Maskable Interrupt)는 무엇인가?

  • 마스킹할 수 없는 인터럽트로 CPU에 의해 무시될 수 없는 중요한 인터럽트를 의미한다.
  • 매우 긴급하고 중요하기때문에 저지되지않고 처리한다. NMI pin 으로 신호가 들어온다
  • 하드웨어 오류 감지, 정전 등 어쩔수없는 오류 

timer interrupt는 누가 발생시키는가?

  • 하드웨어 타이머 칩/타이머 하드웨어 (Programmable Interval Timer, PIT 또는 APIC Timer) 에 의해 발생

EFLAGS register는 무엇인가?

  • 상태와 제어 플래그를 저장하는 32비트 레지스터
  • 상태 플래그(Status Flags): CPU 연산의 결과에 따라 설정되거나 지워지는 플래그들로, 이후 명령어의 흐름을 결정하는 데 사용됨.
  • 제어 플래그(Control Flags): CPU의 동작을 제어하는 플래그들로, 예를 들어 인터럽트 허용 여부에 사용됨

Option: "플래그 세우지 마라"의 그 flag인가? (ㅋㅋㅋ)

조건이 충족되었음을 나타내는 표식이라 비슷한 개념, 프로그램은 비트로 조건을 기록할뿐

Option: 왜 다른 64bit register처럼 r로 시작하지 않고 e로 시작하는가?

다른애들도 32비트 시절에는 레지스터 이름이 extended 라서 e를 썻는데 64비트 되고 나서 r로 바뀐것이다.

근데 EFLAGS는 여전히 32비트만 쓰고있어서 EFLAG임