프로세스 (Process)
프로세스는 운영체제에서 하나의 작업 단위이며, 실행 중인 프로그램을 의미한다. 프로그램은 저장장치에 저장된 정적인 상태이고, 프로세스는 실행을 위하여 메모리에 올라온 동적인 상태를 말한다. 프로그램이 프로세스로 전환되기 위해서는 프로세스 제어 블록(PCB)이 필요하다. 프로그램이 프로세스가 되려면 운영체제로부터 프로세스 제어 블록을 덩어야 하고, 프로세스가 종료되면 해당 프로세스 제어 블록은 폐기된다.
프로세스 구조
프로세스는 코드 영역, 데이터 영역, 스택 영역으로 구성되어 있다.
- 코드 영역(Code Area): 프로그램의 본문이 기술된 곳으로, 개발자가 작성한 프로그램은 코드 영역으로 탑재되고 읽기 전용으로 처리된다.
- 데이터 영역(Data Area): 코드를 실행하며 사용하는 변수, 파일 등의 각종 데이터가 모여있는 곳으로, 읽기와 쓰기가 가능하다.
- 스택 영역(Stack Area): 운영체제가 프로세스를 실행하기 위해 부수적으로 필요한 데이터를 모아놓은 곳으로, 운영체제가 사용자의 프로세스를 작동하기 위해 유지하는 영역이기에 사용자에게는 보이지 않는다.
프로세스의 다섯 가지 상태
운영체제에서 프로세스는 여러 가지 이유로 상태가 변화하게 되는데, 이때 프로세스는 생성, 준비, 실행, 대기, 완료 상태를 거치게 된다.
- 생성 상태(Create Status): 프로그램이 메모리에 올라오고 운영체제로부터 프로세스 제어 블록(PCB)을 할당받은 상태로, 준비 상태에서 자기의 차례를 기다린다.
- 준비 상태(Ready Status): 실행 대기 중인 모든 프로세스가 자기 순서를 기다리는 상태로, 프로세스 제어 블록(PCB)은 준비 큐에서 기다리며 CPU 스케쥴러에 의해 관리된다. (CPU 스케줄러가 실행될 프로세스를 결정한다.)
- 실행 상태(Running Status): 프로세스가 CPU를 할당받아 살행하는 상태로, 실행 상태에 있는 프로세스는 자신에게 주어진 시간인 "타임 슬라이스" 동안에만 작업할 수 있다. 이 시간을 다 사용했을 때 작업을 다 끝내지 못하면 프로세스 제어 블록(PCB)은 실행 상태에서 준비 상태로 옮겨지고, 작업을 다 끝냈다면 완료 상태로 옮겨진다.
- 대기 상태(Blocking Status): 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태로, 이 상태의 프로세스는 입출력장치 별로 마련된 큐에서 기다린다. 입출력이 완료되면 준비 상태로 이동된다.
- 완료 상태(Terminate Status): 프로세스가 종료되는 상태로, 코드와 사용한 데이터를 메모리에서 삭제하고 프로세스 제어 블록(PCB)을 폐기한다.
휴식 상태와 보류 상태
프로세스는 위의 다섯 가지 상태로 운영되며, 이 다섯 가지 상태를 "활성 상태(Active Status)"라고 한다. 이 외에도 휴식 상태와 보류 상태가 있는데, 이에 대해서 알아보도록 하겠다.
- 휴식 상태(Pause Status): 프로세스가 작업을 일시적으로 쉬고 있는 상태로, 프로그램이 종료된 것 같이 보이나 실은 실행을 잠시 멈춘 것이다. 사용하던 데이터가 메모리에 그대로 있고 프로세스 제어 블록(PCB)도 유지되기에 프로세스는 멈춘 지점에서 재시작할 수 있다.
- 보류 상태(Suspend Status): 프로세스가 메모리에서 잠시 쫓겨난 상태로, 특정한 경우에 보류 상태가 된다. 보류 상태로 들어간 프로세스는 메모리 밖으로 쫓겨나 스왑 영역에 보관된다.
(메모리가 꽉 차거나, 프로그램에 오류가 있거나, 악의적인 공격을 하는 프로세스로 판단하거나, 매우 긴 주기로 반복되는 프로세스거나, 입출력이 지연되는 상황 등)
프로세스 제어 블록 (PCB: Process Control Block)
프로세스 제어 블록은 프로세스를 실행하는데 필요한 주요 정보를 보관하는 자료구조로, 프로세스가 생성될 때 만들어지고 프로세스 실행이 완료되면 폐기된다. 프로세스 제어 블록은 다음의 것들로 구성된다.
- 포인터(Pointer): 프로세스 제어 블록의 첫 번째 블록에 저장되며, 프로세스 제어 블록을 연결하여 준비 상태 또는 대기 상태의 큐를 구현할 때 포인터를 사용한다.
- 프로세스 상태(Process Status): 프로세스 제어 블록의 두 번째 블록에 저장되며, 프로세스가 현재 어떤 상태에 있는지를 나타낸다.
- 프로세스 구분자(Process Identification): 운영체제 내의 여러 프로세스를 구별하기 위한 구분자가 저장된다.
- 프로그램 카운터(Process Counter): 다음에 실행된 명령어의 위치를 가리키는 프로그램 카운터의 값을 저장한다.
- 프로세스 우선순위(Process Priority): 프로세스의 중요도는 각각 다르기에, CPU 스케쥴러는 준비 상태의 프로세스 중 실행 상태로 옮길 프로세스를 선택할 때는 프로세스 우선순위를 기준으로 선택한다.
- 각종 레지스터 정보(Register Information): 프로세스가 실행될 때 사용하던 레지스터의 값이 저장된다. 그래야 다음에 실행할 수 있기에 자신이 사용하던 레지스터의 중간값을 보관한다.
- 메모리 관리 정보(Memory Management Information): 메모리의 위치 정보, 경계 레지스터 값, 한계 레지스터 값, 세그먼테이션 테이블, 페이지 테이블 등의 정보들이 보관된다.
- 할당된 자원 정보(Allocated Resource Information): 프로세스를 실행하기 위해 사용하는 입출력 자원, 오픈 파일 등에 대한 정보가 저장된다.
- 계정 정보(Account Information): 계정 번호, CPU 할당 시간, CPU 사용 시간 등에 대한 정보가 저장된다.
- 부모 프로세스 구분자, 자식 프로세스 구분자(PPID, CPID): 부모 프로세스를 가리키는 PPID(Parent PID), 자식 프로세스를 가리키는 CPID(Children PID) 정보가 저장된다.
문맥 교환 (Context Switching)
문맥 교환이란 CPU를 차지하던 프로세스가 나가고 새로운 프로세스를 받아들이는 작업을 말한다. 이떄 두 프로세스 제어 블록의 내용이 변환된다. 프로세스 간의 문맥 교환 과정은 다음과 같다.
- 프로세스 P1이 자신에게 주어진 시간을 다 사용하여 타임아웃이 되면 P1의 프로세스 제어 블록은 현재까지의 작업 결과를 저장하고 P1은 준비 상태로 옮겨진다.
- 준비 상태에 있던 P2가 실행 상태로 가면 CPU의 레지스터는 P2의 프로세스 제어 블록의 값으로 채워져 다음 작업을 실행한다.
프로세스가 작업을 수행하는 시간인 타임 슬라이스가 너무 크면 작업이 끊겨 보일 것이고, 반면 타임 슬라이스가 너무 짧으면 사용자는 여러 프로그램이 동시에 실행되는 것처럼 느끼겠지만 문맥 교환에 시간이 너무 소비되어 시스템의 성능이 떨어질 것이다. 따라서 타임 슬라이스는 되도록 짧게 설정하되 문맥 교환 시간을 고려하여 적당한 크기로 설정하는 것이 중요하다.
프로세스의 생성과 변환
프로세스는 프로그램을 실행할 때 새롭게 생성된다. 사용자가 프로그램을 실행하면 운영체제는 프로그램을 메모리로 가져온 후 코드 영역에 넣고 프로세스 제어 블록을 생성한다. 그리고 메모리에 데이터 영역과 스택 영역을 확보하고 프로세스를 실행한다. 프로세스를 생성하는 방법으로는 이와 같이 새롭게 프로세스를 생성하는 방법이 있고, 또 실행 중인 프로세스에서 새로운 프로세스를 복사하는 방법도 있다.
fork() 시스템 호출
fork() 시스템 호출은 커널에서 제공하는 실행 중인 프로세스로부터 새로운 프로세스를 복사하는 함수로, 사용하면 실행 중인 프로세스와 동일한 프로세스가 하나 생성된다. 이때 실행하던 프로세스는 부모 프로세스, 새로 생긴 프로세스는 자식 프로세스로 둘은 부모-자식 관계가 된다. fork() 시스템 호출을 하면 동일한 프로세스가 만들어지는데, 이때 프로세스 제어 블록 중 일부의 내용은 변경된다. (프로세스 구분자, 메모리 관련 정보, 부모 프로세스 구분자와 자식 프로세스 구분자)
fork() 시스템 호출을 사용하면 다음의 장점이 있다.
- 하드디스크에서 프로그램을 가져오지 않고 기존 메모리에서 복사하는 방식이기에 프로세스의 생성 속도가 빠르다.
- 부모 프로세스가 사용하던 모든 자원을 추가 작업 없이 자식 프로세스에 상속할 수 있다.
- 부모 프로세스와 자식 프로세스가 PPID와 CPID로 연결되어 있기에 시스템 관리를 효율적으로 할 수 있다.
exec() 시스템 호출
exec() 시스템 호출은 기존 프로세스를 새로운 프로세스로 전환하는 함수이다. exec() 시스템 호출을 통하여 이미 만들어진 프로세스의 구조를 그대로 사용할 수 있기에 프로세스 구조를 재활용할 수 있게 된다.
exec() 시스템 호출을 하면 코드 영역에 있는 기존 내용은 모두 지워지고 새로운 코드로 변경되고 데이터 영역에는 새로운 변수로 채워지고, 스택 영역은 리셋된다. 프로세스 제어 블록의 내용 중에서 프로세스 구분자, 부모 프로세스 구분자, 자식 프로세스 구분자, 메모리 관련 사항 등은 변경되지 않지만 각종 레지스터와 사용한 파일 정보는 모두 리셋된다.
이와 같이 fork(), exec() 시스템 호출을 하면 프로세스를 복사하거나 새로운 프로세스로 전환할 수 있다.
프로세스 계층 구조
유닉스 운영체제에서 커널이 처음 메모리에 올라와 부팅되면 커널 관련 프로세스를 여러 개 생성한다. 운영체제는 프로세스를 효율적으로 관리하기 위해 init 프로세스르 만들고 나머지 프로세스를 init 프로세스의 자식으로 만들어 트리 구조를 이룬다. init 프로세스는 일반 사용자 프로세스의 최상단에 위치하며 fork()와 exec() 시스템 호출을 이용해 자식 프로세스를 만들 수 있다. 이와 같이 프로세스를 계층 구조로 나누면 동시에 여러 작업을 처리하고 종료된 프로세스의 자원을 회수함에 있어 매우 유용하다.
다만, 부모 프로세스는 자원을 회수하기 위해 자식 프로세스가 끝날 때까지 기다려야 하는데 만약 부모 프로세스가 자식 프로세스보다 먼저 종료되거나, 자식 프로세스가 비정상적으로 종료되어 부모 프로세스에 연락이 안 되면 문제가 생기게 된다.
- 고아 프로세스(Orphan Process): 자식 프로세스가 종료되기 전, 부모 프로세스가 먼저 종료되는 경우
- 좀비 프로세스(Zombie Process): 자식 프로세스가 종료되었음에도 부모 프로세스가 뒤처리를 하지 못하는 경우
이와 같이 고아 프로세스나 좀비 프로세스가 많아지면 자원이 낭비되기에 효율성에 문제가 생긴다. 따라서 운영체제는 주기적으로 반환되지 못한 자원을 회수해야 한다.