Speaking UNIX: 공유 메모리를 이용한 프로세스 간 통신(IPC)

OS/리눅스 & 유닉스 2012. 6. 1. 09:04

겉모습으로 볼 때, UNIX® 애플리케이션은 기본 호스트의 명령을 전적으로 따르는 것으로 보인다. UNIX 애플리케이션은 프로세서에 자유롭게 액세스할 준비가 되어 있고, 그 메모리는 신성 불가침의 영역이며, 연결된 장치들은 애플리케이션이 부리는 모든 변덕도 다 받아준다. 그러나 "겉모습만 보고는 알 수 없다"는 말도 있듯이, UNIX 애플리케이션이 그렇듯 뭐든 다 할 수 있을 것 같은 생각은 사람들의 뇌리에 교묘하게 각인된 착각이다. UNIX 시스템은 거의 무제한적으로 애플리케이션을 동시에 실행하면서, 무엇보다도 유한한 실제 자원을 현명하게 공유한다. 프로세서 용량은 조금씩만 분배되고, 애플리케이션 이미지는 늘 실제 메모리의 안팎에서 뒤섞이고, 장치 액세스는 요구에 따라 이루어지고 액세스 권한에 의해 단속된다. 친절하게도 쉘 프롬프트가 깜박거려도, UNIX 시스템은 왕성한 활동력을 보인다.

자주 사용하는 약어

  • API:Application programming interface
  • IPv4:Internet Protocol version 4
  • IPv6:Internet Protocol version 6
  • POSIX:Portable Operating System Interface for UNIX

복잡도가 문제가 되더라도, 대부분의 애플리케이션들은 다행히도 공유 테넌시를 잘 감지하지 못한다. 하지만, 애플리케이션들이 서로 상호 작용하도록 애플리케이션을 작성할 수 있다. 예를 들어, 한 애플리케이션이 데이터를 수집하거나 생성하는 동안, 다른 애플리케이션은 진행률을 모니터하고 정보를 동시에 분석할 수 있다. 메시지를 즉시 교환하는 채팅은 애플리케이션이 상대방에게 데이터를 전송하는 동시에 그로부터 데이터를 수신하는 협력 코드의 또 다른 예다. ssh(Secure Shell) 역시 완전히 다른 두 호스트 간에 조화를 이룰 잠재력이 있다. 각각의 예에서 코드는 독립된 다른 코드에 연결하여 정보를 스왑하는데, 이때 종종 이런 교환을 협상하고 제어하기 위한 프로토콜을 사용한다.

UNIX는 그런 프로세스 간 통신을 위한 다양한 기술을 제공한다. 어떤 기술들은 같은 호스트에서의 통신을 위한 것인 반면, 다른 기술들은 호스트 간 교환을 용이하게 하기 위한 것이다. 또한, 기술마다 속도가 다르므로 자신의 요구사항에 가장 적합한 옵션을 선택해야 한다. 타이밍과 배타성을 적용하는 조정도 늘 필수적이다. 예를 들어, 한 애플리케이션에서 데이터를 생산하고 다른 애플리케이션이 이 데이터를 사용하는 경우, 데이터 소비자는 일시 중지하고 공유 풀을 소진할 때마다 데이터 생산자를 기다려야 한다. 재귀적으로, 생산자는 소비자가 풀을 충분히 빨리 고갈시킬 수 없는 경우 느려지거나 속도를 잃을 수 있다.

아래의 표 1은 전형적인 UNIX 시스템에서 사용 가능한 프로세스 간 통신(IPC)의 형태를 요약한 것이다.


