J

2004 | 08 | 09 | 10 | 11 | 12 |
2005 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2006 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2007 | 01 | 02 | 03 | 04 | 05 | 06 | 12 |
2008 | 01 | 02 | 04 | 10 | 11 | 12 |
2009 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2010 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2011 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2012 | 01 | 03 | 04 | 05 | 06 | 07 | 08 | 12 |
2013 | 01 | 02 | 03 | 05 | 06 | 07 | 08 | 09 | 10 | 11 |
2014 | 01 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 10 | 11 | 12 |
2015 | 02 | 03 | 04 | 05 | 06 | 07 | 10 | 11 | 12 |
2016 | 01 | 02 | 04 | 05 |

ホーム

日記内の"morihyphen.hp.infoseek.co.jp"へのリンクは切れてます。必要な場合はお手数ですが int.main.jp へ書き換えをお願いします。

TODO: ファイル名確認を忘れないこと > 自分

twitter

 

2016-05-24

肉と値段 19:36

気分が良かった…いや良くなかったな…まあ気分とは関係無く、Walmart で grassfed とか書いてある肉を試してしまった。(grassfed = 草だけで育てた)

Walmart では、下限が $4.8/lb とかで売ってて、grassfed は $11.8/lb とか書いてた気がするので、2.5倍ぐらい値段が違う。


lb ってなんだよ不思議単位系やめろ。lbて書いてpoundって読むのやめろ。

(書いてなかったけど1月からカリフォルニア州Irvineに住んでます。)


やはり、値段が高い肉はおいしい。定期的にステーキ焼いてるが、焼いてる人間は同じなので、確実に肉のほうの問題だ。硬さが全然違うので、ブラインドテストしてもわかると思う。


値段が高いほうが、おいしいというのは、結構納得いかない。

放牧して、草だけ食べさせて苦労して育てたほうがおいしくなる。一方、狭いところで、成長促進薬投与したりすると、固くて食べづらい、というのに、特に合理的な理由なんて無いと思うけど。


真面目に考えると、安くて、かつ、おいしくできる、みたいなテクニックは、全ての牧場、全ての牛に普及するので、最終的には、値段と味のトレードオフが必要な成分だけが残って、うまくて高い肉と、かたくて安い肉が、住みわけられる、というのは理解できる。

が、その結果が、なんか、「広々とした空間で、のびのび牛を育てるとサシが入って柔らかくておいしい肉になる」「狭い空間で牛にストレスを与えながら育てると肉が固くなる」みたいな、なんかヒューマンの想像するイメージと一致してしまうトレードオフ成分が最後に残るのが納得いかない。

トラックバック - http://d.hatena.ne.jp/w_o/20160524

2016-05-20

clflushopt を使ってキャッシュフラッシュを高速にしよう 17:48

キャッシュフラッシュに苦しんでいる人も世の中にはたくさんいる。

そういう人を救うために、Skylake からはclflushopt という命令が新設された。


これまでは、clflush という命令があって、これを使うとキャッシュフラッシュができた。clflushoptは、これよりはやい。どのぐらい違うかというと、手元(i7-6770 + DDR4 2ch) だと、

clflush 4.1[GB/s]
clflushopt 43.6[GB/s]
memset 30.2[GB/s]

このぐらい違う。


#include <stdio.h>
#include <x86intrin.h>
#include <sys/time.h>
#include <string.h>
#include <time.h>

double
sec(void)
{
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);

    return ts.tv_sec + (ts.tv_nsec / 1000000000.0);
}

unsigned char *data;

void __attribute__((noinline,noclone))
clflush(size_t nline)
{
    int li;
    for (li=0; li<nline; li++) {
        _mm_clflush(data + li*64);
    }

    asm volatile ("" ::: "memory");
}

void __attribute__((noinline,noclone))
clflush_opt(size_t nline)
{
    int li;
    for (li=0; li<nline; li++) {
        _mm_clflushopt(data + li*64);
    }

    asm volatile ("" ::: "memory");
}

void __attribute__((noinline,noclone))
run_memset(size_t nline)
{
    int li;
    memset(data, 0, nline * 64);
    asm volatile ("" ::: "memory");
}

