9장 운영체제
모든 프로그램은 하드웨어를 필요로 합니다. 이미지를 하드 디스크에 저장하는 프로그램은 하드디스크를 필요로 합니다. 이때 프로그램 실행에 마땅히 필요한 요소들을 가리켜 시스템 자원, 혹은 줄여서 자원이라고 합니다.
CPU, 메모리, 보조 기억장치, 입출력 장치 등과 같은 컴퓨터 부품들은 모두 자원이라고 볼 수 있습니다. 즉 모든 프로그램은 실행되기 위해 반드시 자원이 필요합니다. 여기서 실행할 프로그램에 자원을 할당하고 프로그램이 올바르게 실행되도록 돕는 특별한 프로그램이 바로 운영체제입니다.
운영체제는 인터넷 브라우저, 게임과 같은 프로그램이기 때문에 여느 프로그램과 마찬가지로 메모리에 적재되어야 합니다. 다만 운영체제는 매우 특별한 프로그램이기 때문에 항상 컴퓨터가 부팅될 때 메모리 내 커널 영역(kernel space)이라는 공간에 따로 적재되어 실행됩니다. 커널 영역을 제외한 나머지 영역, 사용자가 이용하는 응용 프로그램이 적재되는 영역을 사용자 영역(user space)라고 합니다. 즉 운영체제는 커널 영역에 적재되어 사용자 영역에 적재된 프로그램들에 자원을 할당하고 이를 이들이 올바르게 실행되도록 돕습니다.
운영체제는 실행할 프로그램을 메모리에 적재하고, 더 이상 실행되지 않는 프로그램을 메모리에서 삭제하며 지속적으로 메모리 자원을 관리합니다. 어느 한 프로그램이 CPU를 독점하면 다른 프로그램들은 올바르게 실행될 수 없기 때문에 운영체제는 최대한 공정하게 여러 프로그램에 CPU 자원을 할당합니다.
운영체제는 응용 프로그램과 하드웨어 사이에서 응용 프로그램에 필요한 자원을 할당하고 응용 프로그램이 올바르게 실행되도록 관리하는 역할을 맡습니다.
개발자 입장에서 운영체제가 없는 세상은 상상만 해도 끔찍합니다. 아무리 간단한 프로그램이라도 운영체제가 없다면 하드웨어를 조작하는 코드를 개발자가 모두 직접 작성해야 하기 때문입니다. 1과 2를 더한 결과를 모니터에 출력하는 간단한 프로그램을 생각해봅시다. 이런 간단한 프로그램조차 운영체제가 없다면 작성하기조차 매우 어렵습니다. 프로그램을 메모리에 적재하는 코드, CPU로 하여금 1과 2를 더하게 하는 코드, 모니터에 계산 결과를 출력하는 코드를 개발자가 모두 직접 작성해야하기 때문입니다. 하지만 다행히도 이 세상에는 운영체제가 있습니다. 운영체제가 하드웨어를 조작하고 관리하는 기능들을 제공하기 때문에 개발자는 하드웨어를 조작하는 코드를 직접 작성할 필요 없이 운영체제의 도움을 받아서 간편하게 개발할 수 있습니다. 운영체제는 딱딱한 하드웨어가 아니기 때문에 여러분과 대화할 수 있습니다. 운영체제는 현재 하드웨어 상태는 어떤지, 우리의 코드는 어떻게 실행되었는지, 하드웨어 상에 어떤 문제가 있었는지 등을 여러분에게 상세히 알려줄수 있고 이를 통해 여러분은 문제해결의 실마리를 찾을 수 있습니다.
운영체제는 현존하는 프로그램 중 규모가 가장 큰 프로그램 중 하나입니다. 대표적인 운영체제인 리눅스를 구성하는 소스 코드는 천만 줄이 넘습니다. 또 세상에는 다양한 운영체제가 있습니다. 그래서 운영체제가 응용 프로그램에 제공하는 기능들, 다시 말해 운영체제 서비스 또한 매우 다양합니다. 자원에 접근하고 조작하는 기능, 프로그램이 올바르고 안전하게 실행되게 하는 기능이 운영체제의 핵심 서비스에 속합니다. 이러한 운영체제의 핵심 서비스를 담당하는 부분을 커널이라고 합니다.
운영체제가 설치된 모든 기기에는 커널이 있습니다. 어떤 커널을 사용하는지에 따라 여러분이 실행하고 개발하는 프로그램이 하드웨어를 이용하는 양상이 달라지고, 결과적으로 컴퓨터 전체의 성능도 달라질 수 있습니다. 운영체제가 제공하는 서비스 중 커널에 포함되지 않은 서비스도 있는데, 대표적으로 사용자 인터페이스가 있습니다. 사용자 인터페이스는 윈도우의 바탕화면과 같이 사용자가 컴퓨터와 상호작용 할 수 있는 통로입니다.
운영체제가 제공하는 사용자 인터페이스의 종류에는 그래픽 유저 인터페이스와 커맨드 라인 유저인터페이스가 있습니다. 이러한 사용자 인터페이스는 운영체제가 제공하는 서비스이지만, 이는 그저 컴퓨터와 상호작용하기 위한 통로일 뿐 커널에 속한 기능은 아닙니다. 실제로 같은 커널을 사용하더라도 사용자 인터페이스는 다를 수 있습니다.
운영체제는 사용자가 실행하는 응용 프로그램이 하드웨어 자원에 직접 접근하는 것을 방지하여 자원을 보호합니다. 비유하자면 운영체제는 응용 프로그램의 자원 접근을 대행하는 일종의 문지기 역할을 하는 셈입니다. 응용 프로그램의 요청을 받은 운영체제는 프로그램 대신 자원에 접근하여 요청한 작업을 수행합니다.
이중 모드란 CPU가 명령을 실행하는 모드를 크게 사용자 모드와 커널 모드로 구분하는 방식입니다. CPU는 명령어를 사용자 모드로써 실행할 수 있고, 커널 모드로써 실행할 수 있습니다.
사용자 모드는 운영체제 서비스를 제공받을 수 없는 실행 모드입니다. 즉, 커널 영역의 코드를 질행할 수 없습니다. 일반적인 응용프로그램은 기본적으로 사용자 모드로 실행됩니다. 사용자모드로 실행중인 CPU는 입출력 명령어와 같이 하드웨어 자원에 접근하는 명령어를 실행할 수 없습니다. 그래서 사용자 모드로 실행되는 일반적인 응용 프로그램은 자원에 접근할 수 없습니다. 반면 커널 모드는 운영체제 서비스를 제공받을 수 있는 실행모드입니다. CPU가 커널 모드로 명령어를 실행하면 자원에 접근하는 명령어를 비롯한 모든 명령어를 실행할 수 있습니다. 운영체제는 커널 모드로 실행되기 때문에 자원에 접근할 수 있습니다.
요컨대 사용자 모드로 실행되는 프로그램이 자원에 접근하는 운영체제 서비스를 제공받으려면 운영체제에 요청을 보내 커널 모드로 전환되어야합니다. 이때 운영체제 서비스를 제공받기 위한 요청을 시스템 호출(system call)이라고 합니다. 사용자 모드로 실행되는 프로그램은 시스템 호출을 통해 커널 모드로 전환하여 운영체제 서비스를 제공받을 수 있습니다. 시스템 호출은 일종의 인터럽트입니다. 정확히는 소프트웨어적인 인터럽트입니다. 인터럽트는 입출력 장차에 의해 발생하기도 하지만 인터럽트를 발생시키는 특정 명령어에 의해 발생하기도 하는데, 이를 소프트웨어 인터럽트라고 합니다.
일반적으로 응용 프로그램은 실행과정에서 운영체제 서비스들을 매우 빈번하게 이용합니다. 그 과정에서 빈번하게 시스템 호출을 발생시키고 사용자 모드와 커널모드를 오가며 실행됩니다.
실행중인 프로그램을 프로세스라고 합니다. 일반적으로 하나의 CPU는 한번에 하나의 프로세스만 실행할 수 있기에 CPU는 이 프로세스들을 조금씩 번갈아 가며 실행합니다. 다시 말해 CPU는 한 프로세스를 실행하다가 다른 프로세스로 실행을 전환하고, 그 프로세스를 실행하다가 또 다른 프로세스로 실행을 전환하는 것을 반복합니다.
일반적으로 메모리에는 여러 프로세스가 적재되고, 하나의 CPU는 한번에 하나의 프로세스만 실행할 수 있습니다. 그래서 하나의 프로세스가 CPU를 이용하고 있다면 다른 프로세스는 기다려야합니다. 이에 운영체제는 프로세스들에 공정하게 CPU를 할당하기 위해 어떤 프로세스부터 CPU를 이용하게 할 것인지, 얼마나 오래 CPU를 이용하게 할지를 결정할 수 있어야 합니다. 이를 CPU 스케줄링이라고 합니다.
인터럽트 서비스 루틴은 운영체제가 제공하는 기능으로 커널 영역에 있습니다. 입출력 장치가 발생시키는 하드웨어 인터럽트도 마찬가지입니다. 입출력장치가 CPU에 하드웨어 인터럽트 요청 신호를 보내면 CPU를 하던일을 잠시 백업한뒤 커널 영역에 있는 인터럽트 서비스 루틴을 실행합니다. 이처럼 운영체제는 인터럽트를 처리하는 프로그램, 즉 인터럽트 서비스 루틴을 제공함으로써 입출력 작업을 수행합니다.
운영체제의 핵심 서비스를 제공하는 부분을 커널이라고 합니다. 그리고 사용자 프로세스가 커널의 서비스를 제공받기 위해서는(커널 영역의 코드를 실행하기 위해서는) 사용자 모드에서 커널 모드로 전환해야 하고, 이는 시스템 호출을 통해 이루어집니다. 즉, 시스템 호출은 커널 모드로써 운영체제의 서비스를 제공 받을 수 있는 방법입니다.
10장 프로세스와 스레드
백그라운드 프로세스 중에는 사용자와 직접 상호작용할 수 있는 백그라운드 프로세스도 있지만, 사용자와 상호작용하지 않고 그저 묵묵히 정해진 일만 수행하는 프로세스도 있습니다. 이러한 백그라운드 프로세스를 유닉스 체계의 운영체제에서는 데몬이라고 부르고, 윈도우 운영체제에서는 서비스라고 부릅니다.
모든 프로세스는 실행을 위해 CPU를 필요로 하지만, CPU 자원은 한정되어 있습니다. 즉 모든 프로세스가 CPU를 동시에 사용할 수는 없습니다. 그렇기에 프로세스들은 차례대로 돌아가며 한정된 자원 만큼만 CPU를 이용합니다. 자신의 차례가 되면 정해진 시간만큼 CPU를 이용하고, 시간이 끝났음을 알리는 인터럽트(타이머 인터럽트)가 발생하면 자신의 차례를 양보하고 다음 차례가 올 때까지 기다립니다. 타이머 인터럽트는 클럭 신호를 발생시키는 장치에 의해 주기적으로 발생하는 하드웨어 인터럽트입니다.
운영체제는 빠르게 번갈아 수행되는 프로세스의 실행 순서를 관리하고, 프로세스에 CPU를 비롯한 자원을 배분합니다. 이를 위해 운영체제는 프로세스 제어 블록(process control block)을 이용합니다.
PCB는 프로세스 생성시에 만들어지고 실행이 끝나면 폐기됩니다. 다시 말해 새로운 프로세스가 생성되었다는 말은 운영체제게 PCB를 생성했다는 말과 같고 프로세스가 종료되었다는 말은 운영체제가 해당 PCB를 폐기했다는 말과 같습니다.
PCB에는 어떤 정보들이 담길까요? 먼저 PID가 있습니다. PID는 특정 프로세스를 식별하기 위해 부여하는 고유한 번호입니다. 다음으로 레지스터 값입니다. 프로세스는 자신의 실행 차례가 돌아오면 이전까지 사용했던 레지스터의 중간값들을 모두 복원합니다. 그래야만 이전까지 진행했던 작업들을 그대로 이어 실행할 수 있으니까요. 그래서 PCB 안에는 해당 프로세스가 실행하며 사용했던 카운터를 비롯한 레지스터 값들이 담깁니다. 프로세스 상태와 관련해서는 다음 절에서 배우겠지만, 현재 프로세스가 어떤 상태인지도 PCB에 기록되어야 합니다. 또한 CPU 스케줄링 정보도 기록됩니다. 프로세스가 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보. 다음으로 메모리 관리 정보도 저장됩니다. 프로세스마다 메모리에 저장된 위치가 다릅니다. PCB에는 프로세스가 어느 주소에 저장되어 있는지에 대한 정보가 있어야 합니다. PCB에는 베이스 레지스터, 한계 레지스터 값과 같은 정보들이 담깁니다. 또한 프로세스의 주소를 알기 위한 또 다른 중요 정보 중 하나인 페이지 테이블 정보도 PCB에 담깁니다. 프로세스가 실행 과정에서 특정 입출력장치나 파일을 사용하면 PCB에 해당 내용이 명시됩니다. 즉, 어떤 입출력 장치가 이 프로세스에 할당되었는지, 어떤 파일들을 열었는지에 대한 정보들이 PCB에 기록됩니다.
하나의 프로세스에서 다른 프로세스로 실행 순서가 넘어가면 어떤 일이 일어날까요? 가령 프로세스 A가 운영체제로부터 CPU를 할당받아 실행되다가 시간이 다 되어 프로세스 B에 CPU 사용을 양보한다고 가정해봅시다.
이런 상황에서 바로 직전까지 실행되던 프로세스 A는 프로그램 카운터를 비롯한 각종 레지스터 값, 메모리 정보, 실행을 위해 열었던 파일이나 사용한 입출력 장치 등 지금까지의 중간 정보를 백업해야합니다. 그래야만 다음 차례가 왔을 때 이전까지 실행했던 내용에 이어 다시 실행을 재개할 수 있습니다.
이러한 중간정보, 즉 하나의 프로세스 수행을 재개하기 위해 기억해야할 정보를 문맥이라고 합니다. 하나의 프로세스 문맥은 해당 프로세스의 PCB에 표현되어 있습니다. PCB에 기록되는 정보들을 문맥이라고 봐도 무방합니다. 실행 문맥을 잘 기억해 두면 언제든 해당 프로세스의 실행을 재개할 수 있기 때문에 프로세스가 CPU를 사용할 수 있는 시간이 다 되거나 예기치 못한 상황이 발생하여 인터럽트가 발생하면 운영체제는 해당 프로세스의 PCB에 문맥을 백업합니다. 그리고 뒤이어 실행할 프로세스 B의 문맥을 구합니다. 이처럼 기존 프로세스의 문맥을 PCB에 백업하고 새로운 프로세스를 실행하기 위해 문맥을 PCB로부터 복구하여 새로운 프로세스를 실행하는 것을 문맥 교환(context switching)이라고 합니다. 문맥 교환은 여러 프로세스가 끊임없이 빠르게 번갈아 가며 실행되는 원리입니다. 문맥 교환이 자주 일어나면 프로세스는 그만큼 빨리 번갈아 가며 수행되기 때문에 여러분의 눈에는 프로세스들이 동시에 실행되는 것처럼 보입니다.
프로세스가 생성되면 커널 영역에 PCB가 생성됩니다. 그렇다면 사용자 영역에는 프로세스가 어떻게 배치될까요? 하나의 프로세스는 사용자 영역에 크게 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 나뉘어 저장됩니다.
코드 영역은 텍스트 영역이라고도 부릅니다. 이곳에는 말 그대로 실행할 수 있는 코드, 즉 기계어로 이루어진 명령어가 저장됩니다. 코드 영역에는 데이터가 아닌 CPU가 실행할 명령어가 담겨 있기 때문에 쓰기가 금지되어 있습니다. 다시말해 코드 영역은 읽기 전용 공간입니다. 데이터 영역은 잠깐 썼다가 없앨 데이터가 아닌 프로그램이 실행되는 동안 유지될 데이터가 저장되는 공간입니다. 이런 데이터로는 전역 변수가 대표적입니다. 코드 영역과 데이터 영역은 그 크기가 변하지 않습니다. 프로그램을 구성하는 명령어들이 갑자기 바뀔 일이 없으니 코드 영역의 크기가 변할 리 없고, 데이터 영역에 저장될 내용은 프로그램이 실행되는 동안에만 유지될 데이터니까요. 그래서 코드 영역과 데이터 영역은 '크기가 고정된 영역'이라는 점에서 정적 할당 영역이라고도 부릅니다. 반면 힙 영역과 스택 영역은 프로세스 실행 과정에서 그 크기가 변할 수 있는 영역입니다. 그래서 이 두 영역을 동적 할당 영역이라고 부르지요.
힙 영역은 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장공간입니다. 프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가는 해당 공간을 반환해야합니다. 메모리 공간을 반환한다는 의미는 '더 이상 해당 메모리 공간을 사용하지 않겠다'라고 운영체제에 말해주는 것과 같습니다. 메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래합니다. 이런 문제를 메모리 누수라고 합니다.
이번에는 스택 영역을 알아봅시다. 스택영역은 데이터를 일시적으로 저장하는 공간입니다. 데이터 영역에 담기는 값과는 달리 잠깐 쓰다가 말 값들이 저장되는 공간입니다. 이런 데이터로는 함수의 실행이 끝나면 사라지는 매개변수, 지역 변수가 대표적입니다.
일시적으로 저장할 데이터는 스택 영역에 PUSH되고, 더 이상 필요하지 않은 데이터는 POP됨으로써 스택 영역에서 사라집니다. 힙 영역과 스택 영역은 실시간으로 그 크기가 변할 수 있기 때문에 동적 할당 영역이라고 부릅니다. 그래서 일반적으로 힙 영역은 메모리의 낮은 주소에서 높은 주소로 할당이되고 스택 영역은 높은 주소에서 낮은 주소로 할당됩니다. 그래야만 힙 영역과 스택 영역에 데이터가 쌓여도 새롭게 할당되는 주소가 겹칠 일이 없습니다.
여러분이 컴퓨터를 사용할 때 여러 프로세스들이 빠르게 번갈아 가면서 실행됩니다. 그 과정에서 하나의 프로세스는 여러 상태를 거치며 실행됩니다. 그리고 운영체제는 프로세스의 상태를 PCB를 통해 인식하고 관리합니다. 프로세스의 상태를 표현하는 방식은 운영체제마다 조금씩 차이가 있지만, 프로세스가 가질 수 있는 대표적인 상태는 다음과 같습니다.
- 생성 상태: 프로세스를 생성 중인 상태를 생성 상태(new)라고 합니다. 이제 막 메모리에 적재되어 PCB를 할당받은 상태입니다. 생성 상태를 거쳐 실행할 준비가 완료된 프로세스는 곧바로 실행되지 않고 준비 상태가 되어 CPU의 할당을 기다립니다.
- 준비 상태: 준비 상태는 당장이라도 CPU를 할당받아서 실행할 수 있지만, 아직 자신의 차례가 아니기에 기다리고 있는 상태입니다. 준비 상태 프로세스는 차례가 되면 CPU를 할당받아 실행 상태가 됩니다. 준비 상태인 프로세스가 실행상태로 전환되는 것을 디스패치라고 합니다.
- 실행 상태: CPU를 할당받아 실행중인 상태를 의미합니다. 실행 상태인 프로세스는 할당된 일정 기간 동안만 CPU를 사용할 수 있습니다. 이 때 프로세스가 할당된 시간을 모두 사용한다면(타이머 인터럽트가 발생하면) 다시 준비 상태가 되고, 실행 도중 입출력장치를 사용하여 입출력 장치의 작업이 끝날 때까지 기다려야 한다면 대기 상태가 됩니다.
- 대기 상태: 프로세스는 실행 도중 입출력 장치를 사용하는 경우가 있습니다. 입출력 작업은 CPU에 비해 처리 속도가 느리기에 입출력 작업을 요청한 프로세스는 입출력 장치가 입출력을 끝낼 때까지(입출력 완료 인터럽트를 받을 때까지) 기다려야 합니다. 이렇게 입출력 장치의 작업을 기다리는 상태를 대기 상태(blocked)라고 합니다. 입출력 작업이 완료되면 해당 프로세스는 다시 준비 상태로 CPU할당을 기다립니다.
- 종료 상태: 프로세스가 종료된 상태입니다. 프로세스가 종료되면 운영체제는 PCB와 프로세스가 사용한 메모리를 정리합니다.
위와 같은 도표를 프로세스 상태 다이어그램(process state diagram)이라고 합니다. 이처럼 컴퓨터 내의 여러 프로세스는 생성, 준비, 실행, 대기, 종료 상태를 거치며 실행됩니다. 운영체제는 이 상태를 PCB에 기록하며 프로세스를 관리합니다.
모든 프로세스의 가장 위에 있는 최초의 프로세스는 무엇일까요?
최초의 프로세스는 유닉스 운영체제에서는 init, 리눅스 운영체제에서는 systemd, macOS에서는 launchd라고 합니다. 최초의 프로세스는 PID가 항상 1이며, 모든 프로세스의 최상단에 있는 부모 프로세스입니다.(pstree 명령어를 통해 프로세스 계층 구조를 확인할 수 있습니다.)
부모 프로세스는 fork를 통해 자신의 복사본을 자식 프로세스로 생성해내고, 만들어진 복사본(자식 프로세스)은 exec를 통해 자신의 메모리 공간을 다른 프로그램으로 교체압니다. fork와 exec는 시스템 호출입니다. 부모 프로세스는 fork 시스템 호출을 통해 자신의 복사본을 자식 프로세스로 생성합니다. 즉, fork는 자기 자신 프로세스의 복사본을 만드는 시스템 호출입니다.
자식 프로세스는 부모 프로세스의 복사본이기 때문에 부모 프로세스의 자원들, 이를테면 메모리의 내용, 열린 파일의 목록 등이 자식 프로세스에 상속됩니다.(복사된 자식프로세스라 할지라도 PID 값이나 저장된 메모리 위치는 다릅니다.)
fork를 통해 복사본이 만들어진 뒤에 자식 프로세스는 exec 시스템 호출을 통해 새로운 프로그램으로 전환됩니다. exec는 자신의 메모리 공간을 새로운 프로그램으로 덮어쓰는 시스템 호출입니다. 다시 말해 새로운 프로그램 내용으로 전환하여 실행하는 시스템 호출입니다.
메모리 공간에 새로운 프로그램 내용이 덮어 써진다는 점에서 이는 자식 프로세스가 새로운 옷으로 갈아입었다고도 볼 수 있다. exec를 호출하면 코드영역과 데이터 영역의 내용이 실행할 프로그램의 내용으로 바뀌고, 나머지 영역은 초기화됩니다.
exec는 자신의 메모리 공간을 새로운 프로그램으로 덮어쓰는 시스템 호출입니다. 예를 들어 사용자가 bash 셸에서 ls라는 명령어를 쳤다고 가정해봅시다. 셸 프로세스는 fork를 통해 자신과 동일한 프로세스를 생성하고, 그로부터 탄생한 자식 프로세스(셸의 복제 프로세스)는 exec를 통해 ls 명령어를 실행하기 위한 프로세스로 전환되어 실행됩니다. 그렇게 셸의 복사본으로 탄생한 자식 프로세스는 ls 명령어를 실행하기 위한 프로세스로 바뀌고, 메모리 공간에는 ls 명령어를 실행하기 위한 내용들이 채워집니다. 정리하면 부모가 자식 프로세스를 실행하여 프로세스 계층 구조를 이루는 과정은 fork과 exec가 반복되는 과정이라 볼 수 있습니다.
스레드
스레드란 프로세스를 구성하는 실행 흐름 단위입니다. 하나의 프로세스는 여러개의 스레드를 가질 수 있습니다. 스레드를 이용하면 하나의 프로세스에서 여러 부분을 동시에 실행할 수 있습니다.
스레드는 프로세스 내에서 각기 다른 스레드 ID, 프로그램 카운터 값을 비롯한 레지스터 값, 스택으로 구성됩니다. 각자 프로그램 카운터 값을 비롯한 레지스터 값, 스택을 가지고 있기에 스레드가 각기 다른 코드를 실행할 수 있습니다.
여기서 중요한점은 프로세스의 스레드들은 실행에 필요한 최소한의 정보만을 유치한채 프로세스 자원을 공유하며 실행된다는 점입니다. 프로세스의 자원을 공유하는것이 스레드의 핵심입니다.
CPU 스케줄링
모든 프로세스는 운영체제로부터 자원을 할당받습니다. 프로세스 마다 필요로 하는 자원은 각기 다르지만, 모든 프로세스가 공통으로 사용하는 자원은 다르지만, 모든 프로세스가 공통으로 사용하는 자원이 있다면 그건 CPU입니다. 따라서 운영체제가 프로세스에게 분배하는 자원 중 가장 중요한 자원은 CPU라고 볼 수 있습니다.
프로세스에게 공정하고 합리적으로 자원을 할당하기 위해 운영체제는 어떤 프로세스에 CPU를 할당할지, 어떤 프로세스를 기다리게 할지를 결정합니다. 이렇게 운영체제가 프로세스들에게 공정하고 합리적으로 CPU 자원을 배분하는 것을 CPU 스케줄링이라고 합니다. CPU 스케줄링은 컴퓨터 성능과도 직결되는 대단히 중요한 문제입니다. 프로세스들에게 현명하게 CPU를 배분하지 못하면 반드시 실행되어야 할 프로세스들이 실행되지 못하거나, 당장 급하지 않은 프로세스들만 주로 실행되는 등 무질서한 상태가 발생할 수도 있기 때문입니다.
프로세스마다 우선순위는 다릅니다. 우선순위가 높은 프로세스란 빨리 처리해야하는 프로세스를 의미합니다. 우선순위가 높은 프로세스는 대표적으로 입출력 작업이 많은 프로세스가 있습니다.
대부분의 프로세스는 CPU와 입출력 장치를 모두 사용하며 실행됩니다. 달리 말하면 프로세스는 실행 상태와 대기 상태를 반복하며 실행됩니다. 예를 들어 워드 프로세서는 CPU를 사용하여 명령어를 실행하고, 사용자로부터 입력받은 내용을 보조 기억 장치에 저장하고, CPU를 사용하여 명령어를 실행하고, 사용자가 입력한 내용을 화면에 출력하는 과정을 반복하여 실행됩니다.
그런데 프로세스 종류마다 입출력 장치를 이용하는 시간과 CPU를 이용하는 시간의 양에는 차이가 있습니다. 입출력 작업이 많은 입출력 집중 프로세스(I/O bound process), CPU작업이 많은 CPU 집중 프로세스(CPU bound process)라고 합니다. 입출력 집중 프로세스는 실행 상태보다는 입출력을 위한 대깃 상태에 더 많이 머무르게 됩니다. 반대로 CPU 집중 프로세스는 대기 상태보다는 실행 상태에 더 많이 머무르게 됩니다.
CPU를 이용하는 작업을 CPU 버스트(CPU burst)라고 하고, 입출력 장치를 기다리는 작업을 입출력 버스트(I/O burst)라고 합니다. 즉 프로세스는 일반적으로 CPU 버스트와 입출력 버스트를 반복하며 실행한다고 볼 수 있습니다. 그래서 입출력 집중 프로세스는 입출력 버스트가 많은 프로세스, CPU 집중 프로세스는 CPU 버스트가 많은 프로세스라고 정의할 수 있습니다.
CPU 집중 프로세스는 CPU를 많이 사용해야 하는 프로세스이고, 입출력 집중 프로세스는 그렇지 않은 프로세스인데, CPU 집중 프로세스와 입출력 집중 프로세스가 모두 동일한 빈도로 CPU를 사용하는 것은 비합리적입니다. CPU 집중 프로세스와 입출력 집중 프로세스가 동시에 CPU 자원을 요구했다고 가정해 봅시다. 이러한 경우 입출력 집중 프로세스를 가능한 빨리 실행시켜 입출력 장치를 끊임없이 작동시키고, 그 다음 CPU 집중 프로세스에 집중적으로 CPU를 할당하는 것이 더 효율적입니다. 입출력 장치가 입출력 작업을 완료하기 전까지는 입출력 집중 프로세스는 어차피 대기 상태가 될 예정이기 때문에 입출력 집중 프로세스를 얼른 먼저 처리해 버리면 다른 프로세스가 CPU를 사용할 수 있기 때문입니다.
상황에 맞게, 그리고 프로세스의 중요도에 맞게 프로세스가 CPU를 이용할 수 있도록 하기 위해 운영체제는 프로세스마다 우선순위(priority)를 부여합니다. 운영체제는 각 프로세스의 PCB에 우선순위를 명시하고, PCB에 적힌 우선순위를 기준으로 처리할 프로세스를 결정합니다. 그렇게 자연스레 우선순위가 높은 프로세스는 더 빨리, 더 자주 실행됩니다.
PCB에 우선순위가 적혀 있다고는 하지만, CPU를 사용한 다음 프로세스를 찾기 위해 운영체제가 일일이 모든 PCB를 뒤적거리는 것은 비효율적이다. CPU를 원하는 프로세스들은 한 두 개가 아니고, CPU를 요구하는 새로운 프로세스는 언제든 생길 수 있기 때문이다.
이는 CPU 자원에만 국한된 상황은 아닙니다. 메모리에 적재되고 싶어하는 프로세스도 얼마든지 있을 수 있고, 특정 입출력 장치와 보조 기억 장치를 사용하기를 원하는 프로세스도 여러개가 있을 수 있습니다. 운영체제가 매번 일일이 모든 PCB를 검사하여 먼저 자원을 이용할 프로세스를 결정하는 일은 매우 번거로울뿐더러 오랜 시간이 걸리는 일입니다.
그래서 운영체젠느 프로세스들에 줄을 서서 기다릴 것을 요구합니다. CPU를 사용하고 싶은 프로세스들, 메모리에 적재되고 싶은 프로세스들 또한 큐에 삽입하여 줄을 세우고, 특정 입출력장치를 이용하고 싶은 프로세스들 역시 큐에 삽입하여 줄을 세웁니다. 그리고 운영체제는 이 줄을 스케줄링 큐(scheduling queue)로 구현하고 관리합니다. 큐는 자료구조 관점에서 보았을 때는 먼저 삽입된 데이터가 먼저 나가는 선입 선출(First in First Out)자료 구조이지만, 스케줄링에서 이야기하는 큐는 반드시 선입 선출 구조일 필요는 없습니다.
운영체제가 관리하는 대부분의 자원은 이렇듯 큐로 관리됩니다. 그래서 운영체제가 관리하는 줄, 즉 큐에는 다양한 종류가 있습니다. 대표적인 큐로 준비 큐와 대기 큐가 있습니다. 준비 큐(ready queue)는 CPU를 이용하고 싶은 프로세스들이 서는 줄을 의미하고, 대기 큐(waiting queue)는 입출력 장치를 이용하기 위해 대기 상태에 접어든 프로세스들이 서는 줄을 의미합니다.
준비 상태에 있는 프로세스들의 PCB는 준비 큐의 마지막에 삽입되어 CPU를 사용할 차례를 기다립니다. 운영체제는 PCB들이 큐에 삽입된 순서대로 프로세스를 하나씩 꺼내어 실행하되, 그 중 우선순위가 높은 프로세스를 먼저 실행합니다.
우선 순위가 낮은 프로세스들이 먼저 큐에 삽입되어 줄을 섰다고 할지라도 우선순위가 높은 프로세스는 그들보다 먼처 처리될 수 있습니다. 대기 상태에 있는 프로세스도 마찬가지입니다. 같은 장치를 요구한 프로세스들은 같은 대기 큐에서 기다립니다. 예를 들어 하드 디스크 사용을 요구한 프로세스는 하드 디스크 대기 큐에서 입출력 작업이 완료되기를 기다리고, 프린터 사용으 ㄹ요구한 프로세스는 프린터 대기 큐에서 입출력 작업이 완료되기를 기다리는 것이지요.
입출력이 완료되어 입출력 완료 인터럽트가 발생하면 운영체제는 대기 큐에서 작업이 완료된 PCB를 찾고, 이 PCB를 준비 상태로 변경한뒤 대기 큐에서 제거합니다. 당연히 PCB는 ready queue로 이동합니다.
선점이란 '남보다 앞서서 차지함'을 의미합니다. 선점형 스케줄링(preemptive scheduling)은 프로세스가 CPU를 비롯한 자원을 사용하고 있더라도 운영체제가 프로세스로부터 자원을 강제로 빼앗아 다른 프로세스에게 할당할 수 있는 스케줄링 방식을 의미합니다. 다시 말해 어느 하나의 프로세스가 자원 사용을 독점할 수 있는 스케줄링 방식을 의미합니다. 프로세스마다 정해진 시간만큼 CPU를 사용하고, 정해진 시간을 모두 소비하여 타이머 인터럽트가 발생하면 운영체제가 해당 프로세스로부터 CPU 자원을 빼앗아 다음 프로세스에 할당하는 방식은 선점형 스케줄링의 일종으로 볼 수 있습니다.
반면 비선점형 스케줄링(non-preemptive schedling)이란 하나의 프로세스가 자원을 사용하고 있다면 그 프로세스가 종료되거나 스스로 대기 상태에 접어들기 전까진 다른 프로세스가 끼어들 수 없는 스케줄링 방식을 의미합니다. 다시 말해 비선점형 스케줄링은 하나의 프로세스가 자원 사용을 독점할 수 있는 스케줄링 방식이라고 할 수 있습니다. 만약 비선점형 스케줄링 방식으로 자원을 이용하는 프로세스가 있다면 다른 프로세스들은 그 프로세스의 사용이 모두 끝날 때까지 기다려야 합니다.
현재 대부분의 운영체제는 선점형 스케줄링 방식을 차용하고 있지만, 선점형 스케줄링과 비선점형 스케줄링은 각기 다른 장점을 갖습니다. 선점형 스케줄링은 더 급한 프로세스가 언제든 끼어들어 사용할 수 있는 스케줄링 방식이므로 어느 한 프로세스의 자원 독점을 막고 프로세스들에 골고루 자원을 배분할 수 있다는 장점이 있지만, 그만큼 문맥 교환 과정에서 오버헤드가 발생할 수 있습니다.
반면 비선점형 스케줄링은 문맥 교환의 횟수가 선점형 스케줄링보다 적기 때문에 문맥 교환에서 발생하는 오버헤드는 선점형 스케줄링보다 적지만, 하나의 프로세스가 자원을 사용 중이라면 당장 자원을 사용해야 하는 상황에서도 무작정 기다리는 수밖에 없습니다. 모든 프로세스가 골고루 자원을 사용할 수 없다는 단점이 있습니다.
하나의 프로세스가 자원을 사용하고 있을 때 다른 프로세스가 해당 자원을 빼앗을 수 있는 스케줄링을 선점 스케줄링, 빼앗을 수 없는 스케줄링을 비선점 스케줄링이라고 합니다.
'CS' 카테고리의 다른 글
혼자 공부하는 컴퓨터 구조 + 운영체제 | 1장 ~ 3장 (0) | 2022.12.17 |
---|---|
쉽게 배우는 운영체제 | 교착 상태 (0) | 2022.12.02 |
쉽게 배우는 운영체제 | 공유 자원과 임계구역 (0) | 2022.11.16 |
쉽게 배우는 운영체제 | 프로세스 동기화 (0) | 2022.11.13 |
HTTP 완벽 가이드 | URL과 리소스 (0) | 2022.07.25 |