OS Project1: THREADS(1)

wu2ee·2021년 1월 29일
0

OS PROJECT

목록 보기
4/10

Introduciton (Pintos project)

Pintos-kaist는 x86-64 아키텍처에 대한 simple operating system 이다. 이 프로젝트는 스탠포드 대학의 핀토스 프로젝트에서 파생 되었다. 이번 프로젝트 1은 커널 스레드에 대한 이해와 실습을 진행한다.

* 시작하기 전에 ..

pintos source code 실행하기 앞서 환경 설정이 필요하다.
Makefile에서 메타프로그래밍의 일환으로 빌드시스템을 설정한 뒤 make check를 실행하면 check라는 target을 생성하기 위한 dependency failed 에러가 발생한다. 이를 해결하기 위해 source ./activate를 매번 쳐줘야 하는데, 이를 우분투 home에 있는 .bashrc 에서 환경변수에 추가해주면 매번 쳐줄 필요가 없어진다.

1. '.bashrc'란?
리눅스 부팅 시 미리 적용하여 구동할 때 적용 되도록 설정해 놓는 파일

2. 설정 방법

  • sudo su (root 계정 진입)

  • cd ~ (home directory로 이동)
    (최상위 .bashrc를 설정하면 리눅스 처음 부팅이후 바로 적용한다. 만약 하위 계정의 .bashrc 설정 시 그 계정으로 접속하면 .bashrc를 읽어 적용한다.)

  • vi .bashrc 에 source ./activate 추가한다. (각자의 환경에 맞게 디렉토리를 수정해줄 필요가 있다.)

  • source ./.bashrc (.bashrc 변경된 설정을 재 적용한다, 터미널을 껐다 켜도 적용됨)

참고 : sudo su로 root 계정에 진입 후 다시 빠져 나올때 'su - (유저네임)' 라고 쳐주면 된다.
ex) 'su - ubuntu'


나는 다음 그림과 같이 로컬 Ubuntu 18.04 환경에서 경로를 설정해 주었다. (AWS EC2 인스턴스에서 실행할때도 위에 설명한 방식대로 해주면 된다.)

Background

우선 initial thread system을 이해하기 위해 주어진 코드를 이해할 필요가 있다. 이미 핀토스는 스레드 생성, 완료, 스레드 간의 전환 및 동기화 (세마포어, 잠금, 조건변수등)가 기본적으로 구현되어 있다. 우리는 여기에 특정 기능을 추가하는 것이 이번 프로젝트의 목표이다.

❌경고

핀토스에서는 각 스레드의 크기가 4kB 미만인 고정 크기의 실행 스택이 할당된다. 이때 non-static 지역 변수를 사용하여 더 큰 데이터 구조를 사용하면 Kernel Panic 현상이 일어날 수 있다.

Thread 이해

threads를 위한 핀토스 데이터 구조는 다음과 같이 선언 되어 있다. (threads/thread.h에 선언 되어 있음)


struct thread;

이는 thread 또는 user process를 나타낸다. 모든 스레드 구조는 자체 메모리 페이지의 시작 부분을 차지한다. 페이지의 나머지 부분은 스레드의 스택에 사용되며, 페이지 끝에서 아래로 늘어난다. 다음 그림 처럼 이해할 수 있다.

 4 kB +---------------------------------+
                           |         kernel stack            |
                           |               |                 |
                           |               |                 |
                           |               V                 |
                           |        grows downward           |
                           |                                 |
                           |                                 |
                           |                                 |
                           |                                 |
                           |                                 |
                           |                                 |
                           |                                 |
                           |                                 |
    sizeof (struct thread) +---------------------------------+
                           |             magic               |
                           |          intr_frame             |
                           |               :                 |
                           |               :                 |
                           |             status              |
                           |              tid                |
                      0 kB +---------------------------------+

여기서 주의할 점이 있다.

  • 1. struct thread의 크기가 너무 크지 않도록 해야 한다. 이 경우 커널 스택을 위한 공간이 충분하지 않을 수 있기 때문이다. 기본으로 주어진 struct thread의 크기는 불과 몇 바이트도 되지 않는다. 최대 1kB 이하로 잘 유지해야 한다.
  • 2. 커널 스택이 너무 크면 안 된다. 스택이 오버 플로우 되면 스레드 상태가 손상된다. 따라서 커널 함수는 더 큰 데이터 구조 또는 배열을 non-static 지역 변수로 할당하면 안 된다. 대신 malloc() 또는 palloc_get_page()함수를 사용하여 동적할당 해야 한다.


    참고 블로그 : https://chayan-memorias.tistory.com/87

