사전 다이렉트 렌더링 매니저
구현체

다이렉트 렌더링 매니저

gabury1고친 사람 github-actions[bot]

다이렉트 렌더링 매니저는 리눅스에서 여러 프로그램이 그래픽 칩 하나를 함께 쓰게 해 줍니다. 그래픽 칩을 만든 회사가 달라도 메모리를 나누고 명령을 넘기는 일은 비슷합니다. 리눅스는 그 겹치는 일을 커널 안의 이 틀에 모아 두었습니다. 회사마다 만드는 드라이버는 이 틀 위에 자기 칩에 맞는 부분만 얹습니다.

쉽고 빠른 이해

무슨 일을 하는 물건인가 — 리눅스 커널 안에서 그래픽 칩을 관리하는 공통 틀입니다. 브라우저와 게임이 한 그래픽 칩으로 동시에 그려도 서로의 그림을 건드리지 못하게 막습니다.

왜 이렇게 하나 — 그래픽 칩을 직접 만지는 일은 커널만 할 수 있습니다. 그런데 메모리를 나누고 화면을 내보내는 일은 칩 회사가 달라도 거의 같습니다. 회사마다 따로 만들면 같은 코드와 같은 버그가 여러 벌 생깁니다.

어떻게 도나

  1. 프로그램이 /dev/dri 아래의 장치 파일을 열어 그래픽 칩에 일을 부탁합니다
  2. 공통 틀이 메모리를 떼어 줍니다. 그리기 명령 묶음은 회사별 드라이버를 거쳐 그래픽 칩에 넘깁니다
  3. 모니터 설정과 화면 교체는 화면을 맡은 프로그램 하나만 할 수 있습니다

대가 — 드라이버가 커널 안에 있어서 커널 판에 묶입니다. 새로 나온 그래픽 칩을 쓰려면 커널부터 올려야 할 때가 많습니다.

언제 만나나 — 컨테이너 안에서 그래픽 칩으로 영상을 변환할 때 /dev/dri 아래의 파일을 컨테이너에 넘겨 줍니다.

상세

이 절은 다이렉트 렌더링 매니저가 커널 안에서 맡는 일을 차례로 봅니다. 프로그램이 그래픽 칩에 닿는 길이 먼저입니다. 그 뒤로 메모리 · 명령 · 화면 출력 · 권한을 하나씩 보고, 그 위에 붙는 사용자 공간 소프트웨어와 서버에서 만나는 모습으로 끝냅니다.

이름과 줄임말

다이렉트 렌더링 매니저는 줄여서 DRM(Direct Rendering Manager)이라고 부릅니다. 같은 줄임말을 쓰는 디지털 저작권 관리(Digital Rights Management)와는 상관이 없습니다. 이 편의 DRM 은 언제나 리눅스의 그래픽 장치를 가리킵니다.

DRM 은 리눅스 커널의 한 부분입니다. 커널은 하드웨어를 직접 만지고 그 하드웨어를 프로그램들에 나눠 주는 운영체제의 중심부입니다. DRM 이 맡는 하드웨어는 그림 계산을 하는 칩인 GPU(Graphics Processing Unit, 그래픽 처리 장치)입니다.

이름의 「다이렉트 렌더링」은 직접 그린다는 뜻입니다. 예전 리눅스 화면은 X 윈도 시스템의 X 서버라는 프로그램 하나가 도맡아 그렸습니다. 다른 프로그램은 그려 달라는 요청을 X 서버에 보내야 했습니다. 프로그램이 X 서버를 거치지 않고 GPU 에 바로 그리게 하려고 만든 커널 쪽 장치가 DRM 의 시작입니다.

지금은 그보다 맡는 일이 훨씬 많습니다. GPU 메모리 배분, 명령 전달, 모니터 출력까지 GPU 를 둘러싼 커널 쪽 일 대부분이 DRM 에 모였습니다.

공통 틀과 GPU 별 드라이버

GPU 는 회사마다 구조가 다릅니다. 같은 회사라도 세대가 바뀌면 또 달라집니다. 그러니 GPU 를 움직이는 코드는 GPU 마다 따로 있어야 합니다. 이 코드가 그래픽 드라이버입니다.

