parallella データ共有

なんかのために何かを書いておこうと思う。


parallella でデータ共有する方法を通じて、parallella プログラミング環境への理解を深めたい。というか基本的にこういうの調べるときデータ共有の方法しか調べてない気がする。

簡単に説明すると、

  1. Epiphany側のプログラムでは ldscript で変数が0x8e000000付近に配置されるようになっている
  2. 0x8e000000 は今のParallellaボードの設定だと、East eLinkから外に出るようになっている
  3. East eLinkは、Zynqにつながっていて、Zynqが0x8e000000へアクセスするeLinkプロトコルを0x3e000000のDDRにアクセスするプロトコルに変換する
  4. 0x3e000000から32MBはARM側のLinuxではあらかじめ予約領域になっている(どうやっているかはまだ見つけられてない)
  5. ARM Linux側では、/dev/mem経由で、0x3e000000をユーザプロセスから見える場所にマップする。
  6. これで、0x3e000000 から 32MB の領域は、Epiphany,ARM両方からアクセスできる状態になる。

となる。

以下、詳しい解説

Hello World

みんな大好きHello Worldのコードは↓にある。
https://github.com/adapteva/epiphany-examples/blob/2014.05/apps/hello-world/src/


Epiphany側でprintfするには、gdb-serverを動かさないといけなくて、ちょっと特殊なので、この例では、Epiphany側でメモリ上に文字列を置いて、それをARM側でprintfしている。


それが、
https://github.com/adapteva/epiphany-examples/blob/2014.05/apps/hello-world/src/e_hello_world.c#L46 (Epiphany側)
https://github.com/adapteva/epiphany-examples/blob/2014.05/apps/hello-world/src/hello_world.c#L82 (ARM側)
このへん。(masterはインターフェース変わっているのでよくわからなければ見ないほうがいい)

このe_readが何者か、というのが、おもなテーマになる。

5分でわかる(わけがない)ldscript

ldscript の知識があると、Epiphanyはもちろん、その他色々な場面で役立つことが多いので知っておくとよいと思う。


まず、リンカの挙動をよく知らない人に知っておいて欲しいことは、リンカはそれなりに複雑な処理をしているというという点である。


例えば、

extern void f(void);

typedef void (*funcptr_t)(void);

funcptr_t 
g() {
    return f;
}

とかすると、同じ意味のasmを書きたいとして、そのままasmを書くと、x86だと、

        .text
	.globl g
	.globl f
g:
	mov 	$f, %eax
        ret

とか、書きたくなるわけだが、これ確か、OSXMach-Oだとこのように書けなくて(未確認)、何故だろう?まあ、そういう、リンクしてアドレス解決というのは、それなりに処理をしてるわけです。(まあこれはldscriptとは関係無いけど(関係無いんかい))


なので、リンカの挙動を細かく制御したい、というニーズはそれなりにあって、GNU binutilsで、それを実現するのが、ldscriptなのだった。

ldscriptを使うことで、データやプログラムをメモリ上にどのように配置するか、というのを制御できるわけです。



16コアParallella の Epiphany で使われるldscript は、
https://github.com/adapteva/epiphany-libs/blob/master/bsps/parallella_E16G3_1GB/fast.ldf


これ。


色々書いてあるが、最初は、
https://github.com/adapteva/epiphany-libs/blob/master/bsps/parallella_E16G3_1GB/fast.ldf#L112 のあたりの、

	.data_bank1 : {*.o(.data_bank1)} > BANK1_SRAM
	.text_bank1 : {*.o(.text_bank1)} > BANK1_SRAM
	.data_bank2 : {*.o(.data_bank2)} > BANK2_SRAM
	.text_bank2 : {*.o(.text_bank2)} > BANK2_SRAM

だけ読めればいい。

.data_bank1 : {*.o(.data_bank1)} > BANK1_SRAM

は、.data_bank1 という領域に、*.o の.data_bank1 というセクションに含まれるデータを全て書き込め、という意味。セクション名は、習慣として決まっていて、プログラムは.text、初期値無し変数(0初期化)は.bss、初期値有り変数は.data、定数は.rodataに書かれる。


変数を置く場所を明示的に示したい場合は、GCCなら、__attribute__((section("section_name"))) とかする。


以下、x86の例だが、

 $ cat a.c
