Skip to content

필사 모드: [운영체제] 03. 프로세스: 개념, 생성, 통신

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.
원문 렌더가 준비되기 전까지 텍스트 가이드로 표시합니다.

프로세스 개념

프로세스(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 등 다양한 메커니즘이 있으며, 각각 성능과 편의성 측면에서 장단점이 있다.

현재 단락 (1/269)

프로세스(Process)는 실행 중인 프로그램이다. 프로그램이 디스크에 저장된 수동적 존재라면, 프로세스는 메모리에 올라가 실행되는 능동적 존재다.

작성 글자: 0원문 글자: 6,243작성 단락: 0/269