프로세스 간 통신(Inter-Process Communication, IPC), Socket, RPC

dong5854·2022년 7월 25일
0
post-thumbnail

concurrent하게 동작하는 프로세스는 독립적 프로세스일수도 있고, 협력저인 프로세스일수도 있다.
여기서 독립적이란 해당 프로세스가 실행 중인 다른 프로세스들과 데이터를 공유하지 않는다는 뜻이고, 협력적이란 프로세스가 시스템에서 실행 중인 다른 프로세스들에 영향을 주거나 받는다. 이 협력적 프로세스들이 서로 데이터를 보내거나 받으려면 프로세스 간 통신(Inter-Process Communication, IPC) 기법이 필요하다.


IPC의 2가지 모델

공유메모리(Sharded Memory) 모델과 메시지 전달(Message Passing) 모델

IPC는 기본적으로 데이터를 교환(exchange)하는 것인데, 여기에는 공유 메모리(shared memory)와 메시지 전달(message passing) 두 가지 모델이 있다.

  • 공유 메모리 모델에서는 협력 프로세스들에 의해 공유되는 메모리의 영역이 구축된다.
  • 메세지 전달 모델에서는 통신이 협력 프로세스들 사이에 교환되는 메시지를 통해 이루어진다.

    위의 그림에서 (a)가 메시지 전달 모델, (b)가 공유 메모리 모델을 나타낸다.

두 모델의 의의

운영체제에서 많은 시스템이 두 가지 모델을 모두 구현하는데, 이는 메시지 전달 모델충돌을 회피할 필요가 없어 적은 양의 데이터를 교환하는데 유용하고 분산 시스템에서 공유메모리보다 구현이 쉽다. 하지만 메시지 전달 시스템은 통상 시스템 콜을 사용하여 구현하기 때문에 커널 간섭 등의 부가적인 시간 소비 작업이 필요해 공유 메모리 모델이 메시지 전달 모델보다 빠르다. 공유 메모리 시스템에서는 공유 메모리 영역을 구축할 때만 시스템 콜이 필요한데, 한번 공유 메모리 영역이 구축되면 모든 접근은 일반적인 메모리 접근으로 취급되어 커널의 도움이 필요없다.

공유 메모리 시스템의 프로세스 간 통신

일반적으로 운영체제는 한 프로세스가 다른 프로세스의 메모리에 접근하는 것을 금지한다. 하지만 공유 메모리는 둘 이상의 프로세스가 이 제약 조건을 제거하는 것에 동의하게 된다면 프로세스들은 공유 영역에 읽기 및 쓰기 작업을 진행하여 정보를 교환할 수 있도록 한다. 데이터의 형식과 위치는 이 프로세스들에 의해 결정되며 운영체제의 소관이 아니게 된다. 또한 프로세스들은 동시에 동일한 위치에 쓰지 않도록 해야한다.

생산자-소비자 문제(Producer-Consumer Problem)

생산자 소비자 문제는 협력하는 프로세스의 일반적인 페러다임으로, 생산자 프로세스는 정보를 생산하고, 소비자 프로세스는 정보를 소비한다. 예를 들자면 컴파일러는 어셈블리 코드를 생산하고, 어셈블러는 이것을 소비한다. 혹은 웹 서버는 HTML 파일과 이미지와 같은 웹 컨텐츠를 생산하면 이 자원들을 브라우저가 소비한다.

이 생산자-소비자 문제를 두 개의 프로세스의 문제로 생각해보자. 생산자와 소비자 프로레스들이 concurrent하게 실행되려면 생산자가 정보를 채워넣고 소비자가 이를 소모할 수 있는 버퍼가 반드시 사용 가능해야한다. 이 버퍼는 생산자와 소비자가 공유하는 메모리 영역에 존재하며 생산자가 한 항목을 생산하고, 그동안에 소비자는 다른 항목을 소비할 수 있다. 생산자와 소비자가 반드시 동기화 되어야만 생산되지 않은 항목들을 소비자가 소비하려고 시도하는 문제가 일어나지 않는다. 여기서 중요한 부분은 버퍼가 공유하는 메모리 영역에 존재한다는 점이다.

