livaの雑記帳

(ネタ)ユーザー空間だけでプリエンプションする

今回の話を一言でまとめると:ユーザー空間のみでプリエンプティブマルチタスキングをしたい衝動に駆られ、ptraceを用いてスタックやRIPを操作する事で実装した。

 

 

※初学者向けの補足

今回の話の基礎部分は、武内さんの本の第4章「プロセススケジューラ」の辺りを読むと(説明が分かりやすいので)理解しやすいかもしれません。コンテキストスイッチって何???みたいな人向け。

  

 

また、より高度な内容に興味があれば、以下の資料が参考になると思います。プリエンプション???みたいな人向け。

https://www.pf.is.s.u-tokyo.ac.jp/wp-content/uploads/2018/05/OS_06.pdf

 

 

 

みんな大好きプリエンプション

皆さん、ユーザー空間でプリエンプションして、実行中のタスクを切り替えたりしたいですよね!したくないですか???

 

初歩的な解説:

最近のOSは、一つのCPUの上で複数のタスクを切り替えながら動かす事でタスクを並行動作させる(マルチタスク)事ができます。この時、「アプリケーションが明示的にタスクの中断ポイントを用意してないにも関わらず」タスクを中断し、別のタスクを再開する事ができます。(アプリケーション作る時に、「ここで別のプロセスに切り替えよう」なんて意識しないですよね?)この「アプリケーションの協力を得る事なく、タスクを中断する」事がプリエンプションです。

大事なのは、「アプリケーションの協力を得る事なく」という部分です。協力を得ていたら、それはプリエンプションではありません。

 

LinuxだとプリエンプションはLinuxカーネルが行うわけですが、やっぱりカーネルなんかに依存せず、ユーザー空間だけで実現したいじゃないですか。

 

というわけで、性能とか二の次で良いのでなんとかして実装したい、というのが今回のお話です。

 

やりたい事

userland_contextswitch_test/worker.cc at master · liva/userland_contextswitch_test · GitHub


上のリンクで反転しているコードを並行動作させたい、とします。

numberを1づつインクリメントするworker1と、numberを100づつインクリメントするworker2の両方を、適当なタイムスライスで切り替えながら動かしたい、みたいな。

 

実行時のnumberの変化はこんな感じです。

f:id:liva_h:20181112015855p:plain

最初worker1が動いて、0〜3までインクリメントした後、worker2に切り替わって、103〜303までインクリメント、worker1に切り替わり、303から306までインクリメント・・・・みたいな、単純なマルチタスクです。

 

シグナルで実装できるんじゃね?説

とりあえず実行中のプログラムの中断はシグナルハンドラでできるので、シグナルハンドラから戻る前に戻り先アドレスをいじる事ができれば、別のタスクに飛べるだろ、どうせ戻り先アドレスなんてスタックに積まれてるんじゃろ?みたいな事を最初は思ったわけですが・・・。

 

sigreturn(2)とかいろいろ調べてるとこのページにたどり着きました。

stackoverflow.com

できなくは無いけど、未定義動作だからやっちゃダメだよ、と。

まあ正直言って未定義動作だろうがなんだろうが、動くんならいいんじゃねみたいな気持ちはあるんですが、そんな事言ってるとtwitterの怖い人達に怒られてしまうので、この案はボツ。

 

ちなみにブログ書きながら調べてて見つけたんですが、sigreturn oriented programmingなんていう攻撃手法があるんですね。セキュリティ詳しくないから今始めて知った。

それこそセキュリティ界隈の人たちにとってみれば、「未定義動作でも良いから脆弱性突けたら勝ち」みたいな所ありますよね。楽しそう。

 

レジスタの値を自由に書き換えたい

別にシグナルとか使わなくても、レジスタを強制的に変更する事さえできれば、コンテキストスイッチは実現できるんですよね。

レジスタってどうやったら自由に弄れるかなぁ、、、って考えた所、「ああ、gdbでできるじゃん!」と。

で、別にgdb使う必要は無いので、gdbのベースとなってるptrace(2)を使う事にしました。

 

ptraceって何ができるの

特定のプロセスに対し、シグナルをキャッチしたり、システムコールの発行をキャッチしたり、レジスタやメモリを覗いたり、書き込んだりできます。

 

以下のブログが、ptraceの使い方を具体的に示してくれていて、分かりやすいです。

th0x4c.github.io

 

