본문 바로가기
회고록

[WIL]🙈PINTOS_KAIST : Project 1. THREADS (1) Alarm Clock 🙉

by NOHCODING 2022. 11. 20.
반응형
🚫 현재 글은 정확하지 않은 정보가 있을 수 있습니다. 언제든지 틀린 부분에 대해 댓글을 달아주세요! 🚫

 

00. 운영체제는 왜 개발자에게 필요할까?

 

운영체제란 컴퓨터 하드웨어 바로 위에 설치 되어 사용자 및 다른 모든 소프트웨어 하드웨어를 연결하는 소프트웨어 계층이다. 운영체제의 목적은 컴퓨터의 자원을 효율적으로 관리하는 것에 있다. 여기서 자원이란 프로세서, 기억장치, 입출력 장치 등을 의미한다. 따라서 개발자에게 운영체제를 알고 모르고의 차이는 마치 게임의 현금결제를 하고 말고와 같다.

 

운영체제는 동시 작업 가능여부와 처리방식에 따라 분류된다.

 첫째, 동시 작업 가능여부는 한 번에 하나의 작업만 처리하는 단일작업이 존재한다. 둘째, 동시에 두개 이상의 작업을 처리하는 다중 작업이 존재하며, 현대에는 다중 작업들을 지원하고 있다. 현재 진행하는 PINTOS_KAIST 는 단일 작업으로 진행된다.

 

운영체제 존재 목적의 이유는 하드웨어의 다양한 자원들을 어떻게 효율적으로 관리하는 것에서 온다. 각각의 자원들은 어떻게 효율적으로 사용할 수 있는지에 대해 생각해보면 다음과 같다.

 

- CPU : 누구한테 CPU를 넘겨줄까?(CPU Schduling)
- DISK : 디스크 파일을 어떻게 보관할지(파일관리)
- MEMORY : 한정되어 있는 메모리를 어떻게 나누어 사용할지(Memory 관리)
- I/O Device : 각각 다른 입출력장치와 컴퓨터 간의 어떻게 정보를 주고 받게 하지?
- Process : 프로세스의 생성과 삭제, 자원 할당 및 반환 , 프로세스 간 협력

 

01.  JUNGLE_PINTOS PROJECT1: THREDS 과제 가이드라인

https://casys-kaist.github.io/pintos-kaist/project1/introduction.html

 

Introduction · GitBook

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 A subset of the standard C library.

casys-kaist.github.io

 

(1) introduction 주요 내용

이 과제에서 최소의 기능만 하는 thread system을 갖고 시작하게 됩니다. 이 시스템의 기능을 확장하면서 synchronization의 문제들을 잘 이해하게 되는 것이 여러분이 할 일 입니다. 
In this assignment, we give you a minimally functional thread system. Your job is to extend the functionality of this system to gain a better understanding of synchronization problems. You will be working primarily in the  threads directory for this assignment, with some work in the devices directory on the side. Compilation should be done in the  threads directory. Before you read the description of this project, you should at least skim the material Synchronization.

 

 Pintos에는 이미 쓰레드 생성, 쓰레드 종료, 쓰레드 간에 스위치를 하는 간단한 스케쥴러, 그리고 동기화 함수(semaphores, locks, condition variables, and optimization barriers)가 구현되어있습니다.

 

The first step is to read and understand the code for the initial thread system. Pintos already implements thread creation and thread completion, a simple scheduler to switch between threads, and synchronization primitives (semaphores, locks, condition variables, and optimization barriers).

 

