운영체제 - (2)

2024. 11. 7. 23:33CS/운영체제

프로세스

다음에 실행할 명령어의 PC를 가지고, 관련된 자원이 있는 실행 중인 프로그램

즉, 실행 중인 프로그램의 인스턴스로서, PC와 관련된 자원을 포함하고 있습니다.

프로세스는 다음과 같은 구성 요소를 가지고 있습니다.

  1. Stack : 함수를 호출할 때 임시 데이터 저장장소 (Top -> Bottom)
  2. Heap : 프로그램 실행 중 동적으로 할당되는 메모리 (Bottom -> Top)
  3. Data : 전역 변수
  4. Text : 실행 코드

프로세스 상태

프로세스는 다음과 같은 상태를 가질 수 있습니다.

  1. New(생성) : 프로세스가 새로 생성 중인 상태입니다
  2. Ready(준비) : 프로세스가 CPU에 할당되기를 기다리는 상태입니다. 이 상태에서 프로세스는 스케줄러에 의해 CPU 할당을 대기하며, 디스패처가 CPU 제어권을 넘겨주면 실행 상태로 전환됩니다.
  3. Running(실행) : 프로세스가 CPU에 할당된 상태로, 명령어들이 실행되고 있는 단계입니다.
  4. Waiting (대기): 프로세스가 CPU 사용이 아닌 다른 이벤트(예: 블로킹 I/O 작업, 네트워크, 파일 I/O 등)를 기다리는 상태입니다. 프로세스는 필요한 이벤트가 완료될 때까지 대기 상태에 머무르며, 이벤트가 발생하면 다시 Ready 상태로 돌아갑니다.
  5. Terminated(종료) : 프로세스의 실행이 종료된 상태입니다.

프로세스와 프로그램의 차이

운영체제의 관점에서, 프로그램 그 자체는 프로세스가 아닙니다. 프로그램은 명령어 리스트를 내용으로 가지며
디스크에 저장된 파일(실행 파일)과 같은 수동적인 존재입니다.

하지만 프로세스는 다음에 실행할 명령어를 지정하는 PC와 관련 자원의 집합을 가진 능동적인 존재입니다.


프로세스 스케줄링

멀티프로그래밍의 목적은 CPU 이용을 최대화하기 위하여 항상 어떤 프로세스가 실행되도록 하는 것입니다.

이러한 목표를 달성하기 위해, 프로세스 스케줄러는 코어에서 실행할 수 있는 여러 프로세스 중에서 하나의 프로세스를 선택합니다.

이를 측정하기 위해, 현재 메모리에 있는 프로세스의 수를 다중 프로그래밍 정도(Multi Programming Degree)라고 합니다.

일반적인 작업은 I/O boundCPU Bound 작업으로 나누어질 수 있습니다.
I/O bound 작업은 계산보다 입출력 작업에 많은 시간을 소비하는 프로세스입니다.
반대로 CPU bound 작업은 입출력보다 계산에 더 많은 시간을 사용하는 프로세스입니다.

스케줄링 큐

프로세스가 시스템에 들어가면 준비 큐에 들어가 준비 상태로 실행을 대기합니다. 이 큐는 일반적으로 연결 리스트로 구현됩니다.

준비 큐(Ready Queue)는 일반적으로 연결리스트로 구현됩니다. 하지만 위의 I/O bound 작업을 통해 유추할 수 있듯이 다른 큐도 존재합니다.

프로세스에 CPU 코어가 할당되면 프로세스는 잠깐 실행되어 결국 종료되거나 인터럽트 되거나, 특정 이벤트가 발생할 때까지 대기합니다.

위 큐잉 구조들을 종합하면 다음과 같은 다이어그램으로 나타낼 수 있습니다.

각 프로세스의 큐잉 작업은 프로세스가 종료될 때 까지 진행됩니다.

리눅스의 프로세스 구현

long state; %% 프로세스 상태 %%
struct sched_entity se; %% 스케줄링 정보 %%
struct task_struct *parent; %% 이 프로세스의 부모 %%
struct list_head childre; %% 이 프로세스의 자식둘 %%
struct files_struct *files; %% 오픈 파일 %%
struct mm_struct *mm; %% 이 프로세스의 주소 공간 %%

현재 실행중인 프로세스의 상태를 new_state 로 바꾸기 위해서current ->state = new_state 의 명령문을 사용합니다.


