본문 바로가기

Computer Science/OperatingSystem

[Operating System Concept] Processes - Chapter 3

이전 포스팅:  Operating System Structure - Chapter 2

 

본 내용은 Operating System Concept 10th edition 책에 대한 내용을 담고 있습니다. 

https://www.os-book.com/OS10/

 

Operating System Concepts - 10th edition

 

www.os-book.com

 

Process Concept

프로세스는 실행 중인 프로그램을 의미하며 실행 파일이 메모리에 로드되면 프로그램은 프로세스가 됩니다.

프로세스의 현재 활동 상태는 Program counter 값과 Processor's registers의 내용에 의해 정해지고, 프로세스의 메모리 레이아웃은 여러 개의 섹션들로 나뉩니다. 프로세스는 기본적으로 실행 단위인 단일 스레드(Single Thread)를 가지며, 단일 스레드(Single Thread)는 한 번에 하나의 작업만 수행할 수 있습니다.  

 

Stack section:
함수 호출 시 임시 데이터 저장소

Heap section
:
프로그램 런타임 중에 동적으로 할당된 메모리

Data section:
전역변수(global variables)

- BSS :
  초기화되지 않은 전역변수
- Data :
  초기화된 전역변수

Text section
: 실행 코드

 

Data Section과 Text Section은 프로그램이 실행되는 동안 크기가 변하지 않습니다. 반면, Stack Section과 Heap Section은 프로그램 실행 동안 동적으로 줄어늘고 늘어나곤 합니다. 

 

Stack Section의 경우, function parameters, local variables들과 return address 값들이 스택에 PUSH 되어 Stack이 늘어나고, 함수가 끝날 때 POP을 수행하면서 Stack이 줄어들게 됩니다. 또한, Heap Section의 경우, 동적으로 메모리를 할당하게 되면 Heap이 늘어나게 되고, 할당된 메모리를 반환하면 Heap이 줄어들게 됩니다.

Process State

Diagram of process state

프로세스의 상태에는 New, Running, Waiting, Ready, Terminated로 총 5가지가 존재합니다. 

처음 프로그램이 시작되면 New 상태로 인해, 프로세스가 생성되고, 프로세스는 프로세서(Processor)에 할당될 때까지 대기하는 Ready 상태가 됩니다. 이후, 스케줄러(Scheduler)에 의해 해당 프로세스가 프로세서(Processor)에 할당되어 Running 상태로 바뀌게 되면 프로그램의 인스트럭션(Instructions)이 실행됩니다. 이후, Running 상태에서 프로세스가 실행을 마무리하면 Terminated 상태로 변화합니다. 

 

하지만, Running 상태에서 I/O 혹은 특정 이벤트를 기다리게 되면, Waiting 상태로 바뀌고, I/O 혹은 특정 이벤트가 완료되면 Ready 상태로 다시 돌아가게 됩니다. 또한, 다른 프로세스에 의해 interrupt가 발생 경우, Ready 상태로 다시 돌아가게 됩니다. 

 

그럼, 스케줄러(Scheduler)가 여러 프로세스들이 존재할 경우 어떻게 관리할 수 있을까요?

그것은 특정 프로세스와 관련된 정보를 저장하는 PCB(Process Control Block)에 의해 가능해집니다.

Process Control Block

프로세스가 시작하거나 재시작할 때, PCB를 통해 프로세스와 관련된 많은 정보들이 제공됩니다. 

PCB (Process Control Block)

Process state : Running, Waiting 등의 프로세스 상태
Program counter : 다음 실행될 instruction의 위치를 저장
CPU registers : 프로세스의 레지스터 값들
CPU scheduling information : 우선순위, 스케줄링 큐 포인터
Memory-management information : 프로세스에 할당된 메모리
Accounting information : CPU가 사용된 Clock Time
I/O status information: 프로세스에 할당된 I/O 장치들, 열린 파일 목록

Process Scheduling

프로세스 스케줄러(Process Schedular)는 CPU core에서 다음 실행을 위해 이용가능한 프로세스들을 선택합니다. 즉, 프로세스 스케줄링의 목적은 최대한 CPU를 활용하여 프로세스들을 빠르게 CPU core로 전환시키기 위함입니다. 

 

