2015. 1. 29.

[OS]가상 메모리(Virtual Memory) - 3. 페이지(Page), 페이지 프레임(Page Frame), 페이지 테이블(Page Table)

 시스템과 메모리 할당 간의 관계와 가상 주소 시스템까지 알아보았다.
 이제 아직 해결하지 못한 두 문제를 해결하고자 한다. 이 문제를 해결하면 가상 메모리에 대한 핵심을 거의 이해했다고 봐도 무방하다.


아직 해결하지 못한 문제들 

 1. 32비트 시스템에서 하드디스크를 이용하여 프로세스마다 가상 메모리 공간 4G바이트를 할당하는 것은 효율적이지 못하다.

 2. 하드디스크는 메인 메모리와 비교했을 때 느리다.

 이 두 가지 문제를 해결하기 위해서 페이지, 페이지 프레임, 페이지 테이블이 필요하다.


페이지, 페이지 프레임, 페이지 테이블

 먼저 1번 문제를 해결해보자.

 불필요하게 많은 공간을 하드디스크에 할당하는 것은 올바르지 못하다. 4G바이트나 되는 공간을 할당하면 낭비도 심하고 시간도 오래 걸리기 때문이다. 그래서 좀더 작은 단위로 할당하는 것은 어떤가?



 위의 그림을 살펴보자. 32비트 시스템에서 표현할 수 있는 메모리 주소의 범위는 4G만큼이다. 이를 사용하여 가상 주소를 표시한다. 그림 좌측의 긴 막대가 바로 가상 주소 공간이다. 이는 논리적인 주소일뿐 물리적인 메모리 주소가 아니다. 그래서 가상 주소 공간의 크기는 4G만큼이지만 실제 메인 메모리의 크기와 차이가 발생한다. 여튼 그림에서 가상 주소 공간의 크기는 64K이다. 64K라면 16비트 시스템이라고 유추할 수 있다. 그림에서 64K의 공간을 16개로 분할하였다. 왜 분할하였겠는가? 한 번에 4G바이트씩 할당하는 것은 비효율적이기 때문이다. 그래서 4K바이트로 할당 용량을 줄여서 낭비되는 공간을 줄였다. 1번 문제점의 해결책이 바로 이 방법이다. 이렇게 4K로 나눈 가상 주고 공간의 블록을 페이지(page)라고 부른다.

  이제 그림의 오른쪽 짧은 막대를 보자. 물리적인 메모리 주소라고 표시되어 있다. 이것이 메인 메모리의 실제 주소 공간이다. 메인 메모리의 주소 공간 역시 4K바이트 단위로 나눠져 있다. 가상 주소 공간과 크기를 맞추었기 때문이다. 메인 메모리의 블럭을 페이지 프레임(page frame)이라고 부른다.

 그럼 가상 주소 공간과 물리 주소 공간을 맵핑(mapping)시키기만 하면 된다. 가상 주소 공간에 숫자는 물리 주소 공간의 어디에 맵핑되는지를 나타낸다. 예를 들어 0 ~ 4K 구간은 물리 주소 공간의 8 ~ 12K 구간에 맵핑되어 있다. 이렇게 맵핑을 하면 무엇이 좋은가? 이는 2번 문제점에 대한 답이 된다. 가상 주소 공간을 활용하면 물리 주소 공간에 비해서 표현할 수 있는 메모리 주소의 범위가 증가하고, 더 많은 메모리를 사용하는 효과를 얻을 수 있다. 다만 하드디스크는 느리다는 단점이 있다. 느린 하드디스크의 한계를 극복하고자 하드디스크와 맵핑된 메인 메모리를 활용한다. 이는 메모리 계층 구조와 동일한 원리이다. 


 가상 주소 공간과 물리 주소 공간을 맵핑한 정보는 페이지 테이블(page table)을 통해 관리된다. 위의 그림이 바로 페이지 테이블에 대한 설명이다.

 만약 가상 주소 공간 전체에 페이지가 할당되어 있는 경우를 생각해보자. 메인 메모리는 가상 주소 공간의 페이지 전체를 담을 수 없다. 그럼 가상 주소 공간의 일부분만 메인 메모리에 올려서 사용할 수 밖에 없을 것이다. 그런데 필요한 데이터가 메인 메모리의 존재하지 않으면 어떻게 하나? 이는 캐쉬 알고리즘을 이해하고 있다면 쉽게 알 수 있다. 가상 주소 공간에 존재하는 즉 실제로는 하드디스크에 저장된 데이터를 메인 메모리로 올리고, 가장 불필요하다고 판단되는 데이터를 다시 하드디스크로 내리면 된다. 이 과정에 대해서 조금더 부연 설명을 하면 올리고 내리는 데이터는 스왑파일(swap file)이라는 형태로 저장된다. 내리고자 하는 데이터를 스왑파일로 저장하여 하드디스크로 저장하고, 올리고자 하는 데이터 또한 스왑파일로 저장된 상태이므로 메인 메모리로 바로 올리면 된다. 


정리하면

 시스템이 표현할 수 있는 주소 범위를 활용하여 가상 주소 공간으로 사용한다.
 가상 주소 공간은 하드디스크를 활용하여 데이터를 저장할 수 있다.
 프로세스를 실행하는데 필요한 데이터는 메인 메모리로, 불필요한 데이터는 하드디스크로 저장한다.
 가상 주소 공간을 관리하는 정보는 페이지 테이블에 보관되어 있다.
 가상 메모리는 메모리 계층 구조, 캐쉬의 장점을 활용한다.

[OS]가상메모리(VIrtual Memory) - 2. 가상 주소 시스템(Virtual Address System)

 시스템과 메모리 할당 사이의 관계에서 해결하지 못했던 궁금증을 이번에는 반드시 개운하게 풀어보고자 한다. 하지만 포스팅하면서도 또 분량이 늘어져서 3편, 4편으로 이어질 것 같다. 분명히.


가상주소(virtual address)

 지난 포스팅에서 32비트 시스템에서 2G바이트 RAM으로 어떻게 4G바이트를 할당할 수 있는지 답을 내리지 못하고 마무리하였다. 분명한 사실은 4G바이트를 할당하였다는 점이다. 어떻게든 할당하였다. 메모리가 모자람에도 불구하고 할당하였다는 점은 실재 메모리말고 다른 어떤 메모리가 있다는 것을 의미한다. 그래야 실재 메모리 이상을 할당할 것이 아닌가. 그런 메모리를 할당하고 관리하는 주체가 바로 운영체제이다.


 먼저 32비트 시스템으로 돌아가보자. 프로세스를 하나 실행하여 메모리를 할당받는다. 최대로 할당받을 수 있는 크기는 물리적인 메모리 크기인 2G 바이트와 별개로 시스템이 표현할 수 있는 최대의 메모리 주소 공간의 크기였다. 즉 4G바이트만큼 할당받을 수 있었다. 물리적 메모리를 초과하는 용량은 4G바이트를 도대체 어디서 할당한다는 말인가? 이것 의문을 먼저 해결해보자.

 메모리 계층 구조를 떠올려보자. 느린 대신 값싸고 용량이 엄청 큰 하드디스크는 요즘 1T바이트짜리도 많이 사용한다. 그리고 메모리 계층 구조에서 알 수 있듯이 하드디스크라고 해서 메인 메모리 같은 역할을 수행하지 말라는 법은 없다. 느려서 문제일 뿐이다. 속도가 느리다는 문제를 극복할 수 있는 대안이 나온다면 하드디스크의 빵빵한 용량을 메모리 할당에 활용할 수는 있을 것 같다는 생각이 든다. 그리고 추가로 4G바이트만큼을 늘 할당할 필요도 없을 것 같다. 한번에 4G바이트만큼이나 차지하는 프로세스는 흔치 않다.


