본문 바로가기

오퍼레이팅 시스템

오퍼레이팅 시스템 : Synchronization 1

0. Review

  • Thread
    • 프로세스에서 자신만의 스택이 있음
    • 다른 스레드와 주소 공간을 공유
    • LWP(Light Weight Process)라고도 불림

1. Race Condition

1) Race Condition

  • 공유 자원에 액세스하는 스레드의 결과가 다음과 같은 경우가 존재
    • Non-deterministic → Incorrect error
    • Non-reproducible(재생 불가능)
  • 실행 시기에 따라 다름
    1. Multithreading : CPU 스케줄러가 실행을 "interleave" 할 수 있음
      • 예측할 수 없는 요인
    2. Multiprocessor : 멀티 프로세서에서 실행되는 타이밍은 다를 수 잇음
      • "프로세서 사용량"에 의존
  • 스레드 관련 문제
    • atmoic operation에서 공유 리소스에 액세스하기 어려움
      • Atmoic operation : "전체 또는 전혀" Non-interruptible operation
    • 단일 연산자를 여러 명령으로 컴파일할 수 있음
      • 예) ++연산자는 Load/Add/Stroe의 세 단계를 포함
    • multi-word operation이 atomic이라는 보장은 없음

2) Example

(1) Uni-processor

  • 두 개의 스레드 : A, B
    • 전역 변수 : counter (0x8049a1c)
    • Scenario : 두 스레드는 같은 코드를 실행

(2) Multi-processor

  • 두 개의 스레드 : A, B
    • Scenario : 두 스레드는 멀티 프로세서에서 존재
    • 하나의 가능한 결과 : 하나의 업데이트가 손실됨(이전 예시와 유사)
      • 단일 프로세서 또는 다중 프로세서에 관계없이 race condition이 발생할 수 있음

2. Critical Section

1) Critical Resource

  • 각 스레드에는 자체 스택과 자체 CPU 레지스터 복사본이 있음
  • Resource Sharing
    • static data, heap memory, file, IO 장치와 같은 리소스는 프로세스의 모든 스레드에서 공유됨
    • critical resource에 속하는 하드웨어에는 프린터와 tape dirve가 포함되며 소프트웨어에는 message buffer queues, variables, arrays, buffer가 포함됨
  • Critical resource
    • 한 번에 최대 하나의 스레드에서만 사용할 수 잇는 리소스
    • 예) 프린터

2) Critical Section & Mutual Exclusion

  • Critical Section(또는 Critical Region)
    • critical resource에 액세스하는 코드
    • shared variable 또는 data structure를 검사
      • 스레드가 동시에 액세스할 수 있는지 여부
      • "모든 스레드가 읽기만 하는 코드에서는 race condition이 없음"
  • Mutual Exclusion
    • critical section에서 한 번에 하나의 스레드만 실행되도록 함
    • critical sectoin을 mutally exclusive(상호 배타적)으로 만듦

3) Synchronization Terminologies

  • Process/Thread Synchronization
    • 정확성을 위해 공유 리소스를 사용하거나 프로세스/스레드 실행을 조정하는 방법
    • critical section 내에서 mutual exclusion 기능을 제공하여 race condition을 방지하거나 정확한 실행을 보장하는 메커니즘!!!
  • Race condition
    • 여러 프로세스/스레드가 동시에 공유 데이터에 액세스하고 이를 조작하는 상황
  • Critical Section
    • 공유 리소스에 액세스하는 코드
  • Mutual Exclusion
    • 동시에 하나의 프로세스/스레드만 critical section에 포함되도록 보장해야 하는 요구 사항
  •  Condition variable(다음 정리에서 구체적으로)
    • 프로세스가 특정 작업이 발생할 때까지 대기하도록 허용

3. Mutual Exclusion

1) Mutual Exclusion

  • 모든 프로세스가 동일한 리소스 Ra에 액세스하므로 한 번에 하나의 프로세스만 critical section에 포함되는 것이 좋음
  • Mutual exclusion을 시행하기 위해 entercritical exitcritical 라는 두 가지 함수를 제공
  • 다른 프로세스가 critical section에 있는 동안 동일한 리소스에 대해 critical section으로 들어가려고 하는 모든 프로세스는 wait 상태가 됨