버퍼에는 2가지 유형이 있는데, 무한 버퍼(unbounded buffer)와 유한 버퍼(bounded buffer)이다. 무한 버퍼는 버퍼의 크기에 실질적인 한계가 없기 때문에 소비자는 새로운 항목을 기다려야 할 수도 있지만 생산자는 항상 새로운 항목을 생산할 수 있다. 이에 반에 유한 버퍼는 버퍼의 크기가 고정되어 있다고 가정하기 때문에 소비자가 새로운 항목을 기다리는 것은 물론, 모든 버퍼가 채워져 있으면 생산자 또한 대기해야 할 수 있다.

이 방법은 성능은 좋으나 프로세스간에 동기화 문제가 발생할 수 있고, 메모리에 접근하고 저작하는 코드가 응용 프로그래머에 의해 명시적으로 작성되어야 한다.

메시지 전달 시스템에서의 프로세스 간 통신

메시지 전달 시스템은 운영체제가 메시지 전달 설비를 통해 서로 협력하는 프로세스 간의 통신 수단을 제공해 주는 방법이다. 운영체제가 알아서 동기화 문제를 해결해 주기 때문에 안전하지만 공유 메모리 시스템에 비해 성능이 떨어진다.

메시지 전달 시스템은 최소한 두 가지 연산을 제공한다.

  • send(message)
  • receive(message)

만약 프로세스 P와 Q가 통신을 원하면 반드시 서로 메시지를 보내고 받아야 한다. 이를 위해서는 이들 사이에 통신 연결(Communication Link)가 설정되어야 한다. 하나의 링크와 send(), receive() 연산을 논리적으로 구현하는 여러 방법들은 다음과 같다.

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

직접 통신과 간접 통신

직접 통신하에서 통신을 원하는 각 프로세스는 통신의 수신자 또는 송신자의 이름을 명시해야 한다. 이 기법에서 send()와 receive()의 프리미티브들은 다음과 같이 정의한다.

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

이 기법에서 통신 연결은 아래와 같은 특성을 가진다.

  • 통신을 원하는 각 프로세스의 쌍들 사이에 연결이 자동으로 구축된다. 프로세스들은 통신하기 위해 상대방의 신원만 알면 된다.
  • 연결은 정확히 두 프로세스 사이에만 연관된다.
  • 통신하는 프로세스들의 각 쌍 사이에는 정확하게 하나의 연결이 존재한다.

간접 통신에서 메시지들은 메일박스(mailbox)또는 포트(port)로 송신되고 수신된다. 메일 박스(혹은 포트)는 추상적으로 프로세스들에 의해 메시지들이 넣어지고 메시지들이 제거될 수 있는 객체라고 볼 수 있다. 이 기법에서의 프리미티브들은 다음과 같이 정의 할 수 있다.

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

이 기법에서 통신 연결은 아래와 같은 특성을 가진다.

  • 한 쌍의 프로세스들 사이의 연결은 이들 프로세스가 공유 메일박스를 가직 때만 구축된다.
  • 연결은 두 개 이상의 프로세스들과 연관될 수 있다.
  • 통신하고 있는 각 프로세스 사이에는 다수의 서로 다른 연결이 존재할 수 있고, 각 연결은 하나의 메일박스에 대응된다.

즉 직접 통신 방식에 비해 복잡한 커뮤니케이션이 가능하다.

운영체제는 한 프로세스에 다음을 할 수 있도록 허용하는 기법을 만드시 제공해야한다.

  • 새로운 메일박스를 생성한다.
  • 메일박스를 통해 메시지를 송신하고 수신한다.
  • 메일박스를 삭제한다.

동기화

