[OSTEP] 프로세스 API

나우히즈·2025년 1월 4일

OS

목록 보기
23/27

fork, wait 동작 개요

  • fork()wait() 시스템 콜을 이용하여 부모와 자식 프로세스를 생성하고 동작시킵니다.
  • 실행 결과:
    • fork(): 새로운 자식 프로세스를 생성하며, 부모와 자식 프로세스는 동시에 실행됩니다.
    • wait(): 부모 프로세스가 자식 프로세스의 종료를 기다립니다.

8.1 fork() 시스템 콜

  1. 작동 방식:

    • fork()는 현재 프로세스를 복사하여 새로운 자식 프로세스를 생성합니다.
    • 부모와 자식은 같은 코드와 메모리를 복사하지만 독립적인 주소 공간, 레지스터, 프로그램 카운터(PC)를 가집니다.
  2. 차이점:

    • fork()는 반환 값으로 부모와 자식 간의 차이를 만듭니다:
      • 부모 프로세스: 자식의 PID를 반환받음.
      • 자식 프로세스: 항상 0을 반환받음.
  3. 결과:

    • 반환 값의 차이를 이용해 부모와 자식이 서로 다른 코드를 실행할 수 있음.

wait() 시스템 콜

  1. 역할:

    • 부모 프로세스는 wait()를 호출하여 자식 프로세스의 종료를 기다립니다.
    • 자식 프로세스가 종료되면 관련 자원을 해제하고, 자식의 종료 상태를 반환받습니다.
  2. 장점:

    • 부모가 자식의 종료를 확인할 수 있음.
    • 좀비 프로세스를 방지하며 시스템 자원을 효율적으로 관리.

출력 결과의 비결정성 (Nondeterminism)

  • 프로그램 실행 시 부모와 자식 프로세스의 실행 순서는 CPU 스케줄러에 의해 결정됩니다.
  • 스케줄러의 동작은 상황에 따라 달라지므로, 실행 순서는 매번 다를 수 있습니다:
    • 예시 1: 부모 → 자식 실행.
    • 예시 2: 자식 → 부모 실행.

8.2 wait() 시스템 콜

wait()의 역할

  • wait() 시스템 콜은 부모 프로세스가 자식 프로세스의 종료를 대기하게 만듭니다.
  • 부모는 자식 프로세스가 종료될 때까지 자신의 실행을 중단하고, 자식이 종료되면 wait()는 리턴하며 자식의 PID를 반환합니다.

코드 예제 (p2.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();

    if (rc < 0) {
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // 자식 프로세스
        printf("hello, I am child (pid:%d)\n", (int) getpid());
    } else { // 부모 프로세스
        int wc = wait(NULL); // 자식 프로세스의 종료 대기
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }

    return 0;
}

출력 결과

prompt> ./p2
hello world (pid:29266)
hello, I am child (pid:29267)
hello, I am parent of 29267 (wc:29267) (pid:29266)

분석

  1. 부모와 자식 프로세스의 생성:

    • fork() 시스템 콜로 부모와 자식 프로세스가 생성됩니다.
    • 부모는 rc에 자식의 PID를 반환받고, 자식은 rc == 0을 반환받습니다.
  2. wait() 호출로 인한 부모의 대기:

    • 부모 프로세스가 자식의 종료를 대기하기 위해 wait()를 호출합니다.
    • 자식 프로세스가 종료될 때까지 부모 프로세스는 실행을 멈추고 대기합니다.
  3. 출력 순서 결정:

    • 자식 프로세스는 독립적으로 실행되며 종료 전에 출력 작업을 완료합니다.
    • 부모 프로세스는 wait()가 리턴한 후 출력 작업을 수행하므로 항상 자식 출력이 부모 출력보다 먼저 나타납니다.
  4. wait()의 반환 값:

    • 자식 프로세스의 PID를 반환하며, 이를 통해 부모는 어떤 자식이 종료되었는지 알 수 있습니다.

결과가 항상 동일한 이유

  • wait()가 부모 프로세스의 실행을 자식 프로세스가 종료될 때까지 중단시키므로, 자식이 항상 먼저 실행을 완료하고 출력을 수행합니다.
  • 따라서 출력 순서는 다음과 같이 고정됩니다:
    1. hello world (부모와 자식 공통 출력)
    2. hello, I am child (자식 출력)
    3. hello, I am parent (부모 출력)

8.3 exec() 시스템 콜

exec()의 역할

  • exec() 시스템 콜은 현재 실행 중인 프로세스를 대체하여 새로운 프로그램을 실행합니다.
  • fork()와 달리 새 프로세스를 생성하지 않으며, 기존 프로세스의 코드, 데이터, 힙, 스택 등 주소 공간을 새로운 프로그램으로 대체합니다.

