본문 바로가기
오픈소스 읽기 (OLD)/모던 IPC 시스템 - D-Bus

0. 전통적인 리눅스/Unix IPC 방법들

by 커널패닉 2021. 3. 22.
반응형

D-Bus는 리눅스 상에서 프로세스간 통신(IPC)를 지원하는 메시지 버스 서비스이다. D-Bus를 통해서 리눅스 어플리케이션은 다른 어플리케이션들에 Broadcast를 날릴 수 있고, 또한 상대방의 PID를 알지 못하더라도 1:1 통신을 할 수 있다. 또한 D-Bus 통신은 통일성 있게 규격화된 xml 형태로 이뤄진다. 이러한 특징들을 통해서 D-Bus는 전통적인 리눅스 IPC의 한계를 극복하고, 어플리케이션 수준에서 가장 많이 사용되는 IPC 통신 방법이 되었다.

D-Bus에 대해 본격적으로 살펴보기 앞서, 이번 포스트에서는 리눅스에서 전통적으로 사용되던 IPC들의 특징 및 장단점을 간단히 살펴보고, 전통적인 IPC 방법들이 공통적으로 가지고 있는 문제점에 대해서 살펴보도록 하겠다.

 

0.1 Unix/리눅스 IPC 방법들

IPC 방법 방향 방식 특징
PIPE 단방향 (단, 양방향 파이프도 있음) 메시지 공통 부모를 공유하거나, 부모 자식간에만 사용 가능
쉘에서 stdout과 stdin을 연경하는데 사용하는 IPC
FIFO 양방향 (단, 일반적으로 단방향으로 사용) 메시지 파일을 기반으로 하는 IPC
메시지큐 양방향 메시지 큐방식의 메시지 리스트
타입에 따라서 나중에 들어온 메시지도 먼저 수신 가능
Unix Domain Socket 양방향 메시지 소켓 방식의 IPC
공유 메모리 양방향 데이터 공유 대용량 데이터를 공유하기에 가장 적합한 IPC

0.1.1 PIPE

PIPI는 단방향으로 메시지를 전달하는 IPC 방법이다. 이름처럼 PIPE와 작동이 비슷한데, A의 메시지를 일방적으로 B로 보내는 방식으로 동작한다. PIPE의 가장 중요한 특징은 부모가 같거나, 부모자식 관계에서만 사용이 가능하다는 점이다. 그럴 수 밖에 없는 것이 fd(file descripter)를 이용해서 연결해야 하기 때문에 관계 없는 프로세스간에는 통신이 불가능하다

pipe를 사용하는 대표적인 프로그램으로 쉘(ex. bash)이 있다. 쉘에도 파이프("|")라고 불리는 키워드가 있는데, 이 키워드는 왼쪽의 stdout을 오른쪽의 stdin으로 입력하는 역할을 한다.

$ ls | grep hello
hello_world.c

 

실제 쉘이 어떤 방식으로 PIPE를 활용하는지 보고 싶으면 github.com/sisardor/Mini-shell/blob/master/src/minishellfunc.cexecutePipedCommand 함수가 좋은 참고자료가 될 것이다.

 

0.1.2 FIFO (named pipe)

FIFO는 파일을 이용해서 메시지를 전송하는 방법이다. 별칭인 named pipe처럼, 특정 경로에 파일을 생성해서 pipe와 유사하게  메시지 전송을 한다.

FIFO는 PIPE와 다르게 부모가 다른 프로세스 간에도 IPC 통신을 할 수 있다. PIPE는 fd를 통해서 메시지를 전달하기 때문에 제약이 있는 반면에, FIFO는 파일을 통해서 fd를 얻기 때문에 같은 부모를 공유해야 한다는 제약이 없다. 쉘에서 FIFO를 사용하는 예를 살펴보자.

mkfifo my_pipe                  # fifo 생성
gzip -9 -c < my_pipe > out.gz & # my_pipe로 들어온 데이터를 압축해서 out.gz로 출력한다.
cat file > my_pipe              # my_pipe에 file 데이터를 삽입한다.

 

0.1.3 메시지큐 (Message Queue)

메시지큐도 이름처럼 큐(Queue) 형태로 관리되는 메시지 리스트이다. 메시지큐 자료구조는 커널에서 관리하기 때문에 프로세스 간에 통신이 가능할 뿐 아니라, Sender 프로세스가 종료되어도 메시지가 사라지지 않는다. 뿐만 아니라 특정 타입의 메시지만 선별적으로 받아오는 것도 가능하다.

