Computer Science Basics/운영체제

OS - 2. Processes

타자치는 문돌이

프로세스란?

실행되고 있는 프로그램을 프로세스라고 한다.
프로그램은 디스크에 저장된 명령어의 집합이고,
프로세스는 프로그램을 RAM으로 올려 프로그램 카운터와 함께 실행 중인 상태이다.
프로그램을 실행할 때 OS가 CPU와 메모리를 어떻게 동작시키는지 알아보자.


프로세스와 메모리

프로그램이 메모리에 로드되면 메모리의 Stack, Heap, Data, Text 영역에 데이터가 저장된다.
Text 영역에는 프로그램 코드가,
Data 영역에는 전역 변수가,
Stack 영역에는 임시 데이터(함수 인자, return address, 지역 변수 등),
Heap 영역에는 동적으로 할당한 데이터가 저장된다.


프로세스의 상태

프로세스가 실행되면 5가지 중에 하나의 상태이며, 상황에 따라 상태가 바뀐다.

  • new : 프로세스가 생성됨
  • ready : 프로세스가 CPU에 할당되기 전에 대기 중
  • running : 명령어가 실행 중
  • waiting : 이벤트가 일어날 때까지 대기
  • terminated : 프로세스가 실행을 마치고 종료


Process Control Block (PCB)

CPU가 실행하던 프로세스를 중단하고 다른 프로세스를 실행한 뒤, 언젠가는 다시 중단한 프로세스를 이어서 실행해야 한다.
이를 위해선 실행 중이던 프로세스의 상태를 저장해두어야 한다.
프로세스는 PCB를 통해 상태를 저장한다.

PCB는

  • Process State
  • Process Number
  • Program Counter
  • CPU Register
  • CPU Scheduling Information
  • Memory-management Information
  • Accounting Information
  • I/O State Information
    를 담고 있는 자료구조이다.

프로세스 스케줄링

멀티프로그래밍의 목표는 여러 프로세스가 항상 실행되도록 해 CPU의 효율을 높이는 것이다.
Time-Sharing의 목표는 CPU가 사용하는 프로세스를 매우 짧은 시간 동안 계속 바꿔 여러 프로세스가 동시에 실행되는 착각을 일으키는 것이다.
그러나 프로세서는 한 번에 한 프로세스만 처리할 수 있기 때문에 어떤 프로세스를 다음에 처리할지 정하는 프로세스 스케줄링이 필요하다.


프로세스 스케줄링 큐

프로세스는 running 상태에서 다른 프로세스로 주도권이 넘어가 ready 상태가 되거나, I/O를 기다리는 waiting 상태가 된다. 이 프로세스들은 각각 Ready Queue와 Device Queue (I/O Waiting Queue)에 저장되어 자기 차례가 되면 큐에서 나와 running이나 ready 상태가 된다.

이러한 프로세스 스케줄링은 CPU 스케줄러가 처리한다. CPU 스케줄러는 CPU가 처리할 다음 프로세스를 정하고 CPU를 할당한다. 스케줄러는 밀리세컨드 단위로 작업을 진행하고, 뒤에서 설명할 swapping이라는 방식으로 작동한다.


Context Switch

스케줄러가 다음 프로세스를 정하면 실행 중이던 프로세스는 자신의 Context (Program Counter, CPU 레지스터값, 프로세스 상태, 메모리 정보 등)을 저장해야 다시 자신의 차례가 왔을 때 하던 작업을 이어서 할 수 있다. 이렇게 현재 상태를 저장하고, 다시 진행 중이던 Context를 불러오는 과정을 Context Switch라고 한다. Context Switch 동안에 시스템은 다른 작업을 할 수 없다. 따라서 이 시간이 작을수록 효율적이고, 하드웨어에 따라 필요한 시간이 다르다.


프로세스 연산

프로세스 생성

프로세스는 Process Identifier (pid)라는 번호를 받아 관리된다.
프로세스는 Parent Process가 Child Process를 생성하고, Child가 Parent가 되어, 또다른 Child Process를 생성하는 트리 구조로 생성된다.

프로세스를 생성할 때는 몇 가지 옵션을 줄 수 있다.

  • 자원 공유 옵션 : Parent와 Child가 자원을 모두 공유하거나, 일부만 공유하거나, 공유하지 않을 수 있다.
  • 실행 옵션 : Parent와 Child가 동시에 실행되게 하거나, Child가 끝날 때까지 Parent가 기다리게 할 수 있다.
  • 주소 공간 옵션 : Child 프로세스가 메모리 공간을 확보할 때, Parent의 메모리 내용을 복사하거나, 자기가 프로그램을 로드할 수 있다.

UNIX 시스템의 예시를 보자.
UNIX 시스템에서 Parent 프로세스가 fork()를 실행하면, Child 프로세스가 생성된다.
fork() 실행 시점부터 Parent, Child의 두 프로세스가 실행되는데, 이때 Child 프로세스는 주소 공간 옵션의 Parent 복사가 적용되어 Parent와 같은 메모리 내용을 가진다.
두 프로세스는 같은 코드의 실행을 이어가는데, 이때 두 프로세스의 pid는 다르다. Parent는 getpid의 반환 값이 Child의 pid로 0보다 크고, Child의 getpid는 0을 반환한다.
분기 이후에 Child가 다른 프로그램을 실행하려면, exec 시스템 콜을 실행해 메모리 내용을 리셋해 새 프로그램을 로드한다.

이런 코드가 있다고 해보자

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