쓰레드가 생성될 때, 스케쥴링의 대상이 되는 새로운 문맥(context)이 생성됩니다. 만약 이 문맥에서 어떤 함수를 실행하고자 하는 경우, thread_create()의 인자로 실행하고자 하는 함수를 넣으면 됩니다. 쓰레드가 처음 스케쥴링되고 실행될 때, 쓰레드는 해당 문맥에서 함수의 맨 처음부터 실행합니다. 함수가 리턴될 때 쓰레드도 종료됩니다. 그러므로 각각의 쓰레드는 Pintos 내부에서 실행되는 미니 프로그램 같이 동작한다고 생각하면 됩니다. 마치 프로그램을 실행하면 main() 함수가 실행되는 것처럼, thread_create()가 실행되면 쓰레드에 전달된 함수가 실행됩니다.
When a thread is created, you are creating a new context to be scheduled. You provide a function to be run in this context as an argument to thread_create() The first time the thread is scheduled and runs, it starts from the beginning of that function and executes in that context. When the function returns, the thread terminates. Each thread, therefore, acts like a mini-program running inside Pintos, with the function passed to 
thread_create() acting like main().

 

  (2) Alarm Clock

이미 잘 작동하는 timer_sleep()이 구현되어있지만 이는 busy wait 방식입니다. 즉 이는 계속해서 반복문을 돌면서 현재 시간을 확인하고 충분한 시간이 경과할 때까지 thread_yield()를 호출합니다. busy waiting 을 피하도록 다시 구현합니다.
Although a working implementation is provided, it  busy waits, that is, it spins in a loop checking the current time and calling  thread_yield() until enough time has gone by. Reimplement it to avoid busy waiting.

 

 

 

 

02.  ALARM CLOCK ⏰

  (1) TIMER가 뭔가요?

컴퓨터 안에는 ‘timer’라는 하드웨어가 존재한다. 타이머 하드웨어는 일정 시간 단위(most tick)을 측정하고, 그 단위 마다 (타이머가) CPU에 timer inttrupt를 보내는 것을 발생시키도록 프로그램이 가능하다. 인터럽트가 발생하면 운영체제는 현재 수행중인 프로세스를 중단시키고 해당 인터럽트에 대한 인터럽트 핸들러를 실행한다.  타이머 인터럽트가 발생 할 때마다, 제어권이 운영체제로 넘어가기 때문에, 운영체제는 사용자 프로그램이 비정상적으로 작동하는 경우가 발생하더라도 언제든지 해당 프로그램을 적절히 처리할 수 있는 기회를 가질 수 있다. timer_init() 을 확인해보면 PINTOS는 8254 인텔사의 인터벌 타이머를 사용하고 있다는 것을 확인할 수 있다.

 

/* Sets up the 8254 Programmable Interval Timer (PIT) to
   interrupt PIT_FREQ times per second, and registers the
   corresponding interrupt. */
void
timer_init (void) {
	/* 8254 input frequency divided by TIMER_FREQ, rounded to
	   nearest. */
	uint16_t count = (1193180 + TIMER_FREQ / 2) / TIMER_FREQ;

	outb (0x43, 0x34);    /* CW: counter 0, LSB then MSB, mode 2, binary. */
	outb (0x40, count & 0xff);
	outb (0x40, count >> 8);

	intr_register_ext (0x20, timer_interrupt, "8254 Timer");
}

 

(8254 타이머에 대한 참조 블로그 : )

https://blog.naver.com/PostView.nhn?isHttpsRedirect=true&blogId=stl2k&logNo=46481372&parentCategoryNo=5&categoryNo=&viewDate=&isShowPopularPosts=false&from=postView)

 

8254 프로그래머블 인터벌 타이머(programmable interval timer) 사용법

어셈블러에서 타이머 사용할때 참고 8254 프로그래머블 인터벌 타이머(programmable interval timer) 사용...

blog.naver.com

 

  (2) TICK이 뭔가요?

여기서 tick이란 프로그램에서 시간을 세는 단위이고, PINTOS에서는 device/timer.h 에서 TIMER_FREQ의 값이 100으로 저장되어 있는데, 이 의미는 1초당 tick이 100번 돈다는 의미이다. 즉 1 tick은 100분의 1초(10ms)이다. thread_tick()을 확인해보면 TIME_SLICE(=4)가 지나면 intr_yield_on_return ();을 실행시켜준다.

 

