#DRM #DRI #udev

DRM(Direct Rendering Manager)는 DRI(Direct Rendering Infrastructure)를 지원하는 그래픽 카드를 위한 디바이스 드라이버를 말한다. DRM과 DRI에 대해서는 다음 기회에 정리해야 할 것 같다. 이 노트에서는 주 그래픽 카드에 대응되는 DRM의 디바이스 노드를 찾는 방법만 정리하였다.

보통은 /dev/dri/card0를 주 그래픽 카드에 대응되는 디바이스 노드라고 봐도 무방하다. 단, 그래픽 카드가 DRI를 지원하지 않는 경우 또는 복수의 그래픽 카드를 사용하는 경우를 제외하고 말이다. 따라서, 좀 더 확실한 방법으로 주 그래픽 카드를 식별할 수 있는 방법이 필요하다.

방법 1. udevadm을 이용한 방법

udevadmudev에 의해 생성된 디바이스 노드에 대응되는 장치의 속성들을 출력한다. 주 그래픽 카드는 boot_vga 속성의 값으로 1을 갖는다. 따라서, udevadm으로 출력한 특정 디바이스 노드의 속성 중에 boot_vga 속성의 값이 1이라면, 해당 노드는 주 그래픽 카드에 대응된다. 만약, “/dev/dri/card0”에 대응되는 그래픽 카드의 속성들을 보기 위해서는 아래와 같이 명령어를 입력하면 된다. “/dev/dri/card0”는 udev 관련 용어로 DEVNAME이라고 부르는 듯 하다.

	$ udevadm info --query=all --name=/dev/dri/card0
	
	P: /devices/pci0000:00/0000:00:0f.0/drm/card0
	N: dri/card0
	E: DEVNAME=/dev/dri/card0
	E: DEVPATH=/devices/pci0000:00/0000:00:0f.0/drm/card0
	E: DEVTYPE=drm_minor
	E: ID_FOR_SEAT=drm-pci-0000_00_0f_0
	E: ID_PATH=pci-0000:00:0f.0
	E: ID_PATH_TAG=pci-0000_00_0f_0
	E: MAJOR=226
	E: MINOR=0
	E: SUBSYSTEM=drm
	E: TAGS=:master-of-seat:uaccess💺
	E: USEC_INITIALIZED=8444405

udev는 핫플러그(Hotplug) 방식으로 연결된 장치들에 대해 “/dev” 아래에 디바이스 노드들을 생성한다. 사실, udevsysfs가 제공하는 장치들의 정보를 가지고 “/dev”를 구성한다고 한다. 장치들의 정보는 “/sys” 아래에 계층적으로 제공되며, 이 경로는 udev 관련 용어로 DEVPATH라고 한다. 위 결과를 통해서, DEVNAME인 “/dev/dri/card0”에 대응되는 DEVPATH는 “/devices/pci0000:00/0000:00:0f.0/drm/card0”임을 알 수 있다. DEVPATH를 가지고도 아래와 같이 장치의 정보를 조회할 수 있다.

	$ udevadm info -a -p /sys/devices/pci0000:00/0000:00:0f.0/drm/card0
	
	Udevadm info starts with the device specified by the devpath and then
	walks up the chain of parent devices. It prints for every device
	found, all possible attributes in the udev rules key format.
	A rule to match, can be composed by the attributes of the device
	and the attributes from one single parent device.
	
	  looking at device '/devices/pci0000:00/0000:00:0f.0/drm/card0':
	    KERNEL=="card0"
	    SUBSYSTEM=="drm"
	    DRIVER==""
	
	  looking at parent device '/devices/pci0000:00/0000:00:0f.0':
	    KERNELS=="0000:00:0f.0"
	    SUBSYSTEMS=="pci"
	    DRIVERS=="vmwgfx"
	    ATTRS{boot_vga}=="1"
	    ATTRS{broken_parity_status}=="0"
	    ATTRS{class}=="0x030000"
	    ATTRS{consistent_dma_mask_bits}=="32"
	    ATTRS{d3cold_allowed}=="0"
	    ATTRS{device}=="0x0405"
	    ATTRS{dma_mask_bits}=="32"
	    ATTRS{driver_override}=="(null)"
	    ATTRS{enable}=="1"
	    ATTRS{irq}=="16"
	    ATTRS{local_cpulist}=="0-1"
	    ATTRS{local_cpus}=="00000000,00000000,00000000,00000003"
	    ATTRS{msi_bus}=="1"
	    ATTRS{numa_node}=="-1"
	    ATTRS{subsystem_device}=="0x0405"
	    ATTRS{subsystem_vendor}=="0x15ad"
	    ATTRS{vendor}=="0x15ad"
	
	  looking at parent device '/devices/pci0000:00':
	    KERNELS=="pci0000:00"
	    SUBSYSTEMS==""
	    DRIVERS==""