단, 각 CPU core는 한 번에 하나의 프로세스만 실행시킬 수 있습니다. 그렇기 때문에, 시스템에 CPU core가 하나일 경우, 동시에 여러 개의 프로세스를 구동시킬 수 없습니다. 반면, 시스템에 CPU core가 여러 개일 경우, 한번에 여러 개의 프로세스를 실행시킬 수 있습니다. 

 

만일 실행되어야하는 프로세스가 CPU Core 개수보다 많을 경우에는 어떻게 될까요? 

이런 경우는 다른 프로세스가 종료되어 점유 가능한 CPU Core가 생기거나 다시 스케줄링(Re-scheduling)이 되기까지 대기하게 됩니다. 

The ready queue and wait queues.

위 그림과 같이, 스케줄링(Scheduling)을 위해 스케줄링 큐를 활용합니다. 스케줄링 큐에는 Ready queue와 Wait queue가 존재하며 링크드 리스트(Linked List) 형태로 저장됩니다. 

 

만일 프로세스가 처음 생성되어 Ready queue에 들어가게 되면, 해당 프로세스는 CPU를 점유하기까지 대기합니다. 이후, CPU core에 할당되어 실행되고 있는 중에 특정 이벤트가 발생할 수 있습니다. 

위 그림과 같이, I/O request, create child process 등 특정 이벤트가 발생할 경우, Ready queue로 다시 들어가게 됩니다. 이러한 과정을 프로세스가 종료될 때까지 계속해서 수행합니다. 이후, 프로세스가 종료될 때, 해당 프로세스는 모든 대기열에서 제거되고 PCB와 할당된 자원이 해제됩니다.

Context Switch

Context Switch는 현재 실행되고 있는 프로세스 상태를 저장하고 다른 프로세스가 CPU Core를 점유하는 것을 의미합니다.

Context Switch

Context Switch가 발생하게 되면, 커널은 현재 실행되고 있는 프로세스의 상태를 PCB에 저장하고 저장된 새로운 프로세스의 상태를 로드하여 실행시키게 됩니다. 

 

단, Context Switch가 수행될 때, 시스템은 다른 작업을 하지 않기 때문에 오버헤드가 발생합니다.

 

운영체제와 PCB의 복잡할수록 Context Switch를 수행하는 시간은 더 길어지고, 하드웨어 지원에 따라 또 다르게 결정됩니다. 

 

Operations on Processes

시스템에서 프로세스는 생성되어 종료되는 메커니즘을 갖고 있습니다. 그렇기에, 프로세스 생성에 관한 내용을 알아보겠습니다. 

1. Process Creation

부모 프로세스(Parent process)는 fork()를 통해 자식 프로세스(Child process)를 생성할 수 있으며, 자식 프로세스는 부모 프로세스의 자원들을 물려받고, 부모와 자식은 자원을 공유하지 않습니다. 또한, 부모와 자식들은 동시에 실행될 수 있으며, 자식 프로세스들이 종료될 때까지 부모 프로세스는 대기하게 됩니다. 

 

이와 같이, 프로세스가 여러 개로 구성될 경우 프로세스들은 트리 형태를 이루게 됩니다. 운영체제는 이러한 여러 개의 프로세스를 관리하기 위해 프로세스가 생성될 때 프로세스에 PID를 부여합니다. 그러므로, 프로세스는 다른 프로세스들 간에 PID(Process ID)를 통해 구분되고 관리됩니다.  

 

자식 프로세스는 생성될 당시 부모 프로세스의 권한(Privileges), 스케줄링 속성들(Scheduling Attributes), 그리고 특정 자원들(Resources)을 물려받기에 부모 프로세스와 같다고 볼 수 있습니다. 하지만, 부모 프로세스의 차이점은 PID가 다르다는 점입니다.  

 

UNIX Windows

 

