#메시지 패싱(message passing)

메시지 패싱을 이용한 프로세스간 통신은 send와 receive 두 가지 프리미티브를 사용한다. 이들은 일종의 시스템 호출이다.

send(destination, &message) : 메시지를 명시된 목적지에 전송

receive(source, &message) : 명시된 정보원으로부터/명시되어있지않으면 누구로부터든지 메시지를 수신한다. 전달될 메시지가 없으면 수신자는 메시지가 도착할 때까지 대기하거나, 오류 코드와 함께 즉시 복귀할 수도 있다.

메시지 전달 시스템의 설계 쟁점

메시지는 네트워크 상에서 사라질 수 있다.

메시지 분실에 대처하기 위해서는 메시지가 수신되자마자 수신자는 특별한 응답(acknowledgement) 메시지를 전송하여 송신자와 수신자가 서로 합의에 도달할 수 있어야 한다. 만약 송신자가 지정된 시간동안 응답 메시지를 받지 못하면 메시지를 재전송한다.

* 메시지가 제대로 수신되었으나 송신자에게 보낸 응답 메시지가 사라지면

-> 송신자는 메시지를 재전송한다.

-> 수신자는 같은 메시지를 두 번 받는다. 수신자는 이 메시지가 재전송된 메시지인지 새로운 메시지인지 구분해야한다.

=> 각 새로운 메시지마다 '순차 번호(sequence number)'를 부여한다. 수신자가 이전 메시지와 동일한 순차 번호를 가진 메시지를 받는다면 중복된것임을 인식하고 무시할 수 있다.

 

메시지 전달을 이용한 생산자-소비자 문제

#define N 100		// 버퍼에 존재하는 슬롯의 수

void producer(void){
    int item;
    message m;
    
    while(TRUE){
        item = produce_item();
        receive(consumer, &m);		// 수신자로부터 빈 메시지 전송받음
        build_message(&m, item);	// 아이템을 담은 메시지 생산
        send(consumer, &m);
    }
}

void consumer(void){
    int item, i;
    message m;
    
    for(i=0; i<N; i++)
        send(producer, &m);			// 생산자에게 N개의 빈 메시지 전송
    while(TRUE){
        receive(producer, &m);
        item = extract_item(&m);
        send(producer, &m);			// 아이템을 뺀 빈 메시지 생산자에게 전송
        consume_item(item);
    }
}

- 모든 메시지는 동일한 크기를 가짐.

- 전송되었으나 아직 수신되지 않은 메시지는 운영체제에 의해 자동으로 버퍼링 됨

- 소비자는 시작할 때 N개의 빈 메시지를 생산자에게 보냄.

- 생산자는 소비자에게 전달할 아이템을 생산하면 빈 메시지를 수신하고 아이템이 들어있는 메시지를 전송

=> 시스템 내부의 전체 메시지 개수는 항상 일정. 메시지들은 사전에 할당된 메모리에 저장될 수 있음.

 

이러한 메시지 전달에는 다양한 변형이 가능.

1. 프로세스에게 별도의 고유 주소를 할당, 이 프로세스에게 전송되는 메시지에 수신 프로세스의 주소를 기입하는 것

2. 메일박스(mailbox)라는 새로운 자료구조를 사용. 프로세스의 주소 대신 메일박스의 주소를 사용.

    ex1 ) 생산자-소비자 문제에서 생산자와 소비자 모두 N개의 메시지를 저장할 수 있는 메일박스 생성. 생산자는 소비자의 메일박스에 실제 데이터가 담긴 메시지를 전달, 소비자는 생산자의 메일박스에 빈 메시지를 전달. 목적지 메일박스는 목적지 프로세스에게 전송되었으나 그 프로세스가 아직 수신하지 않은 모든 메시지를 보관한다.(버퍼링)

    ex2 ) 전혀 버퍼링을 하지 않는 방식도 존재. receive 이전에 send가 호출되면 전송하려는 프로세스는 receive가 호출될 때까지 대기한다. receive가 호출되면 메시지는 중간 단계의 버퍼링 없이 송신자에서 수신자로 직접 복사된다. 이러한 기법을 '랑데뷰'라 부른다. 버퍼링보다 구현하기 쉽지만, 송신자와 수신자가 반드시 단계적으로 수행되어야하므로 버퍼링 기법에 비해 유연하지 못하다.

 

# 장벽(Barrier)

프로세스들의 그룹을 대상으로 하는 동기화 메커니즘.

한 프로세스가 장벽에 도착하면 다른 모든 프로세스가 장벽에 도착할 떄까지 이미 도착한 프로세스들은 대기한다.

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

