읽는자 쓰는자 문제

The Readers and Writers Problem

데이터베이스 접근을 모델링한다

다수의 프로세스가 데이터베이스에서 동시에 읽는것은 허용되지만, 한 프로세스가 데이터베이스를 갱신하고 있다면 다른 프로세스는 읽는 경우라도 데이터베이스에 접근할 수 없다.

 

이 문제를 해결하기 위해, 최초의 독자는 데이터베이스를 접근하기 위해 세마포어 db를 down한다.

뒤따라 오는 독자들은 카운트 rc를 증가시킨다. 독자가 일을 마치면 rc를 감소시키고 마지막 독자를 세마포어 db에 대해 up을 수행하여 대기중인 작가가 있으면 작가가 다음 코드로 진입할 수 있게 한다.

typedef int semaphore;
semaphore mutex = 1;		// rc에 대한 접근제어
semaphore db = 1;		// db에 대한 접근제어
int rc = 0;

void reader(void){
    while(TRUE){
    	down(&mutex);		// rc에 대한 접근제어 획득
        rc = rc+1;
        if( rc==1 ) down(&db);	// 첫번째 reader라면 db down
        up(&mutex);		// rc에 대한 접근제어 박탈
        
        read_data_base();
        
        down(&mutex);		// rc에 대한 접근제어 획득
        rc = rc-1;
        if( rc==0 ) up(&db);	// 마지막 reader가 읽고나면 db up
        up(&mutex);		// rc에 대한 접근제어 박탈
        
        use_data_read();
    }
}

void writer(void){
    while(TRUE){
    	think_up_data();
        
        down(&db);		// db에 대한 접근제어 획득
        write_data_base();	// db에 데이터 기록
        up(&db);		// db에 대한 접근제어 박탈
    }
}

 

이 방법에도 문제가 존재한다.

적어도 하나의 독자가 이미 존재하는 한 새로운 독자는 '언제나' 진입이 허용된다.

-> 작가는 독자가 존재하지 않을 때까지 대기하게 된다. 독자의 작업이 끝나기 전에 새로운 독자가 계속해서 도착한다면 작가는 데이터베이스에 절대 접근할 수 없게 된다.

=> 작가가 기다리는 도중 독자가 도착하면 독자는 바로 진입이 허용되는 것이 아니라, 작가 뒤에 수행되도록 대기하도록 해야 한다. -> 작가는 이미 존재하는 독자들은 대기해야 하지만, 작가 이후에 도착한 독자들은 대기하지 않아도 된다.

이 방법의 단점은 병행성이 떨어져 성능이 나빠질 수 있다는 것이다.

-> Courtois et al. 은 작가에게 우선권을 주는 해결책을 제시했다.

# 식사하는 철학자 문제

Dijkstra가 제시. dining philosopher problem.

https://ko.wikipedia.org/wiki/%EC%8B%9D%EC%82%AC%ED%95%98%EB%8A%94_%EC%B2%A0%ED%95%99%EC%9E%90%EB%93%A4_%EB%AC%B8%EC%A0%9C

 

식사하는 철학자들 문제 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 원탁에 둘러앉은 다섯 명의 철학자와 다섯 접시의 스파게티, 그리고 다섯 개의 포크 식사하는 철학자들 문제는 전산학에서 동시성과 교착 상태를 설명하는 예시로, 여러 프로세스가 동시에 돌아갈 때 교착 상태가 나타나는 원인을 직관적으로 알 수 있다. 다섯 명의 철학자가 원탁에 앉아 있고, 각자의 앞에는 스파게티가 있고 양옆에 포크가 하나씩 있다. 그리고 각각의 철학자는 다른 철학자에게 말을 할 수 없다. 이때 철학자가 스파게티

ko.wikipedia.org

#define N 5

void philosopher(int i){
	while(TRUE){
    	think();
        take_fork(i);
        take_fork((i+1)%N);
        eat();
        put_fork(i);
        put_fork((i+1)%N);
    }
}

take_fork는 지정된 포크를 사용할 수 있을 때까지 기다린다.

다섯 명의 철학자가 모두 동시에 왼쪽 포크를 집는 상황에서 아무도 오른쪽 포크를 잡을 수 없는 문제가 발생한다.

왼쪽 포크를 집고, 오른쪽 포크를 사용할 수 있는지 검사하고, 사용할 수 없으면 왼쪽 포크를 내려놓도록 코드를 수정하더라도 문제는 발생한다. 모든 철학자가 동시에 왼쪽 포크를 집고, 오른쪽 포크를 사용할 수 없음을 알고 다시 왼쪽 포크를 내려놓음을 반복하게 될 수 있다.

이런 식으로, 모든 프로그램이 무기한 계속 실행하지만 아무런 진전도 없는 경우를 기아(starvation)이라 한다.

 

교착상태가 기아가 없도록 하려면 think 호출 이후, 포크를 집고, 포크를 놓는 상황을 이진 세마포어로 보호해야한다.

포크를 집기 전에 철학자는 mutex를 down한다. 포크를 집은 후 철학자는 mutex를 up한다.

이 해답에는 실용적인 문제가 발생한다. 이 상황에서 한 번에 한 철학자만 식사할 수 있다. (두 명의 철학자가 식사 가능함에도 불구하고.)

 

