Posts 프로세스 (Process)
Post
Cancel

프로세스 (Process)

Preview Image

공룡책(10판) Chapter 3 요약

슬슬 본격 돌입이다!


프로세스 개념 _ Process Concept

- 프로세스란?

모든 CPU 활동들을 어떻게 부를 것인가? 초창기 컴퓨터는 작업(job)을 실행하는 일괄처리 시스템이었고 이는 곧 사용자 프로그램 또는 task를 실행하는 시분할 시스템으로 발전하였는데, 실행된 프로그램이나 메모리 관리 등 프로그램 내부 활동 등등 프로세스라 할 수 있겠다.

프로세스 = 실행중인 프로그램

프로세스의 활동 상태는 프로그램 카운터 값과 프로세서 레지스터의 내용으로 나타낸다. 메모리 배치는 섹션으로 나뉘며 이는 다음과 같다.

  • 프로세스 메모리 배치 Memory layout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+---------------------------------+ 최대 메모리
|    스택 섹션 (stack section)    | 함수를 호출할 때 임시 데이터 저장장소 (매개변수, 복귀 주소, 지역변수)
+ - - - - - - - - - - - - - - - - +
|                 ↓               |
|                                 | 스택, 힙 섹션이 서로의 메모리 영역 확장
|                 ↑               |
+ - - - - - - - - - - - - - - - - +
|     힙 섹션 (heap section)      | 프로그램 실행 중 동적 할당되는 메모리 (ex. C의 malloc, JAVA의 new)
+---------------------------------+
|                                 | (초기화되지 않은 data)
|   데이터 섹션 (data section)    | 전역 변수 (크기 고정)
|                                 | (초기화 data)
+---------------------------------+
|   텍스트 섹션 (text section)    | 명령어, 실행 코드 (크기 고정)
+---------------------------------+ 0

간단한 C 프로그램에서의 메모리 배치를 코드로 살펴보면,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>

int x; // 초기화되지 않은 데이터 : 데이터 섹션
int y=15; // 초기화 데이터 : 데이터 섹션

int main(int argc, char *argv []) {       // 매개변수 : stack 섹션
   int *values;   //지역변수 : stack 섹션
   int i;         // 지역변수 : stack 섹션

   values = (int *) malloc(sizeof(int)*5);   // 힙 섹션

   for (i=0; i<5; i++)
      values[i] = i;
   
   return 0;
}

- 프로세스 상태

프로세스의 상태는 프로세스의 현재 활동에 따라 정의되며, 아래 상태 중 하나를 가진다.

process_state

  • new : 프로세스가 생성 중 (fork())
  • running : 명령어들이 실행되고 있음
  • waiting : 프로세스가 이벤트를 대기 중 (입출력 혹은 시그널 수신 대기)
  • ready : 프로세스가 프로세서에 할당되기를 대기
  • terminated : 프로세스의 실행 종료 (exit(), return 0)

참고 : terminate vs halt의 차이

  • ex ) 아래 코드의 경우
    1
    
    while(true) { }
    

    중지(halt)되지 않지만 사용자가 종료(terminate)할수 있다.

- 프로세스 제어 블록 (Process Control Block, PCB)

프로세스는 운영체제에서 PCB에 의해 표현된다. 프로세스 제어 블록은 프로세스에 관련한 정보를 저장하며, 아래 것들을 포함한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+-----------------------+ 
|     프로세스 상태     |  new, ready running, waiting ...
+-----------------------+ 
|     프로세스 번호     |  PID (Process ID)
+-----------------------+ 
|    프로그램 카운터    |  프로세스가 다음에 실행할 명령어 주소
+-----------------------+ 
|                       |
|      CPU 레지스터     |  누산기, 인덱스, 스택, 범용 레지스터 및 상태 코드 정보
|                       |  (프로세스가 다시 스케줄 될 때 실행 위해 필요)
+-----------------------+ 
|      메모리 제한      |  메모리 관리 정보
+-----------------------+ 
|    오픈 파일 리스트   |  열린 파일 목록
+-----------------------+ 
|                       |
|       그외 등등       |  CPU 사용 시간, 겨오가 시간, 계정 정보, 입출력 장치 목록 ...  
|                       |
+-----------------------+ 

- 스레드 (Threads)