메시지큐에는 SysV와 Posix 두 가지 방식의 구현이 있다. SysV 메시지큐 라이브러리는 상대적으로 오래되었기 때문에 기능이 제한적이고, 사용법이 약간 더 복잡하다(필요한 인자가 많다.). Posix 메시지큐 라이브러리는 반대로 API가 좀 더 직관적이고, 제공하는 기능이 많다. Posix 메시지큐가 SysV 대비 추가적으로 제공하는 기능은 다음과 같다.

  • 멀티플렉싱 API(select, poll, epoll)로 관리 가능
  • 메시지큐에 대한 notify 기능 제공 (mq_notify)
  • 멀티 쓰레드에 안전

따라서 가급적이면 Posix 메시지큐를 사용하는 것을 권장한다.

 

 

0.1.4 유닉스 도메인 소켓 (Unix Domain Socket)

이번 포스트에서 가장 중요한 Unix Domain Socket(이하 UDS)이다. 실제 프로젝트들에서 많이 사용되기도 하지만, 앞으로 다룰 D-Bus에서 내부적으로 UDS를 사용하기 때문이다. 따라서 UDS에 대해서는 조금 더 자세히 살펴보도록 하자.

UDS는 스트림 인터페이스(a.k.a TCP)와 데이터그램 인터페이스(a.k.a UDP)를 모두 제공한다. 이름에서 알 수 있듯이 일반 네트워크 소켓과 API를 공유한다. 하지만 내부적인 동작은 다르게 이뤄진다. 아래는 USD와 네트워크 소켓의 차이점이다.

  유닉스 도메인 소켓 네트워크 소켓
신뢰성 스트림, 데이터그램 모두 신뢰성을 가짐 데이터그램 인터페이스는 신뢰성이 없음
주소 파일, fd(동일 부모 소유 시) ip, port

물론 네트워크 소켓을 사용해서 IPC를 하는 것도 가능하다. 하지만 UDS는 TCP에서처럼 데이터를 체크하지 않으며, UDP와 달리 신뢰성을 보장해주고, 네트워크 스택이나 라우팅 등을 거치지 않기 때문에 훨씬 성능이 좋다. 따라서 선택이 가능하면 IPC에서는 유닉스 도메인 소켓을 사용하는 편이 유리하다. 아래는 UDS로 간단한 IPC를 수행하는 코드 예제이다. (기본 코드는 www.joinc.co.kr/w/Site/system_programing/IPC/Unix_Domain_Socket를 참조하였다.)

 

UDS server

/* UDS_Server.c */

#include <sys/types.h> 
#include <sys/stat.h> 
#include <sys/socket.h> 
#include <sys/un.h> 
#include <unistd.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 

