OS [OS]Process Synchronization(Problem, DeadLock and Starvation, Semaphore)

zzarbttoo·2021년 8월 7일
0

OS(운영체제)

목록 보기
7/12

이 글은 KOCW에 공개되어있는 '반효경 교수님'의 운영체제 강의 및 강의 교재 Operation System Concepts(a.k.a 공룡책🦕)의 내용을 기반으로 작성했습니다.

이번 챕터에서는 Process Synchronization에 관해 정리해보겠습니다
오류가 있다면 댓글로 정정 부탁드립니다


Process Synchronization Problem

  • Process Synchronization variable : 수행의 동기화(synchronize)를 위해 몇몇의 변수를 공유할 수 있다
  • 공유 데이터(shared data)의 동시 접근(concurrent access)는 데이터의 불일치 문제(inconsistency)를 발생시킬 수 있다(여러 프로세스가 동시에 데이터를 읽은 후 수정한다면, 누가 먼저 읽었느냐에 따라 문제가 발생할 수 있다)
  • 이러한 문제는 CPU-Memory, 컴퓨터 내부-IO 장치, 프로세스-주소공간 등 여러 곳에서 일어날 수 있다
  • 일관성 유지(consistency)를 위해 협력 프로세스(cooperating process)간의 실행 순서(orderly execution)을 정해주는 메커니즘이 필요하다

| Race Condition

여러 프로세스들이 동시에 공유 데이터에 접근하는 상황

  • 데이터의 최종 연산 결과는 마지막에 그 데이터를 다룬 프로세스에 따라 달라짐
  • race condition을 막기 위해서는 concurrent process는 동기화 되어야 한다

| OS에서 Race Condition이 발생 할 수 있는 상황

1. kernel 수행 중 인터럽트 발생

1) 메모리에 있는 값(ex Count)을 CPU 내의 register로 읽고 그 값에 변화를 주고 있는 상태(Count + 1을 명령함)에서 Interrupt가 발생
2) 해당 작업을 멈추고 Interrupt 처리 루틴으로 갔을 때 Interrupt Handler가 Count 값에 변화를 주라고 명령함(Count - 1)
3) 이후 중단됐던 작업을 다시 수행할 때 Context상 2번 작업에 대해 알 수 없기 때문에, 1번의 값 변화만 적용이 된다(Count는 +1 만 됨)

  • 이러한 상황을 발생시키지 않기 위해 중요한 값을 수정하는 상황에서는 중간에 Interrupt가 들어오더라도, 그 작업이 끝나 값을 저장할 때까지는 Interrupt를 받지 않도록 한다

2. Process가 System Call 을 해 Kernel mode로 수행 중인데, Context Switch가 일어나는 경우

1) 각 Process가 User Mode -> Kernel Mode -> ...-> 와 같이 실행이 되며 User Mode, Kernel Mode 모두 실행 시간에 제한을 둔다(Time interrupt 발생)
2) Process A가 Kernel Mode를 수행하며, 메모리에 있는 값(Count)을 조작(Count + 1)하는 와중에 Time Interrupt 당함
3) Process B가 Kernel Mode를 수행하며 메모리에 있는 값(Count)을 조작함(Count + 1)
4) 하지만 Count 값은 2, 3번의 값이 모두 적용되지 않는 문제가 발생함(Count +2가 되지 않은 상태)

  • 이러한 상황을 발생시키지 않기 위해 Process가 Kernel 모드에 있을 때는 CPU 할당 시간이 끝나도 CPU를 뺏기지 않도록 한다

3. Multiprocessor에서 Shared Memory 내의 Kernel data

  • Memory Address Space를 공유하는 CPU Process가 여러개 있는 경우(공유 메모리 사용) 어떤 하나의 주체가 데이터를 수정하는 동안 다른 주체가 데이터를 또 수정한다면, 우리가 원하지 않는 데이터를 얻을 수 있다
  • 이러한 상황을 발생시키지 않기 위해 한번에 하나의 CPU만이 커널에 들어갈 수 있도록 하거나, 커널 내부에 있는 각 공유 데이터에 접근할 때마다 그 데이터에 대한 Lock/Unlock을 하도록 한다

| Critical-Section Problem

critical-section은 공유 데이터에 접근하고자 하는 코드를 말한다

  • n개의 프로세스가 공유 데이터를 동시에 사용하기를 원하는 경우 각 프로세스의 code segment에는 code critical section이 존재

| 프로그램적 해결법

모든 프로세스의 수행 속도는 0보다 크고, 프로세스들 간의 상대적인 수행 속도는 고려하지 않는다고 가정한다