스케줄링 알고리즘의 분류  (0) 2019.11.28
스케줄링 알고리즘  (0) 2019.11.27
슬립과 깨움, 세마포어, 뮤텍스, 모니터  (0) 2019.11.25
경쟁조건, 상호배제  (0) 2019.11.25
프로세스 관리_1  (0) 2019.11.18

#작성 코드

#include <cstdio>
#include <cstdlib>

// 모두 비워진 스도쿠 판 준비 
int sdoku[9][9];

// 행, 열, 3x3블록에 각 숫자가 존재하는지 체크할 bool 배열
bool checkR[9][10];
bool checkC[9][10];
bool checkS[9][10];

int zeros[81];
int n;

int fx(int i, int j){
	return i/3*3+j/3;
}

void solve(int idx){
    if( idx==n ){
        for(int i=0; i<9; i++){
            for(int j=0; j<9; j++){
                printf("%d ", sdoku[i][j]);
            }
            printf("\n");
        }
        exit(0);
    }
	int x = zeros[idx]/9;
	int y = zeros[idx]%9;
	
	for(int num=1; num<10; num++){
		// 행, 열, 3x3블록에 해당 숫자가 존재하면 다음 수로 넘어간다  
		if( checkR[x][num] || checkC[y][num] || checkS[fx(x, y)][num] )
			continue;
		checkR[x][num] = checkC[y][num] = checkS[fx(x, y)][num] = true;
		sdoku[x][y] = num;
		solve(idx+1);		// dfs 
		sdoku[x][y] = 0;	// 백트래킹 
		checkR[x][num] = checkC[y][num] = checkS[fx(x, y)][num] = false;
	}
}

int main()
{
	for (int i=0; i<9; i++) {
        for (int j=0; j<9; j++) {
            scanf("%d", &sdoku[i][j]);
            int tmp = sdoku[i][j];
            if (!tmp) zeros[n++] = i*9+j;
            else {
                checkR[i][tmp] = true;
                checkC[j][tmp] = true;
                checkS[fx(i,j)][tmp] = true;
            }
        }
    }
    solve(0);
    return 0;
}

##

 

'BOJ' 카테고리의 다른 글

BOJ 2748번 :: 피보나치수 2  (0) 2019.11.28
BOJ 14888번 :: 연산자 끼워넣기  (0) 2019.11.28
BOJ 9663번 :: N-Queen  (0) 2019.11.26
BOJ 15652번 :: N과 M(4)  (0) 2019.11.25
BOJ 15651번 :: N과 M(3)  (0) 2019.11.25

#작성 코드

#include <iostream>
#include <cstdio>
using namespace std;

int N;
int count;			// 퀸을 놓을 수 있는 방법의 수 
bool col[15];		// 인덱스 : 열의 번호. 이 열에 퀸이 존재하는가. 
bool *dup;
bool *ddown;

void findQueen(int n){
	if( n==N ){		// n은 행을 가리킴. 0~n-1까지 조사 완료했으면 
		count++;	// count 증가 
	}
	else{
		for(int i=0; i<N; i++){
			if( col[i] ) continue;
			if( dup[n+i] || ddown[N-1+n-i]) continue;
			col[i] = dup[n+i] = ddown[N-1+n-i] = true;
			findQueen(n+1);
			col[i] = dup[n+i] = ddown[N-1+n-i] = false;
		}
	} 
}

int main(){
	scanf("%d", &N);
	dup = new bool[2*N-1];
	ddown = new bool[2*N-1];
	
	findQueen(0);
	
	printf("%d", count);		// 퀸을 놓을 수 있는 방법의 수 출력 
	delete[] dup;
	delete[] ddown;
	return 0;
}

아래는 (n, i)위치에 퀸을 배치할 수 있는지 체크하는 함수를 사용한 코드인데, 아직 완성하지 못했다.

더보기
#include <iostream>
using namespace std;

int N;
int count;
bool *arr;
bool *dup;
bool *ddown;

//bool promising(int n, int i){
//	bool flag = true;
//	int k=0;
//	while( k<i && flag ){
//		if( arr[k]==true ) flag = false;
//		if( dup[n+k] == true || ddown[N-1+n-k]==true ) flag = false;
//	}
//	return flag;
//}

void findQueen(int n){
	if( n>=N ) count++;
	  
	for(int i=0; i<N; i++){		// (n, x)에 퀸을 배치할수있는지 체크
		if( arr[i]==true ) continue;
		if( dup[n+i] == true || ddown[N-1+n-i]==true ) continue;
		arr[i] = dup[n+i] = ddown[N-1+n-i] = true;
		findQueen(n+1);
		arr[i] = dup[n+i] = ddown[N-1+n-i] = false;
	}
}

