안녕하세요!

FE 개발자 유진주입니다.

CS/운영체제

Chapter 5. 병행 프로세스와 동기화(1)

ypearl 2023. 4. 14. 20:11

병행(Concurrent): 같이 존재하고 있다는 의미. 메모리에 다수의 프로세스가 같이 존재하는 것.

프로세스들의 실행 순서는 일차적으로 스케줄링에서 담당하겠지만,

실제 실행 과정에서 프로세스 간의 좀 더 복잡한 문제는 세심한 프로세스 관리를 요구함.

 

다중처리 시스템의 경우 여러 개의 프로세스가 동시에 병렬(Parallel)로 실행됨 (=> 병행 != 병렬)

프로세스의 병행성은 처리기의 수와 상관없으나,

병렬처리가 성공하기 위해서는 기본적으로 병행성이 전제되어야 한다.

 

- 병행 프로세스들은 서로 간에 비동기적(Asyschronous)이다.

= 다른 프로세스들이 어떤 상태에 있는지, 어떤 자원을 가지고 있는지, 어디까지 실행됐는지 등에 대해 모른 채 실행 됨

 

이번 장의 목적

: 공유하는 자원이나 데이터가 있는 병행 프로세스들이 각자 비동기적으로 실행되는 것을 제대로 관리하지 못한 채 방치할 경우 생기는 문제와 이를 해결할 수 있는 방법을 설명한다.

 

병행 프로세스 (Concurrent Processes)

병행 프로세스들의 비동기적 실행은 서로 공유된 자원이 없는 한 아무 문제없이 독립적으로 진행되지만, 공유된 자원이 있을 경우 이 자원의 접근에는 일정한 룰(Rule)을 따라야 한다.

여기서 일정한 룰이란, "한 번에 한 프로세스만이 접근하도록 하고, 해당 자원에 대해 의도했던 실행을 완료하도록 보장" 하는 것.

 

상호배제 (Mutual Exclusion)

:  한 번에 하나의 프로세스만이 임계영역에 들어가야 함(임계영역을 실행해야 함)을 의미

 

- 경쟁 상태 (Race Condition): 프로세스들이 공유 데이터에 대해 서로 접근을 시도하는 상황

   => 교착 상태(Deadlock), 기아(Starvation) 등의 문제 발생.

- 임계영역(Critical Section): 두 개 이상의 프로세스가 동시에 사용할 수 없는 자원(임계자원(Critical Resource)에 대해 접근하고 실행하는 프로그램 내의 코드 부분

 

임게영역의 성공적 실행을 위해서는,

1) 상호배제가 지켜져야 한다.

2) 임계영역에 있지 않는 프로세스가 다른 프로세스의 임계영역 진입을 막아서는 안 된다.

3) 비어있는 (아무도 실행하고 있지 않는) 임계영역에 대한 진입은 바로 허용하되, 특정 프로세스의 진입 시도가 계속 무산되어 기아를 겪지 않도록 해야 한다.

 

- 병행 (concurrent)

: 같이 존재하고 있다.

= 메모리에 다수의 프로세스가 존재 (다중프로그래밍)

  • CPU가 하나 있는 단일처리 시스템에서는 병행 프로세스 중 한 개만이 실제로 실행되지만, CPU의 처리 시간을 효과적으로 나눔으로써 겉으로는 병행 프로세스들이 동시에 처리되는 것

프로세스 처리 순서는

1차적: CPU 스케줄링에 의해 진행.

하지만, 실제 실행 과정에서 프로세스 간의 관계로부터 발생하는 문제 발생 O.

ㄴ 이를 관리하는 것이 프로세스 관리의 핵심!

* 병행(concurrent) ≠ 병렬(parallel)

  • 병행(concurrent): 다중프로그래밍, 메모리에 다수의 프로세스가 존재
  • 병렬(parallel): 다중처리시스템, 여러 개의 CPU가 여러 개의 프로세스를 동시에 처리 (병렬 처리)

⇒ 병행에 있어서는 처리기(CPU) 수 관계 x,

    but, 병렬처리 위해서는 병행성이 전제 되어야 함.
   (ex. 다중처리 프로그래밍 위해서는 다중 프로그래밍이 되어있어야 함)

  • 단일처리 - 논리적 병행성 (Logical Concurrency)
  • 다중처리 - 물리적 병행성 (Physical Concurrency)

~> 이 두 경우 모두 프로세스가 병행하기 때문에 생기는 문제는 같으므로, 해결책 역시 같을 것!

- 병행 프로세스는 서로 간에 비동기적(Asynchronous)이다.

