ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Linux] 2-1. 프로세스와 스레드(Process & Thread)
    CSE/Linux 2015. 8. 12. 16:44

     2.3 시스템 호출과 프로세스의 상태 전이

      본 절에서는 일반적인 입출력 시스템 호출 및 인터럽트 처리와 관련된 프로세스의 상태 변이를 알아보기 위해 프로세스의 시스템 호출부터 복귀까지의 과정을 입출력인 디스크 파일 입력의 예로 설명하도록 하겠습니다.


      사용자가 요구한 입출력이 완료되기까지는 커널 내의 관련 시스템 호출 처리 루틴뿐만 아니라 디바이스 드라이버의 주축인 인터럽트 처리 루틴이 서로 유기적인 관계를 맺고 동작합니다. 이 과정에서는 입출력 완료 대기에 의한 프로세스 대기 상태로의 전이, 문맥 교환이 발생하고, 디스크 입출력을 최소화하기 위한 커널의 버퍼링 기법도 등장합니다. 이러한 처리 과정은 커널의 두 가지 대표적 구성 요소인 시스템 호출과 인터럽트 처리의 관계를 이해하는 중요한 부분이 됩니다.




      1) 예제 프로그램

       이 예제에서 사용자 프로세스는 디스크 파일을 열고(open), read 시스템 호출을 수행합니다.


       read 시스템 호출이 수행되면 사용자 프로세스와 CPU는 커널 모드 실행으로 바뀌며 커널 안의 read 기능 실행 함수로 진입하게 됩니다. 커널 내부 함수 진입은 CPU의 모드를 커널 모드 실행으로 바꾸어 여러가지 특권을 갖게 해야 하므로 일반적인 사용자 모드에서의 함수 호출 기법이 아닌 트랩(Trap)이라는 특수 명령어를 수행함으로써 이루어집니다.



    * 사용자 프로세스


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
     
    #define BUF_LENGTH 256
     
    int main(void) {
        int file_id;
        char buffer[BUF_LENGTH];
        
        file_id = open("data_file"0);    // read-only-mode open
        
        n = read(file_id, buffer, BUF_LENGTH);
        /* 
         * 사용자 모드에서 실행 중 시스템 호출을 하여 커널로 
         * 진입하며 커널 모드실행으로 전환 
         * 복귀 시는 다시 사용자 모드
         * 
         */
      .....
    }
    cs





      2) 커널의 시스템 호출 처리 루틴

      디스크 파일 입력을 위한 시스템 호출 처리 루틴은 파일 시스템 및 매체의 종류, 가상 메모리 및 페이지 버퍼 관리 등과 연결되어 매우 복잡한 구조를 가집니다. 본 적에서는 이러한 복잡한 과정을 피하고 핵심 개념을 위해 실제 리눅스 구조를 축약하여 의사 코드로 설명하도록 하겠습니다. 디스크 파일 입력을 위한 시스템 호출은 다음과 같은 작업을 수행합니다.


       1. 읽고자 하는 물리적 디스크 블록이 이미 커널의 시스템 버퍼에 있으면 이를 사용자에게 반환하고 사용자 모드로 복귀한다. 시스템 버퍼는 한 번 read 했던 디스크 블록에 대한 중복 디스크 입출력을 피하려고 커널이 커널 메모리 영역에 메모리 허용 한도 내에서 디스크 블록을 유지하고 있는 영역으로 리눅스에서는 전통적인 buffer cache나 가상 메모리 관리를 위한 page cache로 유지하는데 이는 파일 시스템의 종류에 따라 달라진다.(가장 많이 사용되는 ext3 file system의 경우 page cache를 사용한다.)


       2. 시스템 버퍼에 원하는 디스크 블록이 없거나 모자라는 경우에는 해당 블록을 시스템 버퍼에 적재하기 위해 버퍼를 할당받고, 디스크 입출력 정보를 탑재한 입출력 제어 블록을 생성하여 디스크 입출력 요구 큐에 등록한다.


       3. 디스크 입출력 요구 큐에 등록된 프로세스의 입출력이 완료될 때까지 프로세스는 대기 상태로 전환하며 문맥 교환을 통해 다른 프로세스에 CPU를 넘긴다. 즉, 프로세스는 시스템 호출 처리 루틴 내부에서 수행이 중지되는 상태가 된다.(Ready State)


       4. 입출력 큐에 등록된 입출력이 디바이스 드라이버에 의해 추후에 완료되면 프로세스는 준비 상태로 전이되고, 스케줄링을 받아 다시 실행이 속개되면(중지되었던 윛에서 다시 커널 모드 실행) 커널의 시스템 호출 처리 루틴은 시스템 버퍼의 내용을 사용자 영역에 복사하고 사용자 모드로 복귀하여 시스템 호출을 완료하게 된다.



      위 3번 과정에서의 수행이 중지된 상태(대기상태)에서 4번 과정의 상태로 오는 과정에는 디스크 디바이스 드라이버의 인터럽트 처리 루틴과 스케줄러(문맥 교환)가 작용하게 됩니다.

      즉, 3번에서 4번으로 되는 과정에는 디스크 입출력의 진행 및 완료, 준비 상태로의 전이, 커널 모드 실행모드로의 전이가 차례로 있어야 합니다.

      다음의 프로그램은 위와 같은 작업을 수행하는 커널 내부의 디스크 입력 루틴인 read와 디스크 인터럽트 핸들러를 의사 코드로 축약한 것입니다.






     1) sys_read: file offset 정보 획득

     2) vfs_read: open mode나 access check, security check

     3) do_sync_read (ext3 file의 경우, read 요청 관련 정보 생성)

     4) __generic_file_aio_read: 가상 메모리 관리의 page cache를 사용하는 모든 file system의 read routine

     5) do_generic_file_read -> do_generic_mapping_read






     * S/W 캐싱: 일반적인 디스크 기반의 운영체제에는 파일 입출력 및 가상 메모리 관리와 관련해서 많은 디스크 입출력이 발생합니다. 그러나 디스크 입출력은 CPU의 처리 속도보다 매우 느리므로 시스템의 성능 향상을 위해 디스크 입출력을 가능한 최소화해야 합니다. 이를 위해 대부분의 커널은 읽어온 디스크 블록의 재활용을 위해 커널 내의 일정 메모리 공간이 허용하는 한도 내에서 시스템 버퍼(각종 S/W 캐시나 가상메모리의 page cache)에 이를 저장합니다. 그리고 후에 발생하는 디스크 입출력 요구가 있을 때, 시스템 버퍼에 이 블록이 있으면 디스크 입출력없이 이를 찾아 재활용하게 됩니다.



     * DMA 디스크 입출력: 디스크 입출력은 블록(보통 4K ~ 32K 정도) 단위의 DMA(Direct Memory Access) 방식으로 진행됩니다. 이러한 디스크 입출력은 CPU의 처리 속도보다 상대적으로 느리므로 디스크 입출력 요구들의 큐가 형성되기 마련입니다. 디스크 입출력 요구 큐는 한 단위의 디스크 입출력에 필요한 모든 정보를 가지는 입출력 정보 블록의 리스트로 형성됩니다. 이러한 정보는


     <메모리 주소, 디스크 트랙/섹터 주소, 입출력 바이트 수, 읽기/쓰기>

     

     들로 구성되고 디스크 입출력은 CPU가 이러한 정보로 디스크 제어기에 입출력 명령을 내림으로써 시작됩니다. 일단 입출력이 시작되면 CPU는 더 이상 관여하지 않으며, 디스크 제어기가 메모리와 디스크 간에 자료를 직접 이동시킴으로써 입출력이 진행됩니다. 이 기간에 CPU는 다른 프로세스를 수행할 수 있으며, 디스크 제어기는 디스크 블록 입출력의 완료 시에 이 사실을 인터럽트를 발생시켜서 CPU에 통보합니다. 따라서 입출력 수행 중에 CPU는 명령어의 인출(fetch)을 위해, 디스크 제어기는 입출력 자료의 읽기나 쓰기를 위해 메모리에 동시에 접근하게 되고, 메모리에 대한 충돌 시에는 DMA가 우선권을 가집니다(cycle stealing).




      다음은 프로세스가 실질적인 대기 상태로 진입하는 block_wait_queue_running에 관한 설명입니다. 



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    inline void block_wait_queue_running(request_queue_t *q) {
        prepare_to_wait_exclusive(.., &wait, TASK_UNINTERRUPTIBLE);
        /*
         * wait queue에 삽입, task 대기상태로 전환
         * 여기서 TASK_UNINTERRUPTIBLE 상태는 signal이 발생해도 깨어나지 않음
         */
        
        io_schedule();
        /*
         * 위 함수를 호출하여 문맥 교환(Context Switch)을 수행하고
         * 다시 깨어날 때에는 disk I/O는 완료되고 read 상태를 거쳐 scheduling을 받아 running 상태로 된 것임.
         * 이러한 과정에 관여하는 것은 disk 제어기에 대한 커널 interrupt handler
         */
     
    }
     
    cs




      위의 의사 코드에서 io_schedule() 함수는 현 실행 프로세스를 중지시키고 새로운 프로세스에 CPU를 할당하는 문맥 교환(Context switch) 함수입니다. 


      그 과정은 CPU 레지스터 내용과 같은 시스템 문맥을 중단될 프로세스의 태스크 구조체(PCB, task_struct)에 저장하고, 새로 선정된 플세스의 태스크 구조체 내에 보존되었던 문맥을 레지스터에 복귀시키는 것입니다. 저장 및 복귀되는 레지스터에는 프로그램 카운터(program counter)도 포함되기 때문에 이러한 문맥 교환이 일어나게 되면 자동으로 예전에 중지되었던 프로세스의 코드 부분으로 분기가 일어나 실행이 시작됩니다. 여기서 주의해야할 점은 문맥 교환으로 실행이 중지되는 프로세스도 이와 마찬가지로 언젠가는 다른 프로세스의 문맥 교환에 의해 현 지점으로 돌아오게 된다는 것입니다.

      

      따라서 switch_to는 함수로 표현되지만, 수행을 마치고 당장 복귀되는 일반 함수와 달리 다른 프로세스로 분기되었다가 다시 다른 프로세스의 문맥 교환에 의해 돌아오는 특수한 형태의 함수로 생각할 수 있습니다.





     3) 디스크 인터럽트 처리 루틴

      예제의 입출력 요청 프로세스가 커널 내부의 문맥 교환으로 중지된 상태에서 디스크 입출력의 진행은 디스크 제어기로부터의 인터럽트를 처리하는 커널 루틴이 담당하게 됩니다. 디스크 입출력 처리 루틴의 주요 작업은 크게 두 가지로 구성됩니다.


       1. 입출력 종료로 인터럽트가 발생하면 인터럽트 처리 루틴은 입출력이 종료된 프로세스의 태스크 구조체를 준비 상태(TASK_RUNNING)로 만들어 스케줄링 큐(run_queue)에 재삽입 한다. 이러한 행위를 일반적으로 wake_up 이라하며 준비 상태가 된 프로세스는 스케줄링을 기다리게 된다.


       2. 입출력 요구 큐에 등록된 다음 입출력 정보 블록으로 디스크 제어기에 입출력을 명령한다.





      위에서 입출력이 완료되어 다시 준비 상태(TASK_RUNNING)로 전환되는 프로세스는 일반적으로 사용자 모드에서 실행할 때보다 높은 우선순위를 할당받아 스케줄링 큐로 삽입됩니다. 그 이유는 시스템 호출 실행 중에 중지되었다가 다시 깨어난 프로세스를 일반 사용자 실행 모드의 프로세스보다 먼저 실행시켜 주기 위함입니다.






      일련의 인터럽트 처리 후에 준비 상태로 가게 된 예제 프로세스는 커널 내부의 여러 부분에 위치하는 문맥 교환 호출로 다른 프로세스로부터 CPU를 할당받아 자신이 예전에 문맥 교환을 한 지점으로 다시 복귀하게 됩니다. 커널 내부에서 문맥 교환이 일어나는 경우는 다음의 두 가지 유형으로 분류됩니다. 



       1. 프로세스가 시스템 호출을 하고 해당 커널 함수 안에서 입출력 요구 등을 하여 프로세스가 대기 상태로 전환하며 자발적으로 문맥 교환을 하는 경우


       2. 시스템 호출 및 인터럽트의 처리 과정에서 여러 가지 이유(타임 슬라이스의 소진, 프로세스의 대기 종료 및 생성, 우선순위의 변화 등)로 스케줄링이 필요하다는 플래그(current -> need_resched)가 설정된 경우, 인터럽트 처리 및 시스템 호출 완료 직후 사용자 모드로의 복귀 이전에 스케줄러가 수행되며 이때 우선순위가 현재 프로세스보다 높은 프로세스가 있으면 비자발적 문맥 교환이 일어난다. 현 프로세스의 타임 슬라이스가 다 소진된 경우에도 클럭 인터럽트 처리 과정에서 이러한 사실을 알게 되고, 이때에도 인터럽트 처리를 마치고 사용자 모드로 복귀하는 과정에서 문맥 교환이 발생한다.




     4) Timer 인터럽트 처리 루틴

      모든 운영체제는 일정한 시간 간격(1 milli-sec 또는 10 milli-sec)으로 인터럽트를 발생시키는 타이머를 갖습니다. 타이머 인터럽트는 정상정인 경우에 가장 우선순위가 높은 인터럽트입니다. 타이머 인터럽트가 발생하면 하던 작업(커널 작업이나 프로세스 실행)을 즉시 중지시키고 커널 내의 타이머 인터럽트 처리기(handler)로 분기하여 여러 가지 작업을 수행합니다. 수행되는 작업은 아래와 같습니다.


       1. 부팅 이후로부터의 시스템 시간 업데이트(jiffies ++)

       2. 주기적 커널 함수 작업 호출

       3. 현재 실행 중인 프로세스의 잔여 타임 슬라이스 감축, 잔여 타임 슬라이스가 0이 된 경우 다른 프로세스를 스케줄링하여야 하므로, 인터럽트 처리가 끝난 후 재 스케줄링 할 수 있도록 PCB 내의 need_resched 플래그를 1로 설정

       4. sleep 이나 alarm을 설정한 프로세스에 대한 서비스 수행

       5. 비사용 메모리가 부족할 경우 커널의 swapper 프로세스를 깨워(ready), swapper가 후에 스케줄링을 받으면 메모리를 비우는 작업을 수행토록 함

       6. return_from_interrupt 수행






     * Clock interrut handler

      Update the system time;

      Call timeout functions (every n ticks)

      Wakeup blocked processes in time queue; (processes called sleep etc.)

      Wakeup kernel processes if free memory size is under the threshold

      (wakeup Swapper kernel process and Swapout pages)

     

      current_PCB.time_slice_left--;

      if (time_slice_left == 0) currentPCB.need_resched = 1;

      Re-compute user mode priority; and do something;

      Return from Interrupt (== Ret from syscall)




    타이머 인터럽트 처리







       Return_from_interrupt는 need_resched 플래그를 테스트하여 재 스케줄링이 필요하면 가장 높은 우선순위의 프로세스를 골라 현 프로세스와 문맥 교환을 수행합니다. 이와 같은 문맥 교환은 인터럽트 처리 내에서 수행되어서는 안 되고 반드시 인터럽트 처리가 종료된 후에 해야 합니다.

       리눅스의 인터럽트는 인터럽트를 disable 시키고 신속히 단기간에 처리하는 fast interrupt와 그 인터럽트 처리 과정이 길어 인터럽트 disable 모드에서 수행되는 인터럽트 처리(ISR: Interrupt Service Routine) 루틴과 인터럽트 enable 모드에서 시스템 호출 함수가 종료된 뒤에 수행되는 botom-half(또는 Soft IRQ)의 2단계로 처리되는 slow interrupt로 구분 됩니다. 

       키보드 인터럽트는 fast interrupt의 대표적인 예이고, 디스크나 타이머 인터럽트는 처리 과정이 길어 ISR과 bottom-half로 나누어 처리되는 slow interrupt 입니다. 



    커널 내에서 문맥 교환이 일어나는 위치


    System Call Functions 

    Interrupt Handler 

      sys_call_rountine() {

         ..

        

         IO Request, lock waiting, etc.

         Voulantary Context Switch;

       

         ..

      

      }

      ret_from_syscall;

      

      if rescheduling is necessary:

         do Involuntary Context Switch; 

      irq_routine() {

          ..


          interrupt handling

          no context switch during in an interrupt handling!

          (because it is a jump)

     

          ..


      } 

      ret_from_syscal: (= return_from_interrupt)


      if rescheduling is necessary:

          do Involuntary Context Switch; 











     2.4 프로세스의 생성과 소멸

      리눅스 운영체제에서 프로세스의 생성 및 소멸 관련 기본 시스템 호출은 다음과 같습니다.






    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
     
    FORK(2)
    #include <sys/types.h> man fork
    #include <unistd.h>
     
    pid_t fork(void);
     
        입력 값: 없음
        반환 값: - child 프로세스 : 0
                - parent 프로세스 : child 프로세스의 PID
                - 에러 : -1
    cs




      fork 시스템 호출은 실행 중인 프로세스가 자신과 같은 프로그램 내용을 가진 새로운 프로세스를 생성할 때 사용합니다.


      fork를 호출한 프로세스를 parent 프로세스라 하고, 새로 생성되는 프로세스를 child 프로세스라고 합니다.


      parent 프로세스의 fork 호출이 완료되면 fork에서의 반환 값은 parent와 child 프로세스 두 곳에서 이루어집니다.


      parent에게는 child의 프로세스 식별자가 반환되고, child에게는 "0"이 반환되어 자신이 parent인지 child인지 구별되도록 합니다.


      새로 생성되는 child 프로세스의 data와 stack은 fork 시의 parent 상태를 복사하지만, 별도의 공간에 할당되어 각각 다른 영역을 사용합니다.






    커널과 프로세스의 주소 공간











    fork에 의한 프로세스 생성 이후의 주소 공간





     다음은 fork를 이용한 전형적인 협동 작업의 예입니다.




    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
     
    int global_int = 5;
     
     
    int main(void) {
     
        pid_t pid;
        ...
     
        if ((pid = fork()) == 0) {    // child
            global_int = 6;
            printf(" child's data = %d\n", global_int);
            exit(0);
        } else {            // parent
            global_int = 7;
            printf(" parent's data = %d\n", global_int);
            wait();
        }
     
        printf(" parent's data = %d\n", global_int);
     
        return 0;
    }
    cs




       위의 예에서 fork 이후의 parent 와 child 프로세스는 커널에 의해 각각 다른 프로세스로 관리되는 것으로 각각 별도로 스케줄링을 받게 됩니다. 따라서 타임 슬라이스 만료나 입출력 요구에 의한 스케줄링으로 수시로 CPU가 각 프로세스에 할당되므로 두 프로세스의 실행 부분은 정해진 순서 없이 실행됩니다. 따라서 마치 두 프로세스가 동시에 수행되는 것처럼 보이고, 또한 실제로 다중 CPU 시스템에서는 병렬로 동시에 실행될 수도 있습니다.


       이러한 프로그램을 병행(Concurrent) 프로그램이라 합니다. 


       위 예에서 전역 변수인 global_int는 초기 값이 5 인 상태로 child 에 복사되지만, child는 별도의 데이터 공간을 갖기 때문에 fork 이후에 global_int는 실제로 두 개가 독립적으로 존재하게 됩니다. 프로세스 fork 이후에 각 프로세스의 메모리 영역은 프로그램 코드 부분인 TEXT 만 공유하고 나머지 부분은 별도의 독자적인 공간을 갖게 됩니다.




      2.4.1 프로세스의 종료와 child 프로세스 종료 대기

       위의 두 프로세스는 child 프로세스의 종료 / 소멸 시점을 기준으로 다시 하나의 수행 흐름으로 합쳐지는 동기화가 수행될 수 있는데, 다음은 이와 관련된 시스템 호출에 대한 설명입니다.


       프로세스의 종료를 위한 시스템 호출 exit은 다음과 같습니다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
     
     
    _EXIT(2)
    #include <stdlib.h>
     
    void exit(int status);
     
        입력 값: 
            - status : 반환되는 상태 값
        반환 값: 없음
     
     
    cs



       exit 은 프로세스의 종료와 함께 프로세스의 사용자 메모리 영역을 해제하며, wait 시스템 호출을 통해 child 프로세스의 종료를 기다리는 parent 프로세스를 깨워주는 역할을 합니다. 이때 exit의 입력 변수는 wait 시스템 호출의 매개 변수로 반환됩니다.


       parent 프로세스가 child 프로세스의 종료를 기다리는 동기화 도구로 사용하는 wait 시스템 호출은 아래와 같습니다.


    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
     
    WAIT(2)
     
    #include <sys/types.h>
    #include <sys/wait.h>
     
    pid_t wait(int *stat_loc);
     
        입력 값: 
            - stat_loc : child 프로세스의 상태 정보를 받기 위한 버퍼
        반환 값:
            - 정상 : 하위 프로세스 식별자(PID)
            - 에러 : -1
     
    cs




       이러한 wait 시스템 호출은 parent 프로세스가 병행으로 어떤 작업을 시킨 child 프로세스의 종료를 기다리는 경우에 사용합니다.


       wait와 exit의 병행으로 실행되는 두 프로세스에 의해 호출되므로 어떤 것이 먼저 일어나는지 알 수 없으며, wait 보다 exit 이 먼저 일어난 경우, wait에 의한 동기화 까지의 child의 상태를 zombie 상태라 하며, zombie 상태의 child는 parent의 wait에 의해 태스크 구조체가 시스템에서 완전 소멸하게 됩니다.

     




     2.4.2 child 프로세스의 상속

      fork 된 child 프로세스는 parent 프로세스의 변수 값들을 상속하고, 그 외에도 여러가지 상태나 사용 자원, 스케줄링 정보 등을 상속받게 됩니다.


      fork된 child 는 parent가 fork 이전에 open한 파일을 그 상태 그대로 상속하는데, open된 file descripter id와 각 파일의 내재적 접근 위치(read/write offset)도 상속하고 공유하게 됩니다.


      read/write offset을 공유하므로 parent와 child가 같은 파일에 입출력하게 되면 서로에게 영향을 미치게 됩니다. 아래 예를 보도록 합시다.




    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
     
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdio.h>
     
     
    int main(void) {
        int old_fd, new_fd, n;
        char buf;
     
        old_fd = open("oldFIle"0);    // read-only mode
        new_fd = creat("newFile"0666);    // File creation
     
        fork();
     
        while ((n = read(old_fd, &buf, 1)) != 0)
            write (new_fd, &buf, n);
     
        close(old_fd);
        close(new_fd); 
     
    cs





      위 프로그램은 새로 만든 newFile에 oldFile의 내용을 1 byte씩 모두 복사하는 프로그램 입니다.


      파일을 열고(open), creat 한 후, fork를 실행하므로 open된 파일의 상속에 의해 parent 와 child는 각각 두 파일을 사용할 수 있습니다.


      이때, 두 프로세스는 read / write offset을 고유하므로 파일의 크기는 두 배가 되지 않으며, 원래의 크기대로 복사를 수행하게 됩니다.


      여기서 주의할 점은 병행 프로그램의 특성상, parent가 read 가 실행하고, write 를 실행하기 이전에 스케줄링이 발생하여 child가 실행되고, child의 read와 write 후에 parent의 write가 실행될 수 있다는 점입니다.


      이런 시나리오로 병행 프로그램이 실행되면 복사된 파일의 크기는 같지만 출력 파일의 순서가 원래의 입력 파일의 순서에서 바뀔 수 있다는 점입니다.


      따라서 위와 같은 병행 프로그램은 파일의 복사 순서에 무관한 경우는 올바른 프로그램이지만, 순서까지 유지해야 하는 복사의 경우에는 잘못된 프로그램이 됩니다.


      이러한 문제를 임계 구역(Critical Section) 문제라 하는데, 이의 해결책으로 상호배재(Mutual Exclusion) 도구들이 운영체제에 의해 제공됩니다.






     2.5 프로세스의 프로그램 교체(로딩)를 위한 시스템 호출 exec 그룹

      실행 중인 프로세스가 exec 시스템 호출 그룹 중에 하나를 호출하게 되면 실행 중인 프로세스의 text, data, stack 영역 등은 지정된 실행 파일의 내용으로 적재되어 새로운 프로그램이 실행됩니다. 프로세스의 몸체는 새로운 실행 파일의 내용으로 교체되지만, 프로세스 id, parent-child 관계, open 하였던 파일 등 여러 프로세스의 환경은 그대로 유지됩니다.

      exec 시스템 호출 그룹은 execl, execle, execv, execve, execlp, execvp 등 인수가 약간씩 다른 여러 개의 호출로 구성됩니다. 이들 중 대표적인 것으로 execve를 살펴보도록 합시다.







    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    EXECVE(2)
    #include <unistd.h>
     
    int execve(const char *path, const char *argv[], char const *envp[]);
     
        입력 값: 
            - path: 프로그램의 경로명
            - argv: 프로그램의 인수들에 대한 포인터
            - envp: 환경 변수로의 포인터
     
        반환 값:
            - 정상: 없음
            - 에러: -1
    cs

      





      exec 계열의 시스템 호출은 새로운 프로그램을 적재(load)하고 적재된 프로그램의 main 함수를 실행하는 것이므로 정상적인 경우에는 return 되지 않습니다. 단, 지정한 실행 파일이 없거나 실행이 불가능한 경우에는 -1을 반환하게 됩니다. 



    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    /*
     * fork.c
     *
     *  Created on: 2015. 8. 12.
     *      Author: palpit
     */
     
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <unistd.h>
    #include <stdlib.h>
    #include <string.h>
    #include <stdio.h>
     
    #define PROMPT "$>"
    #define LINE_LENGTH 80
     
    int main(void) {
     
        char buffer[LINE_LENGTH];
        pid_t pid;
        int c, count = 0;
        int background = 0;
     
        for (;;) {
            printf(PROMPT);
            count = 0;
     
            while (1) {
                if ((c = getchar()) == EOF)
                    exit(0);
     
                if (count < LINE_LENGTH)
                    buffer[count++= c;
     
                if (c == '\n' && count < LINE_LENGTH) {
                    buffer[count - 1= 0;
                    break;
                }
     
                if (count >= LINE_LENGTH) {
                    printf("input is Too Long !!\n");
                    count = 0;
                    printf(PROMPT);
                    continue;
                }
            } // End of while
     
            if (strlen(buffer) == 0
                    || (strlen(buffer) == 1 && buffer[strlen(buffer) - 1== '&'))
                continue// to for loop
     
            if (buffer[strlen(buffer) - 1== '&') {
                background = 1;
                buffer[strlen(buffer) - 1== '\0';
            }
     
            if ((pid = fork()) < 0) {
                perror("fork failed");
                exit(1);
            } else if (pid == 0) { // child process
                execlp(buffer, buffer, (char *0);
                perror("exclp failed");
                exit(1);
            }
     
            if (background == 1)
                continue;
     
            while (wait(&pid) != pid)
                ;    // wait for the exit of foreground
     
        } // end of for
     
        return 0;
    }
     
    cs






     위 프로그램은 간단한 쉘을 구성해 본 예제입니다. 위 프로그램을 수행되면 사용자 화면에는 "$>" 프롬프트(prompt)가 나타나고, 사용자는 자신이 실행시키고자 하는 프로그램을 입력하게 됩니다.


     parent 프로세스는 쉘과 같은 실행 형태로 child를 fork하고, fork 된 프로세스는 입력으로 받은 프로그램을 execlp를 통해 실행하게 됩니다.


     parent 프로세스는 fork와 exec를 통해 실행되는 child의 수행 완료를 waitpid를 통해 기다리고, child의 실행이 exit을 통해 완료되면 다시 프롬프트를 출력하여 다음 실행할 프로그램을 입력받습니다.


     단, 사용자의 프로그램 이름 입력시, "&" 기호를 붙이게 되면 이는 백그라운드 프로세스로 실행시키라는 의미로 parent는 child의 실행 완료를 기다리지 않고 바로 프롬프트를 출력하여 다음 프로그램 명을 입력 받습니다.


     백그라운드로 실행되는 프로세스가 언젠가 종료되면 지정된 포어그라운드(foreground)를 기다리는 wait loop에 의해 zombie 상태에서 소멸하게 됩니다.


    댓글

Designed by Tistory.