/* Called by the timer interrupt handler at each timer tick.
   Thus, this function runs in an external interrupt context. */
void thread_tick (void) {
	struct thread *t = thread_current ();

	/* Update statistics. */
	if (t == idle_thread)
		idle_ticks++;
#ifdef USERPROG
	else if (t->pml4 != NULL)
		user_ticks++;
#endif
	else
		kernel_ticks++;

	/* Enforce preemption. */
	if (++thread_ticks >= TIME_SLICE)  // thread_ticks는 맨 처음 schedule()에서 0으로 만들어줌.
		intr_yield_on_return ();
}
/* During processing of an external interrupt, directs the
   interrupt handler to yield to a new process just before
   returning from the interrupt.  May not be called at any other
   time. */
void
intr_yield_on_return (void) {
	ASSERT (intr_context ());
	yield_on_return = true;
}

 

  (3) timer.c / timer_sleep(ticks) : 인자로 넣어준 ticks 동안 스레드를 잠자게 한다.

잠을 자야 하는 이유는 여러 가지가 있을 수 있다. Round Robin 방식에서 정해져 있는 time slice가 지나면 다른 스레드로 context switching해야 하는 경우가 있을 것이고, 아니면 다른 I/O의 실행을 기다리느라 block이 된 경우도 있을 것이다.

/* Sleeper thread. */
static void
sleeper (void *t_) 
{
  struct sleep_thread *t = t_;
  struct sleep_test *test = t->test;
  int i;

  for (i = 1; i <= test->iterations; i++) 
    {
      int64_t sleep_until = test->start + i * t->duration;
      timer_sleep (sleep_until - timer_ticks ());   
			// 인자로 넣어 준 tick 동안 여기서 움직이지 않는다.
      lock_acquire (&test->output_lock);
      *test->output_pos++ = t->id;
      lock_release (&test->output_lock);
    }
}

 

 

  (4) 현재 상황 : Busy waiting 방식으로 구현

busy waiting 방식으로 구현

 

busy waiting은 일어날 시간이 아님에도 불구하고 매번 ready list를 돌면서 CPU를 사용해야할 타이밍이 되었을 때마다 다른 스레드로 yiled한다. 이러한 것을 timer_sleep()에서 찾을 수 있다. timer_sleep()는 현재 스레드를 thicks시간 동안 잠재우는 함수이다. 이 함수가 호출된 thread는 현재 while문 내부에 집입하여 스케줄링에 의해 자신의 차례가 올 때마다 코드가 다시 실행되고 timer_elapsed(start) 함수를 호출한다. (timer_elapsed(start) 함수는 timer_sleep이 호출된 시점부터 몇 tick이 지났는지 반환하는 함수이다.)  timer_sleep의 인자인 ticks보다 작으면 thread_yield()를 호출하여 ready list에 있는 다른 스레드를 위해 CPU 점유를 반환하고 ready list 가장 뒤로 이동한다. 이 과정은 ticks동안 반복된다.

 

void
timer_sleep (int64_t ticks) {
	int64_t start = timer_ticks ();
	while (timer_elapsed (start) < ticks)
    	//현재 CPU를 점유한 스레드를 다른 스레드에게 양보하고 ready_list 제일 뒤로 이동
		thread_yield ();
}

 

  (5) busy waits를  Block/wakeup 방식으로 바꿔주자

     1) sleep/awake 방식 

이제는 아직 깰 시간이 안된 친구들은 sleep_list를 만들어 푹 자고 일어날 수 있게 만들었다.

재우고 -> 일어나!⏰

     2) struct  thread 개선

struct thread {
	/* Owned by thread.c. */
	tid_t tid;                          /* Thread identifier. */
	enum thread_status status;          /* Thread state. */
	char name[16];                      /* Name (for debugging purposes). */
	int priority;                       /* Priority. */

...

	/* wakeup tick : 깨어나야 할 tick(시각) */
	int64_t wakeup_tick;

...

}

 

     3) sleep_list 생성 및 초기화

