시스템에 따라 스케줄러를 최적화 해야 하는 목표가 다르다.

1. 배치 시스템

    - 비선점형 알고리즘 또는 각 프로세스에게 긴 주기를 제공하는 선점형 알고리즘이 적절하다.

    -> 프로세스간 문맥교환을 줄여서 성능을 향상시킨다.

2. 대화식 시스템

    - 한 프로세스가 CPU를 독점하여 다른 사용자의 서비스를 방해하는 것을 막기 위해서 선점이 필수적이다.

3. 실시간 시스템

    - 때때로 선점이 필요하다. 프로세스들이 자신들이 실행을 오랫동안 할 수 없다는 점을 알고 있어서 보통 자신이 해야 할 일을 바로 수행하고 대기하기 때문.

 

# 스케줄링 알고리즘의 목표

모든 시스템

공평함 : 각 프로세스에게 공평한 몫의 CPU를 할당함.

정책 집행 : 정책이 집행되는지 관찰한다.

균형 : 시스템의 모든 부분이 활동하도록 해야 한다.

배치 시스템

성능 : 시간당 처리되는 작업의 수를 최대화 해야 한다. (처리율, throughput)

반환시간(turnaround time) : 작업의 제출부터 종료까지 걸리는 시간을 최소화해야 한다.

CPU 이용률 : CPU가 항상 활용되도록 해야 한다.

+) 처리량을 극대화하는 스케줄링 알고리즘이 반드시 반환 시간을 최소화 하지는 않는다.

대화식 시스템

응답시간 : 요청에 빠르게 응답해야 한다.

비례(proportionality) : 사용자의 기대를 만족시켜야한다.

실시간 시스템

마감시간 만족(deadline) : 마감시간 내에 처리를 끝내서 데이터 손실을 방지해야한다

예측 가능 : 멀티미디어 시스템에서 품질 저하를 방지.

 

#  배치 시스템의 스케줄링

선입선출 (FCFS: First Come First Served)

        : 요청 순서대로 cpu를 할당받음

최단 작업 우선 (SJF: Shortest Job First)

        : 실행 시간이 가장 짧은 작업을 선택

        작업의 실행 시간을 미리 알고 있어야 한다.

최단 잔여 시간 우선 (SRTN: Shortest Remaining Time Next)

        : 프로세스의 남은 실행 시간이 가장 짧은 작업 선택

        작업의 실행 시간을 미리 알고 있어야 한다.

        새로 도착한 짧은 작업이 좋은 서비스를 받을 수 있도록 해준다.

# 대화식 시스템의 스케줄링

라운드 로빈 스케줄링 (Round-Robin Scheduling)

        : 각 프로세스에게 '시간 할당량(time quantum)'이라 불리는 시간 주기가 할당됨. 한번에 이 시간 동안만 실행된다. 프로세스가 할당 시간 동안 실행하면 CPU는 다른 프로세스에게 주어진다. 프로세스가 할당 시간이 끝나기 전에 대기하거나 종료하면 CPU는 다른 프로세스로 전환한다.

        한 프로세스에서 다른 프로세스로 문맥교환이 자주 일어나면(시간 할당량이 짧음) 문맥교환에 걸리는 시간의 비율이 너무 커져서 CPU의 효율이 떨어짐.

        시간 할당량이 너무 크면 짧은 대화식 요청의 응답의 질이 떨어진다.

우선순위 스케줄링

        : 각 프로세스에게 우선순위가 할당되며 가장 높은 우선순위를 가진 실행 가능한 프로세스가 다음에 수행됨.

        높은 우선순위의 프로세스가 무한히 실행되는 것을 막기 위해 스케줄러는 클록 인터럽트마다 현재 실행중인 프로세스의 우선순위를 낮출 수 있다.(기아 문제 방지, starvation)

        각 프로세스는 최대로 실행할 수 있는 시간 할당량을 가진다.

        시스템의 특정 목표를 달성하기 위해 우선순위는 시스템에 의해 동적으로 할당될 수 있다.

        프로세스들을 그룹으로 묶어 우선순위 클래스를 형성하고, 우선순위 클래스마다 우선순위를 할당하며 각 클래스 내에서는 라운드 로빈을 사용하면 편리하다.

