우리는 보통 '프로그램' 이라는 용어를 더 많이 사용할 것이다. '한글 프로그램을 실행하다',
'게임 프로그램을 실행하다' 하는 말을 자주 사용할 것이다. 하지만 프로그래머 관점에서 보면,
이 프로그램들은 다 각기 하나의 프로세스이다.
프로그램은 저장장치에서 대기하는 정적인 상태이며, 프로세스는 메모리에 올라와서
cpu의 부름을 받기 전까지 실행대기하는 동적인 상태이다.
또한 cpu를 얻은 상태에도 그들을 프로세스라고 부르며,
메모리로 잠시 쫓겨나 입출력을 수행하고 있을 때도 그들을 프로세스라고 부른다.
쉽게 말하자면, 프로그램은 '실행되지 않은' 상태이며, 프로세스는 '실행되고 있는' 상태이다.
프로세스는 코드, 데이터, 스택이라는 고유영역을 지니고 있다.
코드
프로세스가 실행할 코드와 매크로 상수가 기계어 형태로 저장된 영역.
컴파일 시점에 기계어가 결정되기 때문에, Read-Only로 지정 되어있다.
데이터
코드에서 선언한 전역변수, static변수 등등이 저장된 영역이다.
프로그램의 시작과 함께 할당되며, 프로그램이 종료되면 소멸된다.
힙
프로그래머가 직접 공간을 할당하고 해제하는 특수한 메모리 공간이다.
보통 배열이나 클래스의 소스를 저장하는 동적인 영역이다.
힙은 선입선출 구조로 되어있어서 가장 먼저 들어온 데이터가 가장 먼저 나간다.
힙은 낮은 주소부터 높은 주소의 방향으로 할당된다.
데이터가 낮은 주소부터 차곡차곡 쌓인다는 이야기이다.
또한 힙 영역은 런타임 시기에 크기가 결정된다.
스택
스택 프레임 설명 -> http://tcpschool.com/c/c_memory_stackframe
프로그램이 자동으로 사용하는 임시 메모리 영역이다.
함수 호출 시 생성되는 지역변수, 매개변수가 저장되는 영역이고,
함수 호출이 완료되면 소멸된다.
이때 스택 영역에 저장되는 함수의 호출 정보를 스택 프레임(Stack Frame)이라고 한다.
가장 나중에 들어온 데이터가 가장 먼저 나가는 '후입선출' 방식이다.
스택 영역은 메모리의 높은 주소부터 낮은 주소의 방향으로 할당되기 때문이다.
바로 위에서 프로세스의 구성에 대해서 알아봤는데, 그렇다면 운영체제는 이 녀석들을 어떻게
알고 스케쥴링을 해주는 것일까?
프로세스 제어 블럭을 커널 자신의 데이터 영역에 저장시켜놓고, 스케쥴링을 할 수 있기 때문이다.
운영체제도 하나의 프로세스이다. 운영체제는 메모리에 올라가서, 시스템 자원을 관리하기 위해
불철주야 눈을 켜고 있다. 운영체제도 코드, 데이터, 스택이라는 고유 영역을 갖고 있다.
그리고 그 데이터 영역 안에 프로세스 제어 블럭을 보관하고 있는 것이다.
프로세스 제어 블럭에는 여러 정보를 담고 있다.
프로세스는 주로 6가지의 상태로 분류된다.
New
프로세스가 생성 되는 시기. 메모리에 프로세스가 올라간다.
Ready
프로세스가 준비 큐에서 CPU를 사용할 때 까지 대기하는 상태.
Blocked 된 프로세스의 입출력이나 기타 문제들이 해결되면 Ready 상태로 다시 올라올 수 있다.
Running
프로세스가 CPU를 점유하고 있는 상태. 즉 명령어를 해석하고 실행하고 있는 상태이다.
인터럽트 라인이 세팅되면, 입출력 방식에 따라서 CPU를 점유하고 있는 프로세스는 Ready 상태로 전환될 수 있다.
또한 시스템 콜 발동으로 인해서, 입출력 완료 시 까지 대기하기 위해 Blocked 상태로 전환되기도 한다.
즉 경우에 따라 다시 쫓겨날 수 있다는 것이다.
Blocked
시스템 콜이 발생하여 입출력 완료까지, 혹은 특정 이벤트가 만족될 때까지 대기하는 상태.
입출력이 완료되고, 이벤트가 만족되면, Ready 상태로 전환된다.
Suspended
중기 스케쥴러에 의해서 프로세스가 통째로 스왑영역으로 쫓겨난 상태.
사용자가 프로그램을 일시 정지시킨 경우나, 메모리 공간 할당 부족으로 인해 프로그램을
잠시 중단시킨 상태이다. 사용자의 신호나 중기 스케쥴러가 다시 깨워줘야 Blocked 상태로 전환될 수 있다.
쫓겨난 상태는 Swap out이라 표현하며, 다시 메모리에 들어간 상태는 Swap in 이라 표현한다.
프로세스가 Ready 상태이거나 Blocked 상태이던 간에 공간이 모자라면 언제든지 쫓겨날 수 있다.
Terminated
프로세스가 CPU에서 모든 작업을 마치고 종료하는 상태.
장기 스케쥴러
메모리에 들어오도록 허락해주는 역할이다. 즉 시작 프로세스 중에서 어떤 녀석들을
준비 큐(메모리)로 보낼지 결정하는 문제이다. 또한 메모리에 올라간 프로그램 수를 제어한다.
단기 스케쥴러
어떤 Process에게 CPU를 줄건지 고민하는 문제이다.
어떤 프로세스에게 Running상태를 부여할지 결정하는 문제이다.
단기 스케쥴러는 밀리 세컨드 단위로 매우 빈번히 호출되기 때문에, 수행 속도가 빨라야한다.
중기 스케쥴러
메모리가 부족할 때, 여유공간 마련을 위해서 일부 프로세스들을 통째로 스왑영역이나
디스크로 쫓아낸다. 프로세스에게서 메모리를 뺏는 문제를 고민하는 친구이다.
또한 공간 상 여유가 다시 생기면, 쫓겨난 프로세스들을 다시 메모리에 올리는 역할도 수행한다.
이렇게 스케쥴러를 정리해봤다.
이제 Question 하나를 던져보겠다.
만약 단기 스케쥴러에 의해서 CPU 점유권을 빼앗긴다면,
빼앗긴 녀석은 처음부터 코드를 다시 실행하는 것인가?
결론부터 말하자면, "No" 이다. 프로세스끼리 '하이파이브'를 하기 때문이다.
타이머 인터럽트가 발생하면, 처리 루틴에 따라서 현재 실행되고 있는 프로세스는 CPU에서 쫓겨나게 된다.
이 과정에서 쫓겨나게 된 프로세스는 레지스터에 저장된 하드웨어 정보를 자신의 PCB에 저장하고,
다음 프로세스에게 CPU를 이양한다. 이양받은 프로세스는 자신의 PCB에 저장된
하드웨어 정보를 레지스터에 복원하고 이전에 실행했던 명령어부터 다시 실행한다.
문맥교환은 타이머 인터럽트가 발생해서 다른 사용자 프로세스에게 CPU를 이양하거나,
I/O 요청 시스템 콜이 호출되어 다른 프로세스에게 CPU를 이양하게 될 때 발생한다.
모든 종류의 인터럽트와 시스템 콜이 문맥교환을 발생시키진 않는다는 이야기이다.
운영체제의 코드가 실행되게 되어 모드비트가 0으로 바뀔 땐, 문맥교환이 일어나지 않는다.
사용자 프로세스에서 사용자 프로세스로 CPU 점유권이 이양될 때 문맥교환이 일어난다고 표현한다.
만약 크롬 브라우저 창을 딱 1개만 열어서 사용할 수 있다면 매우 불편할 것이다.
유튜브 영상을 틀어놓고, Google Docs에서 문서 작업도 해야하고, 웹 서핑 하면서
참고 문헌도 찾아봐야한다. 한 가지 프로그램 위에서 다수의 작업들을 할 수 있어야 한다.
여기서 Chrome 브라우저는 프로세스이고, 각각의 브라우저 창은 스레드이다.
다시 정리하자면, 하나의 프로세스에서 여러 가지 작업을 할 수 있으며,
각각의 작업 단위는 스레드가 된다.
아니 프로세스는 그저 스택, 데이터, 코드 영역만 있는 단순한 녀석인줄 알았는데..??
그게 아니다.
사실 현대 운영체제의 대부분은 멀티 스레드를 지원한다.
프로세스의 데이터, 힙, 코드 영역은 모든 스레드가 공유하고,
스레드는 각자 고유의 스택 영역과 레지스터를 갖고 있다.
즉 자원만 공유하고, 작업은 따로따로 진행하는 마법을 부릴 수 있게 된 것이다.
이 외에도 우리 하드웨어는 아마 스레드를 좋아할 것이다.
우리가 어떤 프로그램을 실행하면, 메모리에 올라오는 프로세스의 크기는 수 십 MB 이상 일 것이다.
하지만 스레드는 1MB 이내의 메모리만 점유한다. 보통 스레드를 경량 프로세스라고 표현하기도 한다.
각자 스택과 레지스터를 갖고 있기 때문에,
많은 공간의 물리 메모리를 점유하진 않는다.
또한 문맥교환 시 오버헤드를 줄일 수 있다.
사실 참 난해한 개념이다. 그러나 누가 구현을 했냐에 따라서 사용자 수준 스레드와
커널 수준 스레드로 분류된다.
커널 레벨 스레드
운영체제가 자체적으로 스레드를 관리하는 상태이다. 운영체제 즉, 메모리에 상주하는 커널 영역
안에 스레드를 관리할 수 있는 코드가 존재하고, 스레드를 1대1로 전담마킹해서 관리한다.
스레드도 당연히 스케쥴러가 관리할 수 있게 우선순위가 부여된다.
어떤 스레드가 실행 중에 시스템 콜이 발생하면 Blocked 상태가 된다.
그러나 이미 운영체제가 다른 스레드를 관리하고 있기 때문에, 입출력이 필요없는 작업들은
다른 스레드에게 일을 시킨다. 그렇기 때문에 프로세스가 통째로 Blocked되는 상태를
미연에 방지할 수 있다.
그러나 스케쥴링과 동기화를 위해서 커널을 계속 호출해야하기 때문에 비효율적이다.
즉, 사용자 모드에서 커널 모드로의 전환이 빈번하다. 이에 따라 자원은 더 많이 소모된다.
사용자 레벨 스레드
사용자 즉, 프로그래머가 직접 스레드를 관리하는 코드를 만들고 운영하고 있는 것이다.
운영체제 내부에서 스레드를 지원하지 않는 경우, 프로그래머가 직접 스레드를 관리하는 코드를 생성해서
스레드를 운영한다. 이 코드는 커널 영역이 아닌 사용자 영역에 올라간다.
따라서 커널은 사용자가 스레드를 10개를 운영하든 100개를 운영하든 볼 수 없다.
그저 하나의 프로세스로 볼 뿐이다. 그래서 사용자 스레드에서 입출력이 발생하면
해당 프로세스는 입출력이 완료될 때까지 봉쇄상태로 전환하게 된다. 커널 레벨 스레드는
프로세스가 통째로 프로세스를 뺏기지 않는 것과 반대되는 개념이다.
또한 스케쥴링 결정이나 동기화를 위해 커널을 호출할 필요가 없기 때문에,
오버헤드가 적다.
그러나 스레드의 스케쥴링 결정을 지원하지 않는다. 그냥 IO작업이 발생하면
프로세스를 통째로 Blocked 상태로 전환시킨다.