__attribute__((section("watashi_ha_koko"))) int var;
 $
 $ cat a.lds
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)

SECTIONS {
    .foobar_segment : { *.o(watashi_ha_koko); }
}

とかいうファイルを用意して、

 $ gcc -m32 a.c -o a.o
 $ ld -T a.lds a.o -o linked

とかすると、linked というファイルができる。これをreadelf -lとかで見ると、

 $ readelf -l linked

Elf file type is EXEC (Executable file)
Entry point 0x0
There are 2 program headers, starting at offset 52

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x200000 0x00000000 0x00000000 0x00004 0x00004 RW  0x200000
  GNU_STACK      0x000000 0x00000000 0x00000000 0x00000 0x00000 RW  0x10

 Section to Segment mapping:
  Segment Sections...
   00     .foobar_segment 
   01     

とか、の、ように、.foobar_segment というセグメントが、アドレス0の位置に、4バイト分置かれるのが確認できる。


オブジェクトは、書かれた順に置かれていって、

__attribute__((section("watashi_ha_koko2"))) int var2;
__attribute__((section("watashi_ha_koko"))) int var;
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)

SECTIONS {
    .foobar_segment : { *.o(watashi_ha_koko); }
    .foobar_segment2 : { *.o(watashi_ha_koko2); }

}

とかすると、

 $ nm linked
00000000 D var
00000004 D var2

こうなる。(説明がひどい)

.data_bank1 : {*.o(.data_bank1)} > BANK1_SRAM

左半分はいいだろう。右側の > BANK1_SRAMは、書き込むメモリ領域を意味していて、

https://www.sra.co.jp/public/sra/product/wingnut/ld/ld-ja_3.html#SEC38


https://github.com/adapteva/epiphany-libs/blob/master/bsps/parallella_E16G3_1GB/fast.ldf#L62 このあたりに、

MEMORY
{
	// <..snip..>
	BANK0_SRAM (WXAI) : ORIGIN = LENGTH(IVT_RAM) + LENGTH(WORKGROUP_RAM), LENGTH = 8K - LENGTH(IVT_RAM) - LENGTH(WORKGROUP_RAM)
	BANK1_SRAM (WXAI) : ORIGIN = 0x2000, LENGTH = 8K
	BANK2_SRAM (WXAI) : ORIGIN = 0x4000, LENGTH = 8K
	BANK3_SRAM (WXAI) : ORIGIN = 0x6000, LENGTH = 8K
	// <..snip..>
}

とかいうように、BANK1_SRAM は、 0x2000 にあって、サイズは8KBですよ、というようなことが定義されている。


なので、

.data_bank1 : {*.o(.data_bank1)} > BANK1_SRAM

これは、

「全部のオブジェクトの中の.data_bank1セクションに含まれるデータを、.data_bank1という名前を付けて、0x2000 に書き込みなさいよ」、という意味になる。


まあ上の説明は結構ひどいが、開発環境によっては、深追いするならldscriptの知識が必須なことは多いので、必要だと感じた時に勉強しておくと良いと思いますね。


shared_dram セクション

次に、この箇所を見る。
https://github.com/adapteva/epiphany-examples/blob/2014.05/apps/hello-world/src/e_hello_world.c#L34


変数に、SECTION("shared_dram"); と付いているのが確認できると思う。


SECTIONマクロの定義は、
https://github.com/adapteva/epiphany-libs/blob/2014.11/src/e-lib/include/e_common.h#L31
で、これが、さきほど見た、__attribute__((section("xx")))だというのが確認できる。


つまり、このoutbuf変数は、shared_dram セクションに書き出されるということになる。実際にどうなるかは、さきほどのfast.ldfを見ればよくて、

https://github.com/adapteva/epiphany-libs/blob/2014.11/bsps/parallella_E16G3_1GB/fast.ldf#L122

	.shared_dram : {*.o(shared_dram) *.o(.shared_dram)} > EXTERNAL_DRAM_1   // shared_dram セクションの変数は EXTERNAL_DRAM_1 に置く


https://github.com/adapteva/epiphany-libs/blob/2014.11/bsps/parallella_E16G3_1GB/fast.ldf#L49

	EXTERNAL_DRAM_1 (WXAI) : ORIGIN = 0x8f000000, LENGTH = 0x01000000 /* .heap */

