본문 바로가기
학교 강의/운영체제

Synchronization Hardware

by dustnn 2025. 6. 9.

 

Synchronization Hardware

 

synchronization을 지원하는 hw가 있으면 앞의 문제가 수월하게 해결된다.

 

이전까지는 한 사용자가 변경 사항을 저장하지 않은 상태에서 다른 사용자가 읽어갔을 때 불일치하여 생기는 문제였다.

따라서 이 문제는 하드웨어가 test&modify 작업을 원자적으로(atomic하게) 수행하도록 지원하면 해결 가능하다.

 

다음 두 연산을 atomic하게 한 번에 수행 가능하도록 해준다.

1. Read: a값을 읽어가고(0/1)

2. TRUE: a값을 TRUE로 만들기(1로 세팅)

 

예컨대 다음과 같이 빨간 형광펜 부분에서 atomic한 수행이 발생한다.

lock=False일 경우: lock을 read하는 동시에 TRUE로 변경시킴

lock=True일 경우: while문 조건에 만족하지 않으므로 실행x

 

Semaphores와 Monitors는

위처럼 코드를 직접 짜지 않고 추상화시킨 방식들이다.

구조가 어떠한지, 컴퓨터에서 어떻게 표현되는지는 논외로 하고 그냥 객체와 그 객체가 가리키는 연산만 알면 된다.

Semaphores

 

Semaphores란

 

"S"라는 정수형 변수를 사용(Semaphore S)

- atomic 연산에 S를 변수로 넣어 접근 가능

- S값을 통해 lock을 걸어 접근 가능한 프로세스 개수 조절 가능

 

<atomic 연산 2가지 종류>

1. P(S): 공유 데이터를 획득하는 과정을 피연산으로 사용(ex. lock 거는 과정)

- 아직 자원이 다 차지되지 않아 쓸 수 있는 상황(S>0) -> S-- 한 다음 P연산 수행

- 이미 자원이 다 차지된 상황(S<=0) -> P연산을 수행하지 못하고 기다림

 

2. V(S): 공유 데이터를 반납하는 과정을 피연산으로 사용(ex. lock 풀어주는 과정)

다 사용한 다음에 반납하여 다른 사용자가 쓸 수 있도록.

 

=> ex. S=1이면, 하나의 프로세스만 공유 데이터에 접근할 수 있음

=> 실제로 자원을 사용하는 코드는 P와 V 사이에 존재

즉, 공유 자원을 세고, 남은 자원들이 있는지 확인하여 제어 및 관리하는 데 S변수를 사용 가능

 

<코드>

while문을 계속 돌면서 P연산을 못 빠져나감

-> (문제) busy-waiting(spin lock): 자신의 CPU 시간을 쓰면서 while문만 계속 도는 문제

* 계속 while문 spin하며 확인한다는 의미

-> (해결) Block&Wakeup(sleep lock): CPU를 아예 반납하고 blocked 상태로 전환해 낭비 줄임

* lock 이 걸리면 sleep시킴

 

<Block&Wakeup>

: semaphore를 단일값이 아니라 구조체로 정의하는 방법

* 구조체에 들어가는 것: semaphore값 & wait queue

semaphore 구조체
semaphore 구조체 시각화

L이라는 변수를 두어, semaphore을 줄세우는 리스트 구조

- block: block 호출한 프로세스는 suspend되고 이 프로세스의 PCB를 semaphore에 대한 wait queue에 넣음

- wakeup(P): block된 프로세스 P를 wakeup시키고 이 프로세스의 PCB를 ready queue에 옮김

 

 

semaphore가 그냥 값일 때는 변수 S가 여분의 자원 수를 말하지만

semaphore가 구조체로 정의된 경우에는 value가 그냥 여분 자원이 있는지(양수) 없는지(음수/0) 여부를 가리키는 것이다.

 

(+)효율적

(-) overhead 존재

critical section의 길이가 길거나 빈번할 경우에는 효율적이지만 

길이가 짧거나 경쟁이 치열하지 않은 경우에는 busy-wait 방식이 나을 수 있다.

하지만 일반적으로는 block&wakeup 방식이 더 좋음.

 

<semaphore의 종류>

 

- Counting semaphore: 자원 개수를 세야할 때 주로 사용(도메인이 0 이상인 임의의 정수값)

- Binary semaphore(=mutex): 주로 mutual exclusion(lock/unlock)에 사용(0/1값만 가질 수 있음)

* 상호배타적 접근 막으려면 1

 

<문제: deadlock & starvation>

semaphore 변수 2개 S, Q를 모두 얻어야만 하는 작업이 있다고 하자.

 

"deadlock이 발생하면 starvation이라는 문제 생김 -> 획득 순서를 만들어주어 해결 가능"

