포스트

[운영체제 아주 쉬운 세 가지 이야기 - Virtualization] 4. Processes

이 글은 제 개인적인 공부를 위해 작성한 글입니다.
틀린 내용이 있을 수 있고, 피드백은 환영합니다.


개요


이 장에는 운영체제가 제공하는 핵심 개념 중 하나인 프로세스(Process)에 대해 논의한다. 일반적인 프로세스는 실행 중인 프로그램으로 정의한다. 프로그램 자체는 생명이 없는 존재이다. 프로그램은 디스크 상에 존재하며 실행을 위한 명령어와 정적 데이터의 묶음이다. 이 명령어와 데이터 묶음을 읽고 실행하여 프로그램에 생명을 불어넣는 것이 운영체제이다.

사용자는 하나 이상의 프로그램을 동시에 실행시키기를 원한다. 예를 들어, 웹 브라우저, 메일, 게임, 음악 플레이어 등을 실행하는 데스크톱 컴퓨터를 생각해보자. 컴퓨터들은 동시에 수십 혹은 수백 개의 프로세스를 실행하는 것처럼 보인다. 여러 프로그램을 동시에 실행할 수 있으면, 시스템을 쉽게 사용할 수 있다. 사용자는 사용 가능한 CPU가 있는지 신경쓰지 않고 그저 프로그램만 실행시키면 된다.

그럼 CPU가 여러 개 존재한다는 환상을 어떻게 제공할까? 적은 개수의 CPU 밖에 없더라도, 운영체제는 어떻게 무한개에 가까운 CPU가 있는 듯한 환상을 만들 수 있을까?

운영체제는 CPU를 가상화하여 이러한 환상을 만들어 낸다. 하나의 프로세스를 실행하고, 얼마 후 중단시키고 다른 프로세스를 실행하는 작업을 반복하면서 실제 하나 또는 소수의 CPU로 여러 개의 가상 CPU가 존재하는 듯한 환상을 만들어 낸다. 시분할(time sharing)이라 불리는 이 기법은 원하는 수 만큼의 프로세스를 동시에 실행할 수 잇게 한다. 시분할 기법은 CPU를 공유하기 때문에, 각 프로세스의 성능은 낮아진다.

운영체제에서 CPU 가상화를 잘 구현하기 위해, 저수준의 도구와 고차원적인 지능이 필요하다. 저수준 도구를 메커니즘(mechanism)이라 한다. 메커니즘은 필요한 기능을 구현하는 방법이나 규칙을 의미한다. 예를 들어, 나중에 문맥 교환(context switch)의 구현에 대해 배우게 될텐데, CPU에서 프로그램 실행을 잠시 중단하고 다른 프로그램을 실행하는 것을 문맥 교환이라고 한다. 이 시분할 기법은 모든 현대 운영체제들이 채택하고 있다.

팁 : 시분할과 공간 분할
시분할은 자원 공유를 운영체제가 사용하는 가장 기본 기법 중 하나이다. 한 개체가 잠깐 자원을 사용한 후, 다른 개체가 또 잠깐 자원을 사용하고, 그 다음 개체가 사용하면서 이 자원(CPU 또는 네트워크 링크 등)을 많은 개체들이 공유한다. 시분할과 자연스럽게 대응되는 개념은 공간 분할(space sharing)이 될 것이다. 공간 분할은 개체에게 공간을 분할해 준다. 공간 분할의 예로 디스크가 있다. 디스크는 자연스럽게 공간 분할할 수 있는 자원으로, 블럭이 하나의 파일에 할당되면 파일을 삭제하게 전에는 다른 파일이 할당될 가능성이 낮다.

운영체제의 지능은 정책(policy)의 형태로 표현된다. 정책은 운영체제 내에서 어떤 결정을 내리기 위한 알고리즘이다. 예를 들어, 실행 가능한 여러 프로그램들이 있을 때, 운영체제는 어느 프로그램을 실행시켜야 하는가? 운영체제의 스케줄링 정책(scheduling policy)이 이러한 결정을 내린다. 이러한 결정을 내리기 위하여 스케줄링 정책은 과거 정보(직전 1분 동안 어떤 프로그램이 자주 실행되었는지), 워크로드에 관한 지식(어떤 유형의 프로그램들이 실행되었는지), 및 성능 측정 결과(시스템이 대화 성능 혹은 처리량을 높이려 하는지)를 이용한다.