출처는 내 컴퓨터^^

 자극적인 빨간 색 네모 영역이 바로 사용하는 메모리를 표시하고 있다. Chrome이 매우 열심히 일을 하고 있다. 가장 큰 메모리를 Chrome 중 하나가 약 90,000K바이트이다. 90M바이트이다. 4G바이트로 할당하는 것은 비효율의 극치라고 볼 수 있다. 그러니 적당한 크기로 할당하는 것이 좋겠다는 생각이 든다. 실재로 운영체제도 프로세스 당 4G바이트씩 할당하는 짓은 하지 않는다. 이론적으로 그렇다는 말이다.

 지금까지의 내용을 정리해보자. RAM과 같은 메모리의 물리적 크기의 제한은 있지만 이를 초과하는 메모리 할당을 하드디스크로 할 수 있을 것 같다. 그리고 하드디스크를 사용하는 만큼 속도 문제를 고려해야할 것 같다.

 그럼 드디어 가상주소를 알아보자. 32비트 시스템에서 프로세스에 할당할 수 있는 최대 크기인 4G바이트 메모리 공간은 RAM가 같은 메모리의 물리적 주소가 아니라 실제로 존재하지 않은 가상의 주소이다. 그리고 가상의 주소를 사용하여 할당한 4G바이트에 해당하는 공간을 가상 메모리 공간(virtual address space)라고 한다.

 결국 32비트 시스템에서 할당할 수 있는 4G바이트 크기 메모리 공간은 하드디스크를 이용한 가상의 메모리 공간이었고, 이런 가상의 공간에 대한 지정해주기 위해서 가상 주소 시스템이 필요하다. 

 이제 가상 주소 공간이 어떻게 하드디스크에 구성이 되는지, 그리고 실제 RAM과는 어떤 관계가 있는지 살펴보면 된다.

[OS]가상 메모리(Virtual Memory) - 1. 시스템과 메모리 할당 사이의 관계

 가상 메모리가 등장했다. 윈도우를 사용하다보면 어쩌다가 한 번쯤은 만나볼만한 녀석이다. 나는 종종 가상 메모리를 늘려달라는 잔소리를 Windows7으로부터 들은 경험이 있다.


  Google에서 검색해보면 가상 메모리의 크기를 조절하여 Windows7을 최적화할 수 있다는 글도 보인다. 이래나 저래나 가상메모리가 중요한 녀석일 것 같다는 생각은 가질만하다.


32비트 시스템과 프로세스 메모리 할당

 처음부터 '이거 뭐야?'하는 내용이 등장한다. 32비트 시스템에서 프로세스 생성 시 4G바이트의 메모리를 할당받을 수 있다고 한다. 시작부터 황당하기 그지 없다. 내가 전에 사용했던 컴퓨터는 32비트 시스템이고, 돈 좀 써서 2G바이트 RAM를 장착했었다. 그 당시에 상당히 좋은 사양이었고, 게임도 아주 잘 돌아갔다. 그런데 이상한 점은 32비트 시스템에서 프로세스 생성 시 4G바이트의 메모리를 할당받을 수 있다는 사실이다. 나는 분명 2G바이트 크기의 RAM을 가지고 있었다. '모자라는 상태에서 어떻게 메모리를 할당했지?', '뭐하러 4G바이트나 되는 크기를 할당하지? 윈도우 계산기 켜는데 4G를 할당한다고?' 등등의 의문이 생길 수 밖에 없다. 

 이런 의문들을 하나씩 해결해보자. 먼저 32비트 시스템에서 프로세스 생성 시 최대 4G바이트의 메모리를 할당받을 수 있다는데 왜 그런가? 32비트 시스템은 또 무슨 말인가? 이것에 대한 답을 찾기위해서는 시스템 버스(system bus)라는 것에 대해서 간단히 언급할 필요가 있다. 시스템 버스는 컴퓨터에서 데이터를 이곳에서 저곳으로 옮기는 통로와 이를 관리하는 장치들이라고 생각하자. 이 시스템 버스에서 데이터가 이동하는 실질적인 통로 역할을 하는 것이 바로 I/O 버스이다. 이 I/O 버스가 한번에 전달할 수 있는 데이터의 크기가 바로 32비트이다. 이것 말고도 32비트 시스템에서 32비트가 의미하는 것은 바로 CPU와 같은 장치가 한번에 처리할 수 있는 데이터의 크기이다. 정리하면 한번에 I/O 버스로 이동시킬 수 있는 데이터의 크기도 32비트이고, CPU가 처리할 수 있는 데이터의 크기도 32비트이다. 이 둘의 크기가 같은 것은 우연이 아니다. 한번에 32비트를 처리할 수 있는데, 16비트를 전달해주는 것도 웃긴 일이고 64비트를 전달해주는 것은 무리이다.


 위 그림에서 address bus, control bus, data bus라고 표시된 것들이 바로 시스템 버스에 속하는 녀석들이다. 각 장치들을 연결하면서 데이터 전달 통로 역할을 한다는 사실에 집중하자. 그리고 3가지 bus들의 역할은 각각 다르지만 여기서는 넘어가도록 하겠다.

 그런데 프로세스와 쓰레드를 포스팅할 때도 그랬지만 프로그래머에게 가장 중요한 것 중 하나가 메모리 아니던가. 프로그래머는 메모리를 생각하지 않을 수 없다. 32비트 시스템을 생각할 때도 메모리 측면에서 접근해보자. 

 또 어이없는 상황을 가정해보자. 나는 8비트 시스템에 100G 바이트 RAM를 소유하고 있다. 100G바이트라니 엄청 신나는 일이다. 과연 신나는 일인지 생각해보자. 먼저 8비트 시스템에서는 메모리 주소 공간을 표현할 수 있는 범위는 2^8 만큼이다. 왜 2^8인가? 주소값을 표현하는데 8비트 이상 사용할 수 없기 때문이다.(주소값도 데이터 중 하나이다) 8비트 시스템이 한번에 처리할 수 있는 비트 수는 8개이고, 이에 따라 한번에 다룰 수 있는 주소값은 2^8만큼을 표현할 수 있다. 결국 2^8 = 256, 즉 0 ~ 255까지만 주소를 표현할 수 있다. 100G 바이트 RAM을 달아놓으면 뭐하나? 결국 쓸 수 있는 메모리 공간의 크기는 고작 256바이트 만큼이다. 1/4G 바이트 밖에 사용하지 못하였다.

 그러면 32비트 시스템의 경우는 어떠할까? 한번에 처리할 수 있는 데이터의 크기가 32비트이고, 주소값을 표현하는데 32비트를 모두 사용한다면 최대로 표현할 수 있는 메모리 공간의 크기는 2^32가 된다. 딱 봐도 커보이는 숫자이다. 이를 Windows에 있는 전지전능한 calc.exe를 실행시켜 계산해보면 4294967296이라는 숫자가 나온다. 이 숫자가 바로 4G 바이트를 의미하는 숫자이다. 32비트 시스템에서는 프로세스는 최대 4G 바이트를 할당받을 수 있다는 이유가 이제 설명되었으리라 믿는다.

 최근에는 64비트 시스템이 일반화되고 있다. 그럼 64비트 시스템의 경우 프로세스 당 최대 얼마만큼의 메모리를 할당할 수 있는가? calc.exe의 도움을 받아서 계산해보자. 무려 18446744073709551616라는 숫자가 계산된다. 그냥봐도 큰 수이다. 매우 크다. 여튼 중요한 사실은 한 번에 처리할 수 있는 데이터의 단위가 곧 몇 비트 시스템인지를 결정하고, 이는 곧 메모리를 나타내는 주소값을 표현하는데도 연결되는 사항이라는 사실이다.


