Process Control

김민욱·2025년 6월 18일

System Call Error
void를 반환하는 일부 함수를 제외한 모든 system-level function은 리턴 상태를 반드시 체크해야 한다.

if ((pid=fork())<0) {
	fprintf(stderr, "fork error: %s\n", strerror(errno));
    exit(0);
}

리눅스 system-level function은 에러 발생시 -1을 리턴하고 전역변수 errno을 통해 원인을 나타낸다.

Stevens-style error-handling wrapper를 작성하여 에러 확인을 할 수 있다.

// 예
void unix_error(char* msg)
{
	fprintf(stderr, "%s: %s\n", msg, strerror(errno));
    exit(0);
}

pid_t Fork(void)
{
	pid_t pid;
    
    if ((pid=fork())<0) unix_error("Fork error");
    
    return pid;
}

pid = Fork();

Process states

프로그래머의 관점에서 프로세스는 세 가지 상태를 가진다고 생각할 수 있다.

  • Running
    프로세스가 실행 중이거나 kernel에 의해 곧 실행할 작업으로 스케쥴링되어 실행 대기 중인 상태

  • Stopped
    프로세스 실행이 중단되어 추후 signal이 있을 때 까지 스케쥴링 되지 않는 상태

  • Terminated
    프로세스가 영구적으로 중단된 상태

프로세스가 Terminate 되는 데에는 3가지 이유가 있다.
1. terminate signal을 받은 경우
2. main에서 return된 경우
3. exit 함수를 호출한 경우

  • void exit(int status)
    인자로 받은 exit status와 함께 프로세스를 terminate 시킨다.
    일반적인 리턴은 0이고, 리턴이 0이 아니면 에러 발생이다.
    종료 상태를 명시하는 또 다른 방법으로는 main에서 정수 값을 리턴하는 방법이 있다.
    이 리턴은 바로 OS에 전달되므로 호출자에게는 리턴하지 않는다.

Parent 프로세스는 fork를 호출함으로써 child 프로세스를 생성할 수 있다.

  • int fork(void)
    child 프로세스에게 0을 리턴하고, parent에게는 child의 PID를 리턴한다.
    즉, 한 번 호출하면 리턴이 두 번이다.
    child는 parent와 거의 동일하다.
    child는 parent의 virtual address space의 복사본을 동일하게 받는다.
    child는 parent가 open한 파일의 복사본도 동일하게 받는다.
    하지만 child는 parent와 PID가 다르다.

코드를 통해 확인해보자

// fork.c

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
  • call once, return twice
  • concurrent execution (순서는 예측 못함)
  • duplicate but separate address space
    fork가 parent와 child에 리턴할 때 x 값은 1이고, 이후의 변경은 독립적이다.
  • shared open files
    parent와 child의 stdout이 같다.

Process Graph

concurrent program의 부분적인 순서를 파악하기 위해서 process graph를 사용할 것이다.

각 vertex는 statement의 실행이다.
a->b는 "a가 일어난 후 b가 일어난다."의 의미이다.
edge는 현재 변수들의 값들로 레이블링 될 수 있다.
printf vertices는 output으로 레이블링 될 수 있다.
각 그래프는 in-edge 없는 vertex로 시작된다.

그래프의 위상 정렬은 실행 가능한 순서를 나타낸다.


이 때 가능한(Feasible) 순서는 다음과 같다.

Reaping Child Process

프로세스가 종료되어도 시스템 리소스를 소비하는 상태를 좀비(zombie)라고 한다.

Reaping
child가 종료된 후 parent가 child의 종료상태를 확인하고(wait, waitpid) 시스템 자원(메모리, PID 등)을 회수하는 작업이다.

만약 parent가 reaping을 수행하지 않고 종료되면 orphaned child는 init 프로세스(pid==1)에 의해 reaping된다. 그러므로 긴 프로세스에서는 필요할 때만 명시적 reaping이 필요하다.

wait : Child와 동기화
parent는 wait 함수를 호출하여 child를 reaping한다.

int wait(int *child_status) :
child가 종료될 때 까지 현재 프로세스를 대기시킨다.
리턴은 child가 종료될 때의 pid다.
만약 child_status != NULL면, 포인터는 child가 종료된 이유와 종료 상태를 가리킨다.

waitpid : 특정 프로세스를 기다림
pid_t waitpid(pid_t pid, int &status, int options) :
현재 프로세스를 특정 프로세스가 종료될 때 까지 대기시킨다.

execve : 프로그램 로딩, 실행
int execve(char* filename, char* argv[], char* envp[]) :
현재 프로세스에서 프로그램을 로드하고 실행시킨다.
#!로 시작하는 오브젝트 파일 또는 스크립트 파일을 로드할 수 있다.
컨벤션에 의해서 argv[0]==filename이다.
환경변수리스트 envp는 "name=value" 형태로 저장된다.
PID는 유지한 채로 code, data, stack은 덮어써진다.
에러가 없다면 리턴은 없다.


<참고자료>
Bryant and O’Hallaron, Computer Systems: A Programmer’s Perspective, Third Edition
안성용, "시스템소프트웨어", 부산대학교

0개의 댓글