본 포스팅은 인프런의 개발자를 위한 컴퓨터공학 1: 혼자 공부하는 컴퓨터구조 + 운영체제를 참조하여 작성한 글입니다.
지금까지 프로세스는 실행 중인 프로그램이라고 표현을 하였다. 프로그램은 실행되기 전까지 그저 보조기억장치에 있는 데이터 덩어리일 뿐이지만 보조기억장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간 그 프로그램은 프로세스가 된다. 그럼 프로세스에 대해 자세히 알아보자.
우리는 실제로 프로세스를 직접 확인할 수 있다. 윈도우 운영체제같은 경우는 작업 관리자의 프로세스 탭을 클릭하여 확인해보면 되고 리눅스계역은 아래와 같은 명령어를 치면 된다.
ps -ef실제로 확인해보면 다양한 프로세스들이 존재한다. 우리가 직접 실행한 프로그램의 프로세스 외에도 알 수 없는 여러 프로세스가 실행되고 있는 것을 볼 수 있을 것이다. 그 중에 사용자가 볼 수 있는 공간에 실행되는 프로세스를 포그라운드 프로세스라고 말하고 사용자가 볼 수 없는 공간에 실행되는 프로세스를 백그라운드 프로세스라고 한다. 백그라운드 프로세스 같은 경우는 사용자와 직접 상호작용하는 프로세스들도 존재하지만 사용자와 상호작용하지 않고 그저 정해진 일만 수행하는 프로세스들이 있는데 대표적으러 데몬이나 서비스같은 것이 존재한다.
모든 프로세스는 실행을 하기 위해서 CPU가 필요하다. 하지만 CPU 자원은 매우 한정적이다. 그래서 프로세스들은 돌아가면서 한정된 시간만큼만 CPU를 이용한다. 즉, 자신의 차례에 정해진 시간만큼 CPU를 이용하고 타이머 인터럽트가 발생하였다면 다음 프로세스에게 차례를 양보한다.
타이머 인터럽트: 클럭신호를 발생시키는 장치에 의해 주기적으로 발생하는 하드웨어 인터럽트
이렇게 빠르게 번갈아가면서 수행되는 프로세스들을 잘 관리해야 한다. 이를 위해 사용하는 자료구조가 PCB 이다. PCB는 프로세스 관련 정보를 저장하는 자료구조이다. 마치 상품에 달린 태그와 같은 정보라고 생각하면 좋을 것 같다. 이렇게 PCB는 프로세스 생성 시에 커널영역에 생성되고 프로세스가 종료하면 삭제된다.
PCB에 담긴 대표적인 정보는 다음과 같다.
그러면 해당 정보들을 하나씩 알아보자. PID란, 특정 프로세스를 식별하기 위해 부여하는 고유번호이다. 마치 학교의 학번, 회사의 사번과 같은 것이라고 생각하면 좋을 것 같다. 다음으로 레지스터 값이다. 프로세스는 자신의 실행차례가 오면 이전까지 사용한 레지스터 중간 값을 모두 복원해야 한다. 그리고 실행이 재개한다. 이런 것을 위해 레지스터 값을 PCB에 저장한다. 대표적으로 PC, 스택 포인터들이 존재한다. 다음으로 프로세스 상태이다. 입출력 장치를 사용하기 위해 기다리는 상태, CPU 사용을 위해 기다리는 상태, CPU를 이용 중인 상태등을 저장한다. 다음으로 CPU 스케쥴링 정보이다. 프로세스가 언제, 어떤 순서로 CPU를 할당받을지에 대한 정보이다. 다음으로 메모리 관련 정보이다. 프로세스가 어느 주소에 저장되어 있는지에 대한 정보이다. PCB에는 베이스 레지스터, 한계 레지스터 값과 같은 정보들이 담긴다. 또한 프로세스의 주소를 알기 위한 또 다른 중요 정보 중 하나인 페이지 테이블 정보도 PCB에 담긴다. 다음으로 사용한 파일과 입출력 장치 목록이다. 프로세스가 실행 과정에서 특정 입출력 장치나 파일을 사용하면 PCB에 해당 내용이 명시된다. 즉, 어떤 입출력 장치가 이 프로세스에 할당되었는지, 어떤 파일들을 열었는지에 대한 정보들이 PCB에 기록된다.
한 프로세스에서 다른 프로세스로 실행 순서가 넘어가면 어떻게 될까? 기존에 실행되던 프로세스는 지금까지의 중간 정보를 백업한다. PC등 각종 레지스터 값, 메모리 정보, 열었던 파일, 사용한 입출력 장치등 이러한 중간 정보들을 백업해야 하는데 이 중간 정보를 '문맥'이라고 한다. 다음 차례가 왔을 때 실행을 재개하기 위한 정보라고도 한다. 이렇게 실행 문맥을 백업해두면 언제든 해당 프로세스의 실행을 재개할 수 있다. 뒤이어 실행할 프로세스의 문맥을 복구해야 자연스럽게 실행중인 프로세스로 바뀔 것이다.
이처럼 기존의 실행 중인 프로세스 문맥을 백업하고 새로운 프로세스 실행을 위해 문맥을 복구하는 과정을 문백교환이라고 한다. 이 원리가 여러 프로세스가 끊임없이 빠르게 번갈아가며 실행하는 원리이다.
그렇다면 사용자 영역에는 프로세스가 어떻게 배치될까? 크게 코드영역(=텍스트 영역), 데이터 영역, 스택 영역, 힙 영역이 존재한다. 하나씩 살펴보자.
코드 영역은 실행할 수 있는 코드, 기계어로 이루어진 명령어가 저장된다. 데이터가 아닌 CPU가 실행 할 명령어가 담기기에 쓰기가 금지된 영역을 말한다. 그리고 이렇게 크기가 고정되어 있는 영역을 정적 할당 영역(크기 고정)이라고 한다. 다음으로 데이터 영역을 살펴보자. 데이터 영역은 잠깐 썼다가 없앨 데이터가 아닌 프로그램이 실행되는 동안 유지할 데이터를 저장한다. 대표적으로 프로그래밍 언어의 전역 변수가 저장된다. 해당 데이터 영역도 정적 할당 영역이다.
다음으로 힙 영역이다. 힙 영역은 프로그램을 만드는 사용자, 즉 개발자가 직접 할당할 수 있는 저장공간이다. 사용한 공간은 다시 반환해야 하며 그렇지 않으면 Memory Leak문제가 발생한다. 그런데 요즘 프로그래밍 언어에서는 Garbage Collector라는 녀석이 알아서 해준다. 그리고 이런 힙 영역을 동적 할당 영역이라고 부른다. 그리고 저장을 할때는 힙에 특성에 맞게 낮은 주소에서 높은 주소로 쌓아서 올린다. 다음으로 스택 영역이다. 스택 영역은 데이터가 일시적으로 저장되는 공간이다. 데이터 영역에 담기는 값과 달리 잠깐 쓰다가 말 값들이 저장되는 공간으로 매개변수, 지역변수등이 저장된다. 이런 영역을 동적 할당 영역이라고 부르며 힙과 반대로 일반적으로 높은 주소에서 낮은 주소로 할당된다.
프로세스는 저마다의 상태가 있다. 운영체제는 이런 프로세스의 상태를 PCB에 기록하여 관리한다. 그리고 많은 운영체제는 이처럼 동시에 실행되는 수많은 프로세스를 계층적으로 관리한다.
프로세스의 상태는 운영체제마다 조금 씩 차이는 존재하지만 대부분 아래의 상태가 존재한다. 한번 살펴보자.
프로세스가 대기 상태가 되는 이유에 입출력 작업만 있는 것은 아니다. 조금 더 일반적으로 표현하자면 특정 이벤트가 일어나길 기다릴 때 프로세스는 대기 상태가 된다. 다만 프로세스가 대기 상태가 되는 대부분의 원인이 입출력 작업이기 때문에 프로세스가 입출력 작업을 하면 대기 상태가 된다고 생각하면 좋을 것 같다.
프로세스는 실행 도중 시스템 콜을 통해 다른 프로세스를 생성할 수 있다. 이때 새 프로세스를 생성한 프로세스를 부모 프로세스, 부모 프로세스에 의해 생성된 프로세스를 자식 프로세스 라고 한다. 부모 프로세스와 자식 프로세스는 별개의 프로세스이므로 각기 다른 PID를 가진다.
일부 운영체제에서는 자식 프로세스 PCB에 부모 프로세스 PID를 명시한다.
이렇게 프로세스의 계층적인 구조가 형성된다.
그러면 부모 프로세스는 자식 프로세스를 어떻게 만들어 내고 자식 프로세스는 어떻게 자신만의 코드를 실행할까? 이것은 비유적으로 표현할 수 있을 것 같다. 바로 복제와 옷 갈아입기이다. 부모 프로세스는 fork 시스템 콜을 통해 자신의 복사본을 만들어 자식 프로세스를 생성한다. 그러면 자식 프로세스는 exec 시스템 콜을 통해 자신의 메모리 공간을 다른 프로그램으로 교체한다.
fork 시스템 콜은 복사본(= 자식 프로세스)을 생성하고 부모 프로세스의 자원을 상속한다. 그렇다고 하더라도 PID와 저장된 메모리 위치는 달라진다. 그리고 exec 시스템 콜은 메모리 공간을 새로운 프로그램으로 덮어쓰기를 진행한다. 코드/데이터 영역은 실행할 프로그램 내용으로 바뀌고 나머지 영역은 초기화된다.
스레드는 프로세스를 구성하는 실행의 흐름 단위이다. 그리고 하나의 프로세스는 하나 이상의 스레드를 가진다. 우리는 개발자로서 일을 하게 되면서 반드시 스레드를 다루는 날이 올 것이다. 하지만 스레드와 관련된 내용은 프로그래밍 기본서만 보면 놓치기 쉬운 부분이기도 하다. 이번에는 스레드와 멀티스레드가 무엇이고 멀티스레드는 멀티프로세스와 어떤 차이가 있는지 알아보자.
전통적인 관점에서 하나의 프로세스는 한번에 하나의 일만 처리하였다. 즉, 실행의 흐름이 하나인 단일 스레드 프로세스였다. 하지만 스레드라는 개념이 도입되면서 실행 흐름이 여러개인 프로세스로 발전하였다. 이것을 우리는 멀티스레드 프로세스라고 한다. 이렇게 되면 프로세스를 이루는 여러 명령어를 동시 실행이 가능하다.
그러면 스레드에 어떤 것들이 있길래 이렇게 동시 실행이 가능할까? 스레드의 구성요소로는 스레드 ID, PC를 비롯한 레지스터 값, 스택등 실행에 필요한 최소 정보가 저장된다. 여기서 중요한 점은 스레드들은 실행에 필요한 최소한의 정보(프로그램 카운터를 포함한 레지스터, 스택)만을 유지한 채 프로세스 자원을 공유하며 실행된다는 점이다. 이렇게 멀티스레드일 때 프로세스의 자원을 공유하는 것이 스레드의 핵심이다.
컴퓨터는 실행 과정에서 여러 프로세스가 동시에 실행될 수 있고, 그 프로세스들을 이루는 스레드는 여러개 있을 수 있다고 했다. 이때 여러 프로세스를 동시에 실행하는 것을 멀티프로세스 라고 부르고 여러 스레드로 프로세스를 동시에 실행하는 것을 멀티스레드 라고 부른다. 그러면 멀티프로세스와 멀티스레드의 차이는 무엇이 있을까?
멀티프로세스와 멀티스레드에는 큰 차이가 존재한다. 프로세스끼리는 기본적으로 자원을 공유하지 않지만 스레드끼리는 같은 프로세스 내의 자원을 공유한다는 점이다. 프로세스를 fork하면 코드/데이터/힙 영역등 모든 자원이 복제되어 저장된다. 저장된 메모리 주소를 제외하면 모든 것이 동일한 프로세스이다. fork를 이렇게 3번, 4번하면 메모리에는 같은 프로세스가 통째로 3개, 4개 적재된다.
fork 직후 같은 프로세스를 통째로 메모리에 중복 저장하지 않으면서 동시에 프로세스끼리 자원을 공유하지 않는 방법도 있다. 이를 쓰기 시 복사(copy on write) 기법이라고 한다.
스레드들은 각기 다른 스레드 ID(별도의 실행을 위해 꼭 필요한) PC 값을 포함한 레지스터 값, 스택을 가질 뿐 프로세스가 가지는 자원들은 공유를 하는 방식으로 한다. 하지만 프로세스들끼리는 자원을 공유하지 못하고 마치 남남처럼 독립 실행이 된다. 이렇게 스레드는 프로세스의 자원을 공유하기에 협력과 통신에 유리한 장점이 존재한다.
물론 프로세스끼리는 자원을 공유하지 못하는 것은 아니다. 프로세스 간에도 자원을 주고 받을 수 있다. 이런 기법을 IPC라고 한다. 또한 IPC 외에도 파일을 통한 프로세스 간 통신을 진행하고 공유 메모리를 통한 프로세스간 통신도 가능하다. 하지만 기본적으로는 자원 공유를 못한다. 또한 앞서 말한 방법들은 스레드끼리 공유하는 방법보다 더 복잡하고 어럽다.