int
main(int argc, char **argv)
{
    double t0, t1, total;
    size_t size = 128 * 1024*1024;
    void *p;
    int li;
    int nloop = 32;

    if (argc > 1) {
        size = atoi(argv[1]) * 1024*1024;
    }

    posix_memalign(&p, 4096, size);
    data = p;

    clflush(size/64);
    clflush_opt(size/64);

    t0 = sec();
    for (li=0; li<nloop; li++) {
        clflush(size/64);
    }
    t1 = sec();

    total = size * (double)nloop;
    printf("%20s %f[MiB/s]\n", "clflush", total / ((t1-t0) * 1024*1024));

    t0 = sec();
    for (li=0; li<nloop; li++) {
        clflush_opt(size/64);
    }
    t1 = sec();

    total = size * (double)nloop;
    printf("%20s %f[MiB/s]\n", "clflushopt", total / ((t1-t0) * 1024*1024));


    t0 = sec();
    for (li=0; li<nloop; li++) {
        run_memset(size/64);
    }
    t1 = sec();

    total = size * (double)nloop;
    printf("%20s %f[MiB/s]\n", "memset", total / ((t1-t0) * 1024*1024));
    
}

以下書いてたらクソつまらなくて長い文章になったのでひとことで書いておくと、clflushopt を使うときは、ちゃんと mfence も入れたほうがいいです。

(以下、クソつまらなくて長い文章です)


さて、何故、同じキャッシュフラッシュなのに、clflushopt が、 clflush よりとても速いのか、だが、以下に書いておく。


まず、clflush が memset より遅いのは不思議に思うかもしれない。(思ってほしい)


clflush は、データをキャッシュから追い出す処理で、memset はメモリに書き込む処理だが、キャッシュは数MBしか無いのだから、巨大な領域に大してキャッシュフラッシュするのと、memsetするのを比べると

  • clflush : 8MBのキャッシュ領域をチェックして、あとはなにもしない
  • memset : 8MBのキャッシュ領域をチェックして、溢れたら巨大な領域にデータをストアする

と、なるはず。作業量は、clflushのほうが少ないはずだ。しかし、実際には、clflush のほうが10倍近く遅い。何故?


何故かというと、clflush は、処理の完了を待ってから、次の命令を実行する必要があるからだ。


今のCPUでは、物理的にメモリに書き込まれる値は、プログラムから見える値とは、異なる場合がある。

p0[0] = 0; // (1)
p1[0] = 0;
p0[0] = 1;

とかを考えれば、今のCPUにキャッシュが付いてることをを知ってる人ならば(1)の書き込みが、実際にメモリに書きこまれることなく世界から消えてることはわかるだろう。

store 命令は、実際にはstore なんてしてなくて、単に「次にアドレスp0を読んだときに値が見えるようにする」だけだ。

キャッシュに書き込む処理というのは、

  1. MMU経由して、仮想アドレスを物理アドレスに変換する(これも実際には2階層 TLBを引くので結構処理する)
  2. PAT を見てキャッシュの挙動を探す
  3. キャッシュ可能領域なら L1 のタグを探してあるかどうか見る
  4. L1 に無ければ L2 を探す

と、いうような処理をしないといけない。しかし、「次にp0を読んだときに値が見えるようにする」だけなら、直前に書いた値をいくつか履歴として持っておくだけでよい。(これをストアバッファとかいう)

CPUとしては、とりあえず、ストアバッファに値だけ入れておいて、そこから先は、CPUコアとは独立して動くキャッシュシステムに丸投げ、CPUコアは、キャッシュシステムと並列に動き、全体として効率が上がる、というようになる

そして、上の 1,2,3,4 の処理は、依存しない書き込みならパイプライン化できるので、長い目で見れば、スループットはCPUの速度に追い付けるぐらい十分良くできる(ここらへん説明不足。お前こんだけ長い文章書いてて説明不足なのかよぉ!)


一方、clflush では、そういうインチキは許されない。


例えば、次の処理を考える

memset(mem, 0, sizeof(mem)); // メモリ領域に値をセット
dma_set_address(mem);        // これをdma転送する
clflush(mem);                // DMAできるように領域flush
dma_start();                 // DMA開始

wait_dma();