int main(){
	ios_base::sync_with_stdio(0);
	cin.tie(0);
	cin>>N;
	
	arr = new bool[N];
	dup = new bool[2*N-1];
	ddown = new bool[2*N-1];
	
	findQueen(0);
	cout<<count;
	
	delete[] arr;
	delete[] dup;
	delete[] ddown;
	return 0;
}

##

findQueen 함수 내에서 현재 위치가 퀸을 놓기에 적절한 위치인지 promising함수로 체크하려고 했는데 이렇게 하면 (n, i)를 체크하면서 i에 따라 인덱스 0부터 i까지 체크하게 돼서 비효율적이다.

/ 방향 대각선은 행과 열의 합으로 구분할 수 있고,

\ 방향 대각선은 행-열이 n=3인 경우에 2, 1, 0, -1, -2 로 나타나므로 n-1을 더해주면 4, 3, 2, 1, 0으로 표현할 수 있다.

 

'BOJ' 카테고리의 다른 글

BOJ 14888번 :: 연산자 끼워넣기  (0) 2019.11.28
BOJ 2580번 :: 스도쿠  (0) 2019.11.27
BOJ 15652번 :: N과 M(4)  (0) 2019.11.25
BOJ 15651번 :: N과 M(3)  (0) 2019.11.25
BOJ 15650번 :: N과 M(2)  (0) 2019.11.24

Peterson의 해법과 TSL, XCHG를 사용한 해결책 모두 문제를 해결하지만 바쁜 대기를 필요로 한다는 단점이 있다.

* 우선순위 역전 문제(priority inversion problem)

높은 우선순위를 가진 프로세스 H, 낮은 우선순위를 가진 프로세스 L이 있다. 특정 순간에 L이 임계구역에 진입하고 나서 H가 실행이 가능한 준비 상태가 되어 H가 수행되면서 임계구역에 진입하기 위해 바쁜 대기를 수행한다. H가 수행중이므로 L은 스케줄 되지 않아서 임계구역을 빠져나올 수 없다. 따라서 H는 무한루프에 빠진다.

 

#슬립과 깨움

sleep : 호출자를 블록 상태로 만드는 시스템 호출. 다른 프로세스가 호출자를 깨워줄 때까지 블록 상태에 머문다.

wakeup : 깨울 프로세스를 가리키는 인자 하나를 가지고 있다.

 

* 생산자 소비자 문제

생산자와 소비자 두 프로세스가 고정된 크기의 버퍼를 공유한다.

생산자 : 정보를 버퍼에 저장

소비자 : 버퍼에서 정보를 꺼내온다

생산자가 새로운 아이템을 버퍼에 넣으려고 하는데 버퍼가 가득 차 있을 때 문제 발생

-> 생산자가 잠들고 소비자가 아이템을 하나 제거할 때 생산자를 깨워준다.

소비자가 아이템을 버퍼에서 가져오려는데 버퍼가 비어있을 때 문제 발생

-> 소비자는 잠들고 생산자가 버퍼에 아이템을 넣을 때 소비자를 깨워준다.

#define N 100
int count = 0;	// 버퍼 내 아이템의 개수

void producer(void){
	int item;
    
    while(TRUE){
    	item = produce_item();
        if(count==N) sleep();	// 버퍼에 아이템 가득차있으면 sleep
        insert_item(item);		// 버퍼에 아이템을 삽입
        count = count+1;
        if( count==1 ) wakeup(customer);	// 아이템이 하나 있으면 소비자 깨움
    }
}

void customer(void){
	int item;
    
    while(TRUE){
    	if( count==0 ) sleep();	// 버퍼에 아이템이 없으면 sleep
        item = remove_item();	// 버퍼에서 아이템 꺼냄
        count = count-1;
        if( count==N-1 ) wakeup(producer);	// 버퍼에 빈 공간 생기면 생산자 깨움
        consume_item(item);
    }
}

! count에 대한 접근에 제한이 없기 때문에 경쟁조건이 발생한다.

ex) 버퍼가 비어있고, 소비자가 count값을 읽어서 0임을 알았다. 이 순간 스케줄러는 소비자의 '수행을 중지'시키고 생산자를 수행하기 시작한다. 생산자는 아이템을 버퍼에 추가하고 count를 증가시켜 1이 되었다. 이전에 count값이 0이었으므로 생산자는 소비자가 sleep상태일 것이라고 생각하여 wakeup(consumer)를 실행한다.

불행하게도 소비자는 아직 잠든 상태가 아니므로 wakeup(customer)실행 이후에 소비자의 sleep()이 실행되어 wakeup시그널이 사라지고 소비자는 영원히 잠들게 된다. 소비자 프로세스의 수행이 없으므로 생산자에 의해 버퍼가 가득차게 되어 생산자도 잠들게 된다.

