본문 바로가기
3학년 학사/운영체제

[운영체제] #7. InterProcess Communication (IPC)

by whiteTommy 2024. 11. 26.
반응형
더보기

목차

  • 프로세스간 통신이란?
  • IPC 의 두가지 모델
    • Shared Memory
    • Message Passing
  • IPC 의 구체적인 방법론
    • Pipe
    • Signal
    • Shared Memory
    • Message Queue
    • Socket

 

프로세스간 통신이란?

  • InterProcess Communication (IPC)
    • 서로 다른 프로세스 간에 데이터를 교환하기 위한 메커니즘
    • Kernel 이 IPC 를 위한 도구를 제공하며, System Call 을 통해 프로세스들 간 협력을 가능하게 한다.
  • 필요성
    • 협력하는 프로세스 모델 (Cooperating Process Model) 을 구현하기 위해 IPC 가 반드시 필요함
    • 실행 중인 프로세스가 서로 영향을 주고받으며 동작할 수 있음

 

IPC 의 두가지 모델

1. Shared Memory (공유 메모리)

  • 개념
    • 프로세스가 동일한 메모리 공간을 공유하여 데이터 교환 (예: 데이터베이스)
    • 공유한 Memory 영역에 읽기/쓰기를 통해서 통신 수행
    • 빠른 데이터 접근 및 처리 가능
    • 응용 프로그램 레벨에서 통신 기능 제공
      • 한 번 메모리 영역이 설정되면, 이후 Kernel 의 간섭 없이 직접 통신 가능
  • 단점
    • 프로세스 간 동기화를 위한 추가적인 메커니즘 필요 (예: 세마포어, Locking)
  • 기본 동작
    • 주소 공간 일부를 공유 메모리로 설정하여, 프로세스가 바로 접근해 데이터 Read/Write 가능.
    • 커널을 거치지 않아 성능이 우수하지만 동기화 문제 해결 필요.

 

  • Process A와 Process B가 하나의 공유 메모리 공간을 통해 데이터를 주고받음.
  • 예: 한 프로세스가 쓰면(Write), 다른 프로세스가 읽는(Read) 형태.

 

2. Message Passing (메시지 교환)

  • 개념
    • 프로세스 간 메모리 공유 없이 데이터를 주고 받음
    • Kernel (중재자) 을 통한 메시지 통신 기능을 제공
    • Kernel 이 동기화 제공
      • 커널이 동기화를 제공.
      • 송신(Send)과 수신(Receive)은 커널에서 동기화를 제공하므로 프로세스에서 추가적인 고려가 필요 없음.
    • 고정 길이 메시지, 가변 길이 메시지를 송/수신자 끼리 주고 받음 (예: 클라이언트-서버 통신)
      • Pipe, Message Queue, Socket
  • 기본 동작
    • 커널을 경유하여 메시지를 전송.
    • 커널에서 데이터를 버퍼링하고, 문맥 전환과 연관된 오버헤드가 있음.

  • Process A가 메시지를 보낼 때(Send), 커널이 이를 전달하고 Process B가 메시지를 받는(Receive) 방식.

 

방법론

1. Pipe

 

  • 하나의 프로세스가 다른 프로세스로 데이터를 직접 전달하는 방법.
  • 데이터는 한 방향으로만 이동 가능(Half Duplex). 양방향 통신을 위해서는 두 개의 Pipe가 필요.
  • 한 번에 1:1 의사소통만 가능.
  • 순차적으로 데이터를 보냄.
  • 용량 제한이 있어 Pipe가 꽉 차면 더 이상 데이터를 보낼 수 없음.

 

  • 예제
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#define MAXLINE 4096
 
