프로세스
프로세스는 실행 중인 프로그램이다. 실행파일이 메모리에 올라가서 실행되면 프로세스가 되는 것이다.
프로세스의 개념
- 주기억 장치에 상주된 프로그램이 CPU에 의해서 처리되는 상태
- CPU에 의해서 현재 실행되고 있는 프로그램
- 실행을 위해 메모리에 올라온 동적인 상태
- PCB(Process Control Block, 프로세스 제어 블록)의 존재로서 명시되는 것
- 프로세서가 할당되는 개체로서 디스패치(Dispatch)가 가능한 단위
다중 프로그램 - 여러 프로세스들이 메모리에 동시에 있을 수 있다. (동시 실행, 프로세스들은 상호 독립적인 메모리 공간에서 실행)
다중 인스턴스 - 하나의 프로그램은 여러 프로세스가 될 수 있다. (같은 프로그램이어도, 실행될 때마다 독립된 프로세스 생성, 각 프로세스는 독립된 메모리 공간을 가지고, 별개의 프로세스들로 취급)
Loading - 실행 파일이 메모리에 올라가는 과정
로더(Loader): 메모리에 Load(적재)해주는 것
- 절대 로더(Absolute loader) - 항상 고정된 위치에만 로딩됨(재배치 및 링킹이 없음), 다중 프로그래밍 X
- 재배치 로더(Relocation loader) - 주기억 장치의 상태에 따라 목적 프로그램을 주기억 장치 임의공간에 적재 (프로그램이 여러 개 실행되다보면, 메모리 위치상에 충돌이 있을 수 있다.)
- 동적 적재(Dynamic loading) - 필요한 부분만 주기억장치에 적재하고 나머지는 보조기억장치에 저장

프로세스 생명 주기

| 상태 | 설명 | 작업 |
|---|
| 생성 상태 | 프로그램을 메모리에 가져와 실행 준비가 완료된 상태이다. | 메모리 할당, 프로세스 제어 블록(PCB) 생성 |
| 준비 상태 | 실행을 기다리는 모든 프로세스가 자기 차례를 기다리는 상태이다. CPU 스케줄러가 실행할 프로세스를 선택한다. | dispatch(PID): 준비 → 실행 |
| 실행 상태 | 선택된 프로세스가 타임 슬라이스를 얻어 CPU를 사용하는 상태이다. 프로세스 사이의 문맥 교환이 일어난다. | timeout(PID): 실행 → 준비 exit(PID): 실행 → 완료 block(PID): 실행 → 대기 |
| 대기 상태 | 실행 상태에 있는 프로세스가 입출력을 요청하면 입출력이 완료될 때까지 기다리는 상태이다. 입출력이 완료되면 준비 상태로 간다. | wakeup(PID): 대기 → 준비 |
| 완료 상태 | 프로세스가 종료된 상태이다. 사용하던 모든 데이터가 정리된다. 정상 종료(exit)와 비정상 종료(abort)를 포함한다. | 메모리 삭제, 프로세스 제어 블록(PCB) 삭제 |
보류상태
프로세스가 '어떠한 이유로 인해' 실행이 미뤄지고, 메모리에서 쫓겨난 상태
(메모리에서 쫓겨남 == 저장장치(디스크)에 놓여짐)
-> '어떠한 이유'가 되는 상황
- 메모리가 꽉참
- 프로그램 오류 존재
- Malware라서 격리
- 매우 긴 주기로 실행
- 입출력 지연
- 예: 업데이트 소프트웨어

프로세스 관리
프로세스는 생성에서부터 종료까지, 모두 커널(운영체제)에 의해 관리된다.
PCB(Process Control Block)
: 운영체제가 프로세스를 제어하기 위해 프로세스의 상태 정보를 저장하는 자료구조
- 프로스마다 고유의 PCB가 생성되고 프로세스가 종료되면 폐기된다.
- PCB는 커널 영역에 존재
- 운영체제는 PCB들을 PCB table 또는 Process table이라는 곳을 통해 관리

대기 큐
각 자원을 대기할 때 리스트의 형태로 큐를 구성한다. 리스트가 아니어도 Array, heap, tree 등 모두 가능하다.

