Process Control: 두 판 사이의 차이

youngwiki
 
(같은 사용자의 중간 판 하나는 보이지 않습니다)
103번째 줄: 103번째 줄:
또한 인자로 전달한 &status가 가리키는 데이터인 status는 wait.h 헤더 파일에 저장된 여러 메크로를 사용하여 해석될 수 있다. 아래는 그 목록이다.  
또한 인자로 전달한 &status가 가리키는 데이터인 status는 wait.h 헤더 파일에 저장된 여러 메크로를 사용하여 해석될 수 있다. 아래는 그 목록이다.  
* WIFEXITED(status): 자식이 정상적으로 종료되었으면 참을 반환한다. (exit 또는 return 호출로 종료된 경우)
* WIFEXITED(status): 자식이 정상적으로 종료되었으면 참을 반환한다. (exit 또는 return 호출로 종료된 경우)
* WEXITSTATUS(status): 정상 종료된 자식의 종료 상태를 반환한다. 이는 WIFEXITED()가 참을 반환한 경우에만 정의됩니다.
* WEXITSTATUS(status): 정상 종료된 자식의 종료 상태를 반환한다. 이는 WIFEXITED가 참을 반환한 경우에만 정의된다.
* WIFSIGNALED(status): 자식 프로세스가 잡히지 않은 신호로 인해 종료되었으면 참을 반환한다.
* WIFSIGNALED(status): 자식 프로세스가 잡히지 않은 신호로 인해 종료되었으면 참을 반환한다.
* WTERMSIG(status): 자식 프로세스를 종료시킨 signal number를 반환한다. 이는 WIFSIGNALED()가 참을 반환한 경우에만 정의된다.
* WTERMSIG(status): 자식 프로세스를 종료시킨 signal number를 반환한다. 이는 WIFSIGNALED()가 참을 반환한 경우에만 정의된다.
* WIFSTOPPED(status): 자식 프로세스가 현재 정지 상태이면 참을 반환합니다.
* WIFSTOPPED(status): 자식 프로세스가 현재 정지 상태이면 참을 반환합니다.
* WSTOPSIG(status): 자식 프로세스를 정지시킨 signal number를 반환합니다. 이는 WIFSTOPPED()가 참을 반환한 경우에만 정의됩니다.
* WSTOPSIG(status): 자식 프로세스를 정지시킨 signal number를 반환합니다. 이는 WIFSTOPPED()가 참을 반환한 경우에만 정의된다.
* WIFCONTINUED(status): 자식 프로세스가 SIGCONT 신호를 받아 다시 시작되었으면 참을 반환합니다.
* WIFCONTINUED(status): 자식 프로세스가 SIGCONT 신호를 받아 다시 시작되었으면 참을 반환한다.


====예시====
====예시====
177번째 줄: 177번째 줄:
execve 함수는 현재 프로세스의 컨텍스트에서 새로운 프로그램을 로드하고 실행한다. 이때 호출한 프로세스를 새로운 프로그램으로 완전히 덮어 씌우기 때문에 기존 프로그램은 완전히 사라지고 원래 코드로 반환될 수 없다. 이때 문에 그 결과가 오류가 생기지 않는 이상 반환되지 않는 것이 특징이다.<ref>실패하면 -1을 반환한다.</ref> 이 때문에 code, data, stack을 덮어써 기존 프로세스의 메모리 공간이 새로운 프로그램으로 완전히 대체된다.  하지만 새로 실행된 프로그램의 PID와 open file decriptor, signal context는 유지된다.  
execve 함수는 현재 프로세스의 컨텍스트에서 새로운 프로그램을 로드하고 실행한다. 이때 호출한 프로세스를 새로운 프로그램으로 완전히 덮어 씌우기 때문에 기존 프로그램은 완전히 사라지고 원래 코드로 반환될 수 없다. 이때 문에 그 결과가 오류가 생기지 않는 이상 반환되지 않는 것이 특징이다.<ref>실패하면 -1을 반환한다.</ref> 이 때문에 code, data, stack을 덮어써 기존 프로세스의 메모리 공간이 새로운 프로그램으로 완전히 대체된다.  하지만 새로 실행된 프로그램의 PID와 open file decriptor, signal context는 유지된다.  
<syntaxhighlight lang = "c">
<syntaxhighlight lang = "c">
int execve(const char *filename, const char *argv[], const char *envp[]);
int execve(const char *filename, const char *argv[], const char *envp[]); //반환값이 없음, 오류가 있을 때만 반환
</syntaxhighlight>
</syntaxhighlight>
* [[파일:StructureOfStack.png|섬네일|300x300픽셀]]filename: 실행할 프로그램의 경로
* [[파일:StructureOfStack.png|섬네일|300x300픽셀]]filename: 실행할 프로그램의 경로