(1) 예시

  • 데이터 a와 b의 두 항목이 a = b로 유지된다고 가정
    • 즉, 한 값을 업데이트하는 모든 프로그램은 관계를 유지하기 위해 다른 값도 업데이트해야 함
  • 이제 두 프로세스가 각 개별 데이터 항목(a 및 b)에 대한 mutual exclusion을 존중하는 다음과 같은 동시 실행 시퀀스를 고려

  • 각 프로세스의 전체 시퀀스를 critical section으로 선언하여 문제를 피할 수 있음
    • 우리는 공유에 의한 작업의 경우 critical section의 개념이 중요하다는 것을 알 수 있음!!!

2) Critical Section Problem

  • 다음 세 가지 조건을 충족해야함
    1. Mutual Exclusion(상호배제)
      1. 프로세스 Pi가 critical section에서 실행 중이면 다른 프로세스가 critical section에서 실행될 수 없음
    2. Progress(진행)
      1. critical section에서 실행 중인 프로세스가 없고 critical section에 진입하려는 프로세스가 있는 경우, 다음에 critical section에 진입할 프로세스의 선택을 연기할 수 없음
    3. Bounded Waiting(한정 대기)
      1. 프로세스가 critical section에 들어가는 요청을 한 후 다른 프로세스가 critical section에 들어갈 수 있는 횟수에 대한 bound 또는 limit가 존재

4. SW approach - Peterson

1) Naive approach(Incorrect)

  •  Naive solution은 lock의 소유 여부를 추적하기 위한 단순 플래그만 구현할 수 있음

2) SW approach(Incorrect)

  • Synchronization variable
    • Boolean flag[2] = {false, false};
  • mutual exclusion을 충족하지만, progress 요구사항은 만족하지 않는다

3) SW approach - Peterson's

  • 전역 변수 전환을 통해 동시 충돌 해결
    • Mutual exclusion가 보존됨
  • flag[0] 및 flag[1] 모두 참일 수 있지만 turn은 0 또는 1만 될 수 있음
    • Mutual blocking 방지
  • P1이 critical section에 있으며 P0이 차단되고 P0이 loop(flag[1] is true and turn  = 1)하는 동안 차단된다.

  • flag[0] 과 flag[1]은 모두 true가 될 수 있지만 turn은 0 또는 1로만 될 수 있다.
    • 아래와 같은 알고리즘은 현대 하드웨어에서 작동하지 않음
    • 사람들은 약간의 하드웨어 지원을 가정하는 것이 훨씬 쉽다는 것을 깨달았음

5. Disabling Interrupt and HW support

1) Disabling Interrupts

  • 초기 솔루션 중 하나는 중요한 섹션에 대한 인터럽트를 비활성화하는 것이었음
    • 단일 프로세서 시스템에서 프로세스는 OS 서비스를 호출하거나 중단 interrupt(중단)될 때까지 계속 실행됨
    • 따라서 Mutual exclusion을 보장하기 위해서는 프로세스가 중단되는 것을 방지하기에 충분함
    • 이 접근 방식의 주요 장점은 simplicity(단순성)이다.
    • 불행하게도 부정적인 것들이 많음
      1. 인터럽트를 끄면 인터럽트가 손실될 수 있음
      2. 이러한 권한 있는 작업은 일부 malicious program(악성 프로그램)에 의해 악용될 수 있음
      3. 이 접근 방식은 멀티프로세서에서 적절하지 않을 수 있다.

2) Special Machine Instructions

  • 인터럽트를 비활성하는 것은 여러 프로세서에서 작동하지 않기 때문에 시스템 설계자들은 locking을 위한 하드웨어 지원을 발명하기 시작
  • HW support
    • 일부 시스템은 lock mechanism을 구현하기 위한 추가 명령어를 제공
  • Compare and Swap instruction(CAS)
    • 메모리 위치의 내용을 지정된 값과 비교하고 동일한 경우에만 해당 메모리 위치의 내용을 새 지정된 값으로 수정