PCB

PCB는 프로세스 생성 시에 할당되며, 프로세스의 상태를 보관하고 있습니다.

프로세스는 다음과 같은 요소들을 포함하고 있습니다.

프로세스 상태: 현재 프로세스가 Running, Ready, Waiting 등의 상태 중 어느 상태에 있는지 나타냅니다.

프로세스 ID (PID): 각 프로세스를 고유하게 식별하기 위한 ID입니다.

프로그램 카운터 (PC): 프로세스가 다음에 실행할 명령어의 주소를 가리킵니다.

레지스터 상태: 프로세스의 CPU 레지스터 정보입니다.

메모리 관리 정보: 프로세스의 메모리 사용 정보, 예를 들어 페이지 테이블이나 세그먼트 테이블과 같은 정보가 포함됩니다.

입출력 상태 정보: 프로세스가 사용 중인 입출력 장치나 파일에 대한 정보입니다.

CPU 스케줄링 정보: 우선순위와 같은 프로세스의 스케줄링에 필요한 정보입니다.

PCB는 운영체제가 프로세스를 관리하고, 상태 전환 시 필요한 정보를 저장 및 참조할 수 있도록 하는 핵심 데이터 구조입니다.

이전 게시글을 통해 현대의 OS는 시분할을 활용하여 여러 프로세스를 스케줄링 함을 알 수 있었습니다.

그렇다면 시분할 시스템은 어떻게 동시성을 구현할까요 ?

Process Control Block

각 프로세스는 실행 시, 각각의 PCB를 할당받습니다.

PCB에는 스택 형태로, 다음 실행할 Instruction의 주소를 가리키는 Program Counter를 가집니다.
현대의 시분할 시스템은 CPU를 할당된 시간만 점유합니다. 모두 실행되지 않고 도중에 중단되어도 프로그램 카운터가 다음 Instruction을 저장하고 있기 때문에, 다시 실행되었을 때 빠르게 다음 명령 수행이 가능합니다.


문맥 교환 (Context Switch)

위 정보에서 알 수 있듯이, 인터럽트는 운영체제가 CPU 코어를 현재 작업에서 뺏어내 커널 루틴을 실행할 수 있게 합니다.

컨텍스트 스위칭이 진행되는 시간 동안, 시스템이 아무런 유용한 일을 할 수 없기 때문에 OS 입장에서 문맥 교환은 오버헤드입니다.


프로세스 생성

실행되는 동안 프로세스는 여러 개의 새로운 프로세스들을 생성할 수 있습니다.

생성하는 프로세스를 부모 프로세스라 부르고, 새로운 프로세스들은 자식 프로세스라고 부릅니다.

새로운 프로세스들은 다시 다른 프로세스들을 생성할 수 있으며, 프로세스의 트리을 형성합니다.

각 프로세스는 유일한 식별자인 PID 을 사용하여 구분합니다. 리눅스의 경우 운영체제가 로드된 이후 언제나 pid가 1인 systemd 프로세스가 생성됩니다. 루트 프로세스가 로드된 이후, 다양한 자식 프로세스를 생성합니다.

자식 프로세스를 생성하는 방식은 UNIX 기준 fork()exec()이 있는데요, 크게 자식 프로세스가 부모 프로세스의 복사본인지, 자신에게 적재될 새로운 프로그램을 가졌는지에 따라 구분될 수 있습니다.

프로세스 간 통신 (Inter Process Communication)

운영체제 내에서 실행되는 병행 프로세스들은 독립적이거나 협력적인 프로세스들입니다. 한 프로세스가 다른 프로세스에 영향을 주거나 받는다면, 이는 협력적인 프로세스로 구분될 수 있습니다.

Inter Process Communicaiton(IPC)은 프로세스들이 서로 데이터를 교환하고 협력하기 위해 사용됩니다. 프로세스는 운영체제에서 독립적인 실행 단위이기 때문에, 기본적으로 메모리와 자원을 공유하지 않습니다. 하지면 현대의 운영체제는 여러 프로세스가 협력하고 동일한 작업을 분담해야 하기 때문에 정보나 데이터를 주고받는 통신이 필요합니다.