위 프로세스 모델은 단일의 실행 스레드를 실행하는 프로그램이다. 단일 스레드는 프로세스가 한번에 한 가지 일을 실행하도록 허용한다. 현대 운영체제는 프로세스 개념을 확장하여 한 프로세스가 다수의 실행 스레드를 가질 수 있도록 허용 하며, 여러 스레드가 병렬(pararellel) 로 실행 할 수 있다. 이는 4장에서 자세히 알아보고,,


프로세스 스케줄링 _ Process Scheduling

멀티프로그래밍의 목적은 여러 개의 프로세스를 동시에 실행하는 것으로 CPU 이용 효율을 최대화 하는 것이다. 이를 달성하기 위해 시분할 (Time Sharing)이 사용되며, 이는 프로세스 사이에서 CPU 코어를 빠르고 빈번하게 스위칭 하여 프로그램 실행 동안 사용자가 상호 작용을 가능하게 한다. CPU 코어는 한번에 하나의 프로세스를 실행할 수 있기 때문에, 프로세스 스케줄러 (Process Scheduler) 는 여러 프로세스 중 하나의 프로세스를 어떤 CPU 코어에 할당할지 결정한다. 코어보다 많은 프로세스가 있는 경우 초과 프로세스는 스케줄을 대기한다. 메모리에 있는 프로세스 수를 다중 프로그래밍 정도라고 한다.

- 스케줄링 큐 _ Scheduling Queue

주요 특징은 first in first out, FIFO이다.

queueing_diagram

  1. 준비 큐 (Ready Queue) 프로세스가 시스템에 들어가면 준비 큐 (Ready Queue) 에 들어가 준비 상태로 CPU 코어에서 실행되기를 기다린다. 이 큐는 일반적으로 연결 리스트로 저장된다. 준비 큐 헤더에는 리스트의 첫 번째 PCB에 대한 포인터가 저장되고 각 PCB에는 준비 큐의 다음 PCB를 가리키는 포인터 필드가 포함된다.

  2. 대기 큐 (Wait Queue) I/O 완료와 같은 특정 이벤트가 발생하기를 기다리는 프로세스는 대기 큐 (Wait Queue)에 삽입된다.

  • 프로세스의 실행 상태에서 나타날 수 있는 이벤트
    1. 프로세스는 처음에 준비 큐에 들어가 실행을 위해 선택되거나 디스패치 될 때까지 기다린다.
    2. 프로세스에 CPU 코어가 할댕되고 실행 상태가 되면 여러 이벤트가 발생할 수 있다.
      1. 프로세스가 I/O 요청을 알리고 다음 I/O 대기 큐에 놓인다.
      2. 프로세스는 자식 프로세스를 만들고 자식의 종료를 기다리는 동안 대기 큐에 놓인다.
      3. 인터럽트, 타임 슬라이스가 만료되어 프로세스가 코어에서 제거, 준비 큐로 돌아간다.

2.1, 2.2의 경우에는 대기, 준비 상태에서 준비 큐로 들어간다. 프로세스는 종료 시 까지 이 주기를 계속하며, 종료 시점에 모든 큐에서 지거되고 PCB와 자원이 반환된다.

- CPU 스케줄링 _ CPU Scheduling

CPU 스케줄러의 역할은 준비 큐에 있는 프로세스 중에서 선택된 하나의 프로세스에 CPU 코어를 할당하는 것이다.

  • 스와핑(Swapping) : 메모리에서 디스크로 스왑아웃 (Swap out) -> 디스크에서 메모리로 스왑인 (Swap in) 하여 상태를 복원, 메모리가 초과 사용되어 가용공간을 확보해야 할 때 사용

- 컨텍스트(문맥) 교환 _ Context Switch

인터럽트가 발생하면 시스템은 인터럽트 처리가 끝난 후에 컨텍스트를 복구할 수 있도록 현재 실행 중인 프로세스와 현재 컨텍스트를 저장한다.

  • 컨텍스트 (Context) : 프로세스의 PCB에 표현되며, CPU 레지스터 값, 프로세스 상태, 메모리 관리 정보 등을 포함한다.

  • 컨텍스트 교환 (Context Switch) : CPU 코어를 다른 프로세스로 교환하려면 이전 프로세스의 상태를 보관하고, 새로운 프로세스의 상태를 복구하는 작업