다중 프로그래밍의 동작 원리
시분할 (Time-slicing)
메모리에 프로세스가 여러개 있더라도 CPU는 한번에 하나만 처리한다. CPU는 프로세스들을 번갈아가며 처리함으로써 여러개의 프로세스를 동시에 처리하는 것처럼 보이게 할 수 있다.
이러한 작업에서
- 어떤 순서로 번갈아가며 작업을 처리할지가 -> 스케줄링 문제이며
- 어떻게 번갈아 가게 할지의 문제가 -> Context switching이다.
문맥 교환 Context switching
- 한 프로세스에서 다른 프로세스로 CPU를 넘겨주는 과정
- 실행 상태에서 나가는 프로세스의 PCB에는 지금까지의 작업 내용을 저장하고 반대로 실행 상태로 들어오는 프로세스의 PCB의 내용으로 CPU가 다시 세팅한다.

프로세스 메모리 구조
CPU 주소 공간
프로세스 주소 공간
: 프로세스가 실행중에 접근할 수 있도록 허용된 주소의 최대 범위
- (CPU 관점) Segmentation - 할당된 공간에 대한 경계 레지스터와 한계 레지스터를 벗어나는지를 감시
- (프로세스 관점) 가상메모리 - 프로세스는 자신이 CPU 주소 공간 전체를 독점하는 것처럼 보인다.
- 모든 프로세스는 자신만의 가상 주소 공간을 가진다.
- 32bit CPU에서 작동하는 프로세스는 논리적으로 4GB의 메모리를 할당받은 것처럼 보인다. (물리 메모리가 2GB이더라도)
프로세스 주소 공간 분류
- 사용자 공간 - 코드, 데이터, 힙, 스택 영역
- 커널 공간
- 프로세스가 시스템 호출을 통해 이용하는 커널 공간
- 커널 코드, 커널 데이터, 커널 스택(커널 코드가 실행될 때)
- 커널 공간은 모든 사용자 프로세스에 의해 공유
가상 메모리의 이점
- 메모리에 대한 확장성 - 물리적 메모리는 한정적이지만, 가상 메모리는 더 큰 공간으로 구성 가능하다. 초과 분은 보조기억장치등을 활용
- 모든 프로그램에 대한 동일한 메모리 공간 제공
프로세스가 메모리에 올라갈 때
(크기가 Compiled time에 결정)
코드 영역: 실행될 프로그램 코드가 적재되는 영역
- 사용자가 작성한 모든 함수의 코드 및 호출한 라이브러리 함수들의 코드
데이터 영역: 전역 변수 공간, 정적 데이터 공간
- rdata, data, bss 등으로 구분됨
- 사용자 프로그램과 라이브러리 포함
- 프로세스 적재 시 할당, 종료 시 소멸
(크기가 runtime에 결정)
힙 영역: 프로세스가 실행 도중 동적으로 사용할 수 있도록 할당된 공간
- malloc() 등으로 할당받는 공간은 힙 영역에서 할당
- 힙 영역에서 아래 번지로 내려가면서 할당
스택 영역: 함수가 실행될 때 사용될 데이터를 위해 할당된 공간
- 매개변수들, 지역변수들, 함수 종료 후 돌아갈 주소 등
- 함수는 호출될 때, 스택 영역에서 위쪽으로 공간 할당
- 함수가 return하면 할당된 공간 반환

이렇게 나누어진 이유 -> 데이터를 공유하여 메모리 사용량을 줄이기 위해, 스택 구조와 전역변수의 활용성을 위해
Code 영역이 구분된 이유
- 프로그램 코드는 프로그램이 만들어지고 (컴파일) 나서는 바뀔 일이 전혀 없음 -> read-only
- 같은 프로세스가 여러 개의 프로세스로서 실행된다면 코드 영역을 공유함으로써 메모리 사용량을 줄일 수 있음
Data 영역이 구분된 이유
- 전역 변수와 정적 변수가 저장되고 변수들은 read-write이다. (리터럴 예외 read-only)
- 전역 및 정적 변수는 프로그램이 구동되는 동안은 항상 접근 가능해야 한다. -> 프로그램 실행과 관계없이 독립적인 영역이 필요
Heap 영역이 구분된 이유
- 프로그램이 실행 도중에 필요할 때마다 할당받는 공간 -> 얼마나 필요할지 예측 불가
- Stack 영역도 실행 상태에 따라 얼마나 사용될지 모름 -> 동적 공간에서 서로 공간을 유연하게 활용
Stack 영역이 분리된 이유
- 함수는 프로그램 실행 단위, 프로그램은 함수의 호출로 이루어져 있음 (지역변수 및 매개변수, 반환 값 등의 존재)
- 스택 구조를 이용하면 함수의 호출 순서와 반환 대상 등의 관리가 편함
- Callstack - 스택 프레임을 통한 함수의 실행 순서 구분
프로세스 주소 to 물리 주소