ptraceを使えば、worker1を中断し、レジスタを書き換える事でworker2を再開する、みたいな事ができます。

 

f:id:liva_h:20181112075812p:plain

 

worker1が動いている間、worker2の全てのレジスタをdispatcherが保持しておき、切り替えの際は、worker1のレジスタを退避、保持していたworker2のレジスタを書き戻せばOKです。TSS(Task State Segment)ですかね?

 

 

タスク切り替えはアプリケーションプロセスで

まあ正直これでも良いんですが、dispatcherの実装はできるだけ小さくしたいです。

dispatcherに最低限必要なのは、ripを変更する事です。それと、変更前のripを保存しておかなければいけないので、スタック辺りにripを保存する必要もありますね。そうするとrspとripのみを変更すれば良いという事になります。

 

dispatcher側のレジスタ操作はこのようになります。

userland_contextswitch_test/dispatcher.cc at master · liva/userland_contextswitch_test · GitHub

これハードウェアで実装すると、割り込みって言うんですが。

 

raxなどの汎用レジスタの退避、ripやスタックの切り替えは、アプリケーションプロセスに用意した特殊なルーチン(handler)が行う事になります。

f:id:liva_h:20181112081305p:plain

レジスタの退避

userland_contextswitch_test/int.S at master · liva/userland_contextswitch_test · GitHub

 

ripとスタックの切り替え

userland_contextswitch_test/int.S at master · liva/userland_contextswitch_test · GitHub

 

後者のコードでは、next_worker(定義)という構造体に格納されているripとrspを読み込み、同時に現在のコンテキストの再開時のripとrspをnext_workerに保存しています。

 

next_workerは最初worker2で初期化されており、専用のスタックも割り当てられています。

 

ちなみに、handlerの先頭にはnopを2つ挿入しています。これはアプリケーションプロセスがシステムコールを発行している時にhandlerを呼び出そうとすると、Linuxカーネルがシステムコールからの復帰時にripを-2するからだそうで、それを回避するためのものです。

 

dispatcherのインターフェース

アプリケーションプロセスはdispatcherにhandlerの先頭アドレスを伝えなければいけません。ptraceのお陰で、dispatcherはint $3(デバッグ例外)を受け取る事ができるので、int $3が投げられた時のebxの値をdispatcher側が保持する事にしました。これはlidtですかね

 

アプリケーションプロセスからdispatcherの呼び出し

userland_contextswitch_test/worker.cc at master · liva/userland_contextswitch_test · GitHub

dispatcher側のコード

userland_contextswitch_test/dispatcher.cc at master · liva/userland_contextswitch_test · GitHub

 

定期的なプリエンプション

何時どのタイミングでプリエンプションし、タスクを切り替えるかは大事な話です。単純なOSのスケジューラは一定周期でタスクを切り替えますが、今回もその方針で実装する事にしました。

 

100ミリ秒ごとにtimeoutをデクリメントし、timeoutがゼロになったらhandlerを呼び出します。timeoutのデフォルト値は30なので、handlerは3秒おきに呼ばれる事になりますね。タイマ割り込みかな?

 

リエントラントと割り込み禁止

worker1及びworker2ではstd::coutでnumberを出力しています。このstd::coutの最中にhandlerが呼び出されたらどうなるでしょうか?もしかしたらworker1のstd::coutが中断された状態で、worker2のstd::coutが呼び出されるかもしれません。

このような事が起きても上手く動く関数の事を「リエントラントな関数」と呼ぶのですが、残念なことにstd::coutのリエントラント性は仕様では保証されていません。

そこで、std::cout実行中はhandlerが呼ばれないようにします。raxに2や3を代入した状態でアプリケーションプロセスがint $3を実行する事により、dispatcher側にhandlerの制御について通知します。

なぜ僕はclistiを実装しているのか

 

お疲れ様でした

ここまで説明してきた事を実装すると、アプリケーションプロセス、dispatcherを合わせて、およそ500行くらいで「worker1とworker2の並行処理」が実現できます。

 

ところで途中途中でセルフツッコミしてたんですが、dispatcherはどう見てもCPUの機構をソフトウェアで再実装(エミュレーション)していますよね。うん、まあそういう事もある。

OSDI 2010〜2018の中で個人的に面白かった論文を雑に一言づつ紹介

