
폰노이만 구조에서 프로그램이 실행된다는 것은 해당 코드가 메모리에 올라와서 작업이 진행된다는 의미입니다. 프로그램은 저장장치에 저장된 정적인 상태이고, 프로세스는 실행을 위해 메모리에 올라온 동적인 상태입니다.
프로세스에 대해서는 "실행한다"라고 표현하는데, 이는 프로그램으로 작성된 작업 절차를 실행에 옮긴다는 의미입니다. 따라서 누군가가 작성한 프로그램이 실행되면 프로세스가 됩니다.
요리를 시작하려면 종업원이 손님에게 주문을 받아 주문서를 요리사에게 전해주어야 합니다.
주문서에는 여러 가지 내용이 담겨 있습니다. 주문 순서를 알려주는 일련번호와 어떤 테이블에서 주문했는지 알려주는 테이블 번호도 있습니다. 여럿이 함께 왔다면 비슷한 시간에 음식이 나와야 하기 때문에 한 테이블에 몇 명인지도 표시되어 있습니다.
주문서는 요리의 전반적인 조리 방법과 요리 제공 순서를 결정하는 중요한 자료입니다. 주문서가 주방에 전달되면 요리사는 주문서에 적힌 대로 요리하여 손님에게 제공합니다.
일괄 작업 시스템 : 한번에 하나의 작업만 처리하는 것
이는 테이블이 하나만 있는 원테이블 레스토랑이라 할 수 있습니다. 테이블이 하나뿐이니 그 테이블에만 손님을 받을 수 있고 손님이 식사를 마치고 나간 후에야 다음 손님을 받을 수 있습니다. 한 번에 한가지 요리만 만들면 되기 때문에 요리를 제공하는 순서가 단순하고 손님이 식사하는 동안 쉬는 시간이 생기며 손님의 식사가 끝나야 다음 손님을 받을 수 있어 작업 효율은 떨어집니다.
일괄 작업 방식에서 기다리는 손님에게 미리 주문을 받는다면, 주방에서 주문서가 도착한 순서대로 요리를 하기 위해 '주문 목록'을 사용합니다. 먼저 들어온 주문서가 먼저 처리되므로 이 주문 목록은 큐(queue)로 처리됩니다.
요리사는 1명이지만 시간을 적당히 배분하여 여러 가지 요리를 동시에 하는 시분할 방식은 효율적입니다. CPU가 1개인 컴퓨터에서 여러 개의 프로세스를 동시에 실행하는 것도 같은 원리입니다. CPU가 시간을 쪼개어 여러 프로세스에 적당히 배분함으로써 프로세스가 동시에 실행되는 것처럼 느껴지는 것입니다.
시분할 시스템에서 주문서는 중요한 역할을 합니다. 주문서에는 손님의 요구 사항, 현재 진행 상황 등이 나타나 있기 때문입니다.
시분할 방식을 이용한 요리는 여러 목록을 옮겨가면서 진행되는데 운영체제에서 프로세스의 처리도 이와 유사합니다. 현대의 운영체제는 시분할 방식을 기본으로 사용하기 때문에 프로세스가 여러 상태를 오가며 실행됩니다.
우선 운영체제는 프로그램을 메모리의 적당한 위치로 가져옵니다. 그와 동시에 주문서에 해당하는 작업 지시서를 만드는데, 이것이 프로세서 제어 블록(PCB, Process Control Block)입니다.
주문서가 없으면 요리가 진행되지 않듯이 프로세스 제어 블록이 없으면 프로그램이 프로세스로 전환되지 못합니다.
프로세스 제어 블록에 있는 다양한 정보 중 대표적인 세 가지는 다음과 같습니다.
프로세스 구분자 :
메모리에는 여러 개의 프로세스가 존재하므로 각 프로세스를 구분하는 구분자(ID, IDentification)가 필요합니다. 레스토랑의 주문서에 일련번호가 있듯이 프로세스를 구분하기 위해 프로세스 구분자(PID, Process IDentification)가 있습니다.
메모리 관련 정보 :
CPU는 실행하려는 프로세스가 메모리의 어디에 저장되어 있는지를 알아야 작업할 수 있습니다. 이를 위해 프로세스 제어 블록에는 프로세스의 메모리 위치 정보가 담겨 있습니다. 또한 메모리 보호를 위한 경계 레지스터와 한계 레지스터도 포함되어 있습니다.
각종 중간값 :
프로세스 제어 블록에는 다음에 작업해야 할 코드의 위치가 담긴 레지스터인 프로그램 카운터가 저장됩니다. 또한 작업의 중간값을 보관 중인 다른 레지스터도 같이 저장됩니다.
하나의 프로세스를 실행하려면 프로세스 구분자, 메모리 관련 정보, 프로그램 카운터와 각종 레지스터와 같은 중간값을 관리해야 합니다. 이러한 정보를 보관하는 데이터 구조가 프로세스 제어 블록입니다. 프로그램이 프로세스가 되려면 메모리에 올라오는 것과 동시에 프로세스 제어 블록이 생성되어야 합니다.
프로세스 제어 블록은 운영체제가 해당 프로세스를 실행하기 위해 관리하는 데이터 구조이므로 운영체제 영역에 만들어집니다. 프로세스가 종료되면 프로세스는 메모리에서 삭제되고 프로세스 제어 블록도 폐기 됩니다.
종합해보면, 프로그램이 프로세스가 된다는 것은 운영체제로부터 프로세스 제어 블록을 얻는다는 뜻이고, 프로세스가 종료된다는 것은 해당 프로세스 제어 블록이 폐기된다는 뜻입니다.
프로세스 = 프로그램 + 프로세스 제어 블록
프로그램 = 프로세스 - 프로세스 제어 블록
일괄 작업 시스템의 경우 프로세스가 생성된 후 CPU를 얻어 실행되고 작업을 마치면 종료됩니다. 따라서 일괄 작업 시스템의 프로세스 상태는 생성(create), 실행(run), 완료(terminate) 중 하나가 됩니다.
생성 상태(create status)
준비 상태(ready status)
실행 상태(running status)
완료 상태(terminate status)
디스패치 (dispatch) : 준비 상태의 프로세스 중 하나를 골라 실행 상태로 바꾸는 CPU 스케줄러의 작업
프로세스에 배당된 작업 시간을 타임 슬라이스 또는 타임 퀀텀이라고 하고, 프로세스는 자신에게 주어진 하나의 타임 슬라이스 동안 작업을 끝내지 못하면 다시 준비 상태로 돌아가는데 이를 타임아웃(time out)이라고 합니다.
프로세스는 생성, 준비, 실행, 완료라는 네 가지 상태만으로도 작업을 진행하는데 큰 문제가 없습니다. 그러나 오늘날 운영체제의 효율성을 고려하여 한 가지 상태를 더 만들었습니다.
대기 상태 (blocking status) : 입출력을 요구한 프로세스가 입출력이 완료될 때까지 기다리는 상태
작업의 효율성을 높이기 위해 입출력을 요청한 프로세스를 실행 상태에 두지 않고 대기 상태로 옮기는 것입니다. 입출력을 요청한 프로세스가 대기 상태로 옮겨지면 CPU 스케줄러는 준비 상태에 있는 프로세스 중 하나를 가져다 실행 상태로 만듭니다. 그러면 시스템 입장에서는 새로운 작업을 진행하게 되어 효율성이 높아지게 됩니다.
대기 상태에서 입출력이 끝난 프로세스는 실행 상태로 가지 않고 준비 상태로 돌아가 자기 차례를 기다리게 됩니다.
| 상태 | 설명 |
|---|---|
| 생성 상태 | 저장장치에 저장된 프로그램이 메모리로 올라와 실행되어 프로세스가 되는 상태로 커널 영역에 프로세스 제어 블록이 만들어집니다. 생성된 후에는 준비 상태로 이동합니다. |
| 준비 상태 | 실행을 기다리는 모든 프로세스가 준비 큐에서 자기 차례를 기다리는 상태입니다. 실행될 프로세스를 CPU 스케줄러가 선택합니다. |
| 실행 상태 | 선택된 프로세스가 타임 슬라이스를 얻어 CPU를 사용하는 상태입니다. 작업을 마치면 완료 상태로 가고 작업을 끝내지 못하면 준비 상태로 되돌아갑니다. |
| 대기 상태 | 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태입니다. 입출력이 완료되면 준비 상태로 이동합니다. |
| 완료 상태 | 프로세스가 종료된 상태입니다. 사용하던 모든 자원은 반납되고, 메모리에서 지워지며, 프로세스 제어 블록은 폐기 됩니다. |
대부분의 프로세스는 생성, 준비, 실행, 대기 완료 상태로 운영되며 이 다섯 가지 상태를 활성 상태(active status)라고 합니다. 프로세스의 상태는 활성 상태 외에 휴식 상태와 보류 상태가 있는데, 이 상태들은 조금 특별한 경우에 해당합니다.
휴식 상태 (pause status) : 프로세스가 작업을 일시적으로 쉬고 있는 상태
사용하던 데이터가 메모리에 그대로 있고 프로세스 제어 블록도 유지되므로 프로세스는 멈춘 지점에서부터 재시작(resume)할 수 있습니다.
보류 상태 (suspend status) : 프로세스가 메모리에서 잠시 쫓겨난 상태
이는 휴식 상태와 차이가 있습니다. 보류 상태는 "일시 정지 상태" 라고도 불립니다.
프로세스는 다음과 같은 경우에 보류 상태가 됩니다.
프로세스 제어 블록(PCB)은 프로세스를 실행하는 데 필요한 중요한 정보를 보관하는 자료구조로 TCB(Task Control Block)라고도 합니다.
| 포인터 | 프로세스 상태 |
|---|---|
| 프로세스 구분자 | - |
| 프로그램 카운터 | - |
| 프로세스 우선순위 | - |
| 각종 레지스터 정보 | - |
| 메모리 관리 정보 | - |
| 할당된 자원 정보 | - |
| 계정 정보 | - |
| PPID와 CPID | - |
| ... | - |
[프로세스 제어 블록의 구성]
프로세스 제어 블록의 첫 번째 블록에는 포인터가 저장됩니다.
문맥 교환 : CPU를 차지하던 프로세스가 나가고 새로운 프로세스를 받아들이는 작업
문맥 교환이 일어나는 상황은 매우 다양합니다. 일반적으로 한 프로세스가 자신에게 주어진 시간을 다 사용하면 발생하고, 인터럽트가 걸렸을 때도 발생합니다.
타임 슬라이스는 되도록 작게 설정하되 문맥 교환에 걸리는 시간을 고려하여 적당한 크기로 설정하는 것이 중요합니다. 참고로 유닉스 운영체제에서는 타임 슬라이스가 대략 100밀리초입니다. 대략이라고 표현하는 이유는 타임 슬라이스를 고정하지 않고 10 ~ 200 밀리초 사이에서 조정하기 때문입니다.
| 프로세스 | 요리 | 설명 |
|---|---|---|
| 코드 영역 | 요리책 | 스파게티를 만들려면 레시피가 적힌 요리책을 봐야 합니다. 여기서 요리책이 코드 영역에 해당하고, 요리책에 레시피가 나와 있듯이 코드 영역에 프로세스의 본문이 기술되어 있습니다. |
| 데이터 영역 | 재료 | 요리를 완성하려면 재료가 있어야 합니다. 요리 재료는 프로세스의 데이터 영역에 해당됩니다. |
| 스택 영역 | 조리 도구 | 조리하는 데 필요한 칼, 도마 냄비 등의 조리 도구는 스택 영역에 해당됩니다. 스택 영역은 코드를 작동하기 위해 운영체제가 부수적으로 관리하는 데이터 영역입니다. 어떤 조리 도구를 사용하여 요리하는지 손님이 알 수 없듯이 사용자에게는 스택의 내용이 보이지 않습니다. |
데이터 영역은 일반 데이터 영역과 힙(heap)영역으로 나뉩니다.
예를 들어 워드프로세서 프로그램을 실행하면 이 프로그램은 코드 영역에 탑재되고, 워드프로세서로 편집 중인 문서는 데이터 영역에 탑재됩니다. 또한 운영체제가 워드프로세서를 작동하기 위해 사용하는 각종 부가 데이터는 스택 영역에서 관리합니다.
코드 영역(code area) : 프로그램의 본문이 기술된 곳, 텍스트 영역(text area) 라고도 함
코드 영역에 탑재된 코드는 읽기 전용으로 처리됩니다.
데이터 영역(data area) : 코드가 실행되면서 사용하는 변수나 파일 등의 각종 데이터를 모아놓은 곳
데이터는 변하는 값이기 때문에 이곳의 내용은 기본적으로 읽기와 쓰기가 가능합니다.
스택 영역(stack area) : 운영체제가 프로세스를 실행하기 위해 부수적으로 필요한 데이터를 모아놓은 곳
스택 영역은 운영체제가 사용자의 프로세스를 작동하기 위해 유지하는 영역이므로 사용자에게는 보이지 않습니다.
프로세스는 프로그램을 실행할 때 새로 생성됩니다. 사용자가 프로그램을 실행하면 운영체제는 프로그램을 메모리로 가져와 코드 영역에 넣고 프로세스 제어 블록을 생성합니다. 그리고 메모리 데이터 영역과 스택 영역을 확보한 후 프로세스를 실행합니다.
프로세스 생성 방법에는 프로세스를 새로 생성하는 방법도 있지만, 실행 중인 프로세스에서 새로운 프로세스를 복사하는 방법도 있습니다.
fork() 세스템 호출은 실행 중인 프로세스로부터 새로운 프로세스를 복사하는 함수로 커널에서 제공합니다.
컴퓨터를 사용하다 보면 fork() 시스템 호출을 자주 접하게 됩니다. 예를 들어, 구글의 웹 브라우저인 크롬에서 어떤 페이지를 보다가 Ctrl + N 키를 누르면 크롬이 하나 더 실행됩니다. 이는 새로운 크롬을 실행한 것이 아니라 현재의 크롬 프로세스를 복사한 것입니다. 프로세스를 복사할 때 기존 프로세스는 부모 프로세스가 되고 새로 생긴 프로세스는 자식 프로세스가 되어, 두 프로세스는 부모-자식 관계로 연결됩니다.
fork() 시스템 호출 : 실행 중인 프로세스를 복사하는 함수
fork() 시스템 호출을 하면 프로세스 제어 블록을 포함한 부모 프로세스 영역의 대부분이 자식 프로세스에 복사되어 똑같은 프로세스가 만들어집니다. 단, 프로세스 제어 블록의 내용 중 일부가 변경되는데 변경되는 부분은 다음과 같습니다.
운영체제는 주문하는 요리가 달라도 간단하게 처리하는 기능을 제공합니다. fork() 시스템 호출로 요리를 복사한 후, 복사된 요리를 새로운 요리로 바꾸는 함수인 exec() 시스템 호출을 사용하면 됩니다.
exec() 시스템 호출 : 기존 프로세스를 새로운 프로세스로 전환하는 함수
exec() 시스템 호출을 사용하는 목적은 프로세스의 구조를 재활용하기 위함입니다.
exec() 시스템 호출을 하면 코드 영역에 있는 기존 내용이 지워지고 새로운 코드로 바뀝니다. 또한 데이터 영역이 새로운 변수로 채워지고 스택 영역이 리셋 됩니다. 다시말해, exec() 시스템 호출은 각종 프로세스 구분자(PID, PPID, CPID)만 남겨두고 프로세스의 나머지 내용을 새로운 것으로 바꿉니다. 또한 데이터 영역이 새로운 데이터로 채워지고 스택 영역이 리셋 됩니다.
유닉스에서 커널이 처음 메모리에 올라와 부팅되면 커널 관련 프로세스를 여러 개 만드는데, 그중 init 프로세스는 전체 프로세스의 출발점이 됩니다. 운영체제는 프로세스를 효율적으로 관리하기 위해 init 프로세스를 만든 다음 나머지 프로세스를 init 프로세스의 자식으로 만듭니다. 운영체제에 있는 모든 프로세스는 init 프로세스의 자식이 되어 트리 구조를 이룹니다.
프로세스의 계층 구조는 동시에 여러 작업을 처리하고 종료된 프로세스의 자원을 회수하는데 유용합니다.
login 프로세스는 인증을 거쳐 컴퓨터에 접속하는 과정을 처리합니다. 예를 들어 사용자 3명이 동시에 컴퓨터에 접속한다면 동시에 3명을 처리해야 하는데, login 프로세스는 한 번에 1명만 처리할 수 있습니다. 유닉스 운영체제는 여러 사용자들을 동시에 처리하기 위해 fork() 시스템 호출로 login 프로세스를 여러 개 만들어 사용자에게 나누어 줍니다. 새로운 사용자가 들어올 때마다 이러한 작업을 반복하면 여러 사용자를 동시에 처리할 수 있습니다.
login 프로세스를 통과하고 나면 shell 프로세스가 필요합니다. shell 프로세스가 있어야 사용자가 운영체제에 명령을 내리고 결과를 받을 수 있습니다.
exec() 시스템 호출을 사용하여 login 프로세스의 구조를 shell 프로세스로 다시 활용하면 자원을 효율적으로 관리할 수 있습니다.
프로세스를 계층 구조로 만들면 프로세스 간의 책임 관계가 분명해져서 시스템을 관리하기가 수월합니다. 모든 프로세스를 부모-자식 관계로 만들면 자식 프로세스가 작업을 마쳤을 때 사용하던 자원을 부모 프로세스가 회수하면 됩니다.
부모 프로세스는 자원을 회수하기 위해 자식 프로세스가 끝날 때까지 기다려야 합니다. 그런데 부모 프로세스가 먼저 종료되거나 자식 프로세스가 비정상적으로 종료되어 부모 프로세스에 연락이 안되는 경우도 있습니다. 이런 문제가 발생하면 자식 프로세스가 종료되지 않거나, 종료 되었는데도 사용하던 자원이 그대로 남게 됩니다.
고아 프로세스 (orphan process) : 부모 프로세스가 먼저 종료되어 돌아갈 곳이 없는 프로세스
좀비 프로세스 (zombie process) : 자식 프로세스가 종료되었는데도 부모 프로세스가 뒤처리를 하지 않을 때 발생합니다.
고아 프로세스나 좀비 프로세스가 많아지면 자원이 낭비되기 때문에 효율적인 운영에 방해가 됩니다. 따라서 운영체제는 주기적으로 반환되지 못한 자원을 회수해야 합니다.