- 비동기적(Asynchronous)

: 프로세스들이 어떤 상태에 있는지, 어떤 자원을 가지고 있는지, 어디까지 실행됐는지 등에 대해 모른 체 실행되고 있음을 의미

 

 

5.1 병행 프로세스 (Concurrent Process)

  • 병행(concurrent) = 함께 있는 것
  • 비동기적(Asynchronous) = 서로 정보 모르는 것

ㄴ 서로 공유된 자원이 없다면, 문제 X (독립적)

    서로 공유된 자원이 있다면, 일정한 Rule 따라야 함.
                                                 = “한 번에 한 프로세스만이 접근하도록 하고,
                                                     해당 자원에 대해 의도했던 실행을 완료하도록 보장한다.”

 

*다중처리 시스템의 경우, 두 프로세스가 동시에 실행될 수 있으므로

 둘다 각자의 실행 결과를 저장하게 될 것이고,

 결국 변수의 최종값은 조금이라도 늦게 저장하는 프로세스의 결과값이 될 것이다.

*단일처리 시스템의 경우, 조작도중 CPU가 넘겨지더라도 조작 허용을 하지 않아야 한다.

5.2 상호배제 (Mutual Exclusion)

  • Race Condition (경쟁상태)

: 프로세스들이 공유 데이터에 대해 서로 접근을 시도하는 상황

→ <문제>

  • 상호배제 (Mutual Exclusion) →이번 장
  • 교착상태 (Deadlock) → 다음 장
  • 기아 (Starvation) → 다음 장
  • 임계자원 (Critical Resource): 두 개 이상의 프로세스가 동시에 사용할 수 없는 자원
  • 임계영역 (Critical Section): 임계영역에 대해 접근하고 실행하는 프로그램 내의 코드부분

⇒ 상호배제 (Mutual Exclusion): 한 번에 하나의 프로세스만이 임계영역에 들어가야 함.

(★=critical section에 대한 action은 전부 mutual exclusion을 해주어야 한다.)

<상호배제를 실현하기 위한 필요조건(원칙)>

  1. 상호배제 O
  2. 임계영역 X 프로세스가 다른 프로세스의 임계영역 진입을 막으면 X
  3. 비어있는 임계영역 진입 바로 허용
  4. 특정 프로세스의 진입시도가 계속 무산되어 기아를 겪지 않도록

<임계영역 진입 순서>

- 임계영역 진입할 때(enter primitive): 임계영역 내 다른 프로세스 존재 확인

ㄴ 확인한 후 있다면 기다리고, 없다면 들어가면서 자신이 나올 때까지 다른 프로세스가 들어오지 못하도록 함

- 임계영역 나올 때(exit primitive): 자신이 나오는 사실을 알려 다른 프로세스가 들어올 수 있도록!

⇒ 이것들을 어떻게 잘 구현(코딩)느냐가 상호배제의 성공여부 결정!

 

* 운영체제가 다양한 경우마다 구체적인 모든 내역을 파악하여 알아서 상호배제를 해주기가 어렵다.

- 공유 자원에 대한 상호배제의 의무는 일반적으로 프로그래머 책임

- 운영체제는 사용자의 상호배제 구현을 지원하기 위한 도구를 제공

예) 세마포어, 모니터

5.3 상호배제를 위한 소프트웨어 기법들

- 소프트웨어 기법들은,

운영체제의 지원 없이 프로세스들 간에 자신의 프로그램에서

임계영역 앞뒤로 적절한 코딩을 해줌으로써 상호배제를 해결하는 방식

(이 과정에서 CPU를 낭비할 수도, 프로그래머의 실수로 인한 오류가 생길 수도…)

● parbegin/parend 구조 (=cobegin/coend 구조)

parbegin
	statement_1;
	statement_2;
	. . .
	statement_n;
parend

: parbegin과 parend 사이 존재하는 n개의 문장들이 동시에 수행 가능

⇒ 이 구조에서 n개의 문장들의 나열 순서는 아무 의미가 없다!

예) 단일처리기 시스템 - 각 문장의 수행 순서를 임의로 진행해도 된다.

      다중처리기 시스템 - 각 문장을 병렬로 실행하겠다.

 

<★중요> 병행 프로세스 간의 실행 속도와 임계영역을 들어가려는 횟수의 차이는 얼마든지 날 수 O

5.3.1 몇 가지 미완성 시도들

제대로 된 프로그램은 프로세스 critical section에 대한 수행횟수에 제약이 있으면 안 된다. 그리고 프로세스들 사이 상대속도(시행속도)에 전혀 제약을 두어서는 안 된다.

  • 첫 번째 시도