UNIX와 Windows는 자식 프로세스를 생성할 때 차이점이 존재합니다. UNIX에서 제공하는 fork()는 자식 프로세스가 부모 프로세스의 주소 공간을 물려받는 반면에, CreateProcess()는 프로세스 생성 시 지정된 프로그램을 자식 프로세스의 주소 공간으로 로드하도록 요구합니다. 

 

Windows 코드를 보면, ZeroMemory() 함수를 통해 자식 프로세스에게 메모리를 할당해주고, 지정된 프로그램을 자식프로세스의 주소 공간에 로드하도록 구현된 것을 확인할 수 있습니다. 

 

STARTUPINFO : 새로운 프로세스의 많은 속성들을 포함하고 있는 구조체

PROCESS_INFORMATION : 새로운 프로세스를 위한 핸들(handle)과 식별자(identifier)를 포함한 구조체 

2. Process Termination

프로세스가 마지막 구문을 실행하여 종료하게 되면 exit() system call을 호출하여 삭제를 요청하게 됩니다. 이후, 프로세스에게 할당된 모든 자원은 해제(deallocation)됩니다. 

 

부모 프로세스는 그럼 어떻게 자식 프로세스가 종료되었다는 것을 알 수 있을까요? 

pid = wait(&status); 
// If no parent waiting (did not invoke wait()) process is a zombie
// If parent terminated without invoking wait(), process is an orphan

우선, 부모 프로세스는 wait() system call을 사용하여 자식 프로세스가 종료될 때까지 기다립니다. 이후, 자식 프로세스가 종료되면 부모 프로세스에게 SIGCHLD Signal을 보내게 되어 부모 프로세스는 자식이 종료된 것을 알 수 있게 됩니다. 다음으로, wait() system call은 자식 프로세스의 종료 상태를 부모 프로세스에게 전달하고, 종료된 자식 프로세스의 PID 또한 반환됩니다.

 

자식 프로세스가 종료되어 할당된 자원이 모두 해제되었다고 하더라도, 부모 프로세스가 wait() 함수를 호출할 때까지, 프로세스 엔트리 테이블 (Process Entry Table)에는 자식 프로세스가 존재해야합니다.  

 

그럼, 만일 부모 프로세스가 wait()을 호출하지 않고 자식 프로세스보다 먼저 종료되었다면, 자식 프로세스가 고아 프로세스(Orphan Process)가 되는 현상이 발생할 수도 있습니다. 이를 방지하기 위해 부모가 종료되면, 모든 자식 프로세스도 종료되도록 하는 시스템도 존재합니다. 

 

추가적으로, 만일 자식 프로세스가 종료되었는데 부모 프로세스에서 wait() 함수가 아직 호출되지 않은 경우, 좀비 프로세스(Zombie Process)가 생길 수도 있습니다. 

 

즉, 프로세스가 생성되었으면 종료가 수행되는 것 또한 매우 중요하다는 것을 알 수 있습니다.   

IPC(Interprocess Communication)

일반적으로, 프로세스는 자원을 공유하지 않고 독립적으로 시스템에서 실행됩니다. 하지만, 프로세스 간에 통신을 수행한다면 자원을 공유할 수 있습니다. 다른 프로세스와 자원을 공유하는 Process Cooporation 환경을 제공하는 이유는 다음과 같습니다.

 

1. Information sharing : 여러 애플리케이션은 같은 정보를 얻기 원할 수도 있습니다. (ex, Copy and Paste) 

2. Computation speedup : 특정 일을 빠르게 수행하기 위해, Subtask로 나누어 병렬적으로 수행하길 원할 수 있습니다.  

3. Modularity : 우리는 시스템 기능을 별도의 프로세스 또는 스레드로 나누는 모듈식 방식으로 시스템을 구성하길 원할 수 있습니다.

 

위와 같은 이유로 인해, Cooprating processes는 프로세스 간에 데이터를 주고 받을 수 있도록 하기 위해 IPC(Interprocess Communication) 메커니즘을 제공합니다. 

 

IPC 방식에는 기본적으로 Shared Memory와 Message passing 방식이 존재합니다. 

 

(a) Shared Memory (b) Message passing

1. Shared Memory