2025년 4월 21일 (월) 14:30 기준 최신판

상위 문서: Process

개요

해당 문서는 Process(프로세스)를 fork()함수를 통해 생성하고 관리하는 방법에 대해서 서술한다.

Process ID

Process ID(PID)란 각각의 프로세스가 가지는 고유한 음수가 아닌 숫자를 의미한다. PID를 얻는 방법은 다음 두가지가 있다.

pid_t getpid(void);   //현재 실행 중인 프로세스의 PID를 반환
pid_t getppid(void);  //현재 실행 중인 프로세스를 생성한 부모 프로세스의 PID를 반환

위에서 pid_t 자료형은 정수로 저장되고 PID를 저장하는 특수한 자료형이다.

Process의 세가지 상태

프로세스를 다음 세가지 상태 중 하나로 구분할 수 있다.

  1. Running: CPU에 의해 실행되고 있거나 실행되기를 기다리고 있는 상태이며 결국 커널에 의해서 스케쥴링 된다.
  2. Stopped: 프로세스의 실행이 suspend 되었으며 추가적인 시그널을 받기 전까지 스케쥴링되지 않음
  3. Terminated: 프로세스가 영구적으로 정리되어 리소스의 정리가 필요함
    • 다음 세가지 이유 중 하나로 종료됨
      1. default action이 terminate인 signal을 받았을 때
      2. main 루틴에서 return을 할 때 -> main에서 0을 반환하여 명시적으로 종료
      3. exit(): terminate 상태로 프로세스를 종료
        • 정상적인 종료 상태는 0이며, 에러가 일어나면 0이 아니다.

Creating Process

parent 프로세스는 fork() 함수를 호출하여 새롭게 실행하는 child 프로세스를 생성할 수 있다.[1]

pid_t fork(void) //자식 프로세스에는 0을 반환하고, 부모 프로세스에는 child의 PID를 반환

fork() 함수는 한 번 호출되지만 각각의 프로세스에서 따로 반환하므로 두번 반환된다. 이때, child 프로세스는 parent 프로세스의 가상 메모리 공간을 복사한다. 이를 통해서 부모 프로세스가 fork를 호출할 당시 열려있던 파일에 동일하게 접근 가능하다. 이때 부모 자식 프로세스의 실행은 커널에 의해 진행되므로 명령어의 실제적인 실행은 섞일 수 있다. 따라서 프로그래머들은 서로 다른 프로세스에서 명령어들이 어떤 순서로 실행될 지에 대해서 확신할 수 없다. 아래는 프로세스를 만드는 프로그램의 예시와 이를 쉘에서 실행한 결과이다.

int main() {
    pid_t pid;
    int x = 1;

    pid = Fork();
    if (pid == 0) { /* Child */
        printf("child : x=%d\n", ++x);
        exit(0);
    }
    /* Parent */
    printf("parent: x=%d\n", --x);
    exit(0);
}
linux> ./fork
parent: x=0
child : x=2