/* sleep 상태의 스레드들을 저장하는 리스트 */
static struct list sleep_list;
static int64_t next_tick_to_awake; /* ready list에서 맨 처음으로 awake할 스레드의 tick 값 */
void thread_init (void) {

...
/* Init the global thread context */
	lock_init (&tid_lock);
	list_init (&ready_list);
	list_init (&destruction_req);
	list_init (&sleep_list);     // sleep 스레드들을 연결해놓은 리스트를 초기화한다.
	next_tick_to_awake = INT64_MAX;

}

 

  4)  next_tick_to_awake를 관리할 함수

  매 시간마다 깨울 스레드를 찾으면 비효율적이니, 자는 스레드들 중에서 가장 빨리 일어나야할 스레드의 시간을 구하는 함수를 추가했다.

 

/* 가장 먼저 일어나야할 스레드가 일어날 시간을 반환 */
void update_next_tick_to_awake(int64_t ticks){
	/* next_tick_to_awake가 깨워야 할 스레드의 깨어날 tick값 중 가장 작은 tick을 갖도록 업데이트 */
	next_tick_to_awake = (next_tick_to_awake>ticks) ? ticks : next_tick_to_awake;
}

/* 가장 먼저 일어나야할 스레드가 일어날 시간을 반환함 */
int64_t get_next_tick_to_awake(void) {
	return next_tick_to_awake;
}

 

   5)  thread를 재우는 함수 thread_sleep() 구현

thread를 재워야하는데,  중간에 다른 명령들을 무시하고 온전히 실행할 수 있어야 하기 때문에 처음에 intr_disable()함수를 통해인터럽트를 받아들이지 않고 intr_set_level(old_level) 함수를 통해 인터럽트를 받아들이도록 구현했다.

 

void thread_sleep(int64_t ticks){
	/* 현재 실행되고 있는 스레드에 대한 작업이므로. */
	struct thread* cur = thread_current();

	/* 인터럽트 disable*/
	enum intr_level old_level;
	ASSERT(!intr_context());
	old_level = intr_disable(); // 스레드를 list에 추가해주는 일은 인터럽트가 걸리면 안 된다.	

	ASSERT(cur != idle_thread);  // idle thread라면 종료.

	cur->wakeup_tick = ticks;						// wakeup_tick 업데이트
	update_next_tick_to_awake(cur->wakeup_tick); 	// next_tick_to_awake 업데이트
	list_push_back (&sleep_list, &cur->elem);		// sleep_list에 추가

	/* 스레드를 sleep 시킨다. */
	thread_block();

	/* 인터럽트 원복 */
	intr_set_level(old_level);
}

 

   1) idel_thread란?

idle 스레드란 운영체제가 초기화되고, ready_list가 생성되는데, 이때 첫 ready_list에 추가되는 스레드이다. 해당 스레드는 CPU가 실행상태를 유지하기 하기 위해 공회전 되는 thread역할을 한다.  그럼 idel_thread는 왜 사용할까? cpu가 할일이 없어 꺼졌다가 다시 켜지는 방식보다 의미 없는 thread를 공회전 하는 것이 더 효율적이기 때문에 사용한다.
(출처 : https://bowbowbow.tistory.com/20)

 

 

   6) 스레드를 깨우는 함수 

/* pintos/src/thread/thread.c */
//푹 자고 있는 스레드 중에 깨어날 시각이 ticks시각이 지난 애들을 모조리 깨우는 함수
void thread_awake(int64_t wakeup_tick){
  next_tick_to_awake = INT64_MAX;
  struct list_elem *e;
  e = list_begin(&sleep_list);
  while(e != list_end(&sleep_list)){
    struct thread * t = list_entry(e, struct thread, elem);

    if(wakeup_tick >= t->wakeup_tick){
      e = list_remove(&t->elem);
      thread_unblock(t);
    }else{
      e = list_next(e);
      update_next_tick_to_awake(t->wakeup_tick);
    }
  }
}

 

  7) timer_sleep()과 timer_interrupt() 수정