この処理は、正しく動くだろうか?DMA は実際にメインメモリへ読みに行くので、storeと同じように、clflush が「メインメモリには書かないでclflushしたように見えるだけ」なら、間違った値が読めてしまう可能性がある。

つまり、clflush は、

  1. MMU経由して、仮想アドレスを物理アドレスに変換する
  2. PAT を見てキャッシュの挙動を探す
  3. L1 のタグを探してあるかどうか見る
  4. L1 にあればそれをフラッシュ
  5. L2 のタグを探してあるかどうか見る
  6. L2 にあればそれをフラッシュ

この動作を完全に終えた時点で、命令終了としなければならない。完了まで、次の動作を開始できないので、メモリパイプラインを埋めることができず、効率は非常に悪くなってしまう。


ボールが一個ギリギリ通せる配管をイメージしてもらって、store 命令は、この配管にボールを大量に片方向に流しこんでるイメージ

(A) store

\10回ストアして/            
      ○      ----------------        ○ 
      ↑      コロコロ =OOOOOOOOOO        ↑
      ∧      ----------------        ∧
                              /ボールまだかなー\

一方、clflush は、同じ配管を使って、ボールを交互にやりとりしてる状態をイメージしてもらうのがよいかもしれない。

(B) clflush

\フラッシュして/
      ○      ----------------        ○ 
      ↑      コロコロ =O                 ↑
      ∧      ----------------        ∧
                              /ボールまだかなー\


      ○      ----------------        ○ 
      ↑                     =O       ↑
      ∧      ----------------        ∧
                              /はいフラッシュ\


      ○      ----------------        ○ 
      ↑               O=             ↑
      ∧      ----------------        ∧
                                 /おわったよ\

   \(^o^)/
      ○      ----------------        ○ 
      ↑     O=                       ↑
      ∧      ----------------        ∧

これだと、例え、ボールの数が半分だったとしても片方向に一方的に流すよりもボールを相手に送るのに必要な時間は増えることがわかるだろう。


しかし、これは、実際の使いかたを考えると、安全側に倒しすぎだ。実際の用途なら、キャッシュライン毎に完了を待つのはほぼ確実に無駄で、もっと大きい領域で、完了を待ったほうが効率がいい。

上の配管のイメージでいうと、100個ぐらいボールを投入してから、「今まで入れた100個のボールが届いたら、1個ボールを返してくれ」とするほうが、配管の利用効率は上がる。


(C) clflushopt

\フラッシュしといて/
      ○      ----------------        ○ 
      ↑      コロコロ =XOOOOOO           ↑
      ∧      ----------------        ∧
                              /ボールまだかなー\


      ○      ----------------        ○ 
      ↑                    =XOOOO    ↑
      ∧      ----------------        ∧
                              /フラフラフラ…フラッシュ\

      ○      ----------------        ○ 
      ↑                     =X       ↑
      ∧      ----------------        ∧
                              /これで終わりね\


      ○      ----------------        ○ 
      ↑               O=             ↑
      ∧      ----------------        ∧
                                 /おわったよ\

   \(^o^)/
      ○      ----------------        ○ 
      ↑     O=                       ↑
      ∧      ----------------        ∧

ここで、(B)のように一回一回確認するのが、clflush、(C)のように複数のflushリクエストをまとめて実行するのが、clflushopt、ということになる。


なので、clflushoptのほうがはやくなるのだった。


ただし、上のような事情があって、clflushoptのほうは、命令が終わってもflushの完了が終わるわけではないので、clflushopt のあとは、ちゃんとmfence, sfenceを入れる必要がある。忘れないようにね。


// clflush はsfenceいらない

_mm_clflush(mem+64*0);
_mm_clflush(mem+64*1);
_mm_clflush(mem+64*2);
_mm_clflush(mem+64*3);
// clflushopt はちゃんと完了を待つために、最後にsfenceを入れる

_mm_clflushopt(mem+64*0);
_mm_clflushopt(mem+64*1);
_mm_clflushopt(mem+64*2);
_mm_clflushopt(mem+64*3);

_mm_sfence();