즉 인터럽트가 일어났을 때 프로세스 실행중인 컨텍스트를 저장하고 재개될때 저장된 프로세스를 복구한다.


프로세스에 대한 연산 _ Operation on Processes

- 프로세스 생성 _ Process Creation

프로세스가 새로운 프로세스를 실행할 수 있다. 이를 통해 부모, 자식 프로세스와 같은 트리 관계가 성립

  • 부모, 자식의 두 프로세스를 실행 시키는 방법
    1. 부모자식 병행 실행
    2. 부모는 자식 실행종료까지 대기
  • 주소 공간 측면에서 보았을 때 새 프로세스의 두 가지 가능성
    1. 자식은 부모의 복사본 (주소공간을 복제, 부모와 같은 프로그램과 데이터를 가진다)
    2. 자식은 자신의 새 프로그램을 가지고있음 (새 프로그램 로딩)

시스템 콜의 fork() 함수를 호출하면 부모 프로세스는 자신과 같은 자식 프로세스를 생성한다. 둘 중 한 프로세스는 exec()를 통해 자신의 메모리 공간을 새로운 프로그램으로 교체한다. 이 과정을 다음 예를 통해 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/types.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
   pid = fork(); // 자식 프로세스 생성

   if (pid < 0) { // 오류 발생시
      fprintf(stderr, "Fork Failed");
      return 1;
   }
   else if (pid == 0) { // 자식 프로세스
      execlp("/bin/ls", "ls", NULL); // 자식의 새로운 프로그램 : 주소 공간을 ls 명령어로 채움
   }
   else { // 부모 프로세스
      wait(NULL); // 자식이 완료될때까지 기다린다, 자식의 exit() 호출 후 진행
      printf("Child Complete");
   }

   return 0;
}

fork

fork() 함수로 자식 프로세스를 생성하면 자식 프로세스에는 0의 PID 값을 확인할 수 있고, 부모는 자식 프로세스의 PID를 가진다. 자식 프로세스가 끝나면 (exit()) 부모 프로세스는 wait() 호출로부터 재개하여, exit() 시스템 콜을 사용해 끝낼 수 있다.

- 프로세스 종료 _ Process Termination

프로세스가 문장의 실행을 끝내고 exit() 시스템 콜을 이용해 운영체제에 자신의 삭제를 요청하여 종료가 이루어진다. 프로세스는 시스템 콜을 통해 다른 프로세스의 종료를 유발할 수도 있는데, 보통 종료될 프로세스의 부모만이 호출할 수 있다. 위에 설명하였던 대로 부모는 fork() 수행 후 자식의 pid를 전달받아 이를 알고 있다.

다음과 같은 경우에 부모는 자식 중 하나의 실행을 종료할 수 있다.

  1. 자식이 자신에 할당된 자원을 초과하여 사용할 경우 (부모가 자식 상태 모니터링할 방편이 주어져야 함)
  2. 자식에게 할당된 태스크가 더 이상 필요없을 때
  3. 부모가 exit()를 수행하는데 운영체제가 부모 종료 후 자식 계속 실행을 허용하지 않는 경우
  • 연쇄식 종료 (cascading termination) : 부모가 종료 될 경우 거기서 비롯된 자식 프로세스들을 모두 종료시킴, 운영체제가 수행
  • 좀비 프로세스 (Zombie process) : 부모 프로세스는 자식의 종료 상태를 얻도록 인자를 전달받고 종료를 기다리는데(wait()), 자식 프로세스가 종료되었는데도 부모 프로세스가 wait() 호출을 하지 않아 자식이 반환한 정보를 회수 할 수 없는 경우 발생한다.
  • 고아 프로세스 (Orphan process) : 부모가 wait() 대신 종료해버렸을 경우에 발생, UNIX는 새로운 부모 프로세스로 init을 지정하여 이 문제를 해결한다.


프로세스간 통신 _ Interprocess Communication

운영체제 내에서 실행되는 병행 프로세스들은 독립적, 혹은 협력적으로 실행된다.

  • 독립적 (Independent) : 프로세스가 다른 프로세스와 데이터를 공유하지 않는다.
  • 협력적 (Cooperating) : 실행 중에 다른 프로세스들에 영향을 주거나 받는다.