그래도 드라이버마다 겹치는 일이 많습니다. 메모리를 나눠 주는 일, 프로그램에서 명령을 받는 일, 모니터에 화면을 내보내는 일이 그렇습니다. DRM 은 이 겹치는 부분을 한 벌로 만들어 둔 공통 틀입니다. 이 공통 부분을 흔히 DRM 코어라고 부릅니다.

GPU 별 드라이버는 DRM 코어 위에 자기 GPU 에 맞는 부분만 얹습니다. 이 드라이버들은 필요할 때 커널에 끼워 넣는 커널 모듈로 들어 있습니다. 인텔 GPU 의 i915, AMD(Advanced Micro Devices) GPU 의 amdgpu, 엔비디아 GPU 의 오픈소스 드라이버인 nouveau 가 그런 모듈입니다.

일반 프로그램은 사용자 공간이라는 구역에서 돕니다. 사용자 공간의 프로그램은 GPU 도 DRM 코어도 직접 만지지 못합니다. 아래 그림처럼 커널을 거쳐서만 GPU 에 닿습니다.

flowchart TD
    subgraph U["사용자 공간"]
        A["프로그램"]
    end
    subgraph K["커널"]
        B["DRM 코어"]
        C["GPU 별 드라이버 모듈"]
    end
    subgraph H["하드웨어"]
        D["GPU"]
    end
    A --> B
    B --> C
    C --> D

장치 파일로 여는 창구

리눅스는 장치를 장치 파일이라는 파일로 보이게 합니다. 장치 파일은 /dev 아래에 모입니다. DRM 의 장치 파일은 /dev/dri 아래에 있습니다.

프로그램은 이 파일을 열어 파일 디스크립터를 받습니다. 파일 디스크립터는 커널이 열린 파일마다 붙여 주는 번호입니다. 그 뒤로는 이 번호를 대고 GPU 에 일을 부탁합니다.

프로그램이 커널에 일을 맡기는 호출을 시스템 콜이라고 합니다. 파일을 읽는 read 와 파일에 쓰는 write 가 그런 호출입니다. 사용자 공간의 프로그램은 시스템 콜을 거쳐야만 커널의 기능을 씁니다.

DRM 에 일을 맡길 때는 시스템 콜 가운데 ioctl(input/output control, 입출력 제어)을 씁니다. ioctl 은 읽기와 쓰기로 나타내기 어려운 장치별 명령을 보내는 시스템 콜입니다. 「메모리를 한 덩어리 떼어 달라」와 「이 명령 묶음을 실행해 달라」가 각각 ioctl 하나입니다.

ioctl 에는 두 종류가 있습니다. 모든 GPU 에 공통인 ioctl 은 DRM 코어가 받습니다. 명령 묶음 제출처럼 GPU 마다 모양이 다른 ioctl 은 드라이버 모듈이 따로 정해 둡니다.

프라이머리 노드와 렌더 노드

/dev/dri 아래의 파일은 두 종류입니다. GPU 하나마다 대개 card0 같은 파일과 renderD128 같은 파일이 하나씩 생깁니다. 앞의 것을 프라이머리 노드, 뒤의 것을 렌더 노드라고 부릅니다.

프라이머리 노드로는 모니터 설정까지 다룹니다. 화면을 바꿀 수 있는 파일이라 아무 프로그램에나 열어 주기 어렵습니다.

렌더 노드로는 그리기와 계산만 할 수 있습니다. 화면을 건드릴 수 없으니 여러 프로그램에 열어 주어도 서로의 화면을 망치지 않습니다. 화면 없이 GPU 로 계산만 하는 프로그램은 렌더 노드면 충분합니다.

버퍼로 나눠 주는 GPU 메모리

GPU 가 그리는 데 쓰는 자료는 메모리에 놓입니다. 그림 파일인 텍스처, 점의 좌표, 그려진 결과가 모두 메모리에 있습니다. 버퍼는 이런 자료를 담는 크기가 정해진 메모리 한 덩어리입니다. DRM 은 GPU 메모리를 버퍼 단위로 떼어 줍니다.

이 일을 하는 DRM 의 부분을 GEM(Graphics Execution Manager)이라고 부릅니다. 프로그램이 버퍼를 만들어 달라고 하면 GEM 이 버퍼를 만듭니다. 버퍼를 지우거나 다른 장치와 나눌 때도 GEM 을 거칩니다.

