[Linux] 프로세스 생성/교체/실행/대기/종료

mommers·2026년 2월 4일

Linux

목록 보기
40/59


프로세스 생성/교체/종료 대기/종료

1. 프로세스 생성 (fork)

기존 프로세스(부모)를 복제하여 새로운 프로세스(자식)를 만들 때, 부모의 코드, 데이터, 힙, 스택을 복사합니다. (실제로는 COW : Copy On Write 기법으로 처음에는 공유만 하다가 수정 시 복사) 자식은 부모의 Pending Signal이나 File Lock은 상속받지 않습니다.

  • 부모: 자식의 PID 반환 | 자식: 0 반환
  • 파일 디스크립터(FD)도 복사되므로, open된 파일을 부모/자식이 공유하게 됩니다 (lseek 위치 공유).

1-1) 간단한 fork 예제

fork()의 핵심 개념인 복제, 동시 실행, 메모리 독립

  1. PID 구분: fork()의 리턴값으로 내가 부모인지 자식인지 판단한다.
  2. 동시성(Concurrency): 두 프로세스가 동시에(번갈아 가며) 실행됨을 눈으로 확인한다.
  3. 메모리 독립성: 자식에서 변수를 바꿔도 부모 변수는 바뀌지 않음을 확인한다.

예제 코드 (race.c)

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

// 전역 변수 (메모리 독립성 확인용)
int g_score = 0;

int main() {
    printf("=== [Start] 경기를 시작합니다 (PID: %d) ===\n", getpid());

    // 1. 여기서 프로세스가 복제됨 (세포 분열)
    pid_t pid = fork();

    // 2. 분기 처리 (복제된 직후)
    if (pid < 0) {
        perror("Fork 실패");
        return 1;
    }

    if (pid == 0) {
        // [자식 프로세스 영역]
        // 자식은 pid 변수에 0을 받음
        for (int i = 0; i < 5; i++) {
            g_score += 10; // 점수 증가
            printf("🐥 자식: 달리는 중... (점수: %d, 내PID: %d, 부모PID: %d)\n", 
                   g_score, getpid(), getppid());
            sleep(1); // 1초 쉼
        }
    } else {
        // [부모 프로세스 영역]
        // 부모는 pid 변수에 '자식의 PID'를 받음
        for (int i = 0; i < 5; i++) {
            g_score += 1; // 점수 증가 (자식과 다르게 증가)
            printf("🦕 부모: 관전 중...   (점수: %d, 내PID: %d, 자식PID: %d)\n", 
                   g_score, getpid(), pid);
            sleep(1); // 1초 쉼
        }
    }

    return 0;
}

1-2) 실행 결과 (해석 포인트)

=== [Start] 경기를 시작합니다 (PID: 1000) ===
🦕 부모: 관전 중...   (점수: 1, 내PID: 1000, 자식PID: 1001)
🐥 자식: 달리는 중... (점수: 10, 내PID: 1001, 부모PID: 1000)
🐥 자식: 달리는 중... (점수: 20, 내PID: 1001, 부모PID: 1000)
🦕 부모: 관전 중...   (점수: 2, 내PID: 1000, 자식PID: 1001)

1-3) 궁금한 점

  1. Start 메시지는 한 번만 출력되었는가?
    • fork() 호출 전에 printf가 있었기 때문이다. fork 이후의 코드만 복제되어 실행
  2. 왜 출력 순서가 뒤죽박죽인가?
    • OS 스케줄러가 부모와 자식 중 누구에게 CPU를 먼저 줄지 매 순간 결정하기 때문(동시성)
    • 딜레이를 제거하면 뒤죽박죽으로 나온다.
  3. 자식이 점수를 50점까지 올렸는데, 왜 부모는 5점밖에 안 올랐는가?
    • fork 되는 순간 메모리 공간이 완벽히 분리(Copy-on-Write)되었기 때문이다. 자식의 g_score와 부모의 g_score는 이름만 같고 완전히 다른 변수이다.

2. 프로그램 실행 및 교체 (exec family)

fork로 만든 자식 프로세스는 보통 exec를 호출하여 새로운 프로그램으로 변신합니다. 현재 프로세스의 메모리 공간(Text, Data, Stack 등)을 비우고, 새로운 실행 파일의 코드와 데이터로 덮어씁니다. PID는 변하지 않고 유지되며, exec 성공 시, 코드 자체가 바뀌었으므로 기존 코드로 되돌아오지 않습니다.