tid_t tid;

모든 스레드는 커널의 전체 수명 동안 고유한 tid가 있어야 한다. tid_t는 int 자료형의 typedef 이다. 새로운 스레드는 초기 프로세스의 1부터 시작하여 다음으로 높은 tid를 받는다. 원한다면 type과 숫자를 변경할 수 있다.



enum thread_status status;

스레드의 상태를 나타낸다.


THREAD_RUNNING

스레드가 실행중이다. 지정된 시간에 정확히 하나의 스레드가 실행되고 있다. thread_current()는 실행중인 스레드(주소)를 반환한다.

THREAD_READY

스레드가 실행될 준비가 되었지만, 지금 실행되고 있지 않다. 다음에 스케줄러를 호출할 때 이 스레드를 선택하여 실행할 수 있다. 이때 ready_list라는 doubly-linked list에 보관 된다.

THREAD_BLOCKED

스레드는 잠금, 인터럽트 호출과 같은 무언가를 기다린다. 이 스레드는 thread_unblokc()에 대한 호출과 함께 THREAD_READY상태로 전환될 때까지 다시 스케줄(예약) 되지 않는다.
이 state는 자동으로 스레드를 차단하고 해제하는 핀토스 동기화(synchronization)에 사용되며 간접적으로 가장 편하게 이용된다. Block(차단)된 스레드가 무엇을 기다리는지는 알 수 없지만 역추적에 도움이 될 수 있다.

THREAD_DYING

스레드는 다음 스레드로 전환한 후 스케줄러에 의해 삭제 된다.



int priority;

스레드 우선순위 (PRI_MIN(0) 부터 PRI_MAX(63)까지) 이다. 숫자가 낮을 수록 우선순위가 낮아진다. 따라서 0이 우선순위가 가장 낮고 63이 우선순위가 가장 높다.



unsigned magic

이 값은 thread.c에 정의된 임의의 숫자이며, 스택 오버플로를 감지하는데 사용된다. thread_current()는 실행 중인 스레드 구조체의 magic 멤버가 THREAD_MAGIC으로 설정 되었는지 확인한다. 스택 오버플로로 인해 이 값이 변경되어 ASSERT가 발생하는 경우가 있다.

Thread Functions


void thread_init (void);

main()에서 thread system을 초기화하기 위해 호출 된다. 이 함수의 주요 목적은 핀토스의 초기 스레드를 위한 struct thread를 만드는 것이다. 이때 pintos loader는 지금과 또 다른 pintos thread와 동일한 위치, 즉 초기 스레드의 스택을 page 가장 위치 배치한다.

thread_init()을 하지 않고 thread_current()를 호출하면 fail이다. 그 이유는 thread의 magic 값이 잘못 되었기 때문이다. thread_current는 lock_acquire()와 같은 함수에서 많이 호출되므로 항상 핀토스의 초기에 thread_init()을 호출하여 초기화 해준다.


void thread_start (void);

main()에서 호출되어 스케줄러를 시작한다. 이때 idle thread(다른 스레드가 준비지 않았을 때 예약된 스레드를 생성한다) 를 생성한다. 시작할 때 , 인터럽트를 활성화 하고 cpu를 선점할 thread를 스케줄링한다.


void thread_tick (void);

타이머 인터럽트에 의해 호출된다. thread statistics를 track하고 time slice가 만료 될때 스케줄러를 trigger 시킨다.


tid_t thread_create (const char *name, int priority, thread func *func, void *aux);

주어진 우선순위를 이용하여 새로운 스레드 이름을 만들고 새로운 스레드의 tid를 반환한다. 그리고 struct thread와 kernel stack을 위해 page를 할당하고 멤버를 초기화 한다음, fake stack frame을 설정한다. 스레드는 block(차단)된 상태에서 초기화 되었다가 반환 직전에 unblock(차단해제)되어 새로운 스레드를 예약한다.


void thread_block (void);