GEM 은 만든 버퍼의 메모리 주소 대신 핸들을 돌려줍니다. 핸들은 그 버퍼를 가리키는 정수 번호입니다. 프로그램은 이후 이 번호로 버퍼를 가리킵니다.

핸들은 그 버퍼를 만든 파일 디스크립터 안에서만 뜻이 있습니다. 다른 프로세스에 같은 번호를 건네면 아무것도 못 가리킵니다.

그래서 버퍼를 넘길 때는 dma-buf 를 씁니다. dma-buf 의 DMA(Direct Memory Access, 직접 메모리 접근)는 CPU 를 거치지 않고 장치가 메모리를 바로 읽고 쓰는 방식입니다. dma-buf 는 버퍼를 파일 디스크립터 하나로 바꿔 다른 프로세스나 다른 장치에 넘기는 커널의 장치입니다.

dma-buf 로 넘기면 버퍼를 복사하지 않고 나눠 씁니다. 영상 디코더가 풀어 놓은 프레임을 GPU 가 바로 읽을 때 이렇게 합니다.

창을 화면에 띄울 때도 같습니다. 여러 창을 모아 한 장의 화면으로 내보내는 프로그램을 컴포지터라고 합니다. 앱이 그린 창은 dma-buf 로 컴포지터에 건너갑니다. 컴포지터는 뒤의 「화면 주인은 한 프로그램」에서 다시 봅니다.

GPU 메모리가 어디 있는지는 GPU 종류마다 다릅니다. 별도 그래픽 카드는 카드에 붙은 전용 메모리인 VRAM(Video Random Access Memory, 비디오 메모리)을 씁니다. CPU 와 한 칩에 들어간 통합 그래픽은 일반 메모리를 CPU 와 나눠 씁니다.

VRAM 이 모자라면 덜 쓰는 버퍼를 일반 메모리로 내려보냈다가 필요할 때 다시 올려야 합니다. 이 옮기는 일을 맡는 DRM 의 부분이 TTM(Translation Table Manager)입니다.

명령 묶음을 GPU 에 넘기기

그리기 함수를 GPU 명령으로 바꾸는 일은 커널 밖에서 합니다. 프로그램 안에 라이브러리로 올라온 사용자 공간 쪽 드라이버가 그 일을 맡습니다.

까닭은 호출 횟수입니다. 그리기 함수는 한 화면에도 수없이 불립니다. 호출마다 커널로 넘어가면 그 시간이 쌓입니다.

사용자 공간 쪽 드라이버는 바꾼 명령을 커맨드 버퍼에 차례로 적어 둡니다. 커맨드 버퍼는 GPU 명령을 모아 두는 버퍼입니다. 명령을 하나씩 보내지 않고 모았다가 한 번에 넘기려고 씁니다.

명령이 모이면 묶음째 커널에 넘깁니다. 이 넘기는 일을 커맨드 서브미션이라고 부릅니다. 넘길 때는 드라이버 모듈이 정한 ioctl 을 씁니다.

여러 프로그램이 동시에 묶음을 넘기면 커널이 차례를 정해 GPU 에 올립니다. 이 차례를 정하는 GPU 스케줄러는 DRM 코어에 들어 있어서 여러 드라이버가 같이 씁니다.

프로그램은 GPU 가 묶음을 다 끝냈는지 알아야 그 결과를 씁니다. 그래서 묶음마다 펜스가 따라붙습니다. 펜스는 GPU 가 그 묶음을 끝내면 「끝났다」로 바뀌는 표시입니다. 프로그램이나 다른 장치는 이 표시를 보고 기다립니다.

화면 출력을 맡는 KMS

모니터에 띄울 화면의 모양을 정하는 일을 모드 설정이라고 합니다. 해상도와 주사율을 고르는 일이 여기 듭니다. 주사율은 모니터가 1초에 화면을 새로 그리는 횟수입니다.

DRM 에서 모드 설정을 맡는 부분이 KMS(Kernel Mode Setting, 커널 모드 설정)입니다. 이름 그대로 모드 설정을 커널 안에서 합니다.