あと昔のマニュアルでは、clflushが今のclflushoptの挙動(完了を待たない)と書いてあったのだけど、ドライバが壊れることを恐れて日和ったIntel は、マニュアルに反してこれまでのCPU全てで clflush の挙動を安全側に倒しており、clflushopt の実装にあわせて、マニュアルのほうを安全側に修正するという対応をとった。過去は書きかわったのだ!(という話がそもそも書きたかったのだけど前フリが長くなりすぎたので没)

トラックバック - http://d.hatena.ne.jp/w_o/20160520

2016-05-19

無からはじめるFPGAプログラミング(2) 20:05

1 : http://d.hatena.ne.jp/w_o/20160518#1463571239


さて、PS7を置いたら、Synthesizeをしよう。Synthesizeは、ボタンをポチーとするとできる。


Synthesizeすると、netlistができる。あんまりXilinx以外は知らないのだけど、Xilinx では、複雑な演算を、FPGA内にあるprimitiveで表現できるように変換した物体のことを、netlistと呼んでる気がする。


f:id:w_o:20160519185427p:image

このへんの画面で、オブジェクトを右クリックして、Schematicを選ぶと、画像でnetlistが見られる。


f:id:w_o:20160519185452p:image

とりあえず今は、「PS7を置け」というHDLを書いたので、そのとおりPS7が置いてあるはず。


これを、Generate Bitstreamすると、何もしないbitfileが完成する(project_dir/なんとか.runs/impl_1/design_1_wrapper.bit とかにある) 。cat bitfile > /dev/xdevcfg とかすると、何も起こらないことが確認できるはずだ(DDR3とかはn/cになってるけど動く。よくわからないが固定ピンなので大丈夫なのかもしれない)。


(ここからしばらくFPGA関係無いです)

さて、Zynq には、GPIO が付いている。これは、ARMと同じように、固定機能として入っていて、FPGAが何もしないように設定されていても使える。GPIOというのは、まあ、ただの1bit入出力なのだけど(何の機能も無い1bitをGeneral Purposeと呼ぶ姿勢を見習っていきたい)、LEDやボタン等は、1bit取れればいいので、このGPIOを使ってアクセスすることが多い。

http://www.xilinx.com/support/documentation/user_guides/ug585-Zynq-7000-TRM.pdf#381

GPIO の使いかたは、このへんにある。が、別にこれを読まなくてもLinux なら以下のようにして使える。

 $ sudo sh -c 'echo 913 > /sys/class/gpio/export'  # 7番目のGPIOを見えるようにする
 $ ls /sys/class/gpio/gpio913/
active_low  device/     direction   edge        power/      subsystem/  uevent      value       
 $ sudo sh -c 'echo out > /sys/class/gpio/gpio913/direction' # out 方向に設定
 $ sudo sh -c 'echo 1 > /sys/class/gpio/gpio913/value'       # Hi 出力
 $ sudo sh -c 'echo 0 > /sys/class/gpio/gpio913/value'       # Lo 出力

これをすると、Parallella ならLチカができるはずだ。Lチカできたので満足した(完)


終わりではない。


913が何かというと、Zynqはカーネルコンフィグで、GPIO1024個と指定されてるけど、実際は118個しかなくて、何故か後ろから番号詰められるので 1024-118 = 906 が GPIO0 番に対応してるらしい。

https://forums.xilinx.com/t5/Embedded-Linux/GPIO-issues-with-Linux-version-3-17-0-xilinx/td-p/560245/page/2

GPIO 7 番が、LEDに接続されているので、906+7 = 913番目が、LEDと対応するGPIOになる。なので、それをHiにしたりLoにしたりすると、Lチカができる。(ZedboardもLEDが7番に繋がってるので同じようにできると思う)

parallella-utils には、これを簡単に使うスクリプトが入っている https://github.com/parallella/parallella-utils/blob/master/gpio (というかこれ見て上の機能知った)

しかし、残念ながら、このピンは固定なので、ARMからLチカできても、FPGAからはLチカはできない。悲しいなぁ…

(ここまでFPGA関係ないです)


で、このGPIOだが、外部I/Oに54本、 FPGA側に64本出ていて、FPGAで出したbitをARMから読んだり、ARMから出したbitをFPGAから読んだりできるようになっている。