이 때 프로세스간 협력을 하는 이유는

  1. 정보 공유 (Information Sharing) : 여러 응용 프로그램이 동일한 정보에 접근할 수 있는 환경 제공
  2. 계산 가속화 (Computation Speedup) : 태스크를 빠르게 실행하기 위해 여러 개의 서브 태스크로 나누어 병렬로 실행
  3. 모듈성 (Modularity) : 시스템 기능을 나누어 모듈식 형태로 시스템을 구성

협력적 프로세스는 서로 데이터를 보내거나 받을 수 있는 프로세스간 통신 (Interprocess Communication, IPC) 기법이 필요하다. 여기에는 공유 메모리(Shared Memory), 메시지 전달(Message Passing) 의 두 가지 모델이 있다.

ipc_model

좌측이 공유 메모리 방식, 우측이 메시지 전달 방식으로 전자는 메모리를 공유하며 후자는 메시지를 저장할 별도의 영역을 둔다. 상세내용은 아래에 후술


공유 메모리 시스템에서의 프로세스 간 통신 _ IPC in Shared-Memory Systems

공유 메모리 시스템에서의 프로세스 통신에서는 공유 메모리 영역을 구축해야 한다. 일반적으로 운영체제는 한 프로세스가 다른 프로세스의 메모리에 접근을 금지시켜야 하지만 공유 메모리는 이 제약 조건을 제거하여 정보를 교환한다.

- 생산자, 소비자 문제

생산자 프로세스는 정보를 생산하고 소비자 프로세스는 정보를 소비한다. 여기서 정보를 공유할 메모리가 필요하며, 이때 사용하는 항목이 버퍼(buffer)이다.

무한 버퍼 (unbounded buffer) : 버퍼의 크기에 한계가 없으며 소비자는 새로운 항목을 기다릴 수 있지만 생산자는 항상 새로운 항목을 생산할 수 있다. 유한 버퍼 (bounded buffer) : 버퍼의 크기가 고정되어 있으며, 버퍼가 비어 있으면 소비자는 대기하고 모든 버퍼가 채워지면 생산자가 대기한다.

아래는 유한 버퍼의 예로 다음 변수들은 생산자, 소비자 프로세스가 공유하는 메모리 영역에 존재한다.

1
2
3
4
5
6
7
8
9
#define BUFFER_SIZE 10

typedef struct {
   ...
} item;

item buffer [BUFFER_SIZE];
int in = 0;
int out = 0;

공유 버퍼는 두 개의 논리 포인터 inout을 갖는 원형 배열로 구현된다. in은 버퍼 내에서 다음 비어 있는 위치, out은 버퍼 내에서 첫번째로 채워져 있는 위치를 가리킨다. in == out일 때 버퍼는 비어 있고 ((in + 1) % BUFFER_SIZE) == out 일 때 버퍼는 가득 차 있다.

  • 생산자
1
2
3
4
5
6
7
8
9
10
11
item next_produced; // 다음 번 생산되는 item

while (true) {
   /* produce an item in next_produced */

   while (((in + 1) % BUFFER_SIZE) == out)
      ; /* do nothing */
   
   buffer[in] = next_produced;
   in = (in + 1) % BUFFER_SIZE;
}
  • 소비자
1
2
3
4
5
6
7
8
9
10
11
item next_consumed; // 다음 번 소비되는 item

while (true) {
   while (in == out)
      ; /* do nothing */
   
   next_consumed = buffer[out];
   out = (out + 1) % BUFFER_SIZE;

   /* consume an item in next_produced */
}

이 방법은 최대 BUFFER_SIZE - 1 까지 수용할 수 있다. 여기서 in == out은 비어 있는 버퍼를 나타내는데, BUFFER_SIZE 까지 모두 수용한다면 비어 있거나 꽉 찬 버퍼 둘 다 나타낼 수 있다. 따라서 빈 버퍼와 꽉찬 버퍼, 둘 중 하나의 상태만을 나타낼 수 있는 조건 혹은 추가 플래그를 제공할 필요가 있다.

또한 여기서 고려하지 못한 것은 생산자, 소비자가 병행하게 공유 버퍼를 접근할 때의 문제이다. 두 프로세스가 같은 버퍼 주소에 접근할 경우? 라던가… 이런 경우에는 메모리 동시 접근을 막기 위해 그 영역을 어플리케이션 프로그래머가 정의해주어야한다.


메시지 전달 시스템에서의 프로세스 간 통신 _ IPC in Message-Passing Systems