1. Mutual Exclusion(상호 배제)

  • 하나의 프로세스가 critical section에 있을 때 다른 모든 프로세스는 critical-section에 들어갈 수 없어야 한다

2. Progress(진행)

  • 어떤 프로세스도 critical section에 있지 않을 때 critical section에 들어가고자 하는 프로세스가 있다면 들어가게 해줘야 한다
  • 코드를 잘못 짜면 들어가고 싶은 프로세스가 두 개일 경우 아무도 들어가지 못하는 문제가 발생한다

3. Bounded Waiting(유한 대기)

  • 프로세스가 critical section에 들어가려고 요청한 후부터 그 요청이 허용될 때까지 다른 프로세스들이 critical section에 들어가려는 횟수에 제한이 있어야 한다
  • 특정 프로세스 입장에서 critical section에 못 들어가고, starvation이 발생하면 안된다
  • 만약 세 개의 프로세스가 들어가고자 할 때 두 개의 프로세스만 계속 진입하고 하나는 진입하지 못한다면 위와 같은 상황이 발생한다

알고리즘 구현

turn, flag를 이용해 구현을 할 수 있다

Algorithm 1(turn 이용)

Process 0가 아래 코드를 진행하는 상황이고, Process i는 turn == i 여야 critical section에 진입할 수 있다

do{
while(turn != 0);
critical section
turn = 1;
remainder section
}while(1);
  • Mutual Exclusion은 만족하나, Progress를 만족하지 않는다
  • Swap-turn : Process간 반드시 교대가 이루어져야 진행이 된다(다른 Process가 turn 값을 바꿔줘야 다른 프로세스가 진입할 수 있다)
  • 특정 프로세스가 더 빈번하게 critical section에 들어가야 할 경우 Swap-turn 을 하는 것이 비효율적일 수 있다

Algorithm 2 (flag 이용)

do{
flag[i] = true;
while(flag[i]);
critical section
flag[i] = false;
remainder section 
}while(1);
  • Mutual Exclusion은 만족하나, Progress를 만족하지 않는다
  • 결국 모든 값이 false가 되기 때문에 끊임없이 양보를 하는 상황이 발생한다

Algorithm 3(turn과 flag 모두 이용, Peterson's Algorithm)

Process 0가 아래 코드를 진행하는 상황이고, Process i는 turn == i 여야 critical section에 진입할 수 있다

do{
flag[i] = true;
turn = j // j는 다른 process 번호이다 
while(flag[j] && turn == j);
critical section 
flag[i] = false;
remainder section 
}while(1);
  • Mutual Execution, Process, Bounded Waiting 모두를 만족한다
  • Process i 가 critical section으로 들어가려고 할 때 flag=true로 바꾸고, turn 을 상대방 turn으로 바꾸어놓는다
  • critical section 들어가기 전 상대방이 깃발을 들고 있고, 이번이 상대방 차례인 경우에만 기다리게 되고 아니면 critical section으로 진입한다
  • Busy Waiting(spin lock) 발생: while문을 돌면서 lock을 걸어서 상대방이 들어가지 못하게 하기 때문에 기다리면서 쓸데없이 CPU 시간을 할애할 수 있다

Semaphores

멀티프로그래밍 환경에서 공유 자원에 접근을 제한하기 위해 고안된 두개의 원자적 함수로 조작되는 정수 변수

  • 앞선 방식들을 모두 추상화 시킨 것이 Semaphore이다
  • 다음의 원자적(atomic) 함수 두개에 의해 접근이 가능하다( P(S), V(S) )
  • 락을 걸고 푸는 작업, 공유 자원 획득/반납을 간결하게 해준다
  • 세마포어 변수는 정수값을 가질 수 있으며, 자원의 갯수를 의미한다
  • 세마포어 구현을 위해서는 자발적 협력이 필요하다
  • 구현과 정확성 입증이 어렵고, 한 번 실수해서 코딩하면 모든 시스템에 치명적인 영향을 줄 수 있다

P(S)

while (S <= 0) do no-op; // wait 
S--;
  • 세마포어 공유 데이터를 획득하는 함수이다
  • 변수가 하나인 경우, P연산을 하면 락을 거는 것을 의미한다

V(S)

S++;
  • 세마포어 공유 데이터를 반납하는 함수이다
  • 변수가 하나인 경우, V연산을 하면 락을 푸는 것을 의미한다

세마포어 종류는 다음과 같다

1) Counting semaphore

  • 도메인이 0이상인 임의의 점수값, resource counting에 주로 사용된다