구체적으로 프로세스간 통신이 왜 필요할까요 ?

  1. 데이터 공유 관점 : 여러 프로세스가 동일한 데이터를 사용할 때, 데이터를 주고받기 위해 IPC를 통해 데이터를 전달합니다.
    프로세스간 데이터 공유가 불가능하다면, 서로 다른 프로세스가 동일 데이터를 사용하기 위해 중복된 작업을 시도해야 합니다.
  2. 작업 분할 및 협업 : 복잡한 작업을 프로세스로 나누어 병렬로 처리할 때, 각 프로세스가 주고받으며 작업을 조율할 수 있습니다.
  3. 자원 효율성 : IPC를 통해 중요 정보를 공유하거나 데이터를 복제하지 않고 전달해 자원을 효율적으로 사용할 수 있습니다.
  4. 동기화와 제어 : 다수의 프로세스가 자원을 공유하거나 동시에 접근할 때, 접근 순서를 제어하거나 동기화하는 메커니즘이 필요합니다. 이를 위해서는 각 프로세스 간 상태에 접근할 수 있어야 합니다.
  5. 시스템 모듈화 : 프로세스를 나누어 독립적으로 개발, 관리, 배포할 수 있도록 함으로써 시스템을 모듈화하고 유지보수를 쉽게 할 수 있습니다.

프로세스 간 통신 기법에는Shared Memory(공유 메모리)Message Passing(메세지 전달) 방식이 존재합니다.

  • (a) : Shared Memory
  • (b) : Message Passing

공유 메모리에서 IPC

공유메모리 방식을 사용하기 위해서는, 통신하는 프로세스들의 공유 메모리 영역을 구축해야 합니다.
일반적으로 공유 메모리 영역은 공유 메모리 세그먼트를 처음 생성한 프로세스의 주소 공간에 속해 있습니다.

일반적으로 OS는 한 프로세스가 다른 프로세스의 메모리에 접근하는 것을 금지하고 있습니다. 하지만 공유 메모리는 여러 프로세스가 운영체제의 기본적인 메모리 보호 규칙을 예외적으로 무효화하여 특정 메모리 공간을 공유하기로 합의해야 합니다.

이후 프로세스들은 공유 영역에 읽고 씀으로써, 정보를 교환할 수 있습니다. 협력하는 프로세스는 생산자-소비자
패러다임을 사용하여 구현됩니다.

일반적으로 생산자-Producer는 정보를 생산하고, 소비자-Consumer 프로세스는 정보를 소비합니다. 생산자 소비자 문제의 하나의 해결책은 공유 메모리를 사용하는 것입니다.
또한 소비자-생산자간에 자원 공유와 동기화에 대한 문제가 등장하곤 합니다.
이러한 문제를 해결하기 위해선 무한 버퍼(Unbounded Buffer)유한 버퍼(Unbounded Buffer)가 존재합니다.

메세지 전달 시스템에서 IPC

메세지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신하고, 그들의 동작을 동기화 할 수 있도록 허용하는 기법을 제공합니다.

공유메모리는 메모리에 접근하고 조작하는 코드가 응용 프로그래머에 의해서 명시적으로 작성되어야 했습니다. 하지만 메세지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신을 하고, 그들의 동작을 동기화 할 수 있도록 허용하는 기법을 제공합니다.

메세지 전달 방식은 최소 두가지 연산을 제공합니다

  • send(message)
  • receive(message)

메세지는 고정 길이일 수도 있고, 가변 길이일 수도 있습니다. 하지만 고정 길이로 제약이 생긴다면 그만큼 시스템 구현에는 제한으로 작용할 수 있습니다.

만약 프로세스 P와 Q가 통신을 원한다면, 메세지를 주고 받아야 합니다.
하지만 메세지 전달 시스템은 공유 메모리 방식과 다르게 각 프로세스가 동일한 주소 공간을 공유하지 않기 때문에, 프로세스 간 연결을 설정해야 합니다. 이러한 통신 연결을 위해 사용하는 방법들은 다음과 같습니다.

  • 직접 통신 / 간접 통신
  • 동기식 통신 / 비동기식 통신
  • 자동 / 명시적 버퍼링

Naming

직접 통신 하에서, 통신을 원하는 프로세스들은 서로를 가리킬 방법이 있어야 합니다.

  • send(P, message) 프로세스 P에 메세지를 전송한다.
  • 대칭적 : receive(Q, message) , 비대칭적 : receive(id, message) 프로세스 Q로부터 메세지를 수신한다.

