이번 포스팅에서는 IPC(Inter-Process Communication)가 무엇인지에 대해 알아보고,
IPC 통신 방법 중 Named Pipe, Shared Memory가 무엇인지 그리고 예제를 통해 C#에서는 어떻게 사용할 수 있는지 알아보겠습니다.
IPC(Inter-Process Communication)란?
개요
IPC(Inter-Process Communication)는 컴퓨터 운영 체제에서 서로 다른 프로세스 간에 데이터를 주고받는 방법을 의미합니다. IPC는 여러 프로세스가 서로 정보를 교환하거나 리소스를 공유해야 할 때 사용됩니다. 일반적으로 IPC는 다음과 같은 상황에서 사용됩니다:
- 프로세스 간 데이터 교환: 서로 다른 프로세스가 데이터를 주고받아야 할 때.
- 프로세스 간 동기화: 프로세스들이 특정 이벤트를 기다리거나, 순서를 정해 실행되어야 할 때.
- 리소스 공유: 여러 프로세스가 메모리, 파일, 네트워크 소켓 등 특정 리소스를 공유해야 할 때.
IPC 특징
IPC는 다음과 같은 특징을 가지고 있습니다.
- 프로세스 독립성 유지: 프로세스 간 통신은 서로 다른 프로세스의 실행 방식이나 메모리 공간에 영향을 미치지 않습니다.
- 고속 통신: IPC는 동일한 시스템 내에서 실행되는 프로세스 간의 통신이므로 네트워크 통신보다 훨씬 빠릅니다. 이는 주로 메모리를 직접 공유하는 방식인 Shared Memory에서 두드러집니다.
- 낮은 지연 시간: IPC는 프로세스 간 직접적인 통신을 가능하게 하므로, 통신의 지연 시간을 최소화할 수 있습니다. 이는 실시간 데이터 처리가 필요한 애플리케이션에서 매우 중요합니다.
- 효율적인 자원 공유: IPC를 사용하면 여러 프로세스가 동일한 자원을 효율적으로 공유할 수 있습니다. 예를 들어, Named Pipe와 Shared Memory는 데이터의 중복 없이 자원을 공유할 수 있게 해줍니다.
- 동기화 지원: IPC는 프로세스 간의 동기화를 지원하여, 여러 프로세스가 특정 이벤트를 기다리거나 순차적으로 실행될 수 있게 합니다. 이는 특히 동기화가 중요한 멀티스레드 애플리케이션에서 유용합니다.
IPC 통신 방법
IPC 통신 방법에는 여러 가지가 있지만, 이번 강좌에서는 Named Pipe, Message Queue, Shared Memory에 대해 중점적으로 다루겠습니다.