2) Binary semaphore(=mutex)

  • 0 또는 1만 가질 수 있고(Binary) 주로 mutual exclusoin(lock/unlock)에 사용된다

| Critical Section

process(n)이 진입하는 상황이다

| Busy waiting 방식

semaphore mutex;

do {
P(mutex); // 양수이면 진입, 아니라면 wait
critical section 
V(mutex); // semaphore 변수 증가
remainder section;
}while(1);
  • 프로그래머는 세마포어가 지원된다면 P와 V연산만 작업하면 되기 때문에 간편하다
  • 위 코드에서 spin lock(busy waiting) 문제가 발생한다

| Block/Wakeup Implementation

공유자원을 얻지 못하면 쓸데없이 CPU 시간을 낭비하지 않고 잠들도록 하는 것

  • 보통은 block/wakeup을 쓰는 것이 효율적이나, 이 방식도 overhead가 발생하게 된다
  • critical section의 길이가 짧으면 busy waiting을 사용해도 overhead가 덜 발생할 수 있다
typedef struct{
int value; //세마포어 
struct process *L; //process waite queue
}semaphore;
  • IO 작업을 하면 blocked 상태가 되고, CPU를 얻게 되는데 공유데이터도 마찬가지로 자원을 얻을 수 없게 되면 blocked 시키고 자원을 얻을 수 있게 되면 wakeup 시키게 된다

block

  • 커널은 block을 호출한 프로세스를 suspend 시킴
  • 이 프로세스의 PCB를 semaphore에 대한 wait queue에 넣음

wakeup(P)

  • block 된 프로세스(P)를 wakeup 시킴
  • 이 프로세스의 PCB를 ready queue로 옮김

P(S)

S.value--;
if (S.value <0){
add this process to S.L; //자원 waiting하는 queue에 PCB 연결 
block(); //재운다 
}
  • 여분이 있다면 자원을 획득하게 하고 아니면 잠들도록 한다
  • 이 때 자원을 waiting하는 List에 프로세스를 연결시키고 프로세스를 blocked 시킨다

V(S)

S.value++;
if (S.value <=0){ //S.value <= 0이면 자원을 기다리는 상태(자원의 갯수는 아님)
remove a process P from S.L; //자원 waiting queue에서 지움
wakeup(P); //자고 있는 프로세스를 깨움 
}
  • 자원을 반납하게 된다
  • 자원을 반납했을 때도 S가 0 이하라는 것은 누군가 자원을 기다리면서 잠들어 있다는 뜻(S.value가 자원의 갯수를 뜻하는 것은 아님)
  • 잠들어 있는 프로세스를 wait List에서 빼서 자원을 주고 기다려야 함

Deadlock and Starvation

| Deadlock

둘 이상의 프로세스가 서로 상대방에 의해 충족될 수 있는 event를 무한히 기다리는 현상

  • 어떤 일을 하기 위해 각 프로세스(P0, P1)가 세마포어 S와 Q를 모두 획득해야 하는데 P0, P1이 각각 S, Q를 나누어 가져가면 두 프로세스 모두 영원히 기다릴 수 있다

| Starvation(Indefinite blocking)

특정 프로세스가 자원을 얻지 못하고 영원히 기다리는 상황

  • Deadlock도 starvation의 일종이라 할 수 있다

| Bounded-Buffer Problem

Buffer의 갯수(크기)가 제한되어 있기 때문에 생산자 - 소비자 문제가 발생할 수 있다

  • Empty Buffer : 생산자는 Empty Buffer을 찾아 데이터를 집어넣는다(Full Buffer++, Empty Buffer--)
  • Full Buffer : 소비자는 Full Buffer을 찾아 데이터를 꺼낸다(Empty Buffer++, Full Buffer--)
  • Empty Buffer가 없으면 생산자가 대기를 하게 되고, Full Buffer이 없다면 소비자가 대기를 하게 된다

문제점

1) 생산자 두개가 하나의 Empty Buffer 에 동시 도착
2) 소비자 두개가 하나의 Full Buffer에 동시 도착

  • 1, 2를 해결하기 위해 공유 버퍼에 lock을 걸어 다른 프로세스의 접근을 막은 후 버퍼에 데이터를 넣거나 빼도록 한다

3) Buffer의 크기가 제한되어 있기 때문에 생기는 문제

  • 버퍼가 다 차면 생산자는 계속해서 기다려야 하고, 버퍼가 다 비면 소비자는 계속해서 기다려야 하는 문제가 생긴다

