OSTEP 05. Process API

이번 글은 OSTEP 5장 Process API를 공부하며 정리한 내용입니다. 이전 장에서 프로세스가 무엇인지 봤다면, 이번 장에서는 사용자가 운영체제에 어떤 요청을 보내 프로세스를 만들고 제어하는지 살펴봅니다.

한 문장 요약

Unix는 fork(), exec(), wait()를 조합해 프로세스를 만들고 제어하며, 이 API가 분리되어 있기 때문에 셸은 실행 직전에 입출력 재지정이나 파이프 같은 환경 설정을 할 수 있습니다.

핵심 질문

질문 핵심 답변
운영체제는 프로세스 생성을 위해 어떤 인터페이스를 제공하는가? Unix는 새 프로세스를 복제하는 fork(), 자식 종료를 기다리는 wait()/waitpid(), 현재 프로세스 이미지를 다른 프로그램으로 바꾸는 exec() 계열 호출을 제공합니다.
fork()는 왜 특이한가? 한 번 호출되지만 부모와 자식 두 프로세스에서 각각 반환됩니다. 부모는 자식 PID를 받고, 자식은 0을 받기 때문에 같은 코드에서 서로 다른 분기를 실행할 수 있습니다.
부모가 자식을 기다리려면 어떻게 하는가? wait() 또는 waitpid()를 호출해 자식이 종료될 때까지 대기합니다. 이 호출은 종료된 자식의 PID와 종료 상태를 회수하는 역할도 합니다.
다른 프로그램을 실행하려면 어떻게 하는가? 자식 프로세스에서 exec() 계열 함수를 호출합니다. 성공하면 현재 프로세스의 코드와 데이터가 새 프로그램으로 교체되며, 기존 프로그램 흐름으로 돌아오지 않습니다.
fork()exec()을 분리했는가? 분리 덕분에 부모 또는 자식은 exec() 전에 파일 디스크립터, 환경 변수, 파이프, 표준 입출력 등을 설정할 수 있습니다. 이것이 Unix 셸의 명령 실행, 입출력 재지정, 파이프 구현의 기반입니다.
실행 순서는 항상 같은가? fork() 이후 부모와 자식 중 누가 먼저 실행될지는 스케줄러 결정에 따라 달라집니다. wait()처럼 명시적으로 순서를 강제하지 않으면 출력 순서도 비결정적일 수 있습니다.

fork()

fork()는 현재 프로세스를 복사해 자식 프로세스를 만듭니다.

위치 반환값 의미
부모 프로세스 자식 PID 부모는 어떤 자식을 만들었는지 알 수 있습니다.
자식 프로세스 0 자식은 자신이 자식 경로를 실행해야 함을 알 수 있습니다.
실패 -1 새 프로세스를 만들지 못했습니다.

fork() 직후 부모와 자식은 같은 코드 위치에서 실행을 이어가지만, 각자 독립된 주소 공간과 레지스터 상태를 가집니다. 따라서 같은 변수 이름을 사용해도 부모와 자식의 변경은 서로의 메모리에 직접 영향을 주지 않습니다.

실행 예제: code/ostep/process-api/p1_fork.c

비결정적 실행 순서

fork() 후에는 부모와 자식이 모두 실행 가능한 상태가 됩니다. 어느 쪽이 먼저 CPU를 받을지는 스케줄러가 결정합니다.

  1. 부모 프로세스가 fork()를 호출합니다.
  2. 부모 경로에서는 fork()가 자식 PID를 반환합니다.
  3. 자식 경로에서는 fork()가 0을 반환합니다.
  4. 스케줄러가 부모와 자식 중 다음에 실행할 프로세스를 선택합니다.
  5. 따라서 부모가 먼저 실행될 수도 있고, 자식이 먼저 실행될 수도 있습니다.

wait()가 없으면 출력 순서는 실행할 때마다 달라질 수 있습니다. 운영체제 관점에서는 둘 다 준비 상태의 프로세스이며, 누가 먼저 실행되는지는 정책의 결과입니다.