이를 통해서 다음과 같은 사실들을 도출할 수 있다.

  1. Call Once, Return Twice: pid의 반환값이 부모 프로세스와 자식 프로세스가 다르므로 서로 다른 실행과정을 거친다
  2. Duplicate but seperate address spaces: 변수 x의 값은 처음에는 같으나 독립적으로 존재하여 최종적으로는 달라진다.
  3. Shared files: 두 프로세스 모두 실행 결과가 screen에 표시되며, 이는 자식 프로세스가 부모의 파일에 접근 가능하기 때문이다.(stdout이 동일함)

Process Graph

Process Graph는 concurrent한 프로그램에서의 statements의 partial ordering을 나타내는 유용한 도구이다. Process Graph의 규칙은 다음과 같다.

  1. 각각의 vertex는 statement의 실행을 의미함
  2. a -> b는 b전에 a가 발생했음을 의미함
  3. edge는 변수의 현재 상태를 나타낼 수 있음
  4. printf vertex의 경우 해당 출력값을 나타낼 수 있음

Single CPU 기준으로 해당 process graph의 모든 정점에 대한 모든 위상 정렬[2]은 프로그램 문장들의 실행 가능한 전체 순서를 의미한다.

Reaping Child Processes

프로세스가 terminate(종료)될 때 해당 프로세스는 이후에도 여전히 system resource를 소모한다. 이때 해당 프로세스가 처리되지 않으며 이를 zombie라고 한다. 이런 상태는 부모 프로세스가 이를 처리하기 전까지 유지된다. 이는 다음과 같은 과정으로 진행된다.

  1. 부모가 종료된 자식을 wait()이나 waitpid()함수를 이용하여 처리한다.
  2. 커널은 자식 프로세스의 종료 상태[3]를 부모 프로세스에게 전달한다.
  3. 커널은 해당 프로세스를 시스템에서 삭제해 해당 zombie는 없어진다.

혹은 자식 프로세스가 종료하기 전에 부모 프로세스가 먼저 종료될 때가 있다. 이러한 자식 프로세스를 orphaned child라고 한다. 이 경우에는 커널이 init 프로세스로 하여금 orphaned child를 처리하게 한다. init 프로세스의 PID는 1이며, 이는 init 프로세스가 모든 프로세스의 부모 프로세스임을 의미한다.

따라서 종료되지 않는 프로세스의 자식 프로세스가 좀비 상태가 된다면, 부모 프로세스는 종료되지 않으므로 해당 좀비 프로세스는 삭제되지 않는다. 따라서 쉘이나 서버와 같은 장기 실행 프로세스의 경우는 항상 자신의 좀비 프로세스를 처리해야 한다.

wait()함수와 waitpid()함수

먼저 wait()함수가 호출되면 해당 프로세스는 자식 프로세스가 종료될 때까지 대기한다. 자식 프로세스가 종료되면 부모 프로세스는 좀비가 된 자식프로세스를 처리한다. 만약 자식 프로세스가 이미 종료되어 있다면, wait()함수는 즉시 종료된다.

pid_t wait(int &status); //반환값은 종료된 자식의 PID이다. 만약 오류가 발생 -> -1을 반환, errno를 설정

인자는 정수형 포인터인 &status이며, 자식 프로세스의 종료 상태가 저장된다. &status = NULL일 경우 종료 상태를 받지 않는다.
waitpid()함수는 wait()함수의 확장된 버전으로 더 많은 옵션을 제공한다. waitpid()는 부모 프로세스가 지정한 pid에 해당하는 자식 프로세스의 종료를 기다린다. 지정된 pid에 해당하는 자식 프로세스가 종료된 이후의 동작은 wait()함수와 비슷하다.

pid_t waitpid(pid_t pid, int *status, int options); //반환값은 종료된 자식의 PID이다. 만약 오류가 발생 -> -1을 반환, errno를 설정