6. Implementations of Spin Lock

1)  A Simple Spin Lock Using CAS

  • 또한 이러한 유형의 lock을 일반적으로 spin lock이라고 하는 이유를 이해할 수 있음
  • spin lock은 CPU 사이클을 사용하여 간단히 회전하는 가장 간단한 유형의 lock이다. 이것은 preemptive scheduler가 필

parbegin(P1, P2, ..., Pn)은 다음을 의미한다.
→ main program의 실행을 중지한다.
→ P1, P2, ..., Pn의 동시 실행을 시작한다.
→ P1, P2, ..., Pn이 모두 종료되면 main program을 재개한다.

2) Spin Locks

  • busy waiting 또는 spin waiting이라는 용어는 프로세스가 중요한 섹션에 들어갈 수 있는 허가를 얻을 때까지 아무것도 할 수 없는 기술을 말한다.
  • Mutual exclusion을 제공합니까? (mutual exclusion)
    • 이는 간단하므로 확인이 쉽다
    • spin lock 기능은 한 번에 하나의 스레드만 critical section에 들어갈 수 있다.
    • 단일 프로세서 또는 메인 메모리를 공유하는 여러 프로세서의 모든 프로세스에 적용 가능
  • spin lock을 사용하는 데 드는 비용은 얼마입니까? (performance)
    • busy waiting가 사용된다.
    • 단일 프로세서 : lock을 유지하는 스레드를 제외한 모든 스레드는 spinning하는 동안 CPU를 낭비해야 함
    • 멀티 프로세서 : lock을 대기하기 위해 spinning하는 것은 많은 사이클을 낭비하지 않는다.
  • waiting 스레드에 대한 spin lock은 얼마나 공정합니까? (fairness)
    • waiting process 선택이 임의이다.
    • Spin lock은 fairness를 보장하지 않으며 starvation으로 이어질 수 있다.

7. Binary Semaphore

1) Semaphore

  • 동시 시스템에서 액세스를 제어하는 보다 일반화된 동기화 방법
    1. Semaphore S
      • 정수 값을 가진 object(객체)
        • 값은 현재 사용 가능한 리소스의 단위 수이다.
      • 세마포어 개념은 1965년 Dijkstra에 의해 발명되었음
    2. S를 수정하는 두 가지 표준 작업 : semWait()semSignal()
      • semWait : 값을 줄임
      • semSignal : 값을 증가시킴

원래 이름은  P() V()이다.
P는 proberen("try")를 나타냄
V는 verhogen("increase")를 나타냄
P semWait (--) lock
V semSignal (++) unlock

2) Semaphore 종류

  1. busy waiting이 없는 구현
    • busy waiting을 피하기 위해 Semaphore는 semaphore에서 waiting 중인 프로세스의 관련 queue를 사용할 수 있다.
    • semaphore는 n-process critical section 문제를 처리하는 데 사용됨
  2. Binary semaphore
    • lock으로 사용할 정수 값은 0 1사이에서만 사용할 수 있음
      • 0 및 1값으로 제한됨(또는 locked/unlocked, unavailable/available)
    • mutex lock이라고도 함
  3. Counting semaphore
    • 정수 값은 제한되지 않은 domain에 걸쳐 있을 수 있음
    • general semaphore라고도 함

3) Binary Semaphore

처음에 value값을 1로 초기화

8. Counting Semaphore

1) 예시

가장 오랫동안 차단된 프로세스가 queue에서 먼저 해제되는 강력한 세마포어(FIFO)

9. Implementations of Semaphores

1) Implementations of Semaphores

2) Deadlocks

  • waiting queue이 있는 semaphore를 구현하면 deadlock가 발생할 수 있다.
    • 두 개 이상의 프로세스가 각각 다른 프로세스 중 하나가 작업을 수행하기를 기다리고 잇어 진행할 수 없는 경우
  • S와 Q가 1로 초기화된 두 개의 semaphore라고 함