실제로 프리미티브를 구현하기 위해서는 다양한 설계 옵션이 있다.
블로킹(blocking) 혹은 논블로킹(nonblocking), 동기식(sysnchronous) 혹은 비동기식(asynchronous)이라고도 알려져 있다.

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

IPC 시스템의 사례

IPC 시스템의 사례들은 아래와 같다.

  • 공유 메모리를 사용하는 사례: POSIX API
  • 메시지 패싱을 사용하는 사례: Pipes

POSIX는 Portable Operating System Interface의 약자로 서로 다른 UNIX OS의 공통 API를 정리하여 이식성이 높은 응용 프로그램을 개발하기 위한 목적으로 IEEE가 제정한 API 규격이다.


클라이언트 서버 환경에서의 통신

앞에서는 공유 메모리와 메시지 전달 기법을 사용하여 프로세스들의 통신하는 방법에 대해 살펴보았는데, 이 기법들은 클라이언트 서버 시스템의 통신에도 사용될 수 있다. 이 통신 전력에는 두가지가 있는데 이는 바로 소켓(socket)과 원격 프로시저 호출(RPCs)이다.

소켓

소켓은 통신을 위한 양 종단(endpoint)를 뜻한다. 각각의 PC는 IP 주소를 통해 구별되고 파이프는 port를 통해 특정된다.

자바가 처음으로 소켓을 쉽게 사용할 수 있는 인터페이스를 선보였다. 자바는 세 가지 종류의 소켓을 제공한다.

  • Socket class: 연결 기반(connection-oriented), TCP
  • DatagramSocket class: 비연결성(connectionless), UDP
  • MulticastSocket class: DatagramSocket의 서브클래스로 데이터를 여러 수신자에게 보낼 수 있다.

예제
Operating System Concepts 10th edition, Avraham Silberschatz

// 소켓 서버
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*/
                client.close();
            }
        } catch (IOException ioe){
            System.err.println(ioe);
        }
    }
}
//소켓 클라이언트
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);
        }
    }
}

소켓 서버가 돌아가지 않을 때의 클라이언트 output

java.net.ConnectException: Connection refused

소켓 서버가 켜졌을 때의 클라이어트 output

Fri Jul 29 00:39:01 KST 2022

예제2
Java의 정석 3rd edition, 남궁성

// 소켓 서버
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.SimpleDateFormat;
import java.util.Date;

