[OS] 2. Process API

Hayoon·2023년 7월 28일

운영체제

목록 보기
2/5

Operating Systems Three Easy Pieces
작성자: Remzi H. Arpaci-Dusseau, Andrea C. Arpaci-Dusseau · 2018 번역본을 참고하였습니다.
https://github.com/remzi-arpacidusseau/ostep-translations/tree/master/korean

프로세스 API

프로세스 생성에 fork() 시스템 콜이 사용된다.
다음 예를 보자

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

int main(int argc , char *argv[]) {
	printf("hello word (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 (pid:%d)\n", rc, (int) getpid());
    }
    return 0;
}

PID는 프로세스 식별자(process identifier)로 이 프로세스는 상단 코드에서 29146 이라는 PID를 가진다. 새로 생성된 프로세스는 자식 프로세스, 생성한 프로세스가 부모 프로세스로 불리는데, 두 개의 프로세스는 각각 자신의 주소 공간, 자신의 레지스터, 자신의 PC(Program Counter, 메모리에서 실행할 다음 명령어의 주소를 저장하는 레지스터) 값을 갖는다. fork()로부터 부모 프로세스는 생성된 자식 프로세스의 PID를 반환받고, 자식 프로세스는 0을 반환받는다.
CPU 스케줄러는 실행할 프로세스를 선택하는데 어느 프로세스(자식 / 부모)가 먼저 실행된다라고 단정할 순 없다. 비결정성(nondeterminism)으로 인해 멀티 쓰레드 프로그램 실행 시 다양한 문제가 발생한다.

어떤 문제가 발생할까?

  • 경쟁 상태 (Race Condition): 여러 쓰레드가 공유된 자원(예: 변수, 메모리 등)에 동시에 접근하여 값을 변경하려고 할 때, 어떤 쓰레드가 먼저 실행되느냐에 따라 결과가 달라질 수 있습니다. 이러한 상태를 경쟁 상태라고 하며, 원하지 않는 결과를 초래할 수 있습니다.

  • 데드락 (Deadlock): 두 개 이상의 쓰레드가 서로가 가지고 있는 자원을 점유하고 다른 쓰레드가 점유한 자원을 기다리는 상태에 빠지는 것을 데드락이라고 합니다. 데드락이 발생하면 해당 쓰레드들은 무한히 기다리며 더 이상 진행되지 않게 됩니다.

  • 기아 (Starvation): 한 쓰레드가 계속해서 우선권을 얻어 자원을 사용하면서 다른 쓰레드들이 무한히 대기해야 하는 상태를 말합니다. 일부 쓰레드들이 우선권을 갖고 자원을 지나치게 점유하여 다른 쓰레드들이 실행할 기회를 제한할 수 있습니다.

  • 순서 보장 (Ordering Guarantees) 문제: 멀티 쓰레드 환경에서는 쓰레드의 실행 순서를 예측하기 어려우므로, 프로그래머가 의도한 순서가 보장되지 않을 수 있습니다. 이로 인해 예상치 못한 결과가 발생할 수 있습니다.

이러한 문제들은 멀티 쓰레드 프로그래밍 시 주의해야 할 중요한 측면으로 이러한 문제를 피하기 위해서는 동기화 기법잠금(Lock)을 적절히 사용하거나 스레드 간의 통신을 위한 적절한 메커니즘을 사용하여 프로그램을 설계하고 구현해야 한다.

wait() 시스템 콜

부모 프로세스가 자식 프로세스의 종료를 대기해야 하는 경우, wait()을 사용해 자식 프로세스 종료 시점까지 자신의 실행을 중지시킨다. 이를 통해 순서를 지킬 수 있다. 부모 프로세스가 먼저 실행되면 wait()를 호출하여 자식 프로세스가 종료될 때 까지 리턴하지 않아 자식 종료 후 wait()이 비로소 리턴이 되고 비로소 부모 프로세스가 출력한다.

exec() 시스템 콜

자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용하는 시스템 콜이다. fork()는 자신의 복사본을 생성하여 실행하지만, 자신의 복사본이 아닌 다른 프로그램을 실행해야 할 때 exec()가 그 일을 한다.
예를 들어보자.
1. 자식 프로세스(p3)이 어떤 프로그램(p4)을 실행하기 위해 execvp() 콜을 호출하여 그 실행 파일의 코드와 정적 데이터를 읽어 들여 현재 실행 중인 프로세스(자식 프로세스)의 코드 세그먼트정적 데이터 부분을 덮어 쓴다.
2. 스택, 힙 및 다른 주소 공간들로 새로운 프로그램의 실행을 위해 다시 초기화 된다. 그런 다음 운영체제는 프로세스의 argv와 같은 인자를 전달해 프로그램(p4)을 실행시킨다.
3. 이 때 새로운 프로세스를 생성하는게 아닌, 현재 실행 중인 프로그램을(p3) 다른 실행 중인 프로그램(p4)로 대체하는 것이다.
4. 자식 프로세스(p3)이 exec()를 호출한 후에는 p3은 전혀 실행되지 않는 것처럼 보인다. 이 시스템 콜이 성공하면 p3는 절대로 리턴하지 않는다.

왜 fork(), exec() 두 개로 분리했을까?

Unix의 쉘을 구현하기 위해서는 fork()와 exec()을 분리해야 한다. 그래야만 쉘이 fork()를 호출하고 exec()를 호출하기 전에 코드를 실행할 수 있다. 쉘은 프롬프트를 표시하고 사용자가 무언가 입력하기를 기다린다. 그리고 명령어를 입력한다(예, 실행 프로그램의 이름과 필요한 인자). 대부분의 경우 쉘은 파일 시스템에서 실행 파일의 위치를 찾고 명령어를 실행하기 위하여 fork()를 호출하여 새로운 자식 프로세스를 만든다. 그런 후 exec()를 호출하여 프로그램을 실행시킨 후 wait()를 호출하여 명령어가 끝나기를 기다린다. 자식 프로세스가 종료되면 쉘은 wait()로부터 리턴하고 다시 프롬프트를 출력하고 다음 명령어를 기다린다.

(아직까지는 정확히 무슨 말인지 이해가 잘 안된다.) 이후에 나올 file description에서 자세히 다뤄보자.

profile
Junior Developer

0개의 댓글