10-2 확인문제 1번 | ① 생성 ② 준비 ③ 실행 ④ 종료 ⑤ 대기
11-2 확인문제 1번 풀이 | 선입 선처리 스케줄링은 준비 큐에 삽입된 순서대로 실행되기 때문에 프로세스 A부터 차례대로 실행된다. 그러므로 답은 ③번이다.
모든 프로그램은 하드웨어를 필요로 한다. 이때 프로그램 실행에 마땅히 필요한 요소들을
가리켜 시스템 자원, 혹은 줄여서 자원이라고 하는데, 지금까지 나온 CPU, 메모리,
보조기억장치, 입출력장치 등과 같은 컴퓨터 부품들은 모두 자원이라고 볼 수 있다.
즉, 모든 프로그램은 실행되기 위해 반드시 자원이 필요하다.
여기서 실행할 프로그램에 필요한 자원을 할당하고, 프로그램이 올바르게 실행되도록 돕는 특별한 프로그램이 바로 운영체제이다.
운영체제는 인터넷 브라우저, 게임과 같은 프로그램으로 여느 프로그램과 마찬가지로 메모리에 적재되어야 한다. 다만 운영체제는 매우 특별한 프로그램이기 때문에 항상 컴퓨터가 부팅될 때 메모리 내 커널 영역이라는 공간에 따로 적재되어 실행된다. 커널 영역을 제외한 사용자가 이용하는 응용 프로그램이 적재되는 영역을 사용자 영역이라고 한다.
더 자세히 알아보자면 운영체제는 실행할 여러 응용 프로그램을 메모리에 주소가 겹치지 않게 적재하고, 더 이상 실행되지 않는 프로그램을 메모리에서 삭제하며 지속적으로 메모리 자원을 관리한다.
또한 프로그램이 실행되려면 반드시 CPU가 필요한데, 어느 한 프로그램이 CPU를 독점하면 다른 프로그램들은 올바르게 실행될 수 없기 때문에 운영체제는 최대한 공정하게 여러 프로그램에 CPU 자원을 할당한다.
뿐만 아니라 두 개의 응용 프로그램이 똑같은 입출력장치를 필요로 할 때, 두 개의 프로그램이 입출력장치를 동시에 사용하지 못하도록 막고, 하나의 프로그램이 이용하는 동안 다른 프로그램은 기다리게 만들어 자원을 관리한다.
이처럼 운영체제는 커널 영역에 적재되어 응용 프로그램과 하드웨어 사이에서 응용 프로그램에 필요한 자원을 할당하고, 응용 프로그램이 올바르게 실행되도록 관리하는 역할을 맡는다.
운영체제가 없다면 개발자는 하드웨어를 조작하는 코드를 직접 작성해야 한다.
하지만 운영체제가 하드웨어를 조작하고 관리하는 기능들을 제공하기 때문에 개발자는 하드웨어를 조작하는 코드를 직접 작성할 필요 없이 운영체제의 도움을 받아 간편하게 개발할 수 있다.
그렇다면 개발자는 왜 운영체제에 대해서 공부해야 할까.
그 이유는 문제 해결 능력과 관련이 있다. 운영체제는 딱딱한 하드웨어가 아닌 프로그램이다. 그렇기 때문에 직접 대화하며 현재 하드웨어의 상태는 어떠한지, 코드가 어떻게 실행되었는지, 하드웨어 상에 어떤 문제가 있었는지 등을 알려줄 수 있고, 이를 통해 문제 해결의 실마리를 찾을 수 있다.
대표적인 운영체제와의 대화는 오류 메시지가 있다. 대다수의 오류 메시지의 근원은 운영체제이기 때문에 프로그래밍 문법만 학습한 사람들은 운영체제의 메시지를 이해하기 어렵고, 문제를 진단하고 해결하기 어렵다. 그렇기 때문에 운영체제에 대해 공부하면 오류 메시지를 이해하고 해결하기 편리해진다. 이것이 오늘날 다수의 기업에서 운영체제에 대한 이해를 필수적인 기초 역량으로 요구하는 이유이기도 하다.
운영체제는 사용자를 위한 프로그램이 아닌 사용자가 실행하는 프로그램을 위한 프로그램이다.
세상에는 다양한 운영체제가 있다. 그렇기에 운영체제 서비스 또한 매우 다양하다. 하지만 그중에서도 가장 핵심적인 서비스들이 있는데, 그 핵심 서비스를 담당하는 부분을 커널이라고 한다.
운영체제가 설치된 모든 기기에는 커널이 있다. 어떤 커널을 사용하는지에 프로그램이 하드웨어를 이용하는 양상이 달라지고, 결과적으로 컴퓨터 전체의 성능도 달라질 수 있다.
운영체제가 제공하는 서비스 중 커널에 포함되지 않는 서비스도 있는데,
대표적으로 사용자 인터페이스가 있다. 사용자 인터페이스는 윈도우의 바탕화면과 같이
사용자가 컴퓨터와 상호작용할 수 있는 통로인데, 운영체제가 제공하는 사용자 인터페이스는
그래픽 유저 인터페이스와 커맨드 라인 인터페이스가 있다.
그래픽 유저 인터페이스는 윈도우 바탕화면이나 스마트폰의 화면처럼 그래픽을 기반으로 컴퓨터와 상호작용할 수 있는 인터페이스로, 우리가 윈도우 운영체제 컴퓨터를 이용할 때 마우스를 이용해 다양한 프로그램을 실행할 수 있는 것은 윈도우 운영체제가 그래픽 유저 인터페이스를 지원하기 때문이다. 또 안드로이드 스마트폰 속 다양한 앱을 터치하여 실행할 수 있는 것 또한 같은 이유이다.
반면 커맨드 라인 인터페이스는 명령어를 기반으로 컴퓨터와 상호작용할 수 있는 인터페이스이기 때문에 아이콘이나 다채로운 그래픽 화면이 없다. 사용자는 컴퓨터를 사용하기 위해 정해진 명령어를 입력함으로써 컴퓨터와 상호작용할 수 있다.
이러한 사용자 인터페이스는 운영체제가 제공하는 서비스이지만, 이는 그저 컴퓨터와 상호작용하기 위한 통로일 뿐 커널에 속한 기능은 아니다.
운영체제는 사용자가 실행하는 응용 프로그램이 하드웨어 자원에 직접 접근하는 것을 방지하여 자원을 보호한다. 만약 응용 프로그램이 CPU, 메모리, 하드 디스크 등에 마음대로 접근하고 조작할 수 있다면 자원이 무질서하게 관리될 것이고, 조금만 실수해도 컴퓨터 전체에 큰 악영향을 끼칠 것이다. 그래서 운영체제는 오직 자신을 통해서만 자원에 접근할 수 있도록 하는 것이다.
응용 프로그램이 자원에 접근하기 위해서는 운영체제에 도움을 요청해야 한다. 이때 '운영체제에 도움을 요청한다'는 말은 '운영체제 코드를 실행하려고 한다'는 말과 같다. 응용 프로그램의 요청을 받은 운영체제는 응용 프로그램 대신 자원에 접근하여 요청한 작업을 수행한다.
이러한 운영체제의 문지기 역할은 이중 모드로써 구현되는데,
이중 모드란 CPU가 명령어를 실행하는 모드를 크게 사용자 모드와 커널모드로 구분하는 방식이다.
사용자 모드는 운영체제 서비스를 제공받을 수 없는 실행 모드, 즉 커널 영역의 코드를 실행할 수 없는 모드로 일반적인 응용 프로그램은 기본적으로 사용자 모드로 실행된다. 사용자 모드로 실행 중인 CPU는 입출력 명령어와 같이 하드웨어 자원에 접근하는 명령어를 실행할 수 없다. 그래서 사용자 모드로 실행되는 일반적인 응용 프로그램은 자원에 접근할 수 없다.
반면 커널 모드는 운영체제 서비스를 제공받을 수 있는 실행 모드로 CPU가 커널 모드로 명령어를 실행하면 자원에 접근하는 명령어를 비롯한 모든 명령어를 실행할 수 있다.
사용자 모드로 실행되는 프로그램이 자원에 접근하는 운영체제 서비스를 제공받으려면 운영체제에 요청을 보내 커널 모드로 전환되어야 한다. 이때 운영체제 서비스를 제공받기 위한 요청을 시스템 호출이라고 한다. 시스템 호출은 일종의 인터럽트이다. 정확히는 소프트웨어적인 인터럽트인데, 특정 명령어에 의해 발생하는 인터럽트를 뜻한다.
그래서 CPU가 시스템 호출을 처리하는 순서는 인터럽트 처리 순서와 유사한데, 시스템 호출을 발생시키는 명령어가 실행되면 CPU는 지금까지의 작업을 백업하고, 커널 영역 내에 시스템 호출을 수행하는 코드(인터럽트 서비스 루틴)를 실행한 뒤 다시 기존에 실행하던 응용 프로그램으로 복귀하여 실행을 계속해 나간다.
실행 중인 프로그램을 프로세스라고 한다. 컴퓨터를 사용하는 동안에는 새로운 프로세스들이 마구 생성되고, 사용되지 않는 프로세스는 메모리에서 삭제된다. 일반적으로 하나의 CPU는 한 번에 하나의 프로세스만 실행할 수 있기에 CPU는 프로세스들을 조금씩 번갈아 가며 실행한다. 이때 각 프로세스는 상태도, 사용하고자 하는 자원도 다양하다. 그래서 운영체제는 다양한 프로세스를 일목요연하게 관리하고 실행할 수 있어야 한다.
모든 프로세스는 실행을 위해 자원을 필요로 한다. 그리고 운영체제는 그것들을 할당해 준다. 그러면 운영체제가 CPU, 메모리, 입출력장치를 어떻게 관리하고 어떤 기능을 제공하는지 알아보겠다.
CPU
일반적으로 메모리에는 여러 프로세스가 적재되고, 하나의 CPU는 한 번에 하나의 프로세스만 실행할 수 있다. 이에 운영체제는 프로세스들에 공정하게 CPU를 할당하기 위해 어떤 프로세스부터 CPU를 이용할 것인지 얼마나 오래 이용할 것인지 등을 결정할 수 있어야 하는데,
이를 CPU 스케줄링이라 한다.
메모리
메모리에 적재된 프로세스들은 크기도, 적재되는 주소도 가지각색이다. 그래서 운영체제는 새로운 프로세스가 적재될 때마다 어느 주소에 적재해야 할지를 결정해야 한다.
때로는 메모리가 이미 꽉 차 있어 꼭 실행해야 할 프로세스를 적재할 공간이 없는 경우도 있고, 메모리에 공간이 남았는 데도 불구하고 프로세스를 적재하지 못하는 상황도 발생한다. 이러한 경우들을 운영체제는 극복하고 메모리를 할당해야 한다.
입출력장치
인터럽트 서비스 루티은 운영체제가 제공하는 기능으로 커널 영역에 있다. 소프트웨어적 인터럽트뿐 아니라 하드웨어 인터럽트도 마찬가지인데, 이처럼 운영체제는 인터럽트를 처리하는 프로그램, 즉 인터럽트 서비스 루틴을 제공함으로써 입출력 작업을 수행한다.
컴퓨터를 사용할 때 여러 파일을 열고, 생성하고, 삭제하고 이 파일들을 한데 묶어 디렉터리(폴더)로 관리하는 것을 파일 시스템이라 하는데, 이것 또한 운영체제가 지원하는 핵심 서비스이다.
프로그램은 실행되기 전까지는 그저 보조기억장치에 있는 데이터 덩어리일 뿐이지만, 보조기억 장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간 그 프로그램은 프로세스가 된다.
그중에는 사용자가 볼 수 있는 공간에서 실행되는 프로세스도 있지만, 보이지 않는 공간에서 실행되는 프로세스도 있다. 전자는 사용자가 보는 앞에서 실행되는 프로세스라는 점에서
포그라운드 프로세스라고 하고, 후자는 사용자가 보지 못하는 뒤편에서 실행되는 프로세스라는 점에서 백그라운드 프로세스라고 한다.
백그라운드 프로세스 중에는 사용자와 직접 상호작용할 수 있는 백그라운드 프로세스도 있지만, 사용자와 상호작용하지 않고 그저 묵묵히 정해진 일만 수행하는 백그라운드 프로세스도 있다. 이러한 백그라운드 프로세스를 유닉스 체계의 운영체제에서는 데몬이라고 하고, 윈도우 운영체제에선 서비스라고 한다.
모든 프로세스는 실행을 위해 CPU를 필요로 하지만, CPU 자원은 한정되어 있다. 그렇기에 프로세스들은 차례대로 돌아가며 한정된 시간만큼만 CPU를 이용한다.
운영체제는 이렇게 번갈아 수행되는 프로세스의 실행 순서를 관리하고, 프로세스에 CPU를 비롯한 자원을 분배하는데, 이를 위해 프로세스 제어 블록( PCB )을 이용한다.
PCB는 프로세스와 관련된 정보를 저장하는 자료 구조로, 옷이나 가전제품에 달려 있는 태그에
해당 제품을 식별하기 위한 정보가 있는 것처럼 해당 프로세스를 식별하기 위해 꼭 필요한 정보들이 저장된다. PCB는 그렇기에 프로세스 생성 시에 커널 영역에 만들어지고 실행이 끝나면 폐기된다.
옷가게 점원이 많은 옷들 사이에서 태그로 특정 옷을 식별하고 해당 옷과 관련된 정보를 판단하는 것처럼 운영체제도 수많은 프로세스들 사이에서 PCB로 특정 프로세스를 식별하고 해당 프로세스를 처리하는 데 필요한 정보를 판단한다.
PCB에 담기는 정보는 운영체제마다 차이가 있지만, 대표적인 것들은 아래와 같다.
프로세스 ID ( PID )는 특정 프로세스를 식별하기 위해 부여하는 고유한 번호로, 같은 일을 수행하는 프로그램이라 할지라도 두 번 실행하면 PID가 다른 두 개의 프로세스가 생성된다.
프로세스는 자신의 실행 차례가 돌아오면 이전까지 사용했던 레지스터의 중간값들을 모두 복원한다. 그래야만 이전까지 진행했던 작업들을 그대로 이어 실행할 수 있기 때문이다. 그래서 PCB 안에는 해당 프로세스가 실행하며 사용했던 프로그램 카운터를 비롯한 레제스터 값들이 담긴다.
현재 프로세스가 입출력장치를 사용하기 위해 기다리고 있는 상태인지, CPU를 기다리고 있는 상태인지, 아니면 CPU를 이용하고 있는 상태인지 등의 프로세스 상태 정보가 PCB에 저장된다.
포로세스가 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보도 PCB에 기록된다.
프로세스마다 메모리에 저장된 위치가 다르다. 그래서 PCB는 프로세스가 어느 주소에 저장되어 있는지 알 수 있게 베이스 레지스터, 한계 레지스터 값과 같은 정보들을 담는다. 또한 프로세스의 주소를 알기 위한 또 다른 중요 정보인 페이지 테이블 정보도 PCB에 저장된다.
프로세스가 실행 과정에서 특정 입출력장치나 파일을 사용하면 PCB에 해당 내용이 명시된다.
하나의 프로세스에서 다른 프로세스로 실행 순서가 넘어갈 때, 실행되던 프로세스는 프로그램 카운터를 비롯한 레지스터 값, 메모리 정보, 사용한 입출력장치나 파일 등 지금까지의 중간 정보를 백업해 놓아야 한다. 그래야만 다음 차례가 왔을 때 이전까지 실행했던 내용을 이어 실행할 수 있기 때문이다.
이러한 중간정보, 즉 하나의 프로세스 수행을 재개하기 위해 기억해야 할 정보를 문맥이라고 하는데, 하나의 프로세스 문맥은 해당 프로세스의 PCB에 표현되어 있다. 따라서 PCB에 저장된 내용들이 모두 문맥이라고 생각하면 된다.
이처럼 기존 프로세스의 문맥을 PCB에 백업하고, 새로운 프로세스를 실행하기 위해 문맥을 PCB로부터 복구하여 새로운 프로세스를 실행하는 것을 문맥 교환이라고 한다.
하나의 프로세스는 커널 영역에 PCB를, 사용자 영역에 크게 코드 영역, 데이터 영역, 힙 영역, 스택 영역으로 나뉘어 저장된다.
코드 영역과 데이터 영역은 크기가 바뀌지 않기 때문에 '크기가 고정된 영역'이라는 점에서
정적 할당 영역이라고도 부른다. 반면 힙 영역과 스택영역은 프로세스 실행 과정에서 크기가
변하는 영역으로 동적 할당 영역이라고 부른다.
코드 영역은 텍스트 영역이라고도 부르는데, 이곳에는 말 그대로 실행할 수 있는 코드, 기계어로 이루어진 명령어가 저장된다. 코드 영역은 데이터가 아닌 CPU가 실행할 명령어가 담겨 있기 때문에 쓰기가 금지인 읽기 전용 공간이다.
데이터 영역은 잠깐 썼다가 없앨 데이터가 아닌 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간이다. 이런 데이터로는 대표적으로 전역 변수가 있다.
힙 영역은 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간이다. 프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가는 해당 공간을 반환해야 하는데, 메모리 공간을 반환한다는 의미는 '더 이상 해당 메모리 공간을 사용하지 않겠다'라고 운영체제에 말하는 것과 같다.
메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래한다. 이런 문제를 메모리 누수라고 한다.
스택 영역은 데이터를 일시적으로 저장하는 공간으로, 데이터 영역에 담기는 값과 달리 잠깐 쓰다가 말 값들이 저장되는 공간이다. 이런 데이터로는 매개 변수, 지역 변수가 대표적이다.
컴퓨터를 사용할 때는 여러 프로세스들이 각각마다 여러 상태를 거치며 실행된다. 그리고 운영체제는 그런 프로세스의 상태를 PCB를 통해 인식하고 관리한다. 프로세스의 상태를 표현하는 방식은 운영체제마다 조금씩 차이가 있지만, 프로세스가 가질 수 있는 대표적인 상태는 아래와 같다.
프로세스를 생성 중인 상태를 생성 상태( new )라고 한다. 이제 막 메모리에 적재되어 PCB를 할당받은 상태를 말한다. 생성 상태를 거쳐 실행할 준비가 완료된 프로세스는 곧바로 실행되지 않고 준비 상태가 되어 CPU의 할당을 기다린다.
준비 상태( ready )는 당장이라도 CPU를 할당받아 실행할 수 있지만, 아직 자신의 차례가 아니기에 기다리고 있는 상태이다. 준비 상태 프로세스는 차례가 되면 CPU를 할당받아 실행 상태가 된다. 준비 상태인 프로세스가 실행 상태로 전환되는 것을 디스패치( dispatch )라고 한다.
실행 상태( running )는 CPU를 할당받아 실행 중인 상태를 의미한다. 실행 상태인 프로세스는 할당된 일정 시간 동안만 CPU를 사용할 수 있다. 이때 프로세스가 할당된 시간을 모두 사용한다면(타이머 인터럽트가 발생하면) 다시 준비 상태가 되고, 실행 도중 입출력장치를 사용하여 입출력장치의 작업이 끝날 때까지 기다려야 한다면 대기 상태가 된다.
프로세스는 실행 도중 입출력장치를 사용하는 경우가 있다. 입출력 작업은 CPU에 비해 처리 속도가 느리기에, 입출력 작업을 요청한 프로세스는 입출력장치가 입출력을 끝낼 때까지 기다려야 한다. 이렇게 입출력장치의 작업을 기다리는 상태를 대기 상태( blocked )라고 한다. 입출력 작업이 완료되면 해당 프로세스는 다시 준비 상태로 CPU 할당을 기다린다.
종료 상태( terminated )는 프로세스가 종료된 상태다. 프로세스가 종료되면 운영체제는 PCB와 프로세스가 사용한 메모리를 정리한다.
프로세스 상태를 정리하면 이와 같은데 이런 도표를 프로세스 상태 다이어그램이라 한다. 이처럼 프로세스는 여러 상태를 거치며 실행되고, 운영체제는 이 상태들을 PCB에 기록하며 관리한다.
프로세스는 실행 도중 시스템 호출을 통해 다른 프로세스를 생성할 수 있다. 이때 새 프로세스를 생성한 프로세스를 부모 프로세스, 부모 프로세스에 의해 생성된 프로세스를 자식 프로세스라고 한다.
부모 프로세스와 자식 프로세스는 엄연히 다른 프로세스이기에 각기 다른 PID를 가진다.
일부 운영체제에서는 자식 프로세스의 PCB에 부모 프로세스의 PID인 PPID가 기록되기도 한다.
많은 운영체제는 프로세스가 프로세스를 낳는 계층적인 구조로써 프로세스들을 관리한다.
컴퓨터가 부팅될 때 실행되는 최초의 프로세스가 자식 프로세스들을 생성하고, 생성된 자식 프로세스들이 새로운 프로세스들을 낳는 형식으로 여러 프로세스가 동시에 실행된다.
이 과정을 도표로 그리면 트리 구조를 띄는데, 이를 프로세스 계층 구조라 한다.
여기서 백그라운드 프로세스인 데몬이나 서비스 또한 최초 프로세스의 자식 프로세스이다.
모든 프로세스의 가장 위에 있는 최초의 프로세스는 유닉스 운영체제에서는 init, 리눅스 운영체제에서는 systemd, macOS에서는 launchd라고 하는데, 최초의 프로세스 PID는 항상 1번이며, 모든 프로세스 최상단에 있는 부모 프로세스이다.
부모 프로세스를 통해 생성된 자식 프로세스들은 복제와 옷 갈아입기를 통해 실행된다.
자세히 보자면 부모 프로세스는 fork를 통해 자신의 복사본을 자식 프로세스로 생성해 내고, 만들어진 복사본(자식 프로세스)은 exec를 통해 자신의 메모리 공간을 다른 프로그램으로 교체한다.
fork와 exec는 시스템 호출인데, 즉 fork는 자기 자신 프로세스의 복사본을 만드는 시스템 호출인 것이다. 자식 프로세스는 부모 프로세스의 복사본이기 때문에 부모 프로세스의 자원들, 이를테면 메모리 내의 내용, 열린 파일의 목록 등이 자식 프로세스에 상속된다. 하지만 복사된 자식 프로세스라 할지라도 PID값이나 저장된 메모리 위치는 다르다.
또 exec는 자신의 메모리 공간을 새로운 프로그램으로 덮어쓰는 시스템 호출인 것인데, 다시 말해 새로운 프로그램 내용으로 전환하여 실행하는 시스템 호출이다. exec를 호출하면 코드 영역과 데이터 영역의 내용이 실행할 프로그램의 내용으록 바뀌고, 나머지 영역은 초기화된다.
부모 프로세스가 자식 프로세스를 fork한 뒤에 부모 프로세스, 자식 프로세스 누구도 exec를 호출하지 않는 경우도 있는데, 이 경우 부모 프로세스와 자식 프로세스는 같은 코드를 병행하여 실행하는 프로세스가 된다.
스레드란 프로세스를 구성하는 실행의 흐름 단위이다. 하나의 프로세스는 여러 개의 스레드를 가질 수 있는데, 스레드를 이용하면 하나의 프로세스에서 여러 부분을 동시에 실행할 수 있다.
전통적인 관점에서 보면 하나의 프로세스는 한 번에 하나의 일만을 처리했다.
예를 들어 웹 브라우저, 게임, 워드 프로세서 프로세스가 있을 때 이 모든 프로세스가 하나의 실행 흐름을 가지고 한 번에 하나의 부분만 실행되었다. 이렇게 실행되는 프로세스들을 '실행의 흐름 단위가 하나'라는 점에서 단일 스레드 프로세스라고 본다.
하지만 스레드라는 개념이 도입되면서 하나의 프로세스가 한 번에 여러 일을 동시에 처리할 수 있게 되었다. 즉, 프로세스를 구성하는 여러 명령어를 동시에 실행할 수 있게 된 것이다.
스레드의 구성 요소를 보면 프로세스 내에서 각기 다른 스레드 ID, 프로그램 카운터와 같은 레지스터 값, 스택으로 구성된다. 그래서 스레드마다 레지스터 값, 스택을 가지고 있기에 각기 다른 코드를 실행할 수 있는 것이다.
여기서 중요한 점은 프로세스의 스레드들은 실행에 필요한 최소한의 정보만을 유지한 채 프로세스 자원을 공유하며 실행된다는 점이다. 프로세스의 자원을 공유한다는 것은 스레드의 핵심이다.
컴퓨터는 실행 과정에서 여러 프로세스가 동시에 실행될 수 있고, 그 프로세스를 이루는 스레드는 여러 개 있을 수 있다. 이때 여러 프로세스를 동시에 실행하는 것을 멀티프로세스, 그리고 여러 스레드로 프로세스를 동시에 실행하는 것을 멀티스레드라고 한다.
그런데 동일한 작업을 수행하는 단일 스레드 프로세스 여러 개를 실행하는 것과 하나의 프로세스를 여러 스레드로 실행하는 것은 무엇이 다를까. 여기에는 큰 차이가 있다.
프로세스끼리는 기본적으로 자원을 공유하지 않지만, 스레드끼리는 같은 프로세스 내의 자원을 공유한다는 점이다.
프로세스를 fork하여 같은 작업을 하는 동일한 프로세스 두 개를 동시에 실행하면 코드 영역, 데이터 영역, 힙 영역 등을 비롯한 모든 자원이 복제되어 메모리에 적재된다. 이는 어찌 보면 낭비다. 같은 프로그램을 실행하기 위해 메모리에 동일한 내용들이 중복해서 존재하는 것이기 때문이다.
이에 반해 스레드들은 각기 다른 스레드 ID, 프로그램 카운터 값을 포함한 레지스터 값, 스택을 가질 뿐 프로세스가 가지고 있는 자원을 공유한다. 즉, 같은 프로세스 내의 모든 스레드는 동일한 주소 공간의 코드, 데이터, 힙 영역을 공유하고, 열린 파일과 같은 프로세스 자원을 공유한다. 그렇기에 메모리를 더 효율적으로 사용할 수 있다.
또한 서로 다른 프로세스들은 기본적으로 자원을 공유하지 않기 때문에 서로가 남남처럼 독립적으로 실행되는 반면, 스레드는 프로세스의 자원을 공유하기 때문에 서로 협력과 통신에 유리하다.
하지만 프로세스의 자원을 공유한다는 특성은 때론 단점이 될 수도 있는데,
멀티프로세스 환경에서는 하나의 프로세스에 문제가 생겨도 다른 프로세스에는 지장이 적거나 없지만, 멀티스레드 환경에서는 하나의 스레드에 문제가 생기면 프로세스 전체에 문제가 생길 수 있다.
운영체제는 프로세스들에게 공정하고 합리적으로 CPU 자원을 배분하는데, 이를 CPU 스케줄링이라 한다. CPU 스케줄링은 컴퓨터 성능과도 직결되기 때문에 아주 중요하다.
실행을 할 때는 프로세스마다 우선순위가 있는데, 우선순위가 높은 프로세스란 빨리 처리해야 하는 프로세스들을 의미한다. 대표적으로 우선순위가 높은 프로세스에는 입출력 작업이 많은 프로세스가 있다.
대부분의 프로세스들은 CPU와 입출력장치를 모두 사용하며 실행된다. 그런데 프로세스 종류에 따라 입출력장치를 이용하는 시간과 CPU를 이용하는 시간의 양에 차이가 있는 것인데, 비디오 재생이나 디스크 백업 작업을 담당하는 프로세스와 같이 입출력 작업이 많은 프로세스도 있고, 복잡한 수학 연산, 컴파일, 그래픽 처리 작업을 담당하는 프로세스와 같이 CPU 작업이 많은 프로세스가 있는 것이다. 여기서 전자는 입출력 집중 프로세스라고 하고, 후자를 CPU 집중 프로세스라고 한다.
입출력 집중 프로세스는 실행 상태보다는 입출력을 위한 대기 상태에 더 많이 머무르게 되는
반면, CPU 집중 프로세스는 대기 상태보다는 실행 상태에 더 많이 머무르게 된다. 그렇기 때문에 CPU 집중 프로세스는 CPU를 많이 사용해야 하고, 입출력 집중 프로세스는 그렇지 않아도 된다. 그런데 이 두 프로세스가 동일한 빈도로 CPU를 사용하는 것은 비합리적이다.
만약 CPU 집중 프로세스와 입출력 집중 프로세스가 동시에 CPU 자원을 요구했다고 가정할 때, 입출력 집중 프로세스를 가능한 한 빨리 실행시켜 입출력장치를 끊임없이 작동시키고,
그다음 CPU 집중 프로세스에 집중적으로 CPU를 할당하는 것이 더 효율적이다.
이렇듯 모든 프로세스가 CPU를 차례대로 돌아가며 사용하는 것보다 각각의 상황에 맞게 CPU를 배분하는 것이 효율적인데, 그러기 위해 운영체제는 프로세스마다 우선순위를 부여하는 것이다.
그렇게 운영체제는 각 프로세스 PCB에 우선순위를 명시하고, PCB에 적힌 우선순위를 기준으로 먼저 처리할 프로세스를 결정한다.
PCB에 우선순위가 적혀 있다고는 하지만, CPU를 사용할 다음 프로세스를 찾기 위해 운영체제가 일일이 모든 프로세스의 PCB를 뒤적거리는 것은 비효율적이다. 그래서 운영체제는 CPU를 사용하고 싶은 프로세스들, 메모리에 적재되고 싶은 프로세스들, 특정 입출력장치를 사용하고 싶은 프로세스들을 모두 줄을 세우는데, 운영체제는 이 줄을 스케줄링 큐로 구현하고 관리한다.
운영체제가 관리하는 대부분의 자원은 이렇듯 큐로 관리된다. 큐에는 다양한 종류가 있는데, 대표적으로 준비 큐와 대기 큐가 있다. 준비 큐는 CPU를 이용하고 싶은 프로세스들이 서는 줄을 의미하고, 대기 큐는 입출력장치를 이용하기 위해 대기 상태에 접어든 프로세스들이 서는 줄을 의미한다.
운영체제는 PCB들이 큐에 삽입된 순서대로 프로세스를 하나씩 꺼내어 실행하되, 그중 우선순위가 높은 프로세스를 먼저 실행한다. 우선순위가 낮은 프로세스들이 먼저 큐에 삽입되어 있다 할지라도 우선순위가 높은 프로세스는 그들보다 먼저 처리될 수 있다.
만약 이런 상황이 발생할 수 있다. 프로세스가 CPU를 잘 사용하고 있는데 갑자기 다른 급한 프로세스가 CPU를 지금 당장 사용하길 요청하는 것이다. 이런 상황에서는 두 가지 방법을 사용할 수 있는데, 자원을 사용중인 프로세스로부터 자원을 빼앗아 다른 프로세스에 할당할 수도 있고, 자원을 사용 중인 프로세스의 작업이 끝날 때까지 기다릴 수도 있다. 이 방법을 각각 선점형 스케줄링과 비선점형 스케줄링이라고 한다.
선점이란 '남보다 앞서서 차지함'을 의미하여, 선점형 스케줄링은 프로세스가 CPU를 비롯한 자원을 사용하고 있더라고 운영체제가 프로세스로부터 자원을 강제로 빼앗아 다른 프로세스에 할당할 수 있는 스케줄링 방식이다.
반면 비선점형 스케줄링이란 하나의 프로세스가 자원을 사용하고 있다면 그 프로세스가 종료되거나 스스로 대기 상태에 접어들기 전까진 다른 프로세스가 끼어들 수 없는 스케줄링 방식을 의미한다. 다시 말해 비선점형 스케줄링은 하나의 프로세스가 자원 사용을 독점할 수 있는 것이다.
현재 대부분의 운영체제는 선점형 스케줄링 방식을 차용하고 있지만, 선점형 스케줄링과 비선점형 스케줄링은 각기 장단점을 가지고 있다. 선점형 스케줄링은 어느 한 프로세스의 자원 독점을 막고 프로세스들에 골고루 자원을 배분할 수 있다는 장점이 있지만, 그만큼 문맥 교환 과정에서 오버헤드가 발생할 수 있다.
그와 반대로 비선점형 스케줄링은 문맥 교환에서 발생하는 오버헤드는 선점형 스케줄링보다 적지만, 하나의 프로세스가 자원을 사용 중이라면 당장 자원을 사용해야 하는 상황에서도 무작정 기다릴 수밖에 없다.
스케줄링 알고리즘의 종류는 매우 다양하다. 운영체제도 저마다 다른 스케줄링 알고리즘을 사용하는데, 중요한 것은 알고리즘마다 어떠한 아이디어를 사용했는지다. 다음을 보며 스케줄링 알고리즘에 종류를 살펴보겠다.
선입 선처리 스케줄링은 FCFS 스케줄링이라고도 불리는데, 이는 단순히 준비 큐에 삽입된 순서대로 프로세스들을 처리하는 비선점형 스케줄링 방식이다. 선입 선처리 스케줄링은 언뜻 보기에 가장 공정해 보이지만, 때때로 프로세스들이 기다리는 시간이 매우 길어질 수 있다는 점에서 부작용이 있다.
예를 들어 17ms 동안 CPU를 이용하는 프로세스 A, 5ms 동안 CPU를 이용하는 프로세스 B, 2ms 동안 CPU를 이용하는 프로세스 C가 있다면 프로세스 C는 고작 2ms를 실행하기 위해 22ms라는 긴 시간을 기다려야 한다. 그럴 때 이런 현상을 호위 효과라고 한다.
호위 효과를 방지하려면 CPU 사용 시간이 긴 프로세스는 나중에 실행하고, CPU 사용 시간이 짧은 간단한 프로세스를 먼저 실행하면 되는데, 이렇게 준비 큐에 삽입된 프로세스들 중 CPU 이용 시간의 길이가 가장 짧은 프로세스부터 실행하는 스케줄링 방식을 최단 작업 우선 스케줄링 혹은 SJF 스케줄링이라 한다.
최단 작업 우선 스케줄링은 기본적으로 비선점형 알고리즘으로 분류되지만, 선점형으로 구현될 수도 있다.
라운드 로빈 스케줄링은 선입 선처리 스케줄링에 타임 슬라이스라는 개념이 더해진 스케줄링 방식이다. 타임 슬라이스란 각 프로세스가 CPU를 사용할 수 있는 정해진 시간을 의미하는데, 라운드 로빈 스케줄링은 타임 슬라이스만큼의 시간 동안 돌아가며 CPU를 이용하는 선정형 스케줄링이다.
큐에 삽입된 프로세스들은 삽입된 순서대로 CPU를 이용하되 정해진 시간만큼만 CPU를 이용하고, 정해진 시간을 모두 사용하였음에도 아직 프로세스가 완료되지 않았다면 다시 큐의 맨 뒤에 삽입되는데, 이때 문맥 교환이 발생한다.
라운드 로빈 스케줄링에서는 타임 슬라이스 크기가 매우 중요한데, 타임 슬라이스가 지나치게 크면 사실상 선입 선처리 스케줄링과 다를 바 없어 호위 효과가 생길 여지가 있고, 타임 슬라이스가 지나치게 작으면 문맥 교환에 발생하는 비용이 커 CPU는 프로세스를 처리하는 일보다 프로세스를 전환하는 데에 온 힘을 쓰는 일이 발생할 수 있다.
최소 잔여 시간 우선 스케줄링 혹은 SRT 스케줄링은 최단 작업 우선 스케줄링 알고리즘과 라운드 로빈 알고리즘을 합친 스케줄링 방식이다. 최소 잔여 시간 우선 스케줄링에서 프로세스들은 정해진 타임 슬라이스만큼 CPU를 사용하되, CPU를 사용할 다음 프로세스로는 남아있는 작업 시간이 가장 적은 프로세스가 선택된다.
우선순위 스케줄링은 프로세스들에 우선순위를 부여하고, 가장 높은 우선순위를 가진 프로세스부터 실행하는 스케줄링 알고리즘이다.
앞서 설명한 최단 작업 우선 스케줄링, 최소 잔여 시간 우선 스케줄링 알고리즘은 넓은 의미에서 우선순위 스케줄링의 일종으로 볼 수 있다.
다만, 우선순위 스케줄링은 근본적인 문제를 내포하고 있다. 우선순위가 높은 프로세스를 우선하여 처리하는 방식이기에 우선순위가 낮은 프로세스는 우선순위가 높은 프로세스들에 의해 실행이 계속 연기될 수 있다. 이를 기아 현상이라고 한다.
이를 방지하기 위한 대표적인 기법으로 에이징이 있다. 이는 오랫동안 대기한 프로세스의 우선순위를 점차 높이는 방식으로, 에이징 기법을 적용하면 우선순위가 낮아 마냥 기다리기만 하는 프로세스가 없어진다.
다단계 큐 스케줄링은 앞서 설명한 우선순위 스케줄링의 발전된 형태인데, 우선순위별로 준비 큐를 여러 개 사용하는 스케줄링 방식이다. 우선순위가 가장 높은 큐에 있는 프로세스들을 먼저 처리하고, 우선순위가 가장 높은 큐가 비어 있으면 그다음 우선순위 큐에 있는 프로세스들을 처리한다.
이렇게 큐를 여러 개 두면 프로세스 유형별로 우선순위를 구분하여 실행하는 것이 편리해진다.
또한 큐별로 타임 슬라이스를 여러 개 지정할 수도 있고, 큐마다 다른 스케줄링 알고리즘을 사용할 수도 있다.
다단계 피드백 큐 스케줄링은 다단계 큐 스케줄링의 발전된 형태이다. 앞서 설명한 다단계 큐 스케줄링에서는 프로세스들이 큐 사이를 이동할 수 없었다. 그러나 이런 방식대로라면 다시 기아 현상이 발생할 수 있다. 이를 보완한 스케줄링 알고리즘이 바로 다단계 피드백 큐 스케줄링이다.
다단계 피드백 큐 스케줄링이 다단계 큐 스케줄링가 다른 점은 큐 사이를 이동할 수 있다는 것인데, 다단계 피드백 큐 스케줄링은 새로 준비 상태가 된 프로세스가 있다면 우선 우선순위가 가장 높은 큐에 삽입되고 일정 시간 동안 실행된다. 그리고 만약 해당 시간 동안 실행을 끝내지 못했다면 다음 우선순위 큐에 삽입되어 실행되는 방식이다.
즉, CPU를 비교적 오래 사용해야 하는 CPU 집중 프로세스들은 자연스레 우선순위가 낮아지고, CPU를 비교적 적게 사용하는 입출력 집중 프로세스들은 자연스레 운선순위가 높은 큐에서 실행이 끝나는 것이다.