public class TcpIpServer {
    public static void main(String[] args) {
        ServerSocket serverSocket = null;

        try{
            //서버 소켓을 생성하여 7777번 포트와 결합(bind)시킨다.
            serverSocket = new ServerSocket(7777);
            System.out.println(getTime() + "서버가 준비되었습니다.");
        } catch (IOException e){
            e.printStackTrace();
        }

        while (true){
            try{
                System.out.println(getTime() + "연결요청을 기다립니다.");
                // Listens for a connection to be made to this socket and accepts it. The method blocks until a connection is made.
                // 서버 소켓은 클라이언트의 연결 요청이 올 때까지 실행을 멈추고 계속 기다린다.
                Socket socket = serverSocket.accept();
                System.out.println(getTime() + socket.getInetAddress() + "로부터 연결요청이 들어왔습니다.");

                // 소켓의 출력스트림을 얻는다.
                OutputStream out = socket.getOutputStream();
                DataOutputStream dos = new DataOutputStream(out);

                // 원격 소켓(remote socket)에 데이터를 보낸다.
                dos.writeUTF("[Notice] Test Message1 from Server.");
                System.out.println(getTime() + " 데이터를 전송했습니다.");

                // 스트림과 소켓을 닫아준다.
                dos.close();
                socket.close();
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }

    private static String getTime() {
        SimpleDateFormat f = new SimpleDateFormat("[hh:mm:ss]");
        return f.format(new Date());
    }
}
// 소켓 클라이언트
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.ConnectException;
import java.net.Socket;

public class TcpIpClient {
    public static void main(String[] args) {
        try {
            String serverIp = "127.0.0.1";

            System.out.println("서버에 연결중입니다. 서버 IP :" + serverIp);
            // 소켓을 생성하여 연결을 요청한다.
            Socket socket = new Socket(serverIp, 7777);

            // 소켓의 입력스트림을 얻는다.
            InputStream in = socket.getInputStream();
            DataInputStream dis = new DataInputStream(in);

            // 소켓으로부터 받은 데이터를 출력한다.
            System.out.println("서버로부터 받은 메시지 :" + dis.readUTF());
            System.out.println("연결을 종료합니다.");

            // 스트림과 소켓을 닫는다.
            dis.close();
            socket.close();
            System.out.println("연결이 종료되었습니다.");
        } catch (ConnectException ce){
            ce.printStackTrace();
        } catch (IOException ioe){
            ioe.printStackTrace();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

소켓 서버가 돌아가지 않을 때의 클라이언트 output

서버에 연결중입니다. 서버 IP :127.0.0.1
java.net.ConnectException: Connection refused
	at java.base/sun.nio.ch.Net.connect0(Native Method)
	at java.base/sun.nio.ch.Net.connect(Net.java:579)
	at java.base/sun.nio.ch.Net.connect(Net.java:568)
	at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:588)
	at java.base/java.net.SocksSocketImpl.connect(SocksSocketImpl.java:327)
	at java.base/java.net.Socket.connect(Socket.java:633)
	at java.base/java.net.Socket.connect(Socket.java:583)
	at java.base/java.net.Socket.<init>(Socket.java:507)
	at java.base/java.net.Socket.<init>(Socket.java:287)
	at TcpIpClient.main(TcpIpClient.java:14)

소켓 서버가 켜졌을 때의 클라이언트 output

서버에 연결중입니다. 서버 IP :127.0.0.1
서버로부터 받은 메시지 :[Notice] Test Message1 from Server.
연결을 종료합니다.
연결이 종료되었습니다.

소켓 서버의 output

[01:25:34]서버가 준비되었습니다.
[01:25:34]연결요청을 기다립니다.
[01:25:36]/127.0.0.1로부터 연결요청이 들어왔습니다.
[01:25:37] 데이터를 전송했습니다.
[01:25:37]연결요청을 기다립니다.

RPCs(Remote Procedure Calls)

RPC는 별도의 원격 제어를 위한 코딩 없이 다른 주소 공간에서 함수나 프로시저를 실행할 수 있게하는 프로세스 간 통신 기술이다.
RPC는 클라이언트가 원격 호스트의 프로시저 호출을 하는 것을 마치 자기의 프로시저를 호출하는 것처럼 해준다. RPC 시스템은 클라이언트 사이드의 스텁을 제공하여 통신을 하는데 필요한 자세한 사항들을 숨겨준다. 클라이언트가 원격 프로시저를 호출하면 RPC는 그에 대응하는 스텁을 호출하고 원격 프로시저가 필요로 하는 매개변수를 건내준다. 그러면 스텁이 원격 서버의 포트를 찾고 매개변수를 정돈(marshall)한다.


해당 게시물은 흔히 공룡책이라고 불리는 Avraham Silberschatz, ⌜Operating System Concepts 10th edition⌟, 박민규 옮김 과 인프런 주니온님의 운영체제 공룡책 강의 및 구글링을 통해 접한 글들을 보며 공부한 내용을 작성한 것입니다.

수강 강의
운영체제 공룡책 강의, 인프런, 주니온

출처 및 참고자료

Avraham Silberschatz, ⌜Operating System Concepts 10th edition⌟, 박민규 옮김
https://ko.wikipedia.org/wiki/%EC%9B%90%EA%B2%A9_%ED%94%84%EB%A1%9C%EC%8B%9C%EC%A0%80_%ED%98%B8%EC%B6%9C
https://selfdevelope.tistory.com/521

profile
https://github.com/dong5854?tab=repositories

0개의 댓글