메시지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신하고 동기화할 수 있는 기법을 제공한다. 네트워크로 연결된 다른 컴퓨터들에 존재할 수 있는 분산 환경에서 특히 유용하다. 메시지 전달 시스템은 최소한 두 가지 연산을 제공한다.

  1. send(message)
  2. receive(message)

프로세스 P와 Q가 통신을 하고자 할 떄, 이들 사이에 통신 연결 (Communication Link)이 설정되어야 한다. 하나의 링크와 위 연산을 구현하는 방법은 다음과 같다.

  • 직접(direct) 또는 간접(indirect) 통신
  • 동기식(synchronous) 또는 비동기식(asynchronous) 통신
  • 자동(automatic) 또는 명시적(explicit) 버퍼링

- 명명 _ Naming

프로세스들이 통신할 때 서로를 명명할 방법이 필요하다. 직, 간접 통신을 사용한다.

  • 직접(direct) 통신 : 각 프로세스는 통신의 수신자, 송신자의 이름을 명시한다.
    • 대칭성 : 송, 수신자가 모두 서로의 이름을 제시한다.
    • 비대칭 : 송신자만 수신자 이름을 지명한다.

이 방법은 지정하여야 하기 떄문에 모듈성을 제한한다. 이른바 하드코딩(hard-coding) 기법이다.

  • 간접(indirect) 통신 : 메일박스(mailbox) 또는 포트(port)로 송수신된다.
    • 메일박스 : 메시지를 받거나, 혹은 보낼 저장소. 프로세스에 예속되지 않고 생성 -> 송수신 -> 삭제 과정을 거친다. OS는 해당 과정의 방법을 제공
    • 각 메일박스마다 복수의 수신자를 만들 수 있다.

- 동기화 _ Synchronization

send, receive 호출에 의해 프로세스간 통신이 발생할 때 메시지 전달은 blocking, nonblocking 방식으로 전달된다.

  • 동기식(synchronous) 송, 수신 : 성공 보장
    • Blocking send : 보내는 프로세스는 받는 프로세스 또는 메일박스에 수신될 때까지 대기한다.
    • Blocking receive : 메시지를 다 받을 때까지 받는 프로세스가 대기.
  • 비동기식(asynchronous) 송, 수신 : 성공하든 말든 일단 보내보자.
    • Non-Blocking send : 보내는 프로세스는 메시지를 보내고 작업을 재개한다.
    • Non-Blocking receive : 보내는 프로세스가 메시지 또는 null을 수신한다. 받는 프로세스는 계속 작업.

- 버퍼링 _ Buffering

통신하는 프로세스들에 의해 교환되는 메시지는 큐에 들어 있다. 큐를 구현하는 방식은 다음 세 가지가 있다.

  • 무용량 (zero capicity) : 큐의 길이가 0이라 대기하는 메시지가 없고 송신자는 수신자가 수신할때까지 대기한다.
  • 유한 용량 (bounded capacity) : 새로운 메시지 전송 시 큐가 꽉 차지 않았다면 메시지는 큐에 쌓이고 송신자는 작업 실행을 계속한다. 꽉 찼다면, 큐가 빌 때까지 대기한다.
  • 무한 용량 (unbounded capacity) : 큐는 무한한 사이즈를 가지기 때문에 송신자는 대기하지 않는다.


IPC 시스템의 사례 _ Examples of IPC Systems

IPC 시스템들에 대해 알아보자. 여기서는 파이프 정도 알고 가면 좋을 거 같다.

- POSIX 공유 메모리 _ POSIX Shared Memory

가장 먼저 POSIX로, Portable Operating System Interface (for uniX)의 약자라고 하는데 운영체제의 표준화를 시도했다고 한다. UNIX OS API의 공통 규격을 지정하여, POSIX 표준으로 시스템 콜 등을 활용하여 POSIX 기반 프로그램 개발이 가능하다.

POSIX 공유 메모리는 메모리-사상 파일(Memory-mapped files)을 이용하여 구현된다. 메모리의 특정 영역을 파일로 연관 시키며, shm_open() 시스템 콜로 공유 메모리 객체를 생성한다.

1
2
// shm_open() : 공유 메모리 객체 생성, 공유 메모리 객체 file descriptor 반환
fd = shm_open(name, O_CREAT | O_RDWR, 0666);