システム系のトップ学会の一つである、OSDI (USENIX Symposium on Operating Systems Design and Implementation)の論文を2010年まで一通り眺めてみたので、その中で個人的に面白かった物をまとめてみる。

 

一通り眺めたといっても、全て読んだわけではないので、見逃してる論文もたぶんある。あと、英語力低くて趣旨を取り違えてる論文もあると思うけど、寛大な心で見逃してもらえると有り難いです。

(こういう系の話、少し間違えると「おめーここ間違ってるじゃねーか!」ってマサカリが飛んでくる印象があって、あんまり書きたくないんですよね)

 

2018

LegoOS: A Disseminated, Distributed OS for Hardware Resource Disaggregation

https://www.usenix.org/conference/osdi18/presentation/shan

一つのマシンに全てのI/Oを積むのは辛い(PCIeレーン足りなかったり)、データセンター内の隣のマシンのI/Oを透過的に参照できると嬉しい。

それを実現するためのOSの抽象化モデルを提案、実装した論文。抽象化の際は、GPU、ストレージ、メモリ、CPUといった各リソースをコンポーネント単位に分割して(メモリとCPUも分割してる!)、コンポーネント間がネットワーク越しに通信する事で、リソースの柔軟な運用を可能にした。


Arachne: Core-Aware Thread Management

https://www.usenix.org/conference/osdi18/presentation/qin

アプリケーションにはスループット指向の物、レイテンシ指向の物等様々な物が存在するが、大前提としてOSはアプリの事が分からないので、例えばアプリがどれだけのCPUコアリソースを必要とするか分からない。
そこで、CPUコアリソースの調停者を作り、調停者と協調するアプリケーションは調停者管理下のコアで専有的に1コア1カーネルスレッドで動作できるようにした。管理外のコアでは既存のアプリケーションが従来の仕組みで動作する他、管理対象のコアは動的に増減可能。
調停者はユーザーランドで動くので、Linuxに変更を加える必要もなし。


wPerf: Generic Off-CPU Analysis to Identify Bottleneck Waiting Events

https://www.usenix.org/conference/osdi18/presentation/zhou

マルチスレッドアプリケーションでは、CPUの使用率が低いにも関わらずスループットが出ない、という事例が存在する。分かりやすく書くと、ボトルネックとなるスレッドがI/O待ち等でブロックしてサボってる(最適化すればより多くの処理をできるはずなのに、やってない)みたいなケースが該当する。既存の性能解析ツールでは「一番ヒマしている」スレッドを見つける事はできるが、必ずしもそれが実際のボトルネックのスレッドとは限らないので、こういう事例の解析は難しい。
これを解決するため、スレッド間の依存関係のグラフ解析等をする、新しい性能解析ツールを作った。

※1. 問題設定が面白いと思ったが、解決手法までは興味が無かったので、その辺は割愛

※2. SOSP2017でこれの前段の研究が出てる。見逃してた。これから読む。

 

 

2016

Machine-Aware Atomic Broadcast Trees for Multicores

 

https://www.usenix.org/conference/osdi14/technical-sessions/presentation/zellweger

コア間の通信コストはコア間インターコネクトやメモリの構成によって大きくばらつく。(コアAとコアBの通信コストと、コアAとコアCの通信コストが異なる事は良くある。分かりやすい例だと、AとBが同じNUMAノード内に属していて、AとCが異なるNUMAノードに属している場合とか)なので、データをブロードキャストする場合、NUMA越しの通信は極力少なくするなどといった最適化が必要になる。しかし、このような最適化はアーキテクチャ固有であるため、これまでの全てのCPUへの対応に加え、新しい世代のCPUが出る度に再度チューニングする必要があり、最適化コストが非現実的なレベルで高い。
そこで、CPUのスペックシート情報、スペックシートには乗ってない細かいデータを取るためのマイクロベンチマーク、そしてヒューリスティックな最適化を行う事で、自動的に最適なブロードキャスト順序を生成するアルゴリズムを開発した。


2014

Arrakis: The Operating System is the Control Plane

https://www.usenix.org/conference/osdi14/technical-sessions/presentation/peter

OSによるI/Oの仲介は、ネットワーク通信を始めとした広帯域、低遅延I/Oに追いつけなくなりつつあるので、もうカーネルがI/Oデータ処理のルーティングをするのを諦め(?)て、ハードウェアに任せちゃいましょうよ、という話。