예전에는 X 서버가 사용자 공간에서 모드 설정을 했습니다. 부팅 화면에서 X 서버로 넘어갈 때마다 화면이 깜빡였습니다. X 서버가 죽으면 모니터가 엉뚱한 설정으로 남기도 했습니다. 이 일을 커널로 옮긴 것이 KMS 입니다.

KMS 는 화면 출력 장치를 몇 가지 부품으로 나눠 보여 줍니다. 그려진 결과는 아래 표의 위에서 아래 순서로 지나 모니터에 닿습니다.

부품 하는 일
프레임버퍼 모니터에 내보낼 한 장의 픽셀을 담은 버퍼
플레인 프레임버퍼 하나를 받아 화면 위 어디에 얹을지 정하는 층. 바탕 화면용 · 마우스 커서용 · 겹쳐 띄우기용이 있다
CRTC(Cathode Ray Tube Controller) 플레인들을 겹쳐 한 장으로 만들고 모니터의 박자에 맞춰 내보낸다. 옛 브라운관 모니터의 제어기에서 온 이름이다
인코더 CRTC 가 내보낸 신호를 단자에 맞는 형식으로 바꾼다
커넥터 모니터가 꽂히는 단자 하나

모니터를 새로 꽂으면 커넥터 하나가 「연결됨」으로 바뀝니다. 컴포지터는 그 커넥터에 CRTC 를 이어 주어 새 모니터에 화면을 띄웁니다.

화면 교체와 vblank

화면은 보통 프레임버퍼 두 장으로 그립니다. 모니터가 한 장을 읽어 가는 동안 다른 장에 다음 화면을 그립니다. 다 그리면 두 장의 역할을 바꿉니다. 이 바꾸기를 페이지 플립이라고 합니다.

모니터는 한 장을 위에서 아래로 훑어 그린 뒤 잠깐 쉽니다. 이 틈을 vblank(vertical blanking, 수직 공백 구간)라고 부릅니다. 페이지 플립은 이 틈에 해야 합니다.

틈이 아닐 때 바꾸면 위쪽은 앞 장, 아래쪽은 새 장인 화면이 보입니다. 이 어긋남이 티어링입니다.

티어링을 막으려고 컴포지터는 「다음 vblank 에 이 버퍼로 바꿔 달라」고 KMS 에 부탁합니다. KMS 는 그 틈에 바꾼 뒤 이벤트로 알려 줍니다. 컴포지터는 그 알림을 받고 다음 장을 그리기 시작합니다.

화면 주인은 한 프로그램

모니터 설정을 여러 프로그램이 동시에 바꾸면 화면이 엉망이 됩니다. 그래서 프라이머리 노드를 연 프로그램 가운데 하나만 마스터가 됩니다. 마스터만 모드 설정과 페이지 플립을 할 수 있습니다.

마스터는 대개 앞에서 본 컴포지터나 X 서버입니다. 다른 앱은 마스터가 될 필요 없이 렌더 노드로 그리기만 합니다.

DRM 위에 붙는 사용자 공간 셋

DRM 은 GPU 를 다루는 일의 커널 쪽 절반입니다. 나머지 절반은 사용자 공간의 라이브러리와 프로그램이 맡습니다. 이 소절부터 세 소절은 그중 DRM 에 가장 가깝게 붙는 셋인 libdrm · Mesa · 컴포지터를 봅니다.

아래 그림이 Mesa 와 컴포지터의 얼개입니다. 앱은 Mesa 를 거쳐 렌더 노드로 그립니다. 다 그린 창은 dma-buf 로 컴포지터에 넘어갑니다. 컴포지터는 프라이머리 노드로 화면을 내보냅니다.

libdrm 은 그림에 따로 그리지 않았습니다. 커널로 내려가는 두 화살표가 ioctl 이고, 이 ioctl 이 대개 libdrm 이라는 라이브러리를 거칩니다.

flowchart TD
    subgraph U["사용자 공간"]
        A["앱"] --> B["Mesa"]
        C["컴포지터"]
        B -.->|"dma-buf 로 창 버퍼"| C
    end
    subgraph K["커널의 DRM"]
        R["렌더 노드"]
        P["프라이머리 노드"]
    end
    B -->|"ioctl"| R
    C -->|"ioctl"| P