이 문제의 본질은 아직 잠들지 않은 프로세스에 wakeup 시그널을 전송하면 소실된다는 것이다.

wakeup 시그널 소실을 막기 위해 wakeup waiting 비트를 추가하여 응급처치를 할 수 있지만 근본적인 문제는 남아있다.

 

# 세마포어

E.W. Dijkstra가 제안한 개념. 정수 변수를 사용하여 미래를 위해 미리 호출한 wakeup 회수를 저장할 것을 제안함.

세마포어(semaphore) : wakeup이 저장되지 않은 0값 또는 하나 이상의 wakeup이 대기 중인 양수 값을 가질 수 있다.

sleep과 wakeup을 일반화 한 down연산과 up연산

down 연산 : 값이 0보다 큰지를 검사한다. 0보다 크면 이 값을 감소시키고(저장된 wakeup 하나 소비) 수행을 계속한다. 만약 이 값이 0이면 프로세스는 down의 수행을 완료하지 않고 즉시 잠들게 된다.

up 연산 : 세마포어의 값을 증가시킨다. 하나 또는 그 이상의 프로세스가 down연산을 완료할 수 없어서 세마포어에서 잠들어있으면 이 중 한 프로세스가 시스템에 의해 선택되여 down 수행을 완료할 수 있다. 프로세스가 잠들어있는 세마포어에 대해 up을 수행하면 세마포어 값은 여전히 0이지만 잠들어 있는 프로세스의 개수는 하나가 감소한다.

-값을 검사하고, 변경하고, 경우에 따라 잠드는 이러한 모든 동작들은 분할할 수 없는 하나의 원자적 행위(atomic action).

세마포어 연산이 시작되면 원자성이 보장되어야 한다. 이 연산이 완료되거나 프로세스가 잠들 때까지 다른 프로세스가 세마포어에 접근할 수 없도록 보장되어야 한다. 원자성은 동기화 문제를 해결하고 경쟁조건 방지를 위해 꼭 필요하다.

* 세마포어를 이용한 생산자-소비자 문제의 해결

#define N 100
typedef int semaphore;
semaphore mutex = 1;
semaphore full = 0;
semaphore empty = N;

void producer(void){
	int item;
    
    while(TRUE){
    	item = produce_item();
        down(&empty);				// 버퍼의 빈 자리수--
        down(&mutex);				// 임계구역에 진입
        insert_item(item);			// 버퍼에 아이템 삽입
        up(&mutex);				// 임계구역에서 나옴
        up(&full);				// 아이템의 개수++
    }
}

void consumer(void){
	int item;
    
    while(TRUE){
    	down(&full);			// 아이템의 개수--
        down(&mutex);			// 임계구역 진입
        item = remove_item();		// 버퍼에서 아이템 꺼냄
        up(&mutex);			// 임계구역에서 나옴
        up(&empty);			// 버퍼의 빈 자리수++
        consume_item(item);
    }
}

세 개의 세마포어 full, empty, mutex를 사용한다.

full : 아이템으로 채워진 슬롯의 개수. 0으로 초기화됨

empty : 빈 슬롯의 개수. 버퍼에 있는 슬롯의 개수로 초기화됨.

mutex : 생산자와 소비자가 동시에 버퍼에 접근하지 못하도록 한다. 1로 초기화됨.

각 프로세스가 임계구역에 진입할 때 down을 수행하고 임계구역을 나오면서 up을 부르면 상호배제가 이루어진다.

Mutex 세마포어는 상호배제를 위해 사용되었다. 한 번에 하나의 프로세스만 버퍼나 관련된 변수를 읽고 쓰도록 보장하는 용도로 사용된다.

Full과 empty세마포어는 동기화(Synchronization)를 위해 사용되었다. 특정 이벤트 순서들이 발생하거나 발생하지 않도록 보장하기 위해 필요하다. ex) 버퍼가 가득 찬 경우 생산자가 정지하도록 하는 것, 버퍼가 빈 경우 소비자가 정지하도록 하는 것을 보장한다.

 

# 뮤텍스

세마포어의 개수를 세는 능력이 필요 없는 경우 이진 세마포어의 일종인 뮤텍스(mutex)를 사용한다.

공유 자원이나 코드 일부분에 대해 상호배제를 해야 하는 경우 뮤텍스를 사용.

뮤텍스는 unlock(언락)(0), lock(락)(0이 아닌 다른 값) 두 가지 중 한 상태를 가진다.

뮤텍스와 함께 사용되는 두 개의 프로시듀어 mutex_lock, mutex_unlock

