Software Courses/Operating System

Process Synchronization and Mutual Exclusion

김 정 환 2020. 12. 2. 16:59
반응형

 

다중 프로그래밍 시스템

    - 여러 개의 프로세스 존재

    - 프로세스들은 서로 독립적으로 동작

    - 공유 자원 또는 데이터가 있을 때, 문제 발생

 

 

 

동기화

    - 프로세스들이 서로 동작을 맞추는 것

    - 프로세스들이 서로 정보를 공유하는 것

 

 

 

용어 표현

    - Shared data(공유 데이터) : 여러 프로세스들이 공유하는 데이터

    - Critical section(임계 영역) : 공유 데이터를 접근하는 코드 영역

    - Mutual exclusion(상호 배제) : 둘 이상의 프로세스가 동시에 임계 영역에 진입을 막는 것

 

 

 

Critical section에서의 문제

 

 

 

 

과정

1. 공유 데이터를 Register에 저장

2. 저장된 데이터에 1 더하기

3. 저장된 값을 공유 데이터에 저장

    - 문제 : 일관된 답을 주지 않음

 

 

 

 

상호 배제 구현을 통한 문제 해결 시도

 

    - 구현

        * enterCS() : 다른 프로세스가 임계 영역 안에 있는지 검사

        * exitCS() : 임계 영역을 벗어나면 시스템에게 알림

 

    - 조건

        * 상호 배제 : 임계 영역에 프로세스가 있으면, 다른 프로세스 진입 금지

        * Progress(진행) : 임계 영역 안에 있는 프로세스만 다른 프로세스의 임계 영역 진입을 막음

        * Bounded waiting(한정대기) : 프로세스의 임계 영역 진입은 유한시간 내에 허용

 

 

1. Version 1

    - 문제 : Progress 조건 위배

        * P1이 임계 영역에 진입 하지 않을 경우, P2는 진입 못함

        * 같은 프로세스는 두 번 연속 임계 영역 진입 불가능

 

 

2. Version 2

    - 문제 : 상호 배제 조건 위배

        * P1 실행 도중에 선점되어 멈추면 P2가 임계 영역에 진입 -> P1가 다시 CPU 할당 받아 임계 영역 진입

 

 

3. Version 3

    - 문제 : 진행, 유한 대기 조건 위배

        * P1이 Flag[1]을 true로 만들고 선점됨 -> P2가 Flag[2]를 true로 만들고 대기 -> P1가 돌아왔는데, Flag[2]가 true라서 대기 -> P1과 P2 무한 대기

 

 

상호 배제 해결을 위한 SW 솔루션

1. 두 개의 프로세스에 대한 상호 배제 알고리즘

    - Dekker's Algorithm

 

    - Peterson's Algorithm

        * Dekker's Algorith보다 간단하게 구현

 

 

2. N개의 프로세스에 대한 상호배제 알고리즘

    - Dijkstra's Algorithm

 

Flag[] 변수의 의미

        * 문제

            + 속도가 느림 : in-CS가 빈번하면, repeat ~ until 부분을 많이 돌아야 함

            + 구현이 복잡

            + busy waiting : repeat ~ until 하면서 대기, 1단계에서 while(turn != i) do 하면서 대기

 

 

 

 

상호 배제를 해결하기 위한 HW 솔루션

1. 두 개의 프로세스에 대한 상호 배제 알고리즘

    - TestAndSet(TAS) Instruction (기계어) 사용

        * 실행 중 interrupt을 받지 않기 때문에 Test와 Set을 한 번에 수행

 

    - 문제 : 3개 이상의 프로세스인 경우, bounded waiting 조건 위배

        * 상황 : P1은 임계 영역. P2와 P3가 대기하다가 P3가 임계 영역에 진입. P4가 와서 P2와 진입하다가 P4가 진입하고 P2는 다시 대기. 이렇게 하나의 프로세스가 무한 대기를 할 수도 있음

 

 

2. N 개의 프로세스에 대한 상호 배제 알고리즘

    - 장점

        * 구현이 간단

 

    - 단점

        * busy waiting

 

 

 

