본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 28. · 3 Views
프로세스 개념과 구조 완벽 가이드
운영체제의 핵심인 프로세스가 무엇인지, 메모리에서 어떻게 구성되는지, 그리고 어떻게 생성되고 종료되는지를 초급 개발자의 눈높이에서 차근차근 설명합니다.
목차
- 프로그램 vs 프로세스
- 프로세스 메모리 구조
- PCB (Process Control Block)
- 프로세스 속성과 메타데이터
- 프로세스 생성과 종료
- fork()와 exec() 시스템 콜
1. 프로그램 vs 프로세스
김개발 씨는 오늘 면접을 앞두고 있습니다. 이력서에 "운영체제 기초 이해"라고 적어놨는데, 문득 걱정이 됩니다.
"프로그램과 프로세스의 차이가 뭐냐"고 물으면 뭐라고 대답해야 할까요?
프로그램은 디스크에 저장된 실행 파일입니다. 반면 프로세스는 그 프로그램이 메모리에 올라가서 실제로 실행 중인 상태를 말합니다.
마치 요리책과 실제로 요리하는 행위의 차이와 같습니다. 이 구분을 명확히 이해하면 운영체제가 프로그램을 어떻게 관리하는지 알 수 있습니다.
다음 코드를 살펴봅시다.
// 프로그램: 디스크에 저장된 실행 파일
// $ ls -l /usr/bin/python3
// -rwxr-xr-x 1 root root 5479736 python3
// 프로세스: 실행 중인 프로그램 인스턴스
// $ ps aux | grep python
// user 1234 0.1 0.5 python3 script.py
// user 5678 0.2 0.3 python3 server.py
// 같은 프로그램(python3)이지만 두 개의 다른 프로세스
// 각각 고유한 PID(1234, 5678)를 가짐
#include <stdio.h>
#include <unistd.h>
int main() {
// 현재 프로세스의 ID 출력
printf("현재 프로세스 ID: %d\n", getpid());
return 0;
}
김개발 씨는 입사 첫 달, 선배 박시니어 씨와 함께 서버 모니터링을 하고 있었습니다. 터미널에서 ps 명령어를 치자 수십 개의 프로세스 목록이 쏟아져 나왔습니다.
"선배, 저기 python3이 세 개나 떠 있네요. 파이썬이 세 개 설치된 건가요?" 박시니어 씨가 웃으며 고개를 저었습니다.
"아니, 저건 같은 프로그램이 세 번 실행된 거야. 프로그램과 프로세스는 다른 개념이거든." 그렇다면 프로그램과 프로세스는 정확히 어떻게 다른 걸까요?
쉽게 비유하자면, 프로그램은 요리책에 적힌 레시피와 같습니다. 레시피는 책장에 꽂혀 있을 뿐, 아무 일도 하지 않습니다.
반면 프로세스는 그 레시피를 보고 실제로 요리하는 행위입니다. 같은 레시피로 여러 요리사가 동시에 요리할 수 있듯이, 하나의 프로그램으로 여러 프로세스를 만들 수 있습니다.
조금 더 기술적으로 설명해 보겠습니다. 프로그램은 하드디스크나 SSD 같은 보조 기억장치에 저장된 실행 파일입니다.
확장자가 .exe인 윈도우 실행 파일이나, 리눅스의 바이너리 파일이 여기에 해당합니다. 이들은 그냥 파일일 뿐, 아직 살아 움직이지 않습니다.
사용자가 프로그램을 더블클릭하거나 터미널에서 실행 명령을 내리면, 운영체제는 그 프로그램을 **메모리(RAM)**로 복사합니다. 이 순간 프로그램은 프로세스로 변신합니다.
CPU가 명령어를 읽고 실행하기 시작하면서, 비로소 생명력을 얻는 것입니다. 여기서 중요한 점이 있습니다.
프로세스는 단순히 프로그램 코드만 메모리에 올리는 게 아닙니다. 프로그램이 사용할 데이터 영역, 함수 호출을 위한 스택 영역, 동적 할당을 위한 힙 영역까지 함께 메모리에 자리잡습니다.
게다가 운영체제는 각 프로세스마다 고유한 **프로세스 ID(PID)**를 부여합니다. 실무에서 이 개념이 왜 중요할까요?
서버 개발자라면 하나의 웹 서버 프로그램을 여러 프로세스로 실행해서 동시 접속자를 처리하는 경우가 많습니다. 이때 각 프로세스는 독립적인 메모리 공간을 가지므로, 한 프로세스가 죽어도 다른 프로세스에 영향을 주지 않습니다.
초보 개발자들이 자주 혼동하는 부분이 있습니다. "메모장을 두 번 열면 프로그램도 두 개인가요?"라는 질문입니다.
답은 아니오입니다. 프로그램은 하나이고, 프로세스가 두 개입니다.
각 메모장 창은 서로 다른 PID를 가진 독립적인 프로세스입니다. 다시 김개발 씨 이야기로 돌아가 봅시다.
면접관이 물었습니다. "프로그램과 프로세스의 차이를 설명해 주세요." 김개발 씨는 자신 있게 대답했습니다.
"프로그램은 정적인 코드 덩어리이고, 프로세스는 그것이 메모리에서 살아 움직이는 인스턴스입니다." 합격입니다.
실전 팁
💡 - ps aux 명령어로 현재 실행 중인 모든 프로세스를 확인할 수 있습니다
- 같은 프로그램을 여러 번 실행하면 각각 다른 PID를 가진 프로세스가 생성됩니다
2. 프로세스 메모리 구조
김개발 씨가 C 프로그램에서 세그멘테이션 폴트 에러를 만났습니다. "대체 왜 터지는 거지?" 하며 머리를 쥐어뜯고 있을 때, 박시니어 씨가 다가와 말했습니다.
"프로세스 메모리 구조를 알면 디버깅이 훨씬 쉬워져요."
프로세스가 메모리에 올라가면 코드, 데이터, 힙, 스택 네 가지 영역으로 나뉩니다. 마치 잘 정리된 서랍장처럼, 각 영역은 고유한 역할을 담당합니다.
이 구조를 이해하면 메모리 관련 버그를 훨씬 쉽게 잡을 수 있습니다.
다음 코드를 살펴봅시다.
#include <stdio.h>
#include <stdlib.h>
// 전역 변수 - 데이터 영역에 저장
int global_var = 100;
// 함수 코드 - 코드(텍스트) 영역에 저장
void print_addresses() {
// 지역 변수 - 스택 영역에 저장
int local_var = 50;
// 동적 할당 - 힙 영역에 저장
int *heap_var = (int *)malloc(sizeof(int));
*heap_var = 200;
printf("코드 영역: %p\n", (void *)print_addresses);
printf("데이터 영역: %p\n", (void *)&global_var);
printf("힙 영역: %p\n", (void *)heap_var);
printf("스택 영역: %p\n", (void *)&local_var);
free(heap_var);
}
김개발 씨는 디버깅 지옥에 빠져 있었습니다. 분명히 malloc으로 메모리를 할당했는데, 어느 순간 이상한 값이 들어가 있습니다.
대체 메모리에서 무슨 일이 벌어지고 있는 걸까요? 박시니어 씨가 화이트보드에 그림을 그리기 시작했습니다.
"프로세스가 메모리에 올라가면 네 개의 방으로 나뉘어. 각 방의 역할을 알면 버그 잡기가 훨씬 쉬워져." 프로세스 메모리 구조를 4층짜리 아파트로 비유해 보겠습니다.
가장 아래층인 1층에는 코드(텍스트) 영역이 있습니다. 여기에는 실행할 명령어들이 저장됩니다.
컴파일된 함수들, if문, for문 같은 실행 코드가 이곳에 자리잡습니다. 이 영역은 읽기 전용이라 수정할 수 없습니다.
실수로 코드를 덮어쓰면 안 되니까요. 2층은 데이터 영역입니다.
전역 변수와 static 변수가 여기에 삽니다. 프로그램이 시작할 때 할당되고, 종료할 때까지 계속 유지됩니다.
초기값이 있는 변수와 없는 변수가 각각 다른 구역에 저장되는데, 이를 Data 영역과 BSS 영역이라고 부릅니다. 3층은 힙 영역입니다.
malloc이나 new로 동적 할당한 메모리가 이곳에 저장됩니다. 개발자가 직접 관리해야 하는 영역이라, 할당했으면 반드시 해제해야 합니다.
그렇지 않으면 메모리 누수가 발생합니다. 가장 위층인 4층은 스택 영역입니다.
함수가 호출될 때마다 지역 변수와 매개변수, 복귀 주소가 쌓입니다. 함수가 끝나면 자동으로 정리되므로 개발자가 신경 쓸 필요가 없습니다.
다만 너무 깊은 재귀 호출을 하면 스택 오버플로우가 발생합니다. 흥미로운 점이 있습니다.
힙은 아래에서 위로 자라고, 스택은 위에서 아래로 자랍니다. 마치 양쪽에서 짜는 치약과 같습니다.
두 영역이 만나면 메모리가 부족해진 것입니다. 위 코드를 실행해 보면 각 변수의 주소가 다른 범위에 있는 것을 확인할 수 있습니다.
코드 영역의 주소가 가장 낮고, 그 위로 데이터, 힙, 스택 순서로 주소가 높아집니다. 물론 실제 주소 배치는 운영체제와 아키텍처에 따라 다를 수 있습니다.
실무에서 이 지식이 빛을 발하는 순간이 있습니다. 세그멘테이션 폴트가 발생했을 때, 코어 덤프를 분석하면 어느 영역에서 문제가 생겼는지 파악할 수 있습니다.
스택 영역에서 터졌다면 지역 변수나 배열 인덱스를 의심해야 하고, 힙 영역이라면 동적 할당과 해제를 점검해야 합니다. 김개발 씨는 박시니어 씨의 설명을 듣고 다시 코드를 살펴봤습니다.
malloc으로 할당한 메모리를 free한 뒤에도 계속 접근하고 있었습니다. 이미 반납한 방에 들어가려 했으니, 당연히 문제가 생겼던 것입니다.
실전 팁
💡 - 힙 메모리는 할당했으면 반드시 해제해야 메모리 누수를 방지할 수 있습니다
- 스택 오버플로우가 발생하면 재귀 호출 깊이를 점검하거나 반복문으로 변경해 보세요
3. PCB (Process Control Block)
어느 날 김개발 씨가 궁금해졌습니다. "운영체제는 수십 개의 프로세스를 동시에 관리하잖아요.
각 프로세스 정보를 어디에 저장하는 거예요?" 박시니어 씨가 답했습니다. "바로 PCB라는 자료구조에 저장해."
**PCB(Process Control Block)**는 운영체제가 각 프로세스에 대한 정보를 저장하는 자료구조입니다. 마치 학교의 학생 기록부처럼, 프로세스의 신상정보와 현재 상태를 모두 담고 있습니다.
운영체제는 PCB를 통해 프로세스를 관리하고 문맥 교환을 수행합니다.
다음 코드를 살펴봅시다.
// PCB의 개념적 구조 (실제 리눅스 task_struct 간략화)
struct PCB {
// 프로세스 식별 정보
int pid; // 프로세스 ID
int ppid; // 부모 프로세스 ID
int state; // 상태 (실행, 준비, 대기 등)
// CPU 레지스터 정보 (문맥)
unsigned long program_counter; // 다음 실행할 명령어 주소
unsigned long stack_pointer; // 스택 포인터
unsigned long registers[16]; // CPU 레지스터 값들
// 메모리 관리 정보
unsigned long memory_base; // 메모리 시작 주소
unsigned long memory_limit; // 메모리 크기
// 스케줄링 정보
int priority; // 우선순위
unsigned long cpu_time; // 사용한 CPU 시간
};
김개발 씨가 리눅스 서버에서 top 명령어를 실행했습니다. 화면에 수십 개의 프로세스가 나열되고, 각각의 CPU 사용률, 메모리 점유율, 상태가 실시간으로 갱신됩니다.
"이 정보들은 대체 어디서 오는 거지?" 운영체제는 수많은 프로세스를 동시에 관리해야 합니다. 각 프로세스가 어디까지 실행됐는지, 메모리를 얼마나 쓰고 있는지, 언제 CPU를 사용할 차례인지 알아야 합니다.
이 모든 정보를 담는 자료구조가 바로 **PCB(Process Control Block)**입니다. PCB를 학생 기록부에 비유해 보겠습니다.
학교에서는 각 학생마다 기록부가 있습니다. 학번, 이름, 학년, 성적, 출결 현황 등이 적혀 있습니다.
담임선생님이 바뀌어도 기록부를 보면 학생의 모든 정보를 파악할 수 있습니다. PCB도 마찬가지입니다.
프로세스 하나당 PCB 하나가 있고, 여기에 프로세스의 모든 정보가 담깁니다. PCB에 저장되는 핵심 정보를 살펴보겠습니다.
첫째, **프로세스 식별자(PID)**입니다. 각 프로세스를 구별하는 고유한 번호입니다.
둘째, 프로세스 상태입니다. 현재 실행 중인지, CPU를 기다리는 중인지, 입출력을 기다리는 중인지 기록합니다.
셋째, 프로그램 카운터입니다. CPU가 다음에 실행할 명령어의 주소를 저장합니다.
넷째, CPU 레지스터들입니다. 프로세스가 사용하던 레지스터 값들을 보관합니다.
이 두 가지가 특히 중요한데, 문맥 교환(Context Switch) 때 사용되기 때문입니다. 문맥 교환이란 CPU가 한 프로세스에서 다른 프로세스로 전환하는 것입니다.
이때 현재 프로세스의 상태를 PCB에 저장하고, 다음 프로세스의 PCB에서 이전 상태를 복원합니다. 마치 여러 권의 책을 번갈아 읽을 때, 각 책에 책갈피를 끼워두는 것과 같습니다.
리눅스에서 PCB는 task_struct라는 구조체로 구현되어 있습니다. 이 구조체는 수백 개의 필드를 가지고 있으며, 프로세스에 대한 모든 정보를 담고 있습니다.
커널 소스 코드의 include/linux/sched.h 파일에서 확인할 수 있습니다. 실무에서 PCB 개념이 왜 중요할까요?
성능 튜닝을 할 때 문맥 교환 횟수를 모니터링하는 경우가 있습니다. 문맥 교환이 너무 자주 일어나면 PCB를 저장하고 복원하는 오버헤드 때문에 시스템 성능이 떨어집니다.
vmstat 명령어로 초당 문맥 교환 횟수를 확인할 수 있습니다. 김개발 씨는 이제 top 명령어 출력 결과가 다르게 보입니다.
각 프로세스 옆에 표시된 정보들은 모두 해당 프로세스의 PCB에서 가져온 것입니다. 운영체제라는 교장 선생님이 각 프로세스의 기록부를 관리하고 있었던 것입니다.
실전 팁
💡 - 리눅스에서 /proc/[pid]/ 디렉터리를 통해 각 프로세스의 PCB 정보를 확인할 수 있습니다
- vmstat 명령어로 문맥 교환 횟수를 모니터링하면 시스템 성능을 점검할 수 있습니다
4. 프로세스 속성과 메타데이터
김개발 씨가 ps aux 명령어를 쳤더니 복잡한 정보들이 쏟아집니다. USER, PID, %CPU, %MEM, VSZ, RSS...
"이게 다 뭔가요?" 박시니어 씨가 차근차근 설명해 주기로 했습니다.
프로세스는 다양한 속성과 메타데이터를 가지고 있습니다. PID, PPID, UID 같은 식별 정보부터 CPU 사용량, 메모리 점유율, 우선순위까지 다양한 정보가 있습니다.
이 속성들을 이해하면 시스템 모니터링과 디버깅이 훨씬 수월해집니다.
다음 코드를 살펴봅시다.
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/resource.h>
int main() {
// 프로세스 식별 정보
printf("PID (프로세스 ID): %d\n", getpid());
printf("PPID (부모 프로세스 ID): %d\n", getppid());
printf("UID (사용자 ID): %d\n", getuid());
printf("GID (그룹 ID): %d\n", getgid());
// 프로세스 우선순위
int priority = getpriority(PRIO_PROCESS, 0);
printf("Nice 값: %d\n", priority);
// 현재 작업 디렉터리
char cwd[256];
getcwd(cwd, sizeof(cwd));
printf("작업 디렉터리: %s\n", cwd);
return 0;
}
김개발 씨가 서버에서 이상하게 느린 프로세스를 발견했습니다. 터미널에서 ps aux를 입력하자 수십 줄의 정보가 출력됩니다.
각 열이 무엇을 의미하는지 모르면 문제를 진단할 수 없습니다. 프로세스의 속성은 크게 식별 정보, 상태 정보, 자원 정보로 나눌 수 있습니다.
하나씩 살펴보겠습니다. 먼저 식별 정보입니다.
PID는 프로세스를 구별하는 고유 번호입니다. 마치 주민등록번호처럼, 시스템에서 중복되지 않습니다.
PPID는 부모 프로세스의 PID입니다. 모든 프로세스는 다른 프로세스로부터 태어나기 때문에 부모가 있습니다.
최초의 프로세스인 init(또는 systemd)만 예외입니다. UID와 GID는 프로세스를 실행한 사용자와 그룹을 나타냅니다.
이 정보를 바탕으로 운영체제는 파일 접근 권한을 결정합니다. root 사용자의 UID는 0이며, 모든 권한을 가집니다.
다음은 상태 정보입니다. 프로세스 상태는 R(Running), S(Sleeping), D(Disk Sleep), Z(Zombie), T(Stopped) 등이 있습니다.
R은 실행 중이거나 실행 대기 중인 상태입니다. S는 이벤트를 기다리며 잠들어 있는 상태입니다.
Z는 좀비 프로세스로, 종료됐지만 부모가 회수하지 않은 상태입니다. 마지막으로 자원 정보입니다.
%CPU는 CPU 사용률, %MEM은 메모리 사용률입니다. VSZ는 가상 메모리 크기, RSS는 실제 물리 메모리 사용량입니다.
메모리 누수를 의심할 때는 RSS 값이 계속 증가하는지 확인해야 합니다. 프로세스에는 우선순위도 있습니다.
리눅스에서는 nice 값으로 표현하는데, 범위는 -20부터 19까지입니다. 값이 낮을수록 우선순위가 높습니다.
-20은 가장 급한 프로세스, 19는 가장 양보하는 프로세스입니다. renice 명령어로 실행 중인 프로세스의 우선순위를 변경할 수 있습니다.
실무에서 이 속성들을 어떻게 활용할까요? 서버가 느려지면 top 명령어로 CPU를 많이 쓰는 프로세스를 찾습니다.
메모리 부족이 의심되면 RSS가 큰 프로세스를 점검합니다. 좀비 프로세스가 늘어나면 부모 프로세스에 버그가 있는지 확인합니다.
김개발 씨는 ps aux 출력을 다시 보았습니다. 문제의 프로세스가 %CPU 95%를 차지하고 있었습니다.
무한 루프에 빠진 것 같습니다. kill 명령어로 해당 PID의 프로세스를 종료하고, 코드를 점검하기로 했습니다.
실전 팁
💡 - htop 명령어를 설치하면 프로세스 정보를 더 보기 좋게 확인할 수 있습니다
- pstree 명령어로 프로세스 간의 부모-자식 관계를 트리 형태로 볼 수 있습니다
5. 프로세스 생성과 종료
김개발 씨가 물었습니다. "프로세스는 어떻게 태어나고 어떻게 죽나요?" 박시니어 씨가 답했습니다.
"프로세스의 일생은 태어나서, 일하고, 죽는 거야. 운영체제는 이 모든 과정을 관리하지."
프로세스는 부모 프로세스로부터 **생성(fork)**되고, 작업을 마치면 **종료(exit)**됩니다. 부모 프로세스는 자식의 종료를 **대기(wait)**해야 합니다.
이 과정을 제대로 처리하지 않으면 좀비 프로세스나 고아 프로세스가 발생합니다.
다음 코드를 살펴봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("부모 프로세스 시작 (PID: %d)\n", getpid());
pid_t pid = fork(); // 자식 프로세스 생성
if (pid < 0) {
// fork 실패
perror("fork 실패");
exit(1);
} else if (pid == 0) {
// 자식 프로세스
printf("자식 프로세스 (PID: %d, 부모: %d)\n", getpid(), getppid());
sleep(2); // 2초간 작업
printf("자식 종료\n");
exit(0); // 정상 종료
} else {
// 부모 프로세스
printf("부모: 자식 PID %d 생성됨\n", pid);
int status;
wait(&status); // 자식 종료 대기
printf("부모: 자식 종료 확인\n");
}
return 0;
}
김개발 씨가 서버 로그를 보다가 이상한 것을 발견했습니다. defunct라고 표시된 프로세스가 점점 늘어나고 있었습니다.
"이건 뭐지? 좀비라고?" 프로세스의 일생을 사람에 비유해 보겠습니다.
사람은 부모로부터 태어나고, 살면서 일을 하고, 언젠가 생을 마감합니다. 프로세스도 마찬가지입니다.
부모 프로세스로부터 태어나고(fork), CPU를 할당받아 일하고(run), 작업이 끝나면 **종료(exit)**됩니다. 프로세스 생성을 자세히 살펴보겠습니다.
리눅스에서 새 프로세스를 만드는 가장 기본적인 방법은 fork() 시스템 콜입니다. fork를 호출하면 현재 프로세스가 복제되어 자식 프로세스가 탄생합니다.
부모와 자식은 fork 이후 각자의 길을 갑니다. 여기서 중요한 점이 있습니다.
fork()의 반환값입니다. 부모 프로세스에서는 자식의 PID가 반환되고, 자식 프로세스에서는 0이 반환됩니다.
이 반환값으로 현재 코드가 부모에서 실행 중인지, 자식에서 실행 중인지 구분합니다. 프로세스 종료는 exit() 함수로 이루어집니다.
종료 코드를 전달할 수 있는데, 관례적으로 0은 정상 종료, 그 외의 값은 오류를 의미합니다. 쉘에서 echo $?를 입력하면 직전 명령어의 종료 코드를 확인할 수 있습니다.
그런데 자식이 종료되면 바로 사라지는 게 아닙니다. 자식의 종료 상태를 부모에게 전달해야 하기 때문에, 부모가 **wait()**를 호출할 때까지 잠시 대기합니다.
이 상태를 좀비 프로세스라고 합니다. 좀비는 자원을 거의 사용하지 않지만, PID를 차지하고 있어 문제가 될 수 있습니다.
만약 부모가 wait를 호출하지 않으면 어떻게 될까요? 자식은 계속 좀비 상태로 남습니다.
반대로 부모가 먼저 종료되면 어떻게 될까요? 자식은 고아 프로세스가 됩니다.
다행히 운영체제는 고아 프로세스를 init(PID 1)에게 입양시킵니다. init이 wait를 호출해서 뒷정리를 해줍니다.
김개발 씨가 발견한 좀비 프로세스의 원인이 밝혀졌습니다. 부모 프로세스가 fork만 하고 wait를 호출하지 않았던 것입니다.
코드를 수정해서 wait를 추가하자, 좀비 프로세스가 더 이상 생기지 않았습니다.
실전 팁
💡 - 자식 프로세스가 많다면 SIGCHLD 시그널 핸들러에서 wait를 호출하는 것이 효율적입니다
- ps aux | grep defunct로 좀비 프로세스를 찾을 수 있습니다
6. fork()와 exec() 시스템 콜
김개발 씨가 물었습니다. "fork로 자식을 만들면 부모와 똑같은 코드가 실행되잖아요.
완전히 다른 프로그램을 실행하려면요?" 박시니어 씨가 웃으며 답했습니다. "그래서 exec가 있는 거야."
**fork()**는 현재 프로세스를 복제하고, **exec()**는 현재 프로세스를 새로운 프로그램으로 덮어씁니다. 이 두 가지를 조합하면 새로운 프로그램을 실행하는 자식 프로세스를 만들 수 있습니다.
쉘이 명령어를 실행할 때 바로 이 방식을 사용합니다.
다음 코드를 살펴봅시다.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
printf("쉘 시뮬레이션 시작 (PID: %d)\n", getpid());
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스: ls -la 실행
printf("자식: exec로 ls 프로그램 실행\n");
// exec 계열 함수 - 현재 프로세스를 ls로 교체
execlp("ls", "ls", "-la", NULL);
// exec가 성공하면 아래 코드는 실행되지 않음
perror("exec 실패");
exit(1);
} else {
// 부모 프로세스: 자식 완료 대기
wait(NULL);
printf("부모: 자식 프로세스 완료\n");
}
return 0;
}
터미널에서 ls 명령어를 입력하면 어떤 일이 벌어질까요? 단순히 ls 프로그램이 실행되는 것처럼 보이지만, 내부에서는 복잡한 과정이 일어납니다.
바로 fork와 exec의 조합입니다. 먼저 **fork()**를 복습해 보겠습니다.
fork는 현재 프로세스를 그대로 복제합니다. 부모와 자식은 같은 코드, 같은 데이터를 가집니다.
단, 복제 직후 각자 다른 PID를 가지고 독립적으로 실행됩니다. 그런데 여기서 의문이 생깁니다.
자식이 부모와 똑같은 코드를 실행한다면, 어떻게 ls나 python 같은 다른 프로그램을 실행할 수 있을까요? 바로 exec() 시스템 콜이 답입니다.
exec는 현재 프로세스의 메모리를 새로운 프로그램으로 완전히 덮어씁니다. 코드 영역, 데이터 영역, 스택, 힙 모두 새 프로그램의 것으로 교체됩니다.
단, PID는 그대로 유지됩니다. 마치 영혼은 그대로인데 몸만 바꾸는 것과 같습니다.
exec 계열 함수는 여러 가지가 있습니다. execl, execv, execlp, execvp 등입니다.
이름의 규칙이 있는데, l은 인자를 리스트로 받고, v는 벡터(배열)로 받습니다. p가 붙으면 PATH 환경변수에서 프로그램을 찾습니다.
위 코드에서 execlp("ls", "ls", "-la", NULL)을 살펴보겠습니다. 첫 번째 "ls"는 실행할 프로그램 이름이고, 두 번째 "ls"부터는 argv 배열에 들어갈 인자들입니다.
마지막 NULL은 인자 목록의 끝을 표시합니다. 중요한 점이 있습니다.
exec가 성공하면 이후의 코드는 절대 실행되지 않습니다. 왜냐하면 현재 프로세스가 완전히 새 프로그램으로 바뀌었기 때문입니다.
exec 이후에 코드가 실행되고 있다면, 그것은 exec가 실패했다는 뜻입니다. 이제 쉘의 동작 원리를 이해할 수 있습니다.
사용자가 ls를 입력하면, 쉘은 먼저 fork로 자식을 만듭니다. 자식은 exec로 ls 프로그램을 실행합니다.
부모 쉘은 wait로 자식이 끝나기를 기다립니다. 자식이 끝나면 다시 프롬프트를 띄웁니다.
실무에서 fork와 exec는 다양한 곳에서 활용됩니다. 웹 서버가 CGI 프로그램을 실행할 때, 데몬이 자식 프로세스를 생성할 때, CI/CD 파이프라인에서 스크립트를 실행할 때 모두 이 메커니즘을 사용합니다.
김개발 씨는 이제 터미널을 다른 눈으로 바라봅니다. 매번 명령어를 칠 때마다 fork와 exec가 일어나고 있었던 것입니다.
운영체제의 우아한 설계가 느껴집니다.
실전 팁
💡 - exec 함수 호출 후에는 반드시 에러 처리 코드를 작성해야 합니다 (성공하면 실행되지 않음)
- fork 없이 exec만 호출하면 현재 쉘 자체가 새 프로그램으로 바뀌어 버립니다
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
CPU 스케줄링 알고리즘 (2)
Round Robin부터 MLFQ까지, 현대 운영체제가 사용하는 핵심 CPU 스케줄링 알고리즘들을 실무 예제와 함께 알아봅니다. 초급 개발자도 쉽게 이해할 수 있도록 스토리텔링 방식으로 설명합니다.
CPU 스케줄링 알고리즘 완벽 가이드 (1)
운영체제의 핵심인 CPU 스케줄링 알고리즘을 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. FCFS부터 HRRN까지, 각 알고리즘의 동작 원리와 장단점을 실무 스토리와 함께 배워봅니다.
CPU 스케줄링 기초 완벽 가이드
운영체제의 핵심인 CPU 스케줄링을 초급 개발자도 이해할 수 있도록 쉽게 설명합니다. 선점형과 비선점형의 차이부터 다양한 성능 지표까지, 실무에서 꼭 알아야 할 개념을 다룹니다.
프로세스 상태 전이 완벽 가이드
운영체제에서 프로세스가 어떤 상태를 거쳐 실행되는지, 그리고 스케줄러와 디스패처가 어떤 역할을 하는지 초급 개발자도 쉽게 이해할 수 있도록 설명합니다. 프로세스의 생애주기를 따라가며 운영체제의 핵심 개념을 마스터해보세요.
사용자 모드와 커널 모드 완벽 가이드
운영체제가 어떻게 응용 프로그램과 시스템을 보호하는지 알아봅니다. 사용자 모드와 커널 모드의 차이, 시스템 콜의 동작 원리, 모드 전환 과정을 실무 예제와 함께 쉽게 설명합니다.