EXTERNAL_DRAM_1 領域は、0x8f000000 から、 16MB(0x01000000) ということがわかる。


実際、上の hello_world をビルドして、nmで変数のアドレスを確認すると、

 $ ./build.sh
 $ cd Debug
 $ nm e_hello_world.elf  | grep outbuf
8f000000 D _outbuf

0x8f000000 に、_outbufが置かれていることが確認できる。


つまり、このHelloWorld は sprintfを使って、文字列を 0x8f000000 に置いている、というわけである。


eMesh、eLink、core id、0x8f000000

さて、では、この0x8f000000 というアドレスは何者だろうか?何故、データを 0x8f000000 に置くと、ARMからデータが見えるようになるのだろうか?


これを理解するには、Epiphanyアーキテクチャについて、いくらか知識が必要になる。


Epiphany は、16コアの場合、4x4 のタイル型アーキテクチャになっているわけだが、複数のEpiphanyを組み合わせて、さらに大きなタイルにすることができる。


Epiphanyコアの四辺に、eLink と呼ばれるインターコネクトが出ていて、これを繋ぐことで、16 コア Epiphany 自体を、さらに大きなタイルの中に組み込むことができるわけだ。

(http://www.adapteva.com/docs/e16g301_datasheet.pdf の Figure 1 より)


これによって、最大64x64コアのタイルを作ることができる(実際にはCore_0_0は作れないように見えるので、64x64-1だと思う)。


この時、各コアには、コアIDとアドレスが付けられる。コアIDは、(32,32)のように(row,column) のようになる。アドレスは、32bit空間に均等に割り当てられて、全4096 コアで、32bitアドレス空間を割って、4GB / (4096) = 1MBが、各コアの内部レジスタ、コア内蔵SRAM等に割り当てられる。


これが、http://www.adapteva.com/docs/epiphany_arch_ref.pdf の Figure 5 になる。


このアドレス空間は、性能を無視すると、ソフトウェア的にはフラットに見える。つまり、自分のがどのコアだろうと、0x04300000 を読みに行くと、CORE_1_2 のSRAMに書かれたデータが読めるし、0x07f00000に書き込むと、CORE_1_63のSRAM上のデータを更新できる。


この、コア間通信を実現しているハードウェアが、eMesh、eLink である。


32bit空間は、各コアごとに均等分割されているので、アドレスの、上位12bitを見ると、どのコアへのアクセスかがわかる。メッシュ内のルーターは、アクセスが来た場合に、そのアドレスから、アクセスの方向を判断し、その方向に応じて、メモリアクセスをEast、West、North、Southのいずれかの方向に渡すようになっている。

で、16Core Epiphanyは4x4 コアしか無いので、例えば、(48,48) のようなコアにアクセスすると、該当するコアが無いという状態になってしまうわけだが、この時もメッシュ内のルーターは、同じように動いて、North へのアクセスは、North へ投げるようになっている(ハードウェア実装が実際どうなっているかは知らない)。
そうすると、やがてアクセスは、コアの端に辿り付いてしまうわけだが、そうすると、このアクセスは、eLink と呼ばれるインターコネクトのプロトコルに変換されて、外部へ出ていくようになる。


eLinkのプロトコルは、http://www.adapteva.com/docs/e16g301_datasheet.pdf の、3.7 にある。16コアEpiphany (E16G301) は、このeLinkプロトコルを実装しているので、このeLinkの先に、さらにE16G301を繋げてもよい。
http://parallella.org/docs/parallella_schematic.pdf
とかを見ると、読みかたわからないのではっきりしたことは言えないけど、North、South は、http://www.parallella.org/wp-content/uploads/ParallellaBottomView4hi-01.png の North、South と繋がってて、これをくっつけることで、32コアEpiphanyマシンが作れるとかを、想定しているのだと思うのだよね。(まあ実際にはCONFIGの入力を変えないといけないのでそのままでは繋がらない気がするけど)


まあ、そういう感じに、Epiphanyをくっつけていってもいいのだが、このParallella ボードでは、eLinkのEastが、Zynqに繋がっている。なので、Epiphanyプログラムから、East方向に向けて外部へアクセスすると、eLinkプロトコルに乗って、そのアクセスが、Zynqへ伝えられる、というわけだ。


(続きは明日書く)