ioctl 을 감싸는 libdrm

libdrm 은 DRM 의 ioctl 을 C 함수로 감싼 얇은 라이브러리입니다. ioctl 은 구조체를 채워 번호와 함께 넘기는 꼴이라 직접 쓰기 번거롭습니다. libdrm 이 그 구조체 다루기를 대신해 줍니다.

공통 ioctl 을 감싼 부분과 드라이버별 ioctl 을 감싼 부분이 따로 있습니다. 커넥터 목록을 읽거나 페이지 플립을 거는 함수는 공통 쪽에 있습니다. AMD GPU 용 명령 제출 함수는 libdrm_amdgpu 같은 드라이버별 쪽에 있습니다.

libdrm 은 커널의 DRM 과 판을 맞춰야 합니다. 커널에 새 ioctl 이 생겨도 libdrm 이 그 판까지 안 올라오면 libdrm 을 거치는 프로그램은 그 기능을 못 씁니다.

그리기 API 를 구현하는 Mesa

프로그램이 GPU 에 그리기를 시키는 공통 함수 목록을 그래픽스 API(Application Programming Interface)라고 합니다. OpenGL 과 Vulkan 이 그런 목록입니다. Mesa 는 이 API 들을 리눅스에서 구현한 오픈소스 라이브러리 묶음입니다. 앞에서 본 사용자 공간 쪽 드라이버가 Mesa 안에 들어 있습니다.

프로그램이 그리기 함수를 부르면 Mesa 가 그 GPU 의 명령으로 바꿔 커맨드 버퍼에 적습니다. 버퍼는 GEM 에 부탁해 만듭니다. 다 적은 묶음은 드라이버 모듈의 제출 ioctl 로 넘깁니다.

GPU 위에서 도는 작은 프로그램을 셰이더라고 합니다. 셰이더를 GPU 의 기계어로 바꾸는 일도 Mesa 안에서 끝납니다.

Mesa 의 GPU 별 부분은 이렇게 커널의 드라이버 모듈과 짝을 이룹니다. 인텔 GPU 라면 Mesa 의 인텔 드라이버가 i915 모듈의 ioctl 을 부르는 식입니다. 한쪽만 새 판으로 올리면 새 GPU 기능이 안 열릴 수 있습니다.

화면을 쥐는 컴포지터

Wayland 는 앱과 컴포지터가 창을 주고받는 방식을 정한 리눅스의 화면 프로토콜입니다. Wayland 환경에서는 컴포지터가 곧 화면의 주인입니다. 컴포지터는 프라이머리 노드를 열어 마스터가 된 뒤 KMS 로 모니터를 설정합니다. 앱이 그린 창들을 한 장으로 합친 다음 vblank 에 맞춰 페이지 플립을 겁니다.

앱은 자기 창을 렌더 노드에서 Mesa 로 그립니다. 다 그린 버퍼는 dma-buf 로 바꿔 컴포지터에 건넵니다. 버퍼를 복사하지 않으니 창이 많아도 메모리를 덜 오갑니다.

컴포지터가 화면에 내보낼 버퍼는 KMS 가 읽을 수 있는 꼴이어야 합니다. 그런 버퍼를 만들 때는 Mesa 의 GBM(Generic Buffer Management)을 씁니다.

컴포지터 하나가 화면을 쥐는 이 구조에는 대가가 있습니다. 화면 전체가 그 프로그램 하나에 매달립니다. 컴포지터가 죽으면 그 위의 창들도 대개 함께 닫힙니다.

커널 판에 묶이는 드라이버

DRM 의 드라이버 모듈은 대부분 리눅스 커널 소스 안에 함께 들어 있습니다. 리눅스 커널은 커널 안쪽 함수의 모양을 판마다 고정해 주지 않습니다. 판이 바뀌면 드라이버가 부르는 함수의 이름이나 인자가 달라질 수 있습니다.

그래서 드라이버는 그 판의 커널과 함께 빌드됩니다. 고칠 때도 커널과 함께 고칩니다.