<deadlock>
초기에 P0는 S, P1은 Q라는 작업을 차지한 상태
- P0가 S를 빼앗김 -> Q를 가져와 작업하려고 하지만 이미 P1이 차지한 상태 -> 기다려야 함
- P1이 Q를 빼앗김 -> S를 가져와 작업하려고 하지만 이미 P0가 차지한 상태 -> 기다려야 함
=> (아직 끝나기 전이므로) 영원히 서로에게 필요한 것을 차지하고 안 내어놓기 때문에 제자리걸음

따라서 <starvation>
: indefinite blocking(suspend된 이유에 해당하는 세마포어 큐에서 빠져나갈 수 없는 현상)
∵ P0은 Q를, P1은 S를 영원히 얻지 못하기 때문

 

<해결책>
Q를 얻기 위해서는 전에 반드시 S를 먼저 획득해야 가능하도록 지정해두기

 

 

Synchronization의 문제점

 

1. Bounded-Buffer Problem(Producer-Consumer Problem)

- 공유데이터: Bounded-Buffer(크기가 유한해서 producer와 consumer이 공유하는 buffer)

- 접근 주체: producer & consumer

- semaphore 변수: counting semaphore

 

(문제 시나리오) 두 producer(consumer)가 빈자리만 보고 데이터를 동시에 입력하(가져가)는 경우

(해결책) 공유 데이터에 넣을(producer)/가져갈(consumer) 예정이라면 lock을 건다.

 

- producer 입장에서 자원: 빈 동그라미(넣어야 하기 때문) 없으면 consumer가 빼갈 때까지 기다림

- consumer 입장에서 자원: 차있는 동그라미(빼가야 하기 때문) 없으면 producer가 넣을 때까지 기다림

 

 

2. Readers and Writers Problem

- 공유데이터: 데이터베이스 & readcount;

- 접근 주체: reader & writer

- semaphore 변수

* db: reader와 writer가 공유 데베를 올바르게 접근하도록 lock 걸어줌

cf) "readcount; " → reader의 경우 특별히 동시에 가능케 하도록 하므로 접근자 수 count

* mutex: "readcount;" 조작(증가/감소) 위해 사용

 

(시나리오)

reader는 혼자 독점하면 비효율적

writer는 독점해서 써야 함

 

(해결책) 

reader가 접근할 때는 다른 reader도 접근 가능하도록

writer가 접근할 때는 아무도 못 쓰도록 

 

 

=> (한계)starvation 발생 가능

(해결책) 신호등의 원리처럼 특정 시간까지 도착한 reader만 읽을 수 있도록 하고 막음 writer에게 기회 줌 

 

 

3. Dining-Philosophers Problem

 

5명의 철학자가 원탁에 앉아서 생각하고 밥먹고 반복

- 공유데이터: 젓가락

- 접근 주체: 5명의 철학자

 

(문제점) 모든 철학자가 동시에 배가 고파져 왼쪽 젓가락을 집어버린 경우 → deadlock 가능성 있음

(해결 방안) 

1. 4명만 테이블에 동시에 앉을 수 있도록 하기

2. 젓가락을 두 개 모두 집을 수 있을 때만 젓가락을 집을 수 있도록 함

3. 비대칭: 짝수 철학자는 왼쪽 젓가락부터, 홀수 철학자는 오른쪽 젓가락부터 집을 수 있도록(아래와 같은 원리)

<semaphore 시나리오>

- semaphore:

* self[5]
** self=0 → 5명이 모두 양쪽 모두는 집을 수 없는 경우
** self=1 → 5명이 모두 양쪽 젓가락 집을 수 있는 경우

* state[5]
: 상태를 나타내는 semaphore(thinking/hungry/eating)

* mutex
state를 건드리기 위해 lock을 걸어주고 풀기 위한 semaphore

 

 

do{
	pickup(i);
    eat();
    putdown(i);
    think();
}while(1);

 

<pickup()>

void pickup(int i){
	P(mutex); //state 건드리기 위해 lock 풀기
    state[i]=hungry; //state hungry로 변경
    test(i); //5명 철학자가 모두 양쪽 젓가락을 잡을 수 있는지 check
    V(mutex); //state 건드리지 못하도록 lock 걸기
    P(self[i]); //test 결과 self[i]=1 -> 젓가락 잡기 끝 -> self[i]=0으로 변경 -> eat()로 넘어감
    //test 결과 self[i]=0 -> 이미 0으로 되어있기 때문에 변경 불가 -> sleep상태
}

 

<test()>