shm_open()은 공유 메모리 객체를 나타내는 정수형 file descriptor를 반환한다. 다음으로 객체의 크기를 바이트 단위로 설정하는 ftruncate()를 사용한다.

1
2
// ftruncate() : 객체의 크기 설정 (바이트 단위)
ftruncate(fd, 4096)

여기서는 4096바이트로 설정. mmap() 함수는 공유 메모리 객체를 포함하는 Memory-mapped files을 구축한다.

1
2
// mmap() : 함수는 공유 메모리 객체를 포함하는 Memory-mapped files을 구축, 객체에 접근할 포인터 반환
ptr = (char *) mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

mmap()은 공유 메모리 객체에 접근할때 사용될 포인터를 반환한다. sprintf() 함수는 공유 메모리 객체에 쓰기 작업을 한다.

1
2
// sprintf() : 쓰기 작업
sprintf(ptr, "%s", message_0);

소비자는 ptr이 가리키는 공유 메모리 객체를 읽는 등의 작업을 하고 shm_unlink() 함수를 호출하여 접근이 끝난 공유 메모리를 제거한다.

1
2
// shm_unlink() : (소비자, consumer) 접근이 끝난 공유 메모리를 제거
shm_unlink(name)

- Mach 메시지 전달 _ Mach Message Passing

Mach 운영체제에서 대부분의 통신은 메시지로 수행되며, 포트(port)라는 메일박스로 주고 받는다.   각 포트에는 포트 간 상호 작용하는데 필요한 자격을 식별하는 포트 권한이 필요하다.

  • MACH_PORT_RIGHT_RECEIVE
  • MACH_PORT_RIGHT_SEND

태스크가 생성되면 Task self 포트와 Notify 포트라는 두 개의 포트가 생성된다. 커널은 Task self 포트의 수신 권한을 가지고 이벤트 발생 알림을 Notify 포트로 보낸다. Notify 포트는 물론 태스크가 수신 권한을 가진다.   mach_port_allocate() 함수는 새 포트를 작성하고 메세지 큐를 위한 공간을 할당한다.

mach_msg()함수는 메세지를 보내고 받는 표준 API이다. 함수의 매개 변수 중 하나가 MACH_SEND_MSG / MACH_RCV_MSG 값을 가져 송신, 수신연산 여부를 나타낸다.

  1. mach_msg()는 mah_msg_trap()함수를 호출 (mach 커널에 대한 시스템 콜)
  2. 커널 내에서 mach_msg_trap()은 mach_msg_overwrite_trap()함수를 호출하여 메세지의 실제 전달을 처리한다.

메시지 시스템의 문제점은 송신자의 포트에서 수신자의 포트로 메세지를 복사해야 하므로 발생하는 성능 저하이다.

- Windows에서 메시지 전달

Windows에서 응용 프로그램은 메세지 전달 기법을 통해 서로 통신하여 서브시스템 서버의 클라이언트로 간주할 수 있다.

메세지 전달 설비는 고급 로컬 프로시저 호출 설비(Advanced Local Procedure Call facility, ALPC)라 불리며, 동일 기계상에 있는 두 프로세스간의 통신에 사용한다. Mach와 유사하게, windows는 두 프로세스 간에 연결을 구축, 유지하기 위해 연결 포트(Connection port)와 통신 포트(Communication port)를 이용한다.

  • 채널의 생성 과정
    1. 서버 프로세스는 모든 프로세스가 접근할 수 있는 연결 포트 객체를 공표
    2. 클라이언트가 서버 시스템으로부터 서비스를 원할 경우, 서버의 연결 포트 객체에 대한 핸들을 열고 연결 요청을 송신
    3. 서버는 채널을 생성하고 핸들을 클라이언트에게 반환

채널은 클라이언트-서버 간 송수신 통신을 위한 한 쌍의 포트로 구성된다. 추가로 콜백 기법 또한 제공한다.

ALPC 채널이 생성되었을 떄 다음 3가지 메시지 전달 기법의 하나가 선택된다.

  1. (~256 바이트) 작은 메시지의 경우 포트의 메시지 큐가 중간 저장소로 사용되고 메시지는 프로세스 - 프로세스로 복사됨
  2. 대용량 메시지는 섹션 객체(Section object; 채널과 연관된 공유 메모리의 영역)를 통하여 전달되어야 한다.
  3. 섹션 객체에 저장될 수 없을 정도의 데이터의 경우 서버 프로세스가 클라이언트 주소 공간을 직접 읽거나 쓸 수 있는 API 사용