코드 예제 (p3.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc, char *argv[]) {
    printf("hello world (pid:%d)\n", (int) getpid());
    int rc = fork();

    if (rc < 0) { // fork 실패
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // 자식 프로세스
        printf("hello, I am child (pid:%d)\n", (int) getpid());

        // 실행할 프로그램과 인자 설정
        char *myargs[3];
        myargs[0] = strdup("wc");    // 실행할 프로그램: wc
        myargs[1] = strdup("p3.c"); // 인자: 파일 이름 p3.c
        myargs[2] = NULL;           // 인자의 끝 표시

        // execvp()로 현재 프로세스를 대체
        execvp(myargs[0], myargs);

        // execvp()가 성공하면 이 부분은 실행되지 않음
        printf("this shouldn't print out\n");
    } else { // 부모 프로세스
        int wc = wait(NULL); // 자식 프로세스의 종료 대기
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int) getpid());
    }

    return 0;
}

출력 결과

prompt> ./p3
hello world (pid:29383)
hello, I am child (pid:29384)
29 107 1030 p3.c
hello, I am parent of 29384 (wc:29384) (pid:29383)

작동 과정

  1. fork()로 자식 프로세스 생성:

    • 부모 프로세스가 fork()를 호출하여 자식 프로세스를 생성합니다.
  2. 자식 프로세스에서 exec() 호출:

    • 자식 프로세스는 execvp()를 호출하여 현재 프로세스를 wc 프로그램으로 대체합니다.
    • wc는 파일의 행 수, 단어 수, 바이트 수를 출력하는 프로그램입니다.
  3. 부모 프로세스에서 wait() 호출:

    • 부모 프로세스는 wait()를 호출하여 자식 프로세스가 종료될 때까지 대기합니다.
    • 자식 프로세스가 종료되면, 부모는 자식의 종료 상태를 확인하고 출력을 완료합니다.

exec()의 동작

  • 현재 프로세스를 대체:

    • exec() 호출 시 현재 프로세스의 메모리 공간은 새 프로그램으로 완전히 덮어씌워집니다.
    • 기존의 코드, 데이터, 힙, 스택 등은 모두 사라지고, 새 프로그램이 실행됩니다.
  • 프로세스 ID 유지:

    • exec()를 호출해도 프로세스 ID(PID)는 유지됩니다.
  • 리턴하지 않음:

    • exec()가 성공하면 호출한 함수로 다시 돌아오지 않습니다.
    • 따라서 "this shouldn't print out"라는 메시지는 절대 출력되지 않습니다.

결과가 항상 동일한 이유

  1. 자식 프로세스는 execvp()를 호출하여 wc 프로그램으로 대체되며, 바로 출력 작업을 수행합니다.
  2. 부모 프로세스는 wait()를 호출하여 자식 프로세스가 종료될 때까지 대기한 후 출력을 완료합니다.
  3. 실행 순서:
    • hello world (부모와 자식 공통)
    • 자식 프로세스가 wc 실행 결과 출력.
    • 부모 프로세스가 자식 종료 후 출력.

8.4 왜 fork()/exec() API를 사용하는가?

Unix 철학과 프로세스 API의 설계

  • Unix에서는 fork()exec()를 분리하여 프로세스를 생성하고 실행하도록 설계되었습니다.
  • 이 방식은 단순하지만 강력하며, Unix 쉘과 같은 프로그램에서 다양한 작업을 수행하는 데 유용합니다.

왜 fork()와 exec()를 분리하는가?

  1. 쉘의 역할:

    • 쉘은 사용자가 입력한 명령어를 실행하기 위해 새로운 프로세스를 생성하고 환경을 설정합니다.
    • fork()로 자식 프로세스를 생성한 뒤, exec() 호출 전에 추가 작업(환경 설정, 입출력 재지정 등)을 수행할 수 있습니다.
  2. 환경 설정의 유연성:

    • fork()exec()를 분리하면 쉘이 다음과 같은 작업을 쉽게 수행할 수 있습니다:
      • 표준 입출력 재지정: 예, wc p3.c > newfile.txt에서 > newfile.txt를 처리.
      • 환경 변수 설정: 자식 프로세스 실행 전 특정 환경을 설정.
      • 자식 프로세스의 우선순위 조정.
  3. 입출력 재지정 예제:

    • p4.c에서 자식 프로세스는 표준 출력을 닫고 새 파일에 연결합니다.
    • 자식 프로세스의 모든 출력은 새 파일에 기록됩니다.

코드 예제 (p4.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
    int rc = fork();

    if (rc < 0) { // fork 실패
        fprintf(stderr, "fork failed\n");
        exit(1);
    } else if (rc == 0) { // 자식 프로세스
        close(STDOUT_FILENO); // 표준 출력 닫기
        open("./p4.output", O_CREAT | O_WRONLY | O_TRUNC, S_IRWXU); // 새 파일로 출력 연결

        // 실행할 프로그램과 인자 설정
        char *myargs[3];
        myargs[0] = strdup("wc");    // 실행할 프로그램: wc
        myargs[1] = strdup("p4.c"); // 인자: 파일 이름 p4.c
        myargs[2] = NULL;           // 인자의 끝 표시

        execvp(myargs[0], myargs); // execvp()로 현재 프로세스를 wc로 대체
    } else { // 부모 프로세스
        int wc = wait(NULL); // 자식 프로세스 종료 대기
    }

    return 0;
}