방법 2. libudev를 이용한 방법

C코드에서는 libudev를 이용하여 주 그래픽 카드에 대응되는 디바이스 노드를 찾을 수 있다. westonGNOME이 사용하는 컴포지터인 mutter에 반영되어 있다. 이 부분은 1개 이상의 그래픽 카드가 장착된 경우를 고려한 것이다. 패치 전에는 “open("/dev/dri/card0")” 형태로 하드코딩하고 있었다. 패치는 udev_enumerate_*() 함수들을 이용해 “drm/card[0-9]*“에 매칭되는 디바이스 노드들을 순회하여 boot_vga 속성을 찾는다. 결국, 해당 속성을 갖는, 즉, 주 그래픽 카드의 정보를 담은 udev_device 객체(libudev가 제공하는 구조체)를 얻어낸다. 패치의 내용은 아래 링크에서 확인할 수 있다.

패치로 추가되는 함수인 find_primary_gpu()는 이름부터가 매우 직관적이다. 아래는 weston 내부에서 해당 함수까지 이어지는 콜 스택의 일부이다.

  • libweston/compositor-drm.c : find_primary_gpu
  • libweston/compositor-drm.c : drm_backend_create
  • libweston/compositor-drm.c : weston_backend_init(WL_EXPORT)
  • libweston/compositor.c : weston_load_module(WL_EXPORT)

참고, weston_load_module()

두 문자열 “name”과 “entrypoint”를 입력받는다. 우선 “name”을 이용하여 “.libs/[name]”에 위치한 라이브러리를 동적으로 로딩한다. “name”은 로딩하려는 라이브러리의 파일 이름을 말한다. weston은 백엔드(backend)마다 로딩할 라이브러리의 파일 이름을 하드코딩으로 매핑하고 있다. “libweston/compositor.c”의 backend_map[]이라는 문자열 배열이 그것이다. “[drm|fbdev|headless|rdp|wayland|x11]-backend.so"가 배열에 존재하고, 배열 참조를 위한 인덱스는 “libweston/compositor.h” 의 enum 타입인 “weston_compositor_backend”를 이용한다. 즉, “backend_map[WESTON_BACKEND_DRM]”은 “drm-backend.so"라는 문자열로 매핑된다. 이 문자열이 weston_load_moudle() 함수의 첫 번째 인자인 “name”으로 전달되는 것이다. 백엔드의 종류와 각각에 매핑되는 문자열은 아래와 같다.

/// libweston/compositor.h
enum weston_compositor_backend {
	WESTON_BACKEND_DRM,
	WESTON_BACKEND_FBDEV,
	WESTON_BACKEND_HEADLESS,
	WESTON_BACKEND_RDP,
	WESTON_BACKEND_WAYLAND,
	WESTON_BACKEND_X11,
};
			
// libweston/compositor.c
static const char * const backend_map[] = {
	[WESTON_BACKEND_DRM] =		"drm-backend.so",
	[WESTON_BACKEND_FBDEV] =	"fbdev-backend.so",
	[WESTON_BACKEND_HEADLESS] =	"headless-backend.so",
	[WESTON_BACKEND_RDP] =		"rdp-backend.so",
	[WESTON_BACKEND_WAYLAND] =	"wayland-backend.so",
	[WESTON_BACKEND_X11] =		"x11-backend.so",
};

weston_load_module()은 인자로 받은 첫 번째 문자열인 “name”에 위치한 라이브러리를 함수 dlopen()으로 로딩한다. 인자로 받은 두 번째 문자열인 “entrypoint”는 로딩한 라이브러리 내에서 호출할 함수의 이름을 의미한다. 즉, dlsym을 이용하여 “entrypoint”, 즉, 함수 weston_backend_init()의 주소를 찾아 호출한다. libdl은 이처럼 런타임에 라이브러리를 로딩하고 해당 라이브러리의 심볼들에 접근할 수 있게끔 도와준다. weston_load_module()에 도달하기까지의 콜 스택은 다음과 같다.

  • libweston/compositor.c : weston_load_module(WL_EXPORT)
  • libweston/compositor.c : weston_compositor_load_backend(WL_EXPORT)
  • compositor/main.c : weston_compositor_load_backend()
  • compositor/main.c : load_backend()
  • compositor/main.c : main()