LinuxのUIO(User space I/O) その1

#はじめに

組み込みLinuxで開発していると、カーネルでなくてユーザープロセスから物理メモリ空間上のレジスタを読み書きしたい場面が出てきます。
これは /dev/mem を使えば実現できるのですが、制限事項があります。

- /dev/memの読み書きにはroot権限が必要。
- 逆にroot権限で/dev/memをアクセスすると際限なく全てのメモリにアクセスできてしまうので、セキュリティの観点で難あり。

例えば、Androidではアプリケーションのプロセスはroot権限を持っていないので/dev/mem は扱えません。

指定した範囲のメモリだけ、指定した権限でアクセス可能にできればいいのになあ。
それ、UIOでできます。

#UIOとは
UIO(User space I/O)は、デバイスドライバの大部分をユーザー空間側で書けるようにするインタフェースです。カーネル側に書かなければならないコードはほんのわずか、あるいは後述するデバイスツリーを使えばカーネル側のコードは全く書かなくてすみます。

ざっくり言うと、UIOが提供してくれるインタフェースは次の2つです。

- 指定した範囲のメモリをユーザー空間からマッピング可能にする
- 割り込みをユーザー空間からのreadシステムコールに通知する

UIOの概要については、こちらの発表資料が詳しいです。
[Using UIO in an embedded platform(PDF)](https://elinux.org/images/b/b0/Uio080417celfelc08.pdf)

UIOのカーネル内のドキュメントはこちら。
[The Userspace I/O HOWTO](https://www.kernel.org/doc/html/v4.12/driver-api/uio-howto.html)

この2つのドキュメントはけっこう古いのと、Linuxのデバイスドライバを書くための基礎知識はある前提で書かれているため、実際にやってみるには苦労しました。

以降、割り込みは使わずメモリのマッピングに的を絞って、実際にやってみた例を紹介します。

#UIOを使うためのカーネルの準備

kernel configに以下が入っているかを確認し、無ければ追加してカーネルとカーネルモジュールを更新します。

```ini
CONFIG_UIO=m
CONFIG_UIO_PDRV_GENIRQ=m
```

なお、今回私が使ったLinuxカーネルはARM用の3.18です。

#SoC内部のレジスタをUIOを使ってユーザー空間から読み書きできるようにしてみる

SoCの内部のレジスタ、物理アドレス0x120a0000からの4KBをユーザープロセスから読み書きできるようにしてみます。ページ単位での割当になるのでページのサイズ(=4KB)が最小単位になります。


## 2つの方法

この用途ならばすでに用意されているuio_pdrv_genirqというドライバにパラメータをセットするだけで実現できますが、それには2つの方法があります。

1. パラメータをセットするだけの小さなカーネルモジュールを作成し、それをinsmodする
UIOの当初の想定したやり方です。
この方法は別の記事で書こうと思います。
[書きました。](https://qiita.com/tetsu_koba/items/3b2d6bd856ee6d4d98e4)

2. デバイスツリーのDTSファイルでパラメータをセットする
これはデバイスツリーを使用する最近のARMのLinuxで可能です。こちらの方法では全くカーネルモジュールのコードを書く必要がありません。
uio_pdrv_genirqドライバがビルトインでなくてモジュールになっている必要があります。つまり
CONFIG_UIO_PDRV_GENIRQ=m

以降、2.の方法を説明します。

##デバイスツリーのDTSファイルでパラメータをセットする

dts file のルート /{} の中に以下の記述を追加します。

```
/ {
	peri_pmc@120a0000 {
		compatible = "generic-uio";
		reg = < 0x120a0000 0x1000 >;
	};
};
```

これでカーネルをビルドするとdtbファイルが更新されます。

カーネルの起動パラメータに以下を追加します。

```ini
uio_pdrv_genirq.of_id=generic-uio
```
カーネルの起動にu-bootを使用している場合は、これをbootargsに追加すればよいです。

#UIOの状態の確認

うまくいったら /dev/uio0 のデバイスファイルができます。
uioの情報は以下のように /sys/class/uio で見ることができます。

```
# ls /sys/class/uio/uio0/
dev        event      name       subsystem  version
device     maps       power      uevent
# cat /sys/class/uio/uio0/name 
peri_pmc
# cat /sys/class/uio/uio0/maps/map0/
addr    name    offset  size
# cat /sys/class/uio/uio0/maps/map0/addr 
0x120a0000
# cat /sys/class/uio/uio0/maps/map0/size 
0x1000
# cat /sys/class/uio/uio0/maps/map0/offset 
0x0
# cat /sys/class/uio/uio0/maps/map0/name 
/peri_pmc@120a0000
```

# デバイスファイルのアクセス権の変更
生成された/dev/uio0 のデバイスファイルはデフォルトではroot権限がないと読み書きできません。このデバイスファイルのグループや権限を変更するには、udevのルールファイルに以下を追加します。

```ini:/etc/udev/rules.d/70-uio.rules 
SUBSYSTEM=="uio", GROUP="users", MODE="0660"
```

これで再起動すると今度は以下のようなアクセス権で/dev/uio0が作られました。
これなら、rootでなくてもusersのグループに入っていれば読み書きできます。

```
$ ls -l /dev/uio0 
crw-rw----    1 root     users     253,   0 Jan  1  1970 /dev/uio0
```

#ユーザー空間のプログラムのサンプル

```c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <stdint.h>

#define PAGE_SIZE 0x1000

int main()
{
	int fd;
	void *base;
	uint32_t *pmem;
	
	fd = open("/dev/uio0", O_RDWR);
	if (fd < 0) {
		fprintf(stderr, "Failed to open device\n");
		exit(1);
	}
	base = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
	if (base == MAP_FAILED) {
		fprintf(stderr, "Failed to mmap\n");
		exit(1);
	}
	
	pmem = (uint32_t*)base;
	
	printf("pmem[0] = 0x%08x\n", pmem[0]);
	printf("pmem[1] = 0x%08x\n", pmem[1]);
}
```

/dev/uio0をopenしてmmapして得られたアドレスにuioで指定した物理アドレスの内容が見えます。
mmapのときには MAP_PRIVATEでなくてMAP_SHAREDを使用してください。

SoCの内部レジスタは普通はアクセスするサイズが決まっているので、そのサイズに合わせてキャストしてください。今回の例では4バイト単位です。
pmem[0] は0x120a0000, pmem[1]は 0x120a0004 の内容が読めます。

#続き
デバイスツリーを使わずにUIOを使う方法を続きの記事に書きました。
[LinuxのUIO(User space I/O) その2](https://qiita.com/tetsu_koba/items/3b2d6bd856ee6d4d98e4)

#参考
このページはかなり参考にさせていだだきました。
[[Linux] User I/O](http://arch.jpn.org/archives/292)