wait()waitpid()

부모가 자식의 종료를 기다려야 한다면 wait() 또는 waitpid()를 사용합니다.

호출 용도
wait(&status) 종료된 자식 하나를 기다립니다.
waitpid(pid, &status, options) 특정 자식이나 조건을 지정해 기다립니다.

wait()는 단순히 순서를 맞추는 도구가 아닙니다. 자식의 종료 상태를 회수하고, 운영체제가 자식 프로세스에 남아 있던 자원을 정리할 수 있게 합니다.

실행 예제: code/ostep/process-api/p2_wait.c

exec() 계열

exec()는 새 프로세스를 만드는 호출이 아닙니다. 현재 프로세스의 프로그램 이미지를 다른 프로그램으로 바꿉니다.

구분 설명
호출 전 현재 프로세스는 기존 프로그램 코드를 실행 중입니다.
호출 성공 코드, 정적 데이터, 힙, 스택이 새 프로그램 기준으로 바뀝니다.
호출 후 성공한 exec()는 기존 코드로 반환하지 않습니다.
호출 실패 -1을 반환하고 errno가 설정됩니다.

실행 예제: code/ostep/process-api/p3_exec_wc.c

셸이 명령을 실행하는 방식

Unix 셸은 대체로 다음 순서로 명령을 실행합니다.

  1. 셸이 fork()로 자식 프로세스를 만듭니다.
  2. 자식 프로세스가 입출력 재지정, 파이프, 환경 변수 등을 설정합니다.
  3. 자식 프로세스가 exec()로 실제 프로그램을 실행합니다.
  4. 부모인 셸은 필요하면 wait() 또는 waitpid()로 자식 종료를 기다립니다.
  5. 프로그램이 종료되면 셸은 종료 상태를 회수하고 다음 명령을 받을 준비를 합니다.

fork()exec()이 분리되어 있기 때문에 자식은 새 프로그램으로 바뀌기 직전에 표준 출력, 표준 입력, 파이프 등을 원하는 형태로 바꿀 수 있습니다.

입출력 재지정

셸에서 다음 명령을 실행한다고 생각해 봅니다.

핵심 아이디어는 자식 프로세스가 exec()를 호출하기 전에 표준 출력을 파일로 바꾸는 것입니다.

이후 새 프로그램이 STDOUT_FILENO에 쓰는 모든 출력은 터미널이 아니라 파일로 갑니다.

실행 예제: code/ostep/process-api/p4_redirect.c

기타 프로세스 API

도구 역할
kill() 프로세스에 시그널을 보냅니다. 이름과 달리 종료뿐 아니라 다양한 이벤트 전달에 사용됩니다.
ps 현재 실행 중인 프로세스 목록을 봅니다.
top 프로세스별 CPU, 메모리 사용량을 실시간으로 봅니다.
man 시스템 콜과 라이브러리 호출의 정확한 인자, 반환값, 오류 조건을 확인합니다.

시스템 프로그래밍에서는 반환값과 오류 조건을 정확히 알아야 하므로 man fork, man execvp, man waitpid처럼 매뉴얼을 확인하는 습관이 중요합니다.

정리

fork()는 현재 프로세스를 복제하고, exec()는 현재 프로세스를 새 프로그램으로 바꾸며, wait()는 부모가 자식의 종료를 기다리고 정리하게 합니다.

세 호출을 분리한 설계는 처음에는 이상해 보이지만, 셸이 명령 실행 직전에 입출력 재지정, 파이프, 환경 설정을 할 수 있게 만드는 강력한 구조입니다. Unix 프로세스 API의 핵심은 이 조합을 이해하는 데 있습니다.

이 글은 카테고리: Operating System에 포함되어 있으며 태그: , , , , , (이)가 사용되었습니다. 고유주소를 북마크하세요.

댓글 남기기