아직도 해결되지 못한 궁금증

 시스템과 메모리 할당 사이의 관계는 이해했다. 그렇다면 이제 처음으로 돌아가서 32비트 시스템에서 2G 바이트 RAM에 어떻게 4G 바이트 메모리를 할당할 수 있는지 따져보자. 뜬금없지만 포스트이 길어졌으므로 다음 포스팅으로 넘겨야겠다.    

2015. 1. 28.

[OS]캐쉬(Cache)와 캐쉬 알고리즘(Cache Algorithm)

메모리 계층 구조(memory hierarchy)


 메모리 계층 구조에 대해서 간단히 설명하고 넘어가자.

 메모리 계층 구조는 다음과 같이 작동하도록 구성되어 있다. CPU가 데이터를 처리하기 위해서는 레지스터에 데이터가 존재해야 한다. 하지만 레지스터는 가장 빠른 대신에 가장 비싸기 때문에 용량을 한정적으로 사용해야 한다. 만약 CPU에서 처리하고자 하는 데이터가 레지스터에 있으면 그냥 처리하면 되지만, 없다면 CPU cache인 L1 cache에서 데이터가 있는지 없는지 확인해야 한다. 만약 L1 cache에 데이터가 있다면 그 데이터를 레지스터로 올려서 CPU가 처리하면 되지만, 없다면 L1 cache의 하위 메모리인 L2 cache에서 데이터가 있는지 없는지 확인한다. 즉, 해당 계층 구조 내에서 데이터가 존재하지 않으면 하위 계층 구조를 확인하는 방식으로 메모리 계층 구조는 사용된다.

 다시 말해 피라미드의 아래쪽으로 내려갈수록 용량은 커지지만 속도는 느려진다. 그리고 아래쪽으로 내려갈수록 단위 용량당 저장장치의 가격이 비싸진다. 즉 비싸고 빠른 놈들을 적게 쓰고, 싸고 느린 놈들을 많이 쓰겠다는 의미이다. 그럴려면 CPU가 요구하는 데이터를 적절하게 저장하고 있어야 한다. 계층구조에서 CPU로부터의 거리가 가까운 곳에 존재할수록 속도가 빠르므로 CPU가 데이터를 처리하고 좋다. 하지만 상위 계층일수록 비싸기 때문에 데이터를 계층구조 상에서 어떻게 저장하고 관리할지가 중요한 문제가 된다.