함수 접미사 규칙:

  • l (List): 인자를 나열 (execl)
  • v (Vector): 인자를 배열로 전달 (execv)
  • p (Path): 환경변수 PATH 자동 탐색 (execlp)
  • e (Env): 환경변수 직접 설정 (execle)

3. 종료 및 동기화 (exit & wait)

A. 종료 (exit vs _exit)

  • exit(int status): C 라이브러리 함수. atexit 핸들러 실행, stdio 버퍼(printf 등) 비움(flush) 후 종료.
  • _exit(int status): 시스템 콜. 커널 즉시 종료. 버퍼 정리 안 함. (주로 fork 직후 exec 실패 시 사용).

B. 좀비 프로세스 (Zombie)와 처리

자식이 죽었는데(exit), 부모가 wait로 상태 코드를 회수해가지 않은 상태.

  • 해결책 (wait family):
    • wait(&status): 자식이 죽을 때까지 부모가 멈춥니다.(Block)
    • waitpid(pid, &status, options): 특정 PID를 기다리거나, WNOHANG 옵션으로 멈추지 않고(Non-blocking) 상태만 확인할 수 있습니다.
  • 비동기 처리: SIGCHLD 시그널 핸들러를 등록하여, 자식이 죽었을 때만 waitpid를 호출하는 방식이 효율적입니다.

4. 간편 실행 함수 (system vs popen)

직접 fork-exec-wait를 구현하기 번거로울 때 사용하는 래퍼(Wrapper) 함수들입니다.

구분system()popen()
목적단순히 쉘 명령 실행명령 실행 후 결과값 읽기/쓰기
구조fork + exec(/bin/sh) + waitfork + exec + pipe
동기화명령 끝날 때까지 대기 (동기)스트림(FILE*)을 통해 통신
종료함수 리턴 시 명령 종료됨pclose() 호출 시 정리됨

5. 과제 코드 (PPID 추적)

popen으로 ps 명령어를 매번 실행하면 느리고 복잡합니다. 리눅스 커널이 정보를 두는 /proc 파일을 직접 읽는 것이 가장 쉽고 빠르다고 합니다.

아이디어

  • 리눅스의 /proc/[PID]/stat 파일에는 프로세스 정보가 한 줄로 들어있습니다.
  • 4번째 숫자가 무조건 PPID입니다.
  • fscanf를 쓰면 문자열 파싱(strtok) 없이 숫자만 쏙 빼낼 수 있습니다.

코드 (ancestry.c)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

// PID를 주면 PPID를 리턴하는 함수 (핵심)
int get_ppid(int pid) {
    char path[64];
    FILE *fp;
    int ppid;

    // 1. 해당 PID의 정보 파일 열기
    sprintf(path, "/proc/%d/stat", pid);
    fp = fopen(path, "r");
    
    if (fp == NULL) return 0; // 프로세스가 없거나 읽기 실패

    // 2. 포맷 파싱: (1)PID (2)이름 (3)상태 (4)PPID
    // %*d, %*s 등은 읽기만 하고 변수에 저장 안 함(Skip)
    fscanf(fp, "%*d %*s %*c %d", &ppid);
    
    fclose(fp);
    return ppid;
}

int main() {
    int pid = getpid(); // 내 PID부터 시작

    printf("=== 프로세스 족보 추적 ===\n");
    printf("나(Me): %d", pid);

    // PID가 0이나 1(init)이 될 때까지 반복
    while (pid > 1) {
        pid = get_ppid(pid); // 부모 찾기
        
        if (pid == 0) break; // 에러 처리
        printf(" -> %d", pid);
    }
    
    printf("\n=== 추적 완료 (Root 도달) ===\n");
    return 0;
}

실행 결과

=== 프로세스 족보 추적 ===(Me): 756332 -> 756322 -> 756320 -> 742339 -> 742074 -> 742019 -> 742015 -> 741984 -> 741966 -> 741965 -> 741957 -> 86526 -> 1
=== 추적 완료 (Root 도달) ===
[1] + Done                       "/usr/bin/gdb" --interpreter=mi --tty=${DbgTerm} 0<"/tmp/Microsoft-MIEngine-In-bk0ipbx0.cxl" 1>"/tmp/Microsoft-MIEngine-Out-iehzgvak.fdq"
profile
임베디드 개발자가 되기 위해 공부중입니다!

0개의 댓글