state라는 배열을 사용해서 철학자가 식사 중인지, 생각하고 있는지, 포크를 집으려고 시도하는지 추적한다.

철학자는 오직 이웃이 식사중이 아닌 경우에만 식사중인 상태가 될 수 있다.(양 옆 포크를 모두 사용해야하므로)

프로그램이 철학자 한 명 당 세마포어 배열을 사용하므로 이제 필요한 포크가 사용 중이면 배고픈 철학자는 대기하게 된다.

#define N			5
#define LEFT			(i-N-1)%N		// 현재 철학자의 왼쪽 이웃
#define RIGHT			(i+1)%N			// 현재 철학자의 오른쪽 이웃

#define THINKING		0
#define HUNGRY			1
#define EATING			2

typedef int semaphore;
int state[N];						// 각 철학자의 상태 저장
semaphore mutex = 1;
semaphore s[N];						// 철학자 당 하나씩 가지는 세마포어

void philosopher(int i){
    while(TRUE){
        think();
        take_forks(i);		// 두 개의 포크를 동시에 잡을 수 없으면 대기
        eat();
        put_forks(i);		// 두 개의 포크를 동시에 놓는다.
    }
}

void take_forks(int i){
    down(&mutex);
    state[i] = HUNGRY;
    test(i);					// 두 개의 포크를 집으려고 시도한다.
    up(&mutex);
    down(&s[i]);
}

void put_forks(int i){
    down(&mutex);
    state[i] = THINKING;
    test(LEFT);				// 왼쪽, 오른쪽 이웃이 포크를 집을 수 있는 상황인지 체크
    test(RIGHT);
    up(&mutex);
}

void test(int i){
	if( state[i]==HUNGRY && state[LEFT]!=EATING && state[RIGHT]!=EATING ){
    	// 주어진 철학자가 배가 고프고, 좌우 이웃이 식사중이 아니어야 포크를 둘 다 집을 수 있다.
    	state[i] = EATING;
        up(&s[i]);
    }
}

 

I/O장치와 같은 제한된 개수의 자원을 배타적으로 접근하기 위해 경쟁하는 프로세스들을 모델링하는데 유용하다.

여러 개의 프로세스들이 다수의 스레드를 가지고 있을 때 프로세스와 스레드라는 두 개의 병렬성이 존재.

이러한 시스템에서 스케줄링은 사용자 레벨 스레드 또는 커널 레벨 스레드가 지원되는지에 따라 다르다.

1. 사용자 레벨 스레드

커널은 스레드의 존재를 인식하지 못하므로 커널은 항상 하던대로 프로세스 A를 선택.

할당 시간만큼 A에게 CPU의 제어를 넘긴다.

A내부의 스레드 스케줄러는 어느 스레드를 실행시킬지 결정한다.

스레드를 다중 프로그래밍 하기 위한 클록 인터럽트가 존재하지 않으므로 이 스레드는 자신이 원하는 만큼 실행할 수 있다.

이 스레드가 프로세스에게 할당된 할당 시간을 모두 소비하면 커널은 다른 프로세스를 선택하여 실행한다.

사용자 레벨 스레드 스케줄링의 유일한 제약 : 너무 오래 실행되는 스레드를 중단시킬 수 있는 클록 인터럽트가 없다는 것.

 

2. 커널 레벨 스레드

커널은 어떤 스레드를 선택하여 실행한다. 커널은 스레드가 어느 프로세스에 속하는지 고려하지 않지만, 커널이 원한다면 고려할 수 있다. 스레드는 할당 시간을 부여받고 이 시간을 초과하면 강제로 중단된다.

 

사용자 레벨 스레드와 커널 레벨 스레드 사이의 차이 : 성능

사용자 레벨 스레드에서 스레드 문맥 교환 : 몇 개의 기계어로 수행. 빠르다.

커널 레벨 스레드에서 스레드 문맥 교환 : 메모리 맵을 바꾸고 캐시를 무효화하는 몇 배나 오래 걸리는 완전한 문맥교환

=> 동등하게 중요한 두 개의 스레드가 있고, 하나는 방금 대기하게 된 스레드와 동일한 프로세스에 속하고, 다른 하나는 다른 프로세스에 속한다면 동일한 프로세스에 존재하는 스레드로 문맥교환하는 것을 선호한다.

 

커널 레벨 스레드에서 한 스레드가 I/O에 대해 대기하는 경우, 이것이 해당 프로세스 전체를 중단시키지는 않는다.

사용자 레벨 스레드에서는 해당 프로세스가 중단됨.

 

사용자 레벨 스레드는 응용에 특화된 스레드 스케줄러를 사용할 수 있다. -> 작업 스레드가 종종 디스크 I/O를 위해 대기하는 환경에서 병렬성을 극대화시킨다.( 런타임 시스템이 모든 스레드가 무슨 일을 하는지 알 때 )

커널 레벨 스레드에서 커널은 스레드가 무슨 일을 하는지 절대 알 수 없다.

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

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) : 때때로 마감시간을 지키지는 않아도 된다.

* 프로세스 간 문맥 교환

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

+ Recent posts