[OS-02] Process

유영석·2023년 1월 28일
0

OS

목록 보기
2/12
post-thumbnail

Process 란?

프로세스란 말을 많이 들어보셨죠? 컴퓨터에서 프로세스는 가장 간단히 말해서 실행 중인 프로그램을 의미합니다. 실행을 위해 커널에 등록된 개체로서, 커널은 이 프로세스들을 관리하여 전체적인 시스템 퍼포먼스를 위해 노력하죠. 전형적인 시스템은 마치 수십 개, 아니 수백 개의 프로세스들이 동시에 돌아가는 것처럼 보입니다. 위 그림과 같이 코드와 데이터로 이루어진 저장된 실행 파일들이 프로세스에 실려 OS(커널) 의 관리에 의해 CPU 위에서 실행됩니다. 이 때 또 중요한 개념 중 하나가 CPU Virtualization 입니다. 실제 컴퓨터에는 제한된 개수의 CPU 가 장착 되어 있습니다. 이는 부족하기에 OS 가 Time Sharing 기법을 통해서 마치 많은 가상의 CPU 들이 존재하는 것과 같은 환상을 만들어 냅니다. 간단하게 설명해서 하나의 CPU 로 하나의 프로세스를 돌리다가 다른 프로세스를 돌리고.. 하는 식으로 시간 단위를 쪼개어 마치 여러 CPU 위에 여러 프로세스가 동작하고 있는 것처럼 보이게 하는 것이죠.

다시 한 번 정리하자면 프로세스는 실행 중인 프로그램, 커널에 의해 등록되고 관리되는 개체를 의미합니다. 다만 특별한 점은 active 한 개체라는 것이지요. 실행 동안 시스템 리소스를 요청하고, 할당하고, 풀어줄 수도 있다는 겁니다. 위는 익숙한 메모리 영역을 나타냅니다. 프로세는 아래와 같은 메모리를 포함하죠.

  • Program code - text 영역
  • Global data - data 영역
  • Temporary data(Local variables, Function parameters, Return addresses) - stack 영역
  • Dynamic data - heap 영역

또한, program counter, stack pointer, frame pointer 등을 지정하기 위한 레지스터 가져야 합니다. 또한, 프로세스가 연 파일들에 대한 리스트, 즉 I/O information 도 가지고 있어야 하죠.

PCB(Process Control Block)

커널이 프로세스를 통제하기 위해 각각의 프로세스에 대한 정보를 담고 있는 단위를 우리는 PCB(Process Control Block) 이라 부릅니다. PCB 의 정보들은 OS 마다 다릅니다. 리눅스에서는 Process descriptor 로서 tast_sturct 라는 구조체가 그 역할을 하죠. 물론 커널이 이 PCB 에 접근하는 속도는 전체 시스템 퍼포먼스에 큰 영향을 끼치게 됩니다. Task Control Block 이라고도 불리는 PCB 는 아래와 같은 정보들을 담고 있습니다.

  • PID (Process Identification Number)
  • Process state : running, waiting, 등.
  • Program counter : 다음에 실행되어야 할 명령어의 위치
  • CPU registers : 프로세스에 관여하고 있는 레지스터들의 값
  • Scheduling information : 프로세스 우선순위, 스케줄링 파라미터
  • Memory management information : Base/limit registers, page tables, segment tables, 등.
  • I/O status information : 할당된 I/O 디바이스 리스트, 열려있는 파일의 리스트, 등.
  • Accounting information : 사용되는 CPU, clock time, time limits
  • Context save area : 프로세스의 context 를 저장하는 공간

프로세스의 이모저모

우리가 직접 프로세스를 통재하기 위해서는 Process API 를 사용해야 합니다. 크게 아래와 같은 기능들이 있죠.

  • Create : 새로운 프로세스를 생성합니다. 우리가 원하는 프로그램을 돌리기 위해 OS 가 새로운 프로세스를 만드는 것입니다.
  • Destroy : 프로세스를 강제로 없애는 인터페이스가 되겠습니다.
  • Wait : 프로세스가 실행을 멈추기를 기다립니다.
  • Miscellaneous Control : 실행 중인 프로세스를 잠시 멈추고 대기를 시키고, 다시 실행하기 위해 재개합니다.
  • Status : 현재 프로세스에 대한 상태 정보

