GPUや映像入出力など大容量のメモリ領域をバッファとして扱う方法はさまざまありますが、userland と kernel の間を共有する方法として dma-buf があります。ここでは、dma-buf の機構を扱う方法を考えてみます。
dma-buf は kernel driver からも userland からもフロントエンドとして動作します。つまり、決められた API を提供するとともに、背後にあるメモリアロケータを呼び出してメモリ領域を扱うようになっています。ただ、メモリを確保解放する手段はアロケータに任されているため、アロケータがメモリ領域に紐づけられた file descriptor を提供し、dma-buf でその file descriptor を扱うことでメモリ領域を操作します。
userland での操作
dma-buf で扱えるメモリ領域を提供する手段としては DRM/KMS の prime や android の ion、V4L2 の videobuf2 などがあります。これらの後ろには CMA(Contiguous Memory Allocator) が接続されていて、CMA が管理するメモリプールからメモリを提供します。
ここでは userland で ion を使った例を書いてみます(エラー処理は省略)。なお、ion は v4.12 で dma-buf を使う形になったため、それ以前の ion とは別物です。
struct ion_allocation_data arg;
int fd, dmabuf_fd;
fd = open("/dev/ion", O_RDWR | O_CLOEXEC);
memset(&arg, 0, sizeof(arg));
arg.len = size;
arg.heap_id_mask = 0xffffffff;
arg.flags = ION_FLAG_CACHED;
ret = ioctl(fd, ION_IOC_ALLOC, &arg);
dmabuf_fd = arg.fd;
close(fd);
まず ion から size の大きさのメモリを獲得します。heap_id_mask はどの CMA から取るかを示すものですがここでは説明は割愛します。flags にはキャッシュを使えるように設定しています。
uint32_t *buf;
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, dmabuf_fd, 0);
ion アロケータから得られた dmabuf_fd を使って mmap() を行います。これで確保したメモリ領域が userland にマップされました。
struct dma_buf_sync sync;
sync.flags = DMA_BUF_SYNC_START | DMA_BUF_SYNC_RW;
ret = ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
/*
* read from/write to the buffer
*/
sync.flags = DMA_BUF_SYNC_END | DMA_BUF_SYNC_RW;
ret = ioctl(dmabuf_fd, DMA_BUF_IOCTL_SYNC, &sync);
DMA_BUF_IOCTL_SYNC を呼び出すことで、userland 上のマップされた領域でキャッシュが有効になります。START で始めて END で終わることで、この間のキャッシュの一貫性が保証されます。
close(dmabuf_fd);
close() でバッファが破棄されます。
この他、lseek() でサイズの確認、poll() でバッファの待ち合わせをすることで他のバッファを扱う user と排他にする操作などがあります。
kernel driver からの操作
dma-buf を扱う driver の役割としては、メモリを供給する"exporter" と メモリを参照する "importer" があります。ion は exporter です。ここでは exporter から得られたメモリを driver で扱う "importer" の操作の流れを説明します。
struct dma_buf *dmabuf;
dmabuf = dma_buf_get(fd);
userland から渡された file descriptor の fd から dma_buf 構造体を得ます。
struct device *dev;
struct dma_buf_attachment *attach;
attach = dma_buf_attach(dmabuf, dev);
dma_buf をデバイス構造体の dev と紐づけます。dev はドライバの起動時に得る必要があります。
struct sg_table *sgt;
struct scatterlist *sg;
int i;
sgt = dma_buf_map_attachment(attach, DMA_BIDIRECTIONAL);
for_each_sg(sgt->sgl, sg, sgt->nents, i) {
void *vaddr = sg_virt(sg);
dma_addr_t addr = sg_dma_address(sg);
size_t size = sg_dma_len(sg);
}
メモリ領域をこのドライバ内でマップして扱えるようにします。得られた sg_table は scatterlist のテーブルになっているため、for_each_sg() で複数の領域を取り出します。ここでは vaddr, addr, size にそれぞれ領域の先頭論理アドレス、先頭物理アドレス、サイズが入ります。
dma_buf_begin_cpu_access(dmabuf, DMA_BIDIRECTIONAL);
/*
* read from/write to the buffer
*/
dma_buf_end_cpu_access(dmabuf, DMA_BIDIRECTIONAL);
CPU で領域の読み書きを行う場合は、userland と同様にキャッシュの一貫性を保つため begin/end で囲む必要があります。これらの動作は "exporter" に定義された操作を呼び出すため、exporter によって実際の動作は異なります。
dma_buf_unmap_attachment(attach, sgt, DMA_BIDIRECTIONAL);
dma_buf_detach(dmabuf, attach);
dma_buf_put(dmabuf);
逆の操作を行って終了します。
参考資料
- https://www.kernel.org/doc/html/latest/driver-api/dma-buf.html
- https://elinux.org/images/a/a8/DMA_Buffer_Sharing-_An_Introduction.pdf
コメント