このOSでは、アプリケーションが広帯域I/Oデバイスを直に触れるようにした。デバイスはI/O仮想化によって隔離されているので、アプリケーションが直にデバイスを触ってもシステムに影響を及ぼす事は無い。デバイスドライバや、プロトコルスタック層は、アプリケーションにリンクされるライブラリ(ライブラリOS)によって提供される。これによってOSはI/Oデータ処理から解放(遅いI/Oについては引き続きOSが仲裁するものの)され、デバイスのアクセス権やリソース制限といった管理業務のみを行うようになる、というのが提案されているモデルである。

 

Decoupling Cores, Kernels, and Operating Systems

https://www.usenix.org/conference/osdi14/technical-sessions/presentation/zellweger

今後ヘテロジニアスメニーコアアーキテクチャの普及が予想されるが、そのようなアーキテクチャでは電源効率のため、一部のコアを「頻繁に」動的にon/offする事になると考えられる。しかし、既存のアーキテクチャは、コアの動的なon/offを高速に行う事を想定していない。

そこで、コア単位のOSのステート、及びアプリケーションのステートをカーネルから分離したOSモデルを提案した。これによりコアの動的なon/offを高速に行えるようになった他、その応用として、カーネルのライブアップデートや、それまで動いてたカーネルをより低消費電力なコア上でマイグレーションする事を可能にした。

 

2012

Dune: Safe User-level Access to Privileged CPU Features

https://www.usenix.org/conference/osdi12/technical-sessions/presentation/belay

今のアプリケーションはRing3で動くが、Ring0上で動けるようになるとハードウェアを直接制御できるので嬉しい。具体的には、ガーベージコレクションの際にページのdirty bitを直接参照できるのでどのメモリを回収すべきか(回収しないべきか)を高速に判断できるとか、web browserやモバイルアプリケーションといった、untrustedなコードをsandbox上で走らせたい場合にring3上で走らせれば良いだけなので低オーバーヘッドになる、等である。

そこでVt-xを用いてプロセスをアイソレーションするLinuxカーネルモジュールを開発した。プロセスはRing0で動き、システムコール呼び出し時はsyscallでは無く、vmcallを用いてRing -1のLinuxを呼び出す。Linuxカーネルへの変更は不要で、かつアプリケーションもライブラリをリンクしてdune_init()を呼び出すコードを追加するだけなので、ほぼ無変更で適用可能。(標準libcだとシステムコールを呼び出す際にsyscallを経由した後vmcallを呼ぶ事になるので、syscallを省略したければ改造版libcを再コンパイルする必要あり)


2010

An Analysis of Linux Scalability to Many Cores

https://www.usenix.org/conference/osdi10/analysis-linux-scalability-many-cores

2008年、2009年と「メニーコアでOSをスケールさせるためにはカーネルの設計を大きく変えるべきだ!」的な論文が盛り上がっていたが、「いや、別にLinuxもちゃんと同期周りをきちんと改善すればスケールするでしょ、はい解散」と言った論文。

メニーコアだとスピンロックのコストが高すぎるので、できるだけコア毎の構造に分割し、ロックを取らないようにするとか、リファレンスカウンタの厳密性を緩和して同期コストを下げるとか、ボトルネックとなっている細かい同期コストを修正していきましたよ、という話

FlexSC: Flexible System Call Scheduling with Exception-Less System Calls

https://www.usenix.org/conference/osdi10/flexsc-flexible-system-call-scheduling-exception-less-system-calls

システムコールを頻繁に呼び出すのはアプリケーションの性能低下の原因になるため、システムコールの呼び出し回数を減らしたい。システムコールのコストが高いのに加え、キャッシュを汚染してしまうためにアプリケーションコードそれ自体のIPCも低下するからである。

そこで、システムコール処理をバッチする(システムコールが呼び出された瞬間はsyscall()を発行せず、バッファに呼び出し情報を記載しておく)事を提案した。これは同期的なシステムコールを、インターフェースを変える事無く非同期的(aio的な)に扱う事を意味する。また、pthread互換なM:Nスレッドモデルのスレッドライブラリを実装した。このスレッドライブラリのユーザースレッドのスケジューリングは、ユーザースレッドがシステムコールを発行したら次のスレッドに切り替える事を繰り返し、実行できるユーザースレッドがなくなった時点でカーネル空間に遷移、まとめてシステムコールを実行する、という物である。