캐쉬 알고리즘(cache algorithm)

 메모리 계층 구조의 성격에 대해서 알아보았다. 결국은 경제성과 효율성 때문에 발생한 문제이다. 만약 가장 싸고도 가장 빠른 저장장치가 있다면 저런 계층 구조를 만들 필요는 없을 것이다. 하지만 현재는 필연적으로 메모리 계층 구조가 필요하고, 이 계층 구조를 효율적으로 사용할 방법이 필요하다. 이에 대한 내용이 캐쉬 알고리즘(cache algorithm)이다. 

 캐쉬 알고리즘(을 이해하기 위해서 먼저 locality에 대한 이해가 필요하다. locality는 temporal locality와 spatial locality로 나눈다.

 temporal locality는 대부분 프로그램은 한 번 접근이 이루어진 메모리 영역에 자주 접근하는 특성을 의미한다.

 위의 코드에서 변수 total이 저장된 메모리 공간은 1000번 접근이 이루어진다. 이는 temporal locality의 좋은 예이다. 

 spatial locality는 이미 이뤄진 영역 근처에 접근할 확률이 높다는 특성을 의미한다.

 위의 코드는 삽입 정렬(insertion sort)이다. 위 코드에서 arr[j + 1] = arr[j]과 같은 코드가 spatial locality의 좋은 예이다. 

 사실 메모리 계층 구조는 temporal locality와 spatial locality 덕분에 성능 향상을 이룰 수 있다. 상위 계층에 저장되어 있는 데이터가 있다면, 그 데이터와 그 주변의 데이터에 프로그램이 접근할 확률이 높다. 그래서 하위 계층에는 locality를 만족하도록 그 데이터와 그 주변의 데이터를 저장한다. 즉 데이터와 그 주변 데이터를 한 번에 저장하려면 블록 단위로 처리할 수 밖에 없다. 그러므로 메모리 계층 구조에서 데이터의 이동은 블록 단위로 이뤄진다.

 그렇다면 L1 cache에 데이터가 없어서 L2 cache로부터 데이터 블록을 읽어 들여야 하는 상황을 살펴보자. 운영체제가 작동하고, 다른 프로그램이 실행되는 상황이라면 cache는 항상 가득 차있다.(하드 디스크는 제외) L1 cache가 가득 찬 상태에서 L2 cache의 데이터 블록을 L1 cache로 읽어 들이려면 L1 cache 중 일부분을 밀어낼 수밖에 없다. 어떤 부분을 밀어낼지에 대한 정책이 캐쉬 교체 정책(cache's replacement policy)이다. 가장 보편적인 캐쉬 교체 정책은 LRU(Least-Recently Used)이다. 용어 그대로 가장 오래 전 접근한 블록에 밀어낸다.

[OS]임계영역(Critical Section) 접근 동기화3 - 세마포어(Semaphore) 기반의 동기화

뮤텍스에 대한 지식을 가지고 있다면 세마포어는 공짜이다.
왜냐하면 뮤텍스를 세마포어 중 하나로 볼 수 있기 때문이다.

이 글은 Windows system을 기반으로 설명하고 있습니다.


세마포어(semaphore)

 critical section과 뮤텍스를 설명할 때는 옷가게의 fitting room을 예로 들었다. 세마포어는 fitting room으로 설명하기 힘들다. 그래서 별다방 같은 카페로 설명하겠다.

 내가 운영하는 별다방이 있다. 돈이 부족해서 좌석은 테이블이 5개 밖에 안된다. 상황을 좀더 단순화하기 위해서 한 테이블에는 한 명만 사용할 수 있다. 손님이 들어와서 빈 테이블을 보면 찾아서 앉고 시간을 보내다가 집에 간다. 이 카페는 5개의 테이블만큼 손님을 받을 수가 있다. 5개의 테이블이 가득 차면 다음 손님은 기다려야 한다. 옷가게 fitting room과 비교해보자. fitting room에서는 한 명만 이용할 수 있었다. 하지만 카페에서는 다섯 테이블을 이용할 수 있다. 이 점이 뮤텍스와 세마포어의 가장 큰 차이점이다.

 카페가 critical section이 되고, 손님은 쓰레드이다. 5개의 테이블이 바로 세마포어이다. 뮤텍스가 5개가 되는 상황으로 이해할 수 있다. fitting room으로 상황을 바꾸면 이렇게 이해할 수 있다. 최대 5명이 동시에 이용할 수 있는 큰 fitting room이 있다. 개인의 권리 따위는 없는 이상한 fitting room이다. 여튼 이런 fitting room을 이용하려면 5개의 열쇠가 필요하다. 5개의 열쇠, 즉, 5개의 뮤텍스가 바로 세마포어가 된다. 그래서 세마포어는 뮤텍스와 다르게 count와 관련된 사항이 중요하다.


 위 그림은 뮤텍스를 설명하는 그림이다. 세마포어는 다수의 뮤텍스로 이해할 수 있다고 했다. 이를 그림으로 표현하면 다음과 같다.


 임계영역으로 들어갈 수 있는 열쇠인 세마포어가 3개이다. 어떤 쓰레드가 세마포어를 얻으면 남은 세마포어의 수는 2개이다. 또 어떤 쓰레드가 세마포어를 얻어서 진입했다. 남은 세마포어의 수는 1개이다. 또 어떤 쓰레드가 세마포어를 얻어서 진입했다. 남은 세마포어의 수는 0개이다. 또 어떤 쓰레드가 임계영역에 진입할 때, 더이상 남은 세마포어가 없기 때문에 임계영역 내에 어떤 쓰레드가 빠져나오면서 세마포어를 반납할 때까지 기다려야 한다.

 이상이 세마포어에 대한 간략한 설명이다. 그럼 이제 세마포어를 사용하는 코드를 보도록 하자.


세마포어기반의 동기화


 사용하는 형태는 뮤텍스와 유사하다. 다른 점이라면 세마포어는 생성할 때, 뮤텍스와 달리 접근 가능한 쓰레드의 개수를 정할 수 있다. CreateSemaphore() 함수의 2번째 인자이다. 뿐만 아니라 세마포어가 지닐 수 있는 값의 최대 크기를 지정할 수도 있다. 3번째 인자이다. 최대 크기를 1로 정하면 이 세마포어는 뮤텍스와 동일한 기능을 하게 된다. 접근 가능한 쓰레드의 개수가 1개라면 뮤텍스와 동일하기 때문이다.

2015. 1. 27.

[OS]임계영역(Critical Section) 접근 동기화2 - 뮤텍스(Mutex) 기반의 동기화

 임계영역 동기화와 가장 기본적인 아이디어에 대해서 알아보았다.
 이번에는 유사하지만 조금 다른 동기화 기법을 알아보자.
 참고로 예제는 Windows system을 기반으로 하고 있다.


뮤텍스(Mutex)

 뮤텍스 역시 기본적인 아이디어는 critical section 기반의 동기화와 유사하다. 임계영역 기반의 동기화에 뮤텍스라는 오브젝트만 추가했다고 생각하자. 먼저 그림을 보자.

 임계영역 기반의 동기화와 마찬가지로 옷가게의 fitting room을 예로 들어 설명하겠다. fitting room은 한 사람만 들어가야 한다. 한 사람만 들어가려면 일반적으로 '사용중' 표시가 fitting room 문에 표시된다. '사용중'이라면 다른 사람은 fitting room 안으로 들어갈 수 없다. 여기까지의 설명은 critical section 기반의 동기화이다.

 이를 기본으로 하여 뮤텍스를 살펴보자. 이번 fitting room은 '사용중' 표시를 하는 대신에 사용자들에게 열쇠를 지급한다. 열쇠를 지급받은 사용자는 fitting room에 들어가서 옷을 갈아입는다. 열쇠가 없는 사람은 기다린다. 옷을 다 갈아입은 사람은 그 열쇠를 카운터에 맡긴다. fitting room을 사용할 사람은 카운터로 가서 열쇠를 받아서 사용한다. 

 이것이 바로 대략적인 뮤텍스에 대한 설명이다. fitting room이 임계영역이 되고, 열쇠가 mutex object 혹은 뮤텍스가 된다. 사용자는 쓰레드이다. 아래 그림을 보며 정리하자.



 임계영역에 진입하길 원하는 3개의 쓰레드가 있다. 고양이가 쓰레드이다. 앞에서 설명하였듯이 임계영역(그림에서는 protected resource)에 접근하기 위해서 뮤텍스라는 열쇠가 필요하다. 뮤텍스를 얻어야 임계영역에 진입할 수 있다.



 한 쓰레드가 뮤텍스를 획득했다. 이 쓰레드는 임계영역에 진입할 수 있게 된다.



 진입한 쓰레드는 임계영역을 처리한다. 이 때 다른 쓰레드들은 아무일도 못하고 쉬게 된다. 뮤텍스가 반환될 때까지 쉬면서 기다릴 수 밖에 없다.(BLOCK 상태를 의미한다)



  임계영역에 있던 쓰레드가 빠져나와 뮤텍스를 반환했다. 쉬던 쓰레드들이 이제 뮤텍스를 가질 수 있다. 이 쓰레드들 중 하나가 뮤텍스를 가지게 될 것이다. 그리고 지금까지의 과정을 반복된다.


 뮤텍스 기반의 동기화 기법

 먼저 CreateMutex() 함수로 뮤텍스를 생성한다.

 WaitForSingleObject() 함수로 뮤텍스는 한 쓰레드만 가질 수 있다. 이 함수는 signaled 상태의 커널 오브젝트를 반환할 경우, 해당 커널 오브젝트를 non-signaled 상태로 변경해버린다. 결국 한 쓰레드가 뮤텍스를 획득하자마자 뮤텍스커널 오브젝트 상태는 non-signaled 상태가 되기 때문에 다른 쓰레드들은 뮤텍스를 가질 수 없다. 이 때문에 하나의 쓰레드만 임계영역에 진입할 수 있다.

 임계영역을 빠져나온 쓰레드는 뮤텍스를 반납해야 한다. 그래야지 다른 쓰레드가 뮤텍스를 얻어 임계영역에 접근할 수 있다. 이를 위해 ReleaseMutex() 함수를 호출한다.

 그리고 뮤텍스 리소스를 반납한다.

[OS]임계영역(Critical Section) 접근 동기화1 - critical section 기반의 동기화 기법

 쓰레드임계영역을 통해서 쓰레드가 메모리 영역을 어떻게 공유하는지, 공유한 메모리에 동시 접근할 수 있기 때문에 발생할 수 있는 잠재적인 문제에 대해서 알아보았다.

 이를 바탕으로 임계영역의 동시접근을 막는 방법들을 알아보고자 한다.
 참고로 아래의 내용은 WIndows system을 기반으로 설명하고 있다.

임계영역과 동기화

 흔기 동기화라고 그러면 어떤 것들을 일치시켜주는 것을 의미한다. 클라우드와 로컬 저장소 사이의 동기화가 실행되면 클라우드에 저장된 데이터와 로컬 저장소에 저장된 데이터가 일치하지 않는가. 그런데 여기서 동기화는 조금 다른 의미를 지니고 있다. 일치한다는 의미가 아니라 순서에 있어서 질서가 잘 지켜지고 있다는 의미이다.

 질서가 잘 지켜지고 있다는 말은 임계영역에 접근하는 쓰레드의 순서가 잘 지켜진다는 것을 뜻한다. 동시접근하면 발생하는 문제를 예방하기 위해 동시접근을 예방하는 동시에 한 번에 하나의 쓰레드만 임계영역에 접근할 수 있다록 하는 것이 바로 임계영역 접근 동기화이다.


<출처: 윤성우 저, '윈도우즈 시스템 프로그래밍'>

 위의 코드는 전역변수를 선언하였고, 쓰레드들이 그 전역변수에 임의로 접근하는 코드이다. 전에도 말했지만 쓰레드의 실행순서를 예측하는 것은 불가능하다. ResumeThread() 함수를 통해 hThread[0] ~ hThread[5]를 거의 동시에 실행시키므로, IncreaseCount() 함수 내부의 gTotalCount에 하나씩 차례대로 접근할 것이라고 오해해서는 안된다. 쓰레드는 공유하는 메모리 영역에 자유롭게 접근할 수 있기 때문이다. 우리는 total count가 6000이 나올 것이라고 예상하지만, 6000이 나오지 않을 수도 있다.


임계영역에 대한 간단한 이해


 임계영역에 접근하려는 3개의 쓰레드가 있다. 고양이가 바로 쓰레드이다. 



 3개의 쓰레드들이 동시에 임계영역에 접근하려고 한다. 하지만 이는 알고 있듯이 문제가 발생한다. 그래서 하나의 쓰레드만 임계영역에 접근할 수 있다. 하나의 쓰레드가 임계영역에 진입하면 다른 쓰레드들은 기다린다. 위 그림에서는 쓰레드들이 잠을 자는 것으로 그려놓았는데, 실제로 BLOCK 상태가 된다. 



 먼저 들어간 쓰레드가 임계영역을 빠져나오면, 잠자던(BLOCK 상태) 쓰레드 중에 한  쓰레드가 임계영역에 진입할 수 있다. 진입하지 못한 쓰레드는 계속 잠을 자게 된다.

 대략적으로 임계영역을 동기화 하는 방법을 알았다. 이제 코드에 적용해보자.


임계영역 기반의 동기화 기법

 critical section은 임계영역을 나타내는 용어인 'critical section'를 그대로 기법의 이름으로 사용하였다.

 먼저 critical section 기반의 동기화 기법을 사용한 코드를 보자.


<출처: 윤성우 저, '윈도우즈 시스템 프로그래밍'>

 EnterCriticalSection() 함수와 LeaveCriticalSection() 함수를 이해하면 critical section 기반의 동기화 기법은 끝난다. 먼저 EnterCriticalSection() 함수는 인자로 CRITICAL_SECTION의 핸들(handle)을 사용한다.

 옷가게의 fitting room을 떠올려보자. 내가 옷을 골라서 갈아입기 위해 fitting room에 들어가려고 한다. 하지만 이미 fitting room을 사용하는 사람이 있다. '사용중'이라는 표시가 보인다. 그러면 나는 fitting room을 사용할 수 없고, 지금 사용 중인 사람이 사용을 마치기를 기다려야 한다. 그 사람이 사용을 마치고 나오면 이제 내가 사용할 수 있다. 내가 fitting room에 들어가는 동시에 fitting room은 다시 '사용중' 상태가 된다.

 임계영역이 바로 fitting room에 해당하고, critical section 기반의 동기화 기법이 바로 '사용중'을 표시해서 다른 이들이 fitting room에 두 명이 동시에 들어가는 것을 막는 방법이다.

 한 쓰레드가 EnterCriticalSection() 함수를 실행하면 fitting room에 들어가 사용 중이라는 선언한다. 그러면 다른 thread에 의해 EnterCriticalSection() 함수가 호출되면 이미 fitting room이 사용 중이기 때문에 이 쓰레드는 BLOCK 상태가 된다. 만약 fitting room을 사용 중인 쓰레드가 사용을 마치면 BLOCK 상태의 쓰레드가 비로소 BLOCK 상태에서 빠져나와 fitting room에 들어가 사용할 수 있다.

 LeaveCriticalSection() 함수는 어떤 쓰레드가 fitting room을 빠져나올 때, 다른 쓰레드들이 fitting room을 사용할 수 있도록 해주기 위한 함수이다. fitting room 사용을 마치고 LeaveCriticalSection() 함수를 호출하면, EnterCriticalSection() 함수를 호출했다가 BLOCK 상태가 된 쓰레드가 있다면 그 쓰레드는 BLOCK 상태에서 빠져나와서 임계영역에 진입하게 된다.

 정리하면 이 기법은 임계영역에 한 쓰레드만 진입할 수 있도록 다른 쓰레드를 BLOCK 상태로 만든다. 그리고 한 쓰레드가 임계영역을 빠져나오면 다른 쓰레드가 BLOCK 상태에서 빠져나와 임계영역에 진입한다.

2015. 1. 26.

[OS]쓰레드(Thread)와 Critical Section(임계영역)

 쓰레드에 대한 기본적인 지식을 바탕으로 쓰레드의 특성에 대해 좀더 살펴보고자 한다. 메모리 관점에서 본 쓰레드에 대한 지식이 있다면 이해하기 편할 것이다.


프로세스와 메인 쓰레드(main thread)

  쓰레드는 프로세스에 존재하는 실행 흐름 중 하나이다. 그런 실행 흐름 중에는 우리가 흔히 main() 함수라고 부르는 쓰레드가 존재한다. 어떤 프로그램을 실행시키면 main() 함수가 실행되는데, 이는 다시 말해 main 쓰레드가 실행된다는 것을 의미한다.  만약 main 쓰레드를 통해서 다른 쓰레드를 만들어서 다른 쓰레드로 하여금 특정한 일을 시키면 어떨까? 이 상황은 아래의 그림과 같다.(쓰레드의 의미 중에 실, 가닥이라는 의미도 있다)

<출처: http://www.onlinebuff.com/article_understand-threading-and-types-of-threading-in-c-using-an-example_56.html>

 main 쓰레드는 프로세스가 실행되면 자동적으로 실행되는 thread이다. 그림에 등장하는 worker 쓰레드는 main 쓰레드가 생성한 새로운 쓰레드이다. 새로운 worker 쓰레드는 main 쓰레드와는 별개로 자신의 고유한 작업 흐름을 가진다.

 그런데 메모리 관점에서 본 쓰레드를 떠올려 보면 쓰레드는 heap, data 영역을 공유한다는 것을 알 수 있다. 고유한 작업 흐름을 가지는데 메모리를 공유한다는 점에 주목해야 한다.


쓰레드를 사용하여 실행흐름을 분할 

 만약 어떤 사람이 0 ~ 100,000 사이의 숫자의 총합을 구하고자 한다. 이 사람은 thread의 대한 지식을 가지고 있었다. 이 사람은 0 ~ 33,333까지의 합을 thread1로 처리하고, 33,334 ~ 66,666까지의 합은 thread2로 처리하고, 66,667 ~ 100,000가지의 합은 thread3으로 처리해야겠다고 생각하였다. 하나의 thread로 0 ~ 100,000까지의 합을 구하는 것보다는 실행흐름을 3개로 나누고, 3개의 실행 흐름을 병렬적으로 수행하면 합을 구하는 시간이 줄어들 것으로 예상한 것이다. 얼핏 들으면 매우 그럴듯한 발상이다. 하지만 이를 그대로 코드로 옮기면 문제가 발생한다.


 ThreadProc() 함수 바로 앞에 선언된 total은 static 변수이다. static 변수는 메모리의 data 영역에 존재하고, thread는 이를 공유하게 된다. 결국 3개의 쓰레드는 모두 static 변수 total에 접근가능하다. 이런 특성때문에 쓰레드는 IPC와 같은 방법으로 통신할 필요가 없다.

 하지만 위 total 변수는 쓰레드의 동시접근 문제를 발생시킬 수 있다.

 쓰레드는 결국 스케줄러에 의해 번갈아가며 실행된다. 이 과정에서 컨텍스트 스위칭은 불가피하게 발생한다. 프로세스는 독립적인 메모리 공간을 가지기 때문에 컨텍스트 스위칭 과정에서 데이터가 변경되지는 않는다. 하지만 쓰레드 간의 컨텍스트 스위칭은 상황이 다르다. 컨텍스트 스위칭 상황에서 쓰레드는 프로그램 라인 단위로 이뤄지지 않는다. 예를 들어 A() 함수가 실행 중 일 때는 절대 컨텍스트 스위칭이 발생하지 않는다고 생각하면 안 된다. 실행 중인 쓰레드의 변경은 매우 빈번하게 발생하며, 우리가 이에 쓰레드의 실행 흐름을 예측해서 제어하는 것은 불가능하다.

 그래서 둘 이상의 쓰레드가 위의 total과 같이 같은 메모리 영역을 동시에 참조하면 문제를 일으킬 수 있다. 우리는 덧셈이 올바르게 완료될 것으로 예상하지만, 이는 우리의 기대일 뿐이다. 1 + 2의 값을 3이라고 저장하지도 못한 상태에서 33,334 + 33,335를 계산하는 쓰레드가 실행될지도 모른다. 이렇게 되면 결국 total의 값은 3만큼 누락된다.


임계영역(Critical Section)

 위 코드의 worker 쓰레드 함수 내부의 total을 둘 이상의 쓰레드가 동시에 실행할 경우 문제가 발생할 수 있다. 이런 문제를 일으킬 수 있는 코드 블럭을 임계영역이라고 한다. 다시 말해 임계영역은 배타적 접근(한 순간에 하나의 쓰레드만 접근)이 요구되는 공유 리소스(전역변수, static 변수 등)에 접근하는 코드 블럭을 의미한다.

[OS]메모리 관점에서 본 쓰레드(thread)

 메모리 관점에서 본 프로세스를 이해하고 있다면 슬쩍 읽기만 해도 느낌이 딱 온다.


메모리 관점에서 본 process의 특징

 각각의 프로세스는 메모리 공간에서 독립적으로 존재한다.

<출처:https://elgaabeb.wordpress.com/>


 전에 본 적이 있는 그림이다. 이 그림은 프로세스를 구성하는 메모리 공간의 모습이다. 각각의 프로세스는 자신만의 이런 메모리 구조를 가진다. 프로세스A, B, C가 존재한다면 각각 프로세스는 모두 위와 같은 구조의 메모리 공간을 가진다.

 독립적인만큼 다른 프로세스의 메모리 공간에 접근할 수도 없다. A가 B의 메모리 공간에 접근하면 재앙이 발생할 수도 있다. 만약 A가 뜬끔없이 Chrome의 메모리 공간에 접근한다고 생각해보라. Chrome의 안정성이 보장될 수 있겠는가? 더 뜬금없이 A가 windows의 메모리 공간에 접근한다고 생각해보라. 운영체제의 메모리 공간에 접근하여 뭔가를 변경한다면 심각한 문제가 발생할 수 있다.(물론 운영체제의 메모리 공간에 접근하는 것은 원천적으로 불가능하다) 그러므로 프로세스의 안정성을 보장하기 위해서는 프로세스는 각각 독립된 메모리 공간을 가져야 한다.

 그러면 혹시 A에서 연산한 결과를 B에서 받아서 사용하고 싶다면 어떻게 해야할까?


IPC(inter process communication)


<출처: http://madhusudhanrc.blogspot.kr/2012/08/inter-process-communicationipc.html>


 A의 메모리 공간에 B가 직접 접근하지 못하기 때문에 프로세스간의 통신을 하는 특별한 방법들이 존재한다. 메일슬롯(mailslot), 파이프(pipe) 등이 바로 프로세스 간의 통신 즉, IPC의 예들이다.

 IPC에 대한 자세한 설명은 생략~

 중요한 점은 프로세스는 독립적인 메모리 공간을 지니기 때문에 IPC를 통하지 않고는 통신할 수 없다는 사실이다. 그리고 프로세스가 여럿이 병렬적으로 실행되기 위해서는 필연적으로 컨텍스트 스위칭이 발생할 수 밖에 없다.


프로세스가 지니는 제한

 독립적인 메모리 공간으로 컨텍스트 스위칭이 발생한다.
 통신하기 위해서는 IPC가 필요하다.

 이 두 가지 문제점을 한 번에 해결할 수 있는 녀석이 쓰레드이다.


쓰레드

  쓰레드(thread)는 하나의 프로그램 내에 존재하는 여러 개의 실행 흐름을 위한 모델이다. 우리가 생각하는 프로그램이 실행되기 위해서 하나의 실행흐름으로 처리할 수도 있지만 다수의 실행흐롬으로 처리할 수도 있다.(multi-thread)

 다시 말해서 프로세스에 존재하는, 프로세스가 실행되는 흐름이다.


<출처: http://en.wikipedia.org/wiki/Thread_(computing)>


 wikipedia를 보면 쓰레드를 설명하기 위해 위와 같은 그림들을 보여준다. 아래 그림을 보면 프로세스 내부에 2개의 쓰레드가 존재한다. 그리고 시간의 방향을 따라 쓰레드가 실행되고 있다. 그림에서 볼 수 있듯이 쓰레드는 프로세스와 별개가 아닌 프로세스를 구성하고 실행하는 흐름이다.

 프로세스에서도 그러했듯이 이번에도 메모리 관점에서 쓰레드를 보자. 프로세스와 어떤 차이가 있는지 뚜렷하게 알 수 있을 것이다.


메모리 공간에서의 쓰레드

<출처: http://www-01.ibm.com/support/knowledgecenter/SSLTBW_1.12.0/com.ibm.zos.r12.euvmo00/euva3a00451.htm>

 위 그림은 프로세스와 쓰레드의 메모리 구조의 차이점을 보여준다. 왼쪽의 프로세스는 이미 봤기 때문에 설명하지 않고, 오른쪽의 thread에 주목하자. 앞서 말한 것처럼 쓰레드는 프로세스 안에 존재하는 실행흐름이다. 메모리 구조 역시 그러하다. 하지만 특이한 점은 쓰레드는 프로세스의 heap, static, code 영역 등을 공유한다는 사실이다. 각각의 프로세스가 독립적인 stack, heap, code, data 영역을 가진 반면에, 한 프로세스에 속한 쓰레드는 stack 영역을 제외한 메모리 영역은 공유한다. 

 쓰레드가 code 영역을 하기 때문에 한 프로세스 내부의 쓰레드들은 프로세스 가 가지고 있는 함수를 자연스럽게 모두 호출할 수 있다.

 뿐만 아니라 쓰레드는 data, heap 영역을 공유하기 때문에 IPC 없이도 쓰레드 간의 통신이 가능하다. 동일한 프로세스 내부에 존재하는 쓰레드 A, B가 통신하기 위해 heap 영역에 메모리 공간을 할당하고, 두 쓰레드가 자유롭게 접근한다고 생각하면 된다.

 쓰레드는 프로세스처럼 스케줄링의 대상이다. 이 과정에서 컨텍스트 스위칭이 발생한다. 하지만 쓰레드는 공유하고 있는 메모리 영역 덕분에 컨텍스트 스위칭 때문에 발생하는 오버헤드(overhead)가 프로세스에 비해 작다. 

[OS]컨텍스트 스위칭(Contex Switching)

프로세스 메모리와 스케줄링에 대한 지식만 있으면 프로세스의 컨텍스트 스위칭을 이해하기 쉽다. 정말로 쉽다.


컨텍스트 스위칭

 프로세스 A와 B가 존재한다. A가 running, B는 ready 상태이다. 이 때 어떤 이유로 A가 ready, B가 running 상태가 되는 경우가 곧 발생할 것이다. 먼저 실행 중인 A에 대한 데이터는 현재 레지스터에 존재할 것이다. 그리고 ready 상태인 B는 메모리에 존재할 것이다. 하지만 이제 B가 실행되어야 하기 때문에 A는 레지스터를 B에 양보해주어야 한다. 그리고 A에 대한 데이터는 어떻게 해야할까? 그냥 버리면 될까? 아니다. A에 대한 데이터는 B가 실행을 마친 후에 A가 실행될 수 있으므로 계속 가지고 있어야 한다. 즉, 현재 레지스터에 존재하는 A에 대한 데이터는 메모리에 저장되어야 한다.

<출처: http://www.cs.odu.edu/~cs471w/spring10/lectures/Processes.htm>

 마땅한 이미지가 없어서 구글에서 처음 보기엔 복잡해 보이는 이미지를 선택할 수 밖에 없었다. 위 그림에는 P0와 P1 두 개의 프로세스가 있다. 먼저 P0가 실행 중이다. 그러던 중에 interrupt 혹은 system call이 발생하였다.(지금은 프로세스를 멈춰야 하는 상황이 발생하였다 정도로 이해하고 넘어가자) P0은 이제 실행 상태가 아닌 ready(idle) 상태가 된다. P0가 running(excuting) 상태에서 ready(idle) 상태로 변할 때, 프로세스와 실행에 대한 데이터는 레지스터에 존재한다. 이 데이터를 메모리에 저장한다. 이 내용이 위 그림에서 'save state into PCB0'이라고 설명된 부분에 관한 것이다.
 위 그림에서 P0 다음에는 어찌저찌 해서(...으로 표시되어 있는 부분) P1이 실행된다. P1이 실행되기 위해서 메모리에 존재하는 P1에 관련된 데이터를 레지스터에 올려야 한다.(reload state from PCB1) P1이 실행되었다가 다시 ready 상태로 변경되면서 자신의 데이터를 메모리에 저장해야 한다.(save state into PCB1)

 또 어찌저찌 해서(...으로 표시되어 있는 부분) P0가 다시 실행된다. 앞에서 P0의 데이터는 메모리에 저장해둔 것이 기억나는가? 그 데이터를 읽어서 레지스터에 올리고, P0를 이전 멈추었던 시점에서 이어서 실행한다. 

 프로세스를 이것저것 우선순위에 따라 변경하기 위해서는 프로세스 데이터를 레지스터와 메모리 사이를 왔다갔다 하며 복사해야 한다. 이런 일련의 과정을 context switching(컨텍스트 스위칭)이라고 한다. 

 다음 그림은 컨텍스트 스위칭을 잘 보여주고 있다.(윤성우 저 '윈도우 시스템 프로그래밍')



I/O가 생각나는가?

 메모리와 레지스터 사이의 데이터 이동도 I/O이다. 즉, 컨텍스트 스위칭 과정에서 I/O가 발생한다. 빈번한 I/O 발생은 overhead를 발생시킨다. 실행되는 process의 수가 많고, 빈번한 컨텍스트 스위칭이 발생한다면, 이는 성능을 떨어뜨린다. 하지만 I/O가 발생할 때, 멍~하게 기다릴 수도 없는 노릇이다. I/O가 발생할 때, CPU를 게속 사용하려면 컨텍스트 스위칭은 피할 수 없다.

[OS]프로세스 스케줄링(Process Scheduling)

메모리 측면에서 본 프로세스를 통해 간단하게 프로세스를 살펴보았다.
 그냥 실행 중인 프로그램으로는 설명하기 힘든 내용이 있기 때문에 메모리 측면에서 프로세스를 짚어본 것이다.

 이제 프로세스의  스케줄링에 대해서 알아보자.


스케줄링

 스케줄링? 말 그대로 스케줄을 만드는 것이다. 운영체제를 모르는 사람은 무슨 스케줄을 만들냐고 생각할 수 있다. 스케줄을 설명하기 위해서 여러 프로세스가 실행되는 상황을 연출해보자.

 나는 맥도날드 햄버거 만드는 알바이다. (실제로 맥도날드 알바를 해본 적은 없다. 맥도날드가 중요한 것이 아니고, 햄버거를 만든다는 것이 중요하다) 나는 패티도 구워야하고, 햄버거에 재료를 넣기도 해야하고, 감자도 튀겨야 한다. 이 일은 단계별로 수행해야 한다고 생각해보라. 언뜻 생각하면 아무 문제 없을 것 같지만 이는 심각한 재앙이 발생하게 된다.

 빵을 깔고, 패티를 굽는다. 구워질 때까지 할 수 있는 일이 없다고 스마트폰 게임을 하였다. 메니저에게 걸려서 욕을 먹었다. 욕을 먹다보니 패티가 구워졌다. 빵에 양상추, 피클, 치즈를 깔고, 패티를 올려서 햄버거를 완성했다. 하지만 주문은 세트로 들어왔다. 감자도 튀겨야 하고, 음료수도 담아야 한다. 감자를 튀기기 시작했고, 다 익을 때까지 할 일이 없어 게임을 했다. 또 걸려서 욕먹었다.

 현실에서는 아무로 이런 식으로 일을 처리하지 않는다. 이렇게 일을 하면 당장 그만둬야 할 것이다. 현실에서 우리는 패티 구으면서, 감자 튀기고, 음료수도 담는다. 한 번에 여러 일을 처리한다. 이렇게 하는 것이 가장 효율적이기 때문이다. 하지만 분명한 것은 한 번에 여러 일을 처리한다기보다는 조금조금씩 이 일, 저 일을 관리하면서 처리한다는 것이다. 패티 다 굽고, 감자 다 익히는 방식이 아니라는 것이다. 이런 처리 방식은 CPU와 프로세스에도 그대로 적용된다.

 햄버거 이야기에 너무 열을 올렸다. 비유는 적당해야 하거늘 비유 자체에 빠져버렸다.
 여튼, 알바를 CPU라고 생각하고, 패티 굽기, 감자 튀기기, 음료 담기 등을 프로세스라고 생각하자.

 CPU는 한 번에 하나의 프로세스만 처리할 수 있다. 패티 구으면서 감지 튀기는 짓을 하지 못한다. 그래서 CPU는 패티 굽다가 감자가 다 익으면 감자를 먼저 처리하고, 남는 시간에 음료도 담은 다음에, 그러는 동안 완성된 패티를 구워서 햄버거를 완성하였다. 다시 말해서 CPU는 가장 중요한 프로세스를 먼저 처리하고, 시간이 남을 때(패티가 다 구워질 때까지)는 다른 프로세스를 처리하고 있다는 점이다.
 패티가 다 구워질 때까지 기다리는 시간을 I/O에 비교할 수 있을 것이다. 패티가 다 구워질 때까지 기다리지 말고, CPU는 짜투리 시간에서 음료를 담으면서 열심히 일을 한다.

 그렇다면 가장 중요한 일은 무엇이고, 시간이 남는 경우는 어떻게 해야할까?
 이것이 바로 OS가 담당하는 스케줄링에 대한 내용이다.


간단하게 살펴보는 I/O

 Chrome으로 google에 접속한다고 하자. 주소창에 google.co.kr를 입력하였다. 그 다음은 google 사이트가 뜰 때까지 기다려야 한다. 기다리는 일 말고는 무얼할 수 있겠는가? CPU도 마찬가지이다. 사이트 접속하려면 google로부터 정보를 받아야 하고, 받기 위해서는 기다려야 한다. 이런 과정이 바로 I/O이다.

 Input/Output의 준말이라는 사실에서 알 수 있듯이 I/O는 흔히 input과 output에서 발생한다. 사이트 접속을 위해 서버에 input을 해야하고, 서버로부터 output를 받아야 한다. 결국 input, output 사이에 CPU가 멍~하게 기다리는 기간이 발생한다.

 MS WORD를 실행 중이라고 생각해보자. CPU는 사실 엄청난 처리 속도를 지니고 있지만 사용자의 키보드, 마우스 입력이 있기 전에는 멍~ 하게 기다린다. 이 또한 I/O이다. CPU의 능력에 비해서 input.output에 소모되는 시간이 길어서 CPU가 놀게되는 것이다.

 인간은 CPU와 같은 기계가 노는 것을 용납하지 않는다. 늘 일을 시켜야 한다. 그래서 특정 프로세스에서 I/O가 발생하면 CPU는 다른 프로세스를 처리한다. 잠시도 못 쉬는 것이다.


프로세스 스케줄링

<출처: http://www.tutorialspoint.com/operating_system/os_processes.htm>

 위 그림에서 각각의 타원은 프로세스의 상태를 나타낸다. 

 new:  새롭게 생성된 프로세스
 ready: 실행이 준비된 상태인 프로세스
 waiting: I/O 혹은 event에 의해 실행이 일시적으로 중지된 프로세스
 running:  실행 중인 프로세스
 terminated: 종료된 프로세스

 모두 5가지의 상태를 지니고 있다. 일반적으로 프로세스는 생성되면, 즉 프로그램이 실행되면 해당 프로세스는 new 상태를 거쳐 ready 상태가 된다.
 ready 상태가 되면 scheduler에 의해 선택될 수 있는 자격을 얻는 것이다. 그런데 자격을 얻는다고 해서 모두 running 상태가 되지는 않는다. 프로세스는 특정한 기준에 의해 실행될 수 있는 우선순위(priority)를 가지고 있고, 이 우선순위가 높을 수록 스케줄러에 의해 먼저 실행된다.

 만약 프로세스 A가 실행되고 있는 상태라고 가정하자. 이 때  프로세스 B가 생성되었다. 우선순위를 A와 B 중에 어떤 일을 먼저 CPU가 처리하도록 할 것인지 판단한다. 만약 프로세스 B의 우선순위가 A에 비해서 낮다면 A는 계속 running 상태일 것이고, B는 ready 상태가 되어 스케줄러의 선택을 기다릴 것이다. 만약 우선순위만을 기준으로 스케줄러가 판단한다면 우선순위가 낮은 어떤 프로세스 는 계속 해서 ready 상태에 머무를 것이다. 자신의 우선순위보다 높은 프로세스 가 계속해서 생성된다면 말이다. (이런 상태를 기아(starvation))라고 부른다. 이에 대한 자세한 내용은 다루지 않겠다) 프로세스 B의 우선순위가 A보다 높다면 스케줄러를 CPU가 B를 실행하도록 하고, A는 ready 상태로 변경시킨다. 

 프로세스 A, B, C가 존재한다. A는 running, B는 ready, C도 ready인 상태를 가정하자. 이 때 A에서 I/O가 발생하였다. 그러면 A는 waiting 상태로 변경된다. 그리고 스케줄러는 B와 C 중에서 우선순위가 높은 프로세스를 선택하여 running 상태로 변경할 것이다. 그리고 A의 I/O가 끝나면 A가 다시 running될 것이다.(A의 우선순위가 B, C에 비해 높다면 말이다)

이런 일련의 과정이 바로 프로세스 스케줄링이다. 이를 처리하는 주체가 바로 스케줄러이고, 이는 소프트웨어로 구현되어 있다.

[OS]메모리 관점에서 본 프로세스(Process)

프로세스는 자주 소프트웨어를 공부하지 않아도 누구나 한 번쯤은 들어본 단어이다.
그 프로세스에 대해서 메모리 관점에 이야기해보고자 한다.


프로세스는 뭐?

프로세스는 흔히 실행 중에 있는 프로그램으로 정의한다. Chrome 브라우저를 실행하면 Chrome 또한 하나의 프로세스가 된다. 문서를 편집하기 위해서 MS Word를 켰다면, 이또한 프로세스가 된다. 포토샵을 켜도 마찬가지이다. 게임을 켜도.


메모리 관점에서 프로세스는 뭐?

프로세스를 메모리 관점에서 살펴보면 좀더 말할 내용이 늘어난다. 
먼저, 그림을 살펴보자.

<출처:https://elgaabeb.wordpress.com/>


프로그램이 실행되면 위의 그림과 같이 메모리 공간에 존재한다. 

stack: 지역변수 할당과 함수 호출 시 전달되는 인자값들의 저장하기 위한 공간
heap: C의 malloc, calloc과 C++의 new를 통한 동적 할당을 위해 존재하는 공간
data: 전역변수나 static 변수의 할당을 위해 존재하는 공간
code(text): 프로그램을 실행시키면 실행파일 내에 존재하는 명령어가 메모리 상에 올라가야지 프로그램을 동작시킬 수 있을 것이다. 이 명령어들을 위해 존재하는 공간.

이렇게 프로그램을 실행시키기 위해서는 메모리에 process를 위한 공간이 필요하다. 메모리 공간을 기준으로 process를 살펴본 결과이다. 이는 나중에 thread와의 비교할 때에도 중요한 의미를 지닌다. 특정 프로그램이 실행되기 위해서는 즉, process가 되려면 메모리에 위의 그림과 같은 할당이 필수적으로 이루어져야 한다는 의미이다. 메모리가 할당된 다음 CPU에 의해서 처리될 것이기 때문이다.