📌 들어가며

운영체제 수업 중 race condition에 대한 과제를 받았다.

x86 CPU 시뮬레이터를 통해 race condition과 test-and-set을 비교해서 race condition에 대해 분석하고 문제에 대한 답변을 작성하는 것이다.

 

그렇다면.. race condition이란?

경쟁 상태라고도 하며
두 개 이상의 프로세스가 공통 자원을 사용할 때,
타이밍이나 순서에 따라 시스템 결과값이 달라지는 것을 말한다.

race condition은 디버깅 할 때는 문제점을 찾을 수 없고, 늘 항상 같은 결과값을 보장하지 못하므로 치명적인 문제가 발생할 수 있다.

따라서 동시성 이슈를 해결해줘야한다.

 

 

📌 실습 환경

  • Ubuntu OS가 설치된Linux 실습환경
  • Python버전 2.x 이상 (Python 3 아님에 주의!)

 

 📌 분석에 필요한 파일

교수님께서 깃허브 링크를 주셔서 clone 받은 후 실습을 진행했다.
깃헙 링크를 첨부하기가.. 조금 그래서 파일 명을 가지고 설명해보도록 하겠습니다.

  • x86.py
  • race.s (race condition 발생 o)
  • test-and-set.s (race condition 발생 x)

여기서 x86.py는 x86 시뮬레이터 파일이며,
race.s와 test-and-set.s은 count 변수의 값을 1 증가시키는 어셈블리 instruction 코드이다.

 

 📌 결과값 예시

인터럽트가 걸리지 않은 상태

왼쪽의 count는 변수, ax는 레지스터이며 Thread 0, Thread 1은 공통된 자원(count)를 공유하는 쓰레드이다.

현재는 인터럽트가 아무 것도 걸리지 않았으므로 Thread 0에서 count값을 1 더해주고, Thread 1이 1이 저장된 count값을 받아 1을 더해주었다. 따라서 최종 결과값은 count가 2인 것을 볼 수 있다.

 

만약, 이 코드에서 인터럽트가 걸려서 코드의 타이밍이 변경된다면 결과값이 달라질 수도 있을 것이다.

 

 📌 race.s 코드에서 인터럽트 빈도가 1 또는 2라면?

인터럽트 빈도가 1인 경우
인터럽트 빈도가 2인 경우

두 실행 결과 모두 count값이 1이 된다.

 

인터럽트 빈도가 1이나 2일 때는 Thread 0에서 ax값을 count에 저장하는 코드가 실행되기 직전에 인터럽트가 걸린다.

이 경우에는 count값이 공유되는 데이터이므로 Thread1이 1000번 명령어인 count값을 ax레지스터로 가지고 올 때 1로 바뀌지 않고 아직 0으로 셋팅된 count값을 가지고 오기 때문에 문제가 된다.

즉 Thread0에서 ax에 1을 더하는 것과 ax값을 count에 넣는 코드가 연달아 실행되지 않아서 Thread1은 변경되지 않은 count 값을 가지고 명령어를 수행하기 때문에 최종 count값이 1이된다.

 

 📌  race.s 코드에서 인터럽트 빈도가 3이상이라면?

인터럽트 빈도가 3일 경우
인터럽트 빈도가 4일 경우

인터럽트 빈도가 3 이상이라면 count 값은 결과적으로 2가 나온다.

 

인터럽트 빈도가 3이나 4일 때, 그리고 5이상(Thread0의 코드가 이미 끝난 경우)일 때에는 인터럽트 빈도가 2 이하일 경우와는 다르게 1001, 1002번 명령어인 ax에 1을 더하는 것과 ax값을 count에 저장하는 코드가 연달아 수행되기 때문에 최종 count값은 2가 된다.

 

 📌 race.s 코드의 race condition 문제점

ax 레지스터에 각 스레드의 결과를 저장하는 부분이 올바른 타이밍에 실행되지 않으면 결과값이 달라지는 치명적인 문제가 발생한다.

이 코드에서는 1001, 1002번 명령어가 한 번에 실행되지 않을 경우에 해당하는 것이다.

따라서 인터럽트 빈도가 1 또는 2일 경우에 count 결과값이 달라지므로 race condition 문제가 발생한다고 볼 수 있다.

 

 

 📌 test-and-set.s 코드에서 인터럽트 빈도에 따른 count 값은?

test and set 코드

위 사진은 test-and-set.s를 인터럽트 없이 실행시킨 코드이다.

이 코드는 아까 race.s 코드와는 다르게 인터럽트 빈도를 다양하게 걸어도 race condition 문제가 발생하지 않는다.

 

먼저 count와 mutex는 공유되는 데이터, ax는 각각이 가진 레지스터이다.

여기서 mutex값은 lock을 걸어줬는지 아닌지를 알 수 있는 데이터로 사용된다. 

실행결과를 보면 1004번 명령어가 현재 count값을 자신의 ax레지스터로 가져오는 명령어이다. 이 명령어가 제대로 변경된 후의 count값을 가지고 오면 문제가 없을 것이다.

이 코드에서 인터럽트가 상관없는 이유는 1004번 이전의 명령어와 1007번 명령어 때문이다.

Thread1은 인터럽트를 걸고 ax값을 1로 셋팅해서 현재의 mutex값과 비교한다. Thread0이 count값을 가져오고(1004) ax에 1을 더하고(1005), ax를 count에 저장하는(1006) critical section에 해당하는 코드를 실행하기 직전에 mutex값을 1로 셋팅하기 때문에 Thread1은 mutex값을 보고 1이면 Thread0이 아직 critical section코드를 수행하고 있다고 알 수 있기 때문에 다시 1000번 명령어로 점프하게 된다.

즉, 인터럽트가 언제 걸리든 항상 Thread1은 mutex값 비교를 통해 Thread0이 count값 저장을 끝냈는지를 알 수 있다. 따라서 Thread0이 1증가된 ax값을 count에 저장하는 코드인 1006번 코드가 끝나고 mutex값을 다시 0으로 변환해주기 전까지는 Thread1이 1000번 코드로 게속 점프하게 된다. 만약 검사 했을 때 mutex값이 0이라면 Thread1에서는 더이상 1000번코드로 점프하지 않고(검사하지 않고) 비로소 1004번 코드로 넘어가게 된다.

이 코드에서 mutex값을 1로 바꿔주는 것은 critical section전 lock을 거는 것이고 mutex값을 0으로 바꿔주는 것은 lock을 해제한다는 의미를 가진다. 따라서 Thread1은 lock이 걸린지 아닌지를 확인하고 lock이 해제될 때까지 기다리다가 해제되었다고 판단되면 현재 count값을 가지고 와서 다음 명령어들을 실행한다.

결론적으로 Thread1은 count값이 1로 변경되고 나서 가지고 오기 때문에 최종 결과값은 인터럽트 빈도에 상관없이 2가 된다.

 

 

 📌 마치며

처음에 test-and-set 코드를 잘못 이해해서 전혀 다른 답을 쓸 뻔 했다.

이 실습 과제가 없었다면 아마 제대로 이해하지 못한 채로 수업이 끝났을지도 모르겠다.

 

운영 체제는 어렵다 ~

+ Recent posts