- Authors

- Name
- Youngju Kim
- @fjvbn20031
프로세스 개념
프로세스(Process)는 실행 중인 프로그램이다. 프로그램이 디스크에 저장된 수동적 존재라면, 프로세스는 메모리에 올라가 실행되는 능동적 존재다.
프로세스 메모리 구조
[프로세스의 메모리 레이아웃]
높은 주소
+------------------+
| 스택(Stack) | 지역 변수, 함수 매개변수, 반환 주소
| | | (아래로 성장)
| v |
| |
| ^ |
| | |
| 힙(Heap) | 동적 할당 메모리 (malloc, new)
| | (위로 성장)
+------------------+
| 데이터(Data) | 전역 변수, 정적 변수
+------------------+
| 텍스트(Text) | 프로그램 코드 (기계어 명령)
+------------------+
낮은 주소
프로세스 상태
프로세스는 실행 중 다음 상태를 거친다.
[프로세스 상태 전이도]
디스패치
+--------+-------->+---------+
| 준비 | | 실행 |
| (Ready) |<--------| (Running)|
+--------+ 타이머 +---------+
^ ^ 인터럽트 | |
| | | |
admitted | | I/O 완료 | I/O | exit
| | 이벤트 |요청 |
| | | |
+---+ +--------+ | +--------+
|새로| | 대기 |<---+ | 종료 |
|생성| |(Waiting)| |(Terminated)
+---+ +--------+ +--------+
- 새로 생성(New): 프로세스가 생성되는 중
- 준비(Ready): CPU 할당을 기다리는 상태
- 실행(Running): CPU에서 명령을 실행 중
- 대기(Waiting): I/O 또는 이벤트 완료를 기다리는 상태
- 종료(Terminated): 실행이 완료된 상태
프로세스 제어 블록 (PCB)
각 프로세스는 OS 내에서 PCB(Process Control Block)로 표현된다.
// Linux 커널의 task_struct 구조체 (간략화)
struct task_struct {
volatile long state; // 프로세스 상태
pid_t pid; // 프로세스 ID
pid_t tgid; // 스레드 그룹 ID
struct mm_struct *mm; // 메모리 관리 정보
struct files_struct *files; // 열린 파일 테이블
struct thread_struct thread; // CPU 레지스터 상태
unsigned int policy; // 스케줄링 정책
int prio; // 우선순위
cpumask_t cpus_allowed; // 실행 가능한 CPU
struct task_struct *parent; // 부모 프로세스
struct list_head children; // 자식 프로세스 목록
// ... 수백 개의 추가 필드
};
PCB에는 프로세스 상태, 프로그램 카운터, CPU 레지스터, 스케줄링 정보, 메모리 관리 정보, I/O 상태 정보 등이 포함된다.
프로세스 스케줄링
스케줄링 큐
OS는 여러 큐를 사용하여 프로세스를 관리한다.
[프로세스 스케줄링 큐]
준비 큐 (Ready Queue)
+-----+ +-----+ +-----+ +-----+
| PCB |->| PCB |->| PCB |->| PCB |---> CPU
+-----+ +-----+ +-----+ +-----+
디스크 대기 큐
+-----+ +-----+
| PCB |->| PCB |---> 디스크 컨트롤러
+-----+ +-----+
네트워크 대기 큐
+-----+
| PCB |---> 네트워크 인터페이스
+-----+
컨텍스트 스위치 (Context Switch)
CPU가 다른 프로세스로 전환할 때, 현재 프로세스의 상태를 저장하고 새 프로세스의 상태를 복원해야 한다.
[컨텍스트 스위치]
프로세스 P0 운영체제 프로세스 P1
실행 중 유휴
| |
|--인터럽트/시스템콜--> |
| PCB0에 상태 저장 |
| PCB1에서 상태 복원 |
| -------->|
유휴 실행 중 |
| |
| <--인터럽트/시스템콜-|
| PCB1에 상태 저장 |
| PCB0에서 상태 복원 |
|<--------- |
실행 중 유휴
컨텍스트 스위치 시간은 순수한 오버헤드다. 하드웨어에 따라 1~1000 마이크로초 정도 소요된다.
프로세스 연산
프로세스 생성: fork()
Unix/Linux에서 프로세스는 fork() 시스템 콜로 생성한다. fork()는 호출한 프로세스의 복사본을 만든다.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("fork() 호출 전 - PID: %d\n", getpid());
pid = fork(); // 자식 프로세스 생성
if (pid < 0) {
// fork 실패
perror("fork 실패");
return 1;
} else if (pid == 0) {
// 자식 프로세스: fork()가 0을 반환
printf("자식 프로세스 - PID: %d, 부모 PID: %d\n",
getpid(), getppid());
} else {
// 부모 프로세스: fork()가 자식의 PID를 반환
printf("부모 프로세스 - PID: %d, 자식 PID: %d\n",
getpid(), pid);
wait(NULL); // 자식 종료 대기
printf("자식 프로세스 종료됨\n");
}
return 0;
}
[fork() 동작]
부모 (PID 100) fork()
| |
| |---> 자식 (PID 101)
| | 부모의 주소 공간 복사
| | fork() 반환값 = 0
| fork() 반환값 = 101|
| |
fork()와 exec()의 조합
fork() 후 exec()을 호출하면 자식 프로세스가 새로운 프로그램을 실행한다.
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 자식: exec()으로 새 프로그램 실행
// exec()은 현재 프로세스 이미지를 새 프로그램으로 교체
printf("자식: ls 명령 실행\n");
execlp("ls", "ls", "-la", NULL);
// exec() 성공 시 아래 코드는 실행되지 않음
perror("exec 실패");
} else if (pid > 0) {
// 부모: 자식 종료 대기
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("자식 종료 코드: %d\n", WEXITSTATUS(status));
}
}
return 0;
}
프로세스 종료
프로세스는 exit() 시스템 콜로 종료를 요청한다. 부모 프로세스는 wait()로 자식의 종료 상태를 수집한다.
- 좀비 프로세스(Zombie): 종료되었지만 부모가 wait()를 호출하지 않은 프로세스
- 고아 프로세스(Orphan): 부모가 먼저 종료된 프로세스. init(PID 1)이 새 부모가 됨
프로세스 간 통신 (IPC)
프로세스는 독립적(independent)이거나 협력적(cooperating)일 수 있다. 협력적 프로세스는 IPC가 필요하다.
공유 메모리 (Shared Memory)
여러 프로세스가 동일한 메모리 영역에 접근하여 데이터를 교환한다.
// POSIX 공유 메모리 - 생산자
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096
int main() {
// 공유 메모리 객체 생성
int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
ftruncate(shm_fd, SHM_SIZE);
// 메모리 매핑
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, shm_fd, 0);
// 데이터 쓰기
const char *message = "Hello from producer!";
memcpy(ptr, message, strlen(message) + 1);
printf("생산자: 메시지 작성 완료\n");
munmap(ptr, SHM_SIZE);
return 0;
}
// POSIX 공유 메모리 - 소비자
#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>
#define SHM_NAME "/my_shm"
#define SHM_SIZE 4096
int main() {
// 기존 공유 메모리 열기
int shm_fd = shm_open(SHM_NAME, O_RDONLY, 0666);
// 메모리 매핑
char *ptr = mmap(NULL, SHM_SIZE,
PROT_READ,
MAP_SHARED, shm_fd, 0);
// 데이터 읽기
printf("소비자: %s\n", ptr);
munmap(ptr, SHM_SIZE);
shm_unlink(SHM_NAME); // 공유 메모리 삭제
return 0;
}
메시지 전달 (Message Passing)
OS 커널을 통해 프로세스 간 메시지를 주고받는다. 공유 메모리보다 느리지만 동기화가 내장되어 있다.
[IPC 모델 비교]
공유 메모리 모델: 메시지 전달 모델:
프로세스A 프로세스B 프로세스A 프로세스B
| \ / | | |
| \ / | | send(M) |
| 공유 메모리 | |---> 커널 |
| / \ | | | |
| / \ | | v |
| | | receive(M)|
| ----->|
커널 개입 최소화 커널이 매 전송마다 개입
파이프 (Pipes)
파이프는 두 프로세스 간의 통신 채널이다.
일반 파이프 (Ordinary Pipe)
부모-자식 관계의 프로세스 간에서만 사용 가능하며 단방향이다.
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main() {
int fd[2]; // fd[0]: 읽기 끝, fd[1]: 쓰기 끝
pid_t pid;
char buffer[256];
// 파이프 생성
if (pipe(fd) == -1) {
perror("파이프 생성 실패");
return 1;
}
pid = fork();
if (pid == 0) {
// 자식: 파이프에서 읽기
close(fd[1]); // 쓰기 끝 닫기
int bytes = read(fd[0], buffer, sizeof(buffer));
printf("자식이 받은 메시지: %s (%d bytes)\n", buffer, bytes);
close(fd[0]);
} else {
// 부모: 파이프에 쓰기
close(fd[0]); // 읽기 끝 닫기
const char *msg = "안녕하세요, 자식 프로세스!";
write(fd[1], msg, strlen(msg) + 1);
printf("부모가 보낸 메시지: %s\n", msg);
close(fd[1]);
wait(NULL);
}
return 0;
}
이름 있는 파이프 (Named Pipe / FIFO)
부모-자식 관계가 아닌 임의의 프로세스 간에도 사용 가능하다.
# 셸에서 이름 있는 파이프 사용
mkfifo /tmp/my_pipe
# 터미널 1: 쓰기
echo "Hello via named pipe" > /tmp/my_pipe
# 터미널 2: 읽기
cat /tmp/my_pipe
클라이언트-서버 통신
소켓 (Sockets)
소켓은 통신의 끝점(endpoint)이다. IP 주소와 포트 번호의 조합으로 식별된다.
// TCP 서버 예시
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);
char buffer[1024];
// 소켓 생성
server_fd = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(8080);
// 바인드 및 리슨
bind(server_fd, (struct sockaddr *)&server_addr,
sizeof(server_addr));
listen(server_fd, 5);
printf("서버 대기 중 (포트 8080)...\n");
// 클라이언트 연결 수락
client_fd = accept(server_fd,
(struct sockaddr *)&client_addr,
&addr_len);
// 데이터 수신 및 응답
int bytes = recv(client_fd, buffer, sizeof(buffer), 0);
buffer[bytes] = '\0';
printf("수신: %s\n", buffer);
const char *response = "서버 응답: 메시지 수신 완료";
send(client_fd, response, strlen(response), 0);
close(client_fd);
close(server_fd);
return 0;
}
원격 프로시저 호출 (RPC)
RPC는 네트워크를 통해 원격 시스템의 함수를 마치 로컬 함수처럼 호출하는 메커니즘이다.
[RPC 동작 과정]
클라이언트 서버
| |
|-- 1. 로컬 함수 호출 형태 -------- |
| (스텁이 매개변수 마셜링) |
| |
|-- 2. 네트워크를 통해 전송 -------->|
| |-- 3. 서버 스텁이
| | 언마셜링
| |
| |-- 4. 실제 함수 실행
| |
|<-- 5. 결과 반환 ------------------|
| (스텁이 결과 언마셜링) |
| |
RPC에서 주의할 점은 다음과 같다.
- 데이터 표현: 클라이언트와 서버의 데이터 형식이 다를 수 있으므로 XDR(External Data Representation) 같은 표준 형식 사용
- 바인딩: 클라이언트가 서버를 찾는 방법. 고정 포트 또는 랑데뷰 데몬(포트 매퍼) 사용
- 실행 의미론: "최대 한 번(at most once)" 또는 "정확히 한 번(exactly once)" 보장
정리
프로세스는 OS의 핵심 추상화 단위다. fork()와 exec()으로 프로세스를 생성하고 새 프로그램을 실행한다. 프로세스 간 통신에는 공유 메모리, 메시지 전달, 파이프, 소켓, RPC 등 다양한 메커니즘이 있으며, 각각 성능과 편의성 측면에서 장단점이 있다.