다단계 큐 (Multiple Queues)

        : 우선순위 클래스들을 설정. 우선순위가 높은 클래스일수록 프로세스마다 할당된 시간이 짧다. 프로세스가 할당된 시간을 모두 소비하면 한 단계 낮은 클래스로 이동한다.

최단 프로세스 순번 (SPN: Shortest Process Next)

        : 과거의 행동을 기반으로 추정하여 예상 실행 시간이 가장 짧은 프로세스를 실행한다.

        현재 측정된 값과 이전 예상치를 가중 평균하여 연속적으로 다음 값을 예상하는 '에이징(aging)'기법을 사용한다.

보장 스케줄링 (Guaranteed Scheduling)

        : 사용자에게 성능에 대한 현실적인 약속을 하고 이를 보장하는 방법. '단일 사용자 시스템에서 n개의 프로세스가 실행하면 동일하게 각 프로세스는 1/n만큼의 CPU시간을 받아야 한다.'

        어느 프로세스가 받아야 할 CPU시간에 대한 실제 받은 CPU시간 비율이 작은 프로세스를 가장 가까운 경쟁자의 비율을 초과할 때까지 수행시킨다.

복권 스케줄링 (lottery Scheduling)

        : 스케줄링 결정을 내릴 때마다 무작위로 복권 티켓을 선택하고 이 티켓을 가진 프로세스가 자원을 할당 받는다.

        더 중요한 프로세스는 여분의 티켓을 더 받아서 자원 할당 확률을 높일 수도 있고, 협동하는 프로세스들이 티켓을 교환할 수도 있다.

공평-몫 스케줄링 (Fair-Share Scheduling)

        : 스케줄링 하기 전에 프로세스의 소유자를 고려한다. 각 사용자는 CPU시간의 일정 비율을 할당받는다. 스케줄러는 이 비율이 지켜지도록 프로세스를 선택한다.

# 실시간 시스템에서 스케줄링

경성 실시간 (hard real-time) : 마감 시간이 절대적으로 만족되어야 한다.

연성 실시간 (soft real-time) : 때때로 마감시간을 지키지는 않아도 된다.

모든 경우의 수를 검사하되, 특정 조건에 맞을 때만 검사하는 방식!

백트래킹 문제는 보통 DFS알고리즘에 조건을 검사하는 부분을 추가하여 해결한다.

대표적인 문제로 N-Queen이 있다.

N-Queen

N-Queen 문제는 크기가 N × N인 체스판 위에 퀸 N개를 서로 공격할 수 없게 놓는 문제이다.

N이 주어졌을 때, 퀸을 놓는 방법의 수를 구하는 프로그램을 작성하시오.

퀸은 가로, 세로, 대각선 방향으로 움직일 수 있는 체스의 말이다.

모든 경우의 수를 따진다면 (각 행마다 퀸을 놓을 수 있는 방법의 수) N x (행의 개수) N 으로 O(N^N)의 시간 복잡도를 가진다.

조건 검사 없이 모든 경우의 수를 따진다면 체스판의 크기가 조금만 커져도 해답을 찾는데 시간이 오래 걸릴테지만,

백트래킹으로 조건을 검사하면서 해당 조건이 만족될 때만 퀸을 놓고, 그 다음 퀸을 놓을 자리를 찾는다면 시간을 단축할 수 있다.

// 재귀적으로 함수를 호출하여 행 0~n-1까지 체크한다.
void findQueen(int n){
	if( n>=N ) count++;
	  
	for(int i=0; i<N; i++){		
        	// (n, i)에 퀸을 배치할수있는지 체크
		if( arr[i]==true ) continue;
		if( dup[n+i] == true || ddown[N-1+n-i]==true ) continue;
        	// (n,i)에 퀸을 놓고
		arr[i] = dup[n+i] = ddown[N-1+n-i] = true;
		findQueen(n+1);			// 다음 행에서 퀸을 놓을 자리를 재귀적으로 찾는다.
		// (n,i)에서 퀸을 제거하고 n번째 행에서 다른자리에 퀸을 또 놓을 수 있는지 체크할것임
        arr[i] = dup[n+i] = ddown[N-1+n-i] = false;
	}
}

 