int main(int argc, char *argv[]) {
    int server_sockfd, client_sockfd;
    int state;

    struct sockaddr_un clientaddr, serveraddr;
    int client_len = sizeof(client_len);

    char buf[255];

    state = 0;

    if (argc != 2) {
        printf("Usage: %s [UDS path]\n", argv[0]);
        exit(0);
    }

    if (access(argv[1], F_OK) == 0) {
        unlink(argv[1]);
    }

    if ((server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0) {
        perror("Failed to create socket: ");
        exit(0);
    }
    memset(&serveraddr, 0, sizeof(serveraddr));
    serveraddr.sun_family = AF_UNIX;
    strcpy(serveraddr.sun_path, argv[1]);


    state = bind(server_sockfd , (struct sockaddr *)&serveraddr, 
            sizeof(serveraddr));
    if (state == -1) {
        perror("Failed to bind: ");
        exit(0);
    }

    state = listen(server_sockfd, 5);
    if (state == -1) {
        perror("Failed to listen: ");
        exit(0);
    }


    while(1) {
        client_sockfd = accept(server_sockfd, (struct sockaddr *)&clientaddr, 
                (socklen_t*)&client_len);
        if (client_sockfd == -1) {
            perror("Failed to accept: ");
            exit(0);
        }

        while(1) {
            memset(buf, 0, 255);
            if (read(client_sockfd, buf, 255) <= 0) {
                close(client_sockfd);
                break;
            }
            printf("%s", buf);

            if (strncmp(buf, "quit", strlen("quit")) == 0) {
                write(client_sockfd, "Connection Closed\n", strlen("Connection Closed\n"));  
                close(client_sockfd);
                break;
            }

            write(client_sockfd, "IPC SUCCESS", strlen("IPC SUCCESS"));
        }
    }

    close(client_sockfd);
}

 

UDS client

/* UDS_Client.c */

#include <sys/types.h> 
#include <sys/stat.h> 
#include <sys/socket.h> 
#include <unistd.h> 
#include <sys/un.h> 
#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 

int main(int argc, char **argv) {

    int client_len;
    int client_sockfd;

    char buf_in[255];
    char buf_get[255];

    struct sockaddr_un clientaddr;

    if (argc != 2) {       
        printf("Usage: %s [UDS path]\n", argv[0]);
        exit(0);
    }       


    client_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if (client_sockfd == -1) {
        perror("Failed to create socket: ");
        exit(0);
    }
    memset(&clientaddr, 0, sizeof(clientaddr));
    clientaddr.sun_family = AF_UNIX;
    strcpy(clientaddr.sun_path, argv[1]);
    client_len = sizeof(clientaddr);
    if (connect(client_sockfd, (struct sockaddr *)&clientaddr, client_len) < 0) {
        perror("Failed to connect: ");
        exit(0);
    }

    while(1) {
        memset(buf_in, 0, sizeof(buf_in));
        printf("입력 : ");
        fgets(buf_in, 255, stdin);

        write(client_sockfd, buf_in, strlen(buf_in));
        if (strncmp(buf_in, "quit", strlen("quit")) == 0) {
            close(client_sockfd);
            exit(0);
        }

        while(1) {
            read(client_sockfd, buf_get, 255); 
            if (strncmp(buf_get, "end", strlen("end")) == 0)
                break;

            printf("%s\n", buf_get);
        }
    }

    close(client_sockfd);
    exit(0);
}

실행 결과

UDS Server UDS Client
$ gcc uds_server.c -o server
$ ./server /tmp/uds
qwer
$ gcc uds_client.c -o client
$ ./client /tmp/uds
입력 : qwer
IPC SUCCESS

 

0.1.5 공유 메모리

출처: https://www.tutorialspoint.com/shared-memory-model-of-process-communication

공유 메모리는 앞서 소개한 IPC와는 약간 결이 다르다. 앞서 소개한 IPC들이 메시지 기반이었다면, 공유 메모리는 메모리 기반이다. 공유 메모리 방식 IPC의 장점은 메시지 복사, 전송 등에 별도의 오버헤드가 발생하지 않기 때문에 대량의 데이터를 공유할 때 적합하다. 메시지 방식의 IPC보다 안정성과 사용성이 떨어진다. 예를 들어서 특정 프로세스가 공유 메모리 공간에 잘못된 형식의 데이터를 입력하거나, 또는 프로세스들이 동시에 공유 메모리에 접근하면 문제가 발생할 수 있다.

공유 메모리 방식도 메시지큐와 마찬가지로 SysV방식과 Posix 방식의 라이브러리 구현체가 있다. 메시지큐와 마찬가지로 Posix API가 더 직관적이고, 많은 기능을 제공하기 때문에, 가능하면 Posix API를 사용하도록 하자.

 

0.2 리눅스 IPC의 한계들

리눅스는 프로세스간 통신을 위한 다양한 방법들을 제공한다. 하지만 리눅스 IPC 방법들은 실제 어플리케이션 개발자들 입장에서는 아쉬운 부분들이 조금씩 있었다. 아래는 IPC 방법들이 가지고 있는 한계점들이다.

  • API 측면에서 객체지향적인 디자인 부재
  • 데이터가 Type-Safe하지 못함
  • 이벤트 notification 기능 (메세지큐의 경우 부분적으로 존재)
  • 프로세스 상태와 별개로 메시지 유지 (메세지큐 제외)
  • 광역 브로드캐스팅 메시지 전송 기능의 부재 (제한적으로 데이터스트림 유닉스 도메인 소켓으로 구현 가능)

따라서 이러한 한계들을 극복하기 위해서 개발된 것이 바로 D-Bus이다. D-Bus는 내부적인 구현은 유닉스 도메인 소켓을 기반으로 하고 있지만, 메시지를 관리하는 데몬과 추상화된 API, 규격화된 프로토콜을 통해서 기존 리눅스 IPC의 한계점들을 우회할 수 있었다. 다음 포스트에서 D-Bus가 무엇인지, 구체적으로 어떻게 동작하며 API는 어떻게 되는지 등에 대해서 살펴보도록 하겠다.

www.kernelpanic.kr/category/%EC%98%A4%ED%94%88%EC%86%8C%EC%8A%A4%20%EC%9D%BD%EA%B8%B0/%EB%AA%A8%EB%8D%98%20IPC%20%EC%8B%9C%EC%8A%A4%ED%85%9C%20-%20D-Bus

 

'오픈소스 읽기/모던 IPC 시스템 - D-Bus' 카테고리의 글 목록

 

www.kernelpanic.kr

 

반응형

'오픈소스 읽기 (OLD) > 모던 IPC 시스템 - D-Bus' 카테고리의 다른 글

번외. 주요 D-Bus 프로토콜 스펙들  (0) 2021.04.13
2. D-Bus 사용방법  (4) 2021.04.11
1. D-Bus 기초  (2) 2021.03.27