[운영체제] 프로세스(Process)

참고도서: Operating System Concepts (10/E) Abraham Silberschatz, Peter B. Galvin, Greg Gagne

Process

프로세스는 정말 단순히 표현하면, 실행 중인 프로그램이라고 할 수 있다. 우리가 흔히 말하는 프로그램은 application program 을 지칭하는 경우가 많은데, application program 자체는 storage 에 있는 코드나 실행프로그램 상태에 있는 프로그램을 의미하고 이 프로그램이 linker 와 loader에 의해 실행 가능한 형태로 변환되고 메모리에 적재되어 실행되면 그것을 process 라고 부른다.

프로세스의 메모리는 일반적으로 아래와 같은 구조를 가진다.

우리가 잘 알고있는 전형적인 메모리 구조이다. text 영역에는 프로그램의 실행 코드가 담겨있고, data 영역에는 프로그램이 끝날 때까지 사라지지 않는 전역변수가 위치해 있다. heap 영역은 동적으로 할당되는 정보들을 위한 공간이고 마지막으로 stack은 함수를 호출 할 때의 복귀 주소, 지역 변수 등을 모아둔 activation record를 저장해두기 위한 용도로 사용된다.

stack과 heap이 있다는 것은 단순히 프로세스가 명령어를 담고 있는 것이 아니라 프로그램 카운터와 여러 작업을 동적으로 수행하는 active entity 라는 것이다. 따라서 정적인 실행 파일을 가지고 있는 프로그램과 프로세스는 구분되어야 할 필요가 있다.

Process State

동적으로 변하는 프로세스는 프로세스의 활동에 따라 여러 상태를 가지게 된다. 이 상태의 이름은 시스템과 운영체제마다 다르지만 어떤 상태인지 그것이 의미하는 바는 모두 같다.

위 그림은 일반적인 프로세스의 상태의 흐름을 보여준다. 각 상태를 정의해보자.

  1. new : 새로운 프로세스가 생성 중인 상태.
  2. running : 프로세스가 가진 명령어들이 실행되고 있는 상태.
  3. waiting : 프로세스가 I/O가 보내는 신호 같은 이벤트를 기다리고 있는 상태.
  4. ready : 프로세스가 CPU가 어떤 작업을 할당해주기를 기다리고 있는 상태.
  5. terminated : 프로세스의 실행이 종료된 상태.

상태의 변화가 어떤 과정 속에서 일어나는지는 이후 장에서 확인해보자.

Process Control Block(PCB)

Process Control Block은 우리 말로 프로세스 제어 블록이라고 한다. 각 프로세스는 프로세스 제어 블록으로 운영체제에서 표현된다.

위 그림은 PCB의 구조를 보여준다. 그리고 위 그림에는 나와있지 않지만 PCB가 포함하는 정보들도 함께 살펴보자.

  1. Process State : 프로세스의 상태를 나타낸다. 위에서 설명했던, new, ready, terminated, waiting, running이 모두 이곳에서 표현된다.
  2. Program Counter : 프로그램 카운터는 이 프로세스가 다음에 실행할 명령어의 주소를 가르키고 있다.
  3. CPU Registers : 실제 프로세스가 실행되면서 사용되는 레지스터들이 이곳에 모여있다. 어떤 process scheduling 에 의해 프로세스가 정지된다면, 이 레지스터의 모든 정보가 저장되어 있어야 다시 작업으로 돌아왔을 때 계속해서 작업을 실행할 수 있다.
  4. CPU Scheduling Information : 프로세스의 우선순위와 스케줄 큐에 대한 포인터와 매개변수를 기록하고 있다.
  5. Memory Management Information : 운영체제에 의해 사용되는 메모리 시스템에 따라 기준 레지스터의 limit과 메모리 시스템에 따른 Page 나 Segment table 을 가지고 있다.
  6. Accounting Information : CPU 가 사용된 시간과 제한시간, 프로세스 번호들을 기록하고 있다.
  7. I/O Status Information : 프로세스에 할당된 입출력 장치와 열려있는 파일 목록 등에 대한 정보를 가지고 있다.

Process Scheduling

Multi-Programming이 고안되면서 CPU의 이용을 최대화 하기 위한 방법으로 CPU가 항상 어떤 프로그램을 실행하고 있도록 하는 전략이 만들어졌다. Time Sharing 이 그 방법론 중 하나인데, 여러 프로세스를 짧은 주기로 반복적으로 교체히면서 실행해서 사용자가 프로그램과 상호작용하는 것이 가능 하도록 한다.

그런데 이 방법이 실제로 구현되려면 Process Scheduler 가 꼭 필요하다. Process scheduler 는 실행 가능한 상태를 가지고 있는 프로세스들 중 하나를 선택하는 작업을 수행한다. 싱글코어 환경인 시스템에서는 CPU가 한번에 하나의 프로세스만 실행할 수 있지만, 멀티코어 환경에서는 여러 프로세스가 동시에 실행될 수 있게 된다. 만약 시스템이 가진 코어 수를 사용 가능한 프로세스의 수가 넘어섰을 때, 프로세스들은 Process scheduler에 의해 선택될 때까지 대기하고 있게 된다. 이렇게 메모리에서 실행을 대기하고 있는 프로세스의 수를 Multi-Programming Degree 라고 말한다.