P0:
While (ture) do:
	.
	.
	While (turn=1); /*do nothing*/
	<critical section>; //turn 값이 0이면 임계영역 접근 가능
	turn:=1;
	.
	.
endwhile;
P1:
While (ture) do:
	.
	.
	While (turn=0); /*do nothing*/
	<critical section>;
	turn:=0;
	.
	.
endwhile;
Begin /*main*/
	int turn=0;
	parbegin
		p0;
		p1;
 parend
End

<두 프로세스 사이의 상호베제를 위한 첫 번째 시도>

 

<문제점>

  • turn의 초기값이 0으로 되어있으므로, 두 프로세스에서 임계영역의 첫 번째 진입이 고정되어있다.

⇒ 임계영역이 비어있을 경우, 진입을 원하는 프로세스를 방해해서는 안 된다는 원칙 위배

  • 임계영역의 진입이 turn 값을 바꾸어줌으로써 가능하기에, p0과 p1은 정확하게 한 번씩 번갈아가며 진입이 가능하고 누구든 연속해서 두 번 이상 진입할 수 없다.

⇒ 프로세스 각자의 진입을 원하는 횟수는 얼마든지 다를 수 있으므로 상대적으로 많은 횟수의 진입이 요구되는 프로세스는 그 횟수만큼 상대 프로세스가 임계영역을 진입해주어야 함.

이것은 상태 프로세스가 먼저 종료될 경우에 자신도 더 이상 임계영역을 들어갈 수 없다는 문제 야기

& 상대적으로 실행 속도 느린 프로세스의 속도에 의존한다는 문제

 

  • 두 번째 시도
boolean flag[0], flag[1];
void p0()
{
	While (true)
	{   .
			.
		While (flag[1]); /* do nothing */
		flag[0]=true;
		<critical section>;
		flag[0]=false;
			.
			.
	}
}
void p1()
{
	While (true)
	{   .
			.
		While (flag[0]); /* do nothing */
		flag[1]=true;
		<critical section>;
		flag[1]=false;
			.
			.
	}
}
void main()
{
	flag[0]=false;
	flag[1]=false;
	parbegin
		p0;
		p1;
	parend
}

<두 프로세스 사이의 상호베제를 위한 두 번째 시도>

 

<해결점>

-임계영역 최초 진입 제한 X

-상대적으로 많은 횟수의 진입이나 상대 프로세스가 먼저 종료되어도 진입 가능 O

<문제점>

-임계영역에 p0과 p1이 둘 다 존재할 가능성 有 (→ 상호배제 X)

 

 

  • 세 번째 시도 : 위의 코드에서 flag[0]과 flag[1] 값을 모두 true로 변경.

<문제점>

-p0와 p1 모두 임계영역의 진입 불가능

 

 

5.3.2 성공적인 기법들

앞선 시도들의 경험을 바탕으로 flag와 turn 변수를 적절히 사용하여 두 프로세스 사이의 상호배제를 제대로 구현하였다.

● Dekker의 알고리즘

: Dekker의 알고리즘은 이해하기 어렵고 정확성을 증명하기 까다롭다 말해짐.

● Peterson의 알고리즘

: 간결하게 만들어진 Peterson의 알고리즘은 Dekker의 알고리즘의 단점을 해결하였음.

void p0()
{
	While (true)
	{
		flag[0]=true;
		turn=1;
		While (flag[1]&&turn==1); /*do nothing*/
		<critical section>;
		flag[0]=false;
		<remainder>;
	}
}

<Peterson의 알고리즘>

5.3.3 n 프로세스 간의 상호배제를 위한 소프트웨어 기법들

: Dekker나 Peterson이 소개한 알고리즘들은 두 프로세스 사이의 상호배제를 해결한 것이지만, 실제로는 그보다 많은 다수의 프로세스들 사이에서의 상호배제가 요구될 가능성이 높다. 따라서, n개의 프로세스들을 대상으로 하는 상호배제 기법을 살펴보자.

● Dijkstra의 알고리즘

: 무한대기 발생 가능

● Knuth의 알고리즘

: 지연 시간이 커지는 단점

● Eisenberg와 Mcguire의 알고리즘

: 유한 시간 내에 임계영역으로의 진입 보장

● Lamport의 알고리즘 (=베이커리(Bakery) 알고리즘)