int main(void){
 int n, fd[2];
 pid_t pid;
 char line[MAXLINE];
 
 if (pipe(fd) < 0) {
     perror("pipe error");
     exit(-1);
 }
 if ( (pid = fork()) < 0) {
     perror("fork error");
     exit(-1);
 } 
 else if (pid > 0) {/* parent */
     close(fd[0]);
     write(fd[1], "hello, world.\n", 14);
 } 
 else { /* child */
     close(fd[1]);
     n = read(fd[0], line, MAXLINE);
     write(STDOUT_FILENO, line, n);
     waitpid(pid, NULL, NULL);
 }
 exit(0);
}

 

 

  • 동작
    • pipe(fd)는 두 개의 파일 디스크립터를 포함한 배열 fd를 생성한다.
      • fd[0]: 읽기 전용 디스크립터.
      • fd[1]: 쓰기 전용 디스크립터.
    • 부모 프로세스 ( fork() > 0 )
      1. 읽기 디스크립터(fd[0]) 닫기: 부모는 데이터를 쓰기만 하므로 읽기 디스크립터를 닫는다.
      2. 쓰기 디스크립터(fd[1])로 데이터 쓰기: "hello, world.\n" 문자열을 Pipe에 쓴다.
    • 자식 프로세스 ( fork() == 0 )
      1. 쓰기 디스크립터(fd[1]) 닫기: 자식은 데이터를 읽기만 하므로 쓰기 디스크립터를 닫는다.
      2. 읽기 디스크립터(fd[0])에서 데이터 읽기: pipe에서 데이터를 읽고, 이를 line 버퍼에 저장한다.
      3. 표준 출력(STDOUT)으로 데이터 출력: 읽은 데이터를 콘솔에 출력한다.

 

2. Signal

 

  • 특정 프로세스에게 Kernel을 통해 이벤트를 전달하는 방법.
  • 구조 및 특징:
    • 송신 프로세스가 신호(Signal)를 발생시키면, 수신 프로세스가 이벤트를 처리. (수신 Process 의 상태와 무관)
      ex) Process A가 Signal을 발생시키면, Process B가 Signal 처리 루틴을 실행.
    • 수신 프로세스는 신호 처리 방법을 지정할 수 있음(예: 단순히 신호를 무시하거나, 핸들러를 통해 처리).
    • 비동기적 동작:
      • 송신 프로세스가 신호를 보내면, 수신 프로세스가 스케줄링된 이후에 신호를 처리.
  • 예제
#include <stdio.h>
#include <stdbool.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

 void SignalHandlerChild(int signo){
   printf("signal handler\n");
   fflush(stdout);
 }
 
 int main(void){
   pid_t pid;
   struct sigaction act_child;
   act_child.sa_handler = SignalHandlerChild;
   act_child.sa_flags = 0;
   sigemptyset( &act_child.sa_mask );
   sigaction( SIGUSR1, &act_child, 0 );
 
   switch ( pid = fork() ){
     case -1:
       perror("fork failed ");
       exit(-1);
   
     case 0:
       sigpause(SIGUSR1);
       return 0;
   
     default:
       sleep(3);
       kill(pid, SIGUSR1);
       waitpid(pid, 0, 0);
   }
 
   return 0;
 }

 

 

  • 동작
    • SignalHandlerChild : 자식 프로세스에서 Signal 을 받으면 실행되는 핸들러 함수
      • 시그널을 받았을 때 "Signal Handler" 라는 메시지를 출력하고 출력 버퍼를 플러시하여 즉시 출력이 보이게함
    • 자식 프로세스가 SIGUSR1 시그널을 받을 때 실행될 핸들러를 등록한다.
      • sigaction() : SIGUSR1 시그널 발생 시 SignalHandlerChild 함수가 호출된다.
      • sigemptyset() : 시그널 핸들러가 실행될 때 블록될 시그널 집합을 초기화한다.
    • 자식 프로세스 (case 0)
      • sigpause (SIGUSR1) : SIGUSR1 시그널을 받을 때까지 대기 상태에 들어간다.
      • 시그널을 받으면 등록된 시그널 핸들러가 호출된다.
    • 부모 프로세스 (default)
      • 3초 동안 대기하여 자식 프로세스가 sigpause() 상태로 진입할 시간을 준다
      • kill(pid, SIGUSR1) : 부모 프로세스가 자식 프로세스에게 SIGUSR1 시그널을 보낸다.
      • waitpid(pid, 0, 0) : 자식 프로세스의 종료를 기다린다.

 

