livaの雑記帳

livaの雑記帳

OSとか作ってみたい

uio_pci_genericでデバドラを書く<その3>

その2はこちら。

 

今回は厳密にはuio_pci_genericの話ではないのだけども、uioでデバドラを書く上で必須となるであろうお話。

 

当たり前だけど、DMAができないと多くのデバドラは書けない。

DMAするためには良さげなページと物理アドレスが欲しいですよね。

じゃあどうやってページとその物理アドレスを手に入れるか、というわけなのだけど。

 

結論から書くと、hugepageをallocateして、/proc/self/pagemapを舐める。簡単。

 

まずなんでhugepageを使うかというと、普通にmallocした場合、allocateしたページがカーネルによって勝手にスワップされて消えちゃうかもしれないじゃないですか。

hugepageだとそんな事がないらしい。

 

Huge pages cannot be swapped out under memory pressure.

https://www.kernel.org/doc/Documentation/vm/hugetlbpage.txt

 

hugepageをallocateしたい時は、事前に(デバドラプログラムを実行する前に)以下のようにしてhugepageを予約しておく。

echo 20 > /proc/sys/vm/nr_hugepages

 

その上で、デバドラ内で以下のような感じでmmapを呼ぶと2MB huge pageが割当たった仮想アドレスが戻り値として帰ってくる。

mmap(NULL, 2 * 1024 * 1024, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_HUGETLB | MAP_ANONYMOUS | MAP_POPULATE, 0, 0);

 

次に/proc/self/pagemapというのを舐めて、仮想アドレスから物理アドレスに変換する。mmapで受け取った値をそのまま食わせれば良い。

stackoverflow.com

 

ほぼこのコードをコピペしたら動く。なんか一箇所コンパイルエラーが出たような気がしないでもないが、自明すぎて忘れてしまった。

 

おしまい。

uio_pci_genericでデバドラを書く<その2>

その1はこちら。

 

今回はxHCIレジスタ空間を触ってみる。

 

参考になるのは、DPDKのソースとか。

 

前回やった通り、PCIレジスタ空間は/sys/class/uio/uio0/device/configをopenしてread/writeすれば良かったのだが、BARが指すPCIバイスレジスタ空間についてはまた別の方法を用いてアクセスする。

 

BAR0のI/O空間であれば、/sys/class/uio/uio0/device/resource0をmmapして、メモリアクセスすれば良い。

 

実際に使えている事を試したいので、XHCIをenableして、USB Status RegisterのHCHaltedフラグが1から0に落ちる事を確認してみる。

 

コードは以下の通り。例によってエラー処理はしていない。

#include <stdio.h>

#include <unistd.h>

#include <stdint.h>

#include <fcntl.h>

#include <errno.h>

#include <assert.h>

#include <sys/mman.h>

 

int main()

{

  int uiofd = open("/dev/uio0", O_RDONLY);

  int configfd = open("/sys/class/uio/uio0/device/config", O_RDWR);

 

  // read bar0                                                                                                                                                                                              

  uint32_t bar0;

  pread(configfd, &bar0, 4, 0x10);

 

  // write 0xFFFFFFFF to BAR0 and get size                                                                                                                                                                  

  uint32_t tmp = 0xFFFFFFFF;

  pwrite(configfd, &tmp, 4, 0x10);

  uint32_t size;

  pread(configfd, &size, 4, 0x10);

  size = ~size + 1;

 

  // write back bar0                                                                                                                                                                                        

  pwrite(configfd, &bar0, 4, 0x10);

 

 

  int fd = open("/sys/class/uio/uio0/device/resource0", O_RDWR);

  void *addr = mmap(NULL, size, PROT_READ | PROT_WRITE,

                    MAP_SHARED, fd, 0);

  close(fd);

 

 

  // get Capability Registers Length                                                                                                                                                                        

  uint8_t caplen = ((uint8_t *)addr)[0];

 

  // clear bit 0(Run/Stop) of USB Command Register                                                                                                                                                          

  ((uint32_t *)addr)[caplen/4 + 0] &= ~1;

 

  usleep(1000);

 

  // read USB Status Register                                                                                                                                                                               

  printf("%08x\n",((uint32_t *)addr)[caplen/4 + 1]);

 

  // set 1 to bit 0(Run/Stop) of USB Command Register                                                                                                                                                       

  ((uint32_t *)addr)[caplen/4 + 0] |= 1;

 

  usleep(1000);

 

  // read USB Status Register                                                                                                                                                                               

  printf("%08x\n",((uint32_t *)addr)[caplen/4 + 1]);

 

  return 0;

}

 

これを実行すると、一回目のprintfでは1が表示され、二回目のprintfでは0が表示される。きちんとMMIOを用いたデバイス制御ができている事が分かる。

 

続きはその3

uio_pci_genericでデバドラを書く<その1>

男もすなるuioといふものを、女もしてみむとてするなり。

 

uio、流行ってますね。というか、今時Ring0でデバイスを制御しても新鮮味がないです。

なのでuioを使ってパパっとデバドラを書けるようにならなきゃなぁ、というぼんやりとした焦りを感じていたのですが、いろいろな都合でuioでデバドラを書く流れになったので、調べつつ知見を軽くブログにアウトプットしていこうかと思います。あんまり資料も無さそうですし。

一応の前提として、僕は主にPCIバイスのドライバを書く事が多いので、Linuxのuio_pci_genericを使って、PCIデバドラを作るというのがこのシリーズのテーマです。あと、PCIの仕様を既に理解している人向けです。

 

uio_pci_genericのカーネルモジュールは既にロードされているとして、実際にコードを書く所から始めます。今回は、Vendor IDとDevice IDを取得する所まで。

 

実験に使うPCIバイスXHCI(USB3.0 Host Controller)なのですが、まずはXHCIのデバドラをuio_pci_genericに割り当てる作業です。

 

# echo "8086 1e31" > /sys/bus/pci/drivers/uio_pci_generic/new_id

echo -n 0000:00:0c.0 > /sys/bus/pci/drivers/xhci_hcd/unbind

echo -n 0000:00:0c.0 > /sys/bus/pci/drivers/uio_pci_generic/bind

 

参考:Chapter 5. Generic PCI UIO driver

 

 

で、uio_pci_genericを用いたとても小さなコードを書きます。

まあ、このページの劣化版ですね。

Example code using uio_pci_generic

 

こんな感じ。エラー処理とかはきちんとやっていませんが悪しからず。

#include <stdio.h>

#include <unistd.h>

#include <stdint.h>

#include <fcntl.h>

#include <errno.h>

 

int main()

{

  int uiofd = open("/dev/uio0", O_RDONLY);

  int configfd = open("/sys/class/uio/uio0/device/config", O_RDWR);

 

  uint16_t vendor_id, device_id;

  pread(configfd, &vendor_id, 2, 0);

  pread(configfd, &device_id, 2, 2);

  printf("Vendor ID: %x, Device ID: %x\n", vendor_id, device_id);

  return 0;

}

 

Vendor IDはPCI Configuration Spaceの0byte目からの2byte、Device IDは2byte目からの2byteなので、preadを用いて、configfdから該当オフセットと該当サイズ分だけ読んでいます。

 

これをコンパイルしてroot権限で実行すれば、

Vendor ID: 8086, Device ID: 1e31

 

簡単ですね。

 

その2へ続く

Toy OS meets Rump Kernel

Raphine(僕の自作OS)上でRump Kernelを立ち上げる事ができるようになった。

 

Rump Kernelとはなんぞやという話を簡単にすると、

 

  • NetBSDベースのLibraryOS
  • NetBSDカーネルとアプリケーションをstatic linkして1つのバイナリにできる。(Unikernel)
  • Linuxユーザーランドや、Xen、ベアメタル上で実行可能

 

Rump Kernelを使えば、どんな環境でもNetBSDアプリケーションが動かせるわけである。

 

さて、自作OS屋さんの悩みとして、以下のような物がある。

  • バイスを制御するためには自分でデバドラを書かなければならない。(デバドラを自分で書くのは大変なので、使いたいけども使えないデバイスが出てきたりする)
  • 互換性を持たない場合、既存のアプリケーションが動かせない。

 

これらの悩みに対する、ちょっとセコい解決策として、自作OS上でRump Kernelを起動できるようにする、というのを思いついたので、実装してみることにした。

 

これによってどんな事ができるようになるかというと、

とはいえ、NetBSDが諸々の大事な事をやり、自分のOSは特に仕事をしていないので、「自分のOSでいろいろやっている感」はない。

まあそうなると、「それってNetBSDで良くない?」って思う人もいると思うけど、これをどう活用するかは今回の話の趣旨から外れてしまうので、割愛。

 

 

話を戻して、とりあえずRump KernelがRaphine上で動きましたよ、という事なのだけども、軽くその実装について書いてみる。

 

全体構成はこんな感じ。

f:id:liva_h:20170810175344p:plain

Rump Kernelにはベアメタルで動かすためのバックエンドコードがあり、これを改造して、新しいバックエンドを作った。Rump Kernelがベアメタル上で動作する時はRing0なので、今回改造したRump KernelバックエンドコードもRing0で動く。

イメージ的にはRump KernelというカーネルモジュールがRaphineの中で動く、といった感じ。実際の実装としてはRing0上で動くアプリケーションという風に言う事もできるし、RaphineがRump Kernelのハイパーバイザとなっている、という事もできるかもしれない。

 

実装は割と荒削りなので、pciをサポートしていなかったり、タイマ周りが雑だったりするのだけど、とりあえず以下のようなコードをNetBSDアプリケーションにして、Rump Kernel内に組み込んで起動する事ができるようになった。

#include <stdio.h>

#include <unistd.h>

int main() {

  printf("test1\n");

  printf("test2\n");

  return 0;

}

 

起動画面はこんな感じ。

f:id:liva_h:20170810180544p:plain

load rump.binというコマンドでRump Kernelバイナリをロードする。

ELFの解析等をRaphine側でやった後でエントリポイントに飛ぶと、Rump Kernel(NetBSD)がCopyright表示を出して起動する。

初期化が終わると、上に貼ったコードの通りtest1、test2と表示して終了する、といった具合。

 

ソースへのリンクは以下の通り。

Rump Kernelが起動する自作OS

github.com

実装したRump Kernelバックエンド

github.com

 

 

尻切れトンボだけど、終わり。

c++でfinalつけれる時はつけよう、という話

仮想関数を使うとvtableを呼び出すからオーバーヘッドが生じるのは当たり前の話。だから仮想関数にしなくて良い時は仮想関数を使いたくない。

一方で、インタフェースを明示するためには仮想関数を使わなければいけない。インターフェースをソース内で明示したいだけなのに仮想関数のオーバーヘッドが生じるのも癪だなぁと思ってたら、finalつければいいじゃん、となったのでまとめておく。

 

先に書いておくと、たぶん殆どのc++プログラマにとってはあんまり旨味のない話のはず。

 

まずどういう状況かというと、Make時のマクロで使うソースを切り替えたい。

具体的には、aとbという2つのディレクトリがあって、それぞれにtest.hがある。

a/test.h

b/test.h 

 aのディレクトリのtest.hを使うか、bのディレクトリのtest.hを使うかはmake時のマクロ指定で決める事とする。

たぶんこの時点で普通のプログラミングではあまり起きない状況だろう。

 

それぞれのtest.hには異なるTestクラスがある。そしてこのTestクラスのインターフェースをコードレベルで明示したい。

普通に考えればTestInterfaceクラスを作って、それぞれのTestクラスがTestInterfaceを継承すれば良い。でも、test.hの外から使う時はTestInterfaceではなく、Testクラスなので、インターフェース継承によってオーバーヘッドが生じるのはバカバカしい。(型によって動的に実行コードが切り替わらないなら、vtableを使わずに静的に呼ばれて欲しいじゃない?)

 

そこでfinalを付けてみたら、O0でもきちんと静的に呼び出される事が分かった。

 

サンプルコード

#include <stdio.h>

 

class TestInterface {

public:

  virtual int Func(int i) = 0;

};

 

class Test : public TestInterface {

public:

  virtual int Func(int i) override /* final */ {

    // 上のfinalをつけたり、つけなかったり

    return i;

  }

};

 

int test(Test &t, int i) {

  return t.Func(i);

}

 

int main() {

  Test t;

  return test(t, 1);

}

 

これをこんな感じでコンパイルする

 

g++ -O0 -std=c++11 test.cc

ちなみに、バージョンは g++ (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4

 

んで、逆アセンブルする。

objdump -CD a.out

 

final無しの場合

000000000040067d <test(Test&, int)>:

  40067d:       55                      push   %rbp

  40067e:       48 89 e5                mov    %rsp,%rbp

  400681:       48 83 ec 10             sub    $0x10,%rsp

  400685:       48 89 7d f8             mov    %rdi,-0x8(%rbp)

  400689:       89 75 f4                mov    %esi,-0xc(%rbp)

  40068c:       48 8b 45 f8             mov    -0x8(%rbp),%rax

  400690:       48 8b 00                mov    (%rax),%rax

  400693:       48 8b 00                mov    (%rax),%rax

  400696:       8b 4d f4                mov    -0xc(%rbp),%ecx

  400699:       48 8b 55 f8             mov    -0x8(%rbp),%rdx

  40069d:       89 ce                   mov    %ecx,%esi

  40069f:       48 89 d7                mov    %rdx,%rdi

  4006a2:       ff d0                   callq  *%rax

  4006a4:       c9                      leaveq

  4006a5:       c3                      retq

 

final有りの場合

000000000040067d <test(Test&, int)>:

  40067d:       55                      push   %rbp

  40067e:       48 89 e5                mov    %rsp,%rbp

  400681:       48 83 ec 10             sub    $0x10,%rsp

  400685:       48 89 7d f8             mov    %rdi,-0x8(%rbp)

  400689:       89 75 f4                mov    %esi,-0xc(%rbp)

  40068c:       8b 55 f4                mov    -0xc(%rbp),%edx

  40068f:       48 8b 45 f8             mov    -0x8(%rbp),%rax

  400693:       89 d6                   mov    %edx,%esi

  400695:       48 89 c7                mov    %rax,%rdi

  400698:       e8 25 00 00 00          callq  4006c2 <Test::Func(int)>

  40069d:       c9                      leaveq

  40069e:       c3                      retq

 

というわけで、finalはつけような!

 

 

雑記(4月〜7月)

[https://twitter.com/liva_jy/status/860812256392617985:embed#\_\_attribute\_\_*1 というのを覚えた。 #liva\_notebook]

[https://twitter.com/liva_jy/status/860828365992861697:embed#nodiscardでエラーにする方法よくわからないから、\_\_attribute\_\_*2の方が使い勝手良い気が #liva\_notebook]

*1:warn\_unused\_result

*2:warn\_unused\_result

Unikernelのすゝめ

 以下の記事を読んだ。

Single address spaces: design flaw or feature?

 

UnikernelというとMirageOSの論文で最初に出てきた概念で、Rump KernelにもUnikernelで動かすモードがあるのだけど、今回はRump Kernelの事は忘れて、本来のUnikernelの話。(このリポジトリにはMirageOSに関する話もあるので、完全にそっち系の人が書いている様子)

 

まとめると、

 

・ユーザー空間とカーネル空間の切り替えコストは大きい

 ・キャッシュ汚染するため

 ・システムコールの仕組みを変えて、切り替えコストを削減するだけで効果的という研究有り

システムコールの切り替えコストが大きい事は、性能上の制約となる

 ・10Gのパケットの処理の度にシステムコールを発行すると、追いつかない

 ・DPDK(ry

・unikernelなら、アプリケーションとカーネルが共通のメモリ空間(同じユーザー空間上のメモリ)に置かれるので、システムコール発行コストが無くなって、早い

 ・言語が提供する型安全、メモリ保護を用いて、カーネル空間が破壊されない事を保証する

 

なるほど、という感じ。

Rump Kernelsだとkernelとアプリケーションって別々のメモリ空間になるのかな?どうなんだろう。要調査