更に、システムコールバッチ処理する事により、システムコールを処理するコアとシステムコールを呼び出すコアを分離する事ができる。(システムコールは割り込みでは無く、コア間の通信となる)これにより、システムコール処理専用コア、アプリケーションロジック処理専用コアという分離が可能になり、キャッシュのヒット率も向上する上にカーネル空間とユーザー空間の遷移コストも削減できる。

おまけ:satさんイチオシ(かどうかは知らない)

 

 

宣伝

ここに書かれている内容に興味がある方、「もっと良いアイディアで問題解決をしたい!」と思う方は、東京大学大学院情報理工学系研究科コンピュータ科学専攻加藤研究室東京大学情報基盤センター品川研究室(情報理工システム情報学専攻と兼担)がオススメです。
これはあまり一般には知られてない情報なんですが、大学院からだと比較的入りやすいらしいですよ?

alpine linuxのminirootfsからqemuで起動する話

小さくて便利で皆大好きalpine linux

docker触ってたら一度くらい使った事ありますよね?(個人の意見です)

このalpine linuxではminirootfsというものが提供されています。これはalpineのファイルシステムをtar.gzに圧縮して固めたもので、公式サイトいわく、コンテナやchrootする時に使う事を想定されているようです。

このminirootfsは2MBしかなく、数十MBもあるインストールISOと違って、ものすごく軽量です。これなら、この軽量なminirootfsを使ってファイルシステムを構築すれば軽量なんじゃないかと思い、(ちょうど軽量なLinux環境をqemuで立ち上げたかったので)やってみました、というのが今回の話です。

別にminirootfsとか使わなくても、これとか使ってVMイメージを作れば十分軽量なんじゃないかと言われればそれまでなんですけどね・・・。
 

 

kernelを準備する

minirootfsにはカーネルが無いので、適当なバージョンのソースコードを引っ張ってきて、ビルドしましょう。僕はモジュールとかを適切にrootfsに展開するのが面倒になり、全てkernelにstatic buildしてしまいました。

ここで得られたbzImageをqemuのオプションで渡します。

 

qemuでどうやって起動するか

qemuからはこんな感じで起動しようと思います。

 

$ qemu-system-x86_64 -kernel bzImage -initrd rootfs -append "root=/dev/ram rdinit=/bin/sh console=ttyS0,115200" -net nic -net user,hostfwd=tcp::2222-:22 -serial stdio -display none

 

-kernelオプションで先程のbzImageを指定、initramfsとしてrootfs(minirootfs)を指定、出力はシリアルポート、initramfsで起動したまま、シェルを立ち上げる(カーネルパラメータオプションの rdinit=/bin/sh)、みたいなのを想定しています。

 

initramfsをrootとしてしまうのは完全に手抜き(別にinitramfsでの起動でも僕が本当にやりたい事は達成できるので)です。この上でファイルシステムに何か書き込んでも、当然qemuを終了したら吹き飛びます。まあ気になる人はちゃんとハードディスクイメージにするか、適当にNFSマウントでもすれば良いのではないでしょうか。

 

とりあえずこれをやるために必要な事として、rootfsを準備しなきゃいけません。次からこれを準備していきます。

 

rootfsの準備

initramfsのファイルフォーマットはcpioです。minirootfsはtar.gzで落ちてくるので、展開した後、cpioにしなければいけません。

そして展開しようとすると、デバイスファイルが展開できなくてこけます。

 

なので、fakerootを使いましょう。

 

$ fakeroot && tar xf ../alpine-minirootfs-3.8.0-x86_64.tar.gz && 

find | cpio --quiet -o -H newc | gzip -9 > ../rootfs

tarでカレントディレクトリにminirootfsを展開した後、cpioコマンドで固め、gzip圧縮しています。

 

確かこれだけで/bin/shは起動するはず

 

initを起動したい

シェルが立っただけだと、何もサービスが使えないので、使い物にならないです。わかりやすい例で言えばネットワークとか。

というわけで、initスクリプトを起動しましょう。

 

$ qemu-system-x86_64 -kernel bzImage -initrd rootfs -append "root=/dev/ram rdinit=/sbin/init console=ttyS0,115200" -net nic -net user,hostfwd=tcp::2222-:22 -serial stdio -display none

rdinit=/sbin/initにすると、initスクリプトが起動します。

 

これで起動すると、以下のようなエラーが出てしまいます。シェルすら出ません。辛い。

can't open /dev/tty1: No such file or directory

 

