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

1. D-Bus 기초

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

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

 

1.1 D-Bus란?

컴퓨터 세상에는 다양한 개발자들이 개발한 수 많은 앱들이 있다. 안드로이드를 예로 들어보자. 우리는 유투브로 마음에 드는 영상을 "공유" 버튼을 클릭해서 카카오톡으로 친구에게 공유한다. 친구가 공유받은 영상을 보던 중에 스카이프 영상통화가 오면, 영상통화를 하는 중에 유투브는 자동으로 정지된다. 유투브와 카카오톡, 스카이프는 모두 서로 다른 개발자가 만들었다. 이처럼 서드파티가 개발한 앱들이 자연스럽게 소통을 주고 받는 일이, 기존 리눅스 IPC만을 이용해서 가능할까? 기술적으로는 가능하겠지만, 현실적으로는 불가능할 것이다. 그렇다면 왜 현실적으로 기존 리눅스 IPC로 서드파티 앱들간 소통이 어려울까? 대략 다음과 같은 이유들이 있을 수 있다.

  1. 통신 방식이 서로 제각각이다. (e.g. 유닉스 도메인 소켓, 메시지큐 등)
  2. 자료구조가 통일되지 못했다.
  3. IPC에 사용되는 채널이 제각각 다르다. (유닉스 도메인 소켓이라면, 어떤 소켓 파일을 사용하는지 각각 다르다.)
  4. 결정적으로 상대방이 어떤 통신 방식을 사용하고, 어떤 자료구조를 사용하며, 어떤 채널을 사용하는지 알 수 없다.

이러한 문제를 해결하기 위해서 안드로이드는 바인더라는 IPC 시스템을 개발하여 앱들간 통신에 사용하고 있다.

안드로이드와 마찬가지로 리눅스 세상에서도 이와 비슷한 문제가 있었다. 따라서 리눅스 커뮤니티에서도 기존 IPC의 한계를 극복하기 위해서 여러 IPC 시스템을 개발했었다. 대표적으로 Cobra, Dcop 등이 있었다. 이들은 어느정도 기존 IPC의 한계를 극복하며, 리눅스 생태계에 정착되었다. 하지만 여전히 큰 문제가 남아 있었다. 바로 "여러 IPC 시스템"을 개발했기 때문에, IPC 시스템 간 파편화가 발생한 것이다. 결국 리눅스 커뮤니티들은 의견을 모아서 통일된 IPC 시스템을 개발하였고, 이것이 지금 다루려고 하는 D-Bus이다.

D-Bus는 모던 방식의 IPC 시스템이다. 오래된 리눅스 IPC와 비교하여 D-Bus는 아래와 같은 특징을 가지고 있다. (리눅스 기존 IPC에서도 일부 항목은 해당되기도 한다.)

  • 주소 기반 메시지 시스템
  • 서비스 데몬이 메시지 관리 (프로세스가 종료되어도 메시지 상태 유지)
  • 비동기 동작 (Notification 기능)
  • 광역 브로드캐스팅기능 제공
  • 데이터 인터페이스, 타입 공유
  • 객체지향적인 API 설계(GDbus 한정)

그러면 D-Bus가 이러한 특징을 제공하기 위해서 어떤 구조를 가지고 있는지 살펴보자.

 

1.2 D-Bus의 구조

출처: https://ko.wikipedia.org/wiki/D-Bus

 

D-Bus는 기존 IPC와는 완전히 다른, 하지만 꼭 필요한 몇 가지 기능들을 제공해야 했다. (구체적인 항목은 "1.1 D-Bus란?" 항목 참조) 이를 위한 고민의 결과가 IPC를 관리하는 서비스이다. D-Bus에서 IPC는 프로세스간 직접적으로 이뤄지지 않고, D-Bus 서비스를 통해서 이뤄진다. 즉, D-Bus 서비스는 전달받은 IPC들의 타입체크, 주소기반으로 분류, 브로드 캐스팅 등의 동작을 수행할 수 있다.  따라서 D-Bus는 기존 리눅스 IPC 시스템을 변경하지 않고도 전혀 다른 경험을 제공한다.

리눅스 머신이 있다면, 터미널에서 "ps -ef | grep dbus-daemon"를 입력하면, D-Bus 서비스가 동작하는지 확인할 수 있다. 아래는 내 PC에서 실행되는 D-Bus 서비스 리스트이다.

어? D-Bus 서비스가 왜 2개나 동작하고 있지?

