"추상화: 프로세스"
이 장에서는 OS가 제공하는 가장 기본적인 추상화 중 하나인 '프로세스'에 대해 논의한다. 비공식적으로 프로세스의 정의는 실행 중인 프로그램이다. 프로그램은 실행을 기다리는 일련의 명령어(정적 데이터)이다. 디스크에 놓여 있다가, 운영 체제가 이러한 바이트를 가져와 실행하게 함으로써, 프로그램을 유용한 것으로 변환하고 이를 프로세스라 한다.
우리는 한 번에 한 개 이상의 프로그램을 실행하고 싶은 경우가 종종있다. 예를 들어, 데스크톱이나 노트북에서 웹 브라우저, 메일 프로그램, 게임, 음악 플레이어 등을 실행하고 싶을 수 있다. 이때 시스템은 동시에 수십 또는 수백 개의 프로세스를 실행하는 것처럼 보인다.
문제 핵심:
물리적 CPU가 몇 개뿐이다.
그런데 OS는 어떻게 수많은 CPU 가 있는 것처럼 느껴지도록 우리에게 서비스를 제공하는 걸까?
OS는 CPU를 가상화함으로써 이를 가능케 한다. 대표적으로 한 프로세스를 실행한 다음 중지하고 다른 프로세스를 실행하는 등의 방법을 사용한다.
CPU의 가상화를 잘 구현하기 위해, OS는 저수준 기계장치(low-level memory)와 고수준 지능(high-level intelligence)이 필요하다. 여기서 전자를 메커니즘이라 한다. 메커니즘은 필요한 기능을 구현하는 저수준 방법이나 프로토콜이다.
Tip: Time sharing (및 Space sharing)
- 사용시간 공유는 OS가 리소스를 공유하는 데 사용하는 기본적인 기술
- 하나의 엔티티가 잠시 동안 리소스를 사용한 후 다른 엔티티가 잠시 동안 사용하는 식으로 계속함
- 해당 리소스(예: CPU 또는 네트워크 링크)는 많은 사람들에 의해 공유될 수 있음
- 시간 공유의 대응 개념은 공간 공유로, 리소스가 사용하고자 하는 사람들 사이에서 공간적으로 나뉨
- 예를 들어, 디스크 공간은 공간 공유 리소스 (한 블록이 파일에 할당되면, 사용자가 원본 파일을 삭제하기 전까지 다른 파일에는 할당되지 않음)
많은 운영 체제에서 흔한 설계 패러다임은 고수준 정책을 저수준 메커니즘과 분리하는 것이다. 메커니즘은 시스템에 대한 '어떻게' 하는지에 대한 질문에 대한 답을 제공한다.
예를들어,컨텍스트 스위치는 OS에게 주어진 CPU에서 한 프로그램의 실행을 중단하고 다른 프로그램을 실행하기 시작할 수 있도록 한다. 이러한 메커니즘 위에는 OS 내의 정책 형태로 일종의 지능이 존재한다. (메커니즘<->정책)
정책은 OS 내에서 어떤 종류의 결정을 내리기 위한 알고리즘이다. 예를 들어, CPU에서 실행할 수 있는 여러 프로그램들 중 OS는 어떤 프로그램을 실행해야 할지 결정해야 한다고 할때, OS의 스케줄링 정책은 이러한 결정을 내리는 것이다. 이때 이전 정보(예: 지난 분 동안 어떤 프로그램이 더 많이 실행되었나?), 워크로드 지식(예: 어떤 종류의 프로그램이 실행되나?), 성능 메트릭(예: 시스템이 상호작용 성능 또는 처리량을 최적화하나?)등의 여러정보로 결정을 내린다.
OS에 의해 제공되는 추상화는 '프로세스'라고 부른다. 위에서 언급했듯이, 프로세스는 단순히 실행 중인 프로그램이다.
프로세스가 무엇인지 이해하기 위해서는 그 기계 상태를 이해해야 한다. 먼저 프로세스를 구성하는 기계 상태의 하나의 명백한 구성 요소는 그 메모리이다. 명령어, 실행 중인 프로그램이 읽고 쓰는 데이터등이 메모리에 있다.
또한 프로세스의 기계 상태에는 레지스터도 포함된다. 많은 명령어가 명시적으로 레지스터를 읽거나 업데이트한다.여기서 특별히 중요한 레지스터들이 있다. 예를들어, 프로그램 카운터(PC) (명령 포인터,IP)는 다음에 실행될 프로그램의 어떤 명령인지 알려준다.
프로세스 API
• 생성: 운영 체제는 새로운 프로세스를 생성하는 방법을 포함해야 한다. (쉘에 명령어를 입력하거나 애플리케이션 아이콘을 더블 클릭할 때)
• 파괴: 완료되면 스스로 종료하거나,사용자가 프로세스를 종료
• 대기: 때로는 프로세스가 실행을 멈출 때까지 기다리는 것
• 기타 제어: 프로세스 일시 중지(잠시 동안 실행 중지) / 다시 시작(계속 실행)하는 방법
• 상태: 프로세스에 대한 일부 상태 정보를 얻는 인터페이스 (얼마나 오래 실행되었는지, 또는 어떤 상태인지 등)
그렇다면 프로그램이 프로세스로 어떻게 변환될까? OS는 어떻게 프로그램을 실행시키고, 프로세스 생성은 실제로 어떻게 작동하는가?
프로그램을 실행하기 위해 OS가 해야 할 첫 번째 일은 해당 프로그램의 코드와 정적 데이터(예: 초기화된 변수들)를 메모리에 로드하는 것이다. 이것은 프로세스의 주소 공간에 해당한다. 프로그램은 처음에 디스크(또는 일부 현대 시스템에서는 플래시 기반 SSD)에 실행 가능한 형식으로 존재한다. 따라서 프로그램과 정적 데이터를 메모리에 로딩할때, OS가 디스크에서 해당 바이트를 읽고 메모리에 이를 적재한다.
초기(또는 간단한) 운영 체제에서는 로딩 과정이 프로그램을 실행하기 전에 한 번에 이뤄졌지만 현대 OS는 이 과정을 게으르게(lazy) 수행한다. 즉, 프로그램 실행 중 필요할 때만 코드 또는 데이터 조각을 로딩한다. 코드와 데이터 조각의 게으른 로딩 방식을 진정으로 이해하기 위해서는 페이징 및 스와핑의 메커니즘에 대해 더 알아야한다 (이는 나중에 공부).
코드와 정적 데이터가 메모리에 로드되면, OS는 프로세스를 실행하기 전에 몇 가지 다른 작업을 수행해야 합니다. 프로그램의 실행 시간 스택(또는 단순히 스택)에 대한 메모리가 할당되어야 한다. 예를들어, C 프로그램은 지역 변수, 함수 매개변수, 반환 주소에 스택을 사용합니다; OS는 이 메모리를 할당하고 프로세스에 제공한다. OS는 또한 main() 함수의 매개변수, 즉 argc와 argv 배열로 스택을 초기화할 수도 있다.
OS는 프로그램의 힙에도 메모리를 할당할 수 있다. 예를들어 프로그램은 malloc()을 호출하여 이러한 공간을 요청하고 free()를 호출하여 명시적으로 해제할 수 있다.
OS는 또한 입력/출력(I/O)과 관련하여 다른 초기화 작업을 수행한다. 예를 들어, UNIX 시스템에서 각 프로세스는 기본적으로 표준 입력, 출력, 오류에 대한 세 개의 열린 파일 디스크립터를 가지고 있다. 이러한 디스크립터를 통해 프로그램은 쉽게 터미널에서 입력을 읽고 화면에 출력을 출력할 수 있다.
정리해보자. 메모리에 코드와 정적 데이터를 로딩하고, 스택을 생성하고 초기화하며, I/O 설정과 관련된 다른 작업을 수행한다. OS는 이제야 프로그램 실행을 위한 준비를 마친 것이다. 따라서 마지막 할 일은 바로 진입점인 main()에서 프로그램을 실행하는 것이다. main() 루틴으로 점프함으로써(OS가 다음 장에서 논의할 특별한 메커니즘을 통해) OS는 CPU의 제어권을 새로 생성된 프로세스에게 넘겨주고, 비로소 프로그램 실행을 시작한다.
간단하게 보면, 프로세스는 세 가지 상태 중 하나에 있을 수 있다.
• 실행 중: 즉, 명령어를 실행하고 있는 것
• 준비 중: 프로세스가 실행할 준비가 되어 있지만 OS가 실행하는 중은 아님
• 차단됨: 차단 상태에서는 프로세스가 어떤 종류의 작업을 수행하여 다른 이벤트가 발생하기 전까지 실행 준비가 되지 않은 상태 (일반적인 예시로, 프로세스가 디스크에 I/O 요청을 시작할 때, 차단되어 다른 프로세스가 프로세서를 사용할 수 있게된다.)
이러한 상태들을 그래프로 나타낸다면, 그림 4.2와 같다. 다이어그램에서 볼 수 있듯이, 프로세스는 OS의 재량에 따라 준비와 실행 상태 사이에서 이동한다. 준비 상태에서 실행 상태로 이동한다는 것은 프로세스가 스케줄링되었다는 것을 의미하며, 실행 상태에서 준비 상태로 이동한다는 것은 프로세스가 스케줄링 해제되었다는 것을 의미한다.
이러한 상태들을 통해 두 프로세스가 어떻게 전환될 수 있는지 예를 들어 보자. 먼저, CPU만 사용하는 두 프로세스가 실행되고 있다고 가정해 보자. (I/O를 수행 X 가정). 이 경우, 각 프로세스의 상태 추적은 그림 4.3와 같다.
다음 예시에서, 첫 번째 프로세스는 일정 시간 실행된 후 I/O 작업이 시작된다. 그 시점에 해당 프로세스는 차단되어 다른 프로세스가 실행될 기회를 얻는다. (그림 4.4 )
보다 구체적으로, Process0는 I/O를 시작하고 완료될 때까지 차단된 상태에서 대기한다. 예를 들어, 디스크에서 읽거나 네트워크에서 패킷을 기다릴 때 해당 프로세스는 차단된다. 이때 OS는 Process0가 CPU를 사용하지 않고 있음을 인식하고 Process1을 실행하기 시작한다. Process1이 실행되는 동안 I/O가 완료되어 Process0를 다시 준비 상태로 이동했다고 해보자. 그러면 Process1이 완료되고 그 후 Process0이 실행된 후 종료된다.
이 간단한 예시에서도 OS는 많은 결정을 내려야 한다. 먼저, 시스템은 Process0가 I/O를 발행하는 동안 Process1을 실행하기로 결정했다. 이렇게 하면 CPU를 바쁘게 유지하여 자원 활용도를 높인다. 두 번째로, 시스템은 Process0의 I/O가 완료되었을 때 다시 Process0로 전환하지 않고 Process1을 계속 실행했다. 이것이 좋은 결정일까? 이러한 유형의 결정은 OS 스케줄러가 내리는데, 이 주제는 몇 장 후에 논의한다.
운영 체제(OS) 또한 프로그램이며, 모든 프로그램과 마찬가지로 다양한 관련 정보를 추적하는 핵심 데이터 구조가 있다. 예를 들어, OS는 준비된 모든 프로세스에 대한 목록정보와 현재 실행 중인 프로세스를 추적하기 위한 추가 정보를 유지한다. OS는 또한 차단된 프로세스를 추적한다. I/O 이벤트가 완료되면, OS는 프로세스를 깨워야한다.
그림 4.5는 OS가 xv6 커널에서 각 프로세스에 대해 추적해야 하는 정보 유형을 보여준다.
그림에서 볼 수 있듯이, OS는 특정 중요 정보들을 추적한다. 레지스터 컨텍스트는 정지된 프로세스의 레지스터 내용을 보유한다. 프로세스가 중지될 때, 그 레지스터는 이 메모리 위치에 저장된다. 그리고 이러한 레지스터를 복원함으로써(즉, 그들의 값을 실제 물리적 레지스터에 다시 넣음으로써), OS는 프로세스의 실행을 재개한다. 이 기술인 컨텍스트 스위치는 다음에 다룬다.
또한 실행 중, 준비 중, 차단됨 외에도 프로세스가 가질 수 있는 다른 상태들이 있다. 때때로 시스템은 초기 상태를 가질 수 있다.
또한, 프로세스는 종료되었지만 아직 정리되지 않은 최종 상태(UNIX 기반 시스템에서는 이를 '좀비 상태'라고 한다)에 놓일 수 있다.
그리고 최종 상태는 다른 프로세스(보통 프로세스를 생성한 부모)가 프로세스의 반환 코드를 조사하고 방금 완료된 프로세스가 성공적으로 실행되었는지 확인할 수 있게 해준다. (일반적으로, UNIX 기반 시스템에서 프로그램은 작업을 성공적으로 수행했을 때 0을 반환하고, 그렇지 않을 경우 0이 아닌값을 반환한다). 부모는 최종적으로 한 번의 호출(예: wait())을 하여 자식의 완료를 기다린다.
정리
• 프로세스는 실행 중인 프로그램의 주요 OS 추상화
어떤 시점에서든, 프로세스는 그 상태로 설명될 수 있다. : 주소 공간의 메모리 내용, CPU 레지스터의 내용(프로그램 카운터와 스택 포인터 등 포함), 그리고 I/O에 관한 정보(읽거나 쓸 수 있는 열린 파일 등).
• 프로세스 API는 프로세스와 관련된 호출을 할 수 있는 프로그램들로 구성된다. 일반적으로, 이에는 생성, 파괴 등이 있다.
• 프로세스는 실행 중, 실행 준비 완료, 차단 등 여러 다른 프로세스 상태 중 하나를 가진다. 다양한 이벤트(예: 스케줄링 또는 스케줄 해제되거나 I/O 완료를 기다리는 등)는 프로세스를 이러한 상태들 중 하나에서 다른 상태로 전환시킨다.
• OS는 시스템의 프로세스에 대한 정보를 가지고 있다. 각 정보는 프로세스 제어 블록(PCB)이라고 불리는 것에서 찾을 수 있는데, 이것은 특정 프로세스에 대한 정보를 포함하는 구조체이다.
학습 자료 출처
https://pages.cs.wisc.edu/~remzi/OSTEP/cpu-intro.pdf
'운영체제' 카테고리의 다른 글
[Operating System] Scheduling: Proportional Share (1) | 2024.01.02 |
---|---|
[Operating System] Virtualization-Mechanism: Limited Direct Execution (0) | 2023.12.21 |