module design_1_wrapper
   ()
   ;

   wire [63:0] gpio_out;
   wire [63:0] gpio_in;

   assign gpio_in[0] = gpio_out[1];

   PS7 ps7 (
            .EMIOGPIOO (gpio_out),
            .EMIOGPIOI (gpio_in)
            );
endmodule

昨日書いた、design_1_wrapper.v をこんな感じに変更する。ps7 の EMIOGPIOO と、EMIOGPIOI を引き出して、そのうちの、EMIOGPIOO[1] と、EMIOGPIOI[0] を接続する。これでsynthesize,implementation,Generate Bitstream して、できたbitfileを、/dev/xdevcfgに書く。


EMIOGPIO[0] は、GPIO54 と対応している。EMIOGPIO[0] を in に、EMIOGPIO[1] を out に設定して、EMIOGPIO[1] を 0 にしたり、1にしたりすると、あわせて EMIOGPIO[0] が 0 になったり 1 になったりするのが確認できるはず。

$ # (上のparallella-utils に入ってるgpioスクリプトを使ってます)
$ ./gpio ex 960 961   # EMIOGPIO[0] = GPIO54 = 906+54 = 960
Exporting 960
Exporting 961
$ ./gpio in 960       # EMIOGPIO[0] は in
$ ./gpio out 961      # EMIOGPIO[1] は out
$ ./gpio 0 961        # EMIOGPIO[1] に Lo
$ ./gpio ls 960       # EMIOGPIO[0] が Lo 
gpio960: in 0
$ ./gpio 1 961        # EMIOGPIO[1] に Hi
$ ./gpio ls 960       # EMIOGPIO[0] が Hi
gpio960: in 1

これで、1bitデバッグが可能になったはず。


netlist を見ると、がんばって EMIOGPIOO と EMIOGPIOI を接続しているのがわかるはず。

f:id:w_o:20160519200222p:image


配置後の配線を見ても、すっごいがんばって EMIOGPIOO と EMIOGPIOI を接続しているのがわかるね…

f:id:w_o:20160519200220p:image


(飽きてきたので続かないかも)

書きたかった一覧

  • IBUF, OBUF
  • クロック
  • CLB
  • AXI

なんか、HDLだけじゃなくて、primitiveの使いかたも覚えないといけないよねー的な話を書きたかった。あとTclがクソだ…というのは最初に書いたしいいか…

トラックバック - http://d.hatena.ne.jp/w_o/20160519

2016-05-18

無からはじめるFPGAプログラミング (1) 20:33

色々あって、Zynqを使うことになって、色々つらい思いをしている。


VivadoもQuartusもはっきり言ってひどいとしか言いようがない。HDLで書いたモジュールをTcl(!?!?今は21世紀だぞ??)でラップして作ったブロックをくっつけていくという設計は狂っているし、合成配置は遅すぎるというツール実装の問題もある…まあこのへんの話はまたそのうち。


最初は全くわからなかったけど、色々つらい思いをしたおかげで、大分理解してきたのではないかと思う。理解すればそんなに難しくない。もちろん、微妙にアナログの問題が出るとか、FPGA特有の難しさはある、が、まあそれは仕方無いだろうと思う。


以下、僕と同じように、ボトムアップに問題を理解していくのが好きな人向けに書いている。ちゃんとしたチュートリアルは http://www.zynqbook.com/download-tuts.html にあるので各自読んでおいて。


環境として、手元では、Parallella の Micro-Server を使っている。これは、FPGA側のI/Oは、CPUとEpiphany以外には繋がっていないので、Lチカできないし、あんまり勉強用に向いてる感じではない。まあでも安いし、Epiphanyがおまけで付いてくるし、Lチカがやりたいとかでなければまあいいんではないかな。


Parallella 固有の問題として、以下手順が必要

  • /dev/xdevcfg が有効になってない。自分でカーネルビルドして有効にしたほうがいい。
  • よくわからんけど、レギュレータのisl9305というドライバが入ってる状態で、ちゃんとしてないビットファイルをロードするとハングする。isl9305 と epiphany はモジュールにしておいて、bit ファイル変更するときは外せるようにしたほうがいい

それ以外はZedboardでも変わらないんではないかな。Zedboardは会社に返却したので確認してない。

