본문 바로가기

별걸다하는 IT/운영체제 OS

[운영체제]critical section(임계영역) & lock 락 & busy-waits

반응형

[운영체제(OS) 목차 &책 추천]

저번시간에 동기화에 대해서 살펴봤고~ (이어서 진행하는 거라 이전 포스팅을 보고 오지 않으면 이해가 불가능합니다)

이번에는 동기화를 우리가 어떻게 풀어왔느냐~ 의 서두를 열 LOCK에 대해서 살펴볼게요

 

 

critical section 임계영역이란 용어 또한 전에 동기화 포스팅에서 배워서 다 알고 있는거예요! (헷갈리시는 분은 보고오기)

여기서 공유되는 자원, 즉 동시접근하려고 하는 그 포커싱된 자원!에서 문제가 발생하지 않게 독점을 보장해줘야 하는 영역을 임계영역이라고 해요. 이거 제어를 잘해야 동기화 문제가 안생기겠죠? 그래서 critical 중요한~ section 영역! 임계영역이라고 합니다.

 

 

저번 시간의 예시를 빌려오자면

남자친구와 여자친구 쪽에서 동시에 접근하려고 했던 잔액부문 즉 withdraw라는 함수가 우리가 동기화 문제를 유발하지 않도록 살펴줘야할 임계영역이라고 부릅니다.

 

 

그래서 이 critical section 동시접근을 해결하기 위한 방법으로 lock이라는 게 있고 semaphore라는게 있고 monitor가 있고...

메시지를 이용한 가장 일반적인 방법 등등이 있는데 이번 시간을 기점으로 lock부터 하나씩 살펴보도록 할게요.

 

요약

- 임계영역이란 한순간 반드시 프로세스 하나만 진입해야 하는데, 프로그램에서 임계 자원을 이용하는 부분으로 공유 자원의 독점을 보장하는 코드 영역을 의미한다. 임계 구역은 지정된 시간이 지난 후 종료된다.

- 병렬컴퓨팅에서 둘 이상의 스레드가 동시에 접근해서는 안 되는 공유자원에 접근하는 코드의 일부로도 쓰인다. 

- 스레드가 공유자원의 배타적인 사용을 보장받기 위해서 임계 구역에 들어가거나 나올 때에는 세마포어 같은 동기화 메카니즘이 필요하다. (세마포어는 락에서 발전된 동시접근 해결방법이예요) 

 

LOCK이란?

 

Lock이라는 개념은 아주 쉬워요. Lock 의미 그대로 걸어잠구는 행위를 의미하거든요.

내가 자원을 사용하고 있는 동안에는 문을 걸어잠궈서 나 말고는 아무도 못들어오게 하는 방식입니다. 내가 쓰고 있으니 너는 기달려~~ 이게 lock이예요 이러면 동시접근 문제가 발생하지 않겠죠?

 

lock을 이용해서 전에 동기화 때 예시로 들었던 남자친구 여자친구 문제를 해결해볼게요.

 

자 동기화 문제에서 언급되었던 critical section의 withdraw 코드를 다시 가져오겠습니다.

int withdraw(account, amount) //해당 account에서 amount만큼 인출하는 함수 
{
     balance = get_balance(account); //계좌에서 현재 잔액을 가져옴
     balance = balance -amount; //남은 잔액은 기존 잔액에서 amount만큼 뺀 금액
     put_balance(account, balance); //account에 변경된 잔액을 갱신함
     return balance;
}

이 get_balance라는 함수, put_balance라고 하는 함수가 사용하는 그 데이터베이스가, 

즉 이 account의 데이터베이스가 shared data( 공유자원)이고 그 shared data를 남자친구 스레드, 여자친구 스레드가 동시에 접근(access)하려는 상황이니까 race condition입니다.

 

이 race condition을 유발시키는 code segment의 영역을 우리가 critical section이라고 하는 것!!

자 그러면 이 critical section에 한 번에 한 명씩 들어가게끔 하려면 제일 쉽게 생각하는 것이 아까 말했듯이 들어갈 때 Lock을 걸고(걸어 잠그고) 나올 때 Lock을 풀어주는 즉 lock방식입니다.

int withdraw(account, amount) //해당 account에서 amount만큼 인출하는 함수 
{
     lock(lock);
     balance = get_balance(account); //계좌에서 현재 잔액을 가져옴
     balance = balance -amount; //남은 잔액은 기존 잔액에서 amount만큼 뺀 금액
     put_balance(account, balance); //account에 변경된 잔액을 갱신함
     unlock(lock);
     return balance;
}

자 코드에 lock과 unlock의 함수가 추가 되었어요.

이렇게 보호를 한다고 하면 이제 흘러가는 흐름이 어떻게 바뀌었는지 살펴볼게요.

 

스레드A(남자친구)가 먼저 수행이 되어서 먼저 Lock을 겁니다. 첫번째니까 lock이 아예 안걸려있는 상태잖아요. 

그래서 스레드A가 lock을 걸었어요. 그리고 4번 라인을 수행하고 5까지 수행하거나 그 쯤 이때 timer interrupt가 걸렸다고 가정하고 상황극을 진행해볼게요. 

자 그럼 interrupt로 스레드B(여자친구)가 수행이 되는데 스레드B는 3번에 가서 딱 lock을 걸려고 하니까 지금 현재 스레드A에 의해서 lock이 걸려있는거예요! 그러면 더 이상 계좌 잔액을 출력해주는 4번 코드 라인으로 나아가지 못하고 스레드 B는 3에서 멈춰서 lock을 스레드A가 해제해주기를 기다립니다. 그러면 다시 운영체제의 스케줄링에 의해서 스레드A가 수행이 됩니다. 그래서 아까 5라인까지 수행한 이후를 쭉 수행한 후 7번줄에 가서 lock을 드디어 해제해줘요 그제서야 스레드B가 수행되는 겁니다. 

 