표 1. UNIX에서의 프로세스 간 통신
이름설명범위용도
파일데이터가 일반적인 UNIX 파일에 기록되고 그 파일에서 읽혀진다. 수에 관계없이 프로세스들끼리 상호 운용 가능하다.로컬큰 데이터 세트 공유
파이프전용 파일 디스크립터를 사용하여 두 프로세스 간에 데이터가 전송된다. 상위 프로세스와 하위 프로세스 간에만 통신이 이루어진다.로컬생산자 및 소비자와 같은 간단한 데이터 공유
이름 지정된 파이프전용 파일 디스크립터를 통해 프로세스 간에 데이터가 교환된다. 같은 호스트 상의 두 상대 프로세스 사이에서 통신이 이루어질 수 있다.로컬MySQL 서버와 이 서버의 명령행 쿼리 유틸리티로 보여줄 수 있는 것처럼, 생산자와 소비자 또는 명령과 제어
신호인터럽트가 발생하여 애플리케이션에 특정 조건을 경고한다.로컬신호로 데이터를 전송할 수 없으므로, 주로 프로세스 관리에 유용함
공유 메모리메모리의 공통 세그먼트에서 읽고 써서 정보를 공유한다.로컬특히 보안이 필수적이 경우 종류를 막론한 모든 협력 작업
소켓특수한 설정 후, 공통 입/출력(I/O) 연산을 이용해 데이터가 전송된다.로컬 또는 원격FTP, ssh 및 Apache 웹 서버와 같은 네트워크 서비스

위에서 언급한 바와 같이, 각각의 기술은 특정 요구에 적합하다. 여러 프로세스 간의 조정이 대체로 똑같이 복잡하다고 가정하면, 각각의 접근 방식에는 저마다 다음과 같은 장단점이 있다.

  • 친숙한 파일 작업을 사용하므로, 공통 UNIX 파일을 통한 데이터 공유가 간단한 방법이다. 하지만, 파일 시스템을 통해 데이터를 공유하는 것은 본래 느린데, 그 이유는 데이터 입력 및 출력 작업이 메모리의 편의성을 따라갈 수 없기 때문이다. 더욱이, 파일만 통해 읽기 및 쓰기를 조정하기는 어렵다. 궁극적으로, 루트 및 다른 권한을 가진 사용자가 파일에 저장된 정보에 액세스할 수 있기 때문에 파일에 중요한 데이터를 저장하는 것은 안전하지 않다. 어떤 점에서는, 읽기 전용 또는 쓰기 전용으로 볼 때 파일을 사용하는 것이 최선이다.
  • 파이프와 이름 지정된 파이프 역시 간단한 메커니즘이다. 둘 모두 연결의 각 끝점에서 두 개의 표준 파일 디스크립터를 사용하는데, 하나는 읽기 작업, 다른 하나는 쓰기 작업 전용이다. 그러나 파이프는 임의의 두 프로세스 간이 아니라, 상위 프로세스와 하위 프로세스 간에만 사용할 수 있다. 이름 지정된 파이프는 후자의 단점을 해결하므로, 같은 시스템에서의 데이터 교환에 적합하다. 하지만, 파이프나 이름 지정된 파이프 모두 각각 선입선출(FIFO) 장치로 작동하므로 임의 액세스를 제공하지 않는다.
  • 신호는 프로세스 간에 데이터를 전송할 수 없다. 일반적으로, 신호는 프로세스 간에 예외적 조건을 통신하는 데만 사용되어야 한다.
  • 공유 메모리는 메모리를 사용하여 빠른 임의 액세스를 허용하기 때문에 더 큰 메모리 콜렉션에 적합하다. 공유 메모리는 구현하기가 조금 더 복잡하지만, 그 밖의 점에서는 여러 프로세스 간의 호스트 내부 협업에서 진가를 발휘한다.
  • 소켓은 이름 지정된 파이프와 흡사한 기능을 수행하지만 여러 호스트를 그 대상으로 할 수 있다. 로컬 소켓(UNIX 소켓이라고도 함)은 로컬 (동일 호스트) 연결로 한정된다. 각각 IPv4 및 IPv6를 사용하는 Inet  Inet6 소켓은 원격 연결을 허용한다(그리고 로컬 시스템의 인터넷 주소 지정을 통해 로컬 연결을 허용함). 분산 처리 또는 웹 브라우저와 같은 모든 네트워킹 애플리케이션에는 분명히 소켓을 선택하는 것이 최선이다. 이름 지정된 파이프보다 코딩이 약간 더 복잡하지만, 어느 UNIX 네트워크 프로그래밍 서적에서든 패턴이 잘 확립되어 문서화되어 있다.

호스트  애플리케이션은 무시하고, 같은 호스트에서 공유 메모리의 프로세스 간 통신을 살펴보자.