백트래킹에서 중요한 것!

재귀를 타고 들어가기 전에 방문했던(작업했던) 것을 재귀에서 빠져나왔을 때 원래대로 돌리는 작업을 통해서 해당 작업(방문)을 이후에 다시 할 수 있도록 하는 것.

'computerScience > 알고리즘' 카테고리의 다른 글

DP  (0) 2020.02.01
꼬리 재귀 (tail recursion)  (0) 2019.12.08
Dijkstra Algorithm  (0) 2019.12.04
삽입정렬, 퀵정렬, 병합정렬  (0) 2019.11.19

* 프로세스 간 문맥 교환

1. 사용자 모드에서 커널 모드로 전환

2. 재적재할 수 있도록 하기 위해 프로세스 테이블에 레지스터를 저장하는 것을 포함하여 현재 프로세스의 상태, 메모리 맵을 저장

3. 스케줄링 알고리즘을 실행하여 다음에 실행할 프로세스를 선택

4. 새로운 프로세스의 메모리 맵을 MMU로 다시 적재한다.

5. 새로운 프로세스가 수행을 시작한다.

 

* 프로세스의 행동

CPU-바운드 프로세스 : 대부분의 시간을 계산에 소비. 일반적으로 긴 cpu 버스트를 가지고 드물게 i/o를 기다림

I/O-바운드 프로세스 : 대부분의 시간을 I/o를 기다리면서 소비. 짧은 cpu 버스트를 가지고 빈번히 i/o를 기다림

CPU가 빨라질수록 프로세스는 점점 I/O-바운드가 되는 경향이 있다.

 

* 스케줄링이 필요한 시점?

1. 새로운 프로세스를 만들 때 자식 프로세스를 먼저 실행할지 부모 프로세스를 먼저 실행할지 결정해야 한다.

2. 프로세스를 종료할 때 다음으로 어느 프로세스를 실행할지 결정해야 한다.

3. 프로세스가 I/O, 세마포어, 다른 무언가 때문에 대기해야 할 때 다음에 실행할 프로세스를 선택해야 한다.

4. I/O인터럽트가 발행하는 경우 새로 준비된 프로세스를 실행할지, 인터럽트가 발생할 때 실행하던 프로세스를 실행할지, 제3의 프로세스를 실행할지 결정

 

* 비선점(nonpreemptive) 스케줄링 알고리즘

- 어떤 프로세스가 다음 수행 프로세스로 선정되면 이 프로세스는 I/O나 다른 프로세스를 기다리기 위해 대기하거나 자발적으로 CPU를 반환할 때까지 계속 수행할 수 있다. ( 응답 시간의 예측이 용이하다. )

-> 클록 인터럽트에서 스케줄링 결정이 이루어지지 않는다. 클록 인터럽트 처리가 끝난 후, 더 높은 우선순위의 프로세스가 기다리던 타임 아웃이 만족되어 깨어난 경우가 아니면 인터럽트 이전에 수행되던 프로세스가 다시 수행을 재개한다.

* 선점형(preemptive) 스케줄링 알고리즘

- 프로세스를 선택하고 최대 값으로 정해진 시간을 넘지 않는 범위에서 실행되도록 한다. 만약 정해진 시간 간격의 끝에 도달하면 이 프로세스는 중단되고, 스케줄러는 다음에 실행할 다른 프로세스를 선정한다.

- 선점형 스케줄링을 하기 위해서는 주기의 마지막에 클록 인터럽트가 발생하여 스케줄러가 다시 CPU의 제어를 회복할 수 있어야 한다.

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

스레드 스케줄링  (0) 2019.12.03
스케줄링 알고리즘의 분류  (0) 2019.11.28
메시지 패싱, 장벽  (0) 2019.11.27
슬립과 깨움, 세마포어, 뮤텍스, 모니터  (0) 2019.11.25
경쟁조건, 상호배제  (0) 2019.11.25

#메시지 패싱(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

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

+ Recent posts