상호 배제를 해결하기 위한 OS 솔루션

1. Spinlock

    - 정수 변수

    - 초기화 연산, P() 연산, V() 연산으로만 접근 가능 

    - OS가 원자성을 보장하기 때문에 P()와 V() 실행 중에 선점 당하지 않음

 

    의미 : P()는 문 잠그기, V()는 문 열기

    - 문제

        * 멀티 프로세서 시스템에서만 사용 가능 : Pi가 임계 영역에서 실행 중이다가 멈춰서 CPU를 반환 -> Pj가 CPU를 할당 받아서 P() 실행하면서 대기 -> Pi는 다시 CPU를 할당 받을 수 없음(원자성으로 선점 불가능) -> active = 0인 상태 -> Pj는 무한대기, Pi는 CPU 못 받음 -> 따라서 CPU가 여러 개 있는 시스템에서만 사용 가능

        * busy waiting

 

 

2. Semaphore

    - 음이 아닌 정수형 변수(S)

    - 초기화 연산, P() 연산, V() 연산으로만 접근 가능

    - 임의의 S 변수 하나에 ready queue 하나가 할당됨

 

    - Binary semaphore

        * S가 0rhk 1 두 종류의 값만 갖는 경우

        * 상호배제나 프로세스 동기화의 목적으로 사용

 

    - Counting semaphore

        * S가 0이상의 정수 값을 가질 수 있는 경우

        * Producer-Consumer 문제 등을 해결하기 위해 사용

 

    - 연산

        * 초기화 연산 : S 변수에 초기 값을 부여하는 연산

        * P() 연산, V() 연산

        * P() 연산 설명

            + S를 물건의 개수라고 가정

            + if(S>0) : S가 있는지 확인

            + then S <- S - 1 : 물건 한 개를 가져가고 임계 영역으로 진입

            + else wait on the queue Qs : S에 할당된 ready queue에서 대기

            + Spinklock는? S가 없으면 while을 계속 도는 busy waiting 현상 발생, semaphore는 queue에 넣어서 프로세스를 block 상태로 대기

 

        * V() 연산 설명

            + 임계 영역에서 일을 끝냈으면 실행

            + if(waiting processes on Qs) : ready queue에 기다리는 프로세스 있는지 확인

            + then wake up one of them : 기다리는 프로세스 중에 하나 깨우기

            + else S <- S + 1 : 물건 되돌려 놓음

 

    - 특징 : OS가 원자성을 보장

 

    - where to use

         * Windows의 MSDN

         * Unix/Linux의 system V

 

    - Semaphore로 해결 가능한 동기화 문제

        * 상호 배제 문제

        * 프로세스 동기화 문제

        * producer-consumer 문제

        * reader-writer 문제

 

1) 상호 배제 문제

    - P()와 V()로 해결

2) 프로세스 동기화

    - 프로세스들의 실행 순서 맞추기

    - 상황 : 프로세스 Pi는 프로세스 Pj가 Lj 지점을 통과할 때까지 Li 지점에서 대기해야 함

 

    - 해결 경우 1 : Pj가 sync에서 1을 가져옴 => 그래서 sync = 0 상태 => Pj는 처리 중이고 Pi는 대기 => Pj가 끝나고 V()를 통해서 sync = 1이 됨, 동시에 V()가 ready queue에 있던 Pi를 깨움 => Pi은 sync으로부터 1을 가져오고 sync = 0이 됨 => Pi 작업 진행

 

    - 해결 경우 2 : Pi이 먼저 도착, 하지만 Pj가 먼저 sync를 가져가서 Pi는 ready queue에서 대기 => Pj가 오고 작업 완료 후, Pi를 깨우고 sync에게 1 반환 => Pi는 sync에서 1 가져오고 작업 수행

 

 