딱 lock의 이름대로 진행이 되기 때문에 이해하기가 쉬운 개념이예요. 두 개의 프로세스가 shared data에 접근하는 경우도 마찬가지입니다. 다만 multi-threaded환경에서 자주 발생하기 때문에 여기서는 그냥 용어를 스레드로 설명했을 뿐! 프로세스의 경우에는 이런 shared memory경우 말고 messaging passing방법이 있어요 (궁금하신 분은 링크로 게시글 확인하고 오기~)

 

 

 

 

 

 

 

LOCK 함수의 구현

개념은 매우 쉽죠?

이제 그럼 이 lock함수를 구현해볼게요. 어떻게 구현하느냐? 다음처럼 구현할 수 있습니다.

 
struct lock {int held =0;}  
  //lock이라고 하는 변수를 held로 만들었어요. held가 1이면 이미 자원을 쥐고 있다는 뜻이겠죠?
  //lock이 초기에는 안걸려있으니까 초기값은 0이 되겠죠
void lock(struct lock *I) {
    while (I -> held);  
     //자 여기서 held가 1이면 무한루프를 돌게 됩니다. 즉 여기서 lock이 풀릴때까지 계속 도는거예요
     //그래서 우리는 이런거를 빙글빙글 돌고 있다해서 spinlocks 
     //또는 busy-waits라고 합니다. held가 0일 때까지 계속 도는거예요.
    I->held =1;
}
void unlock (struct lock &I){
   I-> held =0;
}

자 여러분들은 5번라인(while)에서 도대체 이 무한루프를 어떻게 빠져나오나 의문이 들 수도 있어요. 그런데 지금은 스레드가 두 개가 있는거죠 unlock이라고 하는 function을 다른 스레드가 호출해주면 held의 값이 0으로 바껴서(11~13번 줄) 그 spinlock에 있던 스레드가 다른 스레드에 의해서 빠져나오게 되고 held를 1로 거는겁니다.

 

 

근데 5번의 spinlock은 뭐하면서 기다리고 있는거죠? CPU를 사용하면서 기다리고 있는거예요!! 계속 무한루프 즉 의미없는 코드를 반복수행하면서 죽치고 있는거니까요 그래서 busy-waits라고도 부릅니다. 비싼 CPU를 사용하면서 기다리고 있는건 좋지않은거예요 그래서 어쩔 수 없는 경우에만 씁니다.

 

LOCK 방법의 문제?

이런 LOCK도 문제가 있으니까 그 뒤에 다른 여러가지 알고리즘들이 제기가 됐겠죠? 이 lock의 문제는 뭐였을까요

어떤 case에서는 안돼요. TA(스레드 A)가 lock을 걸려고 들어갔어요. 그래서 held가 1이 되는 과정을 좀 더 면밀히 살펴봅시다.

1
struct lock {int held =0;}  
cs

 

held가 이 코드에 의해서 초기값이 0이예요

TA가 lock함수에 들어가서 while(I->held)코드 라인을 실행하고 held가 0이니까 while루프를 빠져나오겠죠?

근데! while루프를 빠져나오자 마자 여기서 interrupt가 걸릴 수 있잖아요. 재수없으면 우연히 그 타이밍에 인터럽트가 걸릴 수 있죠.

자 이 코드가 실행되기 전에 인터럽트가 걸려서 이제 TB가 수행될 수 있고 TB가 lock을 걸려고 들어갈거예요 그랬더니 지금 현재 held가 아직은 0입니다 이게 중요해요 TA가 접근했음에도 불구하고 TA 4번 라인의 코드에 접근하기 전에 인터럽트가 걸려서 아직 held가 0이예요. 그럼 TB는 held를 1로 만들고 critical section을 차지하고 수행을하다가 타이머 인터럽트가 걸리면 다시 TA가 실행되게 됩니다. 그러면 TA는 4번라인 전까지 수행했으니 4번라인부터 이어서 수행하게 돼요. 그럼 TA도 held를 1로 만들고 임계영역에 들어가게 되는! 즉 두 스레드가 모드 임계영역에 동시접근이 되어버리는거죠.

lock을 동시 접근을 막기 위해서 고안을 했는데 held라는 변수 자체가 critical section이 돼버려서 동시 접근이 허용되버리는 그런 문제가 생겼어요.

 

그래서 이런 문제를 LOCK이 어떻게 해결?

그래서 이Lock이라고 하는 것을 어떻게 구현해야 하는가?

이 문제를 해결하기 위해 3가지가 있습니다.

1. 소프트웨어 알고리즘

2. 더 이상 쪼개지지 않는 하드웨어 명령어로 구현하는 방법

3. 인터럽트를 disable하고 enable하는 방법

이렇게 3가지 방법이 있습니다.

 

지금 현재 소프트웨어에 의한 해결책은. 현재 운영체제들이 사용하지 않고 있아요. 느리니까. 그러니까 소프트웨어로는 구현을 하고 있지 않습니다. 하지만 공부하는 의의는 크답니다! (대표적 알고리즘: 피터슨 알고리즘을 이어서 살펴보려면 이 링크로 )

1번 2번(대표적 testandset) 3번에 대해서 좀 더 자세히 포스팅하는 시간을 갖도록 할게요.

 

포스팅이 길어졌으니 오늘은 여기서 이만~

도움이 되셨다면 광고클릭/좋아요/댓글♥_♥ 열심히 정보공유를 위해 애쓰는 작성자에게 큰 동기부여가 됩니다.

반응형