: 원래는 분산 시스템용으로 소개되었으나, n개의 프로세스들을 대상으로 하는 상호배제 기법으로도 유용

  • 상호배제가 요구되는 n개의 프로세스에서 임의의 프로세스 i를 위한 프로그램
  • 모든 number 값과 choosing 값은 0과 false로 초기화되어 있고, 임계영역의 진입을 원하는 프로세스 i는 먼저 number 값이 주어지는데 다른 프로세스들이 이미 받은 값보다 더 큰 값을 받게 된다.
  • for문 안에서 첫 번째 while문은 번호 값을 받는 중인 프로세스가 있다면 그 값도 비교해야 하기 때문에 기다리는 것이며, 두 번째 while문에서 자신의 값이 가장 작을 때 임계영역의 진입이 허가되는 것
  • 임계영역을 벗어난 후 자신의 값을 0으로 해줌으로써 차례를 기다리는 다른 프로세스들에게 임계영역의 진입 기회를 주게 되는데, 번호 값에 의해 차례가 정해지므로 특정 프로세스의 무한 대기는 걱정하지 않아도 된다.
do{
	choosing[i]=true;
	number[i]=max(number[0], number[1],...,number[n-1])+1;
	choosing[i]=false;
	for(j=0; j<n; j++)
	{
		while (choosing[j]);
		while ((number[j]!=0)&&((number[j],j)<(number[i],i)));
	}
	<critical section>;
	number[i]=0;
	<remainder>;
} while(1);

<Lamport의 베이커리 알고리즘>

⇒ 지금까지 설명한 소프트웨어 기법들은 운영체제의 특별한 지원 없이, 프로세스 간 협력을 통해 상호배제를 실현하는 것이므로 실행시의 부하가 크며, 실수도 인한 오류의 가능성도 높다.

*바쁜 대기(Busy Wait) 또는 스핀락(Spinlock)

: 임계영역의 중복을 막기 위해 while문을 계속 맴돌며 CPU를 낭비하는 것

*동기화(Synchronization) (=“차례대로”의 의미)

: 임계영역에 대한 실행 “한 번에 한 프로세스만 하도록” 하는 것

 

5.4 상호배제를 위한 하드웨어 기법들

5.4.1 인터럽트 금지를 사용한 기법

단일처리 시스템의 경우, CPU를 뺏기지 않도록 하면 상호배제 가능!

⇒ 즉, 시간 종료나 우선순위 등에 의해 CPU를 뺏길 수 있는 인터럽트를 임계영역의 실행을 완료할 때까지 발생하지 않도록 하면 된다.

 

<장점>

  • 간단하게 구현 가능

<단점>

  • 임계영역의 처리 동안 모든 종류의 인터럽트를 금지해

시스템의 효율적 운영 방해 가능성 多

  • 다중처리 시스템에서는 해결 X (인터럽트 금지는 처리기 단위이기 때문)
While (true) do
	.
	.
	Interrupt Disable;
	<critical section>;
	Interrupt Enable;
	.
	.
endwhile;

<인터럽트 금지를 사용한 상호배제>

 

5.4.2 하드웨어 명령어를 사용한 기법

기본적으로 하드웨어 자체는 특정 메모리 주소에 대한 접근을 한 번에 하나의 요청으로 제한하고 있다. 메모리의 같은 위치에 대한 읽기와 쓰기, 또는 읽기와 검사와 같은 일을 한 명령어 사이클 동안 처리해주는 명령어가 있다면, 이런 종류의 기계명령어는 다른 접근 요청이 차단된 가운데, 원자적(Atomic)으로, 실행 동안 끊기지 않고(Indivisible) 완료될 수 있다.

  • 기계명령어로 잘 알려져 있는 testandset과 exchange(=swap) 명령어
boolean testandset(boolean &target)
{
boolean rv=target;
target=true;
return rv;
}
void exchange (boolean &r, boolean &m)
{
boolean temp=r;
r=m;
m=temp;
}

<testandset과 exchange 명령어>

  • 이해를 돕기 위해 함수 또는 프로시저의 형식으로 표현되어 있으나, 실제로는 기계명령어로서 원자적으로 실행 도중 끊김 없이 완료되는 연산이다.
const int n=...; /* 프로세스 개수 */
boolean lock;
void P(int i);
{
	while(true)
	{
		while(testandset(lock));
		<critical section>;
		lock:=false;
		<remainder>;
	}
}
void main()
{
	lock:=false;
	parbegin
		P(1), P(2),...,P(n);
	parend;
}

(a)

const int n=...; /* 프로세스 개수 */
boolean lock;
void P(int i);
{
	while(true)
	{
		key:=true;
		while(key=true) do exchange(key, lock);
		<critical section>;
		lock:=false;
		<remainder>;
	}
}
void main()
{
	lock:=false;
	parbegin
		P(1), P(2),...,P(n);
	parend;
}

