D-Bus (Desktop Bus)
D-Bus 시스템/세션 버스 아키텍처, 메시지 모델(Method Call/Signal/Property), 버스 이름과 객체 경로 체계, 타입 시스템, 서비스 활성화, 보안 모델(Policy/Polkit), 커널과의 관계(AF_UNIX/kdbus), 주요 서비스, 도구(busctl/dbus-monitor), 프로그래밍 인터페이스(sd-bus/GIO), 성능과 대안 분석을 종합 정리
핵심 요약
- D-Bus는 프로세스 간 통신(IPC)을 위한 메시지 버스 시스템(Message Bus System)으로, 하나의 데몬(Daemon)이 여러 프로세스 사이의 메시지를 중개합니다.
- 시스템 버스(System Bus)는 시스템 전체 서비스(systemd, NetworkManager, BlueZ 등)가 사용하고, 세션 버스(Session Bus)는 사용자 데스크톱 애플리케이션이 사용합니다.
- 통신 방식은 메서드 호출(Method Call)(요청-응답), 시그널(Signal)(브로드캐스트), 프로퍼티(Property)(속성 조회·변경) 세 가지 패턴으로 나뉩니다.
- 모든 서비스는 버스 이름(Bus Name)으로 식별되고, 서비스 내부의 기능은 객체 경로(Object Path)와 인터페이스(Interface)로 구조화됩니다.
- 보안은 D-Bus 정책(Policy)(누가 어떤 메서드를 호출할 수 있는지)과 Polkit(대화형 권한 승인)으로 계층적으로 관리됩니다.
단계별 이해
- 시스템 버스 탐색:
busctl명령으로 현재 시스템에 등록된 모든 D-Bus 서비스 목록을 확인합니다. - 메시지 관찰:
dbus-monitor --system으로 시스템 버스에서 실시간으로 오가는 메시지를 관찰합니다. - 메서드 호출:
busctl call이나dbus-send로 서비스의 메서드를 직접 호출해 봅니다. - 서비스 작성: sd-bus 라이브러리로 자신만의 D-Bus 서비스를 C 코드로 작성해 봅니다.
D-Bus 아키텍처(Architecture)
D-Bus는 중앙 집중형 메시지 버스 구조를 사용합니다. 클라이언트(Client)와 서비스(Service)가 직접 통신하지 않고, 중간에 위치한 버스 데몬(Bus Daemon)이 메시지를 라우팅합니다. 이 구조 덕분에 서비스 발견(Service Discovery), 활성화(Activation), 접근 제어(Access Control)를 한 곳에서 관리할 수 있습니다.
시스템 버스와 세션 버스
리눅스 시스템에서는 항상 두 종류의 버스가 동작합니다.
| 구분 | 시스템 버스(System Bus) | 세션 버스(Session Bus) |
|---|---|---|
| 소켓 경로 | /run/dbus/system_bus_socket | $XDG_RUNTIME_DIR/bus 또는 추상 소켓(Abstract Socket) |
| 데몬 실행 | 부팅 시 systemd가 시작 | 사용자 로그인 시 시작 |
| 주요 사용자 | systemd, NetworkManager, BlueZ, udisks, logind | 데스크톱 앱(파일 관리자, 알림, 미디어 플레이어) |
| 보안 정책 | 엄격 — 루트 권한 서비스 위주, Polkit 인증 필요 | 느슨 — 같은 사용자의 프로세스끼리 자유 통신 |
| 설정 파일 | /usr/share/dbus-1/system.conf | /usr/share/dbus-1/session.conf |
버스 데몬 구현체
D-Bus 사양(Specification)과 버스 데몬 구현은 분리되어 있습니다. 주요 구현체는 두 가지입니다.
| 구현체 | 특징 | 사용 배포판 |
|---|---|---|
| dbus-daemon | 원래의 참조 구현(Reference Implementation). freedesktop.org에서 개발. 단일 스레드(Single-threaded) 이벤트 루프(Event Loop) 방식 | 대부분의 배포판 (전통적 기본값) |
| dbus-broker | 성능 최적화된 대안. 커널의 epoll과 비차단(Non-blocking) I/O를 적극 활용. 메시지 정렬(Message Ordering)을 보장하면서도 높은 처리량(Throughput) 달성 | Fedora, RHEL 8+, Arch Linux (기본값) |
두 구현체 모두 같은 D-Bus 프로토콜(Protocol)을 사용하므로, 클라이언트 코드는 어떤 데몬이 동작하든 동일하게 작동합니다.
dbus-broker 내부 구조
dbus-broker는 단일 프로세스인 dbus-daemon과 달리, 역할을 분리한 다중 컴포넌트(Multi-component) 구조를 사용합니다.
| 컴포넌트 | 역할 | 특징 |
|---|---|---|
| dbus-broker-launch | 런처(Launcher) — 설정 파일 파싱, 정책 컴파일, broker 프로세스 관리 | 정책을 사전 컴파일(Pre-compile)하여 broker에게 전달. 런타임 XML 파싱 오버헤드 제거 |
| dbus-broker | 브로커(Broker) — 실제 메시지 라우팅과 전달 | 최소 권한(Minimal Privilege)으로 실행. epoll + 비차단 I/O로 고처리량 달성 |
| bus driver | org.freedesktop.DBus 인터페이스 구현 | 이름 등록(RequestName), 매칭 규칙(AddMatch), 서비스 활성화 등 버스 관리 기능 |
| controller | systemd와의 통합 인터페이스 | 소켓 활성화(Socket Activation) 수신, 서비스 시작 요청을 systemd에 위임 |
소켓 활성화(Socket Activation)
현대 시스템에서 dbus-daemon/dbus-broker는 systemd 소켓 활성화(Socket Activation)로 시작됩니다. systemd가 먼저 소켓을 열어 두고, 첫 연결이 들어오면 버스 데몬을 시작합니다.
# /usr/lib/systemd/system/dbus.socket
[Unit]
Description=D-Bus System Message Bus Socket
[Socket]
ListenStream=/run/dbus/system_bus_socket
SocketMode=0666
[Install]
WantedBy=sockets.target
# /usr/lib/systemd/system/dbus-broker.service (Fedora/RHEL)
[Unit]
Description=D-Bus System Message Bus
Requires=dbus.socket
[Service]
Type=notify
ExecStart=/usr/bin/dbus-broker-launch --scope system
NotifyAccess=main
WatchdogSec=3min
이 구조 덕분에 부팅 초기에 D-Bus 소켓이 준비되어 있고, 다른 서비스들이 D-Bus 연결을 시도하면 그때 버스 데몬이 시작됩니다. 이는 부팅 순서 의존성(Boot Order Dependency) 문제를 해결합니다.
메시지 라우팅 과정
클라이언트가 메서드를 호출하면 다음과 같은 과정을 거칩니다.
- 클라이언트가 메서드 호출 메시지를 버스 데몬의 소켓으로 전송합니다.
- 버스 데몬이 메시지의 목적지(Destination) 필드를 확인하여 해당 서비스를 찾습니다.
- 서비스가 현재 실행 중이 아니면, 서비스 활성화(Service Activation)를 통해 서비스를 시작합니다.
- 버스 데몬이 보안 정책(Security Policy)을 검사하여 호출이 허용되는지 확인합니다.
- 허용되면 메시지를 목적지 서비스의 소켓으로 전달합니다.
- 서비스가 처리 후 응답 메시지(Method Return) 또는 오류 메시지(Error)를 버스 데몬으로 보내고, 데몬이 이를 원래 호출자에게 전달합니다.
메시지 모델(Message Model)
D-Bus의 모든 통신은 메시지(Message) 단위로 이루어집니다. 메시지는 헤더(Header)와 본문(Body)으로 구성되며, 헤더에는 메시지 타입과 라우팅 정보가, 본문에는 실제 데이터가 담깁니다.
메시지 타입
| 타입 | 방향 | 응답 여부 | 설명 |
|---|---|---|---|
| METHOD_CALL | 클라이언트 → 서비스 | 응답 대기 | 서비스의 특정 메서드를 호출. serial 번호로 응답과 매칭 |
| METHOD_RETURN | 서비스 → 클라이언트 | — | 메서드 호출 성공 시 결과 반환. reply_serial로 원래 요청 식별 |
| ERROR | 서비스 → 클라이언트 | — | 메서드 호출 실패 시 오류 이름과 메시지 반환 |
| SIGNAL | 서비스 → 구독자 전체 | 응답 없음 | 이벤트 발생 알림. 특정 목적지 없이 매칭 규칙(Match Rule)에 따라 전달 |
메시지 헤더 필드
| 필드 | 타입 | 설명 | 예시 |
|---|---|---|---|
| PATH | OBJECT_PATH | 대상 객체의 경로 | /org/freedesktop/NetworkManager |
| INTERFACE | STRING | 메서드가 속한 인터페이스 | org.freedesktop.NetworkManager |
| MEMBER | STRING | 호출할 메서드 또는 시그널 이름 | GetDevices |
| DESTINATION | STRING | 목적지 버스 이름 | org.freedesktop.NetworkManager |
| SENDER | STRING | 발신자의 고유 이름 (데몬이 자동 설정) | :1.42 |
| SIGNATURE | SIGNATURE | 본문(Body) 데이터의 타입 시그니처 | ao (객체 경로 배열) |
| SERIAL | UINT32 | 메시지 일련번호 (응답 매칭용) | 42 |
와이어 포맷(Wire Format)
D-Bus 메시지는 네트워크 바이트 순서가 아닌, 발신자의 엔디언(Endianness)을 따릅니다. 수신자는 첫 바이트로 엔디언을 판별합니다.
| 오프셋 | 크기 | 필드 | 설명 |
|---|---|---|---|
| 0 | 1 | 엔디언(Endianness) | l(0x6C) = 리틀엔디언, B(0x42) = 빅엔디언 |
| 1 | 1 | 메시지 타입 | 1=METHOD_CALL, 2=METHOD_RETURN, 3=ERROR, 4=SIGNAL |
| 2 | 1 | 플래그(Flags) | 비트 마스크: 0x1=NO_REPLY_EXPECTED, 0x2=NO_AUTO_START, 0x4=ALLOW_INTERACTIVE_AUTHORIZATION |
| 3 | 1 | 프로토콜 버전 | 항상 1 (현재 사양 기준) |
| 4-7 | 4 | 본문 길이(Body Length) | 헤더 이후 본문 데이터의 바이트 수 |
| 8-11 | 4 | 시리얼(Serial) | 메시지 일련번호. 응답 매칭에 사용 |
| 12- | 가변 | 헤더 필드 배열 | a(yv) 형식 — (필드 코드, variant 값) 쌍의 배열. 8바이트 경계로 패딩 |
메시지 플래그(Message Flags)
| 플래그 | 값 | 설명 | 사용 시점 |
|---|---|---|---|
| NO_REPLY_EXPECTED | 0x1 | 응답을 기대하지 않음. 서비스는 METHOD_RETURN을 보내지 않아도 됨 | 단방향 알림, 성능 최적화 (응답 대기 제거) |
| NO_AUTO_START | 0x2 | 서비스가 실행 중이 아니면 활성화하지 않고 오류 반환 | 서비스 존재 여부 확인, 선택적 기능 호출 |
| ALLOW_INTERACTIVE_AUTHORIZATION | 0x4 | Polkit 대화형 인증(비밀번호 입력)을 허용 | GUI 애플리케이션에서 권한 상승이 필요한 작업 |
# NO_REPLY_EXPECTED 플래그로 호출 (응답 대기 없음)
busctl call --expect-reply=no org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager Reload
# NO_AUTO_START 플래그로 호출 (자동 활성화 비활성화)
busctl call --auto-start=no org.example.Optional \
/org/example/Optional org.example.Optional Ping
프로퍼티(Property) 접근
D-Bus 서비스의 상태 값은 프로퍼티(Property)로 노출됩니다. 프로퍼티는 실제로 org.freedesktop.DBus.Properties 인터페이스의 메서드 호출로 구현됩니다.
# 특정 프로퍼티 읽기
busctl get-property org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.NetworkManager State
# 모든 프로퍼티 나열
busctl introspect org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.NetworkManager
# 프로퍼티 변경 시그널 구독
dbus-monitor --system "type='signal',interface='org.freedesktop.DBus.Properties',member='PropertiesChanged'"
프로퍼티가 변경되면 서비스는 PropertiesChanged 시그널을 발생시킵니다. 이 시그널은 변경된 프로퍼티 이름과 새 값, 그리고 값이 비싸서 시그널에 포함하지 않은(invalidated) 프로퍼티 목록을 전달합니다.
버스 이름과 객체 경로(Bus Names & Object Paths)
버스 이름의 두 가지 유형
| 유형 | 형식 | 특징 | 예시 |
|---|---|---|---|
| 고유 이름(Unique Name) | :숫자.숫자 | 연결 시 데몬이 자동 할당. 연결 수명 동안 유일. 다른 프로세스가 사용 불가 | :1.42, :1.203 |
| 잘 알려진 이름(Well-known Name) | 역DNS(Reverse DNS) | 서비스가 명시적으로 요청하여 등록. 사람이 읽을 수 있음. 소유자(Owner) 변경 가능 | org.freedesktop.NetworkManager |
하나의 연결(Connection)이 여러 개의 잘 알려진 이름을 소유할 수 있고, 잘 알려진 이름의 소유권(Ownership)은 큐(Queue)로 관리됩니다. 현재 소유자가 해제하면 큐의 다음 요청자가 자동으로 소유자가 됩니다.
이름 소유권 관리
RequestName 메서드로 잘 알려진 이름을 요청할 때 동작 플래그(Flag)를 지정할 수 있습니다.
| 플래그 | 값 | 동작 |
|---|---|---|
| DBUS_NAME_FLAG_ALLOW_REPLACEMENT | 0x1 | 다른 프로세스가 REPLACE_EXISTING으로 요청하면 소유권을 양보 |
| DBUS_NAME_FLAG_REPLACE_EXISTING | 0x2 | 현재 소유자가 ALLOW_REPLACEMENT를 설정했으면 소유권을 빼앗음 |
| DBUS_NAME_FLAG_DO_NOT_QUEUE | 0x4 | 소유 실패 시 큐에 대기하지 않고 즉시 실패 반환 |
# 이름 소유권 확인
busctl list --system | grep NetworkManager
# org.freedesktop.NetworkManager 1234 NetworkManager root :1.15 ...
# 이름 소유자 변경 모니터링
dbus-monitor --system "type='signal',member='NameOwnerChanged'"
# signal sender=org.freedesktop.DBus -> dest=(null)
# member=NameOwnerChanged
# string "org.freedesktop.NetworkManager"
# string ":1.15" ← 이전 소유자
# string ":1.203" ← 새 소유자 (서비스 재시작 시)
# 특정 이름의 소유자 PID 확인
busctl status org.freedesktop.NetworkManager
NameOwnerChanged 시그널은 서비스의 생명주기(Lifecycle)를 추적하는 핵심 메커니즘입니다. 이전 소유자가 빈 문자열이면 서비스가 새로 시작된 것이고, 새 소유자가 빈 문자열이면 서비스가 종료된 것입니다.
객체 경로(Object Path)
D-Bus 서비스 내부에서 특정 기능 단위를 식별하는 경로입니다. 파일 시스템 경로처럼 /로 구분된 계층 구조를 사용합니다.
이름 규칙 요약
| 요소 | 형식 규칙 | 예시 |
|---|---|---|
| 버스 이름 | 역DNS, 마침표(.) 구분, 최소 2개 요소 | org.freedesktop.NetworkManager |
| 객체 경로 | 슬래시(/) 구분, 파일 경로 형태 | /org/freedesktop/NetworkManager/Devices/1 |
| 인터페이스 | 역DNS, 버스 이름과 동일한 형식 | org.freedesktop.NetworkManager.Device |
| 멤버(메서드/시그널) | CamelCase, 마침표 없음 | GetDevices, StateChanged |
인터페이스(Interface)와 인트로스펙션(Introspection)
인터페이스(Interface)는 하나의 객체(Object)가 제공하는 메서드(Method), 시그널(Signal), 프로퍼티(Property)의 집합입니다. 하나의 객체가 여러 인터페이스를 동시에 구현할 수 있습니다.
표준 인터페이스
모든 D-Bus 객체가 반드시(또는 관례적으로) 구현하는 표준 인터페이스가 있습니다.
| 인터페이스 | 주요 멤버 | 설명 |
|---|---|---|
org.freedesktop.DBus.Introspectable | Introspect() → xml | 객체의 인터페이스·메서드·시그널·프로퍼티를 XML로 반환 |
org.freedesktop.DBus.Properties | Get(), Set(), GetAll(), PropertiesChanged 시그널 | 프로퍼티 읽기·쓰기·변경 알림을 통합 관리 |
org.freedesktop.DBus.Peer | Ping(), GetMachineId() | 연결 상태 확인(Liveness Check)과 머신 식별 |
org.freedesktop.DBus.ObjectManager | GetManagedObjects(), InterfacesAdded/Removed 시그널 | 하위 객체 트리를 한 번에 조회, 객체 추가·제거 알림 |
인트로스펙션 XML
Introspect() 메서드를 호출하면 객체의 전체 API 구조를 XML로 받을 수 있습니다.
# busctl로 인트로스펙션 실행
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1
<!-- Introspect() 반환 XML 예시 (요약) -->
<node name="/org/freedesktop/systemd1">
<interface name="org.freedesktop.systemd1.Manager">
<method name="StartUnit">
<arg name="name" type="s" direction="in"/>
<arg name="mode" type="s" direction="in"/>
<arg name="job" type="o" direction="out"/>
</method>
<signal name="UnitNew">
<arg name="id" type="s"/>
<arg name="unit" type="o"/>
</signal>
<property name="Version" type="s" access="read"/>
</interface>
<interface name="org.freedesktop.DBus.Properties">
<!-- Get, Set, GetAll, PropertiesChanged -->
</interface>
<node name="unit"/>
<node name="job"/>
</node>
XML 구조 설명
- <node> 객체 경로 하나를 나타냅니다. 하위
<node>는 자식 객체 경로를 의미합니다. - <interface> 이 객체가 구현하는 인터페이스입니다. 이름은 역DNS 형식입니다.
- <method> 호출 가능한 메서드.
<arg direction="in">은 입력,"out"은 반환값입니다. - <signal> 서비스가 발생시키는 이벤트. 모든 인자는 출력 방향입니다.
- <property>
access="read"는 읽기 전용,"readwrite"는 읽기·쓰기 가능합니다. - type 속성
s=문자열,o=객체 경로 등 D-Bus 타입 시그니처를 나타냅니다.
busctl introspect 출력 읽기
busctl introspect는 인트로스펙션 결과를 테이블 형태로 보여줍니다.
$ busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1 org.freedesktop.systemd1.Manager
NAME TYPE SIGNATURE RESULT/VALUE FLAGS
.GetUnit method s o -
.ListUnits method - a(ssssssouso) -
.Reload method - - -
.StartUnit method ss o -
.StopUnit method ss o -
.UnitNew signal so - -
.UnitRemoved signal so - -
.Version property s "256" const
.NNames property u 412 emits-change
출력 칼럼 설명
- NAME 멤버 이름. 마침표(.)로 시작합니다.
- TYPE
method,signal,property중 하나입니다. - SIGNATURE 메서드의 입력 타입 시그니처.
-는 인자 없음을 의미합니다. - RESULT/VALUE 메서드의 반환 타입 시그니처, 또는 프로퍼티의 현재 값입니다.
- FLAGS
const=읽기 전용(변경 불가),emits-change=변경 시 PropertiesChanged 시그널 발생,emits-invalidation=값 없이 변경 알림만.
인터페이스 설계 패턴
D-Bus 서비스를 설계할 때 반복적으로 사용되는 패턴이 있습니다.
| 패턴 | 구조 | 적용 시점 | 예시 |
|---|---|---|---|
| 매니저 패턴(Manager) | 단일 루트 객체에 모든 메서드 집중 | 전역 상태 관리, 설정 변경 | org.freedesktop.systemd1.Manager |
| 컬렉션 패턴(Collection) | ObjectManager + 번호 매겨진 하위 객체들 | 동적 리소스 (장치, 연결, 세션) | NetworkManager의 .../Devices/1, BlueZ의 .../dev_XX_XX |
| 프로퍼티 중심(Properties-heavy) | 메서드는 최소, 대부분 프로퍼티로 상태 노출 | 읽기 위주, PropertiesChanged로 변경 추적 | UPower의 배터리 상태 |
| 시그널 구동(Signal-driven) | 이벤트를 시그널로 브로드캐스트, 클라이언트가 구독 | 비동기 알림, 로그 스트리밍 | logind의 PrepareForSleep, NM의 StateChanged |
어노테이션(Annotation) 태그
인트로스펙션 XML에서 <annotation> 요소는 멤버에 대한 메타데이터(Metadata)를 제공합니다.
| 어노테이션 이름 | 값 | 의미 |
|---|---|---|
org.freedesktop.DBus.Deprecated | true | 이 멤버는 더 이상 사용하지 않음(Deprecated). 새 코드에서 사용 금지 |
org.freedesktop.DBus.Property.EmitsChangedSignal | true / invalidates / const / false | 프로퍼티 변경 시 PropertiesChanged 시그널 발생 방식. invalidates는 이름만 알리고 값은 생략 |
org.freedesktop.DBus.Method.NoReply | true | 이 메서드는 응답을 반환하지 않음 (fire-and-forget) |
org.freedesktop.systemd1.Privileged | true | 이 멤버는 루트 권한 또는 Polkit 인증이 필요 |
gdbus-codegen 코드 생성
GLib 기반 프로젝트에서는 gdbus-codegen으로 인트로스펙션 XML에서 C 바인딩(Binding)을 자동 생성할 수 있습니다.
# 1. 서비스에서 인트로스펙션 XML 추출
gdbus introspect --system --dest org.freedesktop.UPower \
--object-path /org/freedesktop/UPower --xml > upower.xml
# 2. C 코드 생성 (헤더 + 소스)
gdbus-codegen --interface-prefix org.freedesktop.UPower \
--generate-c-code upower-generated \
--c-namespace UPower \
upower.xml
# 3. 생성된 파일: upower-generated.h, upower-generated.c
# 프록시(Proxy) 객체로 메서드 호출:
# UPowerDevice *proxy = upower_device_proxy_new_for_bus_sync(...);
# gdouble percentage = upower_device_get_percentage(proxy);
D-Bus 타입 시스템(Type System)
D-Bus는 자체 타입 시스템(Type System)을 가지고 있으며, 메시지의 본문 데이터는 이 타입 시스템에 따라 직렬화(Serialization)됩니다. 각 타입은 한 글자의 시그니처 문자(Signature Character)로 표현됩니다.
기본 타입(Basic Types)
| 시그니처 | 이름 | 크기 | 설명 |
|---|---|---|---|
y | BYTE | 1바이트 | 부호 없는 8비트 정수 |
b | BOOLEAN | 4바이트 | 0(false) 또는 1(true) |
n | INT16 | 2바이트 | 부호 있는 16비트 정수 |
q | UINT16 | 2바이트 | 부호 없는 16비트 정수 |
i | INT32 | 4바이트 | 부호 있는 32비트 정수 |
u | UINT32 | 4바이트 | 부호 없는 32비트 정수 |
x | INT64 | 8바이트 | 부호 있는 64비트 정수 |
t | UINT64 | 8바이트 | 부호 없는 64비트 정수 |
d | DOUBLE | 8바이트 | IEEE 754 배정밀도 부동소수점 |
s | STRING | 가변 | UTF-8 문자열 (NUL 종단) |
o | OBJECT_PATH | 가변 | D-Bus 객체 경로 문자열 |
g | SIGNATURE | 가변 | D-Bus 타입 시그니처 문자열 |
h | UNIX_FD | 4바이트 | 유닉스 파일 디스크립터(SCM_RIGHTS로 전달) |
컨테이너 타입(Container Types)
| 시그니처 | 이름 | 설명 | 예시 |
|---|---|---|---|
aT | ARRAY | 같은 타입 T의 0개 이상 나열 | ai = INT32 배열, as = 문자열 배열 |
(T₁T₂...) | STRUCT | 서로 다른 타입의 고정 조합 | (si) = (문자열, INT32) 쌍 |
a{KV} | DICT | 키-값 쌍의 배열 (사전). 키는 기본 타입만 가능 | a{sv} = 문자열→variant 사전 |
v | VARIANT | 런타임에 타입이 결정되는 값. 시그니처가 값과 함께 전달됨 | 프로퍼티의 값 타입으로 자주 사용 |
시그니처 읽기 예제
| 시그니처 | 의미 | 사용처 |
|---|---|---|
a{sv} | 문자열 → 임의 타입 사전 | 프로퍼티 집합, 옵션 전달 (가장 흔한 패턴) |
ao | 객체 경로 배열 | GetDevices() 반환값 |
a{sa{sv}} | 문자열 → (문자열 → variant 사전) 사전 | GetManagedObjects() — 인터페이스별 프로퍼티 맵 |
(ssa{sv}) | (문자열, 문자열, 사전) 구조체 | PropertiesChanged 시그널 — (인터페이스, 변경된 프로퍼티, 무효화 목록) |
a(iiay) | (INT32, INT32, 바이트 배열) 구조체의 배열 | IP 주소 목록 (주소 체계, 프리픽스, 주소 바이트) |
# busctl에서 시그니처 확인
busctl call org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager \
ListUnits
# 반환 시그니처: a(ssssssouso) — 유닛 정보 구조체의 배열
마샬링 정렬 규칙(Marshalling Alignment)
D-Bus 와이어 포맷에서 각 타입은 자연 정렬(Natural Alignment)을 따릅니다. 이전 필드 끝부터 다음 필드 시작까지 필요한 만큼 패딩(Padding) 바이트(0x00)가 삽입됩니다.
| 타입 | 정렬(바이트) | 직렬화 형태 |
|---|---|---|
BYTE (y) | 1 | 1바이트 그대로 |
BOOLEAN (b), INT32 (i), UINT32 (u) | 4 | 4바이트 정수 (엔디언 따름) |
INT16 (n), UINT16 (q) | 2 | 2바이트 정수 |
INT64 (x), UINT64 (t), DOUBLE (d) | 8 | 8바이트 |
STRING (s), OBJECT_PATH (o) | 4 | UINT32 길이 + UTF-8 바이트 + NUL |
SIGNATURE (g) | 1 | BYTE 길이 + ASCII 바이트 + NUL |
ARRAY (a) | 4 | UINT32 바이트 수 + 패딩 + 요소들 |
STRUCT ((...)) | 8 | 8바이트 경계로 패딩 후 필드들 |
DICT_ENTRY ({...}) | 8 | STRUCT와 동일 |
VARIANT (v) | 1 | SIGNATURE(타입) + 패딩 + 값 |
Variant 중첩과 제한
# 복합 Variant 값 설정 (a{sv} 사전 안에 다양한 타입)
busctl call --user com.example.Config \
/com/example/Config com.example.Config SetOptions \
a{sv} 3 \
"timeout" v u 30 \
"name" v s "production" \
"tags" v as 2 "web" "api"
# timeout → UINT32(30), name → STRING, tags → STRING 배열
- 컨테이너 중첩(Nesting)은 최대 64레벨까지 허용됩니다 (예: 배열 안의 구조체 안의 배열... 64단계)
- 시그니처 문자열은 최대 255바이트까지입니다
- 빈 배열(
a{sv} 0)은 유효하지만, 빈 구조체(())는 허용되지 않습니다 - 사전(Dict)의 키는 반드시 기본 타입(Basic Type)이어야 합니다 — 배열이나 구조체는 키로 사용 불가
서비스 활성화(Service Activation)
D-Bus의 서비스 활성화(Service Activation)는 서비스가 실행되지 않은 상태에서 클라이언트가 해당 서비스를 호출하면 자동으로 서비스를 시작하는 메커니즘입니다.
서비스 파일(.service)
서비스 활성화를 위해서는 .service 파일이 필요합니다.
| 버스 종류 | 서비스 파일 경로 |
|---|---|
| 시스템 버스 | /usr/share/dbus-1/system-services/ |
| 세션 버스 | /usr/share/dbus-1/services/ |
# /usr/share/dbus-1/system-services/org.freedesktop.NetworkManager.service
[D-BUS Service]
Name=org.freedesktop.NetworkManager
Exec=/usr/sbin/NetworkManager --no-daemon
User=root
SystemdService=NetworkManager.service
서비스 파일 설명
- Name= 이 서비스가 소유할 잘 알려진 이름(Well-known Name)입니다.
- Exec= 서비스를 직접 실행할 때의 명령어입니다. dbus-daemon이 직접 exec합니다.
- User= 시스템 버스 서비스의 실행 사용자입니다.
- SystemdService= systemd 환경에서는 Exec 대신 이 systemd 유닛을 통해 서비스를 시작합니다. 리소스 제한과 로그 관리를 systemd에 위임할 수 있습니다.
활성화 시퀀스
클라이언트 입장에서는 서비스가 이미 실행 중이든 활성화를 통해 시작되든 동일하게 동작합니다. 단, 서비스 시작에 시간이 걸리므로 첫 호출의 응답 시간이 길어질 수 있습니다. busctl --auto-start=no로 자동 활성화를 비활성화할 수 있습니다.
systemd 유닛 타입: Type=dbus vs Type=notify
D-Bus 서비스의 systemd 유닛을 작성할 때, 준비 완료(Readiness) 신호 방식을 선택해야 합니다.
| 유닛 타입 | 준비 판단 기준 | 장점 | 사용 시점 |
|---|---|---|---|
| Type=dbus | 서비스가 BusName=에 지정된 이름을 D-Bus에 등록하면 준비 완료 | D-Bus 이름 등록과 systemd 준비가 자동으로 동기화 | D-Bus 이름 등록이 곧 서비스 준비를 의미하는 경우 |
| Type=notify | 서비스가 sd_notify("READY=1")을 호출하면 준비 완료 | 내부 초기화(DB 연결 등)가 완료된 후 명시적으로 알림 가능 | D-Bus 이름 등록 이후에도 추가 초기화가 필요한 경우 |
# Type=dbus 예시 — D-Bus 이름 등록 시점에 준비 완료
[Service]
Type=dbus
BusName=org.freedesktop.NetworkManager
ExecStart=/usr/sbin/NetworkManager --no-daemon
# Type=notify 예시 — sd_notify("READY=1") 호출 시점에 준비 완료
[Service]
Type=notify
ExecStart=/usr/lib/bluetooth/bluetoothd --nodetach
NotifyAccess=main
자동 시작 제어
D-Bus 서비스 파일에 SystemdService=가 있으면 dbus-daemon은 직접 프로세스를 시작하지 않고 systemd에 위임합니다. 이를 통해 LimitNOFILE=, MemoryMax= 같은 리소스 제한(Resource Limit)과 journalctl 로그 통합을 활용할 수 있습니다.
자동 활성화를 원하지 않는 서비스는 .service 파일을 제공하지 않거나, 클라이언트에서 NO_AUTO_START 플래그를 설정합니다.
보안 모델(Security Model)
D-Bus의 보안은 여러 계층(Layer)으로 구성되어, 각 단계에서 접근을 제어합니다.
D-Bus 정책 파일
시스템 버스의 정책 파일은 /usr/share/dbus-1/system.d/ 또는 /etc/dbus-1/system.d/에 위치합니다. 서비스별로 하나의 .conf 파일을 가집니다.
정책 규칙 속성
<allow>와 <deny> 요소에서 사용할 수 있는 속성입니다. 여러 속성을 조합하면 AND 조건으로 동작합니다.
| 속성 | 적용 대상 | 설명 |
|---|---|---|
send_destination | 송신 | 메시지를 보낼 목적지 버스 이름 |
send_interface | 송신 | 메시지의 인터페이스 (send_destination과 함께 사용 권장) |
send_member | 송신 | 특정 메서드/시그널 이름만 허용 |
send_type | 송신 | method_call, signal 등 메시지 타입 제한 |
send_path | 송신 | 특정 객체 경로로만 메시지 허용 |
receive_sender | 수신 | 특정 버스 이름에서 오는 메시지만 수신 허용 |
receive_interface | 수신 | 특정 인터페이스의 메시지만 수신 허용 |
own | 이름 등록 | 특정 잘 알려진 이름의 소유를 허용 |
own_prefix | 이름 등록 | 접두사가 일치하는 이름의 소유를 허용 (예: com.example) |
user | 정책 범위 | <policy user="root"> — 특정 사용자에게만 적용 |
group | 정책 범위 | <policy group="netdev"> — 특정 그룹에게만 적용 |
context | 정책 범위 | "default" (모든 연결) 또는 "mandatory" (최종 결정, 오버라이드 불가) |
<!-- /usr/share/dbus-1/system.d/org.freedesktop.NetworkManager.conf -->
<busconfig>
<!-- NetworkManager가 시스템 버스에서 이름을 소유하도록 허용 -->
<policy user="root">
<allow own="org.freedesktop.NetworkManager"/>
</policy>
<!-- 모든 사용자가 인트로스펙션과 프로퍼티 읽기 가능 -->
<policy context="default">
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Introspectable"/>
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.DBus.Properties"/>
</policy>
<!-- netdev 그룹 사용자만 설정 변경 가능 -->
<policy group="netdev">
<allow send_destination="org.freedesktop.NetworkManager"
send_interface="org.freedesktop.NetworkManager"/>
</policy>
</busconfig>
Polkit 통합
D-Bus 정책이 "이 사용자가 이 메서드를 호출할 수 있는가"를 결정하는 1차 관문이라면, Polkit은 "이 작업에 관리자 인증이 필요한가"를 결정하는 2차 관문입니다.
서비스가 Polkit을 사용하는 흐름:
- 클라이언트가 D-Bus 메서드를 호출합니다.
- 서비스가 메서드 핸들러(Handler)에서
polkit_authority_check_authorization()을 호출합니다. - Polkit이
.policy파일과.rules파일을 검사하여 허용/거부/인증 요구를 결정합니다. - 인증이 필요하면 Polkit 에이전트(Agent)가 사용자에게 비밀번호 입력 대화상자를 표시합니다.
- 인증 성공 시 서비스가 요청을 처리합니다.
# Polkit 액션 목록 확인
pkaction | grep NetworkManager
# 특정 액션의 상세 정보
pkaction --verbose --action-id org.freedesktop.NetworkManager.settings.modify.system
Polkit .policy 파일
.policy 파일은 /usr/share/polkit-1/actions/에 위치하며, 액션(Action)별 기본 권한을 정의합니다.
<!-- /usr/share/polkit-1/actions/org.freedesktop.NetworkManager.policy (요약) -->
<?xml version="1.0" encoding="UTF-8"?>
<policyconfig>
<action id="org.freedesktop.NetworkManager.settings.modify.system">
<description>시스템 네트워크 설정 변경</description>
<message>시스템 네트워크 설정을 변경하려면 인증이 필요합니다</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>auth_admin_keep</allow_active>
</defaults>
</action>
</policyconfig>
Polkit 권한 값 설명
- allow_any 원격 세션을 포함한 모든 클라이언트에 적용되는 기본값입니다.
- allow_inactive 로컬이지만 비활성(예: VT 전환된) 세션의 클라이언트에 적용됩니다.
- allow_active 현재 활성 로컬 세션의 클라이언트에 적용됩니다.
- auth_admin 관리자(Administrator) 비밀번호를 매번 입력해야 합니다.
- auth_admin_keep 관리자 비밀번호를 입력하면 일정 시간(기본 5분) 동안 재인증 없이 허용됩니다.
- yes 인증 없이 즉시 허용됩니다 (주의 필요).
Polkit .rules 파일
.rules 파일은 /etc/polkit-1/rules.d/에 JavaScript로 작성합니다. .policy의 기본값을 조건부로 오버라이드(Override)합니다.
// /etc/polkit-1/rules.d/10-networkmanager.rules
// wheel 그룹 사용자에게 NetworkManager 설정 변경을 비밀번호 없이 허용
polkit.addRule(function(action, subject) {
if (action.id === "org.freedesktop.NetworkManager.settings.modify.system" &&
subject.isInGroup("wheel")) {
return polkit.Result.YES;
}
});
강제 접근 제어(MAC) 통합
SELinux가 활성화된 시스템에서는 D-Bus 메시지 전송·수신에 대해 추가 보안 컨텍스트(Security Context) 검사가 이루어집니다. dbus { send_msg receive_msg } 권한 클래스로 제어합니다.
# SELinux D-Bus 거부 로그 확인
ausearch -m AVC --comm dbus-daemon --recent
# type=AVC msg=audit(...): avc: denied { send_msg } for
# scontext=system_u:system_r:httpd_t:s0
# tcontext=system_u:system_r:NetworkManager_t:s0
# tclass=dbus
AppArmor 환경(Ubuntu 등)에서는 D-Bus 프로파일(Profile)에서 dbus send와 dbus receive 규칙으로 특정 버스 이름·인터페이스·멤버에 대한 접근을 제어합니다.
# AppArmor D-Bus 프로파일 예시 (/etc/apparmor.d/usr.sbin.NetworkManager)
# 시스템 버스에서 이름 바인딩 허용
dbus bind bus=system name=org.freedesktop.NetworkManager,
# systemd로부터의 메서드 호출 수신 허용
dbus receive bus=system
peer=(label=/usr/lib/systemd/systemd),
# 모든 클라이언트에 시그널 송신 허용
dbus send bus=system
interface=org.freedesktop.NetworkManager
member={StateChanged,PropertiesChanged},
# Properties 인터페이스 읽기 허용
dbus send bus=system
interface=org.freedesktop.DBus.Properties
member={Get,GetAll},
흔한 D-Bus 보안 실수
| 실수 | 위험 | 올바른 방법 |
|---|---|---|
context="default"에서 send_interface 없이 send_destination만 허용 | 모든 인터페이스의 모든 메서드가 노출됨 | 반드시 send_interface를 함께 지정하여 노출 범위를 제한 |
own_prefix를 너무 넓게 설정 | 의도하지 않은 버스 이름을 등록할 수 있음 | own_prefix 대신 own으로 정확한 이름만 허용 |
Polkit에서 allow_active=yes 남용 | 로컬 로그인 사용자가 인증 없이 관리 작업 수행 가능 | auth_admin_keep을 사용하고, .rules로 특정 그룹만 허용 |
| UNIX_FD를 받은 후 유효성 검증 없이 사용 | 악의적 fd(예: /dev/kmem)를 전달받을 수 있음 | 받은 fd의 fstat() 결과를 검증하고, 예상 타입이 아니면 거부 |
| 시그널에 민감한 데이터 포함 | 시그널은 매칭 규칙만 있으면 누구나 수신 가능 | 민감한 데이터는 메서드 반환값으로만 전달 (Polkit 인증 후) |
send_destination을 거부하므로, 각 서비스의 .conf 파일에서 필요한 최소한의 접근만 명시적으로 허용해야 합니다.
커널과의 관계(Kernel Relationship)
D-Bus는 사용자 공간(Userspace) 프로토콜이지만, 그 기반은 커널이 제공하는 기능에 깊이 의존합니다.
AF_UNIX 소켓 전송
D-Bus의 기본 전송 계층(Transport Layer)은 AF_UNIX 소켓(SOCK_STREAM)입니다. 커널이 제공하는 다음 기능을 활용합니다:
| 커널 기능 | D-Bus 용도 |
|---|---|
| SCM_CREDENTIALS | 연결 시 상대방의 PID, UID, GID를 커널이 검증하여 전달. D-Bus 정책 검사의 기반 |
| SCM_RIGHTS | 유닉스 파일 디스크립터(Unix FD)를 프로세스 간 전달. D-Bus 타입 h(UNIX_FD)의 구현 기반 |
| 추상 소켓(Abstract Socket) | 파일 시스템에 존재하지 않는 소켓. 세션 버스에서 네임스페이스(Namespace) 격리에 활용 |
| SO_PEERCRED | 소켓 옵션으로 연결된 피어(Peer)의 자격 증명(Credentials)을 조회 |
SCM_CREDENTIALS 상세
D-Bus 데몬이 클라이언트를 식별하는 핵심 메커니즘은 SO_PEERCRED 소켓 옵션과 SCM_CREDENTIALS 보조 메시지(Ancillary Message)입니다.
/* SO_PEERCRED로 연결된 피어의 자격 증명 조회 */
#include <sys/socket.h>
struct ucred cred;
socklen_t len = sizeof(cred);
if (getsockopt(client_fd, SOL_SOCKET, SO_PEERCRED, &cred, &len) == 0) {
printf("PID=%d, UID=%d, GID=%d\n", cred.pid, cred.uid, cred.gid);
/* dbus-daemon은 이 정보로 정책의 user=, group= 규칙을 평가 */
}
/* SCM_RIGHTS로 파일 디스크립터 전달 (D-Bus UNIX_FD 타입의 기반) */
struct msghdr msg = { 0 };
struct cmsghdr *cmsg;
char cmsgbuf[CMSG_SPACE(sizeof(int))];
msg.msg_control = cmsgbuf;
msg.msg_controllen = sizeof(cmsgbuf);
cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*((int *)CMSG_DATA(cmsg)) = file_fd; /* 전달할 fd */
sendmsg(socket_fd, &msg, 0);
/* 수신 측에서 recvmsg()로 fd를 받음 — 커널이 fd 테이블 복제 수행 */
코드 설명
- SO_PEERCRED 커널이 소켓 연결 시점에 피어의 PID/UID/GID를 기록합니다. 이 값은 위조할 수 없으므로 신뢰할 수 있는 신원 확인(Identity Verification) 수단입니다.
- SCM_RIGHTS 프로세스의 파일 디스크립터를 다른 프로세스로 전달합니다. 커널이 수신 프로세스의 fd 테이블에 새 항목을 만들어 줍니다. D-Bus의
h타입이 이 메커니즘을 사용합니다.
strace로 D-Bus 통신 관찰
# dbus-daemon의 소켓 I/O 추적
$ strace -e trace=sendmsg,recvmsg -p $(pidof dbus-daemon) 2>&1 | head -8
recvmsg(12, {
msg_iov=[{iov_base="l\1\0\1"... ← 'l'=리틀엔디언, 1=METHOD_CALL
iov_len=184}],
msg_control=[{cmsg_level=SOL_SOCKET,
cmsg_type=SCM_CREDENTIALS, ← 커널이 자동 첨부
cmsg_data={pid=3847, uid=1000, gid=1000}}]
}, 0) = 184
sendmsg(15, { ← fd 15 = 목적지 서비스의 소켓
msg_iov=[{iov_base="l\1\0\1"...
iov_len=184}]
}, 0) = 184
cgroup 통합
dbus-broker는 커널의 cgroup(Control Group) 정보를 추가적인 발신자 식별 수단으로 활용합니다. /proc/PID/cgroup을 읽어 발신 프로세스가 속한 systemd 슬라이스(Slice)와 서비스 유닛(Unit)을 파악할 수 있으며, 이를 통해 PID보다 더 안정적인 프로세스 식별이 가능합니다. PID는 재사용(Reuse)될 수 있지만, cgroup 경로는 서비스 수명 동안 유일합니다.
kdbus 역사
kdbus는 D-Bus를 커널 내부에 구현하려는 시도였습니다. 2013년부터 2015년까지 Greg Kroah-Hartman이 주도하여 커널 메인라인(Mainline) 합류를 시도했으나, 여러 이유로 거부되었습니다:
- 보안 공격 표면(Attack Surface) 증가: IPC 프로토콜 전체를 커널에 넣으면 취약점 위험이 커짐
- 정책 로직의 커널 이동: D-Bus 정책은 복잡한 XML 규칙인데, 이를 커널에서 처리하는 것은 부적절
- 사용자 공간 대안의 충분한 성능: dbus-broker가 커널 기반 접근 없이도 충분한 성능을 달성
kdbus의 유산으로 Bus1 프로젝트가 탄생했고, 이는 범용 커널 IPC 기반 구조를 지향합니다. 하지만 역시 커널 메인라인에는 합류하지 못했고, dbus-broker의 사용자 공간 최적화가 실질적인 대안이 되었습니다.
컨테이너 환경의 D-Bus(D-Bus in Container Environments)
Docker, Podman, LXC 등 컨테이너 환경에서 D-Bus를 사용하려면 네임스페이스 격리, 소켓 경로, 자격 증명(Credential) 전달 등 여러 문제를 해결해야 합니다. 컨테이너가 호스트의 시스템 버스에 접근하는 방식과 내부에서 독립적인 D-Bus 데몬을 운영하는 방식은 보안과 격리 수준에서 큰 차이를 보입니다.
네임스페이스 격리(Namespace Isolation)
컨테이너(Container) 환경에서 D-Bus와 리눅스 네임스페이스(Namespace)의 관계를 이해하는 것은 올바른 구성의 전제 조건입니다.
- 마운트 네임스페이스(Mount Namespace): 시스템 버스 소켓(
/run/dbus/system_bus_socket)이 컨테이너 안에 보이지 않으면 시스템 버스에 접근할 수 없습니다. 바인드 마운트(Bind Mount)로 소켓을 공유하거나, 컨테이너 내부에서 독립 데몬을 실행해야 합니다. - PID 네임스페이스(PID Namespace):
SCM_CREDENTIALS로 전달되는 PID가 호스트와 컨테이너에서 다릅니다. 컨테이너 내부 PID 1인 프로세스가 호스트에서는 전혀 다른 PID를 가지므로, D-Bus 정책의 PID 기반 규칙이 예상과 다르게 동작합니다. - 사용자 네임스페이스(User Namespace): UID 매핑(Mapping)으로 인해 D-Bus 정책의 UID 기반 규칙이 예상과 다르게 동작할 수 있습니다. 컨테이너 내부 root(UID 0)가 호스트에서는 비특권(Unprivileged) UID로 매핑됩니다.
- 네트워크 네임스페이스(Network Namespace): 추상 소켓(Abstract Socket,
\0접두사)은 네트워크 네임스페이스에 바인딩됩니다. 별도 네트워크 네임스페이스를 사용하는 컨테이너에서는 호스트의 추상 소켓 기반 세션 버스에 접근할 수 없습니다. - Flatpak/Snap: 샌드박스(Sandbox) 내 애플리케이션을 위한 D-Bus 프록시(
xdg-dbus-proxy)를 사용하여 허용된 인터페이스만 노출합니다.
/run/dbus/system_bus_socket)을 사용하므로 마운트 네임스페이스만 신경 쓰면 됩니다. 반면 세션 버스가 추상 소켓을 사용하는 경우 네트워크 네임스페이스 격리도 장벽이 됩니다.
컨테이너에서 D-Bus가 필요한 이유
대부분의 단순 컨테이너(마이크로서비스, 웹 서버 등)는 D-Bus가 필요하지 않습니다. 그러나 다음과 같은 시나리오에서는 컨테이너 내부에서 D-Bus 통신이 필수적입니다.
| 시나리오 | D-Bus 의존 서비스 | 권장 패턴 |
|---|---|---|
| systemd 기반 컨테이너 (CI/CD, 인프라 테스트) | systemd, journald, logind | 내부 독립 데몬 |
| 네트워크 관리 컨테이너 | NetworkManager, firewalld, resolved | 내부 독립 데몬 |
| 호스트 서비스 모니터링/제어 | 호스트의 systemd1, UPower | 호스트 소켓 바인드 마운트 |
| 데스크톱 애플리케이션 컨테이너화 | 알림 데몬, IBus, PipeWire | 세션 버스 프록시 |
| 하드웨어 관리 | udisks2, BlueZ, UPower | 호스트 소켓 바인드 마운트 |
호스트 소켓 바인드 마운트(Host Socket Bind Mount)
가장 간단한 방법은 호스트의 시스템 버스 소켓을 컨테이너에 바인드 마운트하는 것입니다. 컨테이너 프로세스가 호스트의 dbus-daemon에 직접 연결됩니다.
# 호스트 시스템 버스 소켓을 컨테이너에 공유
docker run -it \
-v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket \
ubuntu:24.04 bash
# 컨테이너 내부에서 호스트의 시스템 버스에 접근
apt-get update && apt-get install -y dbus
busctl list --system # 호스트의 서비스 목록이 보임
busctl tree org.freedesktop.systemd1 # 호스트 systemd 객체 탐색
org.freedesktop.systemd1.Manager.StartUnit을 호출하여 호스트의 서비스를 시작/중지하거나, org.freedesktop.login1.Manager.PowerOff를 호출하여 호스트를 종료할 수 있습니다. 신뢰할 수 없는 컨테이너에는 절대 사용하지 마십시오.
PID/UID 불일치 문제: 바인드 마운트 환경에서 호스트의 dbus-daemon은 SO_PEERCRED로 연결 프로세스의 자격 증명을 확인합니다. PID 네임스페이스 사용 시 커널이 호스트 관점의 PID를 반환하므로 D-Bus 정책은 정상 동작하지만, GetConnectionUnixProcessID 같은 API로 조회한 PID는 컨테이너 내부에서 의미가 없습니다.
# 컨테이너 내부에서 자신의 D-Bus 연결 정보 확인
busctl --system call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus GetConnectionUnixProcessID s ":1.234"
# → 호스트 PID가 반환됨 (컨테이너 PID와 다름)
컨테이너 내부 독립 D-Bus 데몬
컨테이너가 자체적인 D-Bus 환경이 필요하면서 호스트와 격리되어야 하는 경우, 컨테이너 내부에서 독립적인 dbus-daemon을 실행합니다.
# Dockerfile: 독립 D-Bus 데몬이 있는 컨테이너
FROM ubuntu:24.04
RUN apt-get update && apt-get install -y \
dbus \
&& rm -rf /var/lib/apt/lists/*
# machine-id 생성 (D-Bus 필수)
RUN dbus-uuidgen --ensure=/etc/machine-id
# 시스템 버스 소켓 디렉터리 생성
RUN mkdir -p /run/dbus
# dbus-daemon을 포그라운드로 실행
CMD ["dbus-daemon", "--system", "--nofork", "--nopidfile"]
여러 프로세스가 필요한 경우 슈퍼바이저(Supervisor) 패턴을 사용합니다.
# entrypoint.sh: dbus-daemon을 백그라운드로 시작 후 주 프로세스 실행
#!/bin/bash
mkdir -p /run/dbus
dbus-uuidgen --ensure=/etc/machine-id
dbus-daemon --system --nofork &
# D-Bus 소켓이 준비될 때까지 대기
while [ ! -S /run/dbus/system_bus_socket ]; do sleep 0.1; done
# 주 애플리케이션 실행
exec "$@"
/etc/machine-id 파일을 요구합니다. 이 파일이 없으면 dbus-daemon이 시작되지 않습니다. dbus-uuidgen --ensure=/etc/machine-id로 생성하거나, /var/lib/dbus/machine-id에 심볼릭 링크(Symlink)를 설정합니다.
systemd 컨테이너와 D-Bus
systemd를 PID 1로 실행하는 컨테이너에서는 dbus.socket 유닛이 자동으로 활성화되어 dbus-daemon(또는 dbus-broker)을 소켓 활성화(Socket Activation) 방식으로 시작합니다.
# systemd를 PID 1로 실행하는 Docker 컨테이너
docker run -d --name systemd-container \
--privileged \
--cgroupns=host \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /run \
--tmpfs /run/lock \
ubuntu-systemd:24.04 /sbin/init
# 컨테이너 내부에서 D-Bus 서비스 확인
docker exec systemd-container busctl list --system
--privileged 플래그는 모든 Linux 기능(Capability)을 부여하므로 보안 위험이 큽니다. 최소 권한 원칙을 따르려면 필요한 기능만 명시적으로 추가합니다.
| 옵션 | 설명 | 보안 수준 |
|---|---|---|
--privileged | 모든 기능 부여, 장치 접근 허용 | 최저 (비권장) |
--cap-add SYS_ADMIN | cgroup/mount 조작 허용 | 낮음 |
--cap-add SYS_ADMIN --cap-add NET_ADMIN | systemd + 네트워크 서비스 | 낮음 |
--cap-add SYS_BOOT | reboot 시스콜 허용 (systemd 종료용) | 중간 |
# 최소 권한으로 systemd 컨테이너 실행 (cgroup v2)
docker run -d --name systemd-min \
--cap-add SYS_ADMIN \
--cap-add SYS_BOOT \
--cgroupns=host \
--security-opt seccomp=unconfined \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
--tmpfs /run \
--tmpfs /run/lock \
--stop-signal SIGRTMIN+3 \
ubuntu-systemd:24.04 /sbin/init
--cgroupns=host(또는 private + cgroup 위임)와 /sys/fs/cgroup 마운트가 필요합니다. cgroup이 올바르게 설정되지 않으면 systemd가 dbus.socket을 포함한 유닛 활성화에 실패합니다.
Podman과 Docker 비교
Podman은 데몬리스(Daemonless) 아키텍처와 루트리스(Rootless) 모드로 D-Bus 관련 동작이 Docker와 다릅니다.
| 기능 | Docker | Podman |
|---|---|---|
| systemd 컨테이너 | 수동 설정 필요 (--privileged, tmpfs 등) | --systemd=true 자동 구성 |
| 루트리스(Rootless) | rootless Docker 별도 설치 | 기본 지원 (사용자 네임스페이스) |
| cgroup 위임 | --cgroupns=host 수동 설정 | 자동 위임 (systemd --user) |
| D-Bus 소켓 기본 동작 | 명시적 바인드 마운트 필요 | --systemd=true 시 자동 구성 |
| 컨테이너 관리 유닛 | 별도 systemd 유닛 작성 | podman generate systemd |
| 세션 버스 접근 | 수동 마운트 ($XDG_RUNTIME_DIR/bus) | --systemd=true 시 자동 |
# Podman: systemd 컨테이너 (자동 D-Bus 구성)
podman run -d --name systemd-pod \
--systemd=true \
ubuntu-systemd:24.04 /sbin/init
# Podman이 자동으로 처리하는 항목:
# - /run을 tmpfs로 마운트
# - /sys/fs/cgroup 적절한 마운트
# - SIGRTMIN+3 정지 시그널 설정
# - 환경 변수 container=podman 설정
# Podman: 호스트 세션 버스를 루트리스 컨테이너에 공유
podman run -it \
-v $XDG_RUNTIME_DIR/bus:$XDG_RUNTIME_DIR/bus \
-e DBUS_SESSION_BUS_ADDRESS="unix:path=$XDG_RUNTIME_DIR/bus" \
ubuntu:24.04 bash
podman generate systemd --name systemd-pod 명령으로 컨테이너를 관리하는 systemd 유닛 파일을 자동 생성할 수 있습니다. 이 유닛은 호스트의 systemd/D-Bus를 통해 컨테이너 수명 주기를 관리합니다.
컨테이너 D-Bus 보안(Container D-Bus Security)
호스트 시스템 버스를 컨테이너에 노출하면 공격 표면(Attack Surface)이 크게 확장됩니다. 주요 위험과 완화 방법을 정리합니다.
호스트 버스 노출 시 주요 위험:
org.freedesktop.systemd1.Manager.StartUnit— 호스트의 임의 서비스 시작/중지org.freedesktop.login1.Manager.PowerOff— 호스트 전원 종료org.freedesktop.NetworkManager.Settings.AddConnection— 네트워크 설정 변경org.freedesktop.PolicyKit1.Authority.CheckAuthorization— Polkit 우회 시도
완화 전략 1: D-Bus 정책 파일로 접근 제한
<!-- /etc/dbus-1/system.d/container-restrict.conf -->
<!-- 특정 UID(컨테이너 프로세스)의 접근을 제한 -->
<busconfig>
<policy user="container-user">
<!-- 기본 거부 -->
<deny send_destination="*"/>
<!-- 특정 인터페이스만 허용 -->
<allow send_destination="org.freedesktop.hostname1"/>
<allow send_destination="org.freedesktop.timedate1"/>
</policy>
</busconfig>
완화 전략 2: xdg-dbus-proxy 필터링
# xdg-dbus-proxy로 허용된 인터페이스만 노출
xdg-dbus-proxy \
unix:path=/run/dbus/system_bus_socket \
/run/container-dbus-proxy \
--filter \
--talk=org.freedesktop.hostname1 \
--talk=org.freedesktop.timedate1 \
--call=org.freedesktop.DBus=Hello \
--call=org.freedesktop.DBus=AddMatch
# 컨테이너는 프록시 소켓을 사용
docker run -it \
-v /run/container-dbus-proxy:/run/dbus/system_bus_socket \
ubuntu:24.04 bash
완화 전략 3: AppArmor/SELinux 프로파일
# AppArmor: D-Bus 접근 제한 프로파일
profile container-dbus flags=(attach_disconnected) {
# D-Bus 시스템 버스 소켓 접근 허용
/run/dbus/system_bus_socket rw,
# 특정 D-Bus 대상만 허용
dbus (send) bus=system peer=(name=org.freedesktop.hostname1),
dbus (receive) bus=system peer=(name=org.freedesktop.hostname1),
# 나머지 D-Bus 통신 거부
deny dbus bus=system,
}
# SELinux: 컨테이너의 D-Bus 소켓 접근 제어
# container_t 도메인에서 system_dbusd_t로의 통신 허용
allow container_t system_dbusd_t:unix_stream_socket connectto;
allow container_t system_dbusd_var_run_t:sock_file write;
xdg-dbus-proxy를 중간에 배치하여 필요한 인터페이스만 화이트리스트(Whitelist) 방식으로 허용하는 것이 가장 안전합니다. 독립 데몬 패턴은 격리 수준이 가장 높지만, 호스트 서비스에 접근해야 하는 경우에는 사용할 수 없습니다.
주요 D-Bus 서비스(Major D-Bus Services)
현대 리눅스 시스템에서 시스템 버스를 사용하는 주요 서비스들입니다.
| 서비스 | 버스 이름 | 주 객체 경로 | 용도 |
|---|---|---|---|
| systemd | org.freedesktop.systemd1 | /org/freedesktop/systemd1 | 유닛 관리(시작/중지/재시작), 상태 조회 |
| logind | org.freedesktop.login1 | /org/freedesktop/login1 | 세션·시트(Seat)·사용자 관리, 전원 제어 |
| NetworkManager | org.freedesktop.NetworkManager | /org/freedesktop/NetworkManager | 네트워크 연결 관리, Wi-Fi, VPN |
| BlueZ | org.bluez | /org/bluez | 블루투스(Bluetooth) 어댑터·장치 관리 |
| udisks2 | org.freedesktop.UDisks2 | /org/freedesktop/UDisks2 | 디스크·파티션·파일 시스템 관리 |
| UPower | org.freedesktop.UPower | /org/freedesktop/UPower | 전원 장치(배터리) 상태 모니터링 |
| resolved | org.freedesktop.resolve1 | /org/freedesktop/resolve1 | DNS 이름 해석(Resolution) 관리 |
| timesyncd | org.freedesktop.timesync1 | /org/freedesktop/timesync1 | NTP 시간 동기화 |
| firewalld | org.fedoraproject.FirewallD1 | /org/fedoraproject/FirewallD1 | 방화벽 존·규칙 관리 (상세) |
| PipeWire | org.freedesktop.impl.portal.ScreenCast | — | 오디오·비디오 라우팅 (Portal 인터페이스) |
# 시스템 버스의 모든 서비스 목록
busctl list --system
# 특정 서비스의 객체 트리
busctl tree org.freedesktop.systemd1
# systemd 버전 프로퍼티 조회
busctl get-property org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager Version
# NetworkManager 상태 조회
busctl call org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.DBus.Properties Get ss \
org.freedesktop.NetworkManager State
D-Bus 도구(Tools)
도구 비교
| 도구 | 패키지 | 특징 | 권장 용도 |
|---|---|---|---|
| busctl | systemd | 가장 현대적. 트리 탐색, 인트로스펙션, 호출, 모니터링, pcap 캡처 지원 | 일상 디버깅, 시스템 관리 (권장) |
| dbus-send | dbus | 경량. 시스템/세션 버스 호출. 출력이 간결 | 셸 스크립트, 간단한 호출 |
| dbus-monitor | dbus | 실시간 메시지 모니터링. 필터 표현식 지원 | 메시지 흐름 디버깅 |
| gdbus | glib2 | GLib 기반. 인트로스펙션, 호출, 모니터링 | GNOME/GTK 환경 디버깅 |
| D-Feet | d-feet | GUI 브라우저. 서비스/객체/인터페이스 탐색 | 시각적 API 탐색 |
busctl 사용법
# 시스템 버스의 모든 서비스 나열
busctl list --system
# 서비스의 객체 트리 표시
busctl tree org.freedesktop.systemd1
# 특정 객체의 인터페이스·메서드·프로퍼티 조회
busctl introspect org.freedesktop.systemd1 /org/freedesktop/systemd1
# 메서드 호출 (인자 타입과 값 전달)
busctl call org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager \
StartUnit ss "sshd.service" "replace"
# 프로퍼티 읽기
busctl get-property org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager Version
# 실시간 모니터링
busctl monitor org.freedesktop.NetworkManager
# pcap 형식으로 캡처 (Wireshark로 분석 가능)
busctl capture org.freedesktop.systemd1 > dbus-capture.pcap
dbus-send 사용법
# systemd에서 유닛 시작
dbus-send --system --print-reply --dest=org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager.StartUnit \
string:"sshd.service" string:"replace"
# 프로퍼티 읽기
dbus-send --system --print-reply --dest=org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.DBus.Properties.Get \
string:"org.freedesktop.systemd1.Manager" string:"Version"
# 세션 버스에서 데스크톱 알림 보내기
dbus-send --session --print-reply --dest=org.freedesktop.Notifications \
/org/freedesktop/Notifications \
org.freedesktop.Notifications.Notify \
string:"test" uint32:0 string:"" string:"D-Bus 테스트" \
string:"이것은 D-Bus 알림입니다" array:string:"" dict:string:string:"" int32:5000
dbus-monitor 사용법
# 시스템 버스의 모든 메시지 모니터링
dbus-monitor --system
# 특정 인터페이스의 시그널만 필터링
dbus-monitor --system "type='signal',interface='org.freedesktop.NetworkManager'"
# 특정 서비스로 향하는 메서드 호출만 필터링
dbus-monitor --system "type='method_call',destination='org.freedesktop.systemd1'"
# PropertiesChanged 시그널 모니터링
dbus-monitor --system "type='signal',member='PropertiesChanged'"
프로그래밍 인터페이스(Programming Interfaces)
D-Bus 프로그래밍 라이브러리는 여러 가지가 있으며, 용도에 따라 선택합니다.
| 라이브러리 | 언어 | 특징 | 권장 대상 |
|---|---|---|---|
| sd-bus | C | systemd 내장. 현대적 API, 비동기 지원, 커널 크레덴셜 검증 통합 | 시스템 서비스, 데몬 |
| GDBus (GIO) | C | GLib/GObject 기반. 코드 생성기(gdbus-codegen) 지원 | GNOME/GTK 애플리케이션 |
| dbus-python | Python | libdbus 래핑(Wrapping). GLib 메인 루프 필요 | 프로토타이핑, 스크립트 |
| dasbus | Python | GDBus 기반 현대적 Python 바인딩 | Python 3 애플리케이션 |
| libdbus | C | 원래의 참조 구현. 저수준(Low-level), 직접 사용 비권장 | 다른 바인딩의 기반 |
sd-bus 예제: 클라이언트(메서드 호출)
#include <stdio.h>
#include <systemd/sd-bus.h>
int main(void) {
sd_bus *bus = NULL;
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *reply = NULL;
const char *version;
int r;
/* 시스템 버스에 연결 */
r = sd_bus_open_system(&bus);
if (r < 0) {
fprintf(stderr, "버스 연결 실패: %s\n", strerror(-r));
return 1;
}
/* systemd Manager의 Version 프로퍼티 읽기 */
r = sd_bus_get_property(
bus,
"org.freedesktop.systemd1", /* 버스 이름 */
"/org/freedesktop/systemd1", /* 객체 경로 */
"org.freedesktop.systemd1.Manager", /* 인터페이스 */
"Version", /* 프로퍼티 이름 */
&error,
&reply,
"s" /* 프로퍼티 타입 */
);
if (r < 0) {
fprintf(stderr, "프로퍼티 읽기 실패: %s\n", error.message);
goto finish;
}
r = sd_bus_message_read(reply, "s", &version);
if (r < 0) {
fprintf(stderr, "메시지 파싱 실패: %s\n", strerror(-r));
goto finish;
}
printf("systemd version: %s\n", version);
finish:
sd_bus_error_free(&error);
sd_bus_message_unref(reply);
sd_bus_unref(bus);
return r < 0 ? 1 : 0;
}
코드 설명
- 12행
sd_bus_open_system()은 시스템 버스(/run/dbus/system_bus_socket)에 연결합니다. 세션 버스는sd_bus_open_user()를 사용합니다. - 19-30행
sd_bus_get_property()는 내부적으로org.freedesktop.DBus.Properties.Get메서드를 호출합니다. 마지막 인자"s"는 반환값이 문자열임을 나타냅니다. - 36행
sd_bus_message_read()로 응답 메시지의 본문에서 데이터를 추출합니다. 시그니처"s"에 맞춰 문자열 포인터가 설정됩니다. - 43-45행 sd-bus는 수동 리소스 해제가 필요합니다.
__attribute__((cleanup))을 활용한 자동 해제 매크로도 제공됩니다.
# 컴파일
gcc -o dbus-version dbus-version.c $(pkg-config --cflags --libs libsystemd)
sd-bus 예제: 서비스(메서드 노출)
#include <stdio.h>
#include <stdlib.h>
#include <systemd/sd-bus.h>
/* 메서드 핸들러: Greeting 메서드 */
static int method_greeting(
sd_bus_message *msg,
void *userdata,
sd_bus_error *ret_error) {
const char *name;
int r;
r = sd_bus_message_read(msg, "s", &name);
if (r < 0)
return r;
char buf[256];
snprintf(buf, sizeof(buf), "안녕하세요, %s!", name);
return sd_bus_reply_method_return(msg, "s", buf);
}
/* 인터페이스 vtable 정의 */
static const sd_bus_vtable example_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Greeting", "s", "s", method_greeting,
SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_VTABLE_END,
};
int main(void) {
sd_bus *bus = NULL;
sd_bus_slot *slot = NULL;
int r;
r = sd_bus_open_user(&bus);
if (r < 0) return 1;
/* vtable을 객체 경로에 등록 */
r = sd_bus_add_object_vtable(
bus, &slot,
"/com/example/Greeter", /* 객체 경로 */
"com.example.Greeter", /* 인터페이스 이름 */
example_vtable,
NULL
);
if (r < 0) return 1;
/* 잘 알려진 이름 요청 */
r = sd_bus_request_name(bus, "com.example.Greeter", 0);
if (r < 0) return 1;
/* 이벤트 루프 */
for (;;) {
r = sd_bus_process(bus, NULL);
if (r < 0) break;
if (r > 0) continue;
r = sd_bus_wait(bus, (uint64_t)-1);
if (r < 0) break;
}
sd_bus_slot_unref(slot);
sd_bus_unref(bus);
return 0;
}
코드 설명
- 6-21행
method_greeting은 D-Bus 메서드 호출을 처리하는 콜백(Callback)입니다. 입력으로 문자열("s")을 받고, 문자열("s")을 반환합니다. - 24-29행
sd_bus_vtable은 인터페이스의 메서드·프로퍼티·시그널을 선언적으로 정의합니다.SD_BUS_VTABLE_UNPRIVILEGED는 누구나 호출 가능하다는 의미입니다. - 40-47행
sd_bus_add_object_vtable()로 vtable을 특정 객체 경로와 인터페이스에 연결합니다. 해당 경로로 메서드 호출이 오면 자동으로 매칭됩니다. - 50행
sd_bus_request_name()으로 잘 알려진 이름을 등록합니다. 이후 다른 프로세스가 이 이름으로 메서드를 호출할 수 있습니다. - 53-59행
sd_bus_process()로 대기 중인 메시지를 처리하고,sd_bus_wait()로 새 메시지를 기다립니다. 실전에서는sd-event이벤트 루프와 통합합니다.
# 서비스를 빌드하고 실행
gcc -o greeter-service greeter-service.c $(pkg-config --cflags --libs libsystemd)
./greeter-service &
# 다른 터미널에서 호출
busctl --user call com.example.Greeter \
/com/example/Greeter \
com.example.Greeter \
Greeting s "세계"
# 출력: s "안녕하세요, 세계!"
Python 예제
import dbus
# 시스템 버스에 연결
bus = dbus.SystemBus()
# NetworkManager 프록시 객체 획득
nm_proxy = bus.get_object(
'org.freedesktop.NetworkManager',
'/org/freedesktop/NetworkManager'
)
# 인터페이스를 통한 메서드 호출
nm_iface = dbus.Interface(nm_proxy, 'org.freedesktop.NetworkManager')
devices = nm_iface.GetDevices()
# Properties 인터페이스로 프로퍼티 읽기
props_iface = dbus.Interface(nm_proxy, 'org.freedesktop.DBus.Properties')
state = props_iface.Get('org.freedesktop.NetworkManager', 'State')
print(f"NetworkManager 상태: {state}")
print(f"장치 수: {len(devices)}")
for dev_path in devices:
dev_proxy = bus.get_object('org.freedesktop.NetworkManager', dev_path)
dev_props = dbus.Interface(dev_proxy, 'org.freedesktop.DBus.Properties')
iface_name = dev_props.Get('org.freedesktop.NetworkManager.Device', 'Interface')
print(f" 장치: {iface_name} ({dev_path})")
# 시그널 구독 예제 (GLib 이벤트 루프 필요)
import dbus
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
DBusGMainLoop(set_as_default=True)
bus = dbus.SystemBus()
def on_properties_changed(interface, changed, invalidated):
print(f"인터페이스: {interface}")
for key, value in changed.items():
print(f" 변경: {key} = {value}")
bus.add_signal_receiver(
on_properties_changed,
signal_name='PropertiesChanged',
dbus_interface='org.freedesktop.DBus.Properties',
bus_name='org.freedesktop.NetworkManager'
)
loop = GLib.MainLoop()
print("프로퍼티 변경 모니터링 중... (Ctrl+C로 종료)")
loop.run()
성능과 대안(Performance & Alternatives)
D-Bus 오버헤드 분석
D-Bus의 성능 오버헤드(Overhead)는 다음 요소에서 발생합니다:
- 직렬화/역직렬화(Serialization): 메시지 본문을 D-Bus 와이어 포맷(Wire Format)으로 변환. 데이터 크기에 비례
- 컨텍스트 스위치(Context Switch): 클라이언트 → 커널 → 데몬 → 커널 → 서비스 경로에서 최소 4회 컨텍스트 스위치 발생
- 데몬 홉(Daemon Hop): 모든 메시지가 버스 데몬을 거쳐야 하므로, 직접 소켓 통신 대비 지연(Latency) 증가
- 정책 검사: 매 메시지마다 접근 제어 규칙을 평가
메시지 지연 벤치마크
다음은 D-Bus 메서드 호출의 왕복 지연(Round-trip Latency) 근사치입니다. 실제 값은 하드웨어, 커널 버전, 부하(Load)에 따라 다릅니다.
| 시나리오 | dbus-daemon | dbus-broker | 직접 Unix 소켓 |
|---|---|---|---|
| 빈 메서드 호출 (인자 없음) | ~50-80 µs | ~20-35 µs | ~5-10 µs |
| 1 KB 페이로드(Payload) | ~60-100 µs | ~25-45 µs | ~8-15 µs |
| 64 KB 페이로드 | ~200-400 µs | ~80-150 µs | ~30-60 µs |
| 시그널 브로드캐스트 (10개 구독자) | ~200-500 µs | ~50-100 µs | — (직접 구현 필요) |
| 프로퍼티 Get (단일 값) | ~50-80 µs | ~20-35 µs | — (프로토콜 없음) |
dbus-daemon vs dbus-broker 성능
| 항목 | dbus-daemon | dbus-broker |
|---|---|---|
| 메시지 처리량 | 기준 | ~3-10배 향상 |
| 지연 시간 | 기준 | ~50% 감소 |
| 메모리 사용 | 기준 | 유사하거나 약간 적음 |
| 활성화 방식 | 자체 fork/exec | systemd에 위임 |
| 정책 파싱 | 런타임 XML 파싱 | 사전 컴파일된 정책 |
| 매칭 규칙 처리 | 선형 검색(Linear Scan) | 인덱싱(Indexing) 기반 — 구독자 수에 무관한 성능 |
| 메시지 정렬 | 글로벌 순서 보장 | 글로벌 순서 보장 + 커널 타임스탬프 활용 |
메시지 크기별 처리량
| 메시지 본문 크기 | dbus-daemon (msg/sec) | dbus-broker (msg/sec) | 주요 병목 |
|---|---|---|---|
| 0 B (빈 메서드) | ~10,000-15,000 | ~50,000-80,000 | 컨텍스트 스위치, 정책 평가 |
| 64 B | ~9,000-14,000 | ~45,000-70,000 | 컨텍스트 스위치 |
| 1 KB | ~7,000-10,000 | ~30,000-50,000 | 직렬화 + 소켓 버퍼 복사 |
| 16 KB | ~2,000-4,000 | ~10,000-20,000 | 메모리 복사 지배적 |
| 64 KB | ~500-1,000 | ~3,000-5,000 | 소켓 버퍼 크기 제한 |
| 256 KB | ~150-300 | ~800-1,500 | 메시지 분할 + 재조립(Fragmentation) |
UNIX_FD를 이용한 대용량 데이터 전달
대용량 데이터를 D-Bus 메시지 본문에 직렬화하면 성능이 급격히 저하됩니다. 대신 memfd_create()로 공유 메모리(Shared Memory)를 만들고, 그 파일 디스크립터(UNIX_FD)를 D-Bus로 전달하면 제로카피(Zero-copy)에 가까운 성능을 얻을 수 있습니다.
# 패턴: 대용량 데이터를 fd로 전달하는 서비스 설계
# 1. 서비스가 memfd_create()로 익명 파일 생성
# 2. 데이터를 memfd에 write()
# 3. fd를 D-Bus METHOD_RETURN의 'h' 타입으로 반환
# 4. 클라이언트가 fd를 받아 mmap()으로 직접 접근
# 실제 예: PipeWire는 오디오/비디오 버퍼를 memfd+fd-passing으로 전달
# 실제 예: Flatpak Portal은 파일 선택 결과를 fd로 전달
비동기 배치 호출
sd_bus_call_async()를 사용하면 여러 메서드 호출을 응답을 기다리지 않고 연속으로 전송할 수 있습니다. 응답은 이벤트 루프(Event Loop)에서 콜백(Callback)으로 처리됩니다. 이 방식은 단일 호출의 지연 시간을 줄이지는 않지만, 여러 호출의 총 소요 시간을 크게 단축합니다.
D-Bus 대안 기술
| 기술 | 특징 | D-Bus 대비 장점 | D-Bus 대비 단점 |
|---|---|---|---|
| Varlink | JSON 기반 IPC, Unix 소켓 직접 연결 | 데몬 없음(오버헤드 ↓), 간단한 프로토콜 | 서비스 발견·활성화 없음 |
| gRPC | Protocol Buffers, HTTP/2 기반 | 고성능, 코드 생성, 언어 중립 | 시스템 서비스 통합 미흡, 무거움 |
| 직접 Unix 소켓 | 커스텀 프로토콜 | 최소 오버헤드, 최대 유연성 | 서비스 발견·보안·인트로스펙션 직접 구현 |
| Binder | Android IPC, 커널 드라이버 기반 | 제로카피, 낮은 지연 | Linux 데스크톱/서버 미지원 |
| AF_VSOCK | 호스트-게스트 IPC | VM 경계를 넘는 통신 | 범용 IPC 용도가 아님 |
디버깅(Debugging)
자주 발생하는 오류
| 오류 메시지 | 원인 | 해결 방법 |
|---|---|---|
org.freedesktop.DBus.Error.ServiceUnknown | 요청한 버스 이름을 소유한 서비스가 없음 | 서비스 실행 여부 확인, .service 파일 존재 확인 |
org.freedesktop.DBus.Error.AccessDenied | D-Bus 정책에 의한 거부 | 해당 서비스의 .conf 정책 파일 확인 |
org.freedesktop.DBus.Error.UnknownMethod | 존재하지 않는 메서드 호출 | busctl introspect로 실제 메서드 이름 확인 |
org.freedesktop.DBus.Error.InvalidArgs | 메서드 인자의 타입이나 개수가 잘못됨 | 인트로스펙션으로 정확한 시그니처 확인 |
org.freedesktop.DBus.Error.TimedOut | 서비스가 응답하지 않음 (기본 25초) | 서비스 상태 확인, journalctl -u 서비스로 로그 확인 |
org.freedesktop.DBus.Error.NoReply | 서비스가 응답 전에 종료됨 | 서비스 크래시 여부 확인, coredump 분석 |
디버깅 기법
# 1. 서비스 존재 여부 확인
busctl list --system | grep NetworkManager
# 2. 서비스의 프로세스 정보 확인
busctl status org.freedesktop.NetworkManager
# 3. 실시간 메시지 추적
busctl monitor org.freedesktop.NetworkManager
# 4. D-Bus 데몬 자체의 통계
busctl call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus.Debug.Stats GetStats
# 5. journalctl로 D-Bus 관련 로그 확인
journalctl -u dbus.service --since "5 minutes ago"
# 6. strace로 소켓 통신 추적
strace -e trace=sendmsg,recvmsg -p $(pidof dbus-daemon) 2>&1 | head -50
# 7. dbus-daemon 상세 로그 활성화
# /usr/share/dbus-1/system.conf에서 syslog="true" 설정 후:
DBUS_VERBOSE=1 dbus-daemon --system --nofork
디버깅용 환경 변수
| 환경 변수 | 효과 | 예시 |
|---|---|---|
DBUS_VERBOSE=1 | dbus-daemon의 상세 디버그 로그 출력 | DBUS_VERBOSE=1 dbus-daemon --session --nofork |
DBUS_SESSION_BUS_ADDRESS | 세션 버스 소켓 주소를 명시적으로 지정 | unix:path=/run/user/1000/bus |
DBUS_SYSTEM_BUS_ADDRESS | 시스템 버스 소켓 주소를 명시적으로 지정 | unix:path=/run/dbus/system_bus_socket |
G_DBUS_DEBUG=all | GLib/GDBus 라이브러리의 디버그 로그 출력 | G_DBUS_DEBUG=message,payload,address |
SYSTEMD_LOG_LEVEL=debug | sd-bus 관련 systemd 라이브러리의 디버그 로그 | SYSTEMD_LOG_LEVEL=debug busctl call ... |
실전 트러블슈팅 시나리오
시나리오 1: 서비스가 시작되지 않음 (ServiceUnknown)
# 1단계: 서비스가 버스에 등록되어 있는지 확인
busctl list --system | grep "원하는.서비스.이름"
# 2단계: D-Bus .service 파일이 존재하는지 확인
ls /usr/share/dbus-1/system-services/ | grep "원하는.서비스"
# 3단계: systemd 유닛 상태 확인
systemctl status 해당-서비스.service
journalctl -u 해당-서비스.service --since "5 min ago" --no-pager
# 4단계: 수동으로 서비스 시작 시도
systemctl start 해당-서비스.service
# 실패하면 journalctl에서 원인 확인
시나리오 2: 권한 거부 (AccessDenied)
# 1단계: 호출자의 UID/GID 확인
id
# 2단계: 해당 서비스의 D-Bus 정책 파일 찾기
ls /usr/share/dbus-1/system.d/ /etc/dbus-1/system.d/ | grep "서비스이름"
# 3단계: 정책 파일에서 허용 규칙 확인
grep -A5 'send_destination.*서비스이름' /usr/share/dbus-1/system.d/서비스이름.conf
# 4단계: SELinux AVC 거부 여부 확인 (RHEL/Fedora)
ausearch -m AVC --recent | grep dbus
# 5단계: AppArmor 거부 여부 확인 (Ubuntu/SUSE)
journalctl -k | grep DENIED | grep dbus
시나리오 3: 메서드 호출 타임아웃 (TimedOut)
# 1단계: 서비스 프로세스가 살아있는지 확인
busctl status org.서비스.이름
# 2단계: 서비스의 CPU/메모리 상태 확인 (이벤트 루프 블로킹?)
top -p $(busctl status org.서비스.이름 | grep PID | awk '{print $NF}')
# 3단계: 더 긴 타임아웃으로 재시도
busctl call --timeout=60 org.서비스.이름 /경로 인터페이스 메서드
# 4단계: 서비스 로그에서 블로킹 원인 확인
journalctl -u 서비스.service --since "2 min ago" | tail -30
journalctl D-Bus 필터링 패턴
# dbus-daemon 로그
journalctl -u dbus.service --since "10 min ago"
# dbus-broker 로그 (Fedora/RHEL 8+)
journalctl -u dbus-broker.service --since "10 min ago"
# 특정 서비스의 D-Bus 관련 로그만 필터링
journalctl -u NetworkManager --grep "dbus\|DBus\|bus" --since "10 min ago"
# 커널 감사(Audit) 로그에서 D-Bus 관련 항목
journalctl -k --grep "dbus\|avc.*dbus" --since "10 min ago"
busctl call --expect-reply=no: 응답을 기다리지 않는 fire-and-forget 호출로, 서비스의 메서드 수신 여부만 확인할 때 유용합니다.busctl call --timeout=N: 타임아웃을 초 단위로 조절합니다. 기본값은 25초입니다.busctl --uservs--system: 세션 버스와 시스템 버스를 명시적으로 구분하여 테스트합니다. 잘못된 버스에 연결하는 실수가 의외로 잦습니다.
Wireshark를 이용한 D-Bus 분석
busctl capture로 생성한 pcap 파일을 Wireshark에서 분석할 수 있습니다. Wireshark는 D-Bus 프로토콜 디섹터(Dissector)를 내장하고 있어, 메시지 헤더와 본문을 구조적으로 볼 수 있습니다.
# 시스템 버스의 특정 서비스 메시지를 캡처
busctl capture --system org.freedesktop.NetworkManager > nm-dbus.pcap
# Wireshark로 열기
wireshark nm-dbus.pcap
D-Bus 프로토콜 상세(Protocol Details)
D-Bus 프로토콜은 바이너리 메시지(Binary Message) 형식으로 정의되며, 각 메시지는 고정 헤더(Fixed Header), 헤더 필드 배열(Header Fields Array), 본문(Body)으로 구성됩니다.
메시지 고정 헤더
모든 D-Bus 메시지는 12바이트 또는 16바이트의 고정 헤더로 시작합니다.
| 오프셋 | 크기 | 필드 | 설명 |
|---|---|---|---|
| 0 | 1 | Endianness | 'l' = 리틀엔디언, 'B' = 빅엔디언 |
| 1 | 1 | Message Type | 1=METHOD_CALL, 2=METHOD_RETURN, 3=ERROR, 4=SIGNAL |
| 2 | 1 | Flags | 비트 0: NO_REPLY_EXPECTED, 비트 1: NO_AUTO_START |
| 3 | 1 | Protocol Version | 현재 항상 1 |
| 4 | 4 | Body Length | 본문의 바이트 길이 (0이면 본문 없음) |
| 8 | 4 | Serial | 발신자가 부여한 메시지 고유 번호 (응답 매칭에 사용) |
헤더 필드 상세
고정 헤더 뒤에 가변 길이 헤더 필드 배열이 따릅니다. 각 필드는 (코드, VARIANT) 형태입니다.
| 코드 | 필드 이름 | 필수 여부 | 타입 | 설명 |
|---|---|---|---|---|
| 1 | PATH | METHOD_CALL, SIGNAL | OBJECT_PATH | 목적지 객체 경로 (/org/freedesktop/NetworkManager) |
| 2 | INTERFACE | SIGNAL (METHOD_CALL 권장) | STRING | 인터페이스 이름 (org.freedesktop.DBus.Properties) |
| 3 | MEMBER | METHOD_CALL, SIGNAL | STRING | 메서드/시그널 이름 (Get, PropertiesChanged) |
| 4 | ERROR_NAME | ERROR | STRING | 오류 이름 (org.freedesktop.DBus.Error.ServiceUnknown) |
| 5 | REPLY_SERIAL | METHOD_RETURN, ERROR | UINT32 | 원본 METHOD_CALL의 Serial 번호 |
| 6 | DESTINATION | 선택 | STRING | 수신자 버스 이름 (유니크 또는 잘 알려진) |
| 7 | SENDER | 데몬이 추가 | STRING | 발신자 유니크 이름 (데몬이 자동 설정) |
| 8 | SIGNATURE | 본문이 있을 때 | SIGNATURE | 본문의 타입 시그니처 ("sa{sv}") |
| 9 | UNIX_FDS | FD 전달 시 | UINT32 | SCM_RIGHTS로 전달된 파일 디스크립터 수 |
메시지 타입별 동작
D-Bus 타입 시스템 상세(Type System Details)
D-Bus의 타입 시스템은 시그니처 문자열(Signature String)로 표현되며, 기본 타입(Basic Type)과 컨테이너 타입(Container Type)으로 구분됩니다.
기본 타입 상세
| 시그니처 | 타입 이름 | 크기(바이트) | 정렬 | 설명 | C 대응 타입 |
|---|---|---|---|---|---|
y | BYTE | 1 | 1 | 부호 없는 8비트 정수 | uint8_t |
b | BOOLEAN | 4 | 4 | 0=FALSE, 1=TRUE (UINT32로 마샬링) | int |
n | INT16 | 2 | 2 | 부호 있는 16비트 정수 | int16_t |
q | UINT16 | 2 | 2 | 부호 없는 16비트 정수 | uint16_t |
i | INT32 | 4 | 4 | 부호 있는 32비트 정수 | int32_t |
u | UINT32 | 4 | 4 | 부호 없는 32비트 정수 | uint32_t |
x | INT64 | 8 | 8 | 부호 있는 64비트 정수 | int64_t |
t | UINT64 | 8 | 8 | 부호 없는 64비트 정수 | uint64_t |
d | DOUBLE | 8 | 8 | IEEE 754 배정밀도 부동소수점 | double |
s | STRING | 가변 | 4 | UTF-8 문자열 (길이 접두사 + NUL 종료) | const char * |
o | OBJECT_PATH | 가변 | 4 | 객체 경로 형식의 문자열 | const char * |
g | SIGNATURE | 가변 | 1 | 타입 시그니처 문자열 (최대 255바이트) | const char * |
h | UNIX_FD | 4 | 4 | 파일 디스크립터 인덱스 (SCM_RIGHTS) | int |
컨테이너 타입과 시그니처
| 시그니처 | 타입 | 설명 | 예시 |
|---|---|---|---|
a + 타입 | ARRAY | 동일 타입 요소의 배열 | as = 문자열 배열, ai = INT32 배열 |
(...) | STRUCT | 고정 타입 요소의 튜플 | (si) = 문자열+INT32, (ssi) = 3개 필드 |
a{KV} | DICT | 키-값 배열 (키는 기본 타입만) | a{sv} = 문자열→VARIANT 사전 |
v | VARIANT | 임의 타입 값 (시그니처 포함) | PropertiesChanged 시그널의 값 |
# 시그니처 예시와 busctl 출력
# a{sv}: Properties.GetAll의 반환 타입
busctl call org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.DBus.Properties GetAll s \
"org.freedesktop.NetworkManager"
# → a{sv} 23 "Version" s "1.44.2" "State" u 70 ...
# (ssa{sv}): 인트로스펙션 XML의 구조적 표현
# s = 인터페이스 이름
# s = 멤버 이름
# a{sv} = 속성 사전
# VARIANT의 시그니처 확인
busctl get-property org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.NetworkManager State
# → u 70 (u = UINT32, 70 = NM_STATE_CONNECTED_GLOBAL)
마샬링(Marshaling) 규칙
D-Bus 마샬링은 메모리 정렬(Alignment)을 엄격히 적용합니다. 각 타입은 자신의 정렬 크기의 배수 오프셋에서 시작해야 하며, 필요한 경우 NUL 패딩(Padding)이 삽입됩니다.
/* sd-bus를 이용한 마샬링 예제 */
#include <systemd/sd-bus.h>
/* a{sv} 사전 마샬링 */
sd_bus_message *msg;
sd_bus_message_new_method_call(bus, &msg,
"org.example.Service",
"/org/example/Object",
"org.example.Interface",
"Configure");
/* 사전 열기: a{sv} */
sd_bus_message_open_container(msg, 'a', "{sv}");
/* 첫 번째 항목: "Name" → "test" */
sd_bus_message_open_container(msg, 'e', "sv");
sd_bus_message_append(msg, "s", "Name");
sd_bus_message_open_container(msg, 'v', "s");
sd_bus_message_append(msg, "s", "test");
sd_bus_message_close_container(msg); /* v */
sd_bus_message_close_container(msg); /* e */
/* 두 번째 항목: "Port" → 8080 */
sd_bus_message_open_container(msg, 'e', "sv");
sd_bus_message_append(msg, "s", "Port");
sd_bus_message_open_container(msg, 'v', "u");
sd_bus_message_append(msg, "u", 8080);
sd_bus_message_close_container(msg); /* v */
sd_bus_message_close_container(msg); /* e */
sd_bus_message_close_container(msg); /* a */
/* 메시지 전송 */
sd_bus_call(bus, msg, 0, &error, &reply);
이름 소유권(Name Ownership)
D-Bus에서 서비스를 식별하는 두 가지 이름 체계가 있습니다.
유니크 이름과 잘 알려진 이름
| 구분 | 유니크 이름(Unique Name) | 잘 알려진 이름(Well-Known Name) |
|---|---|---|
| 형식 | :1.42 (콜론으로 시작) | org.freedesktop.NetworkManager (역도메인) |
| 할당 | 데몬이 연결 시 자동 부여 | 프로세스가 명시적으로 요청 |
| 수명 | 연결 해제 시 소멸 | 소유자가 해제하거나 연결 해제 시 소멸 |
| 고유성 | 버스 내에서 항상 유일 | 한 번에 하나의 소유자만 가능 |
| 용도 | 내부 라우팅, 발신자 식별 | 서비스 발견, 클라이언트 접근 |
이름 큐잉(Name Queuing)
잘 알려진 이름에 대해 여러 프로세스가 소유를 요청하면 큐(Queue)에 대기합니다. 현재 소유자가 해제하면 큐의 다음 대기자가 자동으로 소유권을 획득합니다.
# 이름 소유 요청 (RequestName)
busctl call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus RequestName su \
"org.example.MyService" 0
# 반환 코드:
# 1 = DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER (즉시 획득)
# 2 = DBUS_REQUEST_NAME_REPLY_IN_QUEUE (큐에 대기)
# 3 = DBUS_REQUEST_NAME_REPLY_EXISTS (이미 다른 소유자, 큐잉 미요청)
# 4 = DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER (이미 자신이 소유)
# 현재 이름 소유자 확인
busctl call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus GetNameOwner s \
"org.freedesktop.NetworkManager"
# → s ":1.6"
# 이름의 큐 상태 확인
busctl call org.freedesktop.DBus /org/freedesktop/DBus \
org.freedesktop.DBus ListQueuedOwners s \
"org.freedesktop.NetworkManager"
# → as 1 ":1.6"
# 이름 소유권 변경 시그널 감시
busctl monitor --match "type='signal',sender='org.freedesktop.DBus',member='NameOwnerChanged'"
/* sd-bus에서 이름 소유 요청 */
#include <systemd/sd-bus.h>
int r;
/* 이름 요청: DBUS_NAME_FLAG_REPLACE_EXISTING | DBUS_NAME_FLAG_ALLOW_REPLACEMENT */
r = sd_bus_request_name(bus,
"org.example.MyService",
SD_BUS_NAME_REPLACE_EXISTING | SD_BUS_NAME_ALLOW_REPLACEMENT);
if (r < 0) {
fprintf(stderr, "이름 요청 실패: %s\n", strerror(-r));
return r;
}
/* r > 0이면 소유 성공 */
systemd 연동(systemd Integration)
systemd는 D-Bus를 핵심 통신 채널로 사용합니다. systemd-logind, systemd-resolved, systemd-networkd 등 주요 서비스가 D-Bus 인터페이스를 노출합니다.
D-Bus 서비스 활성화
D-Bus 활성화(Bus Activation)는 서비스가 아직 실행되지 않았을 때 첫 메서드 호출 시 자동으로 서비스를 시작하는 메커니즘입니다.
# D-Bus 서비스 파일
# /usr/share/dbus-1/system-services/org.example.MyService.service
[D-BUS Service]
Name=org.example.MyService
Exec=/usr/bin/my-service
User=root
SystemdService=my-service.service
# 대응하는 systemd 유닛 파일
# /etc/systemd/system/my-service.service
[Unit]
Description=My D-Bus Service
After=dbus.socket
[Service]
Type=dbus
BusName=org.example.MyService
ExecStart=/usr/bin/my-service
[Install]
WantedBy=multi-user.target
Alias=dbus-org.example.MyService.service
systemd-logind D-Bus API 실전
# 현재 세션 목록
busctl call org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager \
ListSessions
# → a(susso) 2 1 1000 "user" "/org/freedesktop/login1/session/_31" ...
# 시스템 전원 관리
busctl call org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager \
PowerOff b true
# → Polkit 인증 요청 발생
# Inhibitor Lock (전원 관리 방지)
busctl call org.freedesktop.login1 \
/org/freedesktop/login1 \
org.freedesktop.login1.Manager \
Inhibit ssss \
"shutdown:sleep" "MyApp" "작업 진행 중" "delay"
# → h 0 (파일 디스크립터 반환 — 닫으면 잠금 해제)
# PrepareForShutdown 시그널 모니터링
busctl monitor org.freedesktop.login1 \
--match "interface='org.freedesktop.login1.Manager',member='PrepareForShutdown'"
sd-bus 서비스 작성 완전 예제
/* sd-bus를 이용한 완전한 D-Bus 서비스 예제 */
#include <stdio.h>
#include <stdlib.h>
#include <systemd/sd-bus.h>
#include <systemd/sd-event.h>
static int counter = 0;
/* 메서드 핸들러: Increment */
static int method_increment(sd_bus_message *msg,
void *userdata,
sd_bus_error *error)
{
int32_t delta;
int r;
r = sd_bus_message_read(msg, "i", &delta);
if (r < 0)
return r;
counter += delta;
/* PropertiesChanged 시그널 발행 */
sd_bus_emit_properties_changed(
sd_bus_message_get_bus(msg),
"/org/example/Counter",
"org.example.Counter",
"Value", NULL);
return sd_bus_reply_method_return(msg, "i", counter);
}
/* 프로퍼티 getter: Value */
static int property_get_value(sd_bus *bus,
const char *path, const char *iface,
const char *property, sd_bus_message *reply,
void *userdata, sd_bus_error *error)
{
return sd_bus_message_append(reply, "i", counter);
}
/* 인터페이스 vtable */
static const sd_bus_vtable counter_vtable[] = {
SD_BUS_VTABLE_START(0),
SD_BUS_METHOD("Increment", "i", "i",
method_increment, SD_BUS_VTABLE_UNPRIVILEGED),
SD_BUS_PROPERTY("Value", "i",
property_get_value, 0,
SD_BUS_VTABLE_PROPERTY_EMITS_CHANGE),
SD_BUS_SIGNAL("Overflow", "i", 0),
SD_BUS_VTABLE_END,
};
int main(void) {
sd_bus *bus = NULL;
sd_bus_slot *slot = NULL;
sd_bus_open_system(&bus);
/* 객체에 인터페이스 등록 */
sd_bus_add_object_vtable(bus, &slot,
"/org/example/Counter",
"org.example.Counter",
counter_vtable, NULL);
/* 버스 이름 요청 */
sd_bus_request_name(bus,
"org.example.Counter", 0);
/* 이벤트 루프 실행 */
for (;;) {
sd_bus_process(bus, NULL);
sd_bus_wait(bus, (uint64_t)-1);
}
sd_bus_slot_unref(slot);
sd_bus_unref(bus);
return 0;
}
코드 설명
- SD_BUS_VTABLE — vtable은 인터페이스의 메서드, 프로퍼티, 시그널을 선언적으로 정의합니다. sd-bus가 인트로스펙션 XML을 자동 생성하고, 타입 검증과 디스패치를 처리합니다.
- SD_BUS_VTABLE_UNPRIVILEGED — 이 플래그가 있으면 비특권 사용자도 호출할 수 있습니다. 없으면 root 또는 동일 UID만 호출 가능합니다.
- PROPERTY_EMITS_CHANGE — 프로퍼티 값이 바뀌면
sd_bus_emit_properties_changed()호출 시PropertiesChanged시그널을 발행합니다. - sd_bus_process + sd_bus_wait — 이벤트 루프입니다.
process()가 대기 중인 메시지를 처리하고,wait()가 새 메시지를 기다립니다. 프로덕션에서는sd_event와 통합하는 것이 좋습니다.
컨테이너 환경(Container Environment Details)
컨테이너에서 D-Bus를 사용하는 패턴과 보안 고려 사항을 상세히 다룹니다.
컨테이너 D-Bus 접근 패턴
| 패턴 | 방법 | 보안 수준 | 용도 |
|---|---|---|---|
| 소켓 바인드 마운트 | -v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket |
낮음 (호스트 버스 직접 접근) | 호스트 서비스(NM, logind) 제어 |
| 독립 데몬 | 컨테이너 내부에 별도 dbus-daemon 실행 | 높음 (격리) | 컨테이너 내부 IPC |
| xdg-dbus-proxy | Flatpak 프록시로 특정 버스 이름만 허용 | 중간 (필터링) | Flatpak/Snap 앱 |
| 소켓 활성화 | systemd 소켓 유닛으로 컨테이너 내 서비스 활성화 | 중간 | On-demand 컨테이너 |
# 패턴 1: 호스트 시스템 버스 바인드 마운트
podman run --rm -it \
-v /run/dbus/system_bus_socket:/run/dbus/system_bus_socket:ro \
my-container busctl list
# ⚠ 호스트의 모든 D-Bus 서비스에 접근 가능 — 보안 위험
# 패턴 2: xdg-dbus-proxy로 필터링
xdg-dbus-proxy \
unix:path=/run/dbus/system_bus_socket \
/run/container-dbus-socket \
--filter \
--talk=org.freedesktop.NetworkManager \
--see=org.freedesktop.login1
# --talk: 메서드 호출과 시그널 수신 허용
# --see: 이름 존재 확인만 허용 (호출 불가)
# 패턴 3: 컨테이너 내부 독립 D-Bus
# Dockerfile에서:
# RUN dbus-uuidgen > /var/lib/dbus/machine-id
# CMD ["dbus-daemon", "--system", "--fork"] && exec my-service
# 패턴 4: Kubernetes에서 D-Bus 소켓 공유 (hostPath)
# volumes:
# - name: dbus-socket
# hostPath:
# path: /run/dbus/system_bus_socket
# type: Socket
커널 측 IPC 최적화(Kernel IPC Optimization)
D-Bus 성능은 커널의 AF_UNIX 소켓 구현에 크게 의존합니다. 커널 측 최적화 기법과 dbus-broker의 성능 향상 전략을 살펴봅니다.
AF_UNIX 소켓 최적화
| 커널 파라미터 | 기본값 | D-Bus 영향 | 튜닝 가이드 |
|---|---|---|---|
net.core.wmem_max | 212992 | 큰 메시지 전송 시 버퍼 부족 | 대용량 FD 전달 서비스는 증가 고려 |
net.core.rmem_max | 212992 | 메시지 수신 버퍼 | busctl capture 시 증가 필요 |
net.unix.max_dgram_qlen | 10 | SOCK_DGRAM 큐 길이 | D-Bus는 SOCK_STREAM 사용 (무관) |
kernel.unprivileged_userns_clone | 배포판별 상이 | 컨테이너 네임스페이스 격리 | 보안과 기능 간 균형 |
성능 비교 측정
# dbus-broker vs dbus-daemon 벤치마크
# dbus-send를 이용한 간단한 왕복 시간 측정
# 1000회 Ping 호출 시간 측정
time for i in $(seq 1 1000); do
busctl call org.freedesktop.DBus \
/org/freedesktop/DBus \
org.freedesktop.DBus.Peer Ping >/dev/null
done
# perf로 dbus-broker 핫스팟 분석
perf record -g -p $(pidof dbus-broker) -- sleep 10
perf report --sort dso,symbol
# strace로 시스템 콜 통계
strace -c -p $(pidof dbus-broker) -e trace=sendmsg,recvmsg,epoll_wait -- sleep 5
# busctl monitor로 메시지 속도 관찰
busctl monitor --system 2>&1 | pv -l -i 5 > /dev/null
# → 초당 처리 메시지 수 확인
실전 예제 종합(Practical Examples)
gdbus 명령어 예제
# gdbus (GLib 기반 D-Bus 도구)
# 인트로스펙션
gdbus introspect --system \
--dest org.freedesktop.NetworkManager \
--object-path /org/freedesktop/NetworkManager
# 메서드 호출
gdbus call --system \
--dest org.freedesktop.NetworkManager \
--object-path /org/freedesktop/NetworkManager \
--method org.freedesktop.DBus.Properties.Get \
"org.freedesktop.NetworkManager" "State"
# → (<uint32 70>,)
# 시그널 모니터링
gdbus monitor --system \
--dest org.freedesktop.NetworkManager \
--object-path /org/freedesktop/NetworkManager
Python D-Bus 예제
#!/usr/bin/env python3
# pydbus를 이용한 D-Bus 클라이언트
from pydbus import SystemBus
from gi.repository import GLib
bus = SystemBus()
# NetworkManager 프록시 객체 생성
nm = bus.get("org.freedesktop.NetworkManager")
# 프로퍼티 읽기
print(f"NM Version: {nm.Version}")
print(f"NM State: {nm.State}")
# 메서드 호출: 모든 디바이스 경로 가져오기
devices = nm.GetDevices()
for dev_path in devices:
dev = bus.get("org.freedesktop.NetworkManager", dev_path)
print(f" {dev.Interface}: type={dev.DeviceType}, state={dev.State}")
# 시그널 수신 (비동기)
def on_state_changed(state):
states = {10: "ASLEEP", 20: "DISCONNECTED",
70: "CONNECTED_GLOBAL"}
print(f"NM state → {states.get(state, state)}")
nm.StateChanged.connect(on_state_changed)
# GLib 이벤트 루프 실행
loop = GLib.MainLoop()
try:
loop.run()
except KeyboardInterrupt:
loop.quit()
C 클라이언트 예제
/* sd-bus를 이용한 D-Bus 클라이언트 — NM 상태 조회 */
#include <stdio.h>
#include <systemd/sd-bus.h>
int main(void) {
sd_bus *bus = NULL;
sd_bus_error error = SD_BUS_ERROR_NULL;
sd_bus_message *reply = NULL;
uint32_t state;
int r;
r = sd_bus_open_system(&bus);
if (r < 0) {
fprintf(stderr, "버스 연결 실패: %s\n", strerror(-r));
return 1;
}
/* Properties.Get 호출 */
r = sd_bus_get_property(bus,
"org.freedesktop.NetworkManager",
"/org/freedesktop/NetworkManager",
"org.freedesktop.NetworkManager",
"State",
&error, &reply, "u");
if (r < 0) {
fprintf(stderr, "프로퍼티 조회 실패: %s\n",
error.message);
goto finish;
}
sd_bus_message_read(reply, "u", &state);
printf("NetworkManager State: %u\n", state);
finish:
sd_bus_error_free(&error);
sd_bus_message_unref(reply);
sd_bus_unref(bus);
return r < 0 ? 1 : 0;
}
gcc -o dbus-client dbus-client.c $(pkg-config --cflags --libs libsystemd)
./dbus-client
# → NetworkManager State: 70
매칭 규칙(Match Rules) 상세
D-Bus 매칭 규칙은 클라이언트가 수신할 시그널이나 메시지를 필터링하는 데 사용됩니다. AddMatch 메서드로 등록합니다.
| 매칭 키 | 설명 | 예시 |
|---|---|---|
type | 메시지 타입 | type='signal' |
sender | 발신자 버스 이름 | sender='org.freedesktop.NetworkManager' |
interface | 인터페이스 이름 | interface='org.freedesktop.DBus.Properties' |
member | 메서드/시그널 이름 | member='PropertiesChanged' |
path | 객체 경로 (정확 일치) | path='/org/freedesktop/NetworkManager' |
path_namespace | 객체 경로 접두사 | path_namespace='/org/freedesktop' |
destination | 수신자 버스 이름 | destination=':1.42' |
arg0~arg63 | 본문의 N번째 인자값 | arg0='org.freedesktop.NetworkManager' |
arg0namespace | 첫 인자의 접두사 매칭 | arg0namespace='org.freedesktop' |
eavesdrop | 모든 메시지 수신 (특권) | eavesdrop='true' |
# busctl monitor에서 매칭 규칙 사용
# NetworkManager의 PropertiesChanged 시그널만 수신
busctl monitor --match "type='signal',\
sender='org.freedesktop.NetworkManager',\
interface='org.freedesktop.DBus.Properties',\
member='PropertiesChanged'"
# 특정 객체 경로 하위의 모든 시그널
busctl monitor --match "type='signal',\
path_namespace='/org/freedesktop/NetworkManager/Devices'"
# dbus-monitor 동등 표현
dbus-monitor --system "type='signal',\
sender='org.freedesktop.login1',\
member='PrepareForShutdown'"
표준 D-Bus 오류 이름
| 오류 이름 | 의미 | 일반적 원인 |
|---|---|---|
org.freedesktop.DBus.Error.ServiceUnknown | 목적지 서비스가 존재하지 않음 | 서비스 미실행, 이름 오타 |
org.freedesktop.DBus.Error.UnknownMethod | 메서드가 존재하지 않음 | 인터페이스/메서드 이름 오류 |
org.freedesktop.DBus.Error.UnknownInterface | 인터페이스가 존재하지 않음 | 인터페이스 이름 오류 |
org.freedesktop.DBus.Error.UnknownObject | 객체 경로가 존재하지 않음 | 경로 오류 |
org.freedesktop.DBus.Error.AccessDenied | 정책에 의해 거부됨 | D-Bus 정책 파일 미설정 |
org.freedesktop.DBus.Error.InvalidArgs | 인자 타입/수 불일치 | 시그니처 불일치 |
org.freedesktop.DBus.Error.Timeout | 응답 타임아웃 (기본 25초) | 서비스 블로킹, 과부하 |
org.freedesktop.DBus.Error.NoReply | 서비스가 응답 없이 종료 | 서비스 크래시 |
org.freedesktop.DBus.Error.NameHasNoOwner | 이름에 소유자 없음 | 서비스 미실행 |
참고 자료
- D-Bus Specification (freedesktop.org) — D-Bus 공식 사양 문서입니다
- D-Bus 프로젝트 위키 — freedesktop.org D-Bus 프로젝트 페이지입니다
- D-Bus Tutorial (freedesktop.org) — D-Bus 프로그래밍 입문 튜토리얼입니다
- D-Bus FAQ (freedesktop.org) — D-Bus에 대한 자주 묻는 질문과 답변을 정리한 문서입니다
- sd-bus API Reference (systemd) — sd-bus 라이브러리 API 문서입니다
- busctl(1) 매뉴얼 (systemd) — D-Bus 버스를 탐색하고 모니터링하는 CLI 도구 문서입니다
- kdbus: critical review (LWN.net) — 커널 내 D-Bus 구현인 kdbus에 대한 심층 분석 기사입니다
- The kdbus� saga continues (LWN.net) — kdbus 커널 통합 논의의 후속 전개를 다룬 기사입니다
- Bus1: a new Linux IPC proposal (LWN.net) — kdbus 이후 제안된 Bus1 커널 IPC 메커니즘에 대한 기사입니다
- dbus-broker (GitHub) — 고성능 D-Bus 브로커 구현체입니다
- D-Bus 공식 소스 저장소 (GitLab) — D-Bus 레퍼런스 구현의 공식 소스 코드 저장소입니다
관련 문서
- IPC (Inter-Process Communication) — Unix 도메인 소켓, pipe, System V IPC 등 커널 IPC 메커니즘 전반
- systemd — D-Bus를 핵심 통신 채널로 사용하는 시스템·서비스 관리자
- firewalld — D-Bus API를 통한 동적 방화벽 관리 실전 예제