📀 운영체제

06. 프로세스간 통신의 실제: Chapter 3. Processes

락꿈사 2022. 5. 24. 12:21

3.7 IPC 시스템의 사례

  • IPC 시스템의 두 가지 사례
    • 공유 메모리를 위한 POSIX API 
    • 파이프

 

POSIX(Portable shared Memory for Unix) 공유 메모리

  • mamory-mapped 파일을 사용하여 구현됨
    • mamory-mapped 파일: 공유 메모리의 특정 영역을 파일과 연관시킴
  • 사용하는 코드
    • fd = shm_open(name, O_CREAT | O_RDWR, 0666);
      • name : 공유 메모리의 객체 이름 지정
      • O_CREAT | O_RDWR: 객체가 존재하지 않으면 생성되고 객체는 읽기와 쓰기가 가능한 상태로 열리는 것을 나타냄
      • 0666: 공유 메모리 객체에 파일-접근 허가권 부여
    • ftruncate(fd, 4096)
      • ftruncate: 객체의 크기를 바이트 단위로 설정
      • 4096: 객체의 크기를 4096 바이트로 설정
    • (char *) mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); 
      • mmap: 공유 메모리 객체를 포함하는 mamory-mapped 파일을 구축하고, 공유 메모리 객체에 접근할 때 사용될 mamory-mapped 파일의 포인터를 반환
  • 전체 코드
// Producer 
#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(){
    const int SIZE = 4096;
    const char *name = "OS";
    const char *message_0 = "Hello ";
    const char *message_1 = "World!";

    int fd;
    char *ptr;

    fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, SIZE);
    ptr = (char *)mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    sprintf(ptr, "%s", message_0);
    ptr += strlen(message_0);

    sprintf(ptr, "%s", message_1);
    ptr += strlen(message_1);

    return 0;
}
// Consumer
#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(){
    const int SIZE = 4096;
    const char *name = "OS";
    const char *message_0 = "Hello";
    const char *message_1 = "World!";

    int fd;
    char *ptr;

    fd = shm_open(name, O_CREAT | O_RDWR, 0666);
    ftruncate(fd, SIZE);
    ptr = (char *)mmap(0, SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);

    printf("%s", (char *)ptr);
    shm_unlink(name);

    return 0;
}

 

실행 결과

 

파이프

  • 파이프는 두 프로세스가 통신할 수 있게 하는 전달자로써 동작
  • 파이프를 구현하기 위해 고려해야 할 4가지 문제
    1. 파이프가 단방향 통신 또는 양방향 통신을 허용하는가?
    2. 양방향 통신이 허용된다면 반이중 통신인가, 전이중 통신인가?
      • 반이중 통신: 한번에 한 반향 전송만 가능
      • 전이중 통신: 동시에 양방향 데이터 전송 가능
    3. 통신하는 두 프로세스 간에 부모-자식과 같은 특정 관계가 존재해야 하는가?
    4. 파이프는 네트워크를 통해 통신이 가능한가, 아니면 동일한 기계 안에서 존재하는 두 프로세스끼리만 통신할 수 있는가?
  • 일반 파이프Ordinary pipes
    • 일반 파이프는 생산자-소비자 형태로 두 프로세스 간의 통신을 허용함
      • 생산자는 파이프의 한 종단(쓰기 종단)에 쓰고, 소비자는 다른 종단(읽기 종단)에서 읽음
      • 결과적으로 일반 파이프는 한쪽으로만 데이터를 전송할 수 있으며 오직 단방향 통신만을 가능하게 함
      • 만일 양방향 통신이 필요하다면 각각 다른 방향으로 데이터를 전송할 수 있는 두개의 파이프를 사용해야 함
    • 일반 파이프는 파이프를 생성한 프로세스 이외에는 접근할 수 없음
      • 따라서 부모 프로세스가 파이프를 생성하고 fork()로 생성한 자식 프로세스와 통신하기 위해 사용함
      • 부모가 파이프의 쓰기 종단(fd[1])에 데이터를 씀
      • 자식은 파이프의 읽기 종단(fd[0])에서 읽을 수 있음

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

#define BUFFER_SIZE 25
#define READ_END 0
#define WRTIE_END 1

int main(){
    char write_msg[BUFFER_SIZE] = "Hello Pipes!";
    char read_msg[BUFFER_SIZE];
    int fd[2];
    pid_t pid;

    if (pipe(fd) == 1){
        fprintf(stderr, "Pipe failed!");
        return 1;
    }

    pid = fork();
    
    if(pid < 0){
        fprintf(stderr, "Fork Failed");
        return 1;
    }

    if (pid > 0){
        close(fd[READ_END]);
        write(fd[WRTIE_END], write_msg, strlen(write_msg)+1);
        close(fd[WRTIE_END]);
    }
    else{
        close(fd[WRTIE_END]);
        read(fd[READ_END], read_msg, BUFFER_SIZE);
        printf("read %s", read_msg);
        close(fd[READ_END]);
    }

    return 0;
}​

실행 결과

  • 지명 파이프 Named Pipes
    • 지명 파이프는 통신이 양방향 통신이 가능하며 부모-자식 관계도 필요하지 않음
    • 여러 프로세스들이 이를 사용하여 통신할 수 있음

 

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

  • 클라이언트 서버 시스템 통신에서 사용할 수 있음
  • 두 가지 통신 전략
    1. 소켓
    2. 원격 프로시저 호출(RPCs)

 