실행 중인 스레드를 실행 상태 (THREAD_RUNNING)에서 차단된 상태 (THREAD_BLOCKED)로 전환한다. 스레드가 thread_unblock()을 호출할 때까지 스레드가 다시 실행되지 않으므로 다시 스레드를 실행할 방법을 준비해야 한다. 이 thread_block()은 low-level이기 때문에, synchronization 기본요소 중 하나를 대신 사용하는게 좋다.


void thread_unblock (struct thread *thread);

스레드가 block된 상태에서 다시 실행될 수 있도록 한다. 예를들어 스레드가 lock을 기다리다가 사용할 수 있게 됬을때 호출 된다.


struct thread *thread_current (void);

현재 실행중인 thread(주소)를 반환한다.


tid_t thread_tid (void);

실행중인 thread의 id를 반환한다. (thread_current()->tid 와 동일하다)


void thread_yield (void);

실행할 새로운 스레드를 선택하는 스케줄러의 역할을 한다. 이때 CPU를 양보한다. 즉 현재 스레드가 CPU를 양보하여 ready_list에 삽입 된다.



Synchronization 이해

스레드 간 리소스 공유가 신중하고 잘 제어된 방식으로 처리되지 않으면 혼란이 발생 한다. 이는 특히 운영체제의 커널의 경우에 많이 발생 되며, 시스템 전체가 손상 될 수도 있다.

이번 핀토스 프로젝트에서는 인터럽트를 비활성화 하여 커널 스레드와 인터럽트 핸들러 사이의 공유 데이터를 관리한다. 인터럽트를 비활성화 하는 주된 이유는 커널 스레드를 외부 인터럽트 핸들러와 동기화 하기 위함이다. 이때 인터럽트 핸들러는 sleep 하지 않기 때문에 lock을 acquire할 수 없다. 따라서 커널 스레드와 인터럽트 핸들러 사이의 공유 데이터는 인터럽트를 비활성화 해서 커널 스레드를 통해 보호 받아야 한다.

Alaram Clock의 경우, 타이머 인터럽트가 sleep thread를 깨워야 한다. 이때 타이머 인터럽트는 여러 전역 변수와 스레드 구조체의 변수에 접근해야 한다. 커널 스레드에서는 이런 변수에 접근할 때 타이머 인터럽트가 간섭하지 않도록 인터럽트를 비활성화 해야한다.

주의할 점은 인터럽트를 비활성화 하면, 다시 인터럽트를 활성화 해줄 때까지 최소한의 코드로 여러 기능을 처리해야 한다. 인터럽트 비활성화 시 timer ticks 또는 input events와 같은 중요한 정보를 잃을 수 있다. 또한 인터럽트 비활성화 시 interrupt handling latency가 증가하여 너무 오래 동안 비활성화 되면 기계가 느려질 수 있다.

실제로 synch.c 파일에서 동기화할때 기본적으로 인터럽트를 비활성화 함으로써 구현한다. 이때 코드의 양을 늘려야 할 수도 있지만 그래도 최소로 유지해야 한다

🤔 Context switching 이란?

Q. 스레드간의 문맥 전환을 context switching 이라고 하는가 프로세스 간의 문맥전환을 context switching 이라고 하는가? 다른 책에서는 스레드 간의 문맥 전환이 스레드 간의 문맥전환을 context switching 이라고 보고 있다. 내가 알고 있는 사실 또한 process 안의 스레드가 문맥전환이 일어나는 것이지 프로세스 자체가 문맥 전환이 일어나지 않는 것으로 알고 있는데..?

A. context switch라는 용어는 다양한 시스템에서 사용된다. 전통적인 운영체제의 관점에서는 CPU register의 값(register context)이 저장되고 복구되는 작업을 묶어서 context switching라고 지칭한다. 하지만, context(문맥)은 상황에 따라 다양하게 해석 될수 있어 어느 한 가지 말이 맞다고 말하기는 힘들다. 예를 들어 GPU에서는 GPU thread 들 사이에 전환이 일어나느것을 context swith라고 말한다. 다른 책에서 주 스레드가 문맥을 전환하는것이라는 말도, 주 스레드가 바뀌면 프로세스가 바뀌는것과 같다는 측면에서 맞는 이야기 인것 같다. 정리하면, context swiching은 시스템의 상황에 따라 다양한 의미를 가지고 있다. 따라서, context switch가 가지는 기본적인 개념을 이해시고, 그 개념을 각 시스템에 따라 잘 적용하시는것이 좋을것 같다.

profile
즐겁게 코딩합시다

0개의 댓글