Shared Memory 방식은 공유하기 위한 특정 메모리 공간을 생성하여 프로세스가 Shared Memory 영역에 데이터를 읽기 혹은 쓰기를 수행하여 정보를 교환할 수 있도록 해줍니다.  

 

Shared Memory 방식은 Message passing 방식보다 빠릅니다. 그 이유는 Message passing 방식은 시스템 콜을 사용하도록 구현되어, 커널의 개입으로 많은 시간을 들게 됩니다.  

 

물론, Shared Memory 방식에서도 시스템 콜을 사용합니다. 하지만, 오직 Shared Memory 영역을 생성하는데만 사용된다는 점에서 차이가 있습니다. 공유 메모리 영역이 생성되고 나면, 모든 액세스가 일상적인 메모리 액세스로 취급되어, 커널의 도움이 요구되지 않습니다. 

 

단, 주의해야할 점은 공유된 메모리에 쓰기 작업을 할 때, 두 프로세스 이상이 같은 공간에서 동시에 작업을 수행해서는 안됩니다. 그 이유는 Race Condition 문제가 발생할 수 있기 때문입니다.  

Race Condition

같은 자원에 두 개 이상의 프로세스가 동시에 접근하여 발생하는 문제입니다.

 

예를 들어, Producer 프로세스와 Consumer 프로세스가 있다고 가정해봅시다.

Producer 는 counter 값을 1 증가시키고, Consumer 는 counter 값을 1 감소시키려고 합니다.  

// Producer
register1 = counter
register1 = register1 + 1
counter = register1
// Consumer 
register2 = counter
register2 = register2 - 1
counter = register2

 

S0: producer execute register1 = counter         {register1 = 5}
S1: producer execute register1 = register1 + 1   {register1 = 6}
S2: consumer execute register2 = counter        {register2 = 5}
S3: consumer execute register2 = register2 – 1  {register2 = 4}
S4: producer execute counter = register1         {counter = 6 }
S5: consumer execute counter = register2        {counter = 4}

 

위의 예시와 같이, Producer가 증가한 값을 counter에 쓰지 않았는데, Consumer가 counter 값을 읽어버리게 되면, 증가되지 않은 counter 값을 읽게되어 각각의 프로세스가 다른 counter 값을 가지게 됩니다. 

 

Mutli-Processing 혹은 Multi-Threading 환경에서 특정 자원을 공유해야하는 상황이 생기면, 항상 Race Condition 문제는 따라올 수 밖에 없기 때문에 항상 주의 해야합니다. 

2. Message passing 

Message passing 방식은 프로세스 간에 메세지를 주고 받는 것을 가능하게 해주며, 같은 메모리를 공유할 필요없이 Message queue를 통해 메세지를 송수신하고, 동기화(Synchronize)하는 매커니즘을 제공합니다. 이는 충돌을 피할 필요가 없기 때문에 작은 양의 데이터를 교환하는데 유용하며, 분산된 시스템(Distributed System)에서 Shared Memory 방식보다 더 구현하기 쉽습니다. 

 

두 프로세스 간 데이터를 주고 받기 위해서는 Communication Link를 생성해야합니다. 이후, send()와 receive()를 통해 메세지를 주고 받을 수 있게 됩니다. 

 

Communication Link를 구현하기 위한 방법으로는 물리적인(Physical) 방법과 논리적인(Logical) 방법이 존재합니다. 물리적인 방법에는 Shared Memory, Hardware bus, Network 등이 존재하며, 논리적인 방법에는 Direct or indirect, Synchronous or asynchronous, Automatic or explicit buffering이 존재합니다. 

 

Direct communciation 방식은 자동적으로 Link가 생성되며, 한 Link에 한 쌍을 가지게 됩니다. 또한, 이는 프로세스 간 통신을 위해 서로의 정체(identity)를 필요로 합니다. 반면, Indirect Communication 방식은 메일박스 또는 포트로 메세지를 전달하여, 메일 박스를 공유할 때, Link가 생성됩니다. 이는 각 메일 박스가 unique id를 갖기에 프로세스의 정체(identity)를 필요로 하지 않습니다. 

 