공유 메모리의 작동 방식

그 이름이 암시하는 바와 같이, 공유 메모리는 메모리의 세그먼트가 두 개 이상의 프로세스에 액세스할 수 있게 만든다. 특수한 시스템 호출이나 UNIX 커널에 대한 요청으로 메모리를 할당하고 공간을 늘리며 사용 권한을 설정한다. 공통적인 읽기 및 쓰기 작업으로 해당 영역에서 데이터를 가져오거나 데이터를 기록한다.

공유 메모리는 어떤 프로세스의 자체 메모리에서 빼온 것이 아니며, 항상 비밀로 유지된다. 그 대신, 공유 메모리는 시스템의 사용 가능한 메모리 풀에서 할당되고 액세스를 원하는 각 프로세스에 의해 부가된다. 부가는 맵핑으로 불리고, 여기서 메모리의 공유 세그먼트가 각 프로세스의 자체 주소 공간에서 로컬 주소로 지정된다. 그림 1, 그림 2, 그림 3  그림 4에 프로세스가 설명되어 있다.

  1. 그림 1에 나타낸 것처럼, A와 B라는 두 프로세스가 같은 시스템에서 작동하고 있고 특히 공유 메모리를 통해 정보를 조정하고 공유하도록 코딩되었다고 가정하자. 그림에서 애플리케이션들이 동일할 필요는 없다는 점을 강조하기 위해 A와 B의 크기는 불균형하다. 

    그림 1. 한 호스트에서 작동하면서 서로 다른 코드를 실행하는 두 프로세스 
    한 호스트에서 작동하면서 서로 다른 코드를 실행하는 두 프로세스 

  2. 그림 2에서 프로세스 A는 공유 메모리의 세그먼트를 요청한다. 프로세스 A는 메모리 세그먼트를 초기화하여 사용할 준비를 한다. 또한, 이 프로세스는 다른 프로세스들이 찾을 수 있도록 세그먼트의 이름을 지정한다. 일반적으로, 세그먼트 이름은 동적으로 지정되지 않는다. 그 대신, 세그먼트 이름은 헤더 파일의 상수와 같이 잘 알려져 있고 다른 코드에서 쉽게 참조된다. 

    그림 2. 한 프로세스에서 공유 메모리 세그먼트 요청 
    한 프로세스에서 공유 메모리 세그먼트 요청 

  3. 프로세스 A는 공유 메모리 세그먼트를 자체 주소 공간으로 합병하거나 맵핑한다. 프로세스 B는 이름 지정된 파이프를 통해 세그먼트를 찾고, 이 세그먼트를 주소 공간으로 맵핑도 한다. 이것은 그림 3에도 표시되어 있다. 두 프로세스 모두 공유 메모리 세그먼트의 크기만큼 확대된다. 

    그림 3. 두 프로세스 모두 공유 메모리 세그먼트를 합병 또는 맵핑 
    두 프로세스 모두 공유 메모리 세그먼트를 합병 또는 맵핑 

  4. 마지막으로, 그림 4에서는 프로세스 A와 B가 공유 메모리 세그먼트에서 자유롭게 읽고 쓸 수 있다. 공유 메모리는 로컬 프로세스 메모리와 동일하게 취급된다. read() write()는 정상적으로 작동한다. 

    그림 4. 이제 둘 이상의 프로세스가 공통 메모리를 통해 데이터를 공유할 수 있음 
    이제 둘 이상의 프로세스가 공통 메모리를 통해 데이터를 공유할 수 있음 

위 그림들에 표시된 작업 중 많은 부분이 UNIX 공유 메모리 API에 캡처된다. 사실, 공유 메모리 API에는 POSIX API와 더 오래되었지만 그에 못지않게 효과적인 System V API라는 두 가지 변형이 있다. POSIX는 UNIX 및 Linux®와 이들로부터 파생된 시스템에서 주로 볼 수 있는 승인 표준이므로, 그 버전을 사용하자. 그 밖에도, POSIX API는 읽기 및 쓰기를 위해 간단한 파일 디스크립터를 사용하므로, 훨씬 더 친숙하게 보일 것이다.