출력 결과

prompt> ./p4
prompt> cat p4.output
32 109 846 p4.c

실행 과정

  1. fork()로 자식 프로세스 생성:

    • 부모는 자식 프로세스를 생성하여 독립적인 실행 환경을 제공합니다.
  2. 자식 프로세스에서 입출력 재지정:

    • 자식 프로세스는 close(STDOUT_FILENO)로 표준 출력을 닫고, open()을 통해 새 파일을 열어 연결합니다.
  3. execvp()로 프로그램 실행:

    • execvp()wc 프로그램으로 자식 프로세스를 대체합니다.
    • 자식 프로세스는 새 파일로 출력 내용을 기록합니다.
  4. 부모 프로세스 대기:

    • 부모는 wait()로 자식 프로세스 종료를 대기하며, 이후 다음 작업을 수행합니다.

왜 fork()/exec() 조합이 강력한가?

  • 유연성: exec() 호출 전에 자식 프로세스에서 다양한 작업(환경 설정, 우선순위 변경, 입출력 재지정 등)을 수행할 수 있습니다.
  • 단순함: 분리된 구조가 설계를 단순하게 하고 오류를 줄입니다.
  • 쉘과의 통합: Unix 쉘은 이 API를 통해 사용자가 입력한 명령어를 처리하고 실행합니다.

Unix 철학의 적용

  • 단순하면서 강력한 설계:
    • Lampson’s Law에 따르면, "올바르게 선택된 설계는 단순함과 추상화를 대체할 수 없다."
    • Unix의 fork()exec() 조합은 단순하고 효율적이면서도 다양한 작업을 지원합니다.

8.5 여타 API들

1. 프로세스 관련 주요 API

  • kill():

    • 프로세스에게 시그널(signal)을 보내기 위한 시스템 콜.
    • 시그널을 통해 프로세스를 중단, 종료, 재개하거나 특정 작업을 수행하도록 알림.
    • 예시:
      • kill -9 <PID>: 특정 PID의 프로세스를 강제 종료(SIGKILL).
      • kill -STOP <PID>: 특정 프로세스를 일시 정지(SIGSTOP).
      • kill -CONT <PID>: 정지된 프로세스를 재개(SIGCONT).
  • 시그널(signal):

    • 외부 사건을 프로세스에게 알리는 운영체제의 메커니즘.
    • 주요 시그널:
      • SIGINT: 인터럽트 요청(Ctrl+C).
      • SIGTERM: 정상 종료 요청.
      • SIGKILL: 강제 종료(취소 불가).
      • SIGSEGV: 잘못된 메모리 접근(segmentation fault).

2. 유용한 명령어

  • ps:

    • 현재 실행 중인 프로세스를 보여주는 명령어.
    • 유용한 플래그:
      • ps -e: 모든 프로세스를 표시.
      • ps -f: 프로세스의 세부 정보를 포맷팅하여 표시.
      • ps aux: 실행 중인 모든 프로세스의 상세 정보를 표시.
  • top:

    • 시스템의 실시간 프로세스 정보와 CPU, 메모리 사용량을 모니터링.
    • 특정 프로세스가 시스템 자원을 얼마나 소비하는지 확인 가능.
    • 명령어 자체가 실행 중일 때 자기 자신이 가장 많은 자원을 사용하는 경우가 있음.
  • 파일 기반 모니터링:

    • /proc 파일 시스템:
      • Linux에서 프로세스와 시스템 정보를 제공하는 가상 파일 시스템.
      • /proc/<PID>: 특정 프로세스의 상태와 자원 사용 정보.
      • /proc/stat: 시스템의 CPU 및 메모리 사용 상태.

3. 시스템 부하 및 자원 모니터링 도구

  • MenuMeter (Macintosh):

    • 툴바에 CPU, 메모리, 네트워크, 디스크 I/O 사용률을 표시.
    • 시스템 부하를 실시간으로 확인 가능.
  • Linux의 uptime:

    • 시스템의 부팅 시간, 사용자 수, 로드 평균(1, 5, 15분 평균 부하)를 표시.
  • vmstat:

    • CPU, 메모리, 디스크 I/O와 같은 시스템 성능 메트릭을 표시.
    • 주기적인 리소스 소비 동향을 확인 가능.

4. 활용 방안

  • 프로세스 관리:

    • ps, top, /proc를 통해 프로세스 상태와 자원 사용량을 모니터링.
    • kill과 시그널로 특정 프로세스를 제어하거나 중단.
  • 시스템 부하 분석:

    • topvmstat로 CPU 및 메모리 사용량을 점검.
    • 시스템 성능을 최적화하기 위해 높은 부하를 유발하는 프로세스를 분석 및 종료.
  • 운영체제 학습 및 디버깅:

    • 시그널 및 프로세스 API의 동작을 이해하고 활용하여 Unix/Linux 시스템의 동작을 탐구.

0개의 댓글