Synchronous 방식은 send()와 receive() 둘 다 메세지가 전달될 때까지 혹은 메세지를 받을 수 있을 때까지 대기하게 됩니다. 반면, Asynchronus 방식은 대기하지 않고 메세지를 전달하거나 메세지를 읽습니다.

 

Automatic buffering 방식은 Bounded capacity와 Unbounded capacity가 존재합니다. Bounded capacity는 Message queue 공간이 남으면 계속 send()를 수행하지만, 가득찰 경우 공간이 생기기전까지 block을 수행합니다. 반면, Unbounded capacity는 공간에 제약없이 무한히 send를 보낼 수 있고 항상 unblock 상태를 유지합니다.

 

No buffering 방식은 Zero capacity를 갖습니다. Zero capacity는 Message queue 최대 길이가 0이기 때문에, Link에서 대기하는 메세지가 없습니다. 그렇기에, Sender는 Receiver가 메세지를 받을 때까지 대기해야합니다. 

Examples of IPC Systems

예제를 통해, 위에서 설명한 내용들을 알아봅시다. IPC의 예시로 Windows API는 다루지 않고, POSIX API에 Shared Memory와 Pipes에 대해서만 다루겠습니다. 

1. Shared Memory

Producer process

#include <stdio.h>
#include <stdlib.h> 
#include <string.h> 
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h> 
#include <sys/mman.h> 

int main()
{
/* the size (in bytes) of shared memory object */ 
const int SIZE = 4096; 
/* name of the shared memory object */ 
const char *name = "OS";
/* strings written to shared memory */
const char *message_0 = "Hello"; 
const char *message_1 = "World!"; 

/* shared memory file descriptor */
int fd; 
/* pointer to shared memory object */
char *ptr;

    /* create the shared memory object */
    fd = shm_open(name, O_CREAT | O_RDWR, 0666); 
    
    /* configure the size of the shared memory object */
    ftruncate(fd, SIZE); 
    
    /* memory map the shared memory object */ 
    ptr = (char *) mmap(0, SIZE, PORT_READ | PORT_WRITE, MAP_SHARED, fd, 0); 
    
    /* write to the shared memory object */
    sprintf(ptr,"%s",message_0); 
    ptr += strlen(message_0); 
    sprintf(ptr,"%s",message_1);
    ptr += strlen(message_1);
    
    return 0 ;
}

Producer 프로세스는 Shared Memory를 생성하고, Shared Memory에 Hello World!를 작성합니다.

 

shm_open()의 첫 번째 파라미터는 Shared Memory Object의 이름입니다. 다른 프로세스는 해당 이름을 통해 Shared Memory에 접근 할 수 있게 됩니다. 두 번째 파라미터에  O_CREAT | O_RDWR 값은 파일이 없을 시에 생성하고, 읽기/쓰기를 수행할 수 있도록 해줍니다. 세 번째 파라미터는 파일 접근 권한을 설정합니다. 

 

ftruncate()의 첫 번째 파라미터에는 shm_open()의 반환 값인 File descriptor 값을 넘기고, 두 번째 파라미터에는 Shared Memory의 크기를 지정해줍니다. 

 

다음으로, mmap()을 통해 Shared Memory Object를 포함하는 파일을 메모리에 매핑시킵니다. 이후, 매핑된 메모리 주소에 Hello World!를 쓰게 됩니다. 

Consumer process

#include <stdio.h>
#include <stdlib.h> 
#include <string.h> 
#include <fcntl.h>
#include <sys/shm.h>
#include <sys/stat.h> 
#include <sys/mman.h> 

int main()
{
/* the size (in bytes) of shared memory object */ 
const int SIZE = 4096; 
/* name of the shared memory object */ 
const char *name = "OS";
/* shared memory file descriptor */
int fd; 
/* pointer to shared memory object */
char *ptr;

    /* create the shared memory object */
    fd = shm_open(name, O_RDONLY, 0666); 
    
    /* memory map the shared memory object */ 
    ptr = (char *) mmap(0, SIZE, PORT_READ | PORT_WRITE, MAP_SHARED, fd, 0); 
    
    /* read from the shared memory object */
    printf("%s", (char *)ptr); 
    
    /* remove the shared memory object */ 
    shm_unlink(name); 
    
    return 0 ;
}