(b)
<testandset과 exchange를 사용한 상호배제>

 

<장점>

  • 간단하고, 다중처리 시스템에서도 쉽게 사용 가능
  • 한 프로그램 내에서 서로 다른 변수를 사용하여 여러 개의 임계영역도 지원할 수 있음

<단점>

  • 여전히 바쁜 대기를 함
  • 차례가 정해지지 않아, 기아를 겪을 수도 있음
  • 임계영역 실행 중 높은 우선순위의 프로세스에게 CPU를 뺏길 경우, 교착 상태 빠질 수 있음

 

5.5 세마포어(Semaphore)

  • Dijkstra가 1965년에 제안한 개념인 세마포어는 세 개의 특수한 명령들만 접근할 수 있게 허용되는 보호된 변수로서, 앞서 다루었던 것들보다 더 높은 수준에서 상호배제를 구현 가능
  • 세마포어는 그 변수가 가질 수 있는 값의 범위에 따라,
  • 이진(Binary) 세마포어: 이진값 (0 또는 1)
  • 계수(Counting) 혹은 정수(Integer) 세마포어: 음이 아닌 모든 정수
  • 세마포어를 위한 특수한 명령들 = 비분리(Indivisible) 명령들

: 세마포어 값을 초기화하는 명령, P 명령, V 명령

  • P 명령: wait 또는 down 이라는 이름을 사용하기도 함
  • V 명령: signal 또는 up 이라는 이름을 사용하기도 함
  • 책들에 따라 초기화 명령을 따로 두지 않고 Semaphore 변수로 선언한 다음 초기 값을 부여하는 방식을 쓰기도 하고, P와 V 명령은 각각 wait와 signal 또는 down과 up이라는 이름으로 사용하기도 한다.

  • P(S): if (S>0) then S = S - 1;
  • **else** S > 0 조건이 만족될 때까지 큐에서 대기;
  • V(S): if (큐에서 대기 중인 프로세스들이 존재)
  • **then** 그 중의 한 프로세스를 준비 또는 실행 상태로 만듦; **else** S = S + 1;

  • P 명령은 S값이 0보다 크면 단순히 S값을 1 감소시키는 작업을 하지만, 그렇지 않으면 S값이 0보다 크게 되기를 기다리게 된다.
  • V 명령에서는 S가 0보다 크게 되기를 기다리는 프로세스가 있으면, 그들 중 하나의 프로세스가 수행되고, 만약 그러한 프로세스가 존재하지 않는다면 단순히 S의 값을 1만큼 증가시키는 작업만 한다.
  • 세마포어에 대한 명령들은 각각 분리되지 않고 수행될 수 있도록 구현해야 하며, 같은 세마포어에 대해서는 동시에 시행될 수 없다.
const int n= /* 프로세스의 수*/;
semaphore s=1;
void p(int i)
{
	while (true)
	{
		P(s);
		<critical section>;
		V(s);
		<remainder>;
	}
}
void main()
{
	parbegin
		p(1), p(2),...,p(n);
	parend
}

<세마포어를 사용한 상호배제>

 

⇒ P와 V 명령의 정의에 따라 한 번에 하나의 프로세스만이 임계영역에 들어갈 수 있음을 알 수 있다.

  • 세마포어는 동기화(Synchronization), 상호배제(Mutual Exclusion), 자원관리(Resource Management)까지 활용 범위가 넓다.

예) 상호배제→위의 <세마포어를 사용한 상호배제> 코드

     자원관리→프린트 풀(Pool)

     동기화=”서로를 의식하고 실행의 보조를 맞추다.”

→ P0는 P1이 데이터를 생성하여 주면 받아 계속 실행을 하는 경우

  • 세마포어는 시스템에서 바로 주어지는 도구가 아닐 경우가 많다. 결론적으로 말하면, 쓰임새는 많지만, 세마포어를 선언할 수 있는 상황이 아니거나 P,V라는 operation을 사용할 수 없는 상황이라면 이것을 다른 도구를 사용하여 impliment 해야 한다.

'CS > 운영체제' 카테고리의 다른 글

Chapter 07 메모리 관리  (0) 2023.05.26
Chapter 6. 교착 상태(Deadlock)  (0) 2023.05.22
Chapter 4. CPU 스케줄링  (1) 2023.04.12
Chapter3. 프로세스와 스레드  (0) 2023.04.07
Chapter2. 들어가기 전에 (인터럽트, I/O 방식)  (0) 2023.03.22