void test(int i) {
	if(state[(i+4)%5]!=eating && state[i]==hungry && state[(i+1)%5]!=eating) { //양쪽 사람들이 밥먹고 있지 않다면
    	state[i]=eating; //먹기
        V(self[i]); // 양쪽 사람이 둘다 먹고 있지 않다면 -> self[i]=1 -> pickup()에서 P(self[i])가 self[i]=0으로 바꿔줌
        //옆사람이 먹고 있어 조건 불만족 -> self[i]=0 -> sleep
    }
}

 

<putdown()>

void putdown(int i){
	P(mutex);//state 변경 위해 lock 풀기
    state[i]=thinking; //다 먹었으므로 젓가락 내려놓고 생각
    test((i+4)%5);//자신 때문에 옆 사람들이 test 통과 못한 것 우려해서 젓가락 내려놓은 후 test 대신 수행해줌
    test((i+1)%5);
    V(mutex);//state lock 걸기
}

 

 

Monitor

 

Semaphore 문제점

 

- 코딩하기 힘들다

- 정확성 입증 어려움

- 자발적 협력 필요

 

- 한 번 실수하면 모든 시스템에 치명적 영향

V(mutex)
Critical Section
P(mutex)

=> critical section에 여러 명 들어갈 수 있게 됨

P(mutex)
Critical Section
P(mutex)

=> 이후에는 어느 누구도 critical section에 들어갈 수 없게 됨

 

==> monitor이 해결해줌

Monitor

 

: 동기화 문제(공유데이터에 대한 동시 접근)를 책임져줌(queue에 놓아줌) → 프로그래머가 할 필요x → 실수 방지

: 오직 모니터 안에 정의된 함수만 실행 가능 → 공유데이터에 lock걸 필요 없음

 

semaphor 변수 대신 "condition variable" 사용

1. 자원 여분 있을 때 → 그냥 수행

2. 자원 여분 없을 때queue에 줄세우고 blocked 상태로..(x.wait) → 자원 생기면 깨워라(x.signal)

 

 

semaphor 변수는 원자적으로 1 빼거나 더해야 했지만 monitor은 동시 접근 막는 역할하기 때문에 그런 작업 필요 없음(애초에 한 프로세스만 한 번에 수행하기 때문)

 

  semaphore monitor
변수 역할 자원 개수 세기
→ 원자적으로 1 빼거나 더해야 함
자원 여분 없을 때 잠재워주고 줄 세우는 queue 역할만 수행
→ wait / signal로 상태 변경만
함수 형식 P로 자원 빌리고 V로 자원 반납 P와 V같은 것 쓰지 않음 → 자연스럽고 이해 쉬움
monitor bounded_buffer
{
	int buffer[N];
    condition full, empty;
    
    void produce(int x) //데이터 생산해서 buffer에 집어넣음
    {
    	if there is no empty buffer
        	empty.wait(); //빈 버퍼가 없으면 빈 버퍼 생길 때까지 기다림
        add x to an empty buffer
        full.signal(); //빈 버퍼에 데이터 넣음
    }
    
    void consume(int *x) //buffer에서 데이터 꺼내 감
    {
    	if there is no full buffer
        	full.wait(); //차있는 버퍼가 없으면 producer가 채워넣을 때까지 기다림
        remove an item from buffer and store it to *x
        empty.signal(); //찬 버퍼에서 데이터 꺼내감
    }
}

 

실제로 젓가락을 잡는지는 알수 없고

잡을 수 있는지 여부에 대한 공유 변수 존재

 

do{
	pickup(i);
    eat();
    putdown(i);
    think();
}while(1);

 

<pickup()>

void pickup(int i){
    state[i]=hungry; //state hungry로 변경
    test(i); //5명 철학자가 모두 양쪽 젓가락을 잡을 수 있는지 check
    if(state[i]!=eating) //먹을 수 없으면(self[i]=0)
    	self[i].wait(); //젓가락 양쪽 다 잡을 수 있을 때까지 기다림
}

 

<test()>

void test(int i){
	if(state[(i+4)%5]!=eating && state[i]==hungry && state[(i+1)%5]!=eating) { //양쪽 사람들이 밥먹고 있지 않다면
    	state[i]=eating;
		self[i].signal(); // 조건 만족하면(self[i]=1) 냅두고, 조건 만족하지 않으면(self[i]=0) 깨움 -> pickup()에서 wait() 깨져 밥먹을 수 있게 됨
}

 

<putdown()>

void putdown(int i){
	state[i]=thinking;
    test((i+4)%5);
    test((i+1)%5);
}

 

 

'학교 강의 > 운영체제' 카테고리의 다른 글

Deadlocks  (0) 2025.06.10
[Chapter 4] Process Management  (0) 2025.04.19
[Chapter 3] Process  (0) 2025.04.19
[Chapter 2] System Structure & Program Execution  (0) 2025.04.18
[Chapter 1] Introduction to Operating Systems  (0) 2025.04.10