Consumer 프로세스는 Producer 프로세스에서 Shared Memory를 생성할 때 지은 이름을 사용하여 shm_open()을 통해 파일 디스크립터 값을 받아 mmap()으로 메모리 매핑을 진행합니다. 이후, 매핑된 주소에 접근하여 내용을 읽어오고, shm_unlink를 통해 할당된 Shared Memory를 해제합니다. 

 

2. Message passing

Message passing 방식에는 파이프(Pipes)와 소켓(Socket)이 존재합니다. 파이프(Pipes)는 같은 머신에서 프로세스가 통신할 때 사용되며, Unnamed pipes(= Ordinary pipes)와 Named pipes로 두 가지 종류가 존재합니다. 소켓(Socket)은 다른 머신과 통신할 때 사용됩니다. 

Unnamed pipes

File descriptors for an ordinary pipe

Unnamed pipe는 단방향(unidirectional) 통신을 제공합니다. 한 방향으로만 통신이 가능하며, 양쪽에서 동시에 write를 수행할 수 없습니다. 예를 들어, 부모 프로세스가 fd[1]를 사용하면, 자식 프로세스는 fd[1]를 사용할 수 없게 되는 것입니다.  

 

파이프(Pipe)를 설명하기 앞서, 파일 디스크립터에 대해 알아야합니다. 파일 디스크립터 값이 0인 경우, Standard Input, 1인 경우 Standard Output, 2인 경우 Standard Error를 의미합니다. 그렇기에, fd[0]는 파이프의 read end, fd[1]은 파이프의 write end가 되는 것입니다. 

 

UNIX 시스템에서 Unnamed pipe는 pipe(int fd[]) 함수를 사용하여 파이프를 생성하고, 생성된 파이프는 파이프는 read()와 write() system call을 사용하여 접근할 수 있습니다. 

 

#include <sys/types.h> 
#include <stdio.h>
#include <string.h> 
#include <unistd.h> 

#define BUFFER_SIZE 25
#define READ_END 0
#define WRITE_END 1 

int main(void)
{
    char write_msg[BUFFER_SIZE] = "Greetings"; 
    char read_msg[BUFFER_SIZE]; 
    int fd[2]; 
    pid_t pid ; 
    
    /* create the pipe */
    if (pipe(fd) == -1) { 
    	fprintf(stderr, "Pipe failed") ; 
        return 1; 
    }
    
    /* fork a child process */ 
    pid = fork() ; 
    
    if (pid < 0) { /* error occurred */ 
    	fprintf(stderr, "Fork Failed");
        return 1;
    } 
    
    if (pid > 0) { /* parent process */ 
    	/* close the unused end of the pipe */ 
        close(fd[READ_END]); 
        
        /* write to the pipe */ 
        write(fd[WRITE_END], write_msg, strlen(write_msg) + 1) ; 
        
        /* close the write end of the pipe */ 
        close(fd[WRITE_END]); 
    } 
    else { /* child process */ 
    	/* close the unused end of the pipe */ 
        close(fd[WRITE_END]); 
        
        /* read from the pipe */ 
        read(fd[READ_END], read_msg, BUFFER_SIZE); 
        printf("read %s", read_msg);
        
        /* close the read end of the pipe */ 
        close(fd[READ_END]); 
    }
    
	return 0;
}

위 예제는 파이프를 생성하고 부모 프로세스가 자식 프로세스를 생성한 후, 부모 프로세스가 "Greeting" 문자열을 파이프에 쓰고, 자식 프로세스가 "Greeting" 문자를 읽어 출력하는 코드입니다. 

 

Named pipes

Named pipe는 양방향(bidirectional) 통신을 제공합니다. 하지만, Full-duplex가 아닌 half-duplex 전송을 제공하기 때문에 두 프로세스 사이에서 하나의 프로세스만 한 번에 데이터를 송수신할 수 있습니다. 즉, 프로세스 간 송수신 동시에 수행하려면, 두 개의 Named pipe를 사용해야합니다. 

 