프로세스의 개념


운영체제는 실행 중인 프로그램의 개념을 제공하는데, 이를 프로세스(process)라고 한다. 전술한 바와 같이 프로세스는 실행 중인 프로그램이다. 특정 순간의 프로세스를 간단하게 표현하려면, 실행되는 동안 접근했거나 영향을 받은 자원의 목록을 작성하면 된다.

프로세스의 구성 요소를 이해하기 위해서 하드웨어 상태(machine state)를 이해해야 한다. 프로그램이 실행되는 동안 하드웨어 상태를 읽거나 갱신할 수 있다. 이때 가장 중요한 하드웨어 구성 요소는 무엇일까?

프로세스의 하드웨어 상태 중 가장 중요한 구성 요소는 메모리이다. 명령어는 메모리에 저장된다. 실행 프로그램이 읽고 쓰는 데이터 역시 메모리에 저장된다. 프로세스가 접근할 수 있는 주소 공간(address space)라 불리는 메모리는 프로세스를 구성하는 요소이다.

레지스터도 프로세스의 하드웨어 상태를 구성하는 요소 중 하나이다. 많은 명렁어들이 레지스터를 직접 읽거나 갱신한다. 프로세스를 실행하는 데 레지스터도 빠질 수 없다.

프로세스의 하드웨어 상태를 구성하는 레지스터 중에 특별한 레지스터들이 존재한다. 프로그램 카운터(program counter, PC)는 프로그램의 어느 명령어가 실행 중인지를 알려준다. 프로그램 카운터는 명령어 포인터(instruction pointer, IP)라고도 불린다. 스택 포인터(stack pointer)프레임 포인터(frame pointer)는 함수의 변수와 리턴 주소를 저장하는 스택을 관리할 때 사용하는 레지스터이다.

프로그램은 영구 저장장치에 접근하기도 한다. 이 입출력 정보는 프로세스가 현재 열어 놓은 파일 목록을 가지고 있다.

팁 : 정책과 구현의 분리
운영체제에서 공통된 설계 패러다임은 정책과 기법을 분리하는 것이다.
정책 : “무엇을”할 것인가 -> 선택과 결정
예를 들어 어떤 프로세스를 먼저 실행할 것인가?

기법 : “어떻게”할 것인가 -> 구체적인 구현 방법
예를 들어 문맥 교환을 어떻게 수행할 것인가?

둘을 분리하면 정책을 바꿀 때 기법을 건드리지 않아도 되므로, 소프트웨어 설계 원칙인 모듈성의 한 형태이다.


프로세스 API


실제 프로세스 API 설명은 다음 장으로 미루겠지만, 운영체제가 반드시 API로 제공해야 하는 몇몇 기본 기능에 대해 간단히 살펴보자. 이 API들은 형태는 다르지만 모든 현대 운영체제에서 제공된다.

  • 생성 (Create) : 운영체제는 새로운 프로세스를 생성할 수 있는 방법을 제공해야 한다. 쉘에 명령어를 입력하거나, 응용 프로그램의 아이콘을 더블 클릭하여 프로그램을 실행시키면, 운영체제는 새로운 프로세스를 생성한다.
  • 제거 (Destroy) : 프로세스 생성 인터페이스를 제공하는 것처럼 운영체제는 프로세스를 강제로 제거할 수 있는 인터페이스를 제공해야 한다. 물론, 많은 프로세스는 실행되고 할 일을 다하면 스스로 종료한다. 그러나 프로세스가 스스로 종료하지 않으면 사용자는 그 프로세스를 제거하길 원할 것이고, 필요없는 프로세스를 중단시키는 API는 필요하다.
  • 대기 (Wait) : 어떤 프로세스의 실행 중지를 기다릴 필요가 있기 때문에 여러 종류의 대기 인터페이스가 제공된다.
  • 각종 제어 (Miscellaneous control) : 대부분의 운영체제는 프로세스를 일시정지하거나, 재개하는 기능을 제공한다.
  • 상태 (Status) : 프로세스 상태 정보를 얻어내는 인터페이스도 제공된다. 상태 정보에는 얼마 동안 실행되었는지 또는 프로세스가 어떤 상태에 있는지 등이 포함된다.


좀 더 자세한 프로세스 생성