대가는 새 GPU 를 쓸 때 드러납니다. 그 GPU 를 지원하는 코드가 들어오기 전의 커널에서는 GPU 가 제대로 동작하지 않습니다. 새 GPU 를 쓰려면 커널부터 올려야 할 때가 많은 것도 이 때문입니다.

커널 소스 밖에서 따로 배포하는 드라이버도 있습니다. 이런 드라이버는 커널을 올릴 때마다 그 판에 맞춰 다시 빌드해야 합니다. 빌드가 안 맞으면 커널을 올린 뒤 화면이 안 나오는 일이 생깁니다.

컨테이너에서 쓰는 GPU

백엔드 서버에서도 DRM 을 만납니다. 인텔이나 AMD GPU 로 영상을 변환하는 서버가 대표적입니다. 이런 서버는 VA-API(Video Acceleration Application Programming Interface)로 GPU 의 영상 처리 장치를 부릅니다. VA-API 는 영상 압축과 풀기를 GPU 에 맡기는 함수 목록이고, 이 호출은 렌더 노드를 거쳐 GPU 에 닿습니다.

컨테이너는 호스트의 커널을 같이 씁니다. 그러니 컨테이너 안에서 GPU 를 쓰려면 호스트의 DRM 장치 파일을 컨테이너에 넘겨 주면 됩니다. 컨테이너 안에 커널 드라이버를 따로 깔 필요는 없습니다.

GPU 가 하나인 호스트에서는 아래처럼 보입니다. 첫 줄은 호스트의 DRM 장치 파일을 보고, 둘째 줄은 그중 렌더 노드만 컨테이너에 넘깁니다.

터미널
ls /dev/dri      # card0  renderD128
docker run --device /dev/dri/renderD128 my-app

영상 변환에는 화면 권한이 필요 없으니 렌더 노드면 됩니다. 컨테이너 안의 프로세스는 그 파일을 열 권한도 있어야 합니다. 많은 배포판이 렌더 노드를 render 그룹에 열어 두므로 컨테이너 사용자를 그 그룹에 넣어 줍니다.

엔비디아 GPU 의 계산용 CUDA(Compute Unified Device Architecture)는 이 장치 파일을 쓰지 않습니다. 엔비디아 드라이버가 만든 자기 장치 파일을 거칩니다. CUDA 컨테이너에 /dev/dri 만 넘겨서는 GPU 가 안 잡히는 까닭입니다.

관련 항목

다이렉트 렌더링 매니저가 속하는 상위 분류

Linux · 커널 · 디바이스 드라이버 · 그래픽 드라이버 · 커널 모듈 · 그래픽스

다이렉트 렌더링 매니저를 이루는 구성 요소

KMS · GEM · TTM · 렌더 노드 · DRM 마스터 · dma-buf

다이렉트 렌더링 매니저가 프로그램에 여는 창구

장치 파일 · ioctl · 시스템 콜 · 파일 디스크립터 · 사용자 공간

다이렉트 렌더링 매니저가 다루는 하드웨어

GPU · VRAM · DMA · 인터럽트 · 그래픽 카드 · 통합 그래픽

다이렉트 렌더링 매니저가 GPU 에 일을 넘기는 장치

커맨드 버퍼 · 커맨드 서브미션 · 펜싱 · GPU 스케줄링

다이렉트 렌더링 매니저가 화면으로 내보내는 단계

모드 설정 · 프레임버퍼 · CRTC · 페이지 플립 · vblank · 티어링 · 더블 버퍼링 · 주사율 · 해상도

다이렉트 렌더링 매니저를 부르는 사용자 공간 소프트웨어

libdrm · Mesa · GBM · 컴포지터 · Wayland · X 윈도 시스템 · OpenGL · Vulkan · VA-API

다이렉트 렌더링 매니저 위에 얹히는 GPU 드라이버 모듈

i915 · amdgpu · nouveau · NVIDIA 드라이버

다이렉트 렌더링 매니저 대신 화면과 GPU 를 다루는 다른 장치

fbdev · WDDM · IOKit

다이렉트 렌더링 매니저의 장치 파일을 넘겨 쓰는 서버 환경

컨테이너 · Docker · 호스트 · CUDA · 헤드리스 브라우저 · 영상 트랜스코딩

다른 이름: Direct Rendering Manager · DRM · 리눅스 DRM · DRM 서브시스템