Linux
ARM
kernel
FPGA
zynq

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


udmabuf(User space mappable DMA Buffer)


はじめに

Xilinx 社の ZYNQ や Altera 社の Cylcone V SoC 等の FPGA 部分にアクセラレータを作って試したい場合、CPU 側とアクセラレータ側でメモリを共有したいことがあります。本来ならカーネル空間で動作するデバイスドライバを作るのが良いのでしょうが、ちょっとした実験とかだと面倒です。そこでユーザー空間からもアクセスできるような DMA バッファがあれば便利だろうと思って、作ってみました。

http://github.com/ikwzm/udmabuf


udmabufとは

udmabuf はLinux のカーネル空間に連続したメモリ領域をDMAバッファとして確保し、ユーザー空間からmmapでアクセス可能にするためのデバイスドライバです。主にUIO(User space I/O)を使ってユーザー空間でデバイスドライバを動かす場合のDMAバッファを提供します。

ユーザー空間でudmabufを利用する際は、/dev/udmabuf0をopenしてmmapすると、ユーザー空間からDMAバッファにアクセスすることが出来ます。openする際にO_SYNCフラグをセットすることによりCPUキャッシュを無効にすることが出来ます。また、/sys/class/udmabuf/udmabuf0/phys_addr を読むことにより、DMAバッファの物理空間上のアドレスを知ることが出来ます。

udmabufのバッファの大きさやデバイスのマイナー番号は、デバイスドライバのロード時(insmodによるロードなど)に指定できます。またプラットフォームによってはデバイスツリーに記述しておくこともできます。


対応プラットフォーム


  • OS : Linux Kernel Version 3.6-3.8,3.18, 4.4, 4.8, 4.12, 4.14

  • CPU: ARM Cortex-A9 (Xilinx ZYNQ / Altera CycloneV SoC)

  • CPU: ARM64 Cortex-A53 (Xilinx ZYNQ UltraScale+ MPSoC)


構成

構成



使い方


コンパイル

次のようなMakefileを用意しています。

HOST_ARCH       ?= $(shell uname -m | sed -e s/arm.*/arm/ -e s/aarch64.*/arm64/)

ARCH ?= $(shell uname -m | sed -e s/arm.*/arm/ -e s/aarch64.*/arm64/)
KERNEL_SRC_DIR ?= /lib/modules/$(shell uname -r)/build

ifeq ($(ARCH), arm)
ifneq ($(HOST_ARCH), arm)
CROSS_COMPILE ?= arm-linux-gnueabihf-
endif
endif
ifeq ($(ARCH), arm64)
ifneq ($(HOST_ARCH), arm64)
CROSS_COMPILE ?= aarch64-linux-gnu-
endif
endif

obj-m := udmabuf.o

all:
make -C $(KERNEL_SRC_DIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) modules

clean:
make -C $(KERNEL_SRC_DIR) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) M=$(PWD) clean


インストール

insmod でudmabufのカーネルドライバをロードします。この際に引数を渡すことによりDMAバッファを確保してデバイスドライバを作成します。insmod の引数で作成できるDMAバッファはudmabuf0、udmabuf1、udmabuf2、udmabuf3の最大4つです。

zynq$ insmod udmabuf.ko udmabuf0=1048576

udmabuf udmabuf0: driver installed
udmabuf udmabuf0: major number = 248
udmabuf udmabuf0: minor number = 0
udmabuf udmabuf0: phys address = 0x1e900000
udmabuf udmabuf0: buffer size = 1048576
udmabuf udmabuf0: dma coherent = 0
zynq$ ls -la /dev/udmabuf0
crw------- 1 root root 248, 0 Dec 1 09:34 /dev/udmabuf0

パーミッションがrootのみ読み書き可能になっています。ロード時にパーミッションを変更したい場合は、/etc/udev/rules.d/99-udmabuf.rules というファイルを作成し、以下の内容を書いておきます。


99-udmabuf.rules

KERNEL=="udmabuf[0-9]*", GROUP="root", MODE="0666"


アンインストールするには rmmod を使います。

zynq$ rmmod udmabuf   

udmabuf udmabuf0: driver uninstalled


デバイスツリーによる設定

udmabufはinsmod の引数でDMAバッファを用意する以外に、Linuxのカーネルが起動時に読み込むdevicetreeファイルによってDMAバッファを用意する方法があります。devicetreeファイルに次のようなエントリを追加しておけば、insmod でロードする際に自動的にDMAバッファを確保してデバイスドライバを作成します。