/dev/tty1というデバイスファイルがない、というエラーですね。

minirootfs内の/etc/inittabには以下のような行があり、ここで/dev/tty1を参照していますが、その時までに/dev/tty1が作成されていないため、このようなエラーが出るわけです。

tty1::respawn:/sbin/getty 38400 tty1

 

バイスファイルを準備したい


/dev/tty1を生やす簡単な方法を探すと、こんなのが出てきます。

 

kernhack.hatenablog.com

 

initramfsのinitと同様に、以下のようなコマンドを実行すればデバイスファイルやその他諸々が整備されるはずです。

mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mount -t tmpfs run /run -o nosuid,nodev,mode=0755


なので例えばこんな感じの事を/etc/inittabの最初でやれば、解決します。

 

::sysinit:mount -t proc proc /proc -o nosuid,noexec,nodev

 

最初はこれで解決していたのですが、途中でもっと綺麗なやり方がある事に気づく事になります。ひとまずこれで解決した、という体で話を進めさせてください。

 

そもそも/etc/inittabの編集ってどうやれば良いんだ!という方もいらっしゃるかもしませんが、それはtarで展開した後、/etc/inittabで編集し、cpioに食わせれば良いだけですよ。

 

シリアルでログイン画面が出てこない。

これだけだと、エラーは出なくなりますが、まだログイン画面には到達できません。/etc/inittabでは以下の行がコメントアウトされているためです。

 

ttyS0::respawn:/sbin/getty -L ttyS0 115200 vt100

 

まあそりゃ今はシリアルで画面出力しているから、シリアルコンソールに対応するデバイスファイルを設定しなきゃ駄目ですね。

 

ログインしたい

ログイン画面出ただけではどうしようもないので、ユーザー作りましょう。

 

cpioに固める前に、アカウント情報をファイルシステム上に乗っけます。(cpioに固めた後はファイルシステムに手出しできないので。ログインもできてないですし)

minirootfsの展開先にchrootし、minirootfs上のadduserコマンドでユーザーを作れば、minirootfs上にアカウント情報が作成されます。

$ chroot . addgroup alpine

$ chroot . adduser -S -s /bin/sh -G alpine alpine

$ chroot . addgroup alpine wheel

$ chroot . sh -c "echo '%wheel ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers"

 

alpineユーザーを作り、ついでにwheelにも追加します。wheelはパスワード無しでsudoできるようにします。

 

sudoするためにはminirootfs上にsudoコマンドが無いといけないので、sudoをapkでインストールします。僕はsshサーバーを立てたかったので、ついでにdropbearと、dropbearを自動起動するためにopenrcも入れました。

 

$ chroot . apk add --no-cache --initdb sudo dropbear openrc

 

これだけで終わりと言いたい所ですが、手元だとpermissionエラーでログインできませんでした。minirootfsを展開後に、/(minirootfsのroot)を755に設定する必要があるようです。

 

/etc/network/interfacesの設定

これで無事ログインできたのですが、"rc-service dropbear start"ってしてdropbearを起動しようとすると、「/etc/network/interfacesがナイヨ」と怒られます。

 

というわけで、/etc/network/interfacesを整備しましょう。

 

auto lo

iface lo inet loopback

 

auto eth0

     iface eth0 inet dhcp

こうすると、ifconfigでloとeth0が見えるようになります。でも相変わらず通信できません。ていうか、それ以前にdropbearを起動する事でnetworkingが起動するってどういう事よ、最初からnetworkingは起動しててよ、みたいな気になりますよね。

 

openrcの設定

なので、openrcがinitを自動起動してくれるようにします。

$ chroot . rc-update add dropbear default

$ chroot . rc-update add networking boot

これが上手く動けば良いんですが、後者はなぜかSEGVして動きませんでした。コードを読むと、bootの時だけ何か特殊な処理が走るっぽいんですよね。その中でコケてるらしい。もしかしたらdocker環境上で実行したのが悪かったのかもしれません。

 

で、コードいわく、こいつはシンボリックリンクを貼る以上の事をやってなさそうなので、自分でやります。こんな感じ。

 

$ chroot . ln -s /etc/init.d/networking /etc/runlevels/boot

 

/etc/runlevels/bootは存在しないので、事前に作っておく必要があります。

 

なんかnetworkingとdropbearの設定をするんだったら、ついでに他のサービスも全部立てとこうぜ、みたいな気持ちになったので、alpineのVMイメージの設定と比較して、全部設定する事にしました。

 