프로세스 상태에는 Running, Ready, Blocked 가 있습니다. Running 은 프로세스가 프로세서에 올라가 명령어들을 실행시키고 있는 상태를 의미합니다. 말그대로 프로그램이 실행되는 것이죠. Ready 는 프로세스가 다시 실행될 준비가 된 상태를 의미합니다. 준비는 되었는데 OS 가 어떤 이유로 인해 지금 당장 실행을 시키지는 않고 있는 겁니다. Blocked 는 다른 이벤트가 완료될 때까지 Ready 상태가 될 수 없는 상태를 의미합니다. 대표적인 예시로 I/O 요청이 있습니다. 프로세스가 요청한 디스크로부터의 I/O 가 완료될 때까지 프로세스는 멈춰있어야 하지요.

프로세스가 생성되는 과정은 어떻게 될까요? 순서대로 알아봅시다.

  1. 프로그램은 디스크(현재는 SSD) 에 샐행 가능한 포맷으로 저장되어 있습ㄴ디ㅏ.
  2. 프로그램 코드와 정적 메모리를 데이터에 로드합니다.
  3. 런타임 스택을 할당하고 argc, argv 와 함께 초기화 합니다.
  4. 힙을 할당합니다.
  5. I/O 와 관련된 초기화 태스크를 진행합니다.
  6. main() 으로 점프하여, CPU 컨트롤을 새롭게 생성된 프로세스로 옮깁니다.

이러한 프로세스는 PCB 의 pid 를 통해 식별되고 관리 됩니다. 특별한 점은 프로세스는 트리 의 구조를 한다는 것입니다. 어떤 프로세스에서 새로운 프로세스를 생성하는 것은 자식 노드(프로세스)를 만드는 것과 같습니다.

이 때 부모와 자식이 리소스들을 모두 공유할 수도, 공유하지 않을 수도, 아니면 자식이 부모의 리소스 중 일부만을 공유하도록 지정할 수 있습니다. 뿐만 아니라 부모와 자식 프로세스가 concurrently 하게 동시에 실행될 수도, 자식이 종료될 때까지 부모 프로세스가 기다리도록 할 수도 있죠.

리눅스의 ps 명령어로 현재 실행중인 프로세스들을 확인할 수 있습니다. 위의 예시를 보면 컴퓨터를 키면 기본적으로 실행되는 systemd, kthreadd 들을 부모로 수많은 자식 프로세스들을 확인할 수 있습니다. 이제 리눅스에서의 프로세스 API 들을 살펴보도록 하겠습니다.

Process API (Linux)

가장 먼저 프로세스를 생성하는 fork() 입니다.

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

int main(int argc, char *argv[])
{
    printf("hello world (pid:%d)\n", (int)getpid());
    int rc = fork();
    if (rc < 0)
    { // fork failed; exit
        fprintf(stderr, "fork failed\n");
        exit(1);
    }
    else if (rc == 0)
    { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int)getpid());
    }
    else
    { // parent goes down this path (main)
        printf("hello, I am parent of %d (pid:%d)\n", rc, (int)getpid());
    }
    return 0;
}, I am parent of %d (pid:%d)\n", rc, (int) getpid()); }
return 0; }

fork() 실행 시 호출 프로세스, 즉 부모 프로세스의 모든 주소 공간이 복제가 됩니다. 쉽게 말해 프로세스의 복사본이라 생각하시면 됩니다. 자식 프로세스도 이 똑같은 코드를 실행하는 것이죠. 이 때 부모 프로세스에서는 fork() 의 리턴 값이 rc 는 자식 프로세스의 PID 가 되고 자식 프로세스에게는 0 이 되게 됩니다.

그렇기에 스케줄링 따라 자식과 프로세스 중 어떤 것이 prinf 가 먼저 실행되는지가 다르기 때문에 위와 같은 두 가지 상황이 발생하게 됩니다. 자식 프로세스에게 rc 는 0 이지만 getpid() 함수를 통해서 자신의 고유 PID 를 가져오면 됩니다.