devicetree.dts

        udmabuf@0x00 {

compatible = "ikwzm,udmabuf-0.10.a";
device-name = "udmabuf0";
minor-number = <0>;
size = <0x00100000>;
};


sizeでDMAバッファの容量をバイト数で指定します。

device-nameでデバイス名を指定します。

minor-number でudmabufのマイナー番号を指定します。マイナー番号は0から255までつけることができます。ただし、insmodの引数の方が優先され、マイナー番号がかち合うとdevicetreeで指定した方が失敗します。minor-numberが省略された場合、空いているマイナー番号が割り当てられます。

デバイス名は次のように決まります。


  1. device-nameが指定されていた場合は、 device-name。

  2. device-nameが省略されていて、かつminor-numberが指定されていた場合は、sprintf("udmabuf%d", minor-number)。

  3. device-nameが省略されていて、かつminor-numberも省略されていた場合は、devicetree のエントリー名(例ではudmabuf@0x00)。

zynq$ insmod udmabuf.ko

udmabuf udmabuf0: driver installed
udmabuf udmabuf0: major number = 248
udmabuf udmabuf0: minor number = 0
udmabuf udmabuf0: phys address = 0x1e900000
udmabuf udmabuf0: buffer size = 1048576
udmabuf udmabuf0: dma coherent = 0
zynq$ ls -la /dev/udmabuf0
crw------- 1 root root 248, 0 Dec 1 09:34 /dev/udmabuf0


デバイスファイル

udmabufをinsmodでカーネルにロードすると、次のようなデバイスファイルが作成されます。<device-name>には、前節で説明したデバイス名が入ります。


  • /dev/<device-name>

  • /sys/class/udmabuf/<device-name>/phys_addr

  • /sys/class/udmabuf/<device-name>/size

  • /sys/class/udmabuf/<device-name>/sync_mode

注)2015年12月から新たにキャッシュ制御用のデバイスファイルが追加されました。詳しくはLinuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ(キャッシュのフラッシュと無効化を追加)を参照してください。


/dev/<device-name>

/dev/<device-name>はmmap()を使って、ユーザー空間にマッピングするか、read()、write()を使ってバッファにアクセスする際に使用します。


