D-Bus (Desktop Bus)
D-Bus 시스템/세션 버스(Bus) 아키텍처, 메시지 모델(Method Call/Signal/Property), 버스 이름과 객체 경로 체계, 타입 시스템, 서비스 활성화, 보안 모델(Policy/Polkit), 커널과의 관계(AF_UNIX/kdbus), 주요 서비스, 도구(busctl/dbus-monitor), 프로그래밍 인터페이스(sd-bus/GIO), 성능과 대안 분석을 종합 정리
| 건물 교환실 | D-Bus 개념 | 예시 |
|---|---|---|
| 내선 번호 (예: 213호) | 버스 이름(Bus Name) | org.freedesktop.NetworkManager |
| 부서 내 창구 위치 | 객체 경로(Object Path) | /org/freedesktop/NetworkManager/Devices/1 |
| 담당 업무 종류 (IT·인사·재무) | 인터페이스(Interface) | org.freedesktop.NetworkManager.Device |
| 창구에 요청하는 구체적 업무 | 메서드 호출(Method Call) | Disconnect() — 네트워크 연결 해제 |
| 사내 방송 (전체 수신) | 시그널(Signal) | StateChanged — 연결 상태 변경 알림 |
| 업무 현황판 조회 | 프로퍼티(Property) | State — 현재 네트워크 상태 값 |
핵심 요약
- D-Bus는 프로세스 간 통신(IPC)을 위한 메시지 버스 시스템(Message Bus System)으로, 하나의 데몬(Daemon)이 여러 프로세스 사이의 메시지를 중개합니다.
- 시스템 버스는 시스템 전체 서비스(systemd, NetworkManager, BlueZ 등)가 사용하고, 세션 버스는 사용자 데스크톱 애플리케이션이 사용합니다.
- 통신 방식은 메서드 호출(Method Call)(요청-응답), 시그널(Signal)(브로드캐스트), 프로퍼티(Property)(속성 조회·변경) 세 가지 패턴으로 나뉩니다.
- 모든 서비스는 버스 이름(Bus Name)으로 식별되고, 서비스 내부의 기능은 객체 경로(Object Path)와 인터페이스(Interface)로 구조화됩니다.
- 보안은 D-Bus 정책(Policy)(누가 어떤 메서드를 호출할 수 있는지)과 Polkit(대화형 권한 승인)으로 계층적으로 관리됩니다.
단계별 이해
-
시스템 버스 탐색:
busctl list --system으로 현재 등록된 D-Bus 서비스 목록을 확인합니다.# 시스템 버스에 등록된 서비스 목록 확인 busctl list --system # 출력 예시 # NAME PID PROCESS USER CONNECTION # org.freedesktop.DBus 1 systemd root - # org.freedesktop.NetworkManager 1234 NetworkManager root :1.15 # org.freedesktop.systemd1 1 systemd root :1.1 # org.bluez 1350 bluetoothd root :1.22 # :1.42 887 bash user :1.42org.으로 시작하는 이름은 서비스가 직접 등록한 잘 알려진 이름(Well-known Name)이고,:1.숫자는 연결 시 자동 부여되는 고유 이름(Unique Name)입니다. -
서비스 구조 탐색:
busctl tree로 특정 서비스가 노출하는 객체 트리를 확인합니다.busctl tree org.freedesktop.systemd1 # 출력 예시 # └─ /org/freedesktop/systemd1 # ├─ /org/freedesktop/systemd1/unit # │ ├─ /org/freedesktop/systemd1/unit/NetworkManager_2eservice # │ └─ /org/freedesktop/systemd1/unit/sshd_2eservice # └─ /org/freedesktop/systemd1/job각 줄이 객체 경로(Object Path)입니다. 파일시스템(Filesystem) 경로처럼
/로 계층화되어 있으며, 각 경로에서 인터페이스를 조회할 수 있습니다. -
인터페이스·메서드 확인:
busctl introspect로 객체가 노출하는 메서드, 프로퍼티, 시그널을 봅니다.busctl introspect org.freedesktop.systemd1 \ /org/freedesktop/systemd1 \ org.freedesktop.systemd1.Manager # 출력 예시 (일부) # NAME TYPE SIGNATURE RESULT/VALUE # .GetUnit method s o # .ListUnits method - a(ssssssouso) # .Reload method - - # .Version property s "254" readonly # .UnitNew signal so -method는 호출 가능한 함수,property는 읽거나 쓸 수 있는 상태 값,signal은 서비스가 능동적으로 브로드캐스트하는 이벤트입니다. -
메서드 호출:
busctl call또는busctl get-property로 서비스와 직접 대화합니다.# systemd 버전 프로퍼티 읽기 busctl get-property org.freedesktop.systemd1 \ /org/freedesktop/systemd1 \ org.freedesktop.systemd1.Manager Version # s "254" # 실행 중인 유닛 목록 조회 busctl call org.freedesktop.systemd1 \ /org/freedesktop/systemd1 \ org.freedesktop.systemd1.Manager \ ListUnits -
메시지 실시간(Real-time) 관찰:
dbus-monitor --system으로 버스에서 오가는 메시지를 스트리밍합니다.dbus-monitor --system # 출력 예시 (NetworkManager 상태 변경 시그널) # signal time=1714123456.789 sender=:1.15 -> dest=(null) # path=/org/freedesktop/NetworkManager # interface=org.freedesktop.NetworkManager # member=StateChanged # uint32 70sender는 고유 이름,member는 시그널 이름, 마지막 값이 실제 데이터(연결 상태 코드)입니다. - 서비스 작성: sd-bus 또는 Python으로 자신만의 D-Bus 서비스를 만들어 봅니다. 아래 Hello World 실습과 프로그래밍 인터페이스 섹션을 참고하세요.
D-Bus가 필요한 이유(Why D-Bus?)
D-Bus는 2002년 freedesktop.org 프로젝트에서 시작되었으며, 리눅스 데스크톱과 시스템 서비스들이 공통으로 사용할 수 있는 표준 IPC 메커니즘을 제공하기 위해 만들어졌습니다.
D-Bus 이전의 IPC 문제
D-Bus가 없던 시절, 프로세스끼리 통신하려면 각자 다른 방법을 직접 구현해야 했습니다.
| 방법 | 문제점 |
|---|---|
| Unix 도메인 소켓 직접 연결 | 서비스마다 고유한 프로토콜(Protocol)을 정의해야 함. 다른 프로세스가 서비스를 찾는 표준 방법(Service Discovery) 없음. 접근 제어를 서비스마다 직접 구현해야 함 |
| 공유 메모리(Shared Memory) | 동기화(Synchronization) 복잡도 높음. 비구조적 데이터 전달. 원격 호출(RPC) 패턴 구현 어려움 |
| DCOP (KDE 전용) | KDE 애플리케이션 전용. GNOME, 시스템 서비스와 호환 불가 |
| CORBA | 과도한 복잡성. 데스크톱 수준 애플리케이션에 비해 무거운 스펙(Spec) |
D-Bus가 해결하는 문제
| D-Bus가 해결하는 문제 | 방법 |
|---|---|
| 서비스 발견: 어떤 서비스가 실행 중인지 어떻게 알까? | 버스 이름(Bus Name)으로 모든 서비스가 중앙 등록 — busctl list로 조회 |
| 온디맨드 시작: 서비스가 항상 실행되어야 할까? | .service 파일로 최초 메시지 도착 시 자동 활성화(Activation) |
| 프로토콜 표준화: 서로 다른 서비스의 API를 어떻게 통일할까? | 인터페이스(Interface) + 타입 시그니처(Type Signature)로 언어 독립적 API 정의 |
| 접근 제어: 아무 프로세스나 시스템 서비스를 호출해도 될까? | D-Bus 정책(Policy) + Polkit으로 계층적 권한 관리 |
| 이벤트 통지: 상태 변화를 여러 프로세스에게 동시에 알리려면? | 시그널(Signal) 브로드캐스트 — 관심 있는 프로세스만 구독(Subscribe) |
D-Bus 없이 하면 어떻게 될까?
예를 들어 네트워크 연결 상태를 확인하는 프로그램을 만든다고 가정합니다.
| 방법 | 구현 내용 | 문제 |
|---|---|---|
| 직접 소켓 연결 | NetworkManager 소켓 경로 직접 지정, 독자적 프로토콜 파싱 | 소켓 경로가 배포판마다 다를 수 있음. NetworkManager API 변경 시 모두 수정 필요 |
| 파일 읽기 | /proc/net/dev, /sys/class/net/ 파싱 | 변경 감지를 위한 폴링(Polling) 필요. 이벤트 기반(Event-driven) 아님 |
| D-Bus 사용 | org.freedesktop.NetworkManager의 StateChanged 시그널 구독 | — (표준화된 방법, 즉시 이벤트 알림) |
D-Bus 아키텍처(Architecture)
D-Bus는 중앙 집중형 메시지 버스 구조를 사용합니다. 클라이언트(Client)와 서비스(Service)가 직접 통신하지 않고, 중간에 위치한 버스 데몬(Bus Daemon)이 메시지를 라우팅(Routing)합니다. 이 구조 덕분에 서비스 발견(Service Discovery), 활성화(Activation), 접근 제어(Access Control)를 한 곳에서 관리할 수 있습니다.
시스템 버스와 세션 버스
리눅스 시스템에서는 항상 두 종류의 버스가 동작합니다.
| 구분 | 시스템 버스 | 세션 버스 |
|---|---|---|
| 소켓 경로 | /run/dbus/system_bus_socket | $XDG_RUNTIME_DIR/bus 또는 추상 소켓 |
| 데몬 실행 | 부팅 시 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에서 개발. 단일 스레드(Thread)(Single-threaded) 이벤트 루프(Event Loop) 방식 | 대부분의 배포판 (전통적 기본값) |
| dbus-broker | 성능 최적화된 대안. 커널의 epoll과 비차단(Non-blocking) I/O를 적극 활용. 메시지 정렬(Message Ordering)을 보장하면서도 높은 처리량(Throughput) 달성 | Fedora, RHEL 9+, Arch Linux (기본값) |
두 구현체 모두 같은 D-Bus 프로토콜(Protocol)을 사용하므로, 클라이언트 코드는 어떤 데몬이 동작하든 동일하게 작동합니다.
dbus-broker 내부 구조
dbus-broker는 단일 프로세스인 dbus-daemon과 달리, 역할을 분리한 다중 컴포넌트(Multi-component) 구조를 사용합니다.
| 컴포넌트 | 역할 | 특징 |
|---|---|---|
| dbus-broker-launch | 런처(Launcher) — 설정 파일 파싱, 정책 컴파일, broker 프로세스 관리 | 정책을 사전 컴파일(Pre-compile)하여 broker에게 전달. 런타임 XML 파싱 오버헤드(Overhead) 제거 |
| 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) 문제를 해결합니다.
연결 수립 과정(Connection Lifecycle)
프로세스가 D-Bus에 연결할 때는 단순히 소켓에 접속하는 것으로 끝나지 않습니다. 인증(Authentication)과 핸드셰이크(Handshake) 과정을 거쳐야 합니다.
-
소켓 연결: 클라이언트가
/run/dbus/system_bus_socket(시스템 버스) 또는$XDG_RUNTIME_DIR/bus(세션 버스)에 TCP 연결이 아닌 AF_UNIX(Unix Domain Socket)으로 접속합니다. -
인증(Authentication): D-Bus는 SASL(Simple Authentication and Security Layer) 프레임워크를 사용합니다. 리눅스에서는 EXTERNAL 메커니즘으로 커널이 보장하는 UID(User ID) 정보를 전달합니다.
# 클라이언트 → 버스 데몬 (NUL 바이트로 시작, 이후 ASCII 텍스트) \0AUTH EXTERNAL 31303030 # "1000"의 16진수 ASCII — 현재 프로세스의 UID # 버스 데몬 → 클라이언트 OK 1234567890abcdef # 인증 성공, 서버 GUID(Globally Unique Identifier) -
협상 시작(BEGIN): 인증 완료 후 클라이언트가
BEGIN을 보내면 D-Bus 프로토콜 메시지 교환이 시작됩니다. -
HELLO 메시지: 클라이언트가
org.freedesktop.DBus의Hello()메서드를 호출합니다. 버스 데몬은 이 연결에 고유 이름(Unique Name)을 할당하여 응답합니다.# strace로 busctl 실행 시 HELLO 교환 확인 (첫 write syscall) strace -e trace=write busctl list --system 2>&1 | head -5 # write(4, "l\1\0\1..." ← 'l'=리틀엔디언, 메시지 타입=1(METHOD_CALL) # HELLO 메시지를 버스 데몬으로 전송 -
고유 이름 수신: 버스 데몬이
:1.숫자형식의 고유 이름을 클라이언트에게 반환합니다. 이후 모든 메시지의 발신자(Sender) 필드에 이 이름이 기록됩니다. -
잘 알려진 이름 등록 (서비스인 경우): 서비스라면 추가로
RequestName("org.example.MyService")를 호출하여 사람이 읽을 수 있는 이름을 등록합니다.
NameOwnerChanged 시그널로 이 사실을 통보받습니다.
메시지 라우팅 과정
클라이언트가 메서드를 호출하면 다음과 같은 과정을 거칩니다.
- 클라이언트가 메서드 호출 메시지를 버스 데몬의 소켓으로 전송합니다.
- 버스 데몬이 메시지의 목적지(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 메시지는 네트워크 바이트 순서(Byte Order)가 아닌, 발신자의 엔디언(Endianness)을 따릅니다. 수신자는 첫 바이트로 엔디언을 판별합니다.
| 오프셋(Offset) | 크기 | 필드 | 설명 |
|---|---|---|---|
| 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바이트 경계로 패딩(Padding) |
메시지 플래그(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으로 요청하면 소유권을 양보(Yield) |
| 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"는 읽기 전용(Read-Only),"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바이트 | 유닉스 파일 디스크립터(File Descriptor)(SCM_RIGHTS로 전달) |
컨테이너(Container) 타입(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}) | (문자열, 문자열, 사전) 구조체(Struct) | PropertiesChanged 시그널 — (인터페이스, 변경된 프로퍼티, 무효화(Invalidation) 목록) |
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)을 따릅니다. 이전 필드 끝부터 다음 필드 시작까지 필요한 만큼 패딩 바이트(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는 사용자 공간(User Space)(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)의 구현 기반 |
| 추상 소켓 | 파일 시스템에 존재하지 않는 소켓. 세션 버스에서 네임스페이스(Namespace) 격리(Isolation)에 활용 |
| 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 프로토콜 전체를 커널에 넣으면 취약점(Vulnerability) 위험이 커짐
- 정책 로직의 커널 이동: 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 데몬을 운영하는 방식은 보안과 격리 수준에서 큰 차이를 보입니다.
네임스페이스 격리
컨테이너(Container) 환경에서 D-Bus와 리눅스 네임스페이스(Namespace)의 관계를 이해하는 것은 올바른 구성의 전제 조건입니다.
- 마운트(Mount) 네임스페이스(Mount Namespace): 시스템 버스 소켓이 컨테이너 안에 보이지 않으면 시스템 버스에 접근할 수 없습니다. 바인드 마운트(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): 추상 소켓은 네트워크 네임스페이스에 바인딩됩니다. 별도 네트워크 네임스페이스를 사용하는 컨테이너에서는 호스트의 추상 소켓 기반 세션 버스에 접근할 수 없습니다.
- Flatpak/Snap: 샌드박스(Sandbox) 내 애플리케이션을 위한 D-Bus 프록시(
xdg-dbus-proxy)를 사용하여 허용된 인터페이스만 노출합니다.
컨테이너에서 D-Bus가 필요한 이유
대부분의 단순 컨테이너(마이크로서비스, 웹 서버 등)는 D-Bus가 필요하지 않습니다. 그러나 다음과 같은 시나리오에서는 컨테이너 내부에서 D-Bus 통신이 필수적입니다.
| 시나리오 | D-Bus 의존 서비스 | 권장 패턴 |
|---|---|---|
| systemd 기반 컨테이너 (CI/CD, 인프라 테스트) | systemd, journald, logind | 내부 독립 데몬 |
| 네트워크 관리 컨테이너 | NetworkManager, firewalld, resolved | 내부 독립 데몬 |
| 호스트 서비스 모니터링/제어 | 호스트의 systemd1, UPower | 호스트 소켓 바인드 마운트 |
| 데스크톱 애플리케이션 컨테이너화 | 알림 데몬, IBus, PipeWire | 세션 버스 프록시 |
| 하드웨어 관리 | udisks2, BlueZ, UPower | 호스트 소켓 바인드 마운트 |
호스트 소켓 바인드 마운트
가장 간단한 방법은 호스트의 시스템 버스 소켓을 컨테이너에 바인드 마운트하는 것입니다. 컨테이너 프로세스가 호스트의 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 | 유닛 관리(시작/중지/재시작(Reboot)), 상태 조회 |
| 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 | 방화벽(Firewall) 존·규칙 관리 (상세) |
| 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 캡처 지원 | 일상 디버깅(Debugging), 시스템 관리 (권장) |
| 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'"
첫 번째 D-Bus 실습(Hello World)
명령줄 도구만으로 D-Bus 서비스와 대화하는 완전한 실습 시나리오입니다. 추가 설치가 필요 없으며, 리눅스 시스템이라면 바로 실행할 수 있습니다.
1단계: 버스 탐색
# 1. 시스템에 연결된 모든 D-Bus 서비스 목록 확인
busctl list --system
# 2. systemd 서비스의 객체 트리 구조 확인
busctl tree org.freedesktop.systemd1
# 3. 특정 객체의 인터페이스와 멤버 목록 확인
busctl introspect org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager
2단계: 프로퍼티 읽기
# systemd 버전 읽기
busctl get-property org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager Version
# s "254"
# systemd 전체 상태 읽기
busctl get-property org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager SystemState
# s "running"
# NetworkManager 연결 검사 활성화 여부
busctl get-property org.freedesktop.NetworkManager \
/org/freedesktop/NetworkManager \
org.freedesktop.NetworkManager ConnectivityCheckEnabled
# b true
3단계: 메서드 호출
# 실행 중인 systemd 유닛 목록 조회 (반환 타입: a(ssssssouso))
busctl call org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager \
ListUnits
# 특정 유닛 객체 경로 가져오기 (인자 타입: s = 문자열)
busctl call org.freedesktop.systemd1 \
/org/freedesktop/systemd1 \
org.freedesktop.systemd1.Manager \
GetUnit s "NetworkManager.service"
# o "/org/freedesktop/systemd1/unit/NetworkManager_2eservice"
# 반환된 객체 경로로 유닛 상태 확인
busctl get-property org.freedesktop.systemd1 \
/org/freedesktop/systemd1/unit/NetworkManager_2eservice \
org.freedesktop.systemd1.Unit ActiveState
# s "active"
코드 설명
- busctl call 인자 형식
busctl call <버스이름> <객체경로> <인터페이스> <메서드> [타입시그니처 값...]순서입니다. - GetUnit 반환값 o
o는 OBJECT_PATH 타입입니다. 서비스 이름의.은 D-Bus 경로에서_2e(점의 URL 인코딩)로 변환됩니다. - ActiveState 유닛 상태는
"active","inactive","activating","failed"등의 값을 가집니다.
4단계: 시그널 구독
# NetworkManager 상태 변경 시그널 실시간 관찰
# (Wi-Fi 연결/해제 시 출력됨 — Ctrl+C로 종료)
dbus-monitor --system \
"type='signal',interface='org.freedesktop.NetworkManager'"
# systemd 유닛 생성 이벤트 관찰
dbus-monitor --system \
"type='signal',sender='org.freedesktop.systemd1',member='UnitNew'"
# 다른 터미널에서 서비스를 재시작하면 위 터미널에 이벤트가 출력됨
# systemctl --user restart some.service
5단계: Python으로 시작하기
터미널 도구 대신 Python으로 동일한 작업을 프로그래밍 방식으로 수행합니다. subprocess를 이용한 가장 간단한 방법으로, 추가 라이브러리가 필요 없습니다.
import subprocess, re
def dbus_get_property(bus_name, obj_path, iface, prop):
result = subprocess.run(
["busctl", "get-property",
bus_name, obj_path, iface, prop],
capture_output=True, text=True
)
return result.stdout.strip()
# systemd 버전과 상태 출력
version = dbus_get_property(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"Version"
)
state = dbus_get_property(
"org.freedesktop.systemd1",
"/org/freedesktop/systemd1",
"org.freedesktop.systemd1.Manager",
"SystemState"
)
print(f"systemd {version}, state={state}")
# systemd s "254", state=s "running"
더 본격적인 Python D-Bus 프로그래밍(시그널 구독, 비동기 처리)은 아래 Python 예제 섹션을 참고하세요.
프로그래밍 인터페이스(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()은 시스템 버스에 연결합니다. 세션 버스는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))을 활용한 자동 해제 매크로(Macro)도 제공됩니다.
# 컴파일
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의 성능 오버헤드는 다음 요소에서 발생합니다:
- 직렬화/역직렬화(Serialization): 메시지 본문을 D-Bus 와이어 포맷(Wire Format)으로 변환. 데이터 크기에 비례
- 컨텍스트 스위치(Context Switch): 클라이언트 → 커널 → 데몬 → 커널 → 서비스 경로에서 최소 4회 컨텍스트 스위치 발생
- 데몬 홉(Daemon Hop): 모든 메시지가 버스 데몬을 거쳐야 하므로, 직접 소켓 통신 대비 지연(Latency) 증가
- 정책 검사: 매 메시지마다 접근 제어 규칙을 평가
메시지 지연 벤치마크
다음은 D-Bus 메서드 호출의 왕복 지연 근사치입니다. 실제 값은 하드웨어, 커널 버전, 부하(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) 기반 — 구독자 수에 무관한 성능 |
| 메시지 정렬 | 글로벌 순서 보장(Ordering) | 글로벌 순서 보장 + 커널 타임스탬프 활용 |
메시지 크기별 처리량
| 메시지 본문 크기 | dbus-daemon (msg/sec) | dbus-broker (msg/sec) | 주요 병목(Bottleneck) |
|---|---|---|---|
| 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 | 직렬화 + 소켓 버퍼(Buffer) 복사 |
| 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()를 사용하면 여러 메서드 호출을 응답을 기다리지 않고 연속으로 전송할 수 있습니다. 응답은 이벤트 루프에서 콜백(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 용도가 아님 |
디버깅
자주 발생하는 오류
| 오류 메시지 | 원인 | 해결 방법 |
|---|---|---|
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 9+)
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 패딩이 삽입됩니다.
/* 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을 자동 생성하고, 타입 검증과 디스패치(Dispatch)를 처리합니다.
- 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로 시스템 콜(System Call) 통계
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 | 이름에 소유자 없음 | 서비스 미실행 |
최신 동향 (2025~2026)
D-Bus는 여전히 데스크톱/서비스 IPC의 사실상 표준이지만, 2024~2025년을 기점으로 리눅스 생태계는 D-Bus를 유지하면서도 Varlink로 점진 이행하는 방향으로 크게 움직이고 있습니다. 커널 6.x와 systemd 256~257이 이 흐름의 전환점입니다.
dbus-broker의 지배력 확대
Arch Linux(2024-05부터 기본), Fedora(36부터 기본), openSUSE Tumbleweed, Red Hat Enterprise Linux 9+, Gentoo에서 dbus-broker가 기본 구현이 되었고, Ubuntu도 25.04부터 공식 메타패키지로 포함합니다. 주요 릴리스 특징은 다음과 같습니다.
- dbus-broker 36 (2024-12) — SCM_PIDFD 수신 경로를 사용해 피어 신원을 확인, PID 재사용 레이스 차단.
- dbus-broker 37 (2025-06) — systemd의
Type=notify-reload지원, launcher의 구성 재로드를 서비스 중단 없이 수행. - 성능 기준선 — 동일 하드웨어에서 레퍼런스
dbus-daemon대비 메시지 처리량 3~8배, latency 1/3 수준을 유지. - cgroup
user.*xattr(v6.8 커널) 기반 정책 필터링 — PID 기반보다 훨씬 안정적인 서비스 식별이 가능.
Varlink로의 점진 이행 — systemd 257 (2024-12)
kdbus와 Bus1이 커널에 자리를 잡지 못한 이후, systemd 개발팀은 D-Bus의 구조적 한계(동기 RPC, 흐름 제어(Flow Control) 부재, 대용량 전송 부적합)를 우회하기 위해 Varlink로 서서히 축을 이동하고 있습니다. systemd 257에서 그 방향성이 명확해졌습니다.
- sd-varlink / sd-json 공개 API —
libsystemd에 정식 공개된 라이브러리로, D-Bus 의존 없이 Varlink 서비스/클라이언트를 작성할 수 있습니다. - 인터페이스 수 역전 — systemd 257 기준 Varlink 인터페이스 19개 vs D-Bus API 11개. 신규 기능은 대부분 Varlink부터 제공됩니다.
- 주요 이식 대상 —
systemd-importd,systemd-userdbd,systemd-machined(일부),systemd-hostnamed(병행 제공),systemd-resolved(일부) 등. - D-Bus는 유지 — 기존 클라이언트 호환을 위해 기존 인터페이스는 남기며, 신규 기능만 Varlink 우선입니다.
| 특성 | D-Bus | Varlink |
|---|---|---|
| 페이로드 | 이진(DBus wire format) | JSON(UTF-8) |
| 전송 | AF_UNIX + 버스 데몬 | AF_UNIX(직접 연결) 또는 exec |
| 데몬 필요 | 필수(dbus-broker/dbus-daemon) | 불필요(peer-to-peer) |
| 흐름 제어 | 없음(대용량 전송 비적합) | 있음(스트리밍 결과 지원) |
| 타입 체계 | DBus signature(정적, 복잡) | IDL + JSON(인간 가독) |
| 인증 | SASL, SO_PEERCRED | AF_UNIX + SCM_PIDFD + cgroup |
| 대표 구현 | dbus-broker, libdbus, sd-bus, GIO | libvarlink, sd-varlink, go-varlink |
| 활성화 | 서비스 활성화 표준 | systemd socket activation + exec |
왜 Varlink인가: Lennart Poettering(systemd 수석)은 "D-Bus는 플로우 컨트롤이 없어 스트리밍/대용량 데이터에 부적합"이라고 공개적으로 밝혔습니다. 예를 들어 사용자 목록 덤프(Dump), 컨테이너 이미지 임포트, 시스템 로그 스트림 같은 작업은 D-Bus로 구현하면 버스 데몬을 막거나 메시지를 쪼개야 했습니다. Varlink는 연결당 AF_UNIX 1:1 채널 + JSON 프레이밍을 사용하기에 자연스럽게 스트리밍이 됩니다.
SCM_PIDFD/SO_PEERPIDFD 기반 신원 검증
커널 6.5에서 도입된 SCM_PIDFD/SO_PEERPIDFD는 D-Bus 인증 모델을 강화했습니다. dbus-broker 36+는 연결 초기에 피어의 pidfd를 획득하고, v6.9의 pidfs 고유 ino로 신원을 식별합니다. 이는 기존 SO_PEERCRED의 PID 재사용 취약점을 근본적으로 제거합니다.
/* dbus-broker 내부에서 피어 pidfd를 획득하는 흐름 */
int peer_pidfd = -1;
socklen_t len = sizeof(peer_pidfd);
if (getsockopt(conn_fd, SOL_SOCKET, SO_PEERPIDFD, &peer_pidfd, &len) == 0) {
/* pidfs ino로 고유 신원 저장 (v6.9+) */
struct stat st;
fstat(peer_pidfd, &st);
peer->identity_inode = st.st_ino;
}
kdbus/Bus1의 공식 종료
커널 내부 D-Bus 구현을 목표로 했던 kdbus(2013~2016)는 2016년 Linus Torvalds가 메인라인 병합을 거부한 뒤 사실상 중단되었습니다. 후속 프로젝트 Bus1도 2017년 이후 활동이 중단되었습니다. 2025년 기준 커뮤니티는 "커널 내 IPC는 필요 없고, 사용자 공간 dbus-broker/Varlink 조합이 충분히 빠르다"는 결론에 도달했습니다.
데스크톱 생태계의 병행 전략
- GNOME/GTK 4.14+ — 당분간 GIO D-Bus를 유지하지만, Portal(Flatpak) 계층은 Varlink 실험 중.
- KDE Plasma 6 — KDBusAddons 기반 의존 유지, 신규 서비스(kwallet, kwin 내부 제어)에서 D-Bus 사용 지속.
- Flatpak/Portal — xdg-desktop-portal이 D-Bus API를 유지하되, 내부 권한 검증을 SCM_PIDFD로 전환.
- ChromeOS/Android — Binder로 독자 경로(데스크톱 Linux D-Bus와 별개).
dbus-daemon 1.16.0 — 레퍼런스 구현 신규 안정 브랜치 (2024-12)
dbus-broker가 성능 우위를 점령했지만, dbus-daemon(레퍼런스 구현)도 2024년 12월에 신규 안정 브랜치 1.16.0을 출시했습니다. 1.14.x 구식 안정 브랜치와 1.15.x 개발 브랜치를 대체하며, 하위 호환을 유지하면서 여러 실질적인 개선이 포함되었습니다.
dbus-daemon 1.16.0부터: Autotools 빌드 시스템(Build System) 제거, Meson만 지원. 빌드 옵션 이름 변경됨.
- ProcessFD in GetConnectionCredentials() —
org.freedesktop.DBus.GetConnectionCredentials()응답에ProcessFD딕셔너리 항목(pidfd)이 추가되어, 수신자가 PID 재사용 없이 안전하게 피어 프로세스 정보를 조회할 수 있습니다. - .service 탐색 경로 확장 — 시스템 메시지 버스가
/etc와/run에서도.service파일을 로드합니다. dbus-broker 37의 변경과 일치하여 관리자가 시스템 재시작 없이 서비스 활성화 설정을 변경할 수 있습니다. - close_range() 활용 — 불필요한 파일 디스크립터를 닫을 때 Linux의
close_range()시스템 콜(System Call)을 사용하여 성능 향상. - Y2038 안전성 — 32비트 플랫폼에서 내부 타임스탬프를 64비트로 처리,
time_t오버플로 문제 해결. 관련 API가 64비트 타임스탬프와 inode 번호를 반환하도록 업데이트. - 소켓 경로 변경 — 잘 알려진 시스템 버스 소켓이 기본적으로 런타임 상태 디렉터리(
/run)에 위치. systemd 환경에서 dbus-daemon이 root로 기동 후 권한 강하 방식에서 대상 사용자로 직접 기동으로 변경.
systemd 258~259의 Varlink 확장 (2025-09~2025-12)
systemd 257에서 sd-varlink/sd-json이 공개 API로 출시된 이후, 258(2025년 9월)과 259(2025년 11~12월)에서 Varlink 인터페이스가 대폭 확장되었습니다. D-Bus와 Varlink의 기능 동등성(Feature Parity)이 점점 가까워지고 있습니다.
systemd 258부터: 서비스 관리자(PID 1)가 기본 Varlink API를 제공. 유닛 상태 조회와 목록 열거가 D-Bus 없이 가능합니다.
- 서비스 관리자 Varlink API (systemd 258) —
io.systemd.ManagerVarlink 인터페이스가 추가되어 현재 관리자 상태 조회, 유닛 및 상태 목록 열거가 가능해집니다. - systemd-udevd Varlink 인터페이스 (systemd 258) — udevd가 Varlink 인터페이스를 통해 런타임 및 수명 주기 작업을 노출합니다. 기존
udevadm과 udevd 간의 비공개 "control" IPC를 대부분 대체합니다. - 서비스 관리자 Varlink 확장 (systemd 259) —
Reload/Reexecute등 D-Bus 기능 동등성(Feature Parity)이 되는 Varlink 호출 추가, 실행 설정과 필터링 기능 확장. - 다양한 컴포넌트 Varlink 지원 확장 (systemd 259) —
systemd-repart,systemd-resolved(신규 Varlink 메서드 추가),systemd-machined(전체 Varlink API 집합 완성),systemd-creds(자격증명 암호화(Encryption)/복호화(Decryption) Varlink 노출). - varlink-http-bridge — 로컬 Varlink 서비스를 HTTP를 통해 노출하는 브리지(Bridge). 웹 클라이언트나 Kubernetes 헬스체크에서 systemd 컴포넌트를 직접 조회할 수 있게 됩니다.
- varlinkctl 개선 —
list-methods동사 추가(서비스 메서드 목록),--quiet/--graceful/--timeout옵션 추가. SSH를 통한 원격 Varlink 호출(ssh-exec:주소 지정) 지원.
| 컴포넌트 | systemd 257 | systemd 258 | systemd 259 |
|---|---|---|---|
| 서비스 관리자(PID 1) | - | 기본 조회/목록 | Reload/Reexecute, 실행 설정 |
| systemd-udevd | - | 런타임/수명주기 API | 확장 |
| systemd-machined | 일부 | - | 전체 API 완성 |
| systemd-resolved | 일부 | - | 신규 메서드 추가 |
| systemd-repart | - | - | 신규 지원 |
| systemd-creds | - | - | 신규 지원 |
| varlinkctl | 기본 | - | list-methods, SSH 원격 호출 |
설계 시사점: systemd 258~259 이후 신규 시스템 서비스 개발 시, Varlink 인터페이스 설계를 D-Bus와 동시에 고려해야 합니다. io.systemd.* 네임스페이스의 인터페이스를 참고하면 일관성 있는 Varlink IDL 작성이 가능합니다. 기존 D-Bus 인터페이스를 유지하면서 Varlink를 추가 노출하는 패턴이 현재 systemd의 표준 방식입니다.
실무 권장(2026년 기준): 신규 시스템 서비스는 Varlink를 우선 고려하되, 기존 D-Bus API를 제공해야 하는 경우 병행 노출하는 것이 현재 systemd의 표준 패턴입니다. D-Bus 기반 서비스라면 반드시 dbus-broker를 사용하고, 권한 검증은 cgroup 경로 + SCM_PIDFD 조합으로 설계하세요.
참고자료
- D-Bus Specification (freedesktop.org) — D-Bus 공식 사양 문서입니다
- D-Bus 프로젝트 위키 — freedesktop.org D-Bus 프로젝트 페이지(Page)입니다
- 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 레퍼런스 구현의 공식 소스 코드 저장소입니다
- varlink.org — Varlink 프로토콜 사양, IDL 문법, 기존 구현체 목록 공식 사이트입니다
- sd-varlink(3) (systemd) — systemd 257에서 정식 공개된 Varlink 라이브러리 매뉴얼입니다
- sd-json(3) (systemd) — Varlink 페이로드 직렬화용 JSON 라이브러리 매뉴얼입니다
- systemd: User/Group Record Lookup API via Varlink — Varlink로 제공되는 대표 시스템 서비스 API 문서입니다
- dbus-broker Releases — v36/v37 릴리스 노트에서 SCM_PIDFD와 notify-reload 변경을 확인할 수 있습니다
- Phoronix: Systemd Looking At A Future With More Varlink & Less D-Bus — 2024년 systemd 개발팀의 공식 방향성 발표 기사입니다
- LWN: systemd 257 released — sd-varlink/sd-json 공개와 19개 Varlink 인터페이스 도입을 다룹니다
- Arch Linux: Making dbus-broker our default D-Bus daemon — 주요 배포판의 dbus-broker 기본 채택 공지입니다
- Announcing dbus 1.16.0 (freedesktop.org 메일링 리스트) — dbus 레퍼런스 구현 신규 안정 브랜치 릴리스 공지로, Meson 전환, ProcessFD, Y2038 안전성 등 주요 변경사항을 설명합니다
- dbus-broker v37 Release Notes (GitHub) — notify-reload 지원, /etc·/run 서비스 탐색, GetStats 확장 등 v37 변경사항입니다
- systemd v258 Release Notes (GitHub) — 서비스 관리자 기본 Varlink API 및 systemd-udevd Varlink 인터페이스 추가 내용을 확인할 수 있습니다
- systemd v259 Release Notes (GitHub) — Varlink 기능 동등성 확장(Reload/Reexecute), systemd-repart/machined/resolved/creds Varlink 추가 내용입니다
- varlink-http-bridge (GitHub) — 로컬 Varlink 서비스를 HTTP로 노출하는 브리지 구현체입니다
관련 문서
- IPC (Inter-Process Communication) — Unix 도메인 소켓, pipe, System V IPC 등 커널 IPC 메커니즘 전반
- systemd — D-Bus를 핵심 통신 채널로 사용하는 시스템·서비스 관리자
- firewalld — D-Bus API를 통한 동적 방화벽 관리 실전 예제