다음으로 wait() 함수 입니다. 위와 똑같은 코드에서 else 구문을 다음과 같이 바꿔봅니다.

    else
    { // parent goes down this path (main)
        int wc = wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n", rc, wc, (int)getpid());
    }

부모 프로세스에서 wait() 을 호출하게 되면 자식 프로세스에서 실행이 종료(exit())될 때까지 기다리게 됩니다. 이 때의 리턴값으로 기다렸던 자식 프로세스의 PID 가 됩니다.

그래서 결과는 아래와 같이 항상 자식 프로세스가 먼저 실행되게 되는 겁니다.

그 다음으로 exec() 은 새로운 프로세스를 생성하지만, 자식 프로세스가 되는 것이 아닌 새로운 프로세스로 대체됩니다. 프로세스가 바뀌게 되는 것이지요. 앞에서 자식 프로세스에 해당하는 if 문을 아래와 같이 바꿔봅시다.

    else if (rc == 0)
    { // child (new process)
        printf("hello, I am child (pid:%d)\n", (int)getpid());
        char *myargs[3];
        myargs[0] = strdup("wc");      // program: "wc" (word count)
        myargs[1] = strdup("p3.c");    // argument: file to count
        myargs[2] = NULL;              // marks end of array
        execvp(myargs[0], myargs);     // runs word count
        printf("this shouldn't print out");
    }

exec() 에는 execl(), execlp(), execle(), execv(), execvp(), execvpe() 등 다양한 변형이 있고 위는 execvp 를 사용한 예시입니다. 자식 프로세스는 새로운 wc 라는 프로세스로 대체된 것을 확인할 수 있습니다. 따라서, 밑의 printf 문은 execvp() 가 성공적으로 호출됬다면 실행되지 않게 됩니다.

마지막으로 프로세스의 종료는 exit() 시스템 콜을 행하여 OS 에 요청할 수 있습니다. OS 에 의하여 리소스들은 모두 해제되며 직전에 배운 wait() 에서 아래와 같이 매개변수를 통하여 상태 데이터들이 반환되게 됩니다.

pid = wait(&status)

exit() 은 프로세스가 자체적으로 자신의 실행을 종료하지만, 부모 프로세스가 자식 프로세스를 강제로 종료하는 abort() 시스템 콜도 존재합니다. 자식 프로세스가 너무 많은 리소스를 잡아먹고 있거나 자식 프로세스가 잡고 있는 작업이 더 이상 필요 없어질 때 사용할 수 있겠지요?

Zombie & Orphan

지금까지 배웠듯, 당장 위 그림에서도 확인할 수 있다시피 부모 프로세스는 자식 프로세스를 만들어 종료될 때까지 기다린 후 상태 정보를 회수한 뒤 자신의 일을 재개하는 것이 일반적입니다. 그러나, 이런 경우도 있을 수 있지요.

부모가 자식을 기다리지 않는 경우입니다. 그래서 자식은 종료는 되었는데 부모 프로세스로부터 리소를 회수받지 못한 것입니다. 이런 자식 프로세스를 Zombie, 좀비 프로세스라고 부릅니다. 더 심하게는 부모 프로세스가 먼저 종료가 되어버릴 수 있습니다. 이런 경우 Orphan 이라고 하는데, 이럴 때는 reaper 가 모든 orphan 들의 부모가 되어 회수함으로써 해결하죠.

Queue

프로세스는 컴퓨터 내에서 Queue 의 형태로 관리됩니다.

대표적으로 ready 상태에 있는 프로세스들을 보관하는 Ready queue 가 있습니다. 프로세서를 요청 중이며, 모든 다른 리소스들은 할당이 되어있는 상태이지요. 실행되기 위해 프로세서로만 가면 되는 친구들입니다. 프로세서가 이용 가능해질 때 여기서 실행할 프로세서를 선택해야 하는 데 이를 Process scheduling 이라 합니다. 뒤에 공부하게 될 것입니다. 또한 blocked 상태에 있는 프로세스들은 I/O queue(device queue) 에 보관됩니다. 디바이스 응답이 끝나 interrupt 가 이루어지면 비로소 ready queue 로 이동하게 되는 것이지요. 이를 개괄적으로 나타낸 그림은 아래와 같습니다.

profile
백엔드 개발자

0개의 댓글