DMA
고친 사람 github-actions[bot]
DMA 는 장치가 프로세서를 거치지 않고 메모리와 데이터를 직접 주고받게 해 주는 방식입니다. 프로세서는 무엇을 어디로 옮길지만 일러 두고 다른 일로 넘어갑니다. 옮기기가 끝나면 장치가 다 됐다고 알려 옵니다. 디스크와 네트워크 카드는 큰 데이터를 대개 이 방식으로 주고받습니다.
쉽고 빠른 이해
DMA 는 장치와 메모리 사이의 데이터 나르기를 프로세서 대신 맡아 주는 방식입니다. 예를 들어 네트워크 카드는 받은 데이터를 프로세서 손을 빌리지 않고 메모리에 곧장 써 넣습니다.
이게 없으면 프로세서가 데이터를 한 조각씩 직접 날라야 합니다. 장치는 프로세서보다 한참 느립니다. 그래서 프로세서는 나르는 내내 장치를 기다리느라 다른 일을 못 합니다.
어떻게 도는가:
- 프로세서가 메모리의 어디로 얼마만큼 옮길지 장치에 적어 줍니다
- 장치가 메모리와 데이터를 직접 주고받습니다. 그동안 프로세서는 다른 일을 합니다
- 다 옮기면 장치가 신호를 보내 프로세서를 부릅니다
대가는 셋입니다.
- 프로세서는 자주 쓰는 값을 가까이 복사해 둡니다. 장치가 프로세서 모르게 메모리를 바꾸면 이 사본과 어긋나므로 맞춰 줘야 합니다
- 장치가 메모리 아무 데나 쓸 수 있어서 잘못 쓰면 운영체제의 데이터까지 덮입니다
- 몇 바이트짜리 작은 데이터는 준비하는 품이 더 들어서 이 방식으로 옮기지 않습니다
상세
DMA(Direct Memory Access, 직접 메모리 접근)는 장치가 메모리를 직접 읽고 쓰게 하는 방식입니다. 개요에서 말한 프로세서는 CPU(Central Processing Unit, 중앙 처리 장치)입니다. 장치는 디스크·네트워크 카드·그래픽 카드처럼 컴퓨터에 붙어 데이터를 들이고 내보내는 부품을 말합니다.
이 절은 먼저 DMA 가 없을 때 CPU 가 데이터를 어떻게 나르는지 봅니다. 그다음 DMA 로 한 번 옮기는 흐름과 그 일을 맡는 부품을 따라갑니다.
이어서 DMA 가 남기는 숙제 셋을 봅니다. 장치에 적어 줄 주소, CPU 가 가까이 복사해 둔 값과 어긋나는 문제, 장치의 메모리 접근을 가두는 일입니다. 끝으로 DMA 가 치르는 비용과 백엔드 코드가 DMA 를 만나는 때를 봅니다.
이삿짐을 옮기는 장면과 닮았습니다. 집주인이 상자를 하나하나 직접 나르면 이사가 끝날 때까지 다른 일을 못 합니다. 이삿짐 센터에 무엇을 어디로 옮길지 적어 주면 집주인은 그동안 제 일을 봅니다. 다 옮기고 나면 센터가 전화로 알려 줍니다.
CPU 가 직접 나르는 프로그램 입출력
DMA 가 없으면 CPU 가 데이터를 직접 나릅니다. 이 방식을 PIO(Programmed I/O, 프로그램 입출력)라고 부릅니다. CPU 가 명령을 하나씩 실행해 데이터를 옮긴다는 뜻에서 붙은 이름입니다.
장치는 CPU 와 말을 주고받으려고 작은 저장 칸 몇 개를 열어 둡니다. 이 칸을 장치 레지스터라고 합니다.
PIO 에서 CPU 는 장치 레지스터에서 데이터 한 조각을 읽습니다. 읽은 조각을 메모리에 씁니다. 옮길 데이터가 끝날 때까지 이 둘을 되풀이합니다.
문제는 옮기는 내내 CPU 가 이 일에 묶인다는 것입니다. 디스크 블록 하나만 해도 수천 바이트라서 CPU 는 같은 명령을 수천 번 되풀이합니다. 게다가 장치는 CPU 보다 한참 느립니다. CPU 는 장치가 다음 조각을 내놓기를 기다리며 시간을 흘려보냅니다.
DMA 로 한 번 옮기는 흐름
이 소절은 디스크에서 메모리로 데이터 한 덩어리를 읽어 오는 한 번을 따라갑니다. 등장하는 쪽은 셋입니다. CPU, 장치, 메모리입니다.
CPU 쪽에서 이 일을 맡는 코드는 디바이스 드라이버입니다. 디바이스 드라이버는 운영체제 안에서 특정 장치를 다루는 코드입니다. 장치마다 레지스터의 생김새가 달라서 그 장치를 아는 코드가 따로 있어야 합니다.
드라이버는 먼저 데이터를 받을 빈 공간을 메모리에 마련합니다. 이 공간을 DMA 버퍼라고 부릅니다. 장치는 이 버퍼에 데이터를 써 넣습니다.
그다음 드라이버는 장치 레지스터에 전송 조건을 적습니다. 메모리의 어느 주소로, 얼마만큼, 어느 방향으로 옮길지입니다. 마지막으로 시작하라는 값을 적습니다. 이 뒤로 CPU 는 이 전송에서 손을 뗍니다.
장치는 적힌 대로 메모리에 데이터를 써 넣습니다. 그동안 CPU 는 다른 프로세스를 돌립니다.
다 옮기면 장치는 CPU 에게 신호를 보내 끝났음을 알립니다. 장치가 CPU 에게 보내는 이 신호를 인터럽트라고 합니다. 신호를 받은 CPU 는 하던 일을 잠시 멈추고 드라이버의 처리 코드로 건너갑니다.
sequenceDiagram
participant C as CPU · 드라이버
participant D as 장치
participant M as 메모리
C->>M: DMA 버퍼를 마련한다
C->>D: 주소 · 길이 · 방향을 적고 시작시킨다
Note over C: 다른 프로세스를 돌린다
D->>M: 데이터를 직접 써 넣는다
D-->>C: 인터럽트 · 다 옮겼다
C->>M: 버퍼의 데이터를 꺼내 쓴다
그림에서 CPU 가 나서는 것은 처음과 끝뿐입니다. 가운데 데이터가 오가는 구간에는 CPU 가 없습니다. 메모리의 데이터를 장치로 내보낼 때도 같습니다. 방향만 바뀌어 장치가 메모리를 읽어 갑니다.
아래 표는 앞의 PIO 와 DMA 를 견줍니다. 가르는 것은 누가 나르고 그동안 CPU 가 무엇을 하느냐입니다.
| PIO | DMA | |
|---|---|---|
| 데이터를 나르는 쪽 | CPU | 장치 |
| 옮기는 동안 CPU | 나르기에 묶인다 | 다른 일을 한다 |
| 끝났음을 아는 법 | CPU 가 직접 나르니 따로 없다 | 장치가 인터럽트로 알린다 |
| 어울리는 데이터 | 몇 바이트짜리 명령·상태 값 | 블록·패킷 같은 큰 덩어리 |
표의 마지막 줄은 뒤의 「DMA 가 치르는 비용」 소절에서 다시 봅니다. DMA 는 준비하는 품이 들어서 작은 데이터에는 손해이기 때문입니다.
옮기는 일을 맡는 DMA 컨트롤러
앞 소절에서는 장치가 데이터를 나른다고만 했습니다. 그 나르기를 맡는 부품이 따로 있습니다.
장치와 메모리 사이에서 데이터를 나르는 부품을 DMA 컨트롤러라고 합니다. 메모리 주소와 남은 길이를 세어 가며 한 조각씩 옮기는 작은 회로입니다. PIO 에서 CPU 가 하던 되풀이를 이 회로가 대신 맡습니다. 앞에서 「장치가 나른다」고 한 것은 장치가 쓰는 DMA 컨트롤러가 나른다는 뜻입니다.
CPU·메모리·장치는 버스로 이어져 있습니다. 버스는 부품들이 함께 쓰는 데이터 통로입니다. 한 번에 한 부품만 버스에 데이터를 실을 수 있습니다. 그래서 DMA 컨트롤러는 옮기기 전에 버스를 쓸 권리를 넘겨받습니다.
DMA 컨트롤러가 놓이는 곳은 두 갈래입니다. 옛 컴퓨터는 기판에 DMA 컨트롤러 하나를 두고 여러 장치가 나눠 썼습니다. 요즘의 네트워크 카드나 SSD(Solid State Drive, 반도체 저장 장치)는 장치 안에 DMA 컨트롤러를 품습니다.
장치 안에 컨트롤러가 있으면 장치가 스스로 버스를 잡고 메모리에 닿습니다. 장치가 이렇게 직접 버스를 잡는 것을 버스 마스터링이라고 합니다.
장치가 쓰는 물리 주소
프로그램이 쓰는 주소와 장치가 쓰는 주소는 다릅니다. 이 소절은 그 차이가 DMA 에 무엇을 요구하는지 봅니다.
가상 메모리를 쓰는 운영체제에서 프로그램은 가상 주소로 메모리를 가리킵니다. 가상 주소는 프로세스마다 따로 매긴 번호입니다. 메모리 칩에서 데이터가 놓인 실제 위치에는 번호가 따로 매겨져 있습니다. 이 번호를 물리 주소라고 합니다.
가상 주소를 물리 주소로 바꾸는 일은 CPU 안의 MMU(Memory Management Unit, 메모리 관리 장치)가 맡습니다. 프로그램이 메모리를 읽고 쓸 때마다 이 번역을 거칩니다.
장치는 CPU 안의 MMU 를 거치지 않습니다. 그래서 드라이버는 DMA 버퍼의 물리 주소를 장치에 적어 줘야 합니다. 가상 주소를 적으면 장치는 엉뚱한 곳에 데이터를 씁니다.
운영체제는 메모리를 페이지라는 일정한 크기의 조각으로 나눠 관리합니다. 가상 주소를 물리 주소로 바꾸는 번역도 페이지 단위로 합니다.
메모리가 모자라면 운영체제는 한동안 안 쓴 페이지를 디스크로 내립니다. 비워진 칸은 다른 데이터에 내줍니다. 이렇게 페이지를 디스크로 내렸다가 다시 올리는 일을 스와핑이라고 합니다.
DMA 가 도는 중에 버퍼의 페이지가 디스크로 내려가면 곤란합니다. 장치는 옛 물리 주소에 계속 쓰므로 그 칸에 새로 들어온 남의 데이터를 덮어씁니다. 그래서 드라이버는 전송이 끝날 때까지 버퍼의 페이지를 메모리에 붙들어 둡니다. 이를 페이지 고정이라고 합니다.
흩어진 버퍼를 한 번에 옮기는 스캐터-개더
프로그램 눈에는 이어진 버퍼도 물리 메모리에서는 여러 페이지로 흩어져 있기 쉽습니다. 가상 주소는 이어져 있어도 그 뒤의 물리 주소는 페이지마다 제각각이기 때문입니다. 주소와 길이 한 쌍만 받는 장치라면 페이지마다 전송을 새로 걸어야 합니다.
스캐터-개더는 흩어진 조각의 목록을 한꺼번에 넘기는 방식입니다. 장치는 목록을 차례로 읽으며 조각들을 이어서 옮깁니다. CPU 는 전송을 한 번만 걸면 됩니다.
목록의 한 줄에는 조각 하나의 물리 주소와 길이가 적힙니다. 이 한 줄을 DMA 디스크립터라고 부릅니다. 장치가 읽어 갈 수 있게 디스크립터 목록도 메모리에 둡니다.
이름은 두 방향에서 왔습니다. 장치에서 받은 데이터를 여러 조각에 흩어 쓰는 것이 스캐터(scatter)입니다. 여러 조각을 모아 장치로 내보내는 것이 개더(gather)입니다.
flowchart TD
L["디스크립터 목록"] --> D1["디스크립터 1 · 주소 · 길이"]
L --> D2["디스크립터 2 · 주소 · 길이"]
L --> D3["디스크립터 3 · 주소 · 길이"]
subgraph M["물리 메모리"]
P1["페이지 가"]
P2["페이지 나"]
P3["페이지 다"]
end
D1 --> P3
D2 --> P1
D3 --> P2
그림에서 버퍼를 이루는 페이지 셋은 물리 메모리 안에서 순서가 뒤섞여 있습니다. 장치는 디스크립터 목록 하나만 받고 세 조각을 목록 순서대로 옮깁니다. CPU 는 목록을 한 번 적어 줄 뿐입니다.
CPU 캐시와 어긋나는 문제
DMA 는 CPU 모르게 메모리를 바꿉니다. 이 소절은 그 때문에 CPU 가 낡은 값을 읽는 경우와 그것을 막는 법을 봅니다.
CPU 캐시는 CPU 가 메모리의 일부를 곁에 복사해 두는 작은 저장소입니다. 메모리까지 다녀오는 데 시간이 걸리므로 자주 쓰는 값을 가까이 둡니다. CPU 는 읽고 쓸 때 캐시를 먼저 봅니다.
장치가 메모리에 새 데이터를 써도 캐시에는 옛 사본이 남아 있을 수 있습니다. 그러면 CPU 는 캐시의 옛 값을 읽습니다. 장치가 쓴 새 데이터는 CPU 눈에 안 보입니다.
sequenceDiagram
participant C as CPU
participant K as CPU 캐시
participant M as 메모리
participant D as 장치
C->>K: 버퍼를 읽는다 · 사본이 생긴다
D->>M: DMA 로 새 데이터를 쓴다
Note over K: 옛 사본이 남아 있다
C->>K: 같은 버퍼를 다시 읽는다
K-->>C: 옛 값을 돌려준다
반대 방향도 있습니다. CPU 가 버퍼에 쓴 값이 아직 캐시에만 있고 메모리로 내려가지 않았을 수 있습니다. 그러면 장치는 메모리의 옛 값을 읽어 내보냅니다.
막는 길은 둘입니다. 첫째는 하드웨어가 맞추는 길입니다. 캐시가 장치의 메모리 쓰기를 지켜보다가 같은 주소의 사본을 지우거나 고칩니다. 여러 코어의 캐시를 서로 맞추는 캐시 일관성 하드웨어가 DMA 까지 함께 챙기는 꼴입니다.
둘째는 드라이버가 맞추는 길입니다. 하드웨어가 이것을 안 해 주는 CPU 에서 씁니다. 장치가 버퍼를 읽기 전에 드라이버가 캐시의 새 값을 메모리로 내려보냅니다. 이것이 플러시입니다.
장치가 버퍼에 쓴 뒤에는 캐시의 옛 사본을 버립니다. 이것이 무효화입니다. 어느 길을 쓰는지는 CPU 설계마다 다릅니다.
장치를 가두는 IOMMU
DMA 를 켠 장치는 물리 메모리 어디든 쓸 수 있습니다. 드라이버가 주소를 잘못 적으면 커널의 데이터가 덮입니다. 악의를 품은 장치를 꽂으면 메모리의 비밀번호나 암호 키를 읽어 갈 수도 있습니다. 이런 공격을 DMA 공격이라고 부릅니다.
IOMMU(Input-Output Memory Management Unit, 입출력 메모리 관리 장치)는 장치와 메모리 사이에서 주소를 번역하는 부품입니다. CPU 에 MMU 가 있듯 장치 쪽에 둔 MMU 입니다. 운영체제는 장치마다 닿아도 되는 메모리 범위를 IOMMU 에 적어 둡니다. 그 밖을 가리키는 전송은 IOMMU 가 막습니다.
IOMMU 를 켜면 드라이버가 장치에 적는 주소도 달라집니다. 드라이버는 물리 주소 대신 IOMMU 가 번역할 주소를 적습니다. 장치가 그 주소로 메모리에 닿으면 IOMMU 가 물리 주소로 바꿉니다. 앞 소절에서 「물리 주소를 적어 줘야 한다」고 한 것은 IOMMU 가 없을 때의 이야기입니다.
IOMMU 는 가상 머신에 장치를 넘겨줄 때도 씁니다. 가상 머신은 한 컴퓨터 안에서 운영체제를 하나 더 돌리는 소프트웨어 컴퓨터입니다. 가상 머신 안의 운영체제가 아는 주소는 진짜 물리 주소가 아닙니다.
가상 머신이 장치를 직접 다루게 하는 것을 장치 패스스루라고 합니다. 가상 머신 안의 드라이버는 자기가 아는 주소로 DMA 를 겁니다. IOMMU 가 그 주소를 진짜 물리 주소로 바꿔 줍니다.
DMA 가 치르는 비용
DMA 는 CPU 를 나르기에서 풀어 주는 대신 몇 가지를 치릅니다. 아래 표는 앞 소절들에서 본 비용을 한데 모읍니다.
| 비용 | 무엇이 드나 |
|---|---|
| 준비 | 버퍼 마련 · 물리 주소 찾기 · 페이지 고정 · 레지스터 적기 · 인터럽트 처리 |
| 버스 | 장치가 버스를 쓰는 동안 CPU 의 메모리 접근이 기다릴 수 있다 |
| 캐시 | 하드웨어가 안 맞춰 주면 드라이버가 플러시와 무효화를 챙긴다 |
| 안전 | 장치가 메모리 어디든 쓸 수 있어 IOMMU 로 가둬야 한다 |
준비 비용은 옮기는 양과 상관없이 한 번씩 듭니다. 그래서 몇 바이트짜리 명령이나 상태 값은 CPU 가 장치 레지스터를 직접 읽고 쓰는 편이 품이 덜 듭니다. 한 장치 안에서도 작은 제어 값은 PIO 로, 큰 데이터는 DMA 로 옮기는 일이 흔합니다.
버스를 나눠 쓰는 방식에도 갈래가 있습니다. 사이클 스틸링은 버스를 한 번에 조금씩만 빌리는 방식입니다. CPU 의 메모리 접근 사이사이에 끼어들어서 CPU 가 오래 기다리지 않습니다.
버스트 모드는 한 번 잡은 버스를 한 덩어리를 다 옮길 때까지 쥐고 있는 방식입니다. 전송은 빨리 끝납니다. 대신 그동안 CPU 의 메모리 접근이 기다립니다.
백엔드 코드가 DMA 를 만나는 때
백엔드 코드는 DMA 를 직접 부르지 않습니다. DMA 는 드라이버와 커널 안에서 돕니다. 그래도 서버가 네트워크와 디스크로 데이터를 주고받는 길에는 거의 언제나 DMA 가 끼어 있습니다.
네트워크 카드는 받은 패킷을 DMA 로 메모리의 수신 버퍼에 써 넣습니다. 패킷이 들어오면 카드가 인터럽트로 커널을 부릅니다. 커널은 패킷을 꺼내 해당 소켓으로 넘깁니다.
수신 버퍼는 조각 여러 개로 나뉘어 있습니다. 드라이버는 조각마다 그 조각을 가리키는 디스크립터를 하나씩 만듭니다. 이 디스크립터들은 끝에 닿으면 처음으로 돌아가는 고리 모양 목록을 이룹니다. 이 목록을 링 버퍼라고 합니다.
카드는 링 버퍼를 따라가며 디스크립터가 가리키는 빈 조각에 패킷을 채웁니다. 커널이 패킷을 다 꺼낸 조각은 다시 빈 조각이 되어 돌려 씁니다.
파일을 읽을 때는 디스크가 DMA 로 커널의 페이지 캐시를 채웁니다. 페이지 캐시는 커널이 디스크 내용을 메모리에 들고 있는 저장소입니다. 같은 파일을 다시 읽으면 디스크까지 가지 않고 여기서 꺼냅니다.
그다음 read 호출이 그 데이터를 프로그램의 버퍼로 복사합니다. 이 복사는 DMA 가 아니라 CPU 가 합니다.
프로그램의 버퍼는 사용자 공간에 있습니다. 사용자 공간은 프로그램이 쓰는 메모리 영역으로, 커널이 쓰는 영역과 갈려 있습니다. 프로그램이 커널 메모리를 함부로 건드리지 못하게 둘을 가릅니다. 그래서 커널이 가진 데이터를 프로그램에 주려면 한 번 복사해 넘겨야 합니다.
제로 카피는 이런 CPU 복사를 줄이는 기법입니다. 파일을 소켓으로 보낼 때 데이터를 사용자 공간으로 올리지 않습니다. 네트워크 카드가 스캐터-개더를 지원하면 페이지 캐시의 조각들을 카드가 곧장 읽어 갑니다.
리눅스의 sendfile 호출이 이 기법을 씁니다. 프로그램은 보낼 파일과 받을 소켓만 알려 줍니다. 데이터는 프로그램의 버퍼를 거치지 않습니다.
같은 생각을 서버와 서버 사이로 넓힌 것이 RDMA(Remote Direct Memory Access, 원격 직접 메모리 접근)입니다. 네트워크 카드가 다른 서버의 메모리에 곧장 데이터를 씁니다.
관련 항목
DMA 로 메모리와 데이터를 주고받는 장치
디스크 · SSD · NVMe · 네트워크 카드 · GPU · 사운드 카드 · 저장 장치
DMA 를 이루는 부품과 자료 구조
DMA 컨트롤러 · DMA 버퍼 · DMA 디스크립터 · 스캐터-개더 · 링 버퍼 · 버스 마스터링 · 장치 레지스터
DMA 가 올라타는 버스와 그 사용 방식
버스 · 메모리 버스 · PCI Express · PCI · 사이클 스틸링 · 버스트 모드
DMA 와 맞세워지는 입출력 방식
PIO · 폴링 · 메모리 맵 입출력 · 포트 입출력 · 입출력
DMA 전송의 시작과 끝을 맡는 커널 구성 요소와 신호
디바이스 드라이버 · 인터럽트 · 인터럽트 핸들러 · MSI · 커널
DMA 버퍼의 주소를 정하고 지키는 메모리 관리
가상 메모리 · 가상 주소 · 물리 주소 · MMU · 페이지 · 페이지 고정 · 스와핑 · IOMMU
DMA 가 CPU 캐시와 어긋날 때 쓰는 대책
CPU 캐시 · 캐시 일관성 · 버스 스누핑 · 캐시 플러시 · 캐시 무효화 · 메모리 배리어
장치의 메모리 접근을 가두거나 넘겨주는 보안과 가상화 기술
DMA 공격 · 장치 패스스루 · 가상 머신 · 하이퍼바이저
DMA 위에서 서버의 데이터 복사를 줄이는 기법
제로 카피 · sendfile · 페이지 캐시 · 직접 입출력 · RDMA · 소켓 · 패킷
DMA 가 속하는 상위 분류
다른 이름: Direct Memory Access · 직접 메모리 접근 · 직접 메모리 액세스