3) Producer-Consumer 문제

    - 생산자 프로세스 : 메시지를 생성하는 프로세스 그룹

    - 소비자 프로세스 : 메시지를 전달받는 프로세스 그룹

 

    - 설명 : Producer process는 물건을 생산해서 shared memory buffer에 쌓고, Consumer process는 shared memory buffer에 저장된 데이터를 가져가서 소비

 

    - 문제 상황

        * Producer process가 buffer에 쌓는 중에 Consumer process가 가져가면 안됨

        * A Producer process가 buffer에 넣을 때, B Producer process가 넣으면 안됨

 

    - 해결 방법 : with single buffer

        * single buffer로 물건을 놓을 때 가져 갈 수 없고, 물건을 가져갈 때 물건을 놓을 수 없는 한 번에 한 번 접근만 가능하게 하는 방법

    - 변수

        * consumed : 소비되었으면 1

        * produced : 생산되었으면 1

        * buffer : semaphore(앞에서 S 같은 변수)

 

    - 과정

        * Producer : Producer Pi가 buffer에 넣으려고 함 => P(consumed)로 소비되었는지 확인 => 소비되어 있으면, consumed = 0으로 바꾸고, produced = 1로 바꿈

        * Consumer : Consumer Cj가 소비하려고 함 => P(produced)로 생산되었는지 확인 => 생산되어 있으면, produced = 0으로 바꾸고, consumed=1로 바꿈, 만약에 생산되어 있지 않으면, queue에서 대기 => Producer Pi의 V()가 끝나면 queue에 대기하고 있던 프로세스 깨워서 작업 시작

 

    - 해결 방법 : with N-buffers

    - 변수

        * nrfull : 채워진 buffer 수

        *nrempty : 비워진 buffer 수

        * mutexP : Producer 상호 배제를 위한 변수

        * mutexC : Consumer 상호 배제를 위한 변수

        * in, out : buffer에서 가져가고 가져오는 위치를 표시하는 변수

 

    - 과정

        * Producer : P(mutexP)로 critical section이 비었는지 확인 => P(nrempty)로 buffers에 빈 곳이 있는지 확인 => 공간이 없으면 대기, 공간이 있으면 Buffer[in]에 물건 M 놓기 => in <- (in + 1) mod N으로 포인터 위치 변경 => V(nrfull)로 buffers에 물건 놓았다고 갱신 => V(mutexP)로 critical section 벗어났다고 신호

       * Consumer : P(mutexC)로 critical secction이 비었는지 확인 => P(nrfull)로 buffers에 물건이 있는지 확인 => 물건이 없으면 대기, 있으면 buffers[out]에서 물건 꺼내기 => out <- (out + 1) mod N으로 포인터 위치 변경 => V(nrempty)로 buffers에서 물건 뺏다고 갱신 => V(mutexC)로 critical section 벗어났다고 신호

 

 

4) Reader-Writer 문제

 

    - Reader : 데이터에 대해 읽기 연산만 수행

    - Writer : 데이터에 대해 쓰기(갱신) 연산만 수행

    - 데이터 무결성 보장 필요

        * Reader들은 데이터에 동시 접근 가능

        * Writer와 Reader 사이에는 상호 배제, Writer와 Writer 상이에는 상호 배제

    - 해결 방법 : Reader 또는 Writer에게 우선권 부여

 

 

    - 해결 방법 : Reader preference solution

        * 1단계 : 읽기 전 준비

            + P()와 V() 사이는 critical section

            + if(nreaders = 0) then : 읽는 작업이 없으면, 지금부터 쓸 예정이니 P(wmutex)로 writer 작업 중지

            + nreaders <- nreaders + 1 : 읽는 작업 추가

 

        * Perform read operations : 읽는 작업 실행, 여러 개의 읽은 작업 가능

 

        * 2단계 : 읽는 작업 완료 후 정리

            + P()와 V() 사이는 critical section

            + nreaders <- nreaders - 1 : 다 읽었느니 작업 개수 감소

            + if(nreaders = 0) then : 읽는 작업이 자신이 마지막이면, V(wmutex)로 writer 작업 중지 해제

 

    - 장점

        * no busy waiting : queue라는 대기실이 있어서 프로세스는 block 상태로 대기

        * starvation problem : semaphore queue에 대한 wake-up 순서는 비결정적이기 때문에 queue에 대기하는 프로세스 중에 어떤 프로세스는 무한히 안 깨울 수도 있음 => Eventcounter / Sequencer로 해결

 

 