3. Shared Memory

 

  • 두 개 이상의 프로세스가 하나의 메모리 영역을 공유하여 통신.
  • 특징:
    • 메모리에 직접 접근하므로 빠르고 자유롭게 통신 가능.
    • 하지만 두 프로세스가 동시에 메모리를 변경하지 않도록 동기화가 필요.
  • 구조:
    • Shared Memory를 통해 데이터를 교환하며, 각각의 프로세스가 동일한 메모리 영역에 접근 가능.
  • 예제
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/wait.h>
#define SHM_SIZE  1024

void ChildRun(int shmid);
int main(int argc, char **argv) {
 int         shmId, pid;
 char        *ptrShm;

 if ( (shmId = shmget(IPC_PRIVATE, SHM_SIZE, SHM_R | SHM_W)) < 0 ) {
   perror("shmget error");
   exit(-1);
 }
 
 if ( (ptrShm = shmat(shmId, 0, 0)) == (void *)-1 ) {
   perror("shmat error");
   exit(-1);
 }
 
 ptrShm[0] = 11;
 ptrShm[1] = 22;
 printf("Parent : %d, %d\n", ptrShm[0], ptrShm[1]);
 
 switch ( pid = fork() ){
   case 0:
     ChildRun(shmId);
     return 0;
     
   case -1:
     perror("fork error");
     exit(-1);
     
   default:
     break;
 }
 
 waitpid(pid, NULL, 0);
 printf("Parent : %d, %d\n", ptrShm[0], ptrShm[1]);
 if ( shmdt(ptrShm) < 0 ) {
   perror("shmctl error");
   exit(-1);
 }
 
 
 if ( shmctl(shmId, IPC_RMID, 0) < 0 ) {
   perror("shmctl error");
   exit(-1);
 }
 
 return 0;
}
 
void ChildRun(int shmid){
 int         shmId;
 char        *ptrShm;

 shmId = shmid;
 if ( (ptrShm = shmat(shmId, 0, 0)) == (void *)-1 ) {
   perror("shmat error");
   exit(-1);
 }
 
 printf("Child  : %d, %d\n", ptrShm[0], ptrShm[1]);
 printf("Child  : Modify value.\n");
 ptrShm[0] = 33;
 ptrShm[1] = 44;
 
 if ( shmdt(ptrShm) < 0 ) {
   perror("shmctl error");
   exit(-1);
 }
}

 

 

  •  동작
    • 공유 메모리 생성 
      • shmget() : 공유 메모리 생성
      • IPC_PRIVATE : 특정 프로세스만 공유할 수 있는 메모리 세그멘트 생성
      • SHM_R | SHM_W : 읽기, 쓰기 권한 부여
    • 공유 메모리 연결
      • shmat(shmId, 0, 0) : 공유 메모리 세그먼트를 프로세스의 주소 공간에 연결 (0: 자동 선택)
      • 반환된 포인터 ptrShm 을 통해 공유 메모리에 접근
    • 부모 프로세스 초기화 및 출력
      • 공유 메모리에 초기 값(11, 22)을 설정.
      • 설정한 값을 출력.
    • fork() 를 이용한 프로세스 분기
      • 자식 프로세스 (case 0) : ChildRun() 함수를 호출하여 공유 메모리 데이터를 수정
      • 부모 프로세스 (default) : 자식 프로세스의 종료를 기다림
    • 자식 프로세스 : 공유 메모리 접근 및 수정
      • 자식 프로세스는 공유 메모리를 다시 연결하여 값을 읽고 수정
      • 수정된 값:
        • ptrShm[0] = 33
        • ptrShm[1] = 44
      • 공유 메모리 연결 해제 (shmdt)
    • 부모 프로세스 : 자식 종료 대기 및 데이터 확인
      • 부모 프로세스는 자식 프로세스의 종료를 대기.
      • 자식 프로세스가 수정한 데이터를 공유 메모리에서 읽어 출력.
    • 공유 메모리 해제
      • shmdt() : 공유 메모리 연결을 해제.
      • shmctl() : 공유 메모리를 시스템에서 제거(IPC_RMID).

 