위 함수의 인자는 다음과 같다.

  • pid: 대기할 자식 프로세스의 PID이다. pid가 -1로 지정되면 대기 집합은 부모의 모든 자식 프로세스들로 구성된다.
  • status: 자식 프로세스의 종료 상태를 저장할 포인터이다. status = NULL일 경우 종료 상태를 받지 않는다.
  • options: 대기 동작을 수정하는 옵션들이며 여러 옵션을 조합하여 사용할 수 있다. 해당 인자에는 다음 상수들이 사용된다.
    • WNOHANG: 대기 집합에 있는 자식 프로세스가 아직 종료되지 않았다면 즉시 0을 반환한다. 자식 프로세스가 이미 종료되었다면 종료 상태를 반환한다. 이 옵션은 자식이 종료되기를 기다리는 동안 해당 프로세스에서 다른 유용한 작업을 계속하고 싶을 때 유용합니다.
    • WUNTRACED: 호출한 프로세스를 대기 집합에 있는 프로세스가 종료되거나 정지될 때까지 일시 중지한다. 이를 통해서 종료된 자식과 정지된 자식 모두를 확인하고 싶을 때 유용하다.
    • WCONTINUED: 호출한 프로세스를 대기 집합에 있는 실행 중인 프로세스가 종료되거나, 정지된 프로세스가 SIGCONT 신호를 받아 다시 실행될 때까지 일시 중지합니다.
    • 위 옵션 상수들은 OR 연산자를 통해 결합할 수 있다. 아래는 그 예시이다.
      • WNOHANG | WUNTRACED: 대기 집합에 있는 자식 프로세스가 종료되거나 정지되지 않았다면 즉시 반환하고, 반환 값은 0이다. 그렇지 않으면 종료되거나 정지된 자식의 PID를 반환한다.

또한 반환값은 종료된 자식 프로세스의 PID이다. 만약 오류가 발생할 경우 -1을 반환하고 errno를 설정한다. 또한 인자로 전달한 &status가 가리키는 데이터인 status는 wait.h 헤더 파일에 저장된 여러 메크로를 사용하여 해석될 수 있다. 아래는 그 목록이다.

  • WIFEXITED(status): 자식이 정상적으로 종료되었으면 참을 반환한다. (exit 또는 return 호출로 종료된 경우)
  • WEXITSTATUS(status): 정상 종료된 자식의 종료 상태를 반환한다. 이는 WIFEXITED가 참을 반환한 경우에만 정의된다.
  • WIFSIGNALED(status): 자식 프로세스가 잡히지 않은 신호로 인해 종료되었으면 참을 반환한다.
  • WTERMSIG(status): 자식 프로세스를 종료시킨 signal number를 반환한다. 이는 WIFSIGNALED()가 참을 반환한 경우에만 정의된다.
  • WIFSTOPPED(status): 자식 프로세스가 현재 정지 상태이면 참을 반환합니다.
  • WSTOPSIG(status): 자식 프로세스를 정지시킨 signal number를 반환합니다. 이는 WIFSTOPPED()가 참을 반환한 경우에만 정의된다.
  • WIFCONTINUED(status): 자식 프로세스가 SIGCONT 신호를 받아 다시 시작되었으면 참을 반환한다.

예시

다음은 wait()함수의 예시이다.

void fork9() {
    int child_status;
    if (fork() == 0) {
        printf("HC: hello from child\n");
        exit(0); 
    } else {
        printf("HP: hello from parent\n"); wait(&child_status);
        printf("CT: child has terminated\n");
    }
    printf("Bye\n"); 
}

다음은 wait()함수와 waitpid()함수의 차이를 보여주는 예시이다.