$ chroot . install -d /etc/runlevels/boot /etc/runlevels/default /etc/runlevels/sysinit /etc/runlevels/shutdown /etc/runlevels/nonetwork

$ chroot . ln -s /etc/init.d/acpid /etc/runlevels/default

$ chroot . ln -s /etc/init.d/bootmisc /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/crond /etc/runlevels/default

$ chroot . ln -s /etc/init.d/devfs /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/dmesg /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/dropbear /etc/runlevels/default

$ chroot . ln -s /etc/init.d/hostname /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/hwclock /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/hwdrivers /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/killprocs /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/loadkmap /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/mdev /etc/runlevels/sysinit

$ chroot . ln -s /etc/init.d/modules /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/mount-ro /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/networking /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/savecache /etc/runlevels/shutdown

$ chroot . ln -s /etc/init.d/swap /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/sysctl /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/syslog /etc/runlevels/boot

$ chroot . ln -s /etc/init.d/urandom /etc/runlevels/boot

 

※これは最終系のコードで、後の話でapkによってインストールされるサービスも登場しています。

 

これによって、devfsサービスが自動起動するようになり(/etc/inittabの最初の、"::sysinit:/sbin/openrc sysinit"の中で起動される)、先程のmount文は不要になりました。

 

アドレスが割当らない

さて、これでdropbearを手動で起動しなくても良くなりましたし、networkingも最初から起動するようになりました。でも相変わらずssh接続ができません。ネットワークが死んでます。


しかもこれ、何か変なんですよね。ifconfig走らせるとIPv6アドレスは何か割当たってるっぽいし、udhcpc走らせるとちゃんとIPアドレスは振ってくる。でもifconfigにinet addr(IPv4アドレス)は割当らないみたいな。

なんでじゃなんでじゃって言いながら調べてたら、これが見つかり、「ふんふん、busyboxがudhcpcのサンプルスクリプトを提供してるのか、でもminirootfsには無いな」となり、じゃあサンプルスクリプトコピペするか、なんて思いながらググってたらこんなのを見つけました。

 

Alpine Linux packages

 

どうやらbusybox-initscriptsをインストールすると、/usr/share/udhcpc/default.scriptが手に入るらしい!!

 

$ chroot . apk add --no-cache --initdb busybox-initscripts

 

ちゃんとifconfigするとIPv4アドレスも割り当たるようになったし、apkでパッケージもダウンロードできるようになりました。めでたしめでたし。

ubpfで遊んでみる

eBPFくらい簡単に遊べるようになっておかないとまずいよな、という気持ちになったので、遊んだときの記録。

とりあえずユーザーランドで動かしたいんじゃ、って思ったので、ubpfを使ってみる。

 

github.com

 

clang入れるのと、pipで nose pyelftools parcon を入れる必要あり。
で、リポジトリのルートで`make -C vm`ってやるとvmディレクトリ内に`test`っていう実行ファイルができるから、これで遊ぶ。

 

testsの中から引っ張ってきたコード

extern int strcmp_ext(const char *a, const char *b);

 

int entry(int *mem)

{

  char a = "abcx";

  char b = "abcy";

 

  if (strcmp_ext(a, a) != 0) {

    return 1;

  }

 

  if (strcmp_ext(a, b) == 0) {

    return 1;

  }

 

  return 0;

}

 

こいつ(main.c)を

$ clang -O2 -target bpf -c main.c -o main.o

こうして、

$ vm/test -j main.o

こうすると実行できる。0って帰ってきますね。

 

エントリポイントがバイナリの先頭っぽいので、entry()の前に関数置くとそっちが実行されちゃいます。

 

メモリ使うのもやってみてくなった。

3バイトのmemというファイルを用意し、

int entry(char *mem)

{

  return mem[100];

}

こんなん実行したら、range execeptionで落ちるのかなーとか期待。

 

$ vm/test -j -m mem main.o

 

落ちなかった。ubpfではメモリチェックとかはやってないっぽい。

 

おまけ

ubpf環境はDockerで作った。→livadk/ubpf

こんな感じで使うと、環境構築から開放されて、大変幸せになれる。

Mac+dockerでx11アプリケーションを起動する

Mac上で動作するdockerコンテナ内で、GUIアプリケーションを起動したい事って良くあると思うんだけども、動かすまで結構手間取ったので記録に残しておく。