4. Message Queue

  • 고정된 크기를 가진 메시지의 연결 리스트(Linked List)를 이용하여 통신하는 방법.
  • 특징:
    • 메시지 단위로 통신: 메시지의 형식에 따라 통신하려는 프로세스 간의 약속이 필요.
    • 공유 가능 : 여러 프로세스가 동시에 메시지 큐를 사용할 수 있지만, 동기화가 필요.
    • 사용자 공간에서 메시지를 관리하며, 커널이 메시지 큐를 지원.
      ex) Process A가 메시지를 보내면, 다른 프로세스(예: Process B, Process C)가 메시지 큐에서 이를 가져가 읽음.
  • 예제
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/wait.h>

typedef struct _MSG {
 long type;
 char message[256];
} MSG, *PMSG, **PPMSG;

#define MSG_ sizeof(MSG)

int main(void){
 pid_t pid;
 key_t msg_id;
 MSG msg;
 
 if ( -1 == (msg_id = msgget(IPC_PRIVATE, 0660 | IPC_CREAT)) ) {
   perror( "msgget failed" );
   exit(1);
 }
 
 switch ( pid = fork() ) {
 
 case -1:
   perror( "fork failed " );
   exit(-1);
   
 case 0:
   msg.type = 1;
   strcpy( msg.message, "Hello, world.");
   msgsnd( msg_id, &msg, MSG_-sizeof(long), 0 );
   return 0;
   
 default:
   waitpid(pid, 0, 0);
   memset( &msg, 0, MSG_ );
   msgrcv( msg_id, &msg, MSG_-sizeof(long), 1, 0 );
   printf("PARENT - message from child : %s\n", msg.message);
   fflush(stdout);
 }
 
 if ( -1 == msgctl(msg_id, IPC_RMID, 0) ) {
   perror( "msgctl failed" );
   exit(-1);
 }
 
 return 0;
}

 

  • 동작
    • 자식 프로세스 : 메시지 전송
    • 부모 프로세스 : 메시지 수신차
    • 메시지 큐 생성
      • mesget()
        • 메시지 큐를 생성하거나 기존 큐를 가져온다
        • 0660 : 생성된 큐의 접근 권한을 설정(소유자/그룹 읽기/쓰기 가능)
        • IPC_CREAT : 큐가 존재하지 않으면 새로 생성
    • 프로세스 분기
      • 자식 프로세스 : 메시지 전송
      • 부모 프로세스 : 메시지 수신
    • 메시지 큐 삭제

 

5. Socket

 

  • 네트워크 통신의 끝점(End-Point)으로, 프로세스 간 데이터를 주고받기 위한 인터페이스.
  • 다른 IPC 와 달리 Process 의 위치에 Independent
    • 로컬(Local) 및 원격(Remote) 환경에서 모두 사용할 수 있음.
      • Local 의 경우 Port 번호로만 식별
      • Remote 의 경우 IP 주소 + Port 번호 조합으로 식별
  • Port 를 이용하여 통신하는데 사용됨 
    • 운영체제가 제공하는 추상화된 개념
    • 특정 프로세스의 소켓을 식별하기 위해 사용
    • IP 주소와 결합하여 통신 상대를 고유하게 식별
    • Port 번호를 이용하여 통신하려는 상태 Process 의 Socket 를 찾아감
  • 연결의 Semantics 를 정할 수 있음 (예: TCP / UDP)
  • 특성
    • Port 를 사용하기 떄문에 Machine Boundary 와 관계 없음
      ex) Port 로 여러 Web Browser 생성
      • Remote Machine 은 Local Machine 의 Port 만 보임 (Socket 은 보이지 않음)

 

 

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

