1. 프로세스(Process)
프로세스(Process)란 현재 실행되고 있는 프로그램의 인스턴스를 말합니다. 프로그램은 데이터 상태로 보조기억장치에 저장되어 있으며, 프로그램이 실행되면 메모리에 적재되어 프로세스가 됩니다. 윈도우에서 작업 관리자를 열면 현재 실행중인 프로세스를 확인할 수 있습니다.
프로세스는 포그라운드 프로세스(Foreground Process)와 백그라운드 프로세스(Background Process)로 나눌 수 있습니다.
포그라운드 프로세스(Foreground Process)는 사용자와 상호작용하면서 실행되는 프로세스입니다. 이 프로세스는 주로 사용자 인터페이스를 통해 입력을 받거나 출력을 제공하며, 사용자의 명령을 처리하거나 작업을 수행합니다. 포그라운드 프로세스는 사용자가 직접 볼 수 있고 제어할 수 있으며, 사용자와의 상호작용을 위해 우선순위를 가지고 실행됩니다.
백그라운드 프로세스(Background Process)는 사용자와 상호작용하지 않고 실행되는 프로세스입니다. 이러한 프로세스는 주로 시스템 작업이나 보조 작업을 처리하는 데 사용됩니다. 백그라운드 프로세스는 사용자의 입력이나 제어를 기다리지 않고 실행되므로 보다 높은 우선순위를 가질 수 있습니다. 일반적으로 백그라운드 프로세스는 시간이 오래 걸리는 작업이나 자동화된 작업을 처리하는 데 사용됩니다.
예를 들어, 웹 브라우저를 실행한다고 가정해 봅시다. 웹 브라우저 자체는 포그라운드 프로세스로 실행되며, 사용자가 웹 페이지를 찾아보거나 입력을 하면 이를 처리합니다. 그러나 웹 브라우저에서 파일을 다운로드하는 경우, 다운로드 작업은 백그라운드 프로세스로 실행됩니다. 사용자는 다운로드 작업이 백그라운드에서 진행되는 동안에도 브라우저를 계속 사용할 수 있습니다.
백그라운드 프로세스는 다운로드와 같이 사용자와 상호작용이 가능한 백그라운드 프로세스와 사용자와 상호작용하지 않고 정해진 작업만 수행하는 백그라운드 프로세스로 나눌 수 있습니다. 후자의 경우를 데몬(Daemon) 또는 서비스(Service) 라고 합니다.
윈도우 서비스
2. 프로세스 제어 블록(Process Control Block, PCB)
프로세스 제어 블록(Process Control Block, PCB)은 운영체제에서 프로세스를 관리하기 위해 사용되는 데이터 구조입니다.
CPU는 한정된 자원을 가지기 때문에 프로세스는 돌아가면서 한정된 시간만큼 CPU를 사용합니다. 자신의 차례에 정해진 시간만큼 CPU를 사용하고 타이머 인터럽트가 발생하면 다음 프로세스에게 차례를 양보합니다. 운영 체제는 빠르게 번갈아 수행되는 프로세스를 관리할 필요가 있는데, 이를 위해 PCB를 사용합니다.
운영체제는 각 프로세스에 대해 PCB를 커널 영역에 생성하고 프로세스의 상태 및 관련 정보를 추적합니다. 이러한 정보는 운영 체제가 프로세스를 스케줄링하고 제어하는 데 필요한 기반이 됩니다.
PCB는 프로세스의 상태, 우선 순위, 레지스터 값, 메모리 할당 정보, 입출력 상태 등 다양한 정보를 포함합니다.
1) 프로세스 식별자(Process ID, PID): 각 프로세스를 고유하게 식별하는 번호 또는 이름입니다. PID는 운영 체제에서 프로세스를 식별하는 데 사용됩니다.
2) 프로세스 상태(Process State): 프로세스의 현재 상태를 나타냅니다. 프로세스 상태는 실행(Running), 대기(Waiting), 준비(Ready) 등이 있습니다. 운영 체제는 이 정보를 기반으로 프로세스를 스케줄링하고 상태 전이를 관리합니다.
3) 프로그램 카운터(Program Counter, PC): 현재 실행 중인 명령어의 주소를 가리키는 레지스터 값입니다. 프로세스는 다음에 실행할 명령어를 PC의 값에 따라 결정합니다.
4) 레지스터(Register) 값: 프로세스가 현재 사용 중인 레지스터의 값을 저장합니다. 이는 프로세스가 실행되는 동안 레지스터 값이 유지되도록 합니다.
5) 스케줄링 정보(Scheduling Information): 프로세스의 우선 순위, CPU 점유 시간, 대기 시간 등 스케줄링과 관련된 정보를 포함합니다. 운영 체제는 이 정보를 기반으로 프로세스 스케줄링 알고리즘을 실행하여 어떤 프로세스를 실행시킬지 결정합니다.
6) 메모리 관리 정보(Memory Management Information): 프로세스가 사용하는 메모리 주소 범위, 페이지 테이블 정보 등과 같은 메모리 관리에 필요한 정보를 포함합니다.
7) 입출력 상태(I/O Status): 프로세스가 사용 중인 입출력 장치와 관련된 정보를 저장합니다. 이 정보는 프로세스가 입출력 작업을 수행하는 데 필요합니다.
3. 프로세스 상태
프로세스는 생성(New), 준비(Ready), 실행(Run), 대기(Wait), 종료(Exit) 5가지 상태를 가집니다. 프로세스 상태는 운영 체제가 프로세스의 상태 전이를 관리하고 스케줄링을 수행하기 위해 사용됩니다. 스케줄러는 준비 상태에 있는 프로세스를 실행 상태로 전환하고, 대기 상태에 있는 프로세스를 준비 상태로 전환하여 CPU 할당을 조정합니다. 이를 통해 CPU 자원을 효율적으로 활용하고 다중 프로세스가 동시에 실행될 수 있게 됩니다.
생성 상태(create status)는 프로세스가 생성되었지만 아직 실행되기 전인 상태입니다. 이 단계에서는 프로세스가 초기화되고 필요한 자원이 할당됩니다. PCB는 이때 생성됩니다.
준비 상태(ready status)는 프로세스가 실행을 기다리는 상태입니다. 필요한 자원을 모두 할당받았으며, CPU를 할당받기를 기다리는 상태입니다. 스케줄러에 의해 다음에 실행될 프로세스로 선택될 수 있습니다.
실행 상태(runnig status)는 준비 상태에 있는 프로세스가 CPU를 할당받아 실행되고 있는 상태입니다. 준비 상태에 있는 프로세스가 실행 상태가 되는 것을 디스패치(Dispatch)라고 합니다. 이때 프로세스는 CPU를 사용하여 명령어를 실행하고 작업을 수행합니다. 실행 상태에서는 시간이 지남에 따라 프로세스는 완료되거나 대기 상태로 전환될 수 있습니다. 실행 상태의 프로세스가 대기 상태가 되는 것을 Block 이라고 합니다.
대기 상태(blocking status)는 프로세스가 어떤 이벤트를 기다리는 상태입니다. 이벤트는 입출력 완료, 사용자 입력, 시간 지연 등 다양한 상황일 수 있습니다. 대기 상태에 있는 프로세스는 CPU를 사용할 수 없으며, 해당 이벤트가 발생할 때까지 기다리게 됩니다. CPU에 비해 느린 입출력 작업의 경우 대기상태로 있다가 입출력 완료 인터럽트 신호를 받으면 준비 상태로 전환됩니다. 대기 상태의 프로세스가 준비 상태가 되는 것을 Wake up 이라고 합니다.
종료 상태(terminate status)는 프로세스가 완료되거나 종료된 상태입니다. 프로세스의 실행이 끝났거나, 강제로 종료되었을 때 이 상태로 전환됩니다. 종료된 프로세스는 시스템에서 제거됩니다.
4. 문맥 교환(Context Switch)
문맥 교환(Context Switching)은 운영 체제에서 한 프로세스의 실행 상태를 저장하고 다른 프로세스의 실행 상태로 전환하는 과정을 말합니다.
문맥(Context)이란 프로그램 카운터 등 프로세스가 다음 차례가 왔을 때 실행을 재개하기 위한 PCB 정보를 말합니다. 문맥을 백업해두면 언제든지 프로세스의 실행을 재개할 수 있습니다.
문맥 교환은 멀티태스킹 환경에서 여러 프로세스가 동시에 실행되는 상황에서 CPU의 할당을 조정하기 위해 필요합니다.
5. 프로세스의 구조
메모리는 커널 영역과 사용자 영역으로 나눌 수 있습니다. 사용자 영역에서 프로세스는 다시 코드(Code) 영역, 데이터(Data) 영역, 힙(Heap) 영역, 스택(Stack) 영역 등으로 나눌 수 있습니다.
커널 영역에는 위에서 설명했듯이 PCB가 저장됩니다.
코드 영역에는 실행하는 프로그램의 코드가 기계어로 저장되며, 텍스트(Text) 영역이라고도 합니다. 데이터가 아닌 CPU가 실행할 명령어가 저장되어 있어서 쓰기가 금지된 read-only 영역입니다. CPU는 코드 영역에 저장된 명령어를 하나씩 처리하게 됩니다.
데이터 영역에는 프로그램이 실행되는 동안 유지할 데이터가 저장됩니다. 일반적으로 전역 변수(global variable)나 정적 변수(static variable)가 저장되며, 프로그램이 실행될 때 할당되고 프로그램이 종료되면 소멸합니다.
힙 영역은 사용자가 직접 관리할 수 있는 메모리 영역으로, 런 타임에 할당받을 메모리 크기가 결정됩니다. 예를 들어 C언어에서 동적 크기의 배열을 선언하거나 자바에서 new 명령어를 통해 인스턴스를 생성하면 동적으로 힙 영역을 할당받아 데이터가 저장됩니다.
힙 영역은 사용자가 직접 메모리를 반환해야 하며, 메모리를 반환하지 않으면 지속적으로 메모리 공간을 차지하는 메모리 누수(Memory Leak) 현상이 발생합니다. C언어에서는 free 함수를 통해 메모리를 반환하며, 자바의 경우 가비지 콜렉션(Garbage Collection)에 의해 자동으로 메모리가 반환됩니다.
스택 영역은 함수 호출 시 생성되는 지역 변수(local variable)나 매개 변수(parameter) 등의 데이터가 일시적으로 저장되는 영역으로, 컴파일 타임에 할당받을 메모리 크기가 결정됩니다. 함수를 호출하면 동적으로 스택 영역을 할당받아 데이터가 저장되고, 함수의 호출이 완료되면 할당받은 메모리를 반환합니다.
스택 영역은 푸시(push) 동작으로 데이터를 저장하고 팝(pop) 동작으로 데이터를 인출합니다. 데이터 인출시 후입선출(Last-In First-Out, LIFO) 방식에 따라 동작하므로, 가장 늦게 저장된 데이터가 가장 먼저 인출됩니다.
---------
코드 영역과 데이터 영역은 프로그램 시작시 고정적으로 메모리를 할당받기 때문에 정적 할당 영역 이라고 하며, 힙 영역과 스택 영역은 메모리를 동적으로 할당받기 때문에 동적 할당 영역 이라고 합니다.
동적 할당 영역에서 힙 영역은 낮은 주소에서 높은 주소 방향으로 메모리가 할당되고, 스택 영역은 높은 주소에서 낮은 주소 방향으로 메모리가 할당됩니다.
만약 두 영역이 겹쳐 할당받을 메모리가 없게 된다면 서로의 영역을 침범할 수 있습니다. 이때 스택이 힙 영역을 침범하는 경우를 스택 오버플로우(Stack Overflow), 힙이 스택 영역을 침범하는 경우를 힙 오버플로우(Heap Overflow)라고 합니다.
6. 프로세스의 생성
프로세스는 프로그램을 실행할 때 생성됩니다. 이때 운영체제가 프로그램 코드를 메모리 코드영역에 할당하고 PCB를 생성한 후 메모리에 데이터 영역과 스택 영역을 확보하여 프로세스를 실행합니다.
유닉스 계열의 운영체제의 경우 새로운 프로세스를 생성할 때 위 과정을 모두 거치지 않고 fork 시스템 호출을 통해 기존 프로세스 복사하여 새로운 프로세스를 생성합니다. 이때 fork 시스템 호출을 하는 프로세스는 부모 프로세스, 새로 만들어진 프로세스는 자식 프로세스가 되어 계층 구조를 형성합니다.
fork를 호출하면 PCB를 포함한 부모 프로세스 대부분이 자식 프로세스에 복사되어 동일한 프로세스가 생성됩니다. 다만 PID, PPID, CPID 등 일부 데이터가 수정됩니다. PPID는 부모 프로세스의 PID, CPID는 자식 프로세스의 PID 입니다.
fork 예제 코드
#include <stdio.h> #include <stdlib.h> #include <unistd.h>
void main() {
pid_t pid;
printf("first_PID: %ld\n", (long)getpid()); printf("first_PPID: %ld\n", (long)getppid()); printf("---------------------\n");
pid = fork();
while(1) {
if(pid < 0) { printf("Error"); exit(-1); } else if(pid == 0) { printf("Child Process!\n"); } else { printf("Parent Process!\n"); }
printf("PID: %ld\n", (long)getpid()); printf("PPID: %ld\n", (long)getppid()); printf("saved PID: %ld\n", (long)pid); printf("---------------------\n");
sleep(1); } } |
실행 결과
실행 결과를 살펴보면 first_PID와 first_PPID가 부모 프로세스의 PID, PPID와 동일하다는 것을 볼 수 있습니다. 부모 프로세스와 자식 프로세스가 동기화되어 실행되는 것 처럼 보이지만 실제로는 독립적으로 실행되고 있습니다.
fork 시스템 호출을 요청하면 부모 프로세스에는 자식 프로세스의 PID를 리턴하고, 자식 프로세스에는 0을 리턴합니다. 또한 자식 프로세스는 부모 프로세스의 PCB를 상속받았기 때문에 프로그램 카운터가 동일하여 fork 시스템 호출을 요청한 이후의 코드가 실행됩니다. 그렇기 때문에 자식 프로세스에서는 firt_PID와 first_PPID를 출력하지 않습니다.
----------
자식 프로세스에서 새로운 프로그램을 실행하려면 exec 시스템 호출을 요청하면 됩니다. exec 시스템 호출을 요청하면 자식 프로세스의 코드 영역을 새로운 코드로 교체하고 PCB에서 프로그램 카운터나 각종 레지스터, 파일 정보 등이 리셋됩니다.
exec 예제 코드: parent
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h>
void main() {
pid_t cpid;
printf("parent process!!\n");
cpid = fork();
if(cpid == 0) { exec("./child", "./child", NULL); printf("child process failed to exec\n"); exit(0); }
wait(NULL); exit(0); } |
exec 예제 코드: child
#include <stdio.h> #include <stdlib.h>
void main() { printf("child process!!\n"); exit(0); } |
실행 결과
부모 프로세스가 먼저 종료되면 자식 프로세스도 종료되기 때문에 parent 코드에서 wait 함수를 통해 자식 프로세스와 동기화 시킵니다.
parent를 실행하면 fork 시스템 호출에 의해 자식 프로세스가 생성되고, exec 시스템 호출에 의해 자식 프로세스는 child를 실행합니다. 코드 영역이 child 코드로 변경되었기 때문에 “child process failed to exec” 라는 메시지는 출력되지 않습니다.
fork와 exec를 통해 프로세스를 생성하는 방식은 프로세스 생성 과정 전체를 거치지 않기 때문에 프로세스 생성 속도가 빠르고 추가 작업 없이 자원의 상속이 가능하여 시스템 관리에 효율적입니다. |