/dev/xdevcfg

Vivado とかで作ったbitファイルは、/dev/xdevcfg に cat とかで流し込むと、回路を書きかえられる。やったね!これで君も今日からFPGAを使った再構成プログラムによる画像処理の高速化などといったアレがソレになるね!

Vivado 使いかた

Zynq Book Tutorial では、ポンポンとIPを置いていくだけでプログラムができるみたいな内容になっているが、はっきり言って、初めてやる人が、これをやって何か理解できるとは思えないんだよな。これを読んでも、HDLをどこに書いたらいいのか全くわからないという…(文法がわからないとかではなく、そもそもどのファイルに書けばいいのかとかわからない)


ちゃんと理解できるまでは、IPブロックをポコポコ置いていくスタイルはあんま良くないのではないかな…以下のようにすれば、簡単なHDLは簡単に書けるようになるので、Zynq Book Tutorial 全く理解できなかった人は以下のようにやるとよさそう。(逆に、以下のようなやりかたがよくわからないという人は、ちゃんとZynq Book Tutorialを読んだほうがいいかもしれない)


まず、Zynq Book Tutorial と同じように、新規プロジェクトを作成する。この時、この画面で、チップかボードかを選ぶ。Parallella MicroSever と Desktop は xc7z010clg400-1、Embedded Platform は xc7z020clg400-1。ZedBoardや、ZC702の人は、ボードを選べる。

f:id:w_o:20160518202956p:image

チップかボードを選んだときの違いは、外部IOピンに、名前が付くかどうか、みたいな違いだと思う。例えば、ZedBoardを選ぶと、leds_8bitみたいなのが見えるようになるが、実際にはこれは"D14"とかみたいなチップのよくわからないピンの名前と対応してて、その対応が決まったりする(もうちょっとなんか便利になってる気がする)。まあ外部ピンの話はそのうち(と言っても、手元のParallella MicroServerでは確認できないけど)。

f:id:w_o:20160518203059p:image

さて選んだあと、Zynq Book Tutorial では、Create Board して、HDL Wrapperを作って、"Let Vivado manage wrapper and auto-update" を選べ、と書いてあるが、HDLを1から書きたいのであれば、ここで、"Copy generated wrapper to allow user edits" を選ぶ。

f:id:w_o:20160518203058p:image

ここで出てくるHDLこそが、トップモジュール、つまり、C言語でいうmainみたいなもので、Vivado managed なコードを出すと、その部分がVivadoに奪われてしまうのでよくない(そのかわり、マウスでぽちぽちしたIOが、外と自動で繋がるようになる)。


Copy generated すると、名前デフォのままにしてれば、design_1_wrapper.v みたいな名前で、

module design_1_wrapper
   ();


  design_1 design_1_i
       ();
endmodule

こんな感じのVerilogが出てると思う。この、design_1 が、マウスポチポチして作ったデザインと対応している。上のは、何もポチポチしないで生成したので、空のデザインが置いてある。


今は、ポチポチしないでbitファイル作るのが目標なので(そうなの?)、このdesign_1 とかいうのを消す。かわりに、PS7 というのを置こう。

module design_1_wrapper
   ();

  PS7 ps7();
endmodule

PS7 が何か、というと、なんかprimitiveとか言うので、デバイス依存の物体を、回路上に置くことができる。C言語で言うとインラインアセンブリみたいなもの。PS7 は、Zynqの固定回路部分に該当していて、これを置くことで、PS7 と自分で作った回路を接続できるようになる。Zynqで使えるprimitive一覧は、

http://www.xilinx.com/support/documentation/sw_manuals/xilinx14_7/7series_hdl.pdf

にある。また、それぞれの要素の細かい説明は、

http://www.xilinx.com/products/silicon-devices/soc/zynq-7000.html?resultsTablePreSelect=documenttype:User%20Guides#documentation

にある。

(しかしPS7 primitiveの説明が見当たらないのでundocumentedの可能性がある)