void ErrorHandling(char* message);

int main(void){
 int port = 9999, acceptState, hServSock, hClntSock;
 struct sockaddr_in servAddr, clntAddr;
 char buffer[20], message[20] = "message from server";
 socklen_t szClntAddr;
 pid_t pid;
 
 switch (pid = fork()) {
 case -1:
   perror("fork failed ");
   exit (-1);
   
 case 0:
   sleep (1);
   hClntSock = socket(AF_INET, SOCK_STREAM, 0);
   clntAddr.sin_family = AF_INET;
   clntAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
   clntAddr.sin_port = htons(port);
   szClntAddr = sizeof(clntAddr);
   
   if (connect(hClntSock, (struct sockaddr *)&clntAddr, szClntAddr) < 0){
     perror("<Child> Connect error: ");
     break;
   }
   
   printf("<Child> connect port: %d!\n", port);
   recv(hClntSock, buffer, 20, 0);
   printf ("<Child> %s\n", buffer);
   close(hClntSock);
   break;
   
 default:
   printf("<Parent> binding port: %d...\n", port);
   hServSock = socket(PF_INET, SOCK_STREAM, 0);
   
   if (hServSock == -1)
     ErrorHandling("<Parent> socket() error");
     
   memset(&servAddr, 0x00, sizeof(servAddr));
   servAddr.sin_family = AF_INET;
   servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
   servAddr.sin_port = htons(port);
     
   if (bind(hServSock, (struct sockaddr*)&servAddr, sizeof(servAddr)) < 0 )   
     ErrorHandling("<Parent> bind() error");
     
   printf("<Parent> listening port: %d...\n", port);
   
   if (listen(hServSock, 1) < 0)
     ErrorHandling("<Parent> listen() error");
     
   printf("<Parent> accepting...\n");
   while (1) {
     szClntAddr = sizeof(clntAddr);
     hClntSock = accept(hServSock, (struct
     sockaddr*)&clntAddr, &szClntAddr);
     
     if (hClntSock <0) continue;
       printf("<Parent> accept port: %d!\n", port);
       acceptState = 1; //connected;
       
     if (acceptState == 1) break;
   }
   send(hClntSock, message, strlen(message)+1, 0);
   sleep (1);
   close(hServSock);
  }
 return 0;
 }
 
void ErrorHandling(char* message){
 fputs(message, stderr);
 fputc('\n', stderr);
 exit(1);
}

 

  • 동작
    • 공통 준비 작업
      • 포트 번호 : 9999
      • 소켓 변수 
        • hServSock : 서버 소켓 (부모 프로세스)
        • hClntSock : 클라이언트 소켓 (자식 프로세스)
      • 버퍼
        • buffer : 서버에서 클라이언트로 전송된 메시지를 저장할 공간
        • message : 서버에서 클라이언트로 보낼 메시지
    • fork() 로 부모-자식 프로세스 생성
      • 자식 프로세스 : 클라이언트 동작
        • 소켓 생성 (IPv4 를 사용하는 TCP 소켓)
        • 서버 연결 요청 (IP: 127.0.0.1, Port : 9999)
        • 메시지 수신 : 서버에서 전송한 메시지를 읽어와서 출력
      • 부모 프로세스 : 서버 동작
        • 소켓 생성 (IPv4 기반의 TCP 소켓)
        • 포트 바인딩 : port (9999) 에 소켓을 바인딩하여 클라이언트 요청을 받을 준비
        • 연결 대기 : 클라이언트 연결 요청을 대기
        • 클라이언트 연결 수락
        • 메시지 전송 : 클라이언트로 "message from Server" 메시지 전송
반응형