위 방식의 특성은 어떻게 될까요 ?

  1. 통신을 원하는 프로세스 쌍 사이 연결은 자동으로 구축된다. 프로세스들은 상대방의 identity만 알면 된다.
  2. 연결은 정확히 두 프로세스 사이에만 진행된다.
  3. 프로세스들의 각 쌍 사이에는 정확히 하나의 연결이 존재해야 한다.

간접 통신에서 메세지들은 Mailbox 또는 port로 송신되고, 수신됩니다. 메일박스는 추상화 된 메세지를 저장할 수 있는 자료 구조입니다.

  • send(A, message) 메시지를 메일바스 A로 송신한다.
  • receive(A, message) 메시지를 메일박스 A로부터 수신한다.

메일박스 방식에서 통신 연결은 다음과 같은 성질을 가집니다.

  • 한 쌍의 프로세스 사이 연결은 공유 메일박스가 존재할 경우에만 구축된다.
  • 연결은 두 개 이상의 프로세스들과 맺어질 수 있다.
  • 통신 중인 각 프로세스 사이에는 다수의 서로 다른 연결이 존재할 수 있고, 각 연결은 하나의 메일박스에 대응된다.
    위 성질에 의하면 메일박스를 이용한 통신은 2대 이상의 프로세스가 연결될 수 있습니다.

그렇다면 A, B, C가 모두 메일박스 M을 공유한다고 했을 때, A은 M에 송신하고, B, C은 각각 M으로 부터 receive()를 실행한다고 가정합시다. 그렇다면 어느 프로세스가 A가 보낸 메세지를 수신하게 될까요 ?

답은 '구현에 따라 다르다' 입니다.

  1. 하나의 링크는 최대 두 개의 프로세스와 연관되도록 허용한다.
  2. 한 순간에 최대로 하나의 프로세스가 receive()연산을 실행하도록 허용한다.
  3. 어느 프로세스가 메세지를 수신할 것인지 시스템이 임의로 선택하도록 한다. (시스템이 수신자를 지정한다.)

위 설명에서 알 수 있듯이, 시스템은 운영체제가 될 수 있습니다. 메일박스를 운영체제가 관리한다면 특정 프로세스에 종속되지 않기 때문에, 유연함을 제공할 수 있습니다.

동기화

send(), receive()의 구현은 서로 다른 설계 옵션이 존재합니다. 메세지 전달은 Blocking, Non-blocking으로 설계할 수 있습니다.

  • Blocking send : 송신 프로세스는 수신 프로세스나 메일박스에 의해 수신될 때 까지 Blocking 된다.
  • Non-blocking send: 송신 프로세스는 메세지를 보내고 작업을 재시작한다.
  • Blocking receive : 메세지가 이용 가능할 때까지 수신 프로세스가 Blocking 된다.
  • Non-blocking receive : 송신하는 프로세스가 유효한 메세지 또는 null을 받는다.

위 방식은 간단히 코드로 살펴보겠습니다.

message next_produced; 

while(true){
    /* next_produced에 요소를 생성한다. */
    send(next_produced);
}
message next_consumed; 

while(true){
    receive(next_consumed);

    /* next_consumed에 요소를 소비한다. */
}

버퍼링

통신이 직접적이든 간접적이든 간, 통신 프로세스들에 의해 교환되는 메세지는 임시 큐에 들어있습니다. 기본적으로 이러한 큐를 구현하는 방식은 세 가지가 있습니다.

  1. Zero capacity(무용량) : 큐의 최대 길이가 0 입니다. 즉, 링크는 자체 대기하는 메세지를 가질 수 없습니다.
    그렇기 때문에 송신자는 수신자가 수신할 때 까지 기다려야 합니다. (blocking)
  2. Bounded capacity(유한 용량) : 큐는 유한한 길이 n을 가집니다. 만약 공간이 다 찰 경우, 이용 가능할 때 까지 Blocking 됩니다.
  3. Unbounded capacity(무한 용량) : 큐는 잠재적으로 무한한 용량을 가지고, 큐 안에서 대기할 수 있습니다. 따라서 송신자는 봉쇄되지 않습니다.

'CS > 운영체제' 카테고리의 다른 글

운영체제 - (1)  (1) 2024.10.31
Blocking / Non-Blocking, Synchronous / Asynchronous  (0) 2024.01.20