Named Pipe
개요
Named Pipe(명명된 파이프)는 프로세스 간 통신(IPC, Inter-Process Communication) 방법 중 하나로, Pipe에 이름을 명명하고, 해당 이름을 매개로 프로세스 간 통신할 수 있게 해줍니다.
Named Pipe는 유닉스 계열 운영 체제와 윈도우에서 모두 사용 가능하며, 특히 윈도우에서는 System.IO.Pipes
네임스페이스를 통해 쉽게 사용할 수 있습니다.
Named Pipe의 구조
Named Pipe는 서버와 클라이언트 모델을 사용하여 통신합니다.
서버는 파이프를 생성하고 클라이언트는 해당 파이프에 연결하여 데이터를 주고받습니다.
Named Pipe의 장점
- 간편한 사용:
- 상대적으로 구현이 간단하며, 프로세스 간 데이터 통신을 쉽게 설정할 수 있습니다.
- 높은 성능:
- 로컬 시스템에서 높은 성능을 제공합니다. 특히, 메모리 공유를 통해 데이터를 전달하는 Shared Memory와 비교해도 높은 성능을 보입니다.
- 보안 기능 제공:
- 윈도우의 ACL(Access Control List)을 이용해 파이프에 대한 접근을 제어할 수 있어 보안성이 뛰어납니다.
Named Pipe의 단점
- 운영 체제 종속성:
- 특정 운영 체제에 종속적인 기능이 있어, 윈도우와 유닉스 계열 시스템 간의 호환성 문제가 발생할 수 있습니다.
- 설정 복잡성:
- 네트워크를 통한 Named Pipe 사용 시, 방화벽 설정 등 네트워크 관련 추가 설정이 필요할 수 있습니다.
- 리소스 관리:
- 파이프가 열려 있는 동안 시스템 리소스를 사용하므로, 사용 후 적절한 리소스 해제가 필요합니다.
주요 네임스페이스
- System.IO.Pipes:
- Named Pipe와 관련된 모든 클래스를 포함하는 네임스페이스입니다.
주요 클래스
- NamedPipeServerStream:
- Named Pipe 서버를 생성하고 클라이언트의 연결을 기다립니다.
- NamedPipeClientStream:
- Named Pipe 클라이언트를 생성하고 서버에 연결합니다.
- PipeStream:
- NamedPipeServerStream과 NamedPipeClientStream의 부모 클래스입니다.
주요 함수
- PipeStream.Read(byte[] buffer, int offset, int count):
- 파이프에서 데이터를 읽어옵니다.
- PipeStream.Write(byte[] buffer, int offset, int count):
- 파이프에 데이터를 씁니다.
- NamedPipeServerStream.WaitForConnection():
- 클라이언트의 연결을 기다립니다.
- NamedPipeClientStream.Connect(int timeout):
- 서버에 연결을 시도합니다.
예제 코드
서버(Server)
using System; using System.IO.Pipes; using System.Text; namespace SharedMemoryReader { class Program { static void Main(string[] args) { string message = ""; while (message != "shutdown") { // Create Named Pipe Server Stream with the name "devitworld_pipe" using (NamedPipeServerStream pipeServer = new NamedPipeServerStream("devitworld_pipe", PipeDirection.In)) { Console.WriteLine("Named pipe server started."); pipeServer.WaitForConnection(); // Wait for client connection Console.WriteLine("Client connected."); byte[] buffer = new byte[256]; int bytesRead = pipeServer.Read(buffer, 0, buffer.Length); // Read message from client message = Encoding.UTF8.GetString(buffer, 0, bytesRead); Console.WriteLine("Received from client: " + message); } } } } }
클라이언트(Client)
using System; using System.IO.Pipes; using System.Text; using System.Threading.Tasks; namespace SharedMemoryWriter { class Program { static void Main(string[] args) { string message = "Hello from client!"; while (message != "exit") { // Create Named Pipe Client Stream with the name "devitworld_pipe" using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", "devitworld_pipe", PipeDirection.Out)) { try { // Connect to the server Task connectTask = Task.Run(() => pipeClient.Connect()); if (connectTask.Wait(TimeSpan.FromSeconds(3))) { Console.WriteLine("Connected to server."); Console.Write("Enter message: "); message = Console.ReadLine(); byte[] buffer = Encoding.UTF8.GetBytes(message); pipeClient.Write(buffer, 0, buffer.Length); // Send message to server Console.WriteLine("Message sent to server."); } else { Console.WriteLine("Failed to connect to server. (Timeout)"); } } catch (Exception ex) { Console.WriteLine("Failed to connect to server. " + ex.Message); } } } } } }
결과

Shared Memory
개요
Shared Memory(공유 메모리)는 프로세스 간 통신(IPC, Inter-Process Communication) 방법 중 하나로, 여러 프로세스가 동일한 메모리 공간을 공유하여 데이터를 주고받을 수 있게 해줍니다.
Shared Memory는 특히 빠른 속도가 요구되는 상황에서 유용하며, 메모리의 특정 영역을 여러 프로세스가 읽고 쓸 수 있게 합니다.
Shared Memory의 구조
Shared Memory는 일반적으로 다음과 같은 단계를 통해 설정되고 사용됩니다:
- 메모리 맵 생성:
- 프로세스가 메모리 맵을 생성하고 이를 공유합니다.
- 메모리 맵 열기:
- 다른 프로세스가 이 메모리 맵을 열어 접근합니다.
- 데이터 읽기/쓰기:
- 모든 프로세스가 이 메모리 맵에 접근하여 데이터를 읽고 쓸 수 있습니다.
Shared Memory의 장점
- 고속 통신:
- 프로세스 간 데이터를 메모리로 직접 주고받기 때문에 매우 빠른 통신이 가능합니다.
- 낮은 지연 시간:
- 메모리에 직접 접근하므로 지연 시간이 거의 없습니다.
- 효율적인 자원 사용:
- 메모리를 공유함으로써 데이터 복사를 줄이고, 효율적인 자원 사용이 가능합니다.
Shared Memory의 단점
- 동기화 문제:
- 여러 프로세스가 동시에 메모리에 접근할 때 동기화 문제(예: 레이스 컨디션, 데드락)가 발생할 수 있습니다.
- 보안 문제:
- 공유 메모리의 접근 권한을 적절히 설정하지 않으면, 데이터가 쉽게 노출될 수 있습니다.
- 운영 체제 종속성:
- 공유 메모리 구현은 운영 체제에 따라 다를 수 있습니다.
주요 네임스페이스
- System.IO.MemoryMappedFiles:
- .NET에서 공유 메모리를 사용하기 위한 클래스를 포함하는 네임스페이스입니다.
주요 클래스
- MemoryMappedFile:
- 공유 메모리 파일을 생성하고 관리하는 클래스입니다.
- MemoryMappedViewAccessor:
- 공유 메모리 파일의 뷰를 액세스하는 클래스입니다.
- MemoryMappedViewStream:
- 공유 메모리 파일의 스트림을 액세스하는 클래스입니다.
주요 함수
- MemoryMappedFile.CreateNew(string mapName, long capacity):
- 새로운 메모리 맵을 생성합니다.
- MemoryMappedFile.OpenExisting(string mapName):
- 기존의 메모리 맵을 엽니다.
- MemoryMappedFile.CreateViewAccessor():
- 메모리 맵의 뷰에 접근할 수 있는 뷰 액세서를 생성합니다.
- MemoryMappedViewAccessor.Read<T>(long position):
- 특정 위치에서 데이터를 읽어옵니다.
- MemoryMappedViewAccessor.Write<T>(long position, T value):
- 특정 위치에 데이터를 씁니다.
예제 코드
서버(Server)
using System; using System.IO.MemoryMappedFiles; using System.Text; using System.Threading; namespace SharedMemoryServer { class Program { static void Main(string[] args) { using (MemoryMappedFile mmf = MemoryMappedFile.CreateNew("devitworld_shared_memory", 1024)) { Console.WriteLine("Shared memory server started."); while (true) { // Read message from client using (var accessor = mmf.CreateViewAccessor()) { byte[] buffer = new byte[1024]; accessor.ReadArray(0, buffer, 0, buffer.Length); string message = Encoding.UTF8.GetString(buffer).TrimEnd('\0'); Console.WriteLine("Received from client: " + message); if (message == "shutdown") { break; } // make empty the shared memory accessor.WriteArray(0, new byte[1024], 0, 1024); } Thread.Sleep(2000); } } } } }
클라이언트(Client)
using System; using System.IO.MemoryMappedFiles; using System.Text; namespace SharedMemoryClient { class Program { static void Main(string[] args) { // Open existing shared memory with the name "devitworld_shared_memory" try { using (MemoryMappedFile mmf = MemoryMappedFile.OpenExisting("devitworld_shared_memory")) { string message = "Hello from client!"; while (message != "exit") { Console.Write("Enter message: "); message = Console.ReadLine(); byte[] buffer = Encoding.UTF8.GetBytes(message); using (var accessor = mmf.CreateViewAccessor()) { accessor.WriteArray(0, buffer, 0, buffer.Length); Console.WriteLine("Message sent to server."); } } } } catch (Exception ex) { Console.WriteLine("Failed to open shared memory. " + ex.Message); } } } }
결과

참고 링크
- My Git Repository (devitworld-csharp-basic) – DevitworldConsoleApp/9_1_NamedPipeServer
- My Git Repository (devitworld-csharp-basic) – DevitworldConsoleApp/9_1_NamedPipeClient
- My Git Repository (devitworld-csharp-basic) – DevitworldConsoleApp/9_2_SharedMemoryClient
- My Git Repository (devitworld-csharp-basic) – DevitworldConsoleApp/9_2_SharedMemoryServer