10. 프로세스와 스레드
1. 프로세스 개요
실행 중인 프로그램을 "프로세스(precess)" 라고 한다. 프로그램은 실행되기 전까지는 그저 보조기억장치에 있는 데이터 영역일 뿐이지만, 보조기억장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간 그 프로그램은 프로세스가 된다.
1.1 프로세스 직접 확인하기
컴퓨터가 부팅되는 순간부터 수많은 프로세스들이 실행된다. 윈도에서는 작업관리자를 통해서 확인할 수 있거, 유닉스 체계의 운영체제 에서는 ps 명령어로 확인할 수 있다.
실제로 컴퓨터를 켜고 확인해보면 우리가 실행한 프로세스 외에도 알 수 없는 여러 프로세스가 실행되고 있는 것을 볼 수 있다. 그중에는 사용자가 볼 수 있는 공간에서 실행되는 프로세스도 있지만, 보이지 않는 공간에서 실행되는 프로세스들도 있다. 전자는 사용자가 보는 앞에서 실행되는 프로세스라는 점에서 "포그라운드 프로세스(foregroung process)" 라고 하고, 후자는 사용자가 보지 못하는 뒤편에서 실행되는 프로세스라는 점에서 "백그라운드 프로세스(background process)" 라고 한다.
백그라운드 프로세스 중에는 사용자와 직접 상호작용할 수 있는 백그라운드 프로세스도 있지만, 사용자와 상호작용하지 않고 그저 묵묵히 정해진 일만 수행하는 백그라운드 프로세스도 있다. 이러한 백그라운드 프로세스를 유닉스 체계의 운영체제에서는 "데몬(daemon)" 이라고 부르고, 윈도우 운영체제에서는 "서비스(service)" 라고 부른다.
1.2 프로세스 제어 블록 (PCB)
모든 프로세스는 실행을 위해 CPU 를 필요로 하지만, CPU 자원은 한정되어 있기에, 모든 프로세스가 CPU 를 동시에 사용할 수는 없다. 그렇기에 자신의 차례가 되면 정해진 시간만큼 CPU 를 사용하고, 시간이 끝났음을 알리는 인터럽트가 발생하면 자신의 차례를 양보하고 다음 차례까지 기다린다.
운영체제는 빠르게 번갈아 수행되는 프로세스의 실행 순서를 관리하고, 프로세스 CPU 를 비롯한 자원을 배분한다. 이를 위해 운영체제는 "프로세스 제어 블록(PCB : Process Control Block)" 을 이용한다.
프로세스 제어 블록은 프로세스와 관련된 정보를 저장하는 자료 구조이다. 이는 마치 상품에 달린 태그와도 같다.
PCB 는 커널 영역에 생성된다. 운영체제는 수많은 프로세스들 사이에서 PCB 로 특정 프로세스를 식별하고 해당 프로세스를 처리하는 데 필요한 정보를 판단한다.
PCB 는 프로세스 생성 시 만들어지고 실행이 끝나면 폐기된다.
PCB 에 담기는 대표적인 정보들은 아래와 같다.
프로세스 ID
"프로세스 ID" 는 특정 프로세스를 식별하기 위해 부여하는 고유 번호이다.
레지스터 값
프로세스는 자신의 실행 차례가 돌아오면 이전까지 사용했던 레지스터의 중간값들을 모두 복원한다.
그래야만 이전까지 진행했던 작업들을 그대로 이어갈 수 있을 것이다.
프로세스 상태
CPU 스케줄링 정보
프로세스가 언제, 어떤 순서로 CPU 를 할당받을지에 대한 정보도 PCB 에 기록된다.
메모리 관리 정보
프로세스마다 메모리에 저장된 위치가 다르다. 그래서 PCB 에는 프로세스가 어느 주소에 저장되어 있는지에 대한 정보가 있어야 한다.
사용한 파일 입출력장치 목록
프로세스가 실행 과정에서 특정 입출력장치나 파일을 이용하면 PCB 에 해당 내용이 명시된다.
1.3 문맥 교환(context switching)
하나의 프로세스에서 다른 프로세스로 실행 순서가 넘어가면 기존 프로세스의 프로그램 카운터를 비롯한, 각종 레지스터 값, 메모리 정보, 실행을 위해 열었는 파일이나 사용한 입출력장치 등... 지금까지의 중간 정보를 백업해야 한다. 그래야 다음 차례가 왔을 때 이전까지 실행했던 내용에 이어 다시 실행을 재개할 수 있을것이다.
이러한 중간 정보, 즉 하나의 프로세스 수행을 재개하기 위해서 기억해야 할 정보를 "문맥(context)" 라고 한다. 하나의 프로세스 문맥은 해당 프로세스의 PCB 에 표현되어 있다. PCB 에 기록되는 정보들을 문맥이라고 봐도 무방한다.
이처럼 기존 프로세스의 문맥을 PCB 에 백업하고, 새로운 프로세스를 실행하기 위해 문맥을 PCB 로부터 복구하여 새로운 프로세스를 실행하는 것을 "문맥 교환(context switching)" 이라고 한다.
문맥 교환은 여러 프로세스가 끊임없이 빠르게 번갈아 가며 실행되는 원리이다. 문맥 교환이 자주 일어나면 프로세스는 그만큼 빨리 번갈아 가며 수행되기 때문에, 프로세스들이 동시에 실행되는 것처럼 보인다.
1.4 프로세스 메모리 영역
프로세스 커널 영역에는 PCB 가 생성되고, 사용자 영역에는 크게 "코드 영역", "데이터 영역", "힙 영역", "스택 영역" 으로 나뉘어 저장된다.
1.4.1 코드 영역
"코드 영역(code segment)" 은 "텍스트 영역(text segment)" 이라고도 불린다. 말 그대로 실행할 수 있는 코드, 즉 기계어로 이루어진 명령어가 저장된다.
코드 영역에는 데이터가 아닌 CPU 가 실행할 명령어가 담겨 있기 때문에, 쓰기가 금지되어 있다. 다시 말해 코드 영역은 읽기 전용(read-only) 공간이다.
1.4.2 데이터 영역
"데이터 영역(data segment)" 은 잠깐 썼다가 없앨 데이터가 아닌 프로그램이 실행되는 동안 유지할 데이터가 저장되는 공간이다. 이런 데이터로는 "전역 변수(global variable)" 가 대표적이다.
코드 영역과 데이터 영역은 그 크기가 변하지 않는다.
프로그램을 구성하는 명령어들이 갑자기 바뀔 일이 없으니 코드 영역의 크기가 변할 리 없고,
데이터 영역에 저장될 내용은 프로그램이 실행되는 동안에만 유지될 데이터가 때문에 크기가 변할 리 없다.
그래서 코드 영역과 데이터 영역은 '크기가 고정된 영역' 이라는 점에서 "정적 할당 영역" 이라고도 부른다.
반면, 힙 영역과 스택 영역은 프로세스 실행 과정에서 그 크기가 변할 수 있는 영역이다. 그래서 이 두 영역을 "동적 할당 영역" 이라고도 부른다.
1.4.3 힙 영역
"힙 영역(heap segnemt)" 은 프로그램을 만드는 사용자, 즉 프로그래머가 직접 할당할 수 있는 저장 공간이다. 프로그래밍 과정에서 힙 영역에 메모리 공간을 할당했다면 언젠가는 해당 공간을 반환해야 한다.
메모리 공간을 반환하지 않는다면 할당한 공간은 메모리 내에 계속 남아 메모리 낭비를 초래한다. 이런 문제를 "메모리 누수(memory leak)" 이라고 한다.
1.4.4 스택 영역
"스택 영역(stack segment)" 은 데이터를 일시적으로 저장하는 공간이다. 데이터 영역이 담기는 값과는 달리 잠깐 쓰다가 말 값들이 저장되는 공간이다. 이런 데이터로는 함수의 실행이 끝나면 사라지는 매개 변수, 지역 변수가 대표적이다.
2. 프로세스 상태와 계층 구조
프로세스는 저마다 상태가 있고, 운영체제는 이런 프로세스의 상태를 PCB 에 기록하여 관리한다. 그리고 많은 운영체제는 이처럼 동시에 실행되는 수많은 프로세스를 계층적으로 관리한다.
2.1 프로세스 상태
우리가 컴퓨터를 사용할 때 여러 프로세스들이 빠르게 번갈아 가면서 실행된다. 그 과정에서 하나의 프로세스는 여러 상태를 거치며 실행된다. 그리고 운영체제는 프로세스의 상태를 PCB 를 통해 인식하고 관리한다.
프로세스 상태는 운영체제마다 조금 다를 수 있지만, 프로세스가 가질 수 있는 대표적인 상태는 아래와 같다.
2.1.1 생성 상태
프로세스를 생성 중인 상태를 "생성 상태(new)" 라고 한다. 이제 막 메로리에 적재되어 PCB 를 할당 받은 상태를 말한다. 생성 상태를 거쳐 실행할 준비가 완료된 프로세스는 곧바로 실행되지 않고 준비 상태가 되어 CPU 의 할당을 기다린다.
2.1.2 준비 상태
"준비 상태(ready)" 는 당장이라도 CPU 를 할당받아 실행할 수 있지만, 아직 자신의 차례가 아니기에 기다리고 있는 상태이다. 준비 상태 프로세스는 차례가 되면 CPU 를 할당받아 실행 상태가 된다.
2.1.3 실행 상태
"실행 상태(running)" 는 CPU 를 할당받아 실행 중인 상태를 의미한다. 실행 상태인 프로세스는 할당된 일정 시간 동안만 CPU 를 사용할 수 있다. 이 때 프로세스가 할당된 시간을 모두 사용한다면 다시 준비 상태가 되고, 실행 도중 입출력장치를 사용하여 입출력 장치의 작업이 끝날 때까지 기다려야 한다면 대기 상태가 된다.
2.1.4 대기 상태
프로세스는 실행 도중 입출력장치를 사용하는 경우가 있다. 입출력 작업은 CPU 에 비해 처리 속도가 느리기에, 입출력 작업을 요청한 프로세스는 입출력장치가 입출력을 끝날 때까지 기다려야 한다. 이렇게 입출력장치의 작업을 기다리는 상태를 "대기 상태(bloacked)" 라고 한다. 입출력 작업이 완료되면 해당 프로세스는 다시 "준비 상태"로 CPU 할당을 기다린다.
2.1.5 종료 상태
"종료 상태(terminated)" 는 프로세스가 종료된 상태이다. 프로세스가 종료되면 운영체제는 PCB 와 프로세스가 사용한 메모리를 정리한다.
2.2 프로세스 계층 구조
프로세스는 실행 도중 시스템 콜을 통해 다른 프로세스를 생성할 수 있다. 이떄 새 프로세스를 생성한 프로세스를 "부모 프로세스(parent process), 부모 프로세스에 의해 생성된 프로세스를 "자식 프로세스(child process)" 라고 한다.
부모 프로세스와 자식 프로세스는 엄연히 다른 프로세스기에 각기 다른 PID 를 가진다. 일부 운영체제에서는 자식 프로세스의 PCB 에 부모 프로세스의 PID 인 "PPID(Parent PID)" 가 기록되기도 한다.
많은 운영체제들은 이처럼 프로세스가 다른 프로세스를 낳는 계층적인 구조로써 프로세스들을 관리한다. 컴퓨터가 부팅될 때 실행되는 최초의 프로세스가 자식 프로세스들을 생성하고, 생성된 자식 프로세스들이 새로운 프로세스들을 낳는 형식으로 여러 프로세스가 동시에 실행되는 것이다.
이와 같은 트리 구조를 "프로세스 계층 구조" 라고 한다.
2.3 프로세스 생성 기법
부모 프로세스가 자식 프로세스를 어떻게 만들어내고, 자식 프로세스는 어떻게 자신만의 코드를 실행하는지 조금 더 자세히 알아보자.
결론부터 말하면, 부모 프로세스를 통해 생성된 자식 프로세스들은 "복제와 옷 갈아입기" 를 통해 실행된다.
부모 프로세스는 "fork" 를 통해 자신의 복사본을 자식 프로세스로 생성해내고,
만들어진 복사본(자식 프로세스) 는 "exec" 를 통해 자신의 메모리 공간을 다른 프로그램을 교체한다.
fork, exec 는 시스템 콜이다!
fork 는 자기 자신 프로세스의 복사본을 만드는 시스템 콜로써, 자식 프로세스는 부모 프로세스의 복사본이기 때문에, 부모 프로세스의 자원들(이를테면 메모리의 내용, 열린 파일의 목록 등)이 자식 프로세스에 상속된다.
fork 를 통해 복사본이 만들어진 뒤에 자식 프로세스는 exec 시스템 콜을 통해서 새로운 프로그램으로 전환된다. exec 는 자신의 메모리 공간을 새로운 프로그램으로 덯어쓰는 시스템 콜이다.
메모리 공간에 새로운 프로그램 내용이 덮어 써진다는 점에서 이는 자식 프로세스가 새로운 옷으로 갈아입었다고 볼 수 있다. exec 를 호출하면 코드 영역과 데이터 영역의 내용이 실행할 프로그램의 내용으로 바뀌고, 나머지 영역은 초기화된다.
정리하면, 부모가 자식 프로세스를 실행하며 프로세스 계층 구조를 이루는 과정은 fork 와 exec 가 반복되는 과정이라고 볼 수 있다.
3. 스레드
"스레드(thread)" 는 프로세스를 구성하는 실행 흐름 단위이다. 그리고 하나의 프로세스는 여러 개의 스레드를 가질 수 있다. 스레드를 사용하면 하나의 프로세스에서 여러 부분을 동시에 실행할 수 있다.
3.1 프로세스와 스레드
전통적인 관점에서 보면 하나의 프로세스는 한 번에 하나의 일만 처리했다. '실행의 흐름 단위가 하나' 라는 점에서 이렇게 실행되는 프로세스들은 "단일 스레드 프로세스" 라고 볼 수 있다.
하지만 "스레드" 라는 개념이 도입되면서 하나의 프로세스가 한 번에 여러 일을 동시에 처리할 수 있게 되었다. 즉, 프로세스를 구성하는 여러 명령어를 동시에 실행할 수 있게 된 것이다.
이런 점으로 미루어볼 때 스레드를 '프로세스를 구성하는 실행 단위' 로 볼 수 있다. 스레드는 프로세스 내에서 각기 다른 스레드 ID, 프로그램 카운터 값을 비롯한 레지스터 값, 스택으로 구성된다. 때문에 각각의 스레드를 각각의 코드를 실행할 수 있다.
여기서 중요한 점은 프로세스의 스레드들은 실행에 필요한 최소한의 정보(프로그램 카운터 포함한 레지스터, 스택) 만을 유지한 채 프로세스 자원을 공유하며 실행된다는 점이다.
3.2 멀티프로세스와 멀티스레드
여러 프로세스를 동시에 실행하는 것을 "멀티프로세스", 그리고 여러 스레드로 프로세스를 동시에 실행하는 것을 "멀티스레드" 라도 한다. 그렇다면 동일한 작업을 수행하는 단일 스레드 프로세스를 여러 개를 실행하는 것과 하나의 프로세스를 여러 스레드로 실행하는 것에는 어떤 차이가 있을까?
여기에는 큰 차이가 있다. 기본적으로 프로세스끼리는 자원을 공유하지 않지만, 스레드끼리는 같은 프로세스 내의 자원을 공유한다는 점이다.
프로세스를 fork 하여 같은 작업을 하는 동일한 프로세스 두 개를 동시에 실행하면 코드 영역, 데이터 영역, 힙 영역 등을 비롯한 모든 자원이 복제되어 메모리에 적재된다. fork 를 세 번, 네 번 하면 마찬가지로 메모리에는 같은 프로세스가 통째로 세 개, 네 개 적재된다. 이는 어찌 보면 낭비이다.
이에 반해 스레드들은 각기 다른 스레드 ID, 프로그램 카운터 값을 포함한 레지스터 값, 스택을 가질 뿐 프로세스가 가지고 있는 자원을 공유한다. 여러 프로세스를 병행 실행하는 것보다 메모리를 더 효율적으로 사용할 수 있을 것이다.
하지만 프로세스 자원을 공유한다는 특성은 때론 단점이 될 수도 있다. 멀티프로세스 환경에서는 하나의 프로세스에 문제가 생겨도 다른 프로세스에는 지장이 적거나 없지만, 멀티스레드 환경에서는 하나의 스레드에 문제가 생기면 프로세스 전체에 문제가 생길 수 있다.
Last updated