프로세스 생성과 복사
프로세스가 생성되는 경우
- 시스템 부팅과정에서 필요한 프로셋 생성
- 사용자 로그인 후 사용자와 대화(제어)를 위한 프로세스 생성(bash, explorer.exe, finder.app)
- 새로운 프로세스를 생성하도록 하는 사용자의 명령(vi hello.c)
- 배치 작업 실행 시(at, batch 명령)
- 사용자 응용 프로그램이 시스템 호출로 새 프로세스 생성
(주의)
메모리에 올라갔다고 해서 프로세스가 되는 것이 아니라
PCB가 존재하여 OS가 제어 가능한 형태가 되어야 한다. 즉, CPU를 할당받아 실행이 가능해야 한다.
프로세스 생성 과정
- 생성하려는 실행파일의 경로를 OS에 전달
- OS는 메모리에 프로그램을 적재
- 코드 영역에 프로그램의 코드를 적재시키고, 데이터 영역에 전역/정적 변수들을 할당
- 스택과 힙은 아직 아무것도 없으므로 초기화만 시킨다.
- PCB 공간을 할당받고 (malloc) 필요한 정보들을 채운다.
- 프로세스 식별자를 결정 - 새로운 PID 번호 할당
- 프로세스 정보 기록
- 프로세스 테이블에서 새 항목 할당
- 새로 할당된 프로세스 테이블에서 PCB 연결
- PCB에 프로세스 상태를 ready 상태로 표시하고, 준비 큐에 장착
프로세스 생성 - fork()
기존에 있는 프로세스가 다른 프로세스를 생성하낟. 프로세스를 복사하는 시스템 콜을 통해서 프로세스를 생성할 수 있다.
- 리눅스 - fork() 시스템 콜
- Windows: CreateProcess() 등 시스템 콜 (엄밀히 fork() + exec()에 가까움)
UNIX 계열의 OS는 시스템이 부팅할 때 0번 프로세스(init)만 자체적으로 생성한다. 나머지 프로세스는 복제를 통해 생성
(이점)
-> 자주 사용되는 프로세스에 대해 매번 반복할 필요가 없음 (예: bash shell)
-> 관리상 편리해짐 (프로세스 계층 구조)
-> 프로세스간 통신 용이
fork() 시스템 콜
: 실행 중인 프로세스로부터 새로운 프로세스를 복사하는 함수
- 실행 중인 프로세스와 똑같은 프로세스가 하나 더 만들어진다.
- fork()를 호출한 프로세스를 부모 프로세스, fork된 프로세스를 자식 프로세스라고 한다.
- 부모 프로세스의 모든 환경, 메모리, PCB 등을 복사
- 부모와 동일한 모양이지만 독립된 주소 공간에 위치
PCB에서 달라지는 내용
- PID
- PPID(Parent PID) -> 부모의 PID
- CPID(Child PID) -> 자식이 없으면 -1
- 메모리 관련 정보 - 독립된 주소 공간을 소유하므로
fork() 실행 과정
- 자식은 부모의 program counter도 복제하여 fork() 다음의 if(pid==0) 부터 실행 (자식 프로세스는 pid=fork(); 이전 라인을 실행하지 못함)
- fork() 함수의 리턴값
- 부모 프로세스에게는 자식 프로세스의 PID 리턴
fork()의 장단점
장점
- 프로세스의 생성 속도가 빠
- 추가 작업 없이 자원을 상속할 수 있음
- 시스템 관리를 효율적으로 할 수 있음 (프로세스 계층 구조)
단점
- 매번 모든 context의 복사본을 만드는 것은 비효율적
- fork()만으로는 맨 처음 만든 프로그램 프로세스(0) 이외에는 다른 프로글매을 동작할 수 없다.
-> UNIX OS는 fork()를 한 다음 exec()라는 시스템 콜을 호출한다.
프로세스 재사용 - exec()
exec() 시스템 콜
: 기존의 프로세스를 새로운 프로세스로 전환(재사용)하는 함수
- 현재 실행중인 프로세스의 주소 공간에 새로운 응용 프로그램을 적재하고 실행
- execlp(), execv(), execvp() 등
- 실행 파일을 로딩하여 현재 프로세스의 이미지 위에 단순히 덮어씀
fork() vs exec()
- fork() - 새로운 프로세스를 복사하는 시스템 호출
- exec() - 프로세스는 그대로 둔 채 내용만 바꾸는 시스템 호출
(주의) exec()는 프로세스를 새로 생성하는 것이 아니다.
- 프로세스의 PID 변경 X
- 프로세스의 메모리 공간(코드, 데이터, 힙, 스택)에 새로운 프로그램이 적재
- 보통 fork()를 통해 생성된 자식 프로세스가 exec() 실행
- 로더가 exec()를 통해 호출됨
exec() 실행 과정
메모리
- Code 영역에 있는 기존의 내요을 지우고 새로운 코드로 바꿔버림
- Data 영역이 새로운 변수로 채워지고 힙/스택 영역이 리셋
PCB
- PID, PPID, CPID, 메모리 관련은 유지 (새로운 프로세스가 전환되더라도 종료 후 부모 프로세스로 돌아올 수 있음)
- program counter 및 기타 register, 파일 정보 등이 모두 리셋