라는 생각이 들 수 있다. 각 D-Bus 서비스는 뒤에 붙은 옵션 이름에 따라서 기능이 달라진다. 각 서비스는 다음과 같다.

D-Bus 서비스 옵션 설명
시스템 버스 dbus-daemon --system OS 수준에서 네트워크, 블루투스 등 시스템 서비스와 통신하는 버스
세션 버스 dbus-daemon --session 데스크탑 세션 수준에서 어플리케이션 간 통신하는 버스. 다른 세션간의 통신은 불가능

 

1.3 D-Bus 용어 및 개념

출처: https://dbus.freedesktop.org/doc/diagram.png

나는 처음 D-Bus를 접할때 처음 몇 주간 꽤나 고생했다. D-Bus에는 몇 가지 생소한 개념들이 있었는데, (성격이 급해서..) 이런 개념들에 대한 공부를 소홀히 했었기 때문이다. 게다가 당시에는 객체지향은 그런게 있더라 정도라서, 객체지향 개념을 적극 차용한 D-Bus를 이해하는데 더 어려움이 있었다. 하지만 원리를 이해하면, 그닥 어려운 개념들은 아니다.

D-Bus는 주소 기반의 메시지 서비스이다. 만약에 내가(App 나) 다른 사람(App 다른 사람)에게 편지(메시지)를 전달해 주려면 어떤 정보들을 알아야 할까? 우선 전달 받는 사람의 이름(Bus Name)과 주소(Object Path)를 알아야 한다. 그리고 같은 편지의 내용을 이해할 수 있도록 동일한 언어(인터페이스)를 사용하고 있어야 한다. D-Bus도 이와 마찬가지이다. 우선 간단한 D-Bus 코드를 보면서 각각의 용어들에 대해서 개념을 살펴보자.

 

// D-Bus 서버
// 위는 생략
// 원본 출처: https://github.com/fbuihuu/samples-dbus

int main(void)
{
    DBusConnection *conn;
    DBusError err;
    int rv;

    dbus_error_init(&err);

    /* 
     * Session Bus로 서버 생성 
     */
    conn = dbus_bus_get(DBUS_BUS_SESSION, &err);
    if (!conn) {
        fprintf(stderr, "Failed to get a session DBus connection: %s\n", err.message);
        goto fail;
    }

    /* 
     * Bus Name 할당 
     */
    rv = dbus_bus_request_name(conn, "org.example.TestServer", DBUS_NAME_FLAG_REPLACE_EXISTING , &err);
    if (rv != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
        fprintf(stderr, "Failed to request name on bus: %s\n", err.message);
        goto fail;
    }

    /* 
     * Object Path 연결, 
     * server_vtable 구조체를 통해서 인터페이스 등록 
     */
    if (!dbus_connection_register_object_path(conn, "/org/example/TestObject", &server_vtable, NULL)) {
        fprintf(stderr, "Failed to register a object path for 'TestObject'\n");
        goto fail;
    }

    /*
     * 이하는 D-Bus 서버를 구동하는 메인 루프
     */
    printf("Starting dbus tiny server v%s\n", version);
    mainloop = g_main_loop_new(NULL, false);
    /* Set up the DBus connection to work in a GLib event loop */
    dbus_connection_setup_with_g_main(conn, NULL);
    /* Start the glib event loop */
    g_main_loop_run(mainloop);

    return EXIT_SUCCESS;
fail:
    dbus_error_free(&err);
    return EXIT_FAILURE;
}

1.3.1 Bus Name

어플리케이션(프로세스)이 사용하는 버스 이름이다. D-Bus 데몬에 어플리케이션이 연결될 때 즉시 이름이 생성되며, 항상 중복되지 않는 이름을 부여받는다. 내부적으로 부여받는 이름은 :34-907와 같은 형식을 가진다. 그러나 가독성을 위해서 "org.example.TestServer"와 같은 공개된 이름(well-known names)을 사용한다. 이러한 사용 방법은 인터넷을 생각하면 이해가 쉽다. 이 블로그의 IP 주소는 "27.0.236.139"이지만, "www.kernelpanic.kr"로도 접근이 가능한 것과 같다.

 

1.3.2 Object Path

Object Path는 D-Bus 객체를 구분하는 가상의 경로이다. Java를 사용하는 개발자라면 "/org/example/TestObject"와 같은 객체 표기 방법에 익숙할 것이다. Object Path는 이름처럼 경로의 형식을 취하고 있지만, 실제 Unix 파일 경로와는 무관하다. Object Path를 통해서 클라이언트와 서버는 동일한 객체에 접근할 수 있다.