그럼 프로그램을 어떻게 프로세스로 만드는가에 대해 알아보자. 운영체제는 어떻게 프로그램을 준비하고 실행시키는가? 실제로 어떻게 프로세스를 생성하는가?

image

프로그램 실행을 위하여 운영체제가 하는 첫 번째 작업은 프로그램 코드와 정적 데이터를 메모리, 프로세스의 주소 공간에 탑재(load)하는 것이다. 정적 데이터라 하면 초기 값을 가지는 변수 같은 static data이다. 프로그램은 디스크 또는 요즘 시스템에서는 플래시-기반 SDD에 특정 실행 파일 형식으로 존재한다. 코드와 정적 데이터를 메모리에 탑재하기 위해서 운영체제는 디스크의 해당 바이트를 읽어서 메모리의 어딘가에 저장해야 한다.

초기 운영체제들은 프로그램 실행 전에 코드와 데이터를 모두 메모리에 탑재하였다. 하지만 현대의 운영체제들은 이 작업을 늦춘다. 즉, 프로그램을 실행하면서 코드나 데이터가 필요할 때 필요한 부분만 메모리에 탑재한다. 코드와 데이터의 늦은 탑재의 동작을 정확하게 이해하기 위해서는 페이징(paging)스와핑(swapping) 동작의 이해가 필요하다. 이에 대해서는 나중에 메모리 가상화를 공부할 때 다루게 된다. 지금은 어떤 프로그램이든 실행시키기 전에 운영체제는 프로그램의 중요 부분을 디스크에서 메모리로 탑재해야 한다는 것만 기억하자.

코드와 정적 데이터가 메모리로 탑재된 후, 프로세스를 실행시키기 전에 운영체제가 해야 할 일이 몇가지 있다. 일정량의 메모리가 프로그램의 실행시간 스택(run-time stack, 혹은 그냥 스택) 용도로 할당되어야 한다. 이미 알고 있겠지만 C 프로그램은 지역 변수, 함수 인자, 리턴 주소 등을 저장하기 위해 스택을 사용한다. 운영체제는 스택을 주어진 인자로 초기화한다. 특히 main() 함수의 인자인 argc와 argv 벡터를 사용하여 스택을 초기화한다.

운영체제는 프로그램의 힙(heap)을 위한 메모리 영역을 할당한다. C 프로그램에서 힙은 동적으로 할당된 데이터를 저장하기 위해 사용된다. 프로그램은 malloc()을 호출하여 필요한 공간을 요청하고 free()를 호출하여 사용했던 공간을 반환하여 다른 프로그램이 사용할 수 있도록 한다. 힙은 연결 리스트, 해시 테이블, 트리 등 크기가 가변적인 자료 구조를 위해 사용된다. 프로그램이 실행되면 malloc() 라이브러리 API를 호출하여 메모리를 요청하고, 운영체제가 이를 충족하도록 메모리를 할당한다.

운영체제는 또 입출력과 관계된 초기화 작업을 수행한다. 예를 들어, UNIX 시스템에서 각 프로세스는 기본적으로 표준 입력(stdin), 표준 출력(stdout), 표준 오류(stderr) 장치에 해당하는 세 개의 파일 디스크립터(file descriptor)를 갖는다. 이 디스크립터들을 사용하여 프로그램은 터미널로부터 입력을 읽고 화면에 출력을 프린트하는 작업을 쉽게 할 수 있다. 입출력, 파일 디스크립터 등에 관해서는 영속성 파트에서 다룬다.

코드와 정적 데이터를 메모리에 탑재하고, 스택과 힙을 생성하고 초기화하고, 입출력 셋업과 관계된 다른 작업을 마치게 되면, 운영체제는 프로그램 실행을 위한 준비를 마치게 된다. 프로그램의 시작 지점 (entry point), 즉 main()에서부터 프로그램 실행을 시작하는 마지막 작업만이 남는다. main() 루틴으로 분기함으로써 운영체제는 CPU를 새로 생성된 프로세스에게 넘기게 되고 프로그램 실행이 시작된다.


프로세스 상태


