Mutex Locks
만들어진 이유?
- 임계구역 문제에 대한 하드웨어 기반 해결책은 복잡하고, 응용프로그래머가 사용 불가능
→ 임계구역 문제 해결을 간단하게 하기 위해 만들어진 소프트웨어 도구를 개발 Mutex Locks
: 상위 수준 소프트웨어 도구 중 하나로, 가장 간단한 도구- 임계 구역 보호를 통해 경쟁 조건 방지를 위해 mutex 락 사용
- 임계 구역에 들어갈 때 락을 획득하고, 빠져나올 때 락을 반환해야 한다.
acquire()
: 락 획득release()
: 락 반환
- 임계 구역 보호를 통해 경쟁 조건 방지를 위해 mutex 락 사용
Mutex Lock의 구성
- available이라는 boolean 변수를 가짐 → 이 변수 값이 락의 가용 여부를 표시
- 락이 사용 가능하면 acquire() 호출은 성공하고, 락은 사용 불가 상태가 됨
- 사용 불가 상태의 락을 획득하려고 하면 프로세스는 락이 반환될 때까지 봉쇄
acquire()과 release() 함수 정의
acquire() {
while(!available); // busy wait
available = false;
}
release(){
available = true;
}
// 사용 구조
while(true){
acquire()
/* critical section */
release()
/* remainder section */
}
acquire()
또는release()
함수 호출은 원자적으로 수행되어야 한다.
→ CAS를 사용하여 구현할 수 있음- 구현 방식의 단점 → 바쁜 대기(busy waiting)를 해야 한다는 것
- 프로세스가 임계구역에 있는 동안 진입을 원하는 다른 프로세스들은
acquire()
함수를 호출하는 반복문을 계속 실행해야 한다.
- 하나의 CPU 코어가 여러 프로세스에서 공유되기 때문
→ 다른 프로세스가 생산적으로 사용할 수 있는 CPU 주기를 낭비
→ 실제 다중 프로그래밍 시스템에서는 문제가 됨
- 하나의 CPU 코어가 여러 프로세스에서 공유되기 때문
- 프로세스가 임계구역에 있는 동안 진입을 원하는 다른 프로세스들은
락 경합
- 락은 경합 상태와 비경합 상태로 나뉜다.
경합 상태
: 락을 획득하려고 시도하는 동안 스래드가 봉쇄될 때- 높은 경합 상태 : 비교적 많은 수의 스레드가 락을 획득하려고 시도 중
- 해당 상태는 병행 실행 응용 프로그램의 성능을 전체적으로 저하시킴
- 낮은 경합 상태 : 비교적 적은 수의 스레드가 락을 획득하려고 시도 중
- 높은 경합 상태 : 비교적 많은 수의 스레드가 락을 획득하려고 시도 중
비경합 상태
: 스레드가 락을 얻으려고 시도할 때 락을 사용할 수 있을 때
스핀락(spin lock)
- 위의 mutex 락 유형을 스핀락(spin lock)이라고도 함
- 락을 사용할 수 있을 때까지 프로세스가 회전하기 때문
- 락을 사용할 수 있을 때까지 프로세스가 회전하기 때문
- 잠시 락을 유지해야 하는 경우 다른 스레드가 하나의 코어에서 임계구역을 실행하는 동안
스레드는 다른 처리 코어에서 스핀하고 있을 수도 있다.
장점
- 스핀락은 프로세스가 락을 기다려야 하고 문맥 교환이 필요하지 않다.
→ 다중 코어 시스템의 특정 상황에서는 락이 필요할 때 스핀락이 선호됨
- 최신 다중 코어 컴퓨팅 시스템에서 스핀락은 많은 운영체제에서 널리 사용
스핀락에서 짧은 기간이란?
- 락을 기다리는 스레드는 2번의 문맥교환이 필요
- 첫 번째 → 스레드를 대기 상태로 옮기기 위한 문맥 교환
- 두 번째 → 락이 사용 가능해지면 대기 중인 스레드를 복원하기 위한 문맥 교환
- 락이 유지되는 기간이 문맥 교환을 두 번 하는 시간보다 짧은 경우 스핀락 사용
세마포(Semaphores)
- 프로세스의 행동을 더 정교하게 동기화할 수 있는 방법을 제공하는 도구
- mutex와 유사하게 동작하는 동기화 도구
- mutex와 유사하게 동작하는 동기화 도구
wait()와 signal()
- 세마포 S는 정수 변수로서, 초기화 이외에는 원자적 연산인
wait()
와signal()
로 접근할 수 있음 - wait()와 signal() 연산 시 세마포 정수 값을 변경하는 연산은 반드시 원자적으로 수행되어야 함.
- 원자적 → 한 스레드가 세마포 값을 변경하면, 다른 스레드는 동시에 동일한 세마포 값 변경 불가능
- 원자적 → 한 스레드가 세마포 값을 변경하면, 다른 스레드는 동시에 동일한 세마포 값 변경 불가능
- wait() 정의
- S의 정수 값을 검사하는 작업(
S≤0
)과 실행될 수 있는 변경(S--
) 작업은 인터럽트 되면 안 됨
- S의 정수 값을 검사하는 작업(
wait(S){
while(S <= 0); // busy wait
S--;
}
- signal 정의
signal(S){
S++;
}
세마포 사용법
- 운영체제는
카운팅(counting)과
이진(binary)
세마포를 구분카운팅(counting) 세마포
: 카운팅 세마포의 값은 제한 없는 영역(domain)을 가짐이진(binary) 세마포
: 이진 세마포 값은 0과 1 사이의 값만 가질 수 있음- 해당 특징 때문에 mutex 락과 유사하게 동작
→ 몇몇 시스템에서는 mutex 락 대신 상호 배제를 보장하기 위해 이진 세마포 대신 사용
- 해당 특징 때문에 mutex 락과 유사하게 동작
카운팅(counting) 세마포의 동작
- 유한한 개수를 가진 자원에 대한 접근을 제어하는 데 사용 가능
- 이때, 카운팅 세마포가 가용한 자원의 개수로 초기화
- 이때, 카운팅 세마포가 가용한 자원의 개수로 초기화
- 각 자원을 사용하려는 프로세스는
wait()
연산 수행 → 세마포 값 감소 - 프로세스가 자원을 방출할 때
signal()
연산 수행 → 세마포 값 증가 - 세마포 값이 0 → 모든 자원이 사용 중임을 나타냄
- 이후 자원을 사용하려는 프로세스는 세마포 값이 0보다 커질 때까지 봉쇄(Block)됨
동기화 문제 해결을 위한 세마포 사용
- 가정
- 두 프로세스(P1, P2)가 존재
- P1은 S1의 명령문을 병행하게 수행하려고 함
- P2는 S2의 명령문을 병행하게 수행하려고 함
- S1이 끝난 뒤에만 S2가 수행될 수 있다고 가정
- 두 프로세스(P1, P2)가 존재
- 해결법
- P1과 P2가 세마포 synch를 공유하도록 설정 후, synch를 0으로 초기화
- P2의 S2수행은 P1이
signal(synch)
를 호출한 후에만 가능
→ synch 값이 0으로 초기화되어 있기 때문에 봉쇄
→signal(synch)를
수행하기 위해서는 S1을 수행해야 함
- P2의 S2수행은 P1이
- P1과 P2가 세마포 synch를 공유하도록 설정 후, synch를 0으로 초기화
// P1에 삽입하는 명령문
S1;
signal(synch);
// P2에 삽입하는 명령문
wait(synch);
S2;
세마포 구현
- 위 코드의 세마포 연산은 mutex 락처럼 바쁜 대기(busy wait) 문제를 지님
→ busy wait를 해결하기 위해wait()
와signal()
의 정의를 변경해야 함
세마포로 바쁜 대기(busy wait) 해결하는 원리 - 일시 중지 연산
- 프로세스가
wait()
연산을 실행하고, 세마포 값이 양수가 아니라면 반드시 대기해야 함
→ 바쁜 대기(busy wait) 대신 프로세스는 자신을 일시중지 시킬 수 있다. - 일시 중지 연산 과정
- 프로세스를 세마포에 연관된 대기 큐에 넣고, 프로세스의 상태를 대기 상태로 전환
- 이후, 제어가 CPU 스케줄러로 넘어가고, 스케줄러는 다른 프로세스를 실행하기 위해 선택
- 세마포 S 대기를 위해, 일시 중지된 프로세스는 다른 프로세스가
signal()
연산 시 재시작되어야 함.wakeup()
- 프로세스의 상태를 대기 상태에서 준비 완료 상태로 변경
- 이후 프로세스는 준비 완료 큐에 넣어짐
- 프로세스는
wakeup()
연산에 의하여 재시작됨
바쁜 대기를 해결하는 세마포 구현
typedef struct{
int value;
struct process *list; // 프로세스 리스트
}semaphore;
- 각 세마포는 정수(value)와 프로세스 리스트(list)를 가진다.
wait() 연산
재정의- 프로세스가 세마포를 기다려야 하는 경우 → 해당 프로세스가 프로세스 리스트에 추가됨
sleep()
: 자기를 호출한 프로세스를 일시 중지시키는 연산- 운영체제의 기본적인 시스템콜로 제공
wait(semaphore *S)
{
S->value--; // value가 0 이하인 경우
if(S->value < 0){
add this process to S->list; // 프로세스 리스트에 추가
sleep(); // 프로세스가 잠듦(대기)
}
}
signal()
연산 재정의- 프로세스 리스트에서 한 프로세스를 꺼내서 그 프로세스를 깨움
wakeup(P)
: 일시 중지된 프로세스 P의 실행을 재개시키는 역할- 운영체제의 기본적인 시스템콜로 제공
signal(semaphore *S)
{
S->value++;
// 가용할 수 있는 세마포어가 없는 경우(프로세스 리스트에 대기하는 게 있는 경우)
if(S->value <= 0)
{
// 프로세스 리스트에서 프로세스를 하나 빼옴
remove a process P from S->list;
wakeup(P); // 해당 프로세스를 깨움
}
}
새롭게 구현한 세마포어의 특징
- 바쁜 대기(busy wait)를 하는 고전적인 세마포는 음수를 가질 수 없음
→ But, 새롭게 구현한 세마포어에서는 음수를 가질 수 있음 - 세마포 값이 음수일 경우 → 그 절댓값은 세마포를 대기하고 있는 프로세스의 수
- wait() 연산 구현에서 세마포 값의 감소와 검사의 순서를 바꾼 결과
프로세스 리스트의 구현
- 대기하는 프로세스 리스트는 각 프로세스 제어 블록(PCB)에 있는 연결 필드에 의해 구현 가능
- 각 세마포는 정수 값과 PCB의 리스트에 대한 포인터를 가지고 있음
- 각 세마포는 정수 값과 PCB의 리스트에 대한 포인터를 가지고 있음
- 한정된 대기를 보장하도록 리스트에 프로세스를 추가/삭제하는 방법
- 선입 선출 방식 큐 사용 → 세마포가 큐의 머리와 꼬리에 대한 포인터를 모두 가짐
- 리스트에 임의의 큐잉 전략을 사용 → 일반적인 방법
- 세마포를 정확하게 사용하는 것과 특정 큐잉 전략(세마포 리스트를 위한) 사용과는 무관
세마포의 원자적 실행과 다중 코어 환경
- 세마포는 원자적으로 실행되어야 하는 것은 굉장히 중요
→ 두 프로세스가wait()와
signal()
연산을 실행할 수 없도록 보장해야 함- 이러한 상황은 임계구역 문제에 해당됨
- 이러한 상황은 임계구역 문제에 해당됨
- 단일 처리기 환경일 경우, 연산이 실행되는 동안 인터럽트를 금지하여 쉽게 해결 가능
- 인터럽트를 금지하면, 다른 프로세스들의 명령어들이 끼어들 수 없음
→ 오로지 현재 수행되고 있는 프로세스만 실행- 인터럽트가 다시 가능해지고, 스케줄러가 제어를 다시 얻으면 다른 프로세스 실행 가능
- 인터럽트가 다시 가능해지고, 스케줄러가 제어를 다시 얻으면 다른 프로세스 실행 가능
- 인터럽트를 금지하면, 다른 프로세스들의 명령어들이 끼어들 수 없음
- 다중 코어 환경일 경우 → 모든 처리 코어에서 인터럽트를 금지해야 함
- 다른 코어에서 실행되는 서로 다른 프로세스들의 명령어가 끼어들 수도 있기 때문
- 모든 코어에서 인터럽트를 금지하는 것은 매우 어렵고, 성능을 심각하게 감소시킴
→ SMP(대칭형 다중 처리) 시스템은 연산의 원자적 실행을 위해 다른 기법을 사용해야 함
(예시 : `compare_and_swap()` 또는 `스핀락(spinLock)`)
바쁜 대기(busy wait)와 세마포
wait()
와signal()
연산의 현재 정의에서 바쁜 대기를 완전히 제거하지 못했음- 바쁜 대기를 진입 코드에서 응용 프로그램의 임계 구역으로 이동시켰음
- 바쁜 대기를
wait()
와signal()
연산의 임계구역에만 국한시킴- 해당 임계구역은 매우 짧음 → 임계 구역은 거의 항상 비어있어 바쁜 대기는 드물게 발생
- 바쁜 대기가 발생하더라도 그 시간이 매우 짧음
- 바쁜 대기가 발생하더라도 그 시간이 매우 짧음
- 해당 임계구역은 매우 짧음 → 임계 구역은 거의 항상 비어있어 바쁜 대기는 드물게 발생
- 바쁜 대기가 비효율적인 경우
- 임계구역이 매우 긴 환경일 경우
- 거의 항상 점유된 응용 프로그램들을 갖는 환경일 경우
세마포의 한계점
- 세마포는 자칫 잘못 사용하면 발견하기 어려운 타이밍 오류를 일으킬 수 있다.
- 발견하기 어려운 이유?
- 타이밍 오류들은 특정 실행 순서로 진행되었을 때만 발생,
그런데 이러한 순서가 항상 일어나는 것은 아니기 때문
- 타이밍 오류들은 특정 실행 순서로 진행되었을 때만 발생,
- 예시
- 생산자-소비자 문제에서 count를 사용할 때 발생하는 시간적 오류
→ 시간적 오류 해결을 위해 Mutex 락과 세마포를 도입했지만 타이밍 오류는 여전히 발생
- 생산자-소비자 문제에서 count를 사용할 때 발생하는 시간적 오류
- 발견하기 어려운 이유?
세마포 해결책이 타이밍 오류가 발생하는 이유 - 임계 구역 예시로 설명
- 정상적인 세마포 연산
- 모든 프로세스는 mutex라는 이진 세마포 변수를 공유, 그 초기값은 1이다.
- 각 프로세스는 임계구역에 진입하기 전에
wait(mutex)
를 실행해야 함 - 각 프로세스는 임계구역에서 나올 때
signal(mutex)
를 실행해야 함
wait(mutex);
…
critical section // 임계구역
…
signal(mutex);
- 위 같은 순서가 제대로 지켜지지 않을 경우 → 두 프로세스가 동시에 임계구역에 존재할 수 있음
- 이러한 상황은 순수한 프로그래밍 오류 또는 비협조적인 프로그래머에 의해 발생
wait()
와signal()
연산의 순서가 뒤바뀐 경우
signal(mutex);
…
critical section // 임계구역
…
wait(mutex);
- 여러 프로세스가 동시에 임계구역 안에서 실행될 수 있음
→ 상호 배제 요구 조건 위반 - 이런 오류는 여러 프로세스가 동시에 자신의 임계구역 안에서 실행됐을 때만 발견 가능
→ 언제나 발생하는 상황이 아니기 때문에 발견하기 어려움
signal(mutex)
을 써야 할 곳에wait(mutex)
를 사용한 경우
wait(mutex);
…
critical section // 임계구역
…
wait(mutex);
- 세마포를 사용할 수 없으므로 프로세스는 두 번째
wait()
호출에서 영구적으로 봉쇄
- 프로세스가
wait(mutex)
나signal(mutex)
혹은 둘 다 빠뜨린 경우- 상호 배제 요구 조건을 위반하거나, 프로세스가 영원히 봉쇄됨