소켓

  • 소켓은 통신의 end-point를 뜻함
    • 두 프로세스가 네트워크 상에서 통신하려면 양 프로세스마다 하나씩, 총 두개의 소켓이 필요함
    • 각 소켓은 IP와 포트번호 두 가지를 접합 해서 구별함
    • 클라이언트 프로세스가 연결을 요청하면 호스트 컴퓨터가 포트 번호를 부여함 (이 번호는 1024보다 큰 임의의 정수가 됨)
    • 위 그림에서는 IP가 148.86.5.20인 호스트 X가 포트 1625를 이용하여 IP가 161.25.19.8 이고 포트가 80인 웹서버와 통신하고 있음.
    • 이때 소켓은 148.86.5.20:1625 / 161.25.19.8:80 이 두 개로 이루어짐
  • Java는 3가지 종류의 소켓을 제공함
    1. TCP(연결 기반) 소켓
      • Socket 클래스로 구현
    2. UDP(비연결 기반) 소켓
      • DatagramSocket 클래스로 구현
    3. Multicast 소켓
      • MulicastSocket 클래스로 구현
  • 사용하는 코드
    • new ServerSocket(6013): 포트 6013을 사용하여 listen 한다는 것을 지정함
    • sock.accept(): listen 하게 됨
      • 서버는 accept() 메소드에서 클라이언트가 연결을 요청할 때 까지 봉쇄됨
      • 연결이 요청이 들어오면 accept()는 클라이언트와 통신하기 위해 사용할 수 있는 소켓을 반환함
    • new Socket("127.0.0.1", 6013) : IP 주소 127.0.0.1에 있는 포트 6013의 서버와 연결 요청
  • 전체 코드
package com.os_study;
import java.net.*;
import java.io.*;

public class DateServer {
    public static void main(String[] args) {
        try{
            ServerSocket socket = new ServerSocket(6013);
            while(true){
                Socket client = socket.accept();

                PrintWriter pout = new PrintWriter(client.getOutputStream(), true);
                pout.println(new java.util.Date().toString());
                client.close();
            }
        } catch (IOException e) {
             System.out.println(e);
        }
    }
}
package com.os_study;
import java.net.Socket;
import java.io.*;

public class DataClient {
    public static void main(String[] args) {
        try {
            Socket socket = new Socket("127.0.0.1", 6013);

            InputStream in = socket.getInputStream();
            BufferedReader bin = new BufferedReader(new InputStreamReader(in));

            String line;

            while((line = bin.readLine())!=null)
                System.out.println(line);

            socket.close();
        } catch (Exception e) {
            System.out.println(e);
        }
    }
}

실행 결과

 

원격 프로시저 호출

  • RPC: 네트워크로 연결된 두 시스템 사이의 통신을 사용하기 위하여 프로시저 호출을 추상화하는 패러다임
  • IPC 방식과 달리 RPC 통신에서 전달되는 메시지는 구조화 되어 있음
  • RPC 시스템은 클라이언트 쪽에 Stub을 제공하여 통신을 하는 데 필요한 자세한 사항을 숨겨 줌
  • 원격 프로시저 호출 과정
    • 클라이언트가 원격 프로시저를 호출
    • RPC는 그에 대응하는 stub을 호출하고 원격 프로시저가 필요로 하는 매개변수를 건네줌
    • 그러면 stub이 원격 서버의 포트를 찾고 매개변수를 mashalling
      • parameter mashalling: 클라이언트와 서버 기기의 데이터 표현 방식의 차이 문제를 해결함
      • ex) 최상위 비트를 먼저 저장하는 big-endian 방식 vs 최하위 비트를 먼저 저장하는 little-endial 방식
    • 그후 sub은 메시지 전달 기법을 사용하여 서버에게 메시지를 전송함

 


번외 - 부모와 자식이 모두 읽고 쓰기 할 수 있는 ordinary_pipe

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

#define BUFFER_SIZE 25
#define READ_END 0
#define WRTIE_END 1

int main(){
    char parent_write_msg[BUFFER_SIZE] = "Hello Pipes!";
    char parent_read_msg[BUFFER_SIZE];
    char child_write_msg[BUFFER_SIZE] = "No";
    char child_read_msg[BUFFER_SIZE];
    int fd[2];
    pid_t pid;

    if (pipe(fd) == 1){
        fprintf(stderr, "Pipe failed!");
        return 1;
    }

    pid = fork();
    
    if(pid < 0){
        fprintf(stderr, "Fork Failed");
        return 1;
    }

    if (pid > 0){
        write(fd[WRTIE_END], parent_write_msg, strlen(parent_write_msg)+1);
        
        wait(NULL);

        read(fd[READ_END], parent_read_msg , BUFFER_SIZE);
        printf("Parent read from Child: %s\n", parent_read_msg);
        close(fd[READ_END]);
        close(fd[WRTIE_END]);


    }
    else{
        read(fd[READ_END], child_read_msg, BUFFER_SIZE);
        printf("Child read from Parent: %s\n", child_read_msg);

        write(fd[WRTIE_END], child_write_msg, strlen(child_write_msg)+1);
    }

    return 0;
}