mutex_lock : 스레드/프로세스가 임계구역에 접근할 때 호출.

                 뮤텍스가 현재 언락이면 호출 성공, 임계구역에 진입 가능

                 뮤텍스가 이미 락 상태이면 스레드는 임계구역에 있는 스레드가 수행을 마치고 mutex_unlock 호출할 때까지 블록된다.

mutex_lock:
    TSL REGISTER, MUTEX	// MUTEX값 레지스터에 복사, MUTEX 1로 설정
    CMP REGISTER, #0	// 이전 MUTEX 값 0인지 비교.
    JZE ok		// 이전 MUTEX 값 0이었으면 임계구역 진입
    CALL thread_yield	// 임계구역에 진입하지 못하면 다른 스레드에게 cpu 양보
    JMP mutex_lock
ok: RET

mutex_unlock:
    MOVE MUTEX, #0
    RET

mutex_lock은 락을 획득하지 못하면 thread_yield를 호출하여 다른 스레드에게 cpu를 양보한다. => 바쁜 대기 존재x

mutex_lock과 mutex_unlock 모두 커널을 호출하지 않는다.

* Pthread에서 뮤텍스

Pthread는 스레드를 동기화하기 위해 사용할 수 있는 다수의 함수들을 제공한다.

Thread Call Description
Pthread_mutex_init 뮤텍스 생성
Pthread_mutex_destroy 뮤텍스 파괴
Pthread_mutex_lock 락 설정, 이미 설정되어있으면 대기
Pthread_mutex_trylock 락 설정, 이미 락이 설정되어있으면 오류코드와 함께 복귀
Pthread_mutex_unlock 뮤텍스 언락

Pthread는 뮤텍스 이외에도 '조건변수'라는 또 다른 동기화 기법 제공.

조건변수는 어떤 조건이 만족되지 않으면 스레드의 수행을 대기시킨다.

Thread Call Description
Pthread_cond_init 조건변수 생성
Pthread_cond_destroy 조건변수 파괴
Pthread_cond_wait 다른 스레드가 시그널 보낼때까지 블록(대기한다)
Pthread_cond_signal 다른 스레드에 시그널 보내서 깨운다.
Pthread_cond_broadcast 다수의 스레드에 시그널 보내서 깨운다

조건 변수와 뮤텍스는 항상 같이 사용된다.

ex) 어떤 스레드가 뮤텍스를 락하고 자신이 필요한 무엇인가가 만족될 떄까지 조건변수에서 대기한다.

! 조건변수는 과거를 기억해두지 않는다. 블록된 스레드가 없는 조건 변수에 대해 시그널을 보내면 이 시그널은 그냥 사라진다.

#include <stdio.h>
#include <pthread.h>
#define MAX 1000000000

pthread_mutex_t the_mutex;
pthread_cond_t condc, condp;		//소비자, 생산자 조건변수
int buffer=0;

void *producer(void *ptr){
	int i;
    
    for(i=1; i<=MAX; i++){
    	pthread_mutex_lock(&the_mutex);		// 버퍼 접근 상호배제를 위해
        while(buffer!=0)
        	pthread_cond_wait(&condp, &the_mutex);	// 소비자가 아이템 소비할때까지 대기
        buffer=i;		// 버퍼에 아이템 삽입
        pthread_cond_signal(&condc);	// 소비자 깨움
        pthread_mutex_unlock(&the_mutex);
    }
    pthread_exit(0);
}

void *consumer(void *ptr){
	int i;
    
    for(i=1; i<=MAX; i++){
    	pthread_mutex_lock(&the_mutex);		// 버퍼 접근 상호배제를 위해
        while(buffer==0)
        	pthread_cond_wait(&condc, &the_mutex);	// 생산자가 아이템 생산할때까지 대기
        buffer=0;		// 버퍼에서 아이템 꺼냄
        pthread_cond_signal(&condp);	// 생산자 깨움
        pthread_mutex_unlock(&the_mutex);
    }
    pthread_exit(0);
}

int main(int argc, char**argv){
	pthread_t pro, con;
    
    pthread_mutex_init(&the_mutex, 0);
    //조건변수 생성
    pthread_cond_init(&condc, 0);
    pthread_cond_init(&condp, 0);
    //새로운 스레드 생성
    pthread_create(&con, 0, consumer, 0);
    pthread_create(&pro, 0, producer, 0);
    
    pthread_join(pro, 0);		// producer 스레드 끝날때까지 대기
    pthread_join(con, 0);		// consumer 스레드 끝날때까지 대기
    //조건변수 파괴
    pthread_cond_destroy(&condc);
    pthread_cond_destroy(&condp);
    
    pthread_mutex_destroy(&the_mutex);
}

 

#모니터

모니터(Monitor) -> 단 하나의 프로세스만 한 순간에 모니터에서 활동할 수 있다.