3. Sequencer / Eventcount

    - 은행에서 번호표를 뽑아 일 처리하는 과정과 유사

 

 

    - Sequencer

        * 발생 사건들의 순서를 기록 (번호표 출력기)

        * 정수형 변수, 생성 시 0으로 초기화, 감소하지 않음

        * ticket() 연산으로만 접근 가능

        * ticket()

            + 증가되어 온 sequencer 값을 반환하고 +1 해줌 (번호표 출력되듯이 반환)

 

 

    - Eventcount

        * 작업 중인 사건의 발생 횟수를 기록 (번호 호출판)

        * 정수형 변수, 생성시 0으로 초기화, 감소하지 않음

        * read(E), await(E, v), advance(E) 연산으로만 접근 가능

        * read(E)

            + 현재 번호가 몇 번인지 확인하는 함수로, 현재 eventcount 값 반환

        * await(E, v)

            + E = 현재 작업 번호, v = 내 번호

            + if(E < v)이면, 아직 내 차례가 오지 않았으므로 큐에 프로세스 넣고 대기 그리고 CPU scheduler에게 깨워 달라고 호출

        * advance(E)

            + 은행원이 현재 번호에 + 1 시키고 기다리는 사람 부름

            + E <- E + 1, E 차례를 기다리던 프로세스를 깨움

 

 

    - 상호 배제 문제 해결

 

 

    - Producer-Consumer 문제 해결

        * Producer Pi 설명

            + t <- ticket(Pticket) : 번호표 뽑기

            + await(In, t) : 현재 작업 번호 In에 나의 번호 t가 도달할 때까지 대기

            + await(Out, t-N+1) : 빈 공간이 있는지 확인

               [공간 >= 1 -> 공간 = N-t+Out -> Out >= t-N+1 -> Out < t-N+1 : 빈 공간 없음]

            + Buffer[t mod N] <- M : mod 연산으로 t 자리에 M 넣기

            + advance(In) : 작업이 끝났으니 In <- In + 1 해주고, In 기다리는 프로세스를 깨움

 

        *  Consumer Pi 설명

            + u <- ticket(Uticket) : 번호표 뽑기

            + await(Out, t) : 현재 작업 번호 Out에 나의 번호 u가 도달할 때까지 대기

            + await(In, u+1) : 물건 있는지 확인

               [물건 >= 1 -> In - u >= 1 -> In >= 1 + u -> In < 1 +u : 물건이 없음]

            + M <- Buffer[u mod N] : mod 연산으로 u 자리에서 물건 가져오기

            + advance(Out) : 작업이 끝났으니 Out <- Out + 1 해주고, Out 기다리는 프로세스를 깨움

 

 

    - 장점

        * No busy waiting

        * No starvation : FIFO scheduling for queue

        * semaphore 보다 더 low-level control이 가능 : 순서를 control 가능하기 때문에 더 세부적임

 

 

 

상호 배제를 해결하기 위한 Language 솔루션

 

SW, HW, OS 해결 방법과 차이

    - 위 3가지 방법들은 low-level mechanisms

    - 따라서, Flexible 하지만, 사용하기 어려움.

 

 

High-level mechanism 해결 방법

    - Monitor

    - Path expressions

    - Serializers

    - Critical region, conditional critical region

 

    - 특징

        * Language-level constructs

        * Object-oriented concept과 유사

        * 사용이 쉬움

 

 