/* pintos/src/device/timer.c */
void timer_sleep (int64_t ticks) 
{
  int64_t start = timer_ticks ();

  ASSERT (intr_get_level () == INTR_ON);

  /* 
  기존의 busy waiting을 유발하는 코드를 삭제하고
  새로 구현한 thread를 sleep list에 삽입하는 함수 호출함
  */
  thread_sleep(start + ticks);
}
/* pintos/src/device/timer.c */
static void timer_interrupt (struct intr_frame *args){
   ...
  /* 매 tick마다 sleep queue에서 깨어날 thread가 있는지 확인하여,
  깨우는 함수를 호출하도록 함. */
  if(get_next_tick_to_awake() <= ticks){
    thread_awake(ticks);
  }
}

 

 

 

03.  JUNGLE_PINTOS PROJECT1: THREDS 하면서 알게된 것

 (1) Process

일반적으로 프로세스는 실행 중인 프로그램으로 정의한다. 또한 Process란 현재 실행 중인 프로그램이며 하나의 virtual address space를 가지는 실행 단위를 의미한다. thread는 하나의 process에 속해 있는 더 세부적인 실행 단위를 의미한다. 실제 OS에서는 하나의 process안에 여러 개의 thread가 존재할 수 있으며, 이 thread들은 같은 virtual address space를 공유한다. 

 

우리가 컴퓨터를 사용하면서 유튜브로 영상도 보고, 코드도 작성하고, 블로그도 작성하면서 동시에 일을 진행한다. 하지만 이것은 동시에 진행하는 것 처럼 보이는 것 뿐, 컴퓨터는 하나의 프로세스를 진행하고 얼마 후 중단시키고 다른 프로세스를 실행하는 작업을 반복하면서  실제 하나 또는 소수의 CPU로 여러 개의 가상 CPU가 존재하는 듯한 환상을 보여준다. 시분할(time sharing)이라고 불리는 이 기법은 원하는 수 만큼의 프로세스를 동시에 실행할 수 있게 한다. 시분할 기법은 CPU를 공유하기 때문에 각 프로세스의 성능은 낮아진다.

"시분할/공간분할"
시분할은 자원 공유를 운영체제가 사용하는 가장 기본 기법 중 하나이다. 한 개체가 잠깐 자원을 사용할 후, 다른 개체가 또 잠깐 자원을 사용하고, 그 다음 개체가 사용하면서 이 자원을 많은 개체들이 공유한다.

시분할과 자연스럽게 대응되는 개념은 공간 분할(space sharing)이 있다. 공간 분할은 개체에게 공간을 분할 해 준다. 공간 분할의 예로 디스크를 들 수 있다. 디스크는 자연스럽게 공간을 분할할 수 있는 자원으로, 블럭이 하나의 파일에 할당되면 파일을 삭제하기 전에는 다른 파일이 할당될 가능성이 낮다.

https://www.youtube.com/watch?v=iks_Xb9DtTM&ab_channel=%EC%96%84%ED%8C%8D%ED%95%9C%EC%BD%94%EB%94%A9%EC%82%AC%EC%A0%84 

 

 (2) Threads