3) Linux Approach : Two-Phase Locks

  • 리눅스 접근 방식
    • 이제 이를 two-phase lock이라고 함
    • 첫번째 단계에서는 lock을 획득할 수 있기를 바라며 잠시 동안 lock이 spin한다.
      • Spinning은 특히 lock 해제될 때 유용할 수 있음
    • 두번째 단계에서는 caller가 sleep 모드로 전환되고 나중에 lock이 해제될 때만 sleep에서 해제됨
  • 일반 버전의 Linux 접근 방식은 sleep을 위해 futext support를 사용하기 전에 고정된 시간 동안 루프에서 spin할 수 있다.

4) Pthreads API : Mutual Exclusion

  • Pthreads API는 critical section에 mutual exclusion 기능을 제공하기 위해 다음과 같은 기능을 제공
#include <pthread.h>
int pthread_mutex_init( pthread_mutext_t *mutex, pthread_mutexattr_t *attr );
int ptrhead_mutex_destroy( pthread_mutex_t *mutex );

int pthread_mutex_lock(pthread_mutex_t *mutex );
int pthread_mutex_trylock(pthread_mutex_t *mutex );
int pthread_mutex_timedlock(pthread_mutex_t *mutex, struct timespec *abs_timeout );
int pthread_mutex_unlock(pthread_mutex_t *mutex );

(1) 예시

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define ITER 1000
void *thread_increment(void *arg);
void *thread_decrement(void *arg);
int x;
pthread_mutex_t m;
int main() {
	pthread_t tid1, tid2;
	pthread_mutex_init(&m, NULL);
	pthread_create(&tid1, NULL, thread_increment, NULL);
	pthread_create(&tid2, NULL, thread_decrement, NULL);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	if (x != 0)
		printf("BOOM! counter=%d\n", x);
	else
		printf("OK counter=%d\n", x);
	pthread_mutex_destroy(&m);
}
    
void * thread_increment (void *arg) {
	int i, val;
	for (i=0; i< ITER ; i++) {
		pthread_mutex_lock(&m);
		val = x;
		printf("%u:%d\n",(unsigned int) pthread_self(), val);
		x = val + 1;
		pthread_mutex_unlock(&m);
	}
	pthread_exit(NULL);
}

void * thread_decrement (void *arg) {
	int i, val;
	for (i=0; i< ITER ; i++) {
		pthread_mutex_lock(&m);
		val = x;
		printf("%u: %d\n", (unsigned int) pthread_self(), val);
		x = val - 1;
		pthread_mutex_unlock(&m);
	}
	pthread_exit(NULL);
}

5) POSIX Semaphores API : Semaphore

  • The POSIX semaphores
#include <semaphore.h>
int sem_init(sem_t *s, 0, unsigned int val);} /* s = val */

int sem_wait(sem_t *s); /* P(s) */
int sem_post(sem_t *s); /* V(s) */

(1) 예시

#include <pthread.h>
#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define ITER 1000
void *thread_increment(void *arg);
void *thread_decrement(void *arg);
int x;
sem_t s;
int main() {
	pthread_t tid1, tid2;
	sem_init(&s, 0, 1);
	pthread_create(&tid1, NULL, thread_increment, NULL);
	pthread_create(&tid2, NULL, thread_decrement, NULL);
	pthread_join(tid1, NULL);
	pthread_join(tid2, NULL);
	if (x != 0)
		printf("BOOM! counter=%d\n", x);
	else
		printf("OK counter=%d\n", x);
	sem_destroy (&s);
}

void * thread_increment (void *arg) {
	int i, val;
	for (i=0; i< ITER ; i++) {
		sem_wait(&s);
		val = x;
		printf("%u:%d\n",(unsigned int) pthread_self(), val);
		x = val + 1;
		sem_post(&s);
	}
	pthread_exit(NULL);
}

void * thread_decrement (void *arg) {
	int i, val;
	for (i=0; i< ITER ; i++) {
		sem_wait(&s);
		val = x;
		printf("%u: %d\n", (unsigned int) pthread_self(), val);
		x = val - 1;
		sem_post(&s);
	}
	pthread_exit(NULL);
}