main(int argc, char *argv[])
{
    int childpid, myid;
    childpid = fork();

    if ( childpid < 0 )
    {
    /* error occurred */
    fprintf(stderr, "Fork Failed");
    exit(-1);
    }
    else if ( childpid == 0 )
    {
    /* child process */
    myid = getpid();
    execlp("/bin/ls", "ls", NULL);
    }
    else
    {
    /* parent process */
    myid = getpid();
    wait(NULL);
    printf("Child Complete");
    exit(0);
    }
}

childpid = fork();부터 Parent 프로세스와 Child 프로세스로 분기된다.
Parent 프로세스의 childpid는 Child 프로세스의 pid이므로 0보다 큰 수로 else 문이 실행된다.
Child 프로세스의 childpid는 0으로 else if (childpid == 0)이 실행된다.

else문에는 wait(NULL)이 있어 Parent 프로세스는 Child Process가 끝날 때까지 기다린다.
else if (childpid == 0)에는 exec의 한 종류인 execlp이 있다. 인자로 준 프로그램을 실행하는 명령어로 "/bin/ls"를 실행하는 프로그램을 메모리에 로드할 것이다. 프로세스가 끝나면 Parent 프로세스의 wait이 끝난다.


프로세스 제거

프로세스가 마지막 명령어를 실행하면, OS는 exit(status) 시스템 콜을 호출에 프로세스를 지우는 작업을 실행한다. exit(status)가 실행되면, Parent는 wait(&status)로 종료된 Child가 반환한 상태 값을 얻고, Child의 리소스는 OS가 해제한다.

Parent가 kill 명령어로 Child를 강제로 종료할 수도 있다. Child가 리소스를 초과해서 사용하거나, Child의 작업이 더 이상 필요가 없거나, Parent가 종료되면 Child가 필요 없어져 강제로 종료한다.

Zombie Process

프로세스가 끝났는데 아직도 Process Table에 남아 있으면 이 프로세스를 Zombie Process라고 한다.
버그나 에러로 프로세스가 종료되었지만 Parent가 Status를 받지 못했으면 Zombie Process가 된다.
보통 Parent가 Status를 받기까지 매우 짧지만 시간이 소요되므로 모든 프로세스가 잠깐 Zombie Process가 된다는 말은 맞는 말이긴 하다.

Orphan Process

Parent가 종료됐는데 Child가 실행 중이라면, 이 프로세스를 Orphan Process라고 한다.
이런 경우 보통 init 프로세스를 Child의 Parent로 할당해 줘 Child가 종료되었을 때 Status 값을 반환하도록 한다.


프로세스와 메모리

Paging

각 프로세스는 메모리에 사용 가능 공간을 할당받는다. 이 공간을 Process Address Space라고 한다.
이때 Paging이라는 기법을 사용한다.
우리가 0x100만큼의 메모리가 필요할 때, RAM의 실제 주소 0x000부터 0x100까지 싹둑 잘라서 주는 방식이 그다지 효율적이지 못하다. (6단원에서 다룬다) 대신 가상의 Logical Memory가 있다고 가정하고, 이 메모리를 Page라는 단위로 자른다. 그리고 실제 Physical Memory도 같은 크기의 Frame이라는 단위로 자른다. 그리고 Page에 Frame을 매칭하고, 매칭 내용은 Page Table이라는 표에 저장한다. 이러면 Physical Memory는 연속되지 않더라도 Logical Memory는 연속되게 할당해 줄 수 있다.

프로세스는 Logical Memory를 기준으로 메모리를 사용하고, 할당받는다.
프로세스가 malloc과 같은 함수로 추가 메모리를 요청하는데 할당받은 메모리로는 감당이 안된다면 새 범위의 Virtual Address Space를 할당받는다.

다시 Paging으로 돌아와서, CPU가 Page Table에서 원하는 Physical Address를 빨리 찾을수록 프로세스의 실행이 빠를 것이다. 더 빨리 주소를 찾기 위해 Page Address를 Page Number와 Page Offset으로 나누어 구성하는 방법을 사용한다.

Page Number는 반 같은 것이다. Page Table의 Index로 사용된다. Table을 보고 Page Number에 해당하는 위치를 찾는다.
Page Offset은 번호 같은 것이다. 어차피 같은 Frame을 Page와 같은 크기로 잘랐기 때문에 Page 1의 3번째 주소는 Page 1이 저장된 Physical Frame의 3번째 주소와 같다. Page Table을 보고 5반으로 가서 3번째 자리에 있을 3번 학생을 바로 찾을 수 있듯이 말이다.

Logical Address 0x222333에 접근할 때, 0x222 Page의 Frame을 찾은 뒤, Frame+0x333에 접근하면 된다.
이러면 주소만 보고도 Page Table에서 Frame을 찾을 수 있고, Frame을 찾으면 바로 원하는 주소를 찾을 수 있다.


프로세스 간 통신

보통 프로세스는 다른 프로세스의 영향을 받지 않는다. 그러나 프로세스끼리 정보를 공유하면 더 효율적인 경우도 있다. 이렇게 통신이 가능한 프로세스를 Cooperating Process라고 한다.
두 프로세스가 Cooperate 하기 위해

  • 메모리를 공유하거나 (Shared Memory)
  • 메시지 패싱 시스템을 통해 정보를 직접 보내고 받을 수 있다. (Signal, Pipe, Socket….)

자세한 구현 내용은 넘어가자.

 

 

OS - 0. Index