POSIX는 공유 메모리 세그먼트를 작성, 맵핑, 동기화 및 실행 취소하기 위해 다섯 개의 시작점을 제공한다.

  • shm_open(): 공유 메모리 영역을 작성하거나 기존의 이름 지정된 영역에 연결한다. 이 시스템 호출은 파일 디스크립터를 리턴한다.
  • shm_unlink(): (shm_open()에서 리턴되는) 파일 디스크립터가 지정되어 있을 때 공유 메모리 영역을 삭제한다. UNIX의 어떤 파일과도 거의 마찬가지로, 해당 영역에 액세스하는 모든 프로세스가 종료될 때까지는 영역이 실제로 제거되지 않는다. 하지만, (일반적으로 원래 프로세스에서) shm_unlink()를 호출하면 다른 프로세스는 영역에 액세스할 수 없다.
  • mmap(): 공유 메모리 영역을 프로세스의 메모리로 맵핑한다. 이 시스템 호출은 shm_open()에서 파일 디스크립터를 요구하고 포인터를 메모리로 리턴한다. (어떤 경우에는 파일 디스크립터를 일반 파일로 맵핑하거나 다른 장치를 메모리로 맵핑할 수도 있다. 그런 옵션에 대한 논의는 본 기사의 범위를 벗어나는 주제이며, 구체적인 내용은 사용 중인 운영 체제에 대한 mmap() 문서를 참조한다.)
  • munmap(): mmap()의 반대이다.
  • msync(): 공유 메모리 세그먼트를 파일 시스템과 동기화하는 데 사용되며, 파일을 메모리로 맵핑할 때 유용한 기술이다.

공유 메모리에 대한 패턴은 shm_open()으로 세그먼트를 작성하고, write() 또는 ftruncate()로 세그먼트의 크기를 조정하고,mmap()을 이용해 세그먼트를 프로세스 메모리로 맵핑하고, 하나 이상의 추가 참가자와 함께 필요한 작업을 수행하는 것이다. 완료하기 위해, 원래 프로세스는 munmap()  shm_unlink()를 호출한 다음 종료한다.

샘플 애플리케이션