void fork10() {
    pid_t pid[N];
    int i, child_status;

    for (i = 0; i < N; i++) {
        if ((pid[i] = fork()) == 0) {  // 자식 프로세스
            exit(100 + i);  // 자식 프로세스 종료 상태 설정
        }
    }

    // 부모 프로세스
    for (i = 0; i < N; i++) {
        pid_t wpid = wait(&child_status);  // 자식 프로세스 종료 대기
        if (WIFEXITED(child_status)) { //자식이 정상적으로 종료되었다면 참을 반환
            printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
        } else {  //자식이 비정상적으로 종료된 경우
            printf("Child %d terminated abnormally\n", wpid);
        }
    }
}
void fork11() {
    pid_t pid[N];  // 자식 프로세스의 PID를 저장할 배열
    int i, child_status;

    for (i = 0; i < N; i++) {
        if ((pid[i] = fork()) == 0) {  // 자식 프로세스
            exit(100 + i);  // 자식 프로세스 종료 상태 설정
        }
    }

    // 부모 프로세스: 자식의 종료를 역순으로 기다림
    for (i = N - 1; i >= 0; i--) {
        pid_t wpid = waitpid(pid[i], &child_status, 0);  // 자식 프로세스 종료 대기

        if (WIFEXITED(child_status)) { //자식이 정상적으로 종료되었다면 참을 반환
            printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
        } else { //자식이 비정상적으로 종료된 경우
            printf("Child %d terminated abnormally\n", wpid);
        }
    }
}


execve 함수

execve 함수는 현재 프로세스의 컨텍스트에서 새로운 프로그램을 로드하고 실행한다. 이때 호출한 프로세스를 새로운 프로그램으로 완전히 덮어 씌우기 때문에 기존 프로그램은 완전히 사라지고 원래 코드로 반환될 수 없다. 이때 문에 그 결과가 오류가 생기지 않는 이상 반환되지 않는 것이 특징이다.[4] 이 때문에 code, data, stack을 덮어써 기존 프로세스의 메모리 공간이 새로운 프로그램으로 완전히 대체된다. 하지만 새로 실행된 프로그램의 PID와 open file decriptor, signal context는 유지된다.

int execve(const char *filename, const char *argv[], const char *envp[]); //반환값이 없음, 오류가 있을 때만 반환
  • filename: 실행할 프로그램의 경로
    • 실행 가능한 바이너리 파일[5](예: /bin/ls) 또는 스크립트 파일의 경로를 지정함.
    • 경로는 절대 경로(예: /bin/ls) 또는 상대 경로(예: ./my_program)로 지정할 수 있음.
    • 파일이 존재하지 않거나 실행 권한이 없으면 execve는 실패(-1 반환)함.
  • argv[]: 프로그램의 인수 목록
    • argv는 널(NULL)로 끝나는 문자열 포인터 배열이다.
    • argv[0]은 실행될 프로그램의 이름(일반적으로 filename)을 넣는 것이 관례이며, argv[1], argv[2], ... 에 실제 인수를 배치한다.
  • envp[]: 환경 변수 목록
    • envp는 널(NULL)로 끝나는 문자열 포인터 배열로, 각 요소는 "NAME=VALUE" 형식의 환경 변수를 포함한다.
    • 환경 변수는 실행되는 프로그램이 사용할 설정 값들을 지정하는데 사용된다.
    • envp를 NULL로 설정하면 현재 프로세스의 환경 변수를 그대로 상속한다.

다음은 child 프로세스 내에서 “/bin/ls –lt /usr/include”파일을 실행하는 execve()함수의 예시이다.

if ((pid = Fork()) == 0) { /* Child runs program */ 
    if (execve(myargv[0], myargv, environ) < 0) {
        printf("%s: Command not found.\n", myargv[0]);
        exit(1); 
    }
}

각주

  1. child 프로세스의 PID는 parent 프로세스의 PID를 반환한다.
  2. 위상 정렬은 다음 조건들을 만족한다.
    1. 프로세스 그래프의 정점들을 임의의 순서로 나열
    2. 왼쪽에서 오른쪽으로 정점들을 배치한 후, 간선(directed edge)을 그림
    3. 모든 간선이 왼쪽에서 오른쪽으로 향하면, 해당 정점 나열 순서가 위상 정렬
  3. 프로세스가 종료될 때 운영 체제가 반환하는 값으로, 프로세스의 종료 상태나 실행 결과를 나타낸다.
  4. 실패하면 -1을 반환한다.
  5. 사람이 읽을 수 있는 텍스트가 아닌, 0과 1(바이너리)로 이루어진 파일이며, 컴퓨터가 직접 실행하거나 해석할 수 있는 실행 파일을 의미한다.