프로세스가 모니터 프로시듀어를 호출하면 프로시듀어의 최초 명령 몇 개가 다른 프로세스가 이미 모니터에서 활동 중인지를 검사한다. 만약 다른 프로세스가 모니터에서 활동 중이라면 호출한 프로세스는 다른 프로세스가 모니터에서 나올 때까지 중단(suspend)된다.

임계구역을 모두 모니터의 프로시듀어 형태로 만들면 동시에 두 개의 프로세스가 임계구역에서 실행할 수 없다는 것만 이해하는 것으로 상호배제를 달성할 수 있다.

모니터 + 조건변수 + 연산(wait, signal) => 프로세스가 진행할 수 없을 때 대기할 수 있는 방법 구현

모니터 프로시듀어가 더 이상 진행할 수 없음을 인식하면 어떤 조건 변수에 대해 wait를 수행한다. 이 때 호출한 프로세스를 대기하게 만들고, 현재 모니터 진입을 금지당하고 있는 다른 프로세스가 모니터에 진입할 수 있게 만든다.

두 개의 프로세스가 모니터에서 동시에 활동하는 것을 피하기 위해 signal 이후에 어떻게 수행할지에 대한 규칙

1. Hoare : 기존 프로세스를 중단시키고 새로 깨어난 프로세스가 수행되어야 함

2. Brinh Hansen : signal을 수행한 프로세스는 즉시 모니터를 나간다.

3. 제 3의 해법 : signal을 보낸 프로세스가 계속 수행하도록 하고 이 프로세스가 모니터를 빠져나간 후 대기 중에 깨어난 프로세스가 실행하도록 한다.

monitor ProducerConsumer
    condition full, empty;
    integer count;
    
    procedure insert(item:integer);
    begin
        if count=N then wait(full);		//signal(full) 올때까지 대기(블록)
        insert_item(item);
        count:=count+1;
        if count=1 then signal(empty);		//소비자 깨움
    end;
    
    function remove:integer;
    begin
        if count=0 then wait(empty);		//signal(empty) 올때까지 대기(블록)
        remove = remove_item;
        count:=count-1;
        if count=N-1 then signal(full);		//생산자 깨움
    end;
    
    count:=0;
end monitor;

procedure producer;
begin
    while true do
    begin
        item = produce_item;
        ProducerConsumer.insert(item);
    end
end;

procedure consumer;
begin
    while true do
    begin
        item = ProducerConsumer.remove;
        consume_item(item);
    end
end;

 

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

스케줄링 알고리즘의 분류  (0) 2019.11.28
스케줄링 알고리즘  (0) 2019.11.27
메시지 패싱, 장벽  (0) 2019.11.27
경쟁조건, 상호배제  (0) 2019.11.25
프로세스 관리_1  (0) 2019.11.18

# 경쟁조건(Race Condition)

둘 또는 그 이상의 프로세스가 공유 데이터를 읽거나 기록하는데 최종 결과는 누가 언제 수행되는가에 따라 달라지는 상황.

 

# 경쟁조건을 회피하는 방법

상호배제(Mutual exclusion) : 한 프로세스가 공유 변수나 파일을 사용 중이면 다른 프로세스들은 똑같은 일을 수행하지 못하도록 하는 것.

임계구역(Critical region / Critical section) : 공유 메모리를 접근하는 프로그램 부분.

만약 두 프로세스가 동시에 임계구역에 존재하지 않도록 조절하면 경쟁조건을 피할 수 있다.

* 좋은 경쟁조건 회피 해결책의 조건

1. Mutual Exclusion : 두 개의 프로세스가 동시에 임계구역 내에 존재하는 경우x

2. No preemption : cpu의 개수, 속도에 대해 어떤 가정도 하지x

3. Progress : 임계구역 외부에서 실행하고 있는 프로세스는 다른 프로세스들을 블록시키면x

4. Bounded Waiting : 임계구역 진입을 위해 무한정 기다리는 프로세스는 없어야 한다.

 

# 바쁜 대기를 이용한 상호배제

1. 인터럽트 끄기

각 프로세스가 임계구역에 진입하자마자 인터럽트를 끄고, 임계구역에서 나가기 직전에 인터럽트를 켜는 것.

cpu는 클록이나 다른 인터럽트의 결과로 프로세스간 문맥을 교환하므로 인터럽트를 끄면 cpu는 다른 프로세스로 문맥을 교환하지 않으므로 다른 프로세스의 방해 없이 공유 메모리를 검사하고 변경할 수 있다.

! 사용자 프로세스에게 인터럽트를 끌 수 있는 권한을 주는 것은 좋지 않은 방법이다.

