Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ(V4L2_MEMORY_USERPTR編)

# 注意(2024年4月3日追記)


この記事は2020年3月に投稿したものであり、古い内容が含まれています。2022年12月ごろに次のような修正が Linux Kernel に対して行われ、Linux Kernel 5.15 から適用されました。


  * [[PATCH AUTOSEL 6.0 13/13] v4l2: don't fall back to follow_pfn() if pin_user_pages_fast() fails](https://lore.kernel.org/lkml/20221206094916.987259-13-sashal@kernel.org/)



この修正の結果、この記事で紹介したようなu-dma-buf でバッファを確保して V4L2_MEMORY_USERPTR を使う方法が機能しなくなりました。

上記の投稿によると、もともと V4L2_MEMORY_USERPTR は非推奨になっており、またVM_PFMAP/VM_IO へのフォールバックは基本的に危険であることが指摘されています。そのため、ユーザー空間上のメモリ領域を V4L2 のバッファとして使う際の制約が厳しくなり、その結果 u-dma-buf で確保したバッファは使えなくなったようです。


# はじめに


筆者は[udmabuf] をオープンソースで公開しています。

* [『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita]
* https://github.com/ikwzm/udmabuf

この記事では、[udmabuf] で確保したバッファを V4L2 バッファとして使う際の方法について説明します。


# 経緯


次の記事でXilinx VDMA 等を使った V4L2 ドライバで V4L2 バッファを mmap するとキャッシュが有効にならない問題を示しました。

* [『V4L2 ストリーミングI/O(V4L2_MEMORY_MMAP) で性能が出ない問題』@Qiita]

その対策として、V4L2_MEMORY_MMAP ではなく、ユーザーアプリケーション側でバッファを確保してV4L2ドライバに V4L2_MEMORY_USERPTR で渡す方法が考えられます。しかしこの方法では上手くいかないことが Xilinx のフォーラムで示されています。

* [『V4l2 V4L2_MEMORY_USERPTR:contiguous mapping is too small 4096/1228800』]

これは考えてみれば当たり前な話で、Xilinx VDMA は Scatter Gather をサポートしていないので、V4L2バッファは物理メモリ上に連続していなければなりません。ユーザー空間にバッファを確保した場合は物理メモリ上に連続されている保証はありません。このチェックに引っかかってしまったのです。



そこである方が、udmabuf を使って物理メモリ上に連続したバッファを確保して、そのバッファを mmap でユーザー空間にマッピングしてV4L2 ドライバにV4L2_MEMORY_USERPTR で渡す方法を試みました。残念ながら、当初は上手くいかなくて udmabuf に issue をくださいました。

* [『Buffer from UDMABUF and V4L2_MEMORY_USERPTR #38』]

最終的にはこの試みは成功したようです。1920x1080p NV12 の画像データを転送するのに、V4L2_MEMORY_MMAP だと 24msec かかっていたのが、udmabuf + V4L2_MEMORY_USERPTR だと 1.6msec になったそうです。


# 使用例


ここでは例に基づいてudmabuf で確保したバッファを V4L2 バッファとして使う際の方法を説明します。


## udmabuf の準備


udmabuf は Linux のカーネルモジュールです。udmabuf のビルドおよびインストールは udmabuf の Readm.md などの資料を参考にしてください。


## udmabuf の open


udmabuf をオープンします。


```C:udmabuf.hpp
	name  = device_name;
	sprintf(file_name, "/dev/%s", name.c_str());
	if ((_device_file = open(file_name, O_RDWR)) < 0) {
 		printf("Can not open %s\\n", file_name);
		_status = false;
		goto done;
	}

```



## udmabuf の mmap


udmabuf を mmap でユーザー空間にマッピングします。


```C:udmabuf.hpp
	buf = mmap(NULL, buf_size, PROT_READ|PROT_WRITE, MAP_SHARED, _device_file, 0);
	if (buf == MAP_FAILED) {
		printf("Can not mmap %s\\n", file_name);
		_status = false;
		goto done;
	}

```



## udmabuf のクリア


実はこの処理がとても重要で欠かせません。というのも、udmabuf はオンデマンドページングによって仮想アドレスに物理メモリをマッピングします。もう少し詳しく説明すると、udmabuf は mmap() ですぐに仮想アドレスに物理メモリをマッピングするわけではありません。仮想アドレスに対してなんらかのアクセスを行った際に、物理メモリがマッピングされていなければページフォールトが発生します。udmabuf はページフォールトが発生したときに初めて物理メモリをマッピングします。 

udmabuf の場合は mmap() によって返された仮想アドレスにはまだ物理アドレスがマッピングされていないので、この仮想アドレスを V4L2 デバイスに渡すと[『Buffer from UDMABUF and V4L2_MEMORY_USERPTR #38』]で示されたようなエラーが発生します。

これを防ぐためには、mmap() 直後にバッファをクリアするのが良いでしょう。バッファに書き込みすることによって、ページフォールトを発生させて、仮想アドレスに物理メモリをマッピングします。


```C:udmabuf.hpp
	// Write once to allocate physical memory to u-dma-buf virtual space.
	// Note: Do not use memset() for this.
	//       Because it does not work as expected.
	{
		uint64_t*	word_ptr = reinterpret_cast<uint64_t*>(buf);
		size_t    	words    = buf_size/sizeof(uint64_t);
		for(int i = 0 ; i < words; i++) {
			word_ptr[i] = 0;
		}
	}
```


バッファクリアの際に、memset() を使ってはなりません。memset() はとても複雑なことをしていて思い通りの結果を得ることが出来ません。



将来は udmabuf 内部でこのような処理をすることを検討しています。それまでは、この使用例のように明示的にバッファをクリアしてください。




## V4L2 ドライバにV4L2バッファを要求


V4L2 ドライバに対して V4L2 バッファを要求します。その際、 V4L2 バッファは V4L2 ドライバが確保するのではなくユーザープログラムが確保したものを使うことを示すために、req.memory には V4L2_MEMORY_USERPTR を指定します。


```C:example.cpp
	v4l2_requestbuffers req ;
	memset(&req, 0, sizeof(req));
	req.count = num_buf;
	req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
	req.memory = V4L2_MEMORY_USERPTR;
	ret = xioctl(fd, VIDIOC_REQBUFS, &req);
	if (ret < 0) {
		perror("ioctl(VIDIOC_REQBUFS)");
		return -1;
	}
```



## V4L2 ドライバにV4L2バッファをエンキュー(enqueue)


ストリーミングを開始するに先立って、V4L2 ドライバのキューに V4L2 バッファをエンキューします。buf.m.userptr に udmabuf で確保したバッファの物理メモリをマッピングした仮想アドレスを設定しています。


```C:example.cpp
	for (i = 0; i < num_buf; i++) {
		v4l2_buffer buf;
		memset(&buf, 0, sizeof(buf));
		buf.type	= V4L2_BUF_TYPE_VIDEO_CAPTURE;
		buf.memory = V4L2_MEMORY_USERPTR;
		buf.index = i;
		buf.length = buf_size;
		buf.m.userptr = reinterpret_cast<unsigned long>(udmabuf.buf + i*buf_size);
		ret = xioctl(fd, VIDIOC_QBUF, &buf);
		if (ret < 0) {
			perror("ioctl(VIDIOC_QBUF)");
			return -1;
		}
	}
```









# 参考


* https://github.com/ikwzm/udmabuf
* [『Buffer from UDMABUF and V4L2_MEMORY_USERPTR #38』]
* [『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita]
* [『V4L2 ストリーミングI/O(V4L2_MEMORY_MMAP) で性能が出ない問題』@Qiita]
* [『V4l2 V4L2_MEMORY_USERPTR:contiguous mapping is too small 4096/1228800』]


[udmabuf]: https://github.com/ikwzm/udmabuf "https://github.com/ikwzm/udmabuf"
[『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita]: https://qiita.com/ikwzm/items/cc1bb33ff43a491440ea "『Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ』@Qiita"
[『V4L2 ストリーミングI/O(V4L2_MEMORY_MMAP) で性能が出ない問題』@Qiita]: https://qiita.com/ikwzm/items/8f1c98efe6fd4ae2490a "『V4L2 ストリーミングI/O(V4L2_MEMORY_MMAP) で性能が出ない問題』@Qiita"
[『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 "『V4l2 V4L2_MEMORY_USERPTR:contiguous mapping is too small 4096/1228800』@XilinxForum"
[『Buffer from UDMABUF and V4L2_MEMORY_USERPTR #38』]: https://github.com/ikwzm/udmabuf/issues/38 "『Buffer from UDMABUF and V4L2_MEMORY_USERPTR #38』@gitub.com"