(明日に続く http://d.hatena.ne.jp/w_o/20160519#1463655946 )

トラックバック - http://d.hatena.ne.jp/w_o/20160518

2016-05-16

blk_mq デバイスドライバを作る Part2 (終) 12:05

Part1 : http://d.hatena.ne.jp/w_o/20160511#1462967279


blk_mq では、APIとしては大きく二点変更がある。

  • 普通のblockでは、request() コールバックにキューごと渡されてきたが、blk_mq では、リクエストごとにコールバックが呼ばれる
  • blk_xx みたいな API は、 blk_mq_xx みたいになる

これに加えて、前回書いたように、ロックが細粒度になってるので、競合に注意するという点が増える。

struct driver {
    spinlock_t lock;
    struct request_queue *queue;
    struct gendisk *disk;
    struct blk_mq_tag_set tag_set;   // 設定はここに入れる

    struct blk_mq_hw_ctxt *ctxts[NUM_QUEUE];
};

struct drv_req_data {
    int foo;
    int bar;
};

irqreturn_t irq_handler(int irq)
{
    struct request *req = get_current_request();

    blk_mq_end_request(req,0);
}

void queue_rq(struct blk_mq_hw_context *ctxt,
              const struct blk_mq_queue_data *data)  // リクエストごとにコールバックに来る
{
    spin_lock(&drv->lock);   // これはロックされないで来る
    start_device(data->rq);
    spin_unlock(&drv->lock);
}

int init_hctx(struct blk_mq_hw_ctx *hctx,
              void *data,
              unsigned int index)
{
    struct drviver *driver = (struct driver*) data;
    driver->ctxts[index] = hctx;
    hctx->driver_data = driver;
}

static struct blk_mq_ops drv_mq_ops = {
    .queue_rq       = queue_rq,
    .init_hctx	= init_hctx,
};

void probe()
{
    struct driver *drv = init_driver();

    request_irq(IRQ, irq_handler);

    drv->tag_set.ops = &drv_mq_ops;
    drv->tag_set.nr_hw_queues = NUM_QUEUE;     // 複数キューを作れる。これの数だけinit_hctxが呼ばれる (と思う未確認)
    drv->tag_set.queue_depth = QUEUE_DEPTH; // キュー深さ。これが埋まるまでqueue_rq が呼ばれる(と思う未確認)
    drv->tag_set.numa_node = NUMA_NO_NODE;     // 未確認
    drv->tag_set.cmd_size	= sizeof(struct drv_req_data); // 後述
    drv->tag_set.flags = BLK_MQ_F_SHOULD_MERGE; // 未確認
    drv->tag_set.driver_data = drv;

    blk_mq_alloc_tag_set(&drv->tag_set);
    queue = blk_mq_init_queue(&drv->tag_set);

    disk = alloc_disk(NUM_MINOR);
    disk->queue = queue;
    add_disk(disk);
}

こんな感じになる。


そんなに大きな違いは無い。(いや字面だけ見たら大分変わったように見えるな…まあドライバ本体は、start_device()にあるはずなので、そこは変更しなくてよいという意味で…)



細かい変更がいくつかある。まず、複数キューを作れるようになっている。NVMeとか最近のハイエンドストレージは、複数ハードウェアキューを持てる(まあでもHW内でソフト処理するんだからハードウェアキューというのも変だが)ので、それにあわせて、一個のブロックデバイスで、複数キューが持てるようになっている。

というか、blk-mq の mq は、multi-queue の略で、これが目的だから、細かい点というのも変なのだけど…まあ、multi-queue を実現するために、ロック戦略の変更が必要になって、結果としてロック戦略の変更のほうが大きな変更になってるように感じる。


次の変更点として、リクエスト毎に付けられるドライバ依存のデータが、ブロックレイヤで割り当てられるようになっている。(cmd_size の部分。上の例では使ってないのであまり良い例ではない https://github.com/torvalds/linux/blob/master/drivers/block/null_blk.c#L354 の、null_cmd のあたり)


これのメリットは真面目に計測してないのでよくわからないけど、

http://events.linuxfoundation.org/sites/events/files/slides/scsi.pdf

に、"Avoids per-request allocations in the driver"とあるので、まあなんか意味あるんではないのかな…まあNVMeだと最大64Kリクエストとか積めるわけで、メモリ割り当ても管理するのが大事なのかもしれない。


(書くことなくなったので終わり)

トラックバック - http://d.hatena.ne.jp/w_o/20160516