-> 운영체제 내부에서 사용할 수 있는 유용한 기법이지만 사용자 프로세스간 상호배제를 위해 사용하기에는 적절하지 않다.

 

2. 락 변수

0으로 초기화된 단일 공유 변수(락 변수)를 둔다. 임계구역에 진입하려는 프로세스는 먼저 락을 테스트한다.

락이 0이면 프로세스는 1으로 설정하고 임계구역에 진입한다.

락이 이미 1이라면 프로세스는 이 변수가 0이 될 때까지 임계구역에 진입하지 않고 대기한다.

-> 락 변수가 0이라는 것 : 임게구역 내에 어떤 프로세스도 없다는 뜻.

! 락 변수를 사용해도 경쟁조건이 발생할 수 있다. @첫번째 프로세스가 두 번째 검사를 끝낸 직후에 두 번째 프로세스가 락을 변경하면 경쟁조건 발생.

ex) 한 프로세스가 락을 읽고 이 값이 0임을 알았다. 락을 1로 설정하기 전에 다른 프로세스가 스케줄되어 실행하면서 락을 1로 설정하고 임계구역 내에 진입하면 두 개의 프로세스가 동시에 임계구역에 진입하는 문제가 발생한다.

 

3. 엄격한 교대

// 프로세스 0
while(TRUE){
	while(turn != 0) ;		// turn이 0이 아니면(프로세스0의 차례가 아님) 0이 될때까지 바쁜 대기
    critical_region();
    turn = 1;				// 임계구역에서 빠져나오면서 다른 프로세스로 turn 넘겨줌
    noncritical_region();
}

// 프로세스 1
while(TRUE){
	while(turn != 1) ;		// turn이 1이 아니면(프로세스1의 차례가 아님) 1이 될때까지 바쁜 대기
    critical_region();
    turn = 0;				// 임계구역에서 빠져나오면서 다른 프로세스로 turn 넘겨줌
    noncritical_region();
}

초기값이 0인 변수 turn : 임계구역에 진입하여 공유 메모리를 검사하고 변경할 순번을 나타냄.

각 프로세스는 이 값을 검사하여 turn이 자기 차례가 될 때까지 while 루프를 돌면서 대기한다.

바쁜 대기(Busy waiting) : 변수가 특정 값이 될 떄까지 계속해서 검사하는 것. cpu시간을 낭비하므로 바쁜 대기는 피하는 것이 좋다. 기다리는 시간이 짧을 것이라고 합리적으로 예상할 수 있는 경우에만 바쁜 대기가 사용된다.

+) 바쁜 대기를 사용하는 락을 스핀 락(spin lock)이라 부른다.

-> 한 프로세스가 다른 프로세스보다 상당히 느린 경우 바쁜 대기를 사용하는것이 좋은 생각은 아님.

ex) turn이 1이고 두 프로세스 모두 비임계구역에 존재. 갑자기 프로세스 0이 루프의 맨 윗부분을 실행하면 turn이 1이기 때문에 임계구역 진입 불가능하다. 이 때 프로세스 1은 비임계구역에 존재하므로 프로세스 0은 프로세스 1이 turn을 0으로 바꿀 때까지 while 루프에 묶여있어야 한다.

위의 예시에서 프로세스 0은 비임계구역에 존재하는 다른 프로세스에 의해 블록되므로 좋은 경쟁조건 회피책의 조건 3을 위반한다.(임계구역 외부의 프로세스는 다른 프로세스들을 블록시키면 안됨)

=> 엄격한 교대는 경쟁조건을 피할 수는 있지만 경쟁조건 회피 조건 3을 위반하므로 좋은 해결책이라 할 수 없다.

 

4. Peterson의 해법

Dekker의 해법보다 단순한 방법

(1) Dekker의 해법

/* process pi */
flag[i] = TRUE;
while(flag[j]){
	if( turn == j ){
    	flag[i] = FALSE;		// pj의 차례이면 pi는 양보
        while( turn == j );		// pj가 turn을 i로 바꿀때까지 바쁜 대기
        flag[i] = TRUE;
    }
}


// 임계구역
flag[j] = FALSE;
turn = j;

(2) Peterson의 해법

#define FALSE 	0
#define TRUE 	1
#define N 	2		// 프로세스의 개수

int turn;				// 누구의 차례인지
int interested[N];

void enter_region(int process){
	int other;			// 다른 프로세스의 number
    other = 1-process;
    
    interested[process] = TRUE;
    turn = process;
    while( turn==process && intersted[other]==TRUE ) ;
    // 현재 프로세스의 turn이더라도 다른 프로세스가 임계구역에 진입하고싶어하면
    // while을 돌며 다른 프로세스에게 양보
}