Scheduling Queue

프로세스의 실행 순서를 관리하기 위해서 Ready Queue 라는 연결리스트를 사용한다. 프로세스는 실행되면 이 큐에 들어가 자신의 순서가 오기를 기다리게 되는데, 사실 큐에 프로세스가 직접 들어가는 것이 아니라, 큐가 PCB의 주소에 대한 포인터를 가지고 있어 각 PCB가 자기 다음 차례인 PCB를 가르키고 있는 구조가 된다. 그리고 이 프로세스 상태를 waiting 상태라고 말 할 수 있을 것이다.

프로세스가 실행이 되다가 I/O 와 같은 특정한 이벤트를 기다리게 되는데, 이때 역시 Wait Queue 라는 리스트로 삽입되어 다른 PCB와 연결되어 차례를 기다리게 된다.


위 그림에 따라 프로세스의 상태 흐름을 체크해보자. 일단 프로세스가 처음 실행되면 Ready Queue 로 들어가게 된다. 여기서 프로세스 스케줄러에 의해 선택되면 Ready Queue 에 있던 프로세스를 CPU가 실행시킨다. 실행 중이던 프로세스는 상황에 따라 여러 경우로 상태가 나뉘어 질 수 있는데, 만약 프로세스가 I/O 요청을 시작하게 되면 해당 프로세스는 Wait Queue로 들어가 I/O 작업이 끝나기를 대기하게 된다. 또 다른 경우에는 새로운 자식 프로세스를 만들기도 하는데 이런 작업을 fork 라고 한다. fork를 수행하고 부모가 된 프로세스는 일반적으로 자식 프로세스의 종료 신호를 기다리며 Wait Queue 에 위치하게 된다. 마지막으로 timer에 의해 지정된 시간이 지났을 경우에는 운영체제에 의해 강제적으로 프로세스가 CPU에서 제거되고 Ready Queue 로 들어가 스케줄러를 다시 기다리게 된다. 프로세스는 완전히 종료될 때 까지 위 흐름에 따라 반복적으로 작업을 수행하고 프로세스가 종료되면 모든 Queue에서 해당 PCB가 삭제되게 된다.

Scheduler

Scheduler는 short-term(CPU) schedulerlong-term(job) scheduler 로 나누어 질 수 있다. job scheduler는 저장 장치 안에 따로 저장되어 있는 프로세스 들 중에서 실행할 프로세스를 골라서 다시 메모리에 적재하는 일을 한다. 그리고 CPU scheduler 는 준비 큐에 있는 프로세스에 CPU 코어를 할당해주는 역할을 하게된다.

CPU scheduler를 short-term, Job Scheduler 를 long-term 이라고 부르는 이유는, 두 스케줄러가 얼만큼 자주 실행되는지에 따라 나누게 된 것이다. 새로운 프로세스를 메모리에 적재하는 일은 그다지 자주 일어나지 않고 muti programming degree에 따라 상황이 많이 바뀌지만 준비 상태인 프로세스를 실행시키는 작업은 빠르고 여러번 일어나게 된다.

우리는 프로세스를 말할 때 크게 두 특성으로 분류할 수 있는데, I/O bound process 는 프로세스 중 CPU 연산보다 I/O 처리에 더 많은 시간을 소비하는 프로세스를 지칭하고 CPU bound process는 계산에 더 많은 시간을 사용하는 프로세스를 지칭한다.

어떤 시스템이 효과적으로 사용되려면, I/O bound process와 CPU bound process가 적절한 숫자로 실행되고 있는 것이 좋을 것이다. 만약 시스템의 대부분의 프로세스가 I/O bound process 라면, 시스템의 ready queue 는 비어있는 경우가 많게 될 것이고 short-term scheduler가 할 수 있는 일이 거의 없어지게 될 것이기 때문이다. 이 때문에 long-term scheduler 가 적절한 비율로 두 타입의 프로세스를 가져와 메모리에 적재하는 것이 매우 중요하다.

어떤 운영체제에서는 short-term 과 log-term scheduler의 중간 형태로 swapping 이라는 mid-term scheduler를 사용하기도 한다. 이 방법을 간단하게 설명하면, 메모리에 있던 프로세스의 상태를 저장하고 다시 저장장치로 돌려보내고, 저장장치에 있던 프로세스를 메모리에 적재하고 중단 되었던 위치에서 실행할 수 있도록 하는 것이다.

Context Switch

프로세스의 실행 중에 interrupt 가 발생하면 운영체제는 CPU가 현재 하던 작업을 멈추고 Interrupt Service Routine 을 수행하도록 한다. 그리고 루틴의 모든 작업이 끝났을 때, 원래 하던 작업을 계속 수행하도록 해야하는데, 이를 위해서는 중단하는 프로세스에 대한 context 정보를 저장하는 것이 필요하다. 이 것을 위해 PCB 안에는 프로세스의 문맥정보를 담을 수 있는 공간이 존재하고 이곳에 레지스터의 값, 프로세스의 상태 등이 저장되게 된다.