구현

  • Mutual Execlusion을 충족하기 위해 Binary Semaphore이 필요하다(Shared Data의 Mutual Execlusion)
  • Resource Count를 하기 위해 Integer Semaphore이 필요하다(남은 Full/Empty Buffer 수 표시)
  • Producer과 Consumer의 흐름이 반대로 되는 것을 확인할 수 있다

Producer

do{
produce an item in x 

P(empty); //empty : 비어있는 버퍼의 변수 
P(mutex); // mutex : lock을 걸기 위한 변수 
...
add x to buffer 
...
V(mutex); // 값을 집어넣은 후 lock을 풀어준다 
V(full); //empty -> full이 된 모습

}while(1)

Consumer

do{
P(full); //full : 차있는 버퍼의 변수 
P(mutex); //mutex : lock을 걸기 위한 변수
...
remove an item from buffer to y  
...
V(mutex); //값을 뺀 후 lock을 풀어준다 
V(empty); //full -> empty가 된 모습
...
consume the item in y 
...
}while(1);

| Readers-Writers Problem

한 process가 DB에 write 중일 때 다른 process가 접근하면 안된다(read는 여러 process가 동시에 해도 됨)

  • Writer가 DB의 허가를 얻지 못했다면, Reader은 DB에 접근해도 되며, Writer은 대기 중인 Reader이 없을 때 DB 접근이 허용된다
  • Writer가 DB 접근 중이라면 Reader의 접근은 금지되며, Writer가 DB를 빠져나가면 Reader의 접근이 허용된다
  • Writer가 지나치게 오래 기다려서 starvation이 일어날 수 있기 때문에 Queue를 두어서 Writer을 접근하게 하거나 신호등과 같은 것을 구현해 해결할 수 있다

구현

  • 공유 데이터로는 readCount(0으로 초기화), db가 있다
  • readCount도 공유 변수이기 때문에 lock이 필요하고 이를 위해 mutex라는 세마포어가 필요하다

Writer

semaphore mutex = 1, db = 1;

P(db);
...
writing DB is performed
...
V(db);

Reader

int readCount = 0; //shared Data

P(mutex);
readCount++;
if (readCount == 1) P(db); //writer blocked 되고 readers는 뒤의 과정을 진행할 수 있다  
V(mutex);
...
reading DB is performed
...
P(mutex);
readCount--;
if(readCount == 0) V(db); //마지막으로 읽고 빠져나가면 read count는 0일 것이고 writer 접근이 가능하도록 lock을 풀어줘야한다  
V(mutex); 

| Dining-Philosophers Problem

젓가락을 한짝씩만 준 아주 놀라운 상황

문제

  • 여러개의 Process가 있고, 양쪽의 자원을 모두 점유해야 Process 진행이 가능하다
  • 한 번 잡은 자원은 배가 부를 때 까지 놓지 않게 된다
  • 왼쪽의 자원을 잡았는데 오른쪽의 자원을 옆의 사람이 점유해 얻을 수 없고 그 상황이 지속될 때 deadlock의 가능성이 있다(누구도 자원을 양보하지 않아 진행되는 프로세스가 없다)

해결

  • 철학자를 짝수개로 놓으면 해결될 수 있다
  • 두개의 resource(젓가락)를 모두 점유할 수 있을 때만 resource를 점유하도록 하면 해결할 수 있다
  • 짝수(홀수) process는 왼쪽(오른쪽)의 resource부터 집도록 설계하면 해결할 수 있다

구현
Process(철학자)
Process i의 실행 코드이다

do{
pickup(i); //i는 process 번호
eat();
putdown(i);
think();
}while(1);
void putdown(int i){
P(mutex);
state[i] = thinking;
test((i+4) % 5);
test((i+1) % 5);
V(mutex);
}
void pickup(int i){
P(mutex);
state[i] = hungry;
test(i);
V(mutex);
P(self[i]);
}

//오른쪽, 왼쪽 젓가락을 점유할 수 있고 현재 상태가 hungry이면 젓가락을 점유할 수 있도록 한다 
void test(int i){
if (state[(i+4)%5] != eating && state[i] == hungry && state[(i+1)%5] != eating){
state[i] = eating;
V(self[i]);
}
}
  • state[i]는 철학자의 상태로 enum {thinking, hungry, eating} 으로 이루어져있다
  • test : 오른쪽, 왼쪽 젓가락을 점유할 수 있고 현재 상태가 hungry이면 젓가락을 점유할 수 있도록 한다
profile
기록하는 것을 좋아하는 사람입니다

0개의 댓글