OS란?
유저의 입력을 지휘, 스케쥴링, 매니징 및 보안에 사용되는 하드웨어와 어플리케이션 영역의 중간 계층
쓰레드
- 어떤 명령의 실행주체(CPU의 쿨럭에 따라 코드 위치가 달라짐)
- 쓰레드는 한칸씩 내려오면서 코드를 실행하는데 이걸 여러개 돌리면 서로 번갈아가면서 실행되면서 동시 실행되는 것 처럼 보이는것
프로세스
정적메모리의 단위(Stack, Code, Heap, Data)
프로그램
프로세스를 메모리에 올린것
프로세스 - 쓰레드 - 프로세서
- 사용자가 exe파일을 실행
- Thread는 메모리 프로세스의 Code 영역에서 번호를 찾아서 CPU에 넣어줌
- CPU는 PCI BUS를 통해 H/W로 이동
- CPU는 한번에 하나만 처리가능하기 때문에 순차적으로 처리해주는것이 Thread의 기능
- Thread는 ThreadBlock을 이용해서 위치나 순서를 기억해서 처리하고, 컨텍스트 스위칭 한다.
즉 프로세스와 쓰레드는 1:1개념이 아니기 때문에 많이 만들수록 속도는 느려질수 밖에 없다.
속도는 빠르게 못하나 효율적으로 사용하는 것은 가능하다.
커널쓰레드와 유저쓰레드
- 쓰레드는 커널레벨 쓰레드와 유저레벨 쓰레드가 있다.
- 프로세스가 실행이 되면 하나의 커널쓰레드가 만들어진다.
- 여러개의 유저쓰레드는 하나의 커널쓰레드의 자원을 공유할수도 있다.
- 커널쓰레드는 OS를 통해서 만들수 있다.
즉 유저쓰레드 N : 커널쓰레드 M 구조이다.
커널쓰레드와 유저쓰레드의 관계
코어가 하나일때 쓰레드가 두개 있으면 쓰레드가 커널쓰레드랑 시분할적으로 왔다갔다하면서 아래의 방법으로 스위칭된다.
- 커널은 자신을 실행할 만큼 실행하고 핸들러에 미리 예약해서 자기자신을 죽이고 유저쓰레드를 실행시키고 핸들러가 인터럽트가 오면 펑션에서 다시 유저쓰레드를 죽이고 커널로 스위칭을 한다.
- 쓰레드에는 쓰레드 데이터 블록이 같이 존재해서 OS는 이것을 보고 얼만큼 실행되었는지 파악하여 스케쥴링을 한다.
위와 같은 일련의 과정이 ns안에 모두 수행되기 때문에 우리는 프로그램이 여러개 뛰어져 있는것처럼 보이게 된다.
커널오브젝트
- 커널 오브젝트를 만들기 위해서는 OS를 사용하여 만들고 오브젝트를 다루는 핸들을 리턴받아 유저레벨에는 핸들을 통해 간접적으로 조정한다. (오브젝트 자체를 주는 것이 아님)
- 커널오브젝트를 사용하는 이유는 프로세스, 메모리, 쓰레드를 공유 할수 있다.
즉 공유가 가능하기 때문에 커널 오브젝트 1 : 핸들 N 관계이다.
동기화(Synchronization)
int a = 1;
function int T()
{
a++;
return a;
}
만약 위의 코드를 2개의 쓰레드로 돌릴 경우를 생각해보자 함수가 끝날때 까지 쓰레드가 스위칭 되지 않았다면 2가 출력된다.
하지만 첫번째 쓰레드가 a++를 실행하고 스위칭 되고, 두번째 쓰레드도 a++를 실행한뒤 스위칭 된다면, 첫번째 쓰레드에서 a의 결과는 3이 될수있다.
즉 쓰레드가 다중코어일때 병렬로 프로그램이 실행되기 때문에 사용자가 원했던 결과를 보장받을수 없다. 이럴때 순서에 대한 약속을 정의하는 것이 동기화다.
다음은 동기화하는 총 6가지 방법을 슈도코드로 나타낸 것 입니다.
Mutex
- 동기화 커널 오브젝트
int a = 0;
T()
{
lock()
a++;
unlock()
return a;
}
- 첫번째 쓰레드가 Lock을 만나면 뮤텍스를 잠근다.
- 두번째 쓰레드는 만약 뮤텍스가 잠겨있을경우 쓰레드 스케쥴링을 반납한다.
즉 a++가 실행되는 부분은 하나의 쓰레드만 돌아가게 된다.
- 멀티프로세스일 때 서로의 메모리는 따로 사용하지만, 동기화는 해야될 때 사용한다.
Critical Section
- 동기화 유저 오브젝트
int a = 0;
T()
{
lock()
a++;
unlock()
return a;
}
- 쓰레드 스케쥴링을 반납하지 못하기때문에 Lock으로 잠겨있을경우 계속해서 돌면서 확인하게 된다.
- 뮤텍스는 커널까지 다녀와야하기 lock이 자주 풀리는 경우에는 유저쓰레드를 사용하는것이 성능적으로 좋다.
- 유저쓰레드를 사용한다면 크리티컬 섹션을 사용 해야한다.
Internal Express
- 동기화 유저 오브젝트
int a = 0;
T()
{
interAdd(a)
return a;
}
- CPU 파이프라인이 따로 있어서, Add하는 코드를 CPU에서 한번에 처리한다.
- 할수있는 계산이 한정적이다. (Add와 Sub 뿐)
Sema phore
- 동기화 커널 오브젝트
int a = 0;
T()
{
lock(3)
a++;
unlock()
return a;
}
- Mutex에서 쓰레드를 N개의 쓰레드를 허용하는 기능
- 화장실을 예로 많이 드는데 칸이 꽉차면 다른 사람이 사용 못하는것과 동일하다.
- 보통 동기화의 개념보단 게임의 최대 인원을 정하고 대기열 느낌으로 많이 쓰인다.
Event
- 동기화 커널 오브젝트
EventHanle e = CreateEvent();
T1()
{
e.wait();
sendPacket();
}
T2()
{
if(userInput)
{
fireEvnet(e);
}
}
- 이벤트가 발생되기 전까지 T1은 e.wait()에서 대기
- 이벤트가 발생되면 T1에 wait()이 풀리면서 다음 작업 실행
- 코드를 동기화하는것이 아닌 행동 절차를 동기화 할 때 사용
- 만약 e.wait()하는 쓰레드가 10개라면 T2가 10개의 쓰레드를 동작시킬수 있다.
Atomic
- 동기화 유저 오브젝트
atomic<int> a = 0;
function T()
{
a++;
return a;
}
- atomic으로 선언한 변수는 쓰레드가 1개만 접근할 수 있다.
- 코드를 잠그는 것이 아닌 a라는 객체를 잠근다.
공유자원
공유자원의 주체는 Thread가 공유자원에 접근한다.
데드락
t1이 Alock을 원하고 있고, 그전까지는 Block을 Unlock시키지 않음.
t2은 Block을 원하고 있고, 그전까지는 Alock을 Unlock시키지 않음.
모든 쓰레드가 대기상태가 되어버리는 상태
공유자원에 접근하기 위해 필요한 락을 서로가 바라보고있는 상태
데드락 피하는 방법 (finally 사용)
lock을 했으면 반드시 unlock()을 해준다.
{
lock();
try{}
catch{}
finally{unlock()}
unlock();
}
로직 실행도중 예외가 발생할 때도 있기 때문에, try, catch를 사용한다.
데드락 피하는 방법 (RAII 패턴 사용)
소멸자를 사용하여 unlock시키는방법
struct RAII()
{
RAII(){
lock()
}
~RAII(){
unlock()
}
}
객체를 만들면 lock이 걸리고 함수를 벗어나면 unlock이 됨
데드락 피하는 방법 (이중락을 사용을 지양하자)
각각의 락이 잠길수도, 풀릴수도 있기때문에 데드락이 무조건 일어난다.
라이브락
서로가 양보해서 실행이 안되는 상태