프로세스가 무엇이고, 어떻게 생성되는지 알게 되었으므로, 프로세스의 상태(state)에 대해 알아보자. 프로세스 상태의 개념은 초기 컴퓨터 시스템에서 등장하였다. 프로세스 상태를 단순화하면 다음 세 상태 중 하나에 존재할 수 있다.

  • 실행 (running) : 실행 상태에서 프로세스는 프로세서에서 실행 중이다. 즉, 프로세스는 명령어를 실행하고 있다.
  • 준비 (ready) : 준비 상태에서 프로세스는 실행할 준비가 되어 있지만 운영체제가 다른 프로세스를 실행하고 있는 등의 이유로 대기 중이다.
  • 대기 (blocked) : 프로세스가 다른 사건을 기다리는 동안 프로세스의 수행을 중단시키는 연산이다. 예를 들어 프로세스가 디스크에 대한 입출력 요청을 하였을 때 프로세스는 입출력이 완료될 때까지 대기 상태가 되고, 다른 프로세스가 실행 상태로 될 수 있다.

그림에서 보듯이 프로세스는 준비 상태와 실행 상태를 운영체제의 정책에 따라 이동한다. 프로세스는 운영체제의 스케줄링 정책에 따라 스케줄이 되면 준비 상태에서 실행 상태로 전이한다.

image

실행 상태에서 준비 상태로의 전이는 프로세스가 나중에 다시 스케줄 될 수 있는 상태가 되었다는 것을 의미한다. 프로세스가 입출력 요청 등의 이유로 대기 상태가 되면 요청 완료 등의 이벤트가 발생할 때까지 대기 상태로 유지된다. 이벤트가 발생하면 프로세스는 다시 준비 상태로 전이되고 운영체제의 결정에 따라 바로 다시 실행될 수도 있다.

자료 구조 - 프로세스 리스트
운영체제에는 중요한 자료 구조들이 많이 존재하는데, 프로세스 리스트가 그 중 첫 번째이다. 이 자료 구조는 단순하다. 다수의 프로그램을 동시에 실행할 수 있는 모든 운영체제는 이와 유사한 자료 구조를 가지고 있고, 이 자료 구조를 이용하여 시스템에서 실행 중인 프로그램을 관리한다. 프로세스의 관리를 위한 정보를 저장하는 자료 구조를 프로세스 제어 블럭(Process Control Block, PCB)이라 부른다.


자료 구조


운영체제도 일종의 프로그램이다. 다른 프로그램들과 같이 다양한 정보를 유지하기 위한 자료 구조를 가지고 있다. 예를 들어, 프로세스 상태를 파악하기 위해 준비 상태의 프로세스들을 위한 프로세스 리스트(process list)와 같은 자료 구조를 유지한다. 또한, 어느 프로세스가 실행 중인지를 파악하기 위한 부가적인 자료 구조도 유지한다. 운영체제는 또 대기 상태인 프로세스도 파악해야 한다. 입출력 요청이 완료되면 운영체제는 적절한 프로세스를 깨워 준비 상태로 다시 전이시킬 수 있어야 한다.

그림을 통해서 운영체제가 관리하고 있는 프로세스 정보를 알 수 있다. 레지스터 문맥(register context) 자료 구조는 프로세스가 중단되었을 때 해당 프로세스의 레지스터 값들을 저장한다. 이 레지스터 값들을 복원하여 (해당 값을 실제 물리 레지스터에 다시 저장함으로써) 운영체제는 프로세스 실행을 재개한다. 문맥 교환(context switch)이라고 알려진 이 기법은 나중에 더 자세히 다룰 것이다.

image

그림에서 실행, 준비, 대기 외에 다른 상태들이 존재하는 것을 볼 수 있다. 초기(initial) 상태를 가지는 시스템도 있다. 프로세스가 생성되는 동안에는 초기 상태에 머무른다. 프로세스는 종료되었지만 메모리에 남아있는 상태인 최종(final) 상태도 있다. (UNIX 기반 시스템에서는 이 상태는 좀비 상태라고 불린다) 이 상태는 프로세스가 성공적으로 실행했는지를 다른 프로세스(보통은 부모 프로세스)가 검사하는 데 유용하다. 이를 위하여 최종 상태를 활용한다. (Unix 기반 시스템에서는 프로세스가 성공적으로 종료되면 0을, 그렇지 않으면 0이 아닌 값을 반환한다) 부모 프로세스는 자식 프로세스의 종료를 대기하는 시스템 콜(wait())을 호출한다. 이 호출은 종료된 프로세스와 관련된 자원들을 정리할 수 있다고 운영체제에 알리는 역할도 한다.


참고 링크

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.