스레드란 단어로는 실을 뜻하는 영단어이며, CS에서는 프로세스 내에서 실제로 작업을 수행하는 주체를 의미한다. 모든 프로세스에는 한 개이상의 스레드가 존재하여 작업을 수행한다. 또한, 두 개 이상의 스레드를 가지는 프로세스를 멀티스레드 프로세스라고 한다.(출처: TCP school)  프로그램에서 한 순간 하나의 명령어만 실행하는 고전적인 관점을 벗어나 멀티 스레드 프로그램은 하나 이상의 실행 지점을 가지고 있다.( 독립적으로 불러들여지고, 실행 될 수  있는 여러개의 PC 값)을 가지고 있다. 멀티 스레드를 이해하는 방법은 각 스레드가 프로세스와 매우 유사하지만, 차이가 있다면 쓰레드들은 주소 공간을 공유하기 때문에 동일한 값에 접근할 수 있다는 것이다. 

 

 하나의 스레드 상태는 프로세스의 상태와 매우 유사하다. 스레드는 어디서 명령어를 불러들일지 추적하는 프로그램 카운터(PC)와 연산을 위한 레지스터들을 가지고 있다. 만약 두 개의 스레드가 하나의 프로세서에서 실행 중이라면 실행하고자 하는 스레드는 반드시 문맥 교환을 통해서 실행 중인 스레드와 교체되어야 한다.

 

스레드를 왜 사용할까? 첫째, 병렬 처리이다. 예를 들어 두 개의 큰 배열을 더하거나 배열의 각 원소의 값을 증가시키는 것과 같이 매우 큰 배열을 대상으로 연산을 수행하는 프로그램을 작성하고 있다고 가정해보자 단일 프로세서에서 실행하는 경우 작업은 간단하지만 멀티로 실행하는 경우 각 프로세서가 작업의 일부분의 수행하게 함으로써 실행속도를 매우 높일 수 있다. 단일 스레드 프로그램을 멀티프로세서 상에서 같은 작업을 하는 프로그램으로 변환하는 작업을 병렬화라고 부르며, CPU마다 하나의 쓰레드를 사용하여 주어진 일을 하는 것이 최신 하드웨어상에서 프로그램을 더 빠르게 실행하게 만드는 자연스럽고 전형적인 방법이다.  두번째는 느린 I/O로 인해 프로그램 실행이 멈추지 않도록 하기 위해 스레드를 사용한다. 

 

 (3)  thread context  swich

   T1(Thread 1)이 사용하던 레지스터들을 저장하고, T2(Thread 2)가 사용하던 레지스터의 내용으로 복원한다는 점에서 프로세스의 문맥교환과 유사하다. 프로세스가 문맥 교환을 할 때 프로세스의 상태를 프로세스 제어 블럭(process control block)에 저장하듯이 프로세스의 스레드들의 상태를 저장하기 위해서는 하나 또는 그 이상의 스레드 제어 블럭(thread control block)이 필요하다. 가장 큰 차이 중 하나는 프로세스의 경우와 달리 스레드 간의 문맥 교환에서는 주소 공간을 그대로 사용한다는 것이다.(사용하고 있던 페이지 테이블을 그대로 사용하면 된다.)

 

 (4) 타이머 인터럽트란?

  • 프로세스가 비 협조적인 상황에서도 CPU의 할당을 위한 제어권을 어떻게 하면 할당 받을 수 있을까?
  • 어떻게 하면 악의적인 프로세스가 컴퓨터를 장악하는 것을 막을 수 있을까?

  타이머 인터럽트(timer intterrupt)를 사용하면 된다. 타이머 인터럽트 기능을 사용하면 프로세스가 비 협조적으로 행동하는 상황에서도 운영체제가 실행될 수 있다. 타이머 인터럽트는 운영체제가 컴퓨터를 제어하는데 있어 근간이 되는 핵심 기능이다.

 

 (5) 인터럽트의 비활성화를 직접적으로 사용해만 하는 경우는?

  1. 우선 Response time을 낮추기 때문에 interrupts를 아예 꺼버리는 방법은 가능한 피해야 한다. 따라서 thread와 thread 사이의 race condition 방지를 위해서는 보통 semaphore나 lock 등과 같은 synch primitives를 사용해야 한다.
  2. 참고로 synch primitives의 내부 구현을 보면 사실 interrupts를 disable 해주는 작업이 들어간다. interrupts를 직접적으로 끄는 게 아니라 semaphore나 lock이라는 기법으로 감싸서 interrupts를 간접적으로 끄는 방법을 사용하는 이유는 interrupts를 끄는 작업은 매우 low-level 작업이어서 안전장치가 없기 때문이다. 예를 들어, 코드 어딘가에서 interrupts를 껐다가 실수로 다시 키는 걸 깜빡하면 response time이 크게 증가하는데, 컴파일 에러 혹은 런타임 에러가 발생하는 게 아니라서 수많은 코드 중 어디에서 interrupts가 꺼졌는지 코드를 다 뒤져보지 않는 이상 알 수 있는 방법이 없다. synch primitives의 경우 interrupts를 변동해줬다가 다시 최초 상태로 돌아가게 하는 것을 보장하기 때문에 보다 안전하다.
  3. 그런데, thread(특히 kernel thread)와 외부 하드웨어 모듈에 의해 발생하는 external interrupt handler 간의 race condition 방지에는 synch primitives를 사용할 수 없다. 왜냐하면 synch primitives의 경우 한 thread를 sleep(혹은 block)하게 함으로써 다른 thread와 race condition 들어가는 것을 방지하는데, external interrupt handler의 경우 interrupt가 발생하면 무조건 작동돼야 하기 때문에 sleep/block 상태에 들어가는 것이 불가능하다. 따라서 interrupt 자체를 꺼놔서 external interrupt handler가 절대 동작이 될 수 없게 함으로써 synchronization을 달성해야 한다.

 