ググるといろいろ出てくるけど、結論としてはこのページ通りにすると上手く行った。

medium.com

 

  1. XQuartzをインストールする。
  2. XQuartzを起動し、環境設定を開き、「セキュリティ」タブの「ネットワーク・クライアントからの接続を許可」にチェックをつける。
  3. 一度XQuartzを再起動する。
  4. シェルで以下のコマンドを実行する。

xhost + 127.0.0.1

docker run -e DISPLAY=docker.for.mac.localhost:0 jess/firefox

 

Firefoxが起動するはず。 
3番が一番手こずった。

 

ubuntuのコンテナでxeyesが起動するかも試したけど、問題なさげ

 

qemu(not kvm)上のゲストOSへのsshが遅くてハゲるので何とかした備忘録

QEMU上でLinux動かす時、kvm有効化しないとめちゃくちゃ遅くないですか?

ゲストOSはUbuntuなんですが、ログインシェル出るまで数分かかるとかありえねぇ!kvm有効化すると10秒掛からずに起動するのに!!

 

・・・みたいな感じで深夜の研究室でブチ切れてたんですが、よく考えたら15年前とかOSの起動って普通に数分かかってましたよね。10年前でも1分近くしたような記憶。

確か、SSDとか出た辺りから起動がどんどん早くなっていったなぁ(しみじみ

 

昔語りしてると老けちゃうので、前置きはこのくらいにして本題へ。

kvm有効化しないqemuが遅いのはまあ仕方ないんですが、ホストからゲストにssh接続するのに10秒掛かるのはありえないだろう、と。ハゲそう

もちろんkvm使えば一瞬で繋がります。でもまあちょっとkvm使いたくない事情があって・・・

 

というわけで、なんとかしてssh接続してシェル出すまでの時間を削減すべく頑張った話。

 

ちなみに環境は以下の通り

  • CPU:Xeon E3-1275 v5 @ 3.60GHz
  • ストレージ:適当に生協で買ったCrucialのSATA SSD
  • メモリ:16GB

 

試したこと

UseDNS no, GSSAPIAuthentication no

鉄板ですね。意味無し。

 

dbusの再起動

ぐぐったら結構でてきた。これも意味なし。

 

公開鍵認証を使わずにパスワード認証にする

意味なし。

ただし、パスワード認証にすると速攻でパスワードを聞かれたので、とりあえずゲストOSがおねんねしているわけではないという事はわかった。

 

UsePrivilegeSeparation no, UseLogin no

後で示すやつを試すついでにもしやと思って試したのだけど、意味なし

 

効果があったもの

ログインシェルをbashからdashにする。

bash重いからねー。これで8秒。

ちなみに計測は人間ストップウォッチです。

 

~/.hushloginを作成する。

ログイン時にずらずら出てくるウェルカムメッセージが明らかに遅いので止める。

これで7秒。

これを作るとmotd周りが全て無効化されるらしい(?)んだけど、個人的には lsb_release -s -d が諸悪の根源であって、こいつを削れば良いんじゃないかとか思ったり。

ubuntu@ubuntu:~$ time lsb_release -s -d

Ubuntu 16.04.4 LTS

real 0m1.155s

user 0m1.056s

sys 0m0.100s

なんでたったこれだけの物を表示するのにこんなに時間が掛かるんだろうね、python3が遅いのかな。

 

で、この状態でも依然7秒。

ssh -vvvの出力を眺めると、

 

debug1: Entering interactive session.

debug1: pledge: network

*** ここで5秒くらい待つ ***

debug3: receive packet: type 80

debug1: client_input_global_request: rtype hostkeys-00@openssh.com want_reply 0

 

みたいな感じ。で、これを短縮したのが次。

 

/etc/ssh/sshd.confの設定で、UsePAM noにする

これで2秒になった。

たぶんPAMとの通信が遅いんだろうね。
PAM使わないと困る事があるのかもしれないけど(よくわかってない)、今の環境ではパット見問題なさそうなので大丈夫。

 

これでssh -vvvしてもサラサラッとログが流れて、すぐログインシェルに到達する。

まあもう少しチマチマした改善はできるのかもしれないけど、体感がそんな変わらなさそうなので、まあ良いかなー、と。

最終的に10秒 →2秒になったので、個人的には満足です。

 

僕の髪の毛は守られた!!!!