Named pipes의 장점으로는 프로세스간 통신을 할 때, parent-child 관계가 필요하지 않으며, 여러 프로세스가 named pipe를 통해 통신이 가능합니다. Named pipes를 생성하게 되면, 이는 파일 시스템에 전형적인 파일로 나타나게 됩니다. 그렇기 때문에, 프로세스간 통신이 끝난 후에도 named pipes는 존재하게 됩니다. 

 

추가적으로, Named pipes로 통신하는 프로세스들은 같은 머신(same machine)에 거주하며, 다른 머신과 통신하기 위해서는 Socket을 사용해야합니다. 

Socket

Socket communication

소켓(Socket)은 통신을 위한 끝지점(endpoint)으로 정의됩니다. 소켓(Socket)은 IP address와 Port number로 식별되며, 클라이언트와 서버 구조로 사용됩니다. 서버는 요청이 들어올 때까지 대기하고, 클라이언트는 서버에게 특정 포트(port)로 요청(request)을 보냅니다. 

 

Date Server

import java.net.*; 
import java.io.*; 

public class DateServer
{
    public static void main(String[] args) { 
    	try{
            ServerSocket sock = new ServerSocket(6013); 
            
            /* now listen for connections */
            while (true) { 
            	Socket client = sock.accept() ; 
                
                PrintWriter pout = new PrintWriter(client.getOutputStream(), true) ; 
                
                /* write the Date to the socket */ 
                pout.println(new java.util.Date().toString()); 
                
                /* close the socket and resume */ 
                /* listening for connections */ 
                client.close() ;
           } 
        }
        catch (IOException ioe) {
        	System.err.println(ioe); 
        }
    }  
}

Date client

import java.net.*; 
import java.io.*; 

public class DateClient
{
    public static void main(String[] args) {
    	try{
        	/* make connection to server socket */
            Socket sock = new Socket("127.0.0.1", 6013); 
            
            InputStream in = sock.getInputStream() ;
            BufferedReader bin = new BufferedReader(new InputStreamReader(in)) ; 
            
            /* read the date from the socket */ 
            String line ; 
            while ( (line = bin.readLine()) != null)
            	System.out.println(line); 
            
            /* close the socket connection */ 
            sock.close(); 
        }
        catch (IOException ioe) { 
        	System.err.println(ioe); 
        }
    }
}

위 예제는 Java로 구현된 코드이며, 서버에서 현재 날짜를 클라이언트에게 전송하는 코드입니다. 

 

단, 서버의 포트는 2^10(= 1024) 보다 작거나 같은 값들은 잘 알려진 포트(well known port)들로 지정되어 있으며, 다양한 서비스들이 구현되어 제공되고 있습니다. 포트 번호의 범위는 0 ~ 2^16(=65536)로 1024 보다 큰 포트 번호들은 임의의 번호(arbitrary number)로 구성되어있습니다. 

 

소켓(Socket)은 분산된 프로세스(distributed processes) 간에 통신하는 형태로 low-level로 간주됩니다. 한 가지 이유는 스레드(Thread)가 소켓을 사용하여 다른 스레드와 통신할 떄, 구조화되지 않은 바이트 스트림을 교환하기 때문입니다. 반면, higher-level 통신 방식으로는 RPCs(Remote Procedure Calls) 방식이 존재합니다.

 

이와 같이, 운영체제는 프로세스와 프로세스 간에 통신을 위해 다양한 기능들을 제공하고 있는 것을 알 수 있습니다. 프로세스의 개념과 프로세스 간에 통신 방식이 어떤 것들이 있는지 알아보았고, 다음엔 Thread에 대해 다뤄보겠습니다.

 

*참고 자료

https://www.geeksforgeeks.org/difference-between-shared-memory-model-and-message-passing-model-in-ipc/

https://www.totalphase.com/blog/2022/10/difference-between-half-duplex-vs-full-duplex/