/* Suspends execution for approximately TICKS timer ticks. */
void
timer_sleep (int64_t ticks) { //ticks는 sleep 상태에 돌입해야 할 시간을 의미한다.
int64_t start = timer_ticks (); //현재 타이머의 ticks을 가져오고 start에 대입한다.
ASSERT (intr_get_level () == INTR_ON);
while (timer_elapsed (start) < ticks) //현재 타이머의 tick-start와 인자로 받은 ticks를 비교한다.
thread_yield (); //ticks의 값이 더 크면 실행한다.
}
/* Yields the CPU. The current thread is not put to sleep and
may be scheduled again immediately at the scheduler's whim. */
void
thread_yield (void) {
struct thread *curr = thread_current (); //현재 실행 중인 스레드 가져오기
enum intr_level old_level;
ASSERT (!intr_context ()); //인터럽트 핸들러 내부에서는 yield 호출 금지
old_level = intr_disable (); //인터럽트를 비활성화
if (curr != idle_thread) //idle 스레드는 레디 큐에 넣지 않음
list_push_back (&ready_list, &curr->elem); //현재 스레드를 레디 큐의 마지막에 넣기
do_schedule (THREAD_READY); //스케줄러를 호출하여 현재 스레드를 THREAD_READY 상태로 설정 후, 다음 스레드로 전환
intr_set_level (old_level); //다시 인터럽트 활성화
}
일단 구현해야 할 부분의 일부를 해석해보았다.
아직 뭘 구현해야겠는지 잘 모르겠으니 그만 알아보고 키워드 공부를 하자 🤨
Busy waiting (spinning) / Sleep waiting
서로 다른 프로세스나 스레드가 임계 영역에 동시에 접근하려고 할 때, 두 가지 동기화 처리 방법이 있다.
1. Busy waiting : 조건이 만족될 때까지 무한 대기하며 cpu를 낭비
2. Sleep waiting : 조건이 만족될 때까지 cpu를 양보하고 sleep상태로 있다가 조건이 다시 만족되면 wake
여기서 조건이란 공유 자원에 접근할 수 있는 권한이 만족되었는가이다.
예를 들어 p1이 임계 영역의 lock을 얻어서 사용하던 중 context switch가 발생하여 p2가 공유 자원에 대한 lock을 가질 수 없게 됨
-> 무한 대기 하며 계속해서 cpu 점유하면 Busy waiting, sleep 상태로 전환되었다가 lock이 풀릴 때 다시 wake하면 Sleep waiting
Busy waiting은 필요한 자원을 얻을 수 있는 상태가 될 때까지 계속해서 무한 루프를 돌며 cpu를 낭비하므로 비효율적이다.
Sleep waiting을 사용하면 이 문제점을 극복할 수 있다. -> Semaphore, Mutex로 임계 영역에 접근 제한을 걸어서 구현하면 됨!
주의할 점은 임계 영역 접근 제한 기능을 남용하면 context switching이 너무 잦아서 시스템 성능이 저하될 수 있다는 것이다.
Semaphore
세마포어는 지금 공유 자원을 사용할 수 있는지의 상태를 나타내는 정수형 변수이다.
예를 들어 세마포어 변수가 5이면 동시에 5개의 프로세스 또는 스레드만 임계 영역에 접근 가능하다는 뜻이다.
이 변수는 오직 P, V라는 연산으로만 다룰 수 있다.
세마포어 변수 값으로 0, 1만 가지는 경우와 0 이상을 가지는 경우로 종류가 나뉜다.
나중에 설명하겠지만 Binary Semaphore는 Mutex와 비슷한 방법이지만 차이가 있다고 한다.
1. Binary Semaphore : 0, 1
2. Counting Semaphore : 0 이상
P 연산
- 진입 시 자원이 있는지 확인
- 자원이 있으면 자원 점유 / 없으면 wait
P(S):
while S == 0: //자원이 없으므로 기다림
wait
S = S - 1 //S>0이 되면 S-=1을 수행하고 자원을 점유
V 연산
- 자원 반납
- 다음 스레드에게 자원 사용 허가
V(S):
S = S + 1 //프로세스가 작업을 마치고 S+=1
wake up one waiting thread (if any) //기다리던 스레드가 있다면 깨워서 자원을 사용하도록 함
임계 구역에 세마포어의 P, V 연산을 적용하면 다음과 같다.
P(S); //임계 구역 들어가기 전에 자원 확보
임계 구역에서 작업 수행
V(S); //임계 구역 나와서 자원 반납
(실행 예시)
- 초기 세마포어 값은 S = 1
- p1이 시작됨
- P 연산 : P(S) == 1 이므로 while문 실행되지 않고 패스, S-=1을 수행하고 p1이 자원을 점유
- p2가 시작됨
- P 연산 : P(S) == 0 이므로 while문 실행, p2는 대기 상태로 전환
- p1이 수행 완료됨
- V 연산 : S+=1을 수행하여 세마포어 값이 다시 S == 1로 회복됨, p2가 자원 점유할 수 있게 깨우기
Mutex
하나의 스레드만 임계 구역에 접근할 수 있게 lock을 주는 방법
스레드 간 Mutual Exclusioin(상호 배제)를 실현해줌
다른 스레드는 lock이 풀릴 때까지 block 상태로 기다림
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&lock); //lock: 다른 스레드 진입 금지
임계 구역
pthread_mutex_unlock(&lock); //lock 해제: 다른 스레드 진입 허용
counter++는 비원자적 연산이기 때문에 동시에 여러 스레드가 접근하면 race condition이 발생한다.
뮤텍스로 감싸면 스레드 하나씩 순차적으로 접근하기 때문에 안전하다.
🤔 뮤텍스와 세마포어는 각각 어떤 상황에 쓰일까?
세마포어는 파일 시스템 상의 파일 형태로 존재하므로 시스템 범위에 걸쳐서 사용 가능하다.
시스템콜을 기반으로 동작하며, 커널에서 추적하므로 다른 프로세스들 사이에서도 공유할 수 있다.
즉 IPC(Inter-Process Communication)상황에서 주로 쓰인다.
반면 뮤텍스는 프로세스 범위에서 동작하며 프로세스가 종료될 때 자동으로 clean up 된다.
따라서 특정 주소 공간을 함께 쓰는 스레드 간의 공유 자원을 보호할 때 쓰인다.
POSIX를 이용하면 프로세스 간 공유 뮤텍스를 만들 수 있지만 세마포어가 훨씬 직관적이고 범용적이라고 한다.
//뮤텍스로 프로세스 간 공유 자원 관리하기
int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *worker(void *arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
Semaphore와 Mutex 비교
내가 차이점을 공부하면서 느낀 핵심을 정리하면 다음과 같다.
뮤텍스는 주로 스레드 간 동기화에 쓰이므로 여러 스레드들이 프로세스의 전역 변수에 연산하려고 접근할 때마다 atomic race condition이 발생하지 않게 하는 것이 목적일 것이다. 따라서 좀 더 엄격하게 공유 자원을 관리하는 느낌이다. 하나의 공유 자원을 임계 영역으로 감싸고 그 임계 영역에 하나의 스레드만 접근할 수 있게 lock이라는 권한을 준다. lock을 가지고 있는 스레드만이 lock을 해제할 수 있으므로 스레드 끼리는 해당 임계영역에 있어서 상호배제적이게 된다.
세마포어는 주로 프로세스 간 동기화에 쓰인다. 따라서 프로세스 간에 공유하는 OS 자원이 주로 임계 영역으로 감쌀 대상이 된다. 이미 OS가 관리하고 있는 곳이라서 조금 덜 엄격하게 공유 자원을 관리하는 게 아닐까 생각한다. (수정 - 덜 엄격하다기 보다 세마포어는 lock의 소유자가 없고 signal 기반으로 unlock이 동작하기 때문에 자원을 아예 점유하여 상호 배제를 기대하기보다는 자원 접근 가능 여부를 확인하는 것을 주 목적으로 두고 있다는 점에서 차이가 있다고 보면 될 듯 하다.) 뮤텍스와 다르게 lock을 소유한다든가 lock 소유자만이 unlock할 수 있게 하지 않고 해당 자원이 필요한 프로세스가 또 생기면 signal을 보내 lock을 해제할 수 있다. 또한 하나가 아니라 여러 개의 공유 자원을 임계 영역으로 감싸서 임계 영역에 여러 개의(지정된 세마포어 변수 개수 만큼의) 스레드가 접근할 수 있다.
| Mutex | Semaphore |
| 0 또는 1 (이진) | 0 이상 (정수 카운터) |
| 스레드가 락을 소유 | 소유 개념 없음 |
| 락 소유자만 해제 가능 | 락을 걸지 않은 스레드도 signal을 보내 락을 해제할 수 있음 |
| 1개의 자원을 임계 영역으로 지정하여 한 번에 1개의 스레드만 접근 가능하게 함 | 여러 개의 자원을 임계 영역으로 지정하여 동시에 여러 개의 스레드가 접근 가능하게 함 |
| 스레드 간 상호배제가 목적 | 자원 개수를 제한하여 동기화를 실현하는 것이 목적 |
| 공유 변수 보호 (ex. counter++) | 리소스 풀 (ex. 5개의 DB 커넥션, 3대의 프린터) |
| lock() unlock() |
wait() signal() |
예전에 대학교 OS 강의를 듣다가 일부만 이해하고 넘어갔던 개념인데
뮤텍스와 세마포어 차이를 확실히 정리하고 가니까 굉장히 후련한 느낌이다..
다음 키워드도 얼른 정리하러 가보자앗
'정글 > Pintos' 카테고리의 다른 글
| [pintos] 1주차 - Alarm Clock 구현 과정 (0) | 2025.05.20 |
|---|---|
| [pintos] 1주차 - Alarm Clock 코드 정리 (1) | 2025.05.20 |
| [pintos] 1주차 - 키워드 정리 (2) : Process/Thread, DeadLock, 프로세스 상태/CPU Scheduling (3) | 2025.05.11 |
| [pintos] 1주차 - 문서 정리 (0) | 2025.05.10 |
| [pintos] 1주차 - Pintos 환경 세팅 (0) | 2025.05.09 |