- 파이프 _ Pipes

Shared Memory 방식의 문제 : shm_open , read write를 다 해줘야 되어서 부담

파이프는 두 프로세스가 통신할 수 있게 하는 전달자로써, UNIX 초창기에 사용하던 IPC 매커니즘이다. 파이프를 구성하기 위해서 다음 4가지를 고려해야 한다.

  1. Unidirectional or Bidirectional 단방향인가 양방향인가?
  2. Half-duplex or Full-duplex 반이중 방식인가 전이중 방식인가?
    • 반이중 (Half-duplex) 통신 : 통신 선이 하나로 양방향 통신이나 송신, 수신을 한번에 하나씩만 할 수 있다.
    • 전이중 (Full-duplex) 통신 : 통신 선이 두개로 송신, 수신을 동시에 할 수 있다.
  3. Relationship 프로세스간 관계 (부모, 자식)가 존재해야 하는가?
  4. 네트워크를 통해 통신할 수 있느냐? 프로세스 간에만 하는가?

- 일반 파이프 _ Ordinary Pipes

생산자-소비자 형태로 두 프로세스 간의 통신을 허용한다. One way(단방향) 통신이 가능하다.(unidirectional) Two way(양방향) 통신을 하려면 두개 파이프를 써야 한다.

UNIX 시스템에서 일반 파이프는 다음 함수로 구축된다.

1
pipe(int fd[])    // 파일 설명자를 통해 접근되는 파이프를 생성

이 때, fd[0]는 읽기 종단(read end), fd[1]는 쓰기 종단(write end) 으로 동작한다. 파이프는 read()write() 시스템 콜을 사용하여 접근될 수 있다. 일반 파이프는 파이프를 생성한 프로세스만 접근할 수 있다. 부모 프로세스가 파이프를 생성하고 fork()로 생성한 자식 프로세스와 통신하기 위해 사용한다. 이 때 자식 프로세스는 부모로부터 파이프를 상속한다.

Windows 시스템의 일반 파이프는 익명 파이프(anonymous pipe)로 불리며 UNIX와 유사하게 동작한다. 역시 단방향, 부모-자식 관계이다. 읽기와 쓰기는 ReadFile()WriteFIle()을 사용하여 이루어진다.

- 지명 파이프 _ Named Pipes

지명 파이프는 양방향으로 가능하며 부모, 자식 관계 없이도 사용 가능하다. 구축되며 여러 프로세스들이 이를 사용하여 통신할 수 있으며, 통신 프로세스가 종료되더라도 지명 파이프는 계쏙 존재한다.

UNIX에서는 FIFO라고 부르며, 파일 시스템의 보통 파일처럼 존재한다. mkfifo() 시스템 콜을 이용하여 생성되고 일반적인 open(), read(), write(), close() 시스템 콜로 조작된다. 양방향을 허용하긴 하나 일반적으로 반이중(half-duplex) 전송만이 가능하다. Windows에서의 지명 파이프는 전이중(full-duplex) 통신을 허용한다. 또한 UNIX FIFO가 Byte 단위 통신만 허용하는데 비해 메시지 단위 데이터 전송을 허용한다. CreateNamePipe() 함수를 사용해 생성되고 ConnectNamedPipe() 함수를 사용해 지명 파이프에 연결한다. 읽기와 쓰기는 역시 ReadFile()WriteFIle()을 사용하여 이루어진다.


클라이언트 서버 환경에서 통신 _ Communication in Client-Server Systems

프로세스 간 통신을 넘어서 클라이언트-서버 시스템의 통신에도 사용할 수 있는 기법들을 살펴보자.

소켓 _ Socket

소켓은 통신을 위한 양 종단(endpoint)을 뜻한다. 두 프로세스가 네트워크 상에서 통신하려면 양 프로세스에 하나씩 두 개의 소켓이 필요한데, 이 때 IP 주소와 포트 번호를 접합하여 구별한다. 서버는 지정된 포트에 클라이언트 요청 메시지가 도착하기를 기다리고, 요청이 수신되면 서버는 클라이언트 소켓으로부터 연결 요청을 수락함으로 연결이 완성된다. 클라이언트 프로세스가 연결을 요청하면 호스트 컴퓨터가 포트 번호를 부여한다. 이 번호는 1024보다 큰 임의의 정수가 된다.