그렇다면 이미 Bus Name이 있는데, Object Path를 사용하는 이유는 무엇일까? Bus Name은 어플리케이션에 하나만 할당되는 개념이지만, Object Path는 어플리케이션에 여러개가 할당될 수 있다. 또한 이미 정의된 Object Path를 다른 어플리케이션에서 사용할 수 있다. 예를 들어보자. 리눅스에 캘린더에 일정을 등록해 두면 정해진 시간에 팝업으로 노티가 발생된다. 그런데 리눅스에서는 노티를 띄우는 서비스가 여러가지가 있다. 우분투 20.04를 사용한다면 notification-daemon이 노티를 처리하지만, 만자로 리눅스를 사용한다면 xfce4-notifyd가 노티를 처리할 것이다. 또한 만약 임베디드 Wayland 환경에서 노티를 처리한다면 아마도 mako 또는 커스텀 노티 서비스를 이용해서 노티를 관리할 것이다. 이처럼 서로 다른 노티 서비스지만 모두 동일하게 캘린더의 노티를 처리할 수 있는 것은, 이들 서비스들이 동일한 Object Path(/org/freedesktop/Notifications)를 사용하고 있기 때문에 가능하다.

 

1.3.3 Method와 Signal

D-Bus는 Method와 Signal을 이용해서 메시지를 전달한다. Method는 input을 통해 여러 인자들을 전달하고, Output을 통해서 회신을 받을 수 있다. 반면 Signal은 해당 객체를 구독하는(혹은 관찰하는) 이들에게 브로드캐스트로 메시지를 전달한다. Signal은 광역으로 메시지를 뿌리기 때문에 별도로 Output을 이용해서 회신을 받지는 못한다.

 

1.3.4 Interface

Interface는 Method와 Signal을 어떻게 사용할지 정의해 놓은 약속이다. 위의 예제코드에서는 Interface 부분이 생략되었는데, 실제로 아래와 같이 인터페이스가 정의되어 있다.

const char *server_introspection_xml =
	DBUS_INTROSPECT_1_0_XML_DOCTYPE_DECL_NODE
	"<node>\n"

	"  <interface name='org.freedesktop.DBus.Introspectable'>\n"
	"    <method name='Introspect'>\n"
	"      <arg name='data' type='s' direction='out' />\n"
	"    </method>\n"
	"  </interface>\n"

	"  <interface name='org.freedesktop.DBus.Properties'>\n"
	"    <method name='Get'>\n"
	"      <arg name='interface' type='s' direction='in' />\n"
	"      <arg name='property'  type='s' direction='in' />\n"
	"      <arg name='value'     type='s' direction='out' />\n"
	"    </method>\n"
	"    <method name='GetAll'>\n"
	"      <arg name='interface'  type='s'     direction='in'/>\n"
	"      <arg name='properties' type='a{sv}' direction='out'/>\n"
	"    </method>\n"
	"  </interface>\n"

	"  <interface name='org.example.TestInterface'>\n"
	"    <property name='Version' type='s' access='read' />\n"
	"    <method name='Ping' >\n"
	"      <arg type='s' direction='out' />\n"
	"    </method>\n"
	"    <method name='Echo'>\n"
	"      <arg name='string' direction='in' type='s'/>\n"
	"      <arg type='s' direction='out' />\n"
	"    </method>\n"
	"    <method name='EmitSignal'>\n"
	"    </method>\n"
	"    <method name='Quit'>\n"
	"    </method>\n"
	"    <signal name='OnEmitSignal'>\n"
	"    </signal>"
	"  </interface>\n"

	"</node>\n";

인터페이스는 위와 같이 Method의 이름과 input 인자, output 타입과, Signal의 이름과 인자(Signal은 input 밖에 없다.)를 정의한다. 클라이언트 어플리케이션은 정의된 인터페이스에 따라서 method를 전송하고, Signal을 수신할 수 있다.

 

다음 장에서는 본격적으로 D-Bus 어플리케이션을 작성하고, 실제 리눅스 데스크탑 환경과 상호작용 하는 방법에 대해서 살펴보려 한다. 어플리케이션 입장에서는 D-Bus의 통신 규약만 지키면 되기 때문에 D-Bus를 사용하는데 언어의 제약은 없으며, D-Bus 통신을 위한 라이브러리도 다양하다. 여기서는 C언어와 sd-bus 라이브러리를 사용하여 D-Bus 통신을 하는 예제를 살펴볼 예정이다.

반응형