udmabuf_test.c

    if ((fd  = open("/dev/udmabuf0", O_RDWR)) != -1) {

buf = mmap(NULL, buf_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* ここでbufに読み書きする処理を行う */
close(fd);
}


また、ddコマンド等でにデバイスファイルを指定することにより、shellから直接リードライトすることも出来ます。

zynq$ dd if=/dev/urandom of=/dev/udmabuf0 bs=4096 count=1024

1024+0 records in
1024+0 records out
4194304 bytes (4.2 MB) copied, 3.07516 s, 1.4 MB/s

zynq$dd if=/dev/udmabuf4 of=random.bin

8192+0 records in
8192+0 records out
4194304 bytes (4.2 MB) copied, 0.173866 s, 24.1 MB/s


phys_addr

/sys/class/udmabuf/<device-name>/phys_addr はDMAバッファの物理アドレスが読めます。


udmabuf_test.c

    unsigned char  attr[1024];

unsigned long phys_addr;
if ((fd = open("/sys/class/udmabuf/udmabuf0/phys_addr", O_RDONLY)) != -1) {
read(fd, attr, 1024);
sscanf(attr, "%x", &phys_addr);
close(fd);
}



size

/sys/class/udmabuf/<device-name>/size はDMAバッファのサイズが読めます。


udmabuf_test.c

    unsigned char  attr[1024];

unsigned int buf_size;
if ((fd = open("/sys/class/udmabuf/udmabuf0/size", O_RDONLY)) != -1) {
read(fd, attr, 1024);
sscanf(attr, "%d", &buf_size);
close(fd);
}



sync_mode

/sys/class/udmabuf/<device-name>/sync_mode はudmabufをopenする際にO_SYNCを指定した場合の動作を指定します。


udmabuf_test.c

    unsigned char  attr[1024];

unsigned long sync_mode = 2;
if ((fd = open("/sys/class/udmabuf/udmabuf0/sync_mode", O_WRONLY)) != -1) {
sprintf(attr, "%d", sync_mode);
write(fd, attr, strlen(attr));
close(fd);
}

O_SYNCおよびキャッシュの設定に関しては次の節で説明します。


DMAバッファとCPUキャッシュのコヒーレンシ

CPUは通常キャッシュを通じてメインメモリ上のDMAバッファにアクセスしますが、アクセラレータは直接メインメモリ上のDMAバッファにアクセスします。その際、問題になるのはCPUのキャッシュとメインメモリとのコヒーレンシ(内容の一貫性)です。


ハードウェアでコヒーレンシを保証できる場合

ハードウェアでコヒーレンシを保証できる場合、CPUキャッシュを有効にしても問題はありません。例えばZYNQにはACP(Accelerator Coherency Port)があり、アクセラレータ側がこのPortを通じてメインメモリにアクセスする場合は、ハードウェアによってCPUキャッシュとメインメモリとのコヒーレンシが保証できます。

ハードウェアでコヒーレンシを保証できる場合は、CPUキャッシュを有効にすることでCPUからのアクセスを高速に行うことができます。CPUキャッシュを有効にする場合は、O_SYNCフラグを設定せずにudmabufをopen してください。


udmabuf_test.c

    /* CPUキャッシュを有効にする場合はO_SYNCをつけずにopen する */   

if ((fd = open("/dev/udmabuf0", O_RDWR)) != -1) {
buf = mmap(NULL, buf_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* ここでbufに読み書きする処理を行う */
close(fd);
}


ハードウェアでコヒーレンシを保証できない場合

ハードウェアでコヒーレンシを保証できない場合、別の方法でコヒーレンシを保証しなければなりません。udmabufでは、CPUキャッシュを無効にする方法と、CPUキャッシュを有効にしたまま手動でCPUキャッシュをフラッシュ/無効化する方法を用意しています。


CPUキャッシュを有効にしたまま手動でCPUキャッシュを制御する方法

この方法に関しては別記事にしています。こちらを参照してください。「Linuxでユーザー空間で動作するプログラムとハードウェアがメモリを共有するためのデバイスドライバ(キャッシュのフラッシュと無効化を追加)」


CPUキャッシュを無効にする方法

CPUキャッシュを無効にする場合は、udmabufをopenする際にO_SYNCフラグを設定します。


udmabuf_test.c

    /* CPUキャッシュを無効にする場合はO_SYNCをつけてopen する */   

if ((fd = open("/dev/udmabuf0", O_RDWR | O_SYNC)) != -1) {
buf = mmap(NULL, buf_size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
/* ここでbufに読み書きする処理を行う */
close(fd);
}

O_SYNCフラグを設定した場合のキャッシュの振る舞いはsync_modeで設定します。sync_modeには次の値が設定できます。


  • sync_mode=0: 常にCPUキャッシュが有効。つまりO_SYNCフラグの有無にかかわらず常にCPUキャッシュは有効になります。


  • sync_mode=1: O_SYNCフラグが設定された場合、CPUキャッシュを無効にします。


  • sync_mode=2: O_SYNCフラグが設定された場合、CPUがDMAバッファに書き込む際、ライトコンバインします。ライトコンバインとは、基本的にはCPUキャッシュは無効ですが、複数の書き込みをまとめて行うことで若干性能が向上します。


  • sync_mode=3: O_SYNCフラグが設定された場合、DMAコヒーレンシモードにします。といっても、DMAコヒーレンシモードに関してはまだよく分かっていません。


参考までに、CPUキャッシュを有効/無効にした場合の次のようなプログラムを実行した際の処理時間を示します。


udmabuf_test.c

int check_buf(unsigned char* buf, unsigned int size)

{
int m = 256;
int n = 10;
int i, k;
int error_count = 0;
while(--n > 0) {
for(i = 0; i < size; i = i + m) {
m = (i+256 < size) ? 256 : (size-i);
for(k = 0; k < m; k++) {
buf[i+k] = (k & 0xFF);
}
for(k = 0; k < m; k++) {
if (buf[i+k] != (k & 0xFF)) {
error_count++;
}
}
}
}
return error_count;
}
int clear_buf(unsigned char* buf, unsigned int size)
{
int n = 100;
int error_count = 0;
while(--n > 0) {
memset((void*)buf, 0, size);
}
return error_count;
}



プログラム
O_SYNC
sync_mode
DMAバッファのサイズ


1MByte
5MByte
10MByte


check_buf

-
0.437[sec]
2.171[sec]
4.375[sec]



0
0.434[sec]
2.169[sec]
4.338[sec]


1
2.283[sec]
11.414[sec]
22.830[sec]


2
1.655[sec]
8.391[sec]
16.587[sec]


3
1.661[sec]
8.396[sec]
16.584[sec]


clear_buf

-
0.667[sec]
0.363[sec]
0.716[sec]



0
0.671[sec]
0.362[sec]
0.716[sec]


1
0.914[sec]
4.564[sec]
9.128[sec]


2
0.622[sec]
0.310[sec]
0.621[sec]


3
0.616[sec]
0.311[sec]
0.620[sec]


参考