wait() 시스템 콜
: 자식 프로세스가 끝나기를 기다렸다가, 자식 프로세스가 종료되면 이어서 실행을 계속하는 시스템 콜
예: 리눅스 쉘에서 포그라운드 프로세스가 있으면 셀이 wait 상태 (포그라운드는 백그라운드 프로세스보다 우선순위가 높음, 사용자 독점)
프로세스가 실행 중인 상태에서 컨트롤 제트를 하면 프로세스가 일시중단되고 쉘이 wait에서 빠져나온다.
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid;
int status;
pid = fork();
if (pid > 0) {
printf("Parent: Waiting for the child\n");
wait(&status);
printf("Parent: child's exit code = %d\n", WEXITSTATUS(status));
return 0;
} else if (pid == 0) {
execlp("/bin/ls", "ls", NULL);
} else {
printf("fork error!");
return 0;
}
}

exit() 시스템 콜
: 작업의 종료를 알리는 시스템 콜
- 종료를 명시적으로 알림으로서 부모는 자식이 사용하던 자원을 빨리 회수
종료 코드
: 부모 프로세스에게 상태나 종료의 이유를 전달하는 값 (예: exit(1))
- 보통 정상종료는 0, 나머지는 1~255범위 내에서 임의로 사용
- main() 함수의 리턴 값도 종료 코드이다. (
return 0; == exit(0);)
- 내부적으로 exit() 시스템 콜이 실행되도록 컴파일된다.
- OS가 자동적으로 exit()을 호출해주도록 하여 프로그램을 종료 시키도록 한다.
- 종료코드를 부모 프로세스가 확인해야 최종적으로 자식 프로세스가 종료된다.
exit() 시스템 콜을 통한 프로세스 종료 과정
-
프로세스의 모든 자원 반환
- 코드, 데이터, 스택, 힙 등의 모든 메모리 자원을 반환하고 열어놓은 파일이나 소켓 등을 닫는다.
-
PCB에 프로세스 상태를 terminated로 변경, PCB에 종료 코드 저장
- 이후에 부모 프로세스가 자식 프로세스의 종료를 확인해야 PCB가 프로세스 테이블에서 제거된다.
-
자식 프로세스들을 init 프로세스에게 입양
-
부모 프로세스에게 SIGCHLD 신호 전송 (일종의 종료 알림 신호)
- 부모 프로세스는 SIGCHLD를 수힌하고 wait() 시스템 호출로 자식의 종료 코드 읽기를 실행한다. 죽은 자식이 남긴 정보를 확인 후, 자식 프로세스의 PCB가 완전히 제거된다.
- 만약에 부모가 자식의 종료 신호를 제때 확인하지 못하면 자식은 좀비 프로세스가 된다. PCB가 남아있으므로 ps 명령어 등으로 존재를 확인할 수 있다.
ㅤ
아래 코드는 부모가 sleep 상태여서 자식의 종료를 확인하지 못하는 동안 자식이 exit했지만 좀비 프로세스가 되어 ps 명령어로 확인할 수 있는 예제이다.
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
pid_t pid, zompid;
int status;
pid = fork();
if (pid > 0) {
sleep(10);
zompid = wait(&status);
printf("Parent: child %d has been finished with %d\n", zompid, WEXITSTATUS(status));
return 0;
} else if (pid == 0) {
printf("Child: I am done %d \n", getpid());
exit(100);
} else {
printf("fork error\n");
return 0;
}
}

