ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 프로세스의 이해
    운영체제 2024. 3. 1. 16:37
    728x90

    Process Concept

    프로세스는 실행 중인 프로그램을 나타내며, 운영체제는 이를 작업의 기본 단위로 활용합니다.

    • 프로세스는 작업을 수행하기 위해 아래와 같은 특정 리소스가 필요합니다.
      • CPU time
      • memory
      • files or I/O devices (resource)

    일반적으로 CPU는 직접 스토리지에 저장된 프로그램을 실행하지 않으며, 프로그램은 메모리에 로드되어야 합니다.

    이때 메모리에 로드된 프로그램을 실행 중인 프로세스라고 할 수 있습니다.

    프로세스의 메모리 레이아웃

    프로세스의 메모리 레이아웃은 아래와 같이 여러 섹션으로 나뉩니다.

    2NTmbQe.png


    Text section: 실행 코드

    Data section: 전역 변수

    Heap section: 프로그램 실행 시간 동안 동적으로 할당되는 메모리 (자바의 경우 new 키워드)

    Stack section: 함수를 호출할 때 사용되는 임시 데이터 저장소
    (function parameters, return addresses and local variables 등이 포함됩니다.)

     

     

    위와 같은 코드가 컴파일 되어 실행된다면 하나의 프로세스로 취급되어 운영 체제의 관리 대상이 됩니다.

    프로세스의 상태 종류

    프로세스는 실행된 후 상태가 변경됩니다.

    New: 프로세스가 생성된 상태
    Running: 명령 실행 중
    Waiting: 프로세스가 일부 이벤트가 발생하기를 기다리고 있습니다.
    (I/O completion 또는 reception of a signal.)

    Ready: 프로세스가 프로세서에 할당되기를 기다리고 있습니다.
    Terminated: 프로세스 실행 종료

    프로세스 관리

    PCB (Process Control Block)

    각 프로세스는 PCB로 운영 체제에 표시되는데 PCB에는 프로세스가 가져야하는 모든 정보가 포함되어 있습니다.
    (Task Control Block TCB 라고도 불립니다.)

    PCB에 포함되는 정보의 종류 :

    Process state : new / running / wait / ready …
    Program counter : 실행 번지 저장소
    CPU 레지스터 : IR / DR …
    CPU 스케줄링 정보
    메모리 관리 정보
    Accounting information
    I/O status information

    프로세스 특징

    프로세스는 다음과 같은 특징을 가지고 있습니다:

    • 프로세스는 단일 실행 스레드를 수행하는 프로그램입니다.
    • 이 단일 제어 스레드를 통해 프로세스는 한 번에 하나의 작업만 수행할 수 있습니다.
    • 하지만 현대의 운영체제는 프로세스가 여러 실행 스레드를 가질 수 있도록 확장되어 있어, 한 번에 여러 작업을 수행할 수 있습니다.

    Process Scheduling 기법

    Multiprogramming:
    multiprogramming의 목적은 동시에 여러 개의 프로세스를 실행 시키는데 있는데 이는 CPU의 사용 효율을 최대로 올리기 위해서입니다.

    Time sharing:
    time sharing은 CPU를 점유하는 프로세스를 빈번하게 변경하여 사용자로 하여금 프로그램이 동시에 사용되는것처럼 보이게 하는것을 목표로 하는 기법입니다.

     

    위와 같이 여러 개의 Process를 사용하는 방법이 존재할 때 이를 관리(Scheduling) 하는 기법 역시 필요한데 주로 Scheduling Queues를 통해 이루어집니다.

     

    Scheduling Queues:

    • Queue는 FIFO 구조를 갖는 자료 구조로 프로세스가 시스템에 진입하면 ready queue에 들어가게 되며, 여기서는 CPU 코어에서 실행될 준비가 되어 대기합니다.
    • 특정 이벤트를 기다리는 프로세스는 대기 큐에 배치됩니다.
    • 이러한 큐는 일반적으로 PCB들의 linked list로 구현됩니다.

    Queueing에 대해 좀 더 세분화 해서 내부를 들여다 본다면 아래와 같은 다이어그램을 갖을 수 있습니다.

    1. ready queue에 존재하는 프로세스가 CPU를 획득한 후 종료되었다가 다시 ready queue로 들어가는 경우가 있을 수 있습니다.
    2. I/0 request가 오면 I/O queue로 들어간 후 I/O 작업을 하고 다시 ready queue로 들어갈 수 있습니다.
    3. time slice가 만료되었을 경우 다시 ready queue로 들어갈 수 있습니다.
    4. CPU가 child를 fork한 경우 new 상태가 되기 때문에 child process가 ready queue로 들어갈 수 있습니다.
    5. 마지막으로 특정 interrupt가 발생할 경우 ready queue로 들어갈 수 있습니다.

    Context Switch

    프로세스 입장에서 Context란 사용되고 있는 상태를 의미한다고 볼 수 있고 그 상태는 모두 PCB에 저장되어있습니다.

    (OS 입장에서 문맥 교환이란 PCB를 변경하는것입니다.)

    인터럽트가 발생할 경우

    • 시스템은 현재 실행 중인 프로세스의 현재 상태를 저장합니다.
    • 나중에 해당 상태를 다시 시작해야 할 때, 시스템이 이를 복원할 수 있도록 합니다.

    Context switch 과정

    1. CPU 코어를 다른 프로세스로 전환합니다.
    2. 현재 프로세스의 상태를 저장합니다.
    3. 다른 프로세스의 상태를 복원합니다.

    Operations on Processes

    • 프로세스 생성 및 종료 메커니즘:
      • 운영체제는 프로세스를 생성하는 기능과 프로세스를 종료하는 기능을 제공해야 합니다.
      • 하나의 프로세스는 여러 개의 새로운 프로세스를 생성할 수 있습니다.
    • 프로세스 관계:
      • 프로세스 생성시, 생성되는 프로세스를 생성한 원래 프로세스를 "부모 프로세스"라고 부릅니다.
      • 생성된 새로운 프로세스는 "자식 프로세스"입니다.

    간단하게 말하면, 운영체제는 프로세스를 만들고 제거할 수 있는 방법을 제공하며, 한 프로세스가 다른 프로세스를 만들 때 이러한 관계를 부모-자식 관계로 설정합니다.

    두 가지 실행 가능성

    • 부모 프로세스와 자식 프로세스가 동시에 실행:
      - 프로세스 생성 후에도 부모 프로세스와 자식 프로세스가 동시에 실행됩니다.
    • 부모 프로세스가 자식 프로세스의 종료를 기다림:
      - 부모 프로세스는 생성한 자식 프로세스가 종료될 때까지 기다릴 수 있습니다.

    두 가지 주소 공간의 가능성:

    • 자식 프로세스가 부모 프로세스의 주소 공간을 그대로 복제:
      - 자식 프로세스는 부모 프로세스와 똑같은 프로그램 및 데이터를 가집니다.
    • 자식 프로세스가 새로운 프로그램을 로드:
      - 자식 프로세스는 부모 프로세스와는 독립적인 다른 프로그램을 가집니다.
    ##실습 예제 
    
    #include <stdio.h> 
    #include <unistd.h> 
    #include <wait.h> 
    
    int main() 
    { 	
        pid_t pid; 	
        // fork a child process 	
        pid = fork(); 	
        if (pid < 0) { 
        // error occurred 		
        fprintf(stderr, "Fork Failed"); 		
        return 1; 	
        } 	
        else if (pid == 0) { 
        // child process 		
        execlp("/bin/ls","ls", NULL); 	
        } 	
        else { 
        // parent process 		
        wait(NULL); 		
        printf("Child Complete"); 	
        } 	
       return 0; 
      }

    프로세스 종료 조건과 과정

    위 예제를 통해 확인했듯 fork()를 통해 프로세스가 생성이 되는데 종료는 어떤 조건을 통해 실행할 수 있을까?

    • 프로세스 종료 조건:
      • 프로세스는 마지막 명령문을 실행하여 작업을 완료하면 종료됩니다. (주로 return)
    • exit() 시스템 콜:
      • 프로세스가 종료를 요청하기 위해 exit() 시스템 콜을 호출할 수 있습니다.
      • 이 호출은 운영체제에게 해당 프로세스를 삭제해 달라고 요청합니다.
    • 자원 해제:
      • 프로세스 종료 시, 운영체제는 해당 프로세스가 사용한 모든 자원을 해제하고 회수합니다.
        • 할당된 메모리, 열린 파일, 입출력 버퍼 등의 자원이 여기에 포함됩니다.

    간단히 말하면, 프로세스는 작업을 마치거나 명시적으로 종료를 요청할 때, 운영체제는 해당 프로세스가 사용한 자원을 해제하고 프로세스를 삭제합니다.

     

    Zombie Process:

    • Zombie 프로세스는 이미 종료된 프로세스인데 부모 프로세스가 아직 wait() 시스템 콜을 호출하지 않은 상태를 나타냅니다.
    • 부모 프로세스는 자식 프로세스의 종료 상태를 확인하기 위해 wait()을 호출해야 합니다. 그렇지 않으면 자식 프로세스는 완전히 종료되지 않은 상태로 남아있게 되며, 이 상태가 Zombie라고 불립니다.

    좀비 프로세스는 주로 daemon 프로그램 혹은 background 프로그램을 만들때 사용합니다.

     

    Orphan Process:

    • Orphan 프로세스는 부모 프로세스가 이미 종료되었고, 부모 프로세스가 wait()을 호출하지 않은 상태에서 남아 있는 자식 프로세스를 의미합니다.
    • 이 상태에서 자식 프로세스는 새로운 부모 프로세스를 가지게 됩니다. 일반적으로 init 프로세스가 이 역할을 맡아 책임집니다.

    In UNIX-like O/S:

     

    • 새로운 프로세스는 `fork()`라는 시스템 콜을 통해 생성됩니다.
      • 자식 프로세스는 상위 프로세스(부모)의 주소 공간을 복사하여 구성됩니다.
      • 두 프로세스 모두  `fork()` 시스템 콜에 따른 후 실행을 계속합니다.
    • 차이점
      • `fork()`를 통해 복사된 자식 프로세스의 경우 반환 코드 값이 0 입니다.
      • 부모 프로세스는 운영 체제(OS 커널)이 제공한 pid 값으로 반환됩니다.

     

    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    
    {
    pid_t pid;
    pid = fork();
    printf("Hello, Process!\n");
    }
    
    출력 결과 :
    Hello, Process!
    Hello, Process!
    
    ---------------------------------------------
    
    #include <stdio.h>
    #include <unistd.h>
    
    int main()
    
    {
    pid_t pid;
    pid = fork();
    printf("Hello, Process! %d\n", pid);
    }
    
    출력 결과:
    3217
    0

     

    fork( ) 시스템 호출 후 


    • 부모 프로세스는 `fork()` 여부와 무관하게 실행을 지속할 수 있습니다.
    • 부모 프로세스는 `fork()`를 통해 생성된 자식 프로세스가 실행되는 동안 다른 할 일이 없다면 
    `wait( )` 시스템 콜을 통해 자식 프로세스의 작업이 끝날 때까지 `ready queue`로 이동해 대기할 수 있습니다.

     

    #include <stdio.h>
    #include <unistd.h>
    #include <wait.h>
    
    int main()
    
    {
    pid_t pid;
    pid = fork();
    if (pid > 0)
    wait(NULL);
    printf("Hello, Process! %d\n", pid);
    }
    
    출력 결과:
    Hello, Process! 0
    Hello, Process! 3217
    ```
    
    ```c
    int value = 5;
    
    int main()
    
    {
    pid_t pid;
    pid = fork();
    
    if (pid == 0) { // child process
    value += 15;
    return 0;
    }
    
    else if (pid > 0) { // parent process
    wait(NULL);
    printf("Parent: value = %d\n", value); // LINE A
    }
    }
    
    출력 결과:
    Parent: value = 5

     

    #include <stdio.h>
    #include <unistd.h>
    #include <wait.h>
    
    /*
    * How many processes are created?
    */
    
    int main()
    
    {
    fork(); // fork a child process
    fork(); // fork another child process
    fork(); // and fork another
    return 0;
    }
    
    fork()를 통해 자식 프로세스를 생성한다는것은 재귀적으로 새로운 프로세스가 생성된다고 볼 수 있다.
    
    부모 프로세스 - 자식 프로세스 - fork()를 통해 생성된 또 다른 자식 프로세스 ..
    ```
    
    ```c
    #include <stdio.h>
    #include <unistd.h>
    
    /*
    * How many processes are created?
    */
    
    int main()
    {
    int i;
    for (i = 0; i < 4; i++)
    fork();
    return 0;
    }
    
    -> 2^n 개 로 16개의 새로운 프로세스가 생성된다.

     

     

    int main()
    {
    	pid_t pid;
    	pid = fork();
    	if (pid == 0) { // child process
    		execlp("/bin/ls","ls", NULL);
    		printf("LINE J\n");
    	}
    	else if (pid > 0) { // parent process
    		wait(NULL);
    		printf("Child Complete\n");
    	}
    	return 0;
    }
    int main()
    
    {
    	pid_t pid, pid1;
    	pid = fork();
        
    	if (pid == 0) { // child process
    		pid1 = getpid();
    		printf("child: pid = %d\n", pid); // A
    		printf("child: pid1 = %d\n", pid1); // B
    	}
    	else if (pid > 0) { // parent process
    		pid1 = getpid();
    		printf("parent: pid = %d\n", pid); // C
    		printf("parent: pid1 = %d\n", pid1); // D
    		wait(NULL);
    	}
    	return 0;
    }
    
    출력 결과:
    parent: pid = 5157
    parent: pid1 = 5156
    child : pid = 0
    child : pid1 = 5157

     

Designed by Tistory.