아래의 목록 1은 작은 공유 메모리 예제를 나타낸 것이다. (이 코드는 2007년 3월에 Prentice Hall Professional에서 출간한 John Fusco의 저서 The Linux Programmer's Toolbox(ISBN 0132198576)에서 유래된 것으로, 출판사의 허가를 받아 사용했다.) 이 코드는 공유 메모리 세그먼트를 통해 통신하는 상위 및 하위 프로세스를 구현한다.


목록 1. 공유 메모리 예제
	
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/file.h>
#include <sys/mman.h>
#include <sys/wait.h>

void error_and_die(const char *msg) {
  perror(msg);
  exit(EXIT_FAILURE);
}

int main(int argc, char *argv[]) {
  int r;

  const char *memname = "sample";
  const size_t region_size = sysconf(_SC_PAGE_SIZE);

  int fd = shm_open(memname, O_CREAT | O_TRUNC | O_RDWR, 0666);
  if (fd == -1)
    error_and_die("shm_open");

  r = ftruncate(fd, region_size);
  if (r != 0)
    error_and_die("ftruncate");

  void *ptr = mmap(0, region_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
  if (ptr == MAP_FAILED)
    error_and_die("mmap");
  close(fd);

  pid_t pid = fork();

  if (pid == 0) {
    u_long *d = (u_long *) ptr;
    *d = 0xdbeebee;
    exit(0);
  }
  else {
    int status;
    waitpid(pid, &status, 0);
    printf("child wrote %#lx\n", *(u_long *) ptr);
  }

  r = munmap(ptr, region_size);
  if (r != 0)
    error_and_die("munmap");

  r = shm_unlink(memname);
  if (r != 0)
    error_and_die("shm_unlink");

      return 0;
}

다음은 코드에서 몇 가지 강조할 사항이다.

  • shm_open()에 대한 호출이 친숙해 보일 것이다. 이 호출은 세그먼트와 사용 권한을 초기화하는 방법을 포함하여, open() 함수와 매우 유사하다. 여기서, 세그먼트는 누구나 읽고 쓸 수 있다. 사용하지 않는 다음 파일 디스크립터는 호출에 성공하는 경우 리턴되고, 그렇지 않으면 -1이 리턴되고 그에 따라 errno가 설정된다.
  • ftruncate()는 파일의 크기를 이전에 시스템의 표준 페이지 크기로 설정된 region_size 바이트로 조정한다. sysconf()가 libc의 일부로 제공된다. (쉘 유틸리티 getconf를 사용하여 시스템의 구성 설정을 탐색할 수도 있다.)
  • mmap()은 공유 메모리 세그먼트를 합병하고 세그먼트에서 직접 바이트를 읽고 쓰기에 적합한 포인터를 리턴한다. PROT_READPROT_WRITE는 각각 세그먼트를 읽고 쓸 수 있는 페이지를 표시한다. MAP_SHARED는 메모리 세그먼트의 모든 변경 내용이 모든 협력 프로세스에 "공개"되어야 함을 지정한다.
  • fork()를 사용해보았다면 코드의 계산 파트가 친숙하게 느껴질 것이다. fork 후에 상위 및 하위 프로세스에는 공개된 모든 파일 디스크립터와 데이터 값의 사본이 있으므로, 둘 모두에 대해 포인터가 적용된다. 하지만, pid는 다르다. 하위 프로세스에서 0을 받고, 상위 프로세스에서는 하위 프로세스의 프로세스 ID를 받고, 변수 값에 따라 if/then/else 중 어떤 분기를 택할지 결정된다. 하위 프로세스는 포인터에 몇 바이트의 데이터를 쓴 다음 종료한다. 상위 프로세스는 하위 프로세스의 종료를 기다린 다음, 쓰인 데이터를 읽는다.
  • 하지만, 상위 프로세스가 종료할 수 있으려면 우선 공유 메모리를 해제해야 한다. munmap() shm_unlink()는 트릭을 수행한다.

이 예제는 매우 기초적인 것이다. 실제 애플리케이션에서는 세마포어나 다른 기술을 사용하여 공유 세그먼트에 대한 읽기 및 쓰기를 제어한다. 그런 제어는 일반적으로 애플리케이션마다 다른데, UNIX 유형이 오픈 소스가 아닌 경우 BSD(Berkeley Software Distribution) 및 Linux 소스에서 다양한 예제를 찾을 수 있다.

모든 것이 하나로

UNIX는 겉보기에는 많은 애플리케이션을 동시에 실행하기 때문에, 모니터링, 데이터 수집, 협력 및 분산 컴퓨팅, 클라이언트-서버 애플리케이션을 위한 이상적인 플랫폼이다. 공유 메모리는 사용 가능한 프로세스 간 통신 옵션 중에서 가장 빠르고 꽤 유연하다. 파일을 메모리로 맵핑할 수도 있으므로, 데이터 액세스를 가속화하기에 이상적인 솔루션이다.


참고자료

교육

  • The Linux Programmer's Toolbox: Linux 프로그래머 도구 상자를 찾아보자. 

  • Shared memory: 공유 메모리에 대한 입문서를 읽어보고 사용 가능한 다양한 구현 방법에 대해 배워보자. 

  • Interprocess communications: 공유 메모리와 다른 형태의 프로세스 간 통신(IPC)이 구현되는 방법에 대해 자세히 알아보자. 

  • Speaking UNIX: 본 시리즈의 다른 파트를 살펴본다. 

  • AIX와 UNIX developerWorks 영역: AIX와 UNIX 영역에서는 AIX 시스템 관리와 UNIX 스킬 확장의 모든 측면과 관련된 풍부한 정보를 제공한다.

  • AIX 및 UNIX 입문: AIX 및 UNIX 입문 페이지를 방문하면 자세한 내용을 확인할 수 있다. 

  • 기술 서점: 다양한 기술 주제와 관련된 서적을 살펴볼 수 있다. 

토론


'OS > 리눅스 & 유닉스' 카테고리의 다른 글

리눅스 명령어 모음  (0) 2012.07.05
POSIX Semaphore & System V Semaphore  (0) 2012.06.01
ibm aix polling  (0) 2012.05.31
공유 메모리 (shared memory)  (0) 2012.05.29
poll을 이용한 채팅서버  (0) 2012.05.24
: