プログラマが知るべき97のこと 読書メモ
プログラマが知るべき97のこと をスキマ時間に読んでいた。気になったところをメモしておく。 技術的負債は、即座に次のイテレーションで返済する。そうでなければ利息が膨れ上がる。 ゼロから描き直したい衝動・誘惑に打ち勝たなければならない。できるだけ既存コードを生かす。 新たな変更を加える時には1箇所でも良いので改善する。 コードを書くことは設計をすること。機械的な作業ではなく、創造的なもの。 コードレビューの目的は誤りの修正だけでなく、コードの共有所有を確立すること。 コメントには読む人が価値があるものを書く。つまり、コードに書いていないことや、コードにかけないことが書かれるべき。 プロジェクトの初期からデプロイ作業を始める。 1万時間やればエキスパートに必ずなれる。ほぼ自分がなろうとするかという意思の問題。 Windows 2000には"TERRIBLE HORRIBLE NO GOOD VERY BAD" と書かれている。見られて恥ずかしいデータは使わないこと。 厄介なのは今はプログラマでなくても以前プログラミングを「ちょっとしたことがある」くらいの人。 SCM へのチェックイン後は開発者は傍観者になるべき 開発者は開発サーバより後の環境へはアクセスしない、QA・顧客は開発サーバへアクセスしない API自身だけじゃなく、APIを利用するコードのテストもする ハードワークは報われない。自分の働く時間や労力を減らすほどプロジェクトへの貢献が大きくなる。 知識と技術の研鑽を怠ってはならない。 バグレポートには、バグの再現方法、頻度、本来の仕様、実際の動作を書く。 パラダイムの異なる第二の言語を学ぶ。 データ構造、アルゴリズムの時間と空間の複雑性を知る。 見積もりを求められた時には、見積もり、ターゲット、コミットメントを区別する。 正しく使用する方がミスするよりも簡単。インターフェイスが良ければ、正しく使用できる。 メモリ共有の代わりに、メッセージパッシングを使う。 テスト担当者は敵ではなく友人。 環境に関する情報もバージョン管理する。 互いの理解には、定義の共有ではなく経験の共有が必要。 シングルトンパターンはテストや保守性の点で不利。 Ubuntuはズールー語で他者への思いやりという意味。 このコードは生涯、自分がサポートし続けると思って書く。 テストは見る人のために書く。 顧客には頻繁に疑問を投げかける。顧客から言われたことを自分の言葉で言い直すと良い。 議論の時は図や絵を利用することが大切。設計の早い段階でモックアップを作るのも良い。 ステートを取り扱う時はステートマシンを意識する。 コードネームをつける。身近でよく知っている名前が良い。チームの結束のための合言葉。 単純作業であってもルーチンワークを1日の最初に行う余地を残しておくと良い。 師匠となる人を見つけると良い。 快適な環境を追求する。 声の大きいユーザからの要望に忠実に答えると、バグの出やすいソフトウェアになってしまう。Feature Creepに陥る。 命名にこだわる。機能の設計の重要な一つ。
入門 eBPF 読書メモ
入門eBPF を読んだので、 読書メモを残しておきたい。去年発売された本でずっと読みたかったもの。 lizrice/learning-ebpf - github.com にサンプルコードが豊富に準備されているので参考になる。 文脈は意識せずとりあえず個人的に気になったところをメモしておく。 BCC まずはBCCを使った例から始まる。 bpf_trace_printk() を使うと擬似ファイル/sys/kernel/debug/tracing/trace_pipeにテキストを出力できる。 簡易的にはこれを使えば良いが、eBPFプログラムごとに出力先を分けたい場合には、BPF MAP を使って自前で カーネルとユーザ間でデータをやり取りすれば良い。 PerfリングバッファやBPFリングバッファを使えば柔軟なデータ構造(構造体)でやり取りできる。 前者はCPUごとに領域が分かれているが、後者は全てのCPUで共通の領域で順番も正しい。さらに性能も良いらしい。 Tail Callという仕組みを使えば別のeBPFプログラムを呼び出せる。 Tail Callは完了しても元のeBPFプログラムには戻ってこないのでジャンプのようなもの。 Tail Callを使うにはあらかじめ BPF_MAP_TYPE_PROG_ARRAY 型のMAPを準備しておく必要がある。 仮想マシン 汎用レジスタは10個、そしてスタックフレームポインタがある 1。 Calling Conventionについて解説がされていた。 reg0がeBPFプログラムの引数で、reg1がその戻り値となる。 関数呼び出しではreg1からreg5が引数となる。 命令長は64bitだけど、それを組み合わせたワイド命令もある。 SEC()マクロでコンパイル後のセクション名がわかる。 CからeBPFバイトコードへ、あるいはRustからeBPFバイトコードへコンパイルできる。 bpf(2) bpftool コマンドは eBPF のロードやアタッチができる便利なツールだが、その中では bpf(2)が使われている。 多くの操作は bpf(2) で実現できるが、アタッチに関してはいくつかバリエーションがあり、bpf(2)な場合もあればperf_even_open(2)とioctl(2)を組み合わせる場合もある。 カーネルにロードされたeBPFプログラムやMAPは、複数参照カウンタが0になれば自動削除されるが、BPF linkや特殊なファイルへのピニングによって参照カウンタを1つ増やせる。 これによって bpftool コマンドは実行が終了したとしても、eBPFプログラムをロードしたままにできる。 CO-RE, libbpf CO-RE (Compile Once - Run Everywhere)を使うには BCC ではなく libbpf を使う。 カーネル5.4以降でサポートされている 。vmlinux.h を include するとカーネル内の多くの構造体を使える。 bpf_core_read を使うと実行カーネルバージョンとコンパイル時カーネルバージョンの差を埋めるよう、自動的にリロケートを考慮しつつカーネルデータを読み込める。 -g をつけてコンパイルしておくとeBPF検証機のデバッグも楽になる。 以前はループをunrollする必要があったが、今はbpf_loopやbpf_for_eachが用意されている。 プログラムタイプ 30個くらいのプログラムタイムと40を超えるアタッチメントタイプがある。 bpftool features でプログラムタイプごとに利用できるヘルパ関数一覧を取得できる。 Kfuncs という仕組みを使うと、カーネル内の関数をBPFサブシステムに登録できる。 さらにCORE BPF Kfuncs というカーネルバージョン互換のあるものもある x86限定であるが、kprobe/kretprobeよりもfentry/fexitを使うと良い。 fentry/fexitはカーネル5.5でのBPFトランポリンのアイデアと一緒に導入された。 fexit は引数と返り値をまとめて取得できる点でも便利。 tracepoint は安定したインターフェースを提供してくれている。 さらにBTF Tracepointもあり、カーネルバージョンごとの構造体メンバの差分を吸収してくれる。 uprobe/uretprobeやUSDTでユーザ空間の関数にアタッチできる。 プログラムタイプにはLSM(Linux Security Module)向けのものもある。 これによってeBPFからセキュリティポリシーを強制できる。もともとはカーネルモジュールがやっていたこと。 ...
OVSでSRv6を使ってみる
OVS(Open vSwitch) 3.2でSRv6がサポートされた 1 ので使ってみる。 その使い方を端的にいうと以下のように type=srv6 としたポートを作成すれば良い。 VXLANやGeneveなど既存のトンネリングと同様のフレームワークを使って実装されているので、 options:remote_ipやoptions:local_ip でトンネルの両端(SRv6では双方のSIDに相当)を指定する。 それらに加えてSRv6では中継するルータをSegment Listとして設定できるよう特別なオプション options:srv6_segs が存在する。 ちなみにInner PacketとしてはIPv4、IPv6両方をサポートしている。 ovs-vsctl add-br br0 ovs-vsctl add-port br0 srv6_0 -- \ set int srv6_0 type=srv6 \ options:remote_ip=fc00:100::1 \ options:srv6_segs="fc00:100::1,fc00:200::1,fc00:300::1" OVSでは主にkernelspaceとuserspaceの2種類のデータパスが存在しているが、 SRv6はuserspaceのみ対応している 2。 つまりDPDKやAFXDP 3 のような仕組みを使ってデプロイする必要がある。 Feature Linux upstream Linux OVS tree Userspace GRE 3.11 1.0 2.4 VXLAN 3.12 1.10 2.4 Geneve 3.18 2.4 2.4 … … … … SRv6 NO NO 3.2 さてどうやって動かすのか。 mininetのスクリプト 4 をベースとしてそれをシンプルなコマンド列に落とし込んだ。 以下のような構成で動かしてみる。 まずOVS 3.2以上が動作していることを確認する。 またこの記事ではAFXDPを使っているので、コンパイル時に ./configure --enable-afxdp としておく。 ...
Ansible実践ガイド [基礎編] 第4版 読書メモ
どうやら2016年にもAnsibleを勉強していたようだが 1 、 すっかり忘れてしまったので改めて Ansible実践ガイド 第4版[基礎編] を読んだ。 気になったところをメモしていく。 Playbookの階層 1つのPlaybookはいくつかのPlayから構成される。 PlayはTargets、Vars、Tasks、Handlersの4つのセクションから構成される。 TargetsとTasksは基礎的なセクションで、 それぞれ対象のホストと実行するタスクの一覧を指定するために使われる。 VarsとHandlersは補助的なセクションで、それぞれ変数と実行制御 (例えばsystemdサービスのリスタート)を指定するために使われる。 Playbook Play: Targets Vars Tasks Handlers Play: Targets Vars Tasks Handlers テスト 単純なテストはansible.builtin.assertモジュールでカバーできる。 複雑なテストのためには Ansible Molecule 2 を使う。 テスト環境構築、文法チェック、冪等性のチェックなどの仕組みを持っている。 あるいは Ansible Spec 3 を使うと、RubyのRSpecを利用した Serverspecによってテストできる。 Ansible Galaxy ansible-galaxy install geerlingguy.mysql のようにしてロールをインストールできる。 ~/.ansible/roles あるいは /usr、/etc 配下に配置される。 --roles-path で指定することもできる。 Collectionは ~/.ansible/collections に配置される。 requirements.txt でその一覧を管理することができる。 チューニング ansible.cfgまたはANSIBLE_CACHE_PLUGINでファクトキャッシュを有効化しておくと良い。 forksを増やしておくと、 ローカルノードのリソースやネットワークの負荷とトレードオフになるが、 並列数を増やせる。 Ansibleは並列実行するとき全てのホストで足並みを揃えてタスクを1つずつ実行している。 これはストラテジプラグインで制御できる。 例えば ansible.builtin.free とするとホストごとに独立してタスクを進められる。 もちろんホスト間の依存には注意する。 SSHの多重接続(ControlMaster、ControlPath、ControlPersist)を有効化しておくと、 タスクごとにSSHを確立することによるオーバーヘッドを削減できる。 ちなみにOpenSSH6.5以降ならデフォルトで多重接続が有効化されている。 デバッグ ansible-playbookコマンドの--checkで文法の静的チェックを実行できる。 また--diffオプションでfileおよびtemplateで展開される結果の差分を確認できる。 ...
つくって学ぶkubebuilder 読書メモ
つくって学ぶkubebuilder を読んだ。 簡潔によく纏まっている資料で勉強になった。 読んだ感想としてCustom Resource(CR)は自由度が高いので作りたくなるけど、 迂闊にその数を増やすと管理しきれなくなると思う。 おそらく本質的にCRでないと実現できない課題に対してのみ採用して、 かつミニマムに作ったり使ったりするのが良いんだろうと感じた。 例えばここで題材としたMarkdownViewに関しては、 現実にはCRじゃなく標準リソースの組み合わせで実現した方が良いんだろう。 もちろん題材としてはとっつきやすく良かった。 性能面はどうだろうか。 Reconcileループは1ループを軽量にしないと収束時間の予測が難しく運用しづらそう。 リトライはその状態を次のループにわたすなど、おそらく実装上のテクニックがいくつかあるんだろう。 標準のコントローラ、etcd、kube-apiserverへ与える性能影響も気になる。 Kubebuilerに相当するフレームワークは他にあるんだろうか? Kubebuilderが標準でメジャーなのかな。 ざっくり調べたところKubernetes Way(client-goとcode-generator)とOperator SDKがありそうだ。 ここからメモ。私は Kubebuilder を今回初めて触るので誤解も含まれているはず。 序章 KubebuilderはCustom ControllerやOperatorを開発するためのフレームワークで、 controller-toolsとcontroller-runtimeが含まれる。 Custom Resource(CR)のコントローラをCustom Controllerと呼ぶ。 CRの仕様はCRD(Custom Resource Definition)で定義され、これはGoの構造体から自動生成される。 Kubernetesのリソースは宣言的、冪等、レベルドリブントリガーといった特徴があるので、 それに則った形で実装する。 MarkdownViewというCustom Resource/Controllerを題材にして手を動かしながら学べる。 ここ にコードが公開されているのであわせて読むと良い。 MarkdownView Custom Resourceを使うと以下を実現できる。 ConfigMapにMarkdownを1ファイルずつ保存する MarkdownのレンダラーとしてmdBookイメージを使ったDeploymentを作成する コンテナイメージとレプリカ数をCustom Resourceで指定する mdBookのDeploymentに外部からHTTPでアクセスできるようSerivceを作成する kubebuilder init/edit サブコマンド まずは空のディレクトリ上でinitサブコマンドを発行してプロジェクトの雛形を作る。 途中で変更する場合にはeditサブコマンドを使う。 重要なオプションは以下の2つ。 --domainでCRDのグループ名を指定する --repoにGoモジュール名を指定 makeコマンドを頻繁に使うのでmake helpでmakeターゲット一覧を確認しておくと良い。 ファイル中の//+kubebuilderは重要なマーカーなので削除しないよう注意する。 cmd/main.goがCustom Controllerのエントリポイントとなる。 config/配下にマニフェストが集約されている。 これらマニフェストは kustomization.yaml でまとめて管理されている。 create サブコマンド createサブコマンドで新たなAPIやWebhookを追加することができる。 例えばAPIの場合には、 create apiサブコマンドでCustom ResourceやCustom Controllerの雛形を生成できる。 ...
MininetでSRv6 L3VPNを動かす
Mininet 1 の中でSRv6 L3VPNを動かす実験をやってみた。 スクリプトはこちら。 ひとまず図のような小さな構成で動かすことができたのでここで紹介したい。 2台のルータ(r1とr2)がSRv6によるEncap/Decapを担当しており、 r1-r2間でL3VPNに関する情報をeBGPで交換する。 r1とr2はそれぞれ2つのVRF(vrf10とvrf20)を持っており、 VRFごとにテナントが分けられている(Tenant10とTenant20)。 もちろんテナントごとにL3の疎通性はなくIPレンジの重複は許されているので、 ここでは同じPrefixを割り当てている。 r1とr2ではBGPデーモンとしてFRRを使っている。設定の一部を抜粋するとこんな感じ。 FRRの設定ファイル(frr.conf)全体についてはmininetlab 2 に記載している。 FRR本体のテストコード 3 が非常に簡潔で綺麗にまとまっているので、それを参考にした。 FRRではSRv6 L3VPNの開発は活発に行われているので、できれば新しいバージョンをおすすめする。 ここではFRR 8.5を使った。 mininet> r1 vtysh -c "show running-config" # 一部抜粋 router bgp 65001 bgp router-id 203.0.113.1 bgp default ipv4-vpn bgp default ipv6-unicast bgp bestpath as-path multipath-relax no bgp network import-check neighbor r1-eth0 interface remote-as external ! segment-routing srv6 locator default exit exit ! router bgp 65001 vrf vrf10 bgp router-id 203.0.113.1 ! address-family ipv4 unicast redistribute connected sid vpn export 16 rd vpn export 65001:10 rt vpn both 0:10 export vpn import vpn exit-address-family exit ! segment-routing srv6 locators locator default prefix 2001:db8:1:1::/64 block-len 40 node-len 24 func-bits 16 ! end SRv6ではEncap時に宛先をSID(Segment ID)のリストで表現する。 SIDはIPv6フォーマットで記載できLocator、Function、Argumentの3つのフィールドから構成される。 Locatorは普通ルータごとに個別の値を与えるのでr1には2001:db8:1:1::/64を割り当てた。 ...
TRexのラッパーを作ってみた
まずはTRexの紹介から。 TRex 1 はソフトウェア実装のトラフィックジェネレータで、 Stateful/Stateless の2モードをサポートしている。 Statelessは状態を持たない対象DUT(Device Under Test)宛に パケット列を生成するためのモードで、 スイッチングやルーティングの性能計測ツールとして利用できる。 TRexは多機能ではあるが、個人的なニーズとしては単純なTCP/IPのパケット列を そのサイズを変えながら生成したいことが多いのでTRexのラッパーとしてautotrex 2 を作ってみた。 autotrexは自動的にベンチマークの実行・集計をするためのもので、 例えば成果物として下記のような図を簡単に生成できる。 ここでは簡単に使い方を記録しておく。 autotrex では trex_cfg.yaml (TRexの世界でよく使われる設定ファイル)に記載した2つのポートのうち、 1つ目のポートが送信、2つ目のポートが受信を担当する。 送信・受信それぞれのパケット数をカウントし、その誤差がある閾値 (例えば0.01%)以下であるような最大のパケットレート (Packet Per Second、PPS)を二分探索で求める。 もちろん生成したいパケット列は自分で下記のように自由に設定できる。 私がよく使うものはautotrexリポジトリ同梱されていて 3、 簡単に使えるようになっている。 # tcp_1pkt.py から抜粋 pkt=Ether()/IP(src="16.0.0.1", dst="48.0.0.1") / TCP(dport=12, sport=1025)/(payload_size*'x') ./run.sh tcp_1pkt.py のようにパケット列を記載したPythonファイル tcp_1pkt.py を 引数に与えて run.sh コマンドを発行すると、自動的にサイズを 変えながらパケット生成が行われる。 一連のベンチマーク実行が完了すると、成果物としてパケットレート、L1・L2のスループットを表すcsvファイルとpngファイルが出力される。 どのようなパケットが生成されるかについては./simulate.sh tcp_1pkt.pyコマンドで 事前に知ることができる。 独自にパケットを生成したい場合には便利かもしれない。 性能が欲しい環境では下記のように trex_cfg.yaml でCPU情報を設定する。 経験的にmaster_thread_id、latency_thread_id、threads にはユニークなCPUを 割り当てた方が高い性能を得られる。詳しくはドキュメント 4 に説明がある。 platform: master_thread_id: 0 latency_thread_id: 5 dual_if: - socket: 0 threads: [1, 2, 3, 4] 以上、個人的に使っているツールの紹介。おしまい。 ...
プログラマーのためのCPU入門 読書メモ
非常に分かりやすく要点が整理されていて良い本だった。 特にアセンブリで書かれた実験用サンプルコード 1 があったのが嬉しかった。 概念として知ってはいても動かしてみると腑に落ちる感覚がある。 ここでは自分に宛てて雑多なメモを残しておく。 アセンブリ記法 AT&T記法よりもIntel記法の方がわかりやすい。これは自分も感じる。 Intel記法はdestination が左側にくる記法。 gdb向けに~/.gdbinit内でset disassembly-flavor intel とやっとくと良いかもね。 命令実行にかかるサイクル数の計測 add、mul、mov命令にかかるサイクル数を統計的に計測できる 2 。 大量に同一の命令を実行することで統計的に値を出していく。 さらにそれらの命令間で真のデータ依存を持たせることで、 スーパースカラやスーパーパイプラインの影響を排除し1命令ずつ実行していく。 分岐予測 分岐命令のメモリアドレス → ジャンプ先のメモリアドレスをテーブルにキャッシュしておく。 そして再度同じ分岐命令が発行された時にジャンプ先を予測するのが基礎的なアイデア。 関数からの復帰の場合にはreturn address専用のテーブルにキャッシュしておく。 条件を伴う分岐の場合には”100010”のようなビットマップの構造で過去数回分の結果を保持しておいてそれとマッチさせることで予測する。 あるいは対象の命令とメモリアドレスの観点で近くにある他の条件分岐の結果も考慮に入れる場合もある。 精度は95%程度、普通のCPUキャッシュヒット率は97〜%くらいなのでちょっと低め。 投機実行 条件分岐予測によってアウトオブオーダー実行することを投機実行と呼ぶ。 まあ本質的にはこれがアウトオブオーダー実行の一番の目的になる。 つまり、基本ブロック(条件分岐などで区切られた命令列)の領域を超えた命令のアウトオブオーダー実行できるため。 キャッシュコヒーレンシ SMPにおけるキャッシュコヒーレンシはMSIプロトコルとその派生によって実現されている。 アイデアとしてはCPU0がメモリに書き込んだとき、同一アドレスを持つキャッシュライン(64バイトの集まり)が CPU0以外に登場すればそこにinvalidateフラグを立てるというもの。 つまりCPU0以外のCPUがそのメモリアドレスにアクセスする場合には必ずキャッシュミスが起こり、 それに伴い最新の値を主記憶から取得できるようにしている。 Memory consistency 特にマルチコアでメモリアクセスがある時に、メモリアクセスの順番が入れ替わることで意図しない結果を引き起こすので、それの対策としてMemory Consistencyを考えないといけない。 Memory consistencyとは、要するにアウトオブオーダー実行に制約を加えること。 とはいえインオーダー実装だとしてもメモリの仕組み(複数バンクとか?)によっては順序の入れ替えが発生しうるので注意。 x86だとTSOモデル?を元にしている。 x86ではstoreやload命令のメモリアクセス順は普通入れ替わらないが、異なるメモリ属性領域にある場合には入れ替えが起こる可能性がある。 x86 SSEではlfenceやsfence命令が入った。 Linuxのbarrier()マクロ、GCCのmemory clobber、C言語のvolatile修飾子はここでいうメモリ順序を保証しないことに注意する。 これは複雑な領域なので、プログラミング言語の提供するチャネルのような抽象化されたものを使った方が良い。 ordering_unexpected.S 3 でメモリの前段にあるストアバッファに起因するメモリアクセス順の入れ替わりを再現できる。 アトミック操作 キャッシュコヒーレンシプロトコルのMESIにおけるE(排他状態)を使うと実現できる。 LL/SC命令で囲った領域もアトミックな操作ができる。 実際にはSC命令の時にLL命令で読み出したメモリアドレスに書き込みがあれば失敗・なければ成功となる。 なので、LL/SC命令のブロックを成功するまで繰り返せば良い。 もちろん性能を確実に引き出すのは難しい。 単一のプロセッサであっても時分割されるとアトミック操作は必要。 現代的なCPU 現代的なCPUとしてRISC-VアーキテクチャのBOOM(The Berkeley Out-of-Order RISC-V Processor) 4 がある。 https://github.com/takenobu-hs/cpu-assembly-examples ↩︎ ...
Intel NUC 12 Pro で仮想環境をつくる
開発機として Intel NUC 12 Pro 1 を買って Proxmox VE 2 を入れてみた。 これまでは古い Thinkpad を開発機としていたので、快適になったかなと思う。 スペックはこんな感じ。 メモリとディスクは重視したいところだったので多めにした。 一方でコア数はそこまで拘らないのでi3のモデルとした。 vPro 搭載モデルも検討したが、流通量が少なそうなこと、仮想化すればリモートでの管理操作の頻度は減るだろうということ、から見送った。 NUC:Intel NUC12WSHi3(Core i3-1220P) 電源コード:サンワサプライ KB-DM3S-1 メモリ:Team SO-DIMM DDR4 3200MHz PC4-25600 32GBx2 ストレージ:Crucial CT2000P3PSSSD8JP 2TB M.2 PCIe4.0 元々はESXi 8を使おうかと思っていたが、 “Shutting down firmware services” のエラーに苦しみ結局解決できず諦めることにした。 ちなみにPコア・Eコアが混在することによる問題もいくつか報告されているが、 それに対する知見はそれなりに広まっていて 3 4 私の環境でも同じように解決できた。 ESXi 7についてはいくつか 12 世代 Intel NUC での動作事例が見つかったが、コミュニティドライバ?が必要なので面倒に感じてしまい諦めた。 残りの候補として cockpit-machines と Proxmox VE を考えていた。 この中でもできるだけ手軽に使いたかったので Proxmox VE を選択した。 Proxmox VEのインストール手順については特筆するものはなく、 ダイアログにしたがってやっていけば自然にインストールできた。 まだまだ触れていないが、第一印象はなかなか快適で良さそうだ。 Intel NUC12WSHi3 ↩︎ Proxmox VE ↩︎ ...
LUA4-U3-AGTE-NBK ドライバのインストール
特権コンテナで遊んでいたら /lib/modules 配下を壊してしまったようだ。 よくわからないがこのマシンから外部に出ていくときに、疎通したりしなかったりする。 イーサネットをUSBタイプAとして受けるアダプタ BUFFALO LUA4-U3-AGTE-NBK 1 を使っていて、 それに対応するドライバが怪しい。 ログはこんな感じ。 ubuntu2004thinkpad:~$ dmesg IPv6: ADDRCONF(NETDEV_CHANGE): enx9096f349a025: link becomes ready usb 3-1: USB disconnect, device number 45 ax88179_178a 3-1:1.0 enx9096f349a025: unregister 'ax88179_178a' usb-0000:00:14.0-1, ASIX AX88179 USB 3.0 Gigabit Ethernet ax88179_178a 3-1:1.0 enx9096f349a025: Failed to read reg index 0x0002: -19 ax88179_178a 3-1:1.0 enx9096f349a025: Failed to write reg index 0x0002: -19 ax88179_178a 3-1:1.0 enx9096f349a025 (unregistered): Failed to write reg index 0x0002: -19 ax88179_178a 3-1:1.0 enx9096f349a025 (unregistered): Failed to write reg index 0x0001: -19 ax88179_178a 3-1:1.0 enx9096f349a025 (unregistered): Failed to write reg index 0x0002: -19 usb 3-1: new SuperSpeed Gen 1 USB device number 46 using xhci_hcd usb 3-1: New USB device found, idVendor=0b95, idProduct=1790, bcdDevice= 1.00 usb 3-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3 usb 3-1: Product: AX88179 usb 3-1: Manufacturer: ASIX Elec. Corp. usb 3-1: SerialNumber: 000000000013D6 ax88179_178a 3-1:1.0 eth0: register 'ax88179_178a' at usb-0000:00:14.0-1, ASIX AX88179 USB 3.0 Gigabit Ethernet, 90:96:f3:49:a0:25 ax88179_178a 3-1:1.0 enx9096f349a025: renamed from eth0 ax88179_178a 3-1:1.0 enx9096f349a025: ax88179 - Link status is: 1 ax88179_178a 3-1:1.0 enx9096f349a025: ax88179 - Link status is: 1 IPv6: ADDRCONF(NETDEV_CHANGE): enx9096f349a025: link becomes ready ubuntu2004thinkpad:~$ modinfo ax88179_178a filename: /lib/modules/5.11.0-41-generic/kernel/drivers/net/usb/ax88179_178a.ko license: GPL description: ASIX AX88179/178A based USB 3.0/2.0 Gigabit Ethernet Devices srcversion: 6804C9DF57CD4C6DED0FF5F 最新のドライバ 2 をインストールしてことなきを得た。 ...
vhost-userのネゴシエーション
はじめに gokvmをvhost-userに対応させるにあたり、初期化部分について調査したのでメモとして残しておく。 QEMUのドキュメントVhost-user Protocol 1 に詳しくまとまっているが、 実際に動かしてみないことには分からない部分(例外処理だったり、リクエストの順番だったり、ログの落ち方だったり) もあると思うので動かしてみた。 色々試行錯誤してみたものの、結局 QEMU と DPDK の2つだけで手軽に試すことができた。 ここでは QEMU をサーバモード、DPDK をクライアントモードで動かした。 サーバモードが vhost-user 用の Unix Domain Socket の生成、クライアントがその Socket への接続を担当する。 dpdk-skeleton のビルド DPDK にはいくつかサンプルプログラムが用意されているが、ここでは dpdk-skeleton を利用することにした。 私の使っているディストリビューションではそれは DPDK パッケージに同梱されていなかったので、以下でビルドした。 $ git clone git@github.com:DPDK/dpdk.git $ cd dpdk $ meson setup -Dexamples=skeleton build $ cd build $ ninja $ file ./examples/dpdk-skeleton ./examples/dpdk-skeleton: ELF 64-bit LSB shared object, x86-64, ... 仮想マシンの起動 軽量なVMイメージであるCirrosをQEMUからブートさせた。 ここでpath=$HOME/vhost-net0 がUnix Domain Socket に対応する。 logfile=$HOME/vhost-net0.log でその通信内容をログに吐くことができる(これは後の調査で必要になる)。 今回はサーバモードで動かすので server=on とした。 ...
自己分析ツール
一年前に受けたストレングスファインダーの結果を見つけた。いつか役に立つかもしれないのでここに残しておく。 さらに似たような 16personalities.com なるものを今日見つけたのでやってみた。 ストレングスファインダー 2021/1/18 に実施。2,000円程度の書籍を買う必要があり、テストには1時間弱かかる。 16personalities 2023/1/31 に実施。無料で15分程度でテストを受けることができる。 profile から結果に飛べる。 どうやら冒険家(Turbulent Adventurer, ISFP-T)に該当するようだ。
自作VMM u-rootベースのinitrd
はじめに gokvm開発 1 2 3 4 5 6 の続き。 前回までに紹介したとおり virtio-blk と virtio-net に対応したことで、仮想マシンが外部とIOを通してやり取りができるようになった。 今回は initrd を busybox ベースから u-root ベースへと変更したので、それについて述べていく。 0d89a47f u-rootベースの initrd の導入 Go言語で作られたVMMには、同じくGo言語で書かれた initrd が相応しいのではないかということで、Pull Requestをもらった。 1コマンドで成果物を生成でき、busyboxと比べると手順が少なく簡単な印象を受けた。 cb504d85 u-rootベースのinitrdをデフォルトとする u-rootによるinitrdをしばらく触ってみると自分のやりたいことはこなせるだろうという感触を持ったので、デフォルトとした。ただ、busyboxでは特に意識せずできていたことが u-root ではできないことがあった。例えば以下のもの。 ctrl-lやctrl-eでシェル内カーソル移動を行うために、clearやticコマンドに加えて terminfo ファイルが必要だった。 ゲストの起動時に、NIC・ファイルシステムの初期化やHTTPサーバの起動のために、それを記載したスクリプトファイルを /bin/uinit に配置したが、デーモンが途中でkillされるような挙動になってしまった。init関連の挙動に対する自分の理解が甘いのだと思う。しょうがないのでワークアラウンドとして .bashrc に記載した。 終わりに この他にもいくつかリファクタリングを実施した。 今回はVMMらしい変更はなかった。今後はマイグレーションをやっていきたい。 KVMを使ったVMMを自作してLinuxを起動するまでの記録 ↩︎ KVMを使ったVMMを自作してLinuxを起動するまでの記録2 ↩︎ KVMを使った自作VMMのSMP対応 ↩︎ 自作VMMの PCI デバイス対応 ↩︎ 自作VMM の virtio-net 対応 ↩︎ 自作VMM の virtio-blk 対応 ↩︎
自作VMM の virtio-blk 対応
はじめに gokvm開発 1 2 3 4 5 の続き。 前回の virtio-net 対応に引き続いて、virtio-blk に対応した。 virt queueのデータ構造や挙動はそのまま流用できる。 この辺り Virtio はうまく設計されているなと感動する。 7389ff59 カーネルコンパイルオプションの調整 ゲストカーネルからファイルシステムを経由してブロックIOを実現するにあたって、以下のオプションを有効にした。 CONFIG_VIRTIO_BLK=y CONFIG_XFS_FS=y CONFIG_EXT3_FS=y CONFIG_EXT4_FS=y 4f4bbb78 virtio-blkの実装 さて、それでは本題である virtio-blk の実装に移っていく。 virtio-blk の挙動は virtio-net のものとほとんど同じなので、もし前回のブログを読んでいなければ、そちらを先に読むことをお勧めする。 差分はキュー数とdescripterテーブルのエントリが指す先のデータ構造だけである。 virtio-net では送受信のため2つのキューを必要としたが、virtio-blk の場合には 1つのキューで読み書きを実現する。 これはディスクへの読み書きはどちらもOS側からの発行となるため、外部割り込みを受ける必要がないためである。 descripterテーブルエントリが指すデータ構造は、以下のように3つのエントリがLinked Listの要領で繋がっている 6 。 1つ目のエントリが指すデータ構造は blkReq であり、typeフィールドが1なら書き込み、0なら読み込みを意味する。 sectorフィールドがディスクの先頭からのオフセットを意味する。 1セクタは512バイトなので、仮想ディスク用ファイルの sector x 512 バイト目から読み書きすることを意味する。 type blkReq struct { typ uint32 _ uint32 sector uint64 } 2つ目のエントリが実データを指す。ここに実際に読み書きしたいデータをバイナリで格納する。 3つ目のエントリがステータスである。エラーが発生した場合には0以外の数値を書き込む。 その他 Virt Queue の初期化方法や Avail Ring、Used Ring の使い方は virtio-net と全く同じ。 0819b1ed シナリオテストの追加 Go言語の標準的なテストフレームワーク $ go test を使って、virtio-blk によって提供されたブロックデバイスが正しく動作していることをテストしたい。 やり方はいくつかあるだろうが、今回は vda.img というテスト用のディスクファイルを作り、これを ext2 でフォーマットした。 さらに vda.img をファイルシステムとしてマウントして、その中に index.html を配置しておいた。 ...
自作VMM の virtio-net 対応
はじめに gokvm開発 1 2 3 4 の続き。 最近の一連の開発によって、gokvm 上のVMに virtio-net によって仮想NICを提供することができた。 ネットワーキングのサポートは当初の目標の一つだったので、達成感がある。 この対応によって gokvm 上のVMはホスト(あるいはソフトウェアスイッチを経由して外部)との間で通信できるようになった。 WEBサーバを提供したり、SSHでログインできたり、と出来ることの幅が広がる大きな変更だと思う。 例によって、重要なコミットを抜き出して振り返りたい。 c5217550 Virt Queue データ構造の追加 そもそも Virt Queue とは何なのか。 Virt Queue は ゲスト・ホスト間におけるデータのやり取りに使うリング構造のキューを意味する。 例えば送受信でそれぞれ1つのキューを使うナイーブな virtio-net の場合には、 送受信それぞれ1つの Virt Queue (全体で合わせて2つのVirt Queue)が必要になる。 もちろん、マルチキューをサポートする場合やコントロールキューをサポートする場合には、さらに Virt Queue が必要になる。 1つの Virt Queue は Descripter Table、Avail Ring、Used Ring から構成される。 取り扱いたいデータのアドレスと長さを1つのディスクリプタとしてまとめ、それをテーブル状に並べたものが Descripter Table である。 Avail Ring と Used Ring は似ていて、どちらもディスクリプタのIDをゲスト・ホスト間で 伝え合うために利用される。 方向も決まっていて、Avail Ring がゲストからホスト宛、Used Ring がホストからゲスト宛 となる。 ちなみに virtio 仕様 5 の中では、ゲストをdriver、ホストをdevice と表現している。 ...
自作VMMの PCI デバイス対応
はじめに gokvm開発 1 2 3 の続き。 gokvm 上のVMからPCIデバイスを取り扱えるよう開発を進めてきた。 道のりは長いだろうが、最終的には virtio-net を経由して、VMと外部の間でIP疎通を取りたい。 現時点では virtio-net デバイスをゲストカーネルのネットワークインターフェイスとして認識させることができたので、 ひとまずそこまでのログを残しておく。 やったことを大きく分けると、(1) ゲストのLinuxカーネルに対してvirtio-netデバイスをPCIデバイスとして認識させ、 (2) virtio-netデバイス初期化を完了させることでネットワークインターフェイスとして登録させることの2点。 virt queue上の操作やパケットのやり取りについては、この記事には含まれない。 例によって、コミット単位で実装の経過を残しておく。 fc02176d lspciコマンドの追加 busyboxにはlspciコマンドが同梱されているが、pci.ids ファイル 4 が存在しない。 pci.ids はベンダIDやデバイスIDなどの数値と、それに対応する文字列が組になっているようなファイルである。 このファイルがあれば、人間に読みやすいフォーマットで出力できる。 後々のデバッグをスムーズに進めたいので、対応させておいた。 e126392e PCI Config空間に対するIOエミュレーション カーネルがPCIデバイスを認識するための重要なフェーズ。 PCI Config 空間を読む方法はいくつかあるようだが、ここではタイプ1 5 と呼ばれる方法でアクセスした。 ここで使われるIOポートのアドレスは以下の通り。 0xcf8:アドレスレジスタに対応する。バス番号、デバイス番号、Function番号、PCI Config 空間内のオフセットに対応する。 0xcfc ~ 0xcff:データに対応する。 アドレスレジスタは32bit幅で以下のように解釈される。 位置 内容 Bit 31 Enable Bit Bit 30-24 Reserved Bit 23-16 Bus Number Bit 15-11 Device Number Bit 10-8 Function Number Bit 7-0 Register Offset ざっくり PCI Config 空間のあるオフセットにあるデータを読みたいときは次のような手続きになる。 ...
Open vSwitch AF_XDPの背景と使い方
最近、OVS(Open vSwitch)がAF_XDPに対応したとの話を聞いたのでどういう背景があったのか、そしてどうのように使えば良いのか調べてみた。 OVSは、カーネルモジュールとユーザスペースプロセスから構成されている。 その構成の部分で、以下のような課題が見えてきた1ので、最近AF_XDPを使った実装に置き換えが進められているようだ。 カーネル本体の更新やシステム全体のリスタートを要求する修正がある カーネル開発者の方針や実装に影響を受ける DPDKで速度面で劣る バックポートが多すぎる ディストリビューションのサポートが受けられなくなることがある どれも構成変更を推し進めるには妥当な理由に思う。 下図は、バックポートと新規機能それぞれに起因する差分をコード行数によって比較したもの。 バックポートにかかるコストが読み取れる。 ちなみに、カーネルモジュールを使った実装は2022年4月にリリース予定のOVS 2.18で廃止される予定2になっている。 出典:Revisiting the Open vSwitch Dataplane Ten Years Later それならユーザスペースでデータプレーンを実装したDPDK(Data Plane Development Kit)で良いじゃないかいうと、 それはそれで課題がある。 ipコマンドなどカーネルネットワークスタック向けのツールと相性が良くない 特定のNICやCPUを占有してしまう この辺りの課題を解決するアプローチとしてAF_XDPの導入が進められている。AF_XDPを使うと、XDPのフックポイントに小さなeBPFプログラムを仕組んでおき、カーネルネットワークスタックをバイパスした上で、ユーザプロセスへとパケットを転送することができる。 安定した仕様を持つので、将来のカーネルリリースでも継続して使えるはず。 既存ツールとも相性が良い。DPDKからAF_XDPを使おうという話 3 もあるが、DPDKとOVSの間のメンテナンスコストが残る。というわけで、OVS本体でAF_XDPをサポートしようということになったようだ。 ところでAF_XDPを使うとどのようにパケットが転送されるのだろうか。 AF_XDP は fill リングと competion リングの2つのリングを持つ。 その各要素はディスクリプタとなっていて、umem 領域を指している。 パケット受信時の流れを図中の番号に沿って見ていく。 まずアプリケーションはfillリングに空きディスクリプタを登録する カーネルは fill リングからそのディスクリプタを取り出す umem領域にパケット本体を書き込む そのumem領域を指すようなディスクリプタを completion リングに登録する アプリケーションは completion リングからディスクリプタを取り出す そのディスクリプタの指す umem 領域からパケット本体を取り出す 出典:Revisiting the Open vSwitch Dataplane Ten Years Later 性能はどうだろう。25G NICで64バイトショートパケットを流した時のスループットとCPU使用率についてのデータがあった。 仮想マシンを経由するPVPのシナリオでは、vhostuser の採用によってCPU使用率は差し置いたままで、 スループットが向上する。 ただ、DPDKには届かない。コンテナを経由するPCPのシナリオでは、ユーザ・カーネル間のデータコピーを省略できるため、AF_XDPがベストチョイスかと思う。 向き不向きがあるので、シナリオによって選び方が変わるだろう。 ...
KVMを使った自作VMMのSMP対応
はじめに gokvm開発 1 2 の近況報告。 これまでは1つの仮想CPUにしか対応していなかった。 マルチCPUのためSMP(Symmetric Multiprocessing)対応させたいと思い立ってから2~3週間くらい試行錯誤し、無事実装することができた。 自分の知る限り KVM でVMMを作ってみたという取り組みを探す中で、 具体的にSMP対応とはどのような実装なのか解説されている資料がなかなか見つからなかった。 稚拙な記事ではあるけれど、今後自作VMMに挑戦する方にこの記事が役に立てば嬉しい。 例によってコミット単位に開発の経過を紹介していく。もちろん実際にはもっともっと泥臭い実装から始めていて、 何度もgit rebaseを繰り返しながら、最終的に解説できるよう粒度を調整したので、コミットのタイムスタンプはあてに出来ない。 #34 vCPUスレッドを複数生成 プルリクエストをいただいた。まずはioctl(fd,KVM_CREATE_VCPU,...)でvCPUを複数生成できるよう変更する。 その後vCPUごとに個別のスレッドを生成して各vCPUごとに独立してioctl(fd,KVM_RUN,...)を発行する。 vCPUはライフタイム全体を通して、同一のスレッドからioctlを発行する必要がある。 Go言語の場合にはスレッドの代わりにgoroutineを使うことが多いので、 runtime.LockOSThread()を呼び出してgoroutineとスレッドを静的に関連づけた。 ce22a91 struct mpf_intel の実装 vCPUがカーネルに認識されるためにはIntel MultiProcessor Specification 3 に従ったデータ構造を認識させる必要がある。 このデータ構造はLinuxカーネルの中で struct mpf_intel のPhysPtrが指す先 struct mpc_table に対応する。 コード 4 を読むと、チェックサム・バーション・マジックナンバーを読み取ることができたので、 仕様書は斜め読みしかしていない。 このデータ構造はどこに配置すれば良いのか。仕様書を読むとExtended BIOS Data Area (EBDA)の最初の1KB以内とあるのでそこに配置することにした。 EBDAは典型的に 0x0009FC00 に置かれる 5 ようなので、それに倣った。 a. In the first kilobyte of Extended BIOS Data Area (EBDA), or b. Within the last kilobyte of system base memory (e.g., 639K-640K for systems with 640 KB of base memory or 511K-512K for systems with 512 KB of base memory) if the EBDA segment is undefined, or c. In the BIOS ROM address space between 0F0000h and 0FFFFFh. この仕様書の中でブートを担当するCPUをBoot Strap Processor(BSP)、その他のCPUをApplication Processor(AP) と呼ぶことを知った。 このコミットでは struct_mpf_intel の準備まで。 いくつかデータ構造が登場するので、下図にメモリマップとしてまとめておく。 ...
Understanding Linux Network Internal 1~2部 読書メモ
このブログではネットワークに関する比較的新しい技術について触れてきたが、たまには古きを温めるのも良いだろうということで読んでみた。Linuxカーネルは今後も長きにわたって使われるはずで、カンペキな理解でなくとも、取っ掛かりだけでも掴んでいる意味は大きいと思う。この本は1,000頁超えで、1~7部から構成されているので、一気に読むのはモチベーション維持が難しいと思う。この記事ではとりあえず現時点で読んだところまでをまとめたい。カーネルバージョン 2.6.39 のソースコードを手元に置いて、読み進めていった。ビルド方法などは前回の記事 1 のとおり。 1部 ネットワークに関する重要なデータ構造として struct sk_buff と struct net_device がある。まずはこの2つのデータ構造を掴むことが肝要だと思う。struct sk_buff は(フラグメンテーション云々の話を抜きにすると)1つのパケットに対応する。しばしばそのインスタンスは skb という名前が付けられる。skb->data が処理を担当しているネットワークレイヤのヘッダを指している。例えば、L2の処理を行っている際にはskb->data はL2ヘッダ の先頭を指している。処理の進行に伴って、このポインタは移動していく。実データの前後に余白が設けられている。 +------------+ skb->mac skb->nh | | | | | head-----------> +------------+ | | | | | headroom | v v | data-----------> +------------+ +---------+---------+---------+--- | | | | | L2 | L3 | L4 | | tail | | Data | | header | header | header | ... | | | | | +---------+---------+---------+--- | end | | | | ^ ^ | | +----------> +------------+ | | | | | | tailroom | | | | +-------------> +------------+ +---------+ | | skb->data +------------+ struct sk_buff この構造体にどんなメンバがいるか見ていく。users が参照カウンタに対応していて、sk_getやkfree_skbで操作できる。mac_header など各レイヤに対応するポインタもある。cbはコントロールバッファの略で、48バイトの領域を各レイヤの中でプライベート(他のレイヤを意識せず)に使える。struct sk_buffは双方向リストで管理されていて、リスト全体は struct sk_buff_head に対応する。デバッガを使って、中身を見ていく。送信を担当する関数にアタッチしてみると、struct sk_buff内部に保持されたIPヘッダ の中身を見ることができる。 ...
BusyboxベースのミニマルなLinux環境を作りQEMUで起動
すでに多くの方が似たような取り組みを行っていてブログ記事 1 2 3 として丁寧にまとめられているように、 やはりこういった環境を手元にさっと作れることの意味は大きいと思う。 ここではざっくりとした仕組みを記録しておく。 成果物をスクリプトとしてまとめGithubにあげている。 特徴 CentOS6、CentOS7、Ubuntu20.04などメジャーなディストリビューション向けカーネルのビルドに対応しているので、実務よりの応用ができる Busyboxを使ってユーザランドをメモリ上に展開するので、起動のたびにピュアでミニマルな環境を作れる SSHログインや外部ネットワーク疎通が可能なので、他システムとの連携が絡む動作を検証しやすい GDBを使ったデバッグによってカーネル内部のデータを参照できる 現時点ではx86/64のみに対応している カーネルのビルド カーネルのビルドは端的に言えば、ビルド設定を.config ファイルに記述し、makeコマンドを叩くことに対応する。.config はテキストファイルなので適用なエディタでも編集できるが、専用のコマンド(make oldconfig、make defconfig、make menuconfig など)が用意されているので、それを使うことが多い。カーネルはアップストリームのものと各ディストリビューションが手を加えたものがあるがここでは以下の全てのカーネルをビルドできるよう環境を整えた。 upstream (kernel v2.6.39) centos6 (kernel v2.6.32-754.35.1.el6) centos7 (kernel v3.10.0-1160.13.1.el7) ubuntu20.04 (kernel v5.4.0-65.73) 最近のGCCで古いカーネルをコンパイルするのは難儀なので、カーネルバージョンごとにビルド専用Dockerイメージを用意した。例えばカーネルv2.6.39はCentOS6のビルド環境を使ってビルドすることにした。 FROM ghcr.io/buddying-inc/centos68:latest RUN sed -i "s|#baseurl=|baseurl=|g" /etc/yum.repos.d/CentOS-Base.repo \ && sed -i "s|mirrorlist=|#mirrorlist=|g" /etc/yum.repos.d/CentOS-Base.repo \ && sed -i "s|http://mirror\.centos\.org/centos/\$releasever|https://vault\.centos\.org/6.10|g" /etc/yum.repos.d/CentOS-Base.repo RUN yum install -y gcc perl glibc-static kernel kernel-devel \ autoconf zlib-devel zlib-static openssl-static openssl-devel 上のDockerfileをもとに buildenv-v2.6.39 という名前のDockerイメージを作る。これがビルド専用環境に対応する。 あとはLinuxカーネルソースツリーをDockerコンテナにアタッチして、make bzImageコマンドを叩けば良い。 ...