UNIX OS에서의 프로세스 생성(및 종료) 과정
fork() -> exec() 구조
- fork()를 통해 프로세스를 만들고, exec()를 통해 필요한 프로세스를 실행
- 부모는 wait()를 통해 기다리고, 자식은 exit()를 통해 자신의 종료를 알림
모든 프로세스의 최초의 조상 -> 1번 프로세스 init

프로세스 계층 구조
모든 프로세스의 조상은 init 이다. 유닉스의 모든 프로세스는 init 프로세스의 자식이 되어 트리 구조를 이룬다.
계층 구조
- 프로세스는 일반적으로 부모-자식 관계이다.
- #0 프로세스가 시스템 부팅시 실행되는 최초의 프로세스이다.
- 부모 프로세스는 여러 개의 자식 프로세스를 가질 수 있다.
- #0 프로세스를 제외하고 모든 프로세스는 부모 프로세스를 가진다.
- 모든 프로세스는 부모 프로세스에 의해 생성되며 시스템 호출(fork())를 통해서만 가능하다.
- PID 0,1,2 등의 몇몇 조상 프로세스는 시스템 콜이 아닌 부팅 시 OS 차원에서 수작업(hand-craft)로 생성된다.
계층 구조에서 fork-exec는 여러 작업을 처리하기에 용이하고(fork) 프로세스의 재사용에 용이(exec)하다. 프로세스 간 책임관계가 분명해짐으로써 자원 회수등의 관리가 쉬워진다.
좀비 프로세스
부모가 자식을 생성한 후 자식의 종룔를 기다리는 경우, 부모가 자식의 종료를 (제때) 못 받아주면 자식 프로세스는 좀비 상태가 된다.
좀비 프로세스가 메모리를 차지 하지 않는다 하지만(크기가 0) PCB의 낭비는 발생한다.
- 커널 입장에서 봤을 때 PCB를 유지하기 위한 어느정도의 자원을 소모하게 된다.
- 커널이 유지할 수 있는 PCB 테이블의 크기에 제한이 있다.
-> 많은 좀비프로세스가 발생할 경우 시스템 성능에 영향(예: 프로세스 스케줄링을 위한 확인 등)
좀비 프로세스 제거하는 법
- 부모 프로세스에게 SIGCHILD 신호를 보내기 -> 부모 프로세스에서 wait() 호출하여 처리
- 부모 프로세스를 강제 종료 -> 좀비를 고아(Orphan)화
- 좀비는 init 프로세스의 자식이 되고 init이 wait() 호출하여 좀비 프로세스 제거
고아 프로세스
: 부모가 먼저 종료한 자식 프로세스
부모 프로세스가 종료할 때 일반적으로 커널은 자식 프로세스가 있는지 확인하고 자식이 있으면 자식 프로세스를 init 프로세스에게 입양한다. -> PPID가 1로 변경
(운영체제에 따라 모든 자식 프로세스를 강제 종료시키기도 한다.

백그라운드 프로세스 & 포그라운드 프로세스
백그라운드 프로세스
- 터미널에서 실행되었지만, 터미널 사용자와의 대화가 없는 채 실행되는 프로세스
- 사용자와 대화없이 실행되는 프로세스
- 사용자 입력을 필요로 하지 않는 프로세스
- idle 상태로 잠을 자거나 디스크에 스왑된 상태의 프로세스
포그라운드 프로세스
- 실행되는 동안 터미널 사용자의 입력을 독점하는 프로세스
CPU 집중 프로세스 vs I/O 집중 프로세스
CPU 집중 프로세스(CPU intensive process)
- 대부분의 시간을 계산 중심의 일(CPU 작업)을 하느라 보내는 프로세스
- 배열 곱, 인공지능 연산, 이미지 처리
- CPU 속도가 성능 좌우(CPU bound)
I/O 집중 프로세스(I/O intensive process)
- 입출력 작업을 하느라 대부분의 시간을 보내는 프로세스
- 네트워크 전송, 파일 입출력에 집중된 프로세스
- 파일 서버, 웹 서버
- 입출력 장치나 입출력 시스템의 속도가 성능 좌우(I/O bound)
운영체제의 스케줄링 우선순위: I/O 집중 프로세스 > CPU 집중 프로세스
- I/O 작업하는 동안 다른 프로세스에게 CPU 할당 가능
0번 프로세스 & 2번 프로세스
0번 프로세스
- swapper(UNIX): 부팅 담당 및 #1 init 생성
- idle(LINUX), system idle process(Windows)
- 우선 순위가 가장 낮은 프로세스로 아무것도 안함
- 실행 중인 프로세스가 1개도 없는 상태에 빠지지 않기 위해 만든 프로세스
(왜 프로세스는 적어도 하나가 실행되어야 할까?)
CPU는 인터럽트나 작업 요청을 처리하기 위해 항상 실행 컨텍스트를 가져야 한다. 실행할 사용자 프로세스가 없을 경우, 운영체제는 0번(idle) 프로세스를 실행시켜 CPU를 대기 상태로 유지하며 인터럽트를 수신할 수 있도록 한다.
2번 프로세스; kthreadd
- 커널 프로세스는 커널 공간에서만 실행하는 프로세스를 의미
- 대부분 커널 스레드 형태로 구동 -> kthreadd는 모든 커널 프로세스(thread)의 조상

셸 작업/세션 관리
셀(Shell): 사용자와 운영 체제(kernel) 사이의 인터페이스 역할을 하는 프로그램 또는 환경
작업(Job): 셸이 백그라운드로 프로세스를 제어하는 동작 구조
세션(Session): 사용자가 terminal을 사용해 시스템에 로그인했을 때, 활동하는 상태 등을 관리하는 단위
- 각 세션에는 해당 세션을 제어하는 terminal 존재 -> Session ID를 통해 관리
nohup
터미널이 닫히면, 세션이 닫히고, 결국 실행 중이던 모든 것이 종료된다. (단말이 끊기면 세션에는 SIGHUP 시그널이 전달되어 종료)
긴 작업을 돌리기 위해 터미널이 닫혀도 계속 실행되도록 하기 위해 nohup 명령어를 사용할 수 있다. SIGHUP을 무시하도록 설정하고 프로세스를 기동하여 만약 세션이 종료되더라도 프로세스를 종료하지 않도록 할 수 있다.
nohup을 사용하지 않고 계속 실행시킬 수 있는 방법으로는, ssh의 경우 timeout 시간을 증가시키거나, heartbeat를 주기적으로 보내는 방법이 있다.
프로세스 그룹
: 여러 프로세스를 하나로 묶어서 관리하는 단위(PGID)
Foreground process group: 현재 사용자가 직접 사용하고 있는 프로세스
- 터미널에서 접근 가능하고 세션당 하나만 존재한다.
- 여러 프로세스가 pipe로 연결되면 하나의 프로세스 그룹으로 간주할 수 있다.
Background process group: 사용자와 직접적인 상호작용 없이 실행되고 있는 프로세스
- 터미널 세션과 관련없이 실행되며 사용자 입력을 기다리지 않는다.

데몬(Daemon)
: 백그라운드로 실행되는 상주하는 프로세스
- 터미널 입출력이 필요 없어 단말 할당이 되지 않는다.
- 로그인 세션을 종료해도 영향을 받지 않도록 독자적인 세션을 가진다.
- 데몬을 생성한 프로세스가 데몬 종료 여부를 신경 쓰지 않도록 init이 부모가 된다.