注意(2024年8月22日追記)
この記事は2020年3月に投稿したものであり、古い内容が含まれています。2024年8月に、この記事の改訂版を投稿しました。詳細は次の記事を参照してください。
はじめに
V4L2 のストリーミングI/O(V4L2_MEMORY_MMAP) はV4L2 ストリーミング I/O の方式の一つで、V4L2 ドライバ内(カーネル内)で確保した V4L2 バッファを mmap 機構を使ってユーザー空間にマッピングすることで、ユーザープログラムが V4L2 バッファにアクセスできるようにします。V4L2バッファをユーザー空間から直接アクセス出来るため、この方式は比較的よく使われます。
しかし、ある種の V4L2 ドライバでは、mmap でユーザー空間にマッピングする際にキャッシュがオフになってしまってメモリアクセスが非常に遅くなり性能が出ない問題がありました。
この問題を起こす V4L2 ドライバの一つとして、Xilinx の Video DMA があります。
この記事では、そのメカニズムを説明します。
キャッシュがオフになるメカニズム
V4L2 バッファメモリアロケーターのdma-contig の mmap に問題があって、場合によってはキャッシュが無効になります。したがってdma-contig を採用している V4L2 ドライバの mmap でキャッシュが無効になります。
V4L2 バッファのメモリアロケータ−
V4L2 バッファのメモリアロケータ−には次の3種類があります。
- vmalloc : DMA を伴わない V4L2 ドライバ用
- dma-sg : Scatter Gather に対応した DMA デバイス用
- dma-contig : Scatter Gather に対応していない DMA デバイス用
このうち、問題となるのは最後の dma-contig です。
vmalloc
vmalloc はDMAを伴わない V4L2 ドライバのためのメモリアロケータ−です。例えば、USB Camera のV4L2 ドライバがこれにあたります。USB の場合は USB のデバイスドライバが USB デバイスとのデータ転送を行い、V4L2 ドライバ自体は直接 USB デバイスとのデータ転送を行いません。そのため、カーネルが普通に使っている vmalloc を使ってメモリを確保します。
dma-sg
dma-sg は Scatter Gather に対応した DMA を持つデバイスのためのメモリアロケータ−です。Scatter Gather に対応しているため、バッファが物理メモリ空間上に連続していなくても DMA 転送が可能です。Linux カーネルの dma_sg API を使ってメモリを確保します。
dma-contig
dma-contig は Scatter Gather に対応していない DMA を持つデバイスのためのメモリアロケータ−です。Scatter Gather に対応していないため、バッファは物理メモリ空間上に連続していなければなりません。Linux カーネルの dma API を使ってメモリを確保します。
実はこの dma-contig のmmap に問題があって、この dma-contig を採用した V4L2 ドライバの mmap だとキャッシュが無効になる場合があります。
dma-contig の mmap
dma-contig の mmap は次のようになっています。
static int vb2_dc_mmap(void *buf_priv, struct vm_area_struct *vma)
{
struct vb2_dc_buf *buf = buf_priv;
int ret;
if (!buf) {
printk(KERN_ERR "No buffer to map\n");
return -EINVAL;
}
/*
* dma_mmap_* uses vm_pgoff as in-buffer offset, but we want to
* map whole buffer
*/
vma->vm_pgoff = 0;
ret = dma_mmap_attrs(buf->dev, vma, buf->cookie,
buf->dma_addr, buf->size, buf->attrs);
if (ret) {
pr_err("Remapping memory failed, error: %d\n", ret);
return ret;
}
vma->vm_flags |= VM_DONTEXPAND | VM_DONTDUMP;
vma->vm_private_data = &buf->handler;
vma->vm_ops = &vb2_common_vm_ops;
vma->vm_ops->open(vma);
pr_debug("%s: mapped dma addr 0x%08lx at 0x%08lx, size %ld\n",
__func__, (unsigned long)buf->dma_addr, vma->vm_start,
buf->size);
return 0;
}
実際に mmap を行うのは dma_mmap_attrs() です。dma_mmap_attrs() はアーキテクチャに依存したマッピング関数です。
static inline int
dma_mmap_attrs(struct device *dev, struct vm_area_struct *vma, void *cpu_addr,
dma_addr_t dma_addr, size_t size, unsigned long attrs)
{
const struct dma_map_ops *ops = get_dma_ops(dev);
BUG_ON(!ops);
if (ops->mmap)
return ops->mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
return dma_common_mmap(dev, vma, cpu_addr, dma_addr, size);
}
arm64 の mmap
アーキテクチャが arm64 の場合、dma_mmap_attrs() は最終的に _swiotlb_mmap() を呼び出します。
static pgprot_t __get_dma_pgprot(unsigned long attrs, pgprot_t prot,
bool coherent)
{
if (!coherent || (attrs & DMA_ATTR_WRITE_COMBINE))
return pgprot_writecombine(prot);
return prot;
}
static int __swiotlb_mmap(struct device *dev,
struct vm_area_struct *vma,
void *cpu_addr, dma_addr_t dma_addr, size_t size,
unsigned long attrs)
{
int ret;
unsigned long pfn = dma_to_phys(dev, dma_addr) >> PAGE_SHIFT;
vma->vm_page_prot = __get_dma_pgprot(attrs, vma->vm_page_prot,
is_device_dma_coherent(dev));
if (dma_mmap_from_dev_coherent(dev, vma, cpu_addr, size, &ret))
return ret;
return __swiotlb_mmap_pfn(vma, pfn, size);
}
vm->vm_page_prot はキャッシュの設定に関する値です。この変数が _get_dma_pgprot () で計算された値で上書きされていることがわかります。
具体的には、DMA デバイスがcoherent だった場合(!is_device_dma_coherent(dev)の場合)、またはattrs に DMA_ATTR_WRITE_COMBINE がセットされていた場合、pgprot_writecombine に設定されます。write-combine はライト時には数ワード分まとめて書き込みますが、基本的にはキャッシュ無効になります。
デバイスツリーなどで特に指定しなけば、DMA デバイスの coherent は0なので、キャッシュは無効になります。
arm の mmap
アーキテクチャが arm の場合、dma_mmap_attrs() は最終的に arm_dma_mmap() を呼び出します。
static inline pgprot_t __get_dma_pgprot(unsigned long attrs, pgprot_t prot)
{
prot = (attrs & DMA_ATTR_WRITE_COMBINE) ?
pgprot_writecombine(prot) :
pgprot_dmacoherent(prot);
return prot;
}
int arm_dma_mmap(struct device *dev, struct vm_area_struct *vma,
void *cpu_addr, dma_addr_t dma_addr, size_t size,
unsigned long attrs)
{
vma->vm_page_prot = __get_dma_pgprot(attrs, vma->vm_page_prot);
return __arm_dma_mmap(dev, vma, cpu_addr, dma_addr, size, attrs);
}
arm の場合も arm64 と同様に、vm->vm_page_prot が _get_dma_pgprot () で計算された値で上書きされていることがわかります。ただし、arm の場合はattrs に DMA_ATTR_WRITE_COMBINE がセットされていた場合、pgprot_writecombine に、それ以外の場合は pgprot_dmacoherent に設定されます。実は arm の場合 pgprot_writecombine も pgprot_dmacoherent もキャッシュは無効になります。
対処方法
こちらに udmabuf を使って対処する方法を投稿しました。
参考
「V4l2 V4L2_MEMORY_USERPTR:contiguous mapping is too small 4096/1228800」
Xilinx のフォーラムに次のスレッドがありました。
- 「V4l2 V4L2_MEMORY_USERPTR:contiguous mapping is too small 4096/1228800」
https://forums.xilinx.com/t5/Embedded-Linux/V4l2-V4L2-MEMORY-USERPTR-contiguous-mapping-is-too-small-4096/td-p/825067
Xilinx の VDMA を使ってキャプチャーしようとしたら mmap によるデータ転送が遅いので、mmap の代わりにユーザー空間にバッファを確保して、それをV4L2 ドライバに渡したら、今度は連続した空間が小さすぎてエラーになったという話です。
これは考えてみれば当たり前な話で、Xilinx の VDMA は Scatter Gather に対応していないのでバッファは連続したメモリ空間になければなりません。
「Buffer from UDMABUF and V4L2_MEMORY_USERPTR」
もともと私がこの問題に興味を持ったのは、udmabuf に次の issue があげられたからです。
- 「Buffer from UDMABUF and V4L2_MEMORY_USERPTR」
https://github.com/ikwzm/udmabuf/issues/38
mmap によるデータ転送が遅いので、mmap の代わりに udmabuf で連続した物理メモリを確保してユーザー空間にマッピングして、それを V4L2 ドライバに渡せばデータ転送が速くしようという試みでした。
最初は上手くいかなくて udmabuf に issue があげられたのですが、最終的にはこの試みは成功したようです。
「Ultra96 で Julia set をぐりぐり動かせるやつを作った」
これは、ある方のブログの記事です。
- 「Ultra96 で Julia set をぐりぐり動かせるやつを作った」
https://blog.myon.info/entry/2019/05/15/ultra96-julia-set-explorer/
この記事では Vivado-HLS で記述したJulia set を Ultra96 の FPGA に実装してディスプレイに表示しています。その際、FPGA との通信に Xilinx の VDMA を使っていて、最初は V4L2 の mmap を使っていたけど全然性能が出なくて、dma-buf というバッファを共有する方法にしたという話です。
「imx6ull v4l2 slow memcpy for captured memory」
NXP社のフォーラムでも同じような投稿がありました。
- 「imx6ull v4l2 slow memcpy for captured memory」
https://community.nxp.com/thread/483135