1
2
3
4
5
6
7
8
9
10
+-----------------------+ 
|         소켓          |  host X
|    147.86.5.20:1625   |  (147.86.5.20)
+-----------------------+ 
            |
            |
+-----------------------+ 
|         소켓          |  web server
|     161.25.19.8:80    |  (161.25.19.8)
+-----------------------+ 

모든 연결은 유일해야 하기 때문에 위 호스트 X에 있는 다른 클라이언트 프로세스가 동일한 웹 서버로 연결을 하고자 하면 1024보다 크고 1625가 아닌 포트 번호를 부여받아 통신을 한다.

원격 프로시저 호출 _ Remote Procedure Calls, RPC

IPC(1개의 pc 안에)의 확장 개념 -> RPC (여러 개의 pc 안의 프로세스)

RPC는 클라이언트가 원격 호스트의 프로시저 호출을 마치 자기의 프로시저를 호출하는 것처럼 해준다. RPC 시스템은 클라이언트 쪽에 스텁(stub) 을 제공하여 통신을 하는데 필요한 사항을 숨겨준다. 보통 원격 프로시저마다 다른 스텁이 존재한다. 클라이언트-서버에서 RPC 과정은 다음과 같다.

  1. 클라이언트가 원격 프로시저를 호출
  2. 클라이언트 측 스텁이 원격 서버의 포트를 찾고 매개변수를 정돈(marshall)
  3. 스텁은 메시지 전달 기법을 사용하여 서버에게 메시지를 전송
  4. 서버 측 스텁이 메시지를 수신한 후 서버의 프로시저를 호출
  5. 필요한 경우 반환 값들도 동일한 방식으로 전송

여기서 매개변수 정돈(parameter marshalling) 을 이야기하자면, 어떤 서버 기기는 최상위 바이트(most-significant byte) 를 먼저 저장하는 big-endian 방식이고, 어떤 기기는 최하위 바이트(least-significant byte)를 먼저 저장하는 little-endian 방식이다. 이러한 데이터 표현 방식의 차이 문제를 해결하기 위해 RPC 시스템은 기종 중립적인 데이터 표현 방식을 정의하는 것이다. 이러한 표현 방식 중 하나가 XDR (external data representation)로, 클라이언트에서 서버로 데이터를 보내기 전 데이터를 중립적인 XDR 형태로 바꾸어 보낸다. 서버에서는 XDR 데이터를 받아 매개변수를 풀어 자기 기종의 형태로 데이터를 바꾸어 넘겨받는다.

원리는 이정도인거 같고, RPC 시스템에서 다룰 수 있는 문제를 두 가지 살펴보자.

  1. 호출이 네트워크 오류로 실패하거나 중복되어 호출되는 경우, 운영체제는 메시지가 최대 한 번이 아니라 정확히 한번 처리하도록 보장하여야 한다.
    • 최대 한 번은 모든 메시지에 타임스탬프(timestamp) 기록을 포함하여 실행하여 보장한다.
    • 정확히 한 번은 서버가 RPC 요청이 수신되었고 실행되었다는 ACK (acknowledgement) 메시지를 보내어 보장한다. 클라이언트는 ACK를 받을 떄까지 주기적으로 각 RPC 호출을 재전송한다.
  2. 클라이언트-서버의 통신에서 두 시스템은 공유 메모리가 없기에 완전한 정보가 없어서 포트를 바인딩하기 어렵다.
    • 고정된 포트 주소 형태를 미리 정해 놓는다.
    • 랑데부 방식에 의해 동적으로 포트를 바인딩한다. 운영체제는 고정 RPC 포트를 통해 랑데부용 daemon (matchmaker라고 불림)을 제공하며, 클라이언트가 daemon에 메시지를 보내면 RPC 이름에 대응되는 포트 번호가 클라이언트에 반환된다.


이 단원은 유독 길군. 아직 책의 초반부인데, 갈길이 멀다. 차근차근 정리해보자..!

This post is licensed under CC BY 4.0 by the author.

OS 구조 (Operating System Structures)

스레드와 병행성 (Threads & Concurrency)