1. Monitor

    - 마치, 한 번에 한 명만 들어올 수 있는 책방. Critical data는 빌리는 책. Critical section은 대출과 반납을 위한 연산

    - Monitor에서 일어나느 과정을 Language가 보장

 

    - 구조

        * entry queue : monitor 내의 procedure 수 만큼 존재

        * mutual exclusion : monitor 내에는 항상 하나의 프로세스만 진입 가능

        * Information hiding : critical data는 monitor 내의 프로세스만 접근 가능

        * condition queue : monitor 내의 특정 이벤트를 기다리는 프로세스가 기다리는 대기실

        * signal queue : signal() 명령을 실행한 프로세스가 임시로 대기하는 대기실

 

 

    - 과정

        * 초기 상태

            + Pj 요청 전 상태

            + monitor 안에 프로세스 없음

            + R_Available : 자원 있는지 확인 변수

 

        * 상태 1

            + Pj가 자원 요청

 

        * 상태 2

 

            + 자원 R은 Pj에게 할당

            + Pi와 Pl은 자원이 없어서 condition queue 대기실에서 대기

 

        * 상태 3

            + Pj는 자원을 사용하고 반환하기 위해서 대기

 

        * 상태 4

            + Pj가 자원을 반환

 

        * 상태 5

            + Pj는 signal queue에 들어가서 R_Free.signal() 호출에 의해 condition queue에서 Pi를 깨움

            + Pi는 자원 요청

 

        * 상태 6

            + Pi에게 자원 R 할당

            + Pj가 monitor 안으로 돌아와서 남은 작업 수행하고 나감

 

 

1) Producer-Consumer 문제 해결

    - 설명

        * Entry queue for fillBuf() : buffer에 넣는 procedure. Producer가 사용

        * Entry queue for emptyBuf() : buffer에서 빼는 procedure. Consumer가 이용

        * bufHasData : 데이터가 없을 때 대기하는 장소. Consumer가 이용

        * bufhasSpace : 공간이 없을 때 대기하는 장소. Producer가 이용

        * signaler queue : 대기하는 깨워주는 신호 대기하는 장소

        * in : 넣은 위치

        * out : 뺄 위치

        * validBufs : buffer에 물건 개수

 

 

2) Reader-Writer 문제 해결

    - Reader/Writer 프로세스들 간의 데이터 무결성 보장 기법

    - Writer 프로세스에 의한 데이터 접근 시에만 상호 배제 및 동기화 필요

 

    - 하는 방법 (한 번 해보기)

        * 변수 2개

            + 현재 읽기 작업을 진행하고 있는 Reader 프로세스의 수

            + 현재 Writer 프로세스가 쓰기 작업을 진행 중인지 표시

        * 조건 큐 2개

            + Reader/Writer 프로세스가 대기해야 할 경우에 사용

        * 프로시저 4개

            + Reader/Writer 프로세스가 읽기/쓰기 작업을 원할 경우에 호출

            + Reader/Writer 프로세스가 읽기/쓰기 작업을 마쳤을 경우에 호출

        * 정답 : 22:07

https://www.youtube.com/watch?v=AnYN-kbCbRI&list=PLBrGAFAIyf5rby7QylRc6JxU5lzQ9c4tN&index=18

 

 

 

3) Dining philosopher 문제 해결

    - 철학자(프로세스) : 생각하는 일, 스파게티 먹는 일 반복

    - 공유 자원 : 스파게티, 포크

    - 조건 : 스파게티를 먹기 위해서는 좌우 포크 2개 모두 필요

    - 과정

        * numForks : 자신이 사용가능한 포크 개수 (즉, 좌우에 1개씩 있으면 2)

 

 

    - 장점

        * 사용이 쉬움

        * Deadlock 등 error 발생 가능성 낮음

 

    - 단점

        * 지원하는 언어에서만 사용 가능

        * 언어를 기계어로 컴파일 해줘야 하기 때문에 컴파일러가 OS를 이해하고 있어야 함

 

 

 

 

 

 

 

 

 

본 내용은 한국기술교육대학교 김덕수 교수님의 유튜브 강의를 듣고 정리한 내용입니다.
반응형

'Software Courses > Operating System' 카테고리의 다른 글

[Deadlock] Deadlock Solutions  (0) 2020.12.06
[Deadlock] Deadlock and Resources types  (0) 2020.12.04
Process Scheduling  (0) 2020.12.01
Thread 관리  (0) 2020.11.30
Process 관리  (0) 2020.11.30