(6) 병행성 관련 용어

  • 임계영역(ciritical section)  : 보통 변수나 자료구조와 같은 공유자원을 접근하는 코드의 일부분을 말한다.
  • 경쟁조건(race condition):  혹은 데이터 경쟁은 멀티 스레드가 거의 동시에 임계영역을 실행하려고 할때 발생하며, 공유 자료 구조를 모두가 갱신하려고 시도한다면 의도하지 않은 결과를 만든다. 
  • 비결정적(indeterminate) : 하나 또는 그 이상의 경쟁 조건을 포함하여 그 실행 결과가 각 스레드가 실행된 시점에 의존하기 때문에, 프로그램의 결과가 실행할 때마다 다르다. 
  • 스레드는 상호 배제(mutual exclusion)라는 기법을 사용해 하나의 스레드만이 임계영역에 진입할 수 있도록 보장한다.

 

(7) Critical Section : 임계영역

멀티 스레드가 같은 코드를 실행할 때 경쟁 조건이 발생하기 때문에 이런 코드 부분을 임계영역(Critical section)이라고 부른다. 이 속성은 하나의 스레드가 임계 영역 내의 코드를 실행 중일 때는 다른 쓰레드가 실행할 수 없도록 보장해준다. 

 

 

 (8) LOCKS

공유자원들은 너도 나도 사용할 수 있어 내가 원하는 결과가 나오지 않을 가능성이 매우 높다. 이러한 것을 예방해주는 것이 바로 lock이다.  lock은 소스 코드들의 임계영역 즉, 공유자원을 락으로 둘러서 공유자원이 마치 하나의 원자 단위 명령어인 것처럼 실행 되도록 한다.

 

  1) synch.h

/* Lock. */
struct lock {
	struct thread *holder;      /* Thread holding lock (for debugging). */
	struct semaphore semaphore; /* Binary semaphore controlling access. */
};

void lock_init (struct lock *);
void lock_acquire (struct lock *);
bool lock_try_acquire (struct lock *);
void lock_release (struct lock *);
bool lock_held_by_current_thread (const struct lock *);

 

  2) synch.c

void
lock_acquire (struct lock *lock) {
	ASSERT (lock != NULL);
	ASSERT (!intr_context ());
	ASSERT (!lock_held_by_current_thread (lock));

	sema_down (&lock->semaphore);
	lock->holder = thread_current ();
}

void
lock_release (struct lock *lock) {
	ASSERT (lock != NULL);
	ASSERT (lock_held_by_current_thread (lock));

	lock->holder = NULL;
	sema_up (&lock->semaphore);
}

 

반응형

댓글