void leave_region(int process){
	interested[process] = FALSE;
}

! 두 프로세스가 거의 동시에 enter_region을 호출하는 경우

누구의 변수 값 변경이 늦게 수행되었는지가 중요하다.(처음 값이 다음 변경으로 덮어쓰여짐)

프로세스 1이 마지막에 변경하여 turn이 1일 때, 두 프로세스 모두 while문장에 도착하면 프로세스 0은 루프를 수행하지 않고 임계구역에 진입한다.(프로세스 1이 양보함) 프로세스 1은 루프를 돌면서 임계구역에 진입하지 못하고 대기한다.

 

5. TSL 명령

하드웨어의 도움을 약간 필요로 한다. 다중처리기를 염두에 두고 설계된 몇몇 컴퓨터들은 TSL REGISTER, LOCK 과 같은 명령을 가지고 있다.

TSL(Test and Set Lock)명령은 메모리 워드 LOCK의 값을 읽어 레지스터 REGISTER에 저장하고, 메모리 주소 LOCK에 0이 아닌 값을 기록한다. 워드를 읽고 어떤 값을 저장하는 연산은 분할이 불가능하여 이 연산이 완료될 떄까지 다른 어떤 처리기도 메모리 워드에 접근할 수 없다.

-> TSL 명령을 수행하는 CPU는 명령 수행이 끝날 때까지 메모리 버스를 잠금으로써 다른 어떤 CPU도 메모리에 접근할 수 없도록 한다.

(인터럽트를 끄는 것과는 다름. 한 CPU에서 인터럽트를 끄는 것은 다른 CPU의 접근에 영향을 미치지 않는다.)

TSL을 이용해 두 개의 프로세스가 임계구역에 진입하는 것을 방지하는 방법

enter_region:
    TSL REGISTER, LOCK	// LOCK의 이전 값을 레지스터로 복사, LOCK을 1로 설정
    CMP REGISTER, #0	// LOCK의 이전 값이 0인지 비교
    JNE enter_region	// 0이 아니면 LOCK이 이미 전에 설정되었으므로 TSL명령 다시실행
    RET
    
leave_region:
    MOVE LOCK, #0		// LOCK에 0을 저장한다. = 락 반환
    RET

 

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

스케줄링 알고리즘의 분류  (0) 2019.11.28
스케줄링 알고리즘  (0) 2019.11.27
메시지 패싱, 장벽  (0) 2019.11.27
슬립과 깨움, 세마포어, 뮤텍스, 모니터  (0) 2019.11.25
프로세스 관리_1  (0) 2019.11.18

#작성 코드

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int N, M;
int visit[9];
vector<int> v;

void dfs(int a, int cnt){
	if( cnt == M ){
		for(int i=0; i<v.size()-1; i++){
			if( v[i]>v[i+1] ) return;		//비내림차순만 출력 
		}
		for(int i=0; i<v.size(); i++){
			cout<<v[i]<<' ';
		}
		cout<<'\n';
		return;		// 출력 후에는 dfs함수를 반드시 종료시켜야한다 
	}
	
	for(int i=a+1; i<=N; i++){
		if( visit[i]==M ) continue;	// 최대 M번까지 방문할 수 있도록 
		visit[i]++;
		v.push_back(i);
		dfs(a, cnt+1);
		visit[i]--;
		v.pop_back();
	}
}

int main(){
	cin>>N>>M;
	dfs(0, 0);
	return 0;
}  

##

15651번 N과 M(3)번과 유사하지만 '중복 조합'을 출력해야 했다.

cnt==M일 때 출력하게되는데 이 때 벡터 v에 대해 v[i]>v[i+1]인지 조사하여 비내림차순이 확정되면 출력하고, 아니면 dfs함수를 종료하도록 했다.

조합을 출력하는 15650번과의 다른점은 dfs를 재귀호출할 때 dfs(a, cnt+1)으로 재귀호출한다는 것이다.

조합을 출력할 때는 for( int i=a+1; i<=N; i++){ ... dfs(i, cnt+1) ... } 으로 재귀호출했었지만 중복으로 선택하기 위해서는 한 번 체크했던 a+1번째에 다시 방문해야하므로 중복조합을 찾을 때는 dfs(a, cnt+1)으로 재귀호출한다.

'BOJ' 카테고리의 다른 글

BOJ 2580번 :: 스도쿠  (0) 2019.11.27
BOJ 9663번 :: N-Queen  (0) 2019.11.26
BOJ 15651번 :: N과 M(3)  (0) 2019.11.25
BOJ 15650번 :: N과 M(2)  (0) 2019.11.24
BOJ 15649번 :: N과 M(1)  (0) 2019.11.24

+ Recent posts