이렇게 프로세스를 다른 프로세스로 전환하고 다시 돌아오는 과정을 context switch 라고 하는데, 정확히는 state save 를 통해 현재 상태를 저장하고 새로운 프로세스에 대한 상태 복구 작업인 state restore 작업이 순차적으로 이루어 진다. context switch 가 발생하게 되면 프로세스의 상태저장과 복구에 대해 시간을 소요하게 되는데, 이 시간동안에는 CPU가 다른 작업을 수행하지 못하기 때문에 pure overhead가 된다. 그리고 이 속도는 하드웨어에 따라 크게 좌우되게 된다.

아래 그림을 보면 context switch의 큰 흐름을 알 수 있을 것이다.


Operations on Process

프로세스에 대한 연산은 동적으로 활동하는 프로세스에 대해 새로운 프로세스의 생성과 실행 중인 프로세스를 종료시키고 삭제하는 작업이 될 것이다.

Process Creation

프로세스는 새로은 프로세스를 생성할 수 있고, 다수의 프로세스를 생성하는 것도 가능하다. 만약 어떤 프로세스가 새로운 프로세스를 생성했다면, 생성을 요청한 프로세스가 부모 프로세스가 되고, 새로 생성된 프로세스는 자식 프로세스가 된다.

대부분의 운영체제에서는 프로세스에 고유한 번호를 부여해서 각 프로세스를 식별할 수 있도록 하는데, 이 프로세스 번호를 Process Identification (pid) 라고 부른다. 컴퓨터 시스템이 부팅될 때 처음 실행되는 프로세스인 systemd(운영체제에 따라 init 인 경우도 있다.) 는 항상 pid로 1을 가지게 되고, systemd로부터 다양한 프로세스들이 자식 프로세스로 생성되기 시작한다.

자식 프로세스가 생성되면 부모 프로세스는 자식 프로세스와 함께 병행하게 계속 실행되거나, 자식 프로세스가 실행을 멈출 때까지 기다리는 방법 중 하나로 동작하게 된다. 자식 프로세스는 생성되면서 부모 프로세스와 똑같은 데이터를 가진채로 생성되거나 자식 프로세스가 자신에게 적재될 프로그램을 가지고 있게된다.

유닉스 운영체제에서의 프로세스 생성을 생각해보자.

  1. 새로운 프로세스는 fork 라는 system call을 통해서 생성된다. 이때 자식 프로세스는 부모 프로세스의 데이터를 모두 복사해서 가지고 있게 된다.
  2. fork 를 통해 두 개의 프로세스가 생성되면, 두 프로세스 중 하나가 exec system call를 사용해서 자신의 메모리 공간을 새로운 프로그램으로 덮어씌운다. 그리고 그 프로그램을 시작시킨다.
  3. 부모 프로세스는 달리 해야할 역할이 없다면 wait system call을 통해서 자식 프로세스가 종료되길 기다린다. 왜냐하면 부모 프로세스는 더 이상 ready queue 에 있을 필요가 없고 wait queue에서 자식 프로세스의 종료를 기다리면 되기 때문이다.

기억해야 할 점은 부모 프로세스가 종료될 때는 해당 프로세스가 fork의 복귀 코드로 0이 아닌 숫자를 반환하고, 자식 프로세스가 종료될 때는 fork 의 복귀 코드로 0이 반환된다.

Process Termination

프로세스는 가지고 있는 마지막 문장의 실행을 끝내고 exit system call을 사용해서 운영체제에게 프로세스의 종료를 알리게 된다. 그리고 wait queue 에서 자신의 종료를 기다리고 있던 프로새스에게 상태 값을 반환해준다.

자식 프로세스가 비정상적으로 작동하거나 스스로 종료하지 못하는 문제가 생겼을 경우에는 부모 프로세스가 자식 프로세스를 kill 할 수 있다. 어떤 시스템에서는 부모 프로세스가 종료되면 운영체제가 모든 다른 프로세스를 종료시키는 일 하게 되는데, 이런 작업을 cascading termination 이라고 부른다.

어떤 자식 프로세스가 종료되었을 때, 부모 프로세스가 아직 wait() 을 호출하지 않아서 자식 프로세스가 종료된 채로 계속 대기하고 있는 상태가 있을 수 있다. 이런 프로세스를 zombie 프로세스라고 한다. 일반적으로 wait()은 단시간 안에 호출 되기 때문에 zombie 프로세스가 존재하는 시간은 매우 짧을 것이다.

그렇다면 만약에 부모 프로세스가 자식 프로세스를 생성했지만 wait() system call의 호출없이 종료되었다면 어떻게 될까? 자식 프로세스는 갈 길을 잃게 되고 이런 프로세스를 orphan 프로세스라고 부른다. 전통적으로는 이런 프로세스가 발생하면 systemd에 해당 프로세스를 연결하고 systemd 프로세스는 주기적으로 wait() system call을 호출 하도록 설계해둔다.


Written by@전여훈 (Click Me!)
고민이 담긴 코드를 만들자, 고민하기 위해 공부하자.