2014/07/24

Linux システムコールのブロック・ノンブロックまとめ

はじめに

Linux にはブロックするシステムコールとノンブロックなシステムコールがあります。さて、システムコールが「ブロックする」とはどういうことでしょうか。よく、ブロックするシステムコールとは「処理が完了するまでプロセスの動作が中断され待たされること」という説明を見ますが、より詳細に、どういう処理の場合に待たされるのか、整理してみましょう。

「ブロックする」とは

Linux において、システムコールがブロックするとは、「プロセスが、システムコール呼び出しの延長で待状態(TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE) に遷移し、CPU時間を消費せずにあるイベントが完了するのを待つようになる」、ことを指します。ブロックするシステムコールのうち代表的なものと、完了待ち対象イベントをまとめると、以下のようになります。


システムコール待ち対象イベント
read, write, fsyncディスク I/O
read, write, send, recv, accept, connect, select, poll, epoll FIFO読み書き可能
futex, flock ロック取得
nanosleep タイマ発火

待ち対象イベントをそれぞれ解説します。

ディスクI/O

ディスクをバックエンドとするファイルへの読み書きで、ディスクアクセスが生じる場合にはディスクへのI/O発行が完了するまでプロセスは待ち状態になります。例えば、読み込み対象のファイルがページキャッシュに乗っていない場合や、キャッシュを介さないダイレクトI/Oを発行した場合、fsync(2) でメモリ上の内容をディスクと同期させた場合、です。
なお、細かい話ですが、この場合の待ち状態は TASK_UNINTERRUPTIBLE という、シグナルを受け付けない待ち状態です。ディスクI/O以外の待ち対象イベントでは、TASK_INTERRUPTIBLEで待ちます。

キュー読み書き可能

パイプやソケットなど、キュー(FIFO)の構造を持つファイルを読み書きしようとした時に、キューが空で読み取れるデータがない場合と、キューが満杯でこれ以上書き込めない場合には、読み書きできる状態になるまでプロセスは待ち状態になります。キューに新しくデータが到着すると、キューが読み込み可能になります。キューに空きが出来ると、キューは書き込み可能状態になります。
read(2)、write(2)、select(2) については上記の説明で良いのですが、accept(2) は少し状況が違うので補足して説明します。
accept(2) はクライアントからの接続要求が キューにない場合にプロセスが待状態に遷移します。複数のプロセスが同じファイルに対し accept(2) を発行している時にクライアントからの接続要求があった場合、待状態にあるプロセスたちのうち、一つのプロセスのみが起床され、accept(2) 処理を実行します。一つのプロセスのみが起床されるのは所謂 Thundering Herd 問題への対処です。日本語の解説では、id:naoya さんによるprefork サーバーと thundering herd 問題 が詳しいです。

ロック取得

futex(2)は指定したアドレスに対応するキューでプロセスを待状態にしたり(FUTEX_WAIT)、指定したアドレスに対して待ち状態にあるプロセスを起床する(FUTEX_WAKE)同期機構です。
分かりやすくいうと、pthread_mutex_lock(3) などを通してロックを取得しようとしたが、すでに他のプロセスがロックを取得していた場合に、プロセスはFUTEX_WAIT で待ち状態に遷移します。flock(2) も同様です。

タイマ発火

所謂タイムアウトです。指定時間が経過するとプロセスが起床します。


「ノンブロック」とは

ブロックと対をなす概念:ノンブロックについても触れておきます。
ノンブロックなシステムコールとは、ブロックしないシステムコールのことです。システムコールをノンブロックにするには、対象とするファイルにfcntl(2)  でノンブロッキングフラグ(O_NONBLOCK)を付与します。ノンブロッキングフラグを付与されたファイルに対して、完了待ち対象イベントが「キュー読み書き可能」なシステムコールを発行すると、キューが読み書き可能でない場合、システムコールは即座に失敗し(return -1)、errno に EAGAIN が設定されます。キューが読み書き可能になるのを待ちはしません。
完了待ち対象イベントが「ディスクI/O」なシステムコールについては、ノンブロックには出来ませんが、その代わり非同期I/Oシステムコール io_submit(2) が用意されています。

まとめ

システムコールがどのような場合にブロックするのか、完了待ち対象イベント別に分けて説明しました。待ち対象イベントには大きくわけて「ディスクI/O」「キュー読み書き可能」「ロック取得」「タイマ発火」があります。「キュー読み書き可能」なシステムコールについては、ノンブロッキングフラグを付与することで、ノンブロックにできます。

ブロックするシステムコールは上記ですべてではありません。他にもちょくちょくありますが、細かいので省略します。

参考文献

各システムコール Man Page
Linux Kernel 3.15 ソースコード

2014/05/12

Docker を支える Linux Kernel の機能 (概要編)

はじめに

Docker はコンテナ型仮想化技術を使ってOSレベル仮想化を実現するコンテナ管理ソフトウェアです。類似のコンテナ管理ソフトとしては、Docker の他にも libvirt、 lxc-tools などがありますが、 Docker には以下の大きな特徴があります。
  • Infrastructure as Code の思想に基づき、コンテナをコード(Dockerfile) で管理できる
  • docker index  で、コンテナイメージを手軽に取得、共有できる
Docker は上記のような特徴を持つため、アプリケーションのポータビリティを大きく向上させることができると期待されています。


大変便利な Docker ですが、Docker によるコンテナ管理は、実は数多くの Linux Kernel の機能により実現されています。今回は Docker を支える Linux Kernel の機能についてご紹介します。
調査対象の Docker Version: 0.11

Docker を支える Linux Kernel の機能を、一枚絵にすると、以下のような図になります。



各機能は、大きく分けて、NamespacesCgroupsStorageNetworkingSecurity に大別できます。それぞれについて、概要を簡単に説明します。


Namespaces

ユーザプロセスが動作する空間を分離する Namespace はコンテナ型仮想化を実現する上で、核となる機能です。これらの機能の多くはParallels 社の OpenVZ チームを中心として開発されました。Docker が利用する Namespace には PID、MNT、IPC、UTS、NETの 5種類があります。

PID Namespace(Kernel 2.6.24)

PID(Process ID) Namespace は、プロセスが動作する空間を生成・分離します。あらたに作られたプロセス空間からは、親空間で動作するプロセスが見えなくなります。 "見えなくなる" というのは、kill() システムーコール等、PIDを指定するシステムコールで親空間で動作するプロセスと通信できなくなる、ということです。親空間からは、生成した空間で動作するプロセスは見えます。

MNT Namespace(Kernel 2.4.19)

MNT(Mount) Namespace は、プロセスに見えるファイルシステムのマウント空間を分離する機能です。mount コマンドで見えるファイルシステムのマウント情報を分離します。

IPC Namespace(Kernel 2.6.30)

IPC(Inter-Process Communication) Namespace は System V IPC(メッセージ・キュー、 セマフォ、共有メモリ) と Posix メッセージキュー の空間を分離します。IPC 関連の識別子が、他の空間からは見えなくなります。

UTS Namespace(Kernel 2.6.19)

UTS(Unix Time-sharing System) Namespace は、uname() システムコールで取得できる情報を分離します。Namespace のうち、最も単純で理解しやすいです。

NET Namespace(Kernel 2.6.29)

NET(Network) Namespace は、ネットワークデバイス、IPアドレス、ルーティングテーブル、iptables 情報を分離します。

Docker は、これらの機能により、プロセス間に仕切りを設け、空間を分離することでコンテナを構成します。
上記以外にも、ユーザおよびグループを分離する User Namespace(Kernel 3.8)という機能もあります。将来的に Docker が利用するかもしれません。


Cgroups

Cgroups はプロセス群に割り当てる計算資源(CPU、メモリ、I/O帯域)を管理する機能です。コンテナに割り当てる資源を調整するために使用します。

cpu

指定した通常優先度(Nice -19 ~ 20) プロセスが使用するCPUの利用割合です。

cpuset

指定したプロセスが動作するCPUを制限します。

memory

指定したプロセスが使用するメモリ量を制限します。

device

指定したプロセスが使用できるデバイス(/dev/*)を制限します。

Docker はこれらの機能を使い、コンテナに割り当てる計算資源を制限します。
上記以外にも ブロックI/O 帯域を制限する blkio subsystem や、プロセス群を停止する freeze subsystem 等があります。将来的には Docker でも使われるでしょう。

Storage

Docker は CoW(Copy on Write) と呼ばれる方式でコンテナイメージ間の差分を扱うことで、無駄なくコンテナイメージを管理します。Docker のコンテナイメージを管理する Storage プラグインでは、以下の Kernel の機能を使っています。

Device Mapper

Device Mapper は、ファイルシステム等が発行するブロック I/O とデバイスのマッピング関係を管理します。Docker は Device Mapper の提供する thin-provisioning の機能と、snapshot 機能を活用しています。個別のファイルシステムに依存しないため、幅広い環境で使用することができます。ファイルシステムより下層にあり、ファイル差分を管理できないため、docker diff コマンドの実行速度は Btrfs と比べ遅くなります。

Btrfs

Btrfs は Linux Kernel に取り込まれているファイルシステムの一つで、先進的な機能を持ちます。Docker は Btrfs の subvolume / snapshot 機能を使い、コンテナイメージの差分を管理します。差分はファイルシステム層で管理されるため、docker diff コマンドの実行速度はとても速いです。使用するには、docker のホームディレクトリが btrfs 形式でないといけません。現在 Docker の Btrfs Plugin は、200行ちょっとなのでとても簡単に読めます。

Aufs

Aufs は union ファイルシステムの一種で、ファイルシステム層で差分を管理できる機能を持ちますが、Linux Kernel のメインラインに入っていないため、今後はあまり使われないでしょう。

Networking

veth

仮想的なネットワークデバイスのペアを作る機能です。Network Namespace と組み合わせ、ホストとコンテナ間での通信に使います。これも OpenVZ チームが中心に開発したものです。

bridge

仮想的なブリッジを作る機能です。上述の veth と組み合わせ、コンテナ間の通信に使います。QEMU /KVM でおなじみと思います。

iptables

コンテナ間の通信を制御(Drop/Accept)するために使います。



Security

Capability

プロセスが持つ特権を細かい粒度で管理する機能です。コンテナ内から、ホストに悪影響を及ぼさないよう制御します。Docker では、例えば、カーネルモジュールのロード、OS時刻の変更などができないよう、デフォルト設定でコンテナ内プロセスの特権を落としています。デフォルト設定:default_template.go

SElinux

強制アクセス制御機能です。Docker ではコンテナ起動時に SElinux MCS ラベルをコンテナに付与し、コンテナ内プロセスの動作をコンテナ内に制限します。

seccomp

プロセスが発行できるシステムコールの種類を制限する機能です。Docker では、--lxc-conf でシステムコールリストファイルを指定してコンテナ内プロセスのシステムコール発行を制限できます。


まとめ

Docker は数多くの Linux Kernel の機能により実現されていることがわかりました。今後、Cgroups、User Namespace、Checkpoint/Restart In Userspace などの実装が進むと思われます。

参考文献

[LWN.net] Namespaces in operation, part 1: namespaces overview
[LWN.net]  LSS Secure Linux container
Linux コンテナ入門


2014/05/08

Docker のビルド方法に見る Golang の利点

以前、Docker をビルドしていて、以下の事実に気づきました。

事実: Docker は自身をビルドするのに Docker を用いてコンテナ内でビルドしている


実際、ソースコード直下に、以下の Dockerfile が置いてあります。中を参照すると、ubuntu のコンテナイメージをベースに、依存するソフトウェアを apt-get したり、git clone で取得したりしています。make コマンドで、依存するソフトウェアをインストールしたコンテナ内で、hack/make.sh を実行し、バイナリを作成します。生成したバイナリをコンテナから取り出してビルド終了となります。

Docker は Golang で書かれていますが、その理由の一つに、Golang の優れたポータビリティ (libc が入っている環境であればどこでも動作するバイナリを手軽に生成できること)があります。これにより、ビルドは Docker 内の固定した環境で行い、生成したバイナリだけを取得しインストールを済ませることができるのです。
参考:Docker and Go: why did we decide to write Docker in Go?

Docker を使ったビルドの利点は、Docker さえ動く環境であれば、どこでもビルドできるため、ビルド環境の構築に手こずらなくて済むことにあります。
誰しも、ソフトウェアのビルド・インストール作業で以下のような苦い経験があるのでは無いでしょうか。

  1. あるソフトウェアの最新のソースコードを取得してくる
  2. ./configure がエラーを吐くので、エラーメッセージを参照し、依存するソフトウェアを yum install する
  3. まだ ./configure がエラーを吐くので、yum-builddep で依存するソフトウェアをまとめてインストールする
  4. まだ ./configure がエラーを吐く。yum-builddep でインストールしたソフトウェア X のバージョンが古いらしい。
  5. ソフトウェア X の最新バージョンをインストールすべく、1. に戻る
Docker 内ビルドが広まると、上記のような作業で手こずることは稀になることでしょう。

Docker 内ビルドの欠点としては、Golang のようにポータビリティに優れた言語を採用しないといけないこと、と、実行バイナリが大きくなってしまうことがあります。

まとめ
  • Docker は自身をビルドするのに Docker を用いている
  • Docker が Golang で書かれている理由に、Golang の優れたポータビリティがある
  • Docker 内ビルドでビルド・インストールにかかる手間が大幅に削減出来る 
余談

2014/02/26

Erlang VM(BEAM) スレッド構成

Erlang の実行環境である BEAM の動作を理解するため、BEAM のスレッド構成を調査しました。

BEAM は SMP(マルチコア) 環境と非 SMP 環境では動作が大きくことなります。SMP環境と非SMP環境に分けてスレッド構成を記載します。
調査対象の OTP のバージョンは R16B03-1です。

非SMP環境

Erlang Interactive Shell を起動する際に、オプションとして '-smp disable' を付与すると、CPUはSMPでも、BEAMとしては非SMPモードで起動できます。
'erl -smp disable' で起動すると、11個のスレッドが見つかりました。11スレッドの内訳は以下のようになります。
スレッド名関数名個数
Main Threadprocess_main1
Async Threadasync_main10

Main Thread
 BEAM byte-code を解釈し、実行するスレッドです。Erlang プロセスをスケジュールします。

Async Thread
 Erlang プロセスによるファイル操作を非同期に行います。プロセスが file モジュールを通じてファイルの読み書きや開閉を行うと、Main Thread に代わってAsync Threadがそれらの処理を請け負います。byte-code を解釈実行する Main Thread の動作を止めないために、処理を肩代わりしているのです。スレッドの起床は futex()システムコールでおこないます。
 Async Thread の個数は erl 起動時に '+A' オプションで変更できます。例えば、'erl +A 5' とすると、Async Thread は5個になります。ちなみに riak はデフォルトで64個起動します。

SMP環境

 SMP環境では些か構成が複雑になります。オプションなしで erl を起動すると、論理4core(物理2core)環境では19スレッドできました。内訳は以下になります。

スレッド名関数名個数
Main Threaderts_sys_main_thread1
Signal Handling Threadsignal_dispatcher_thread_func1
System Message Handling Threadsys_msg_dispatcher_func1
Async Threadasync_main10
Child Waiting Threadchild_waiter1
Scheduling Threadsched_thread_func4
Aux Threadaux_thread1

Main Thread
 非SMP環境とは異なり、Erlang プロセスの実行はしません。単にシグナルを受信して、pipe経由でSignal Handling Thread に通知するだけのスレッドです。select(0, NULL, NULL, NULL, NULL) で待ちぼうけです。

Signal Handling Thread
 シグナルハンドラ本体です。Main Threadが受信したシグナルに相当するハンドラを起動します。erl 起動時に '+B' オプションでシグナル受信時の挙動を変更できます。例えば 'erl +B i' でブレークシグナルを無視するようになります。

System Message Handling Thread
 システムメッセージのハンドラです。システムメッセージは、トレース情報の出力やプロセスの再開・中断等をリクエストする特殊なメッセージです。詳しくは sys module のドキュメントを参照ください。

Async Thread
 非SMP環境と同様の非同期I/Oスレッドです。

Child Waiting Thread
 「OTP-3906 : Solaris で子スレッドが大量に終了した際、 SIGCHLD がうまく伝わらない問題」を修正するため、子スレッドの終了を waitpid() で待ち受けます。

Scheduling Thread
 process_main() を実行し、 byte-code 解釈実行、プロセススケジューリングを行います。デフォルトでは論理コアと同じ数だけ生成されます。'+S' オプションでスレッド数を調整できます。他の Scheduling Thread と比較して負荷が偏らないようにバランシングとプロセスマイグレーションも行います。

Aux Thread
 若干時間のかかる処理を受け持つ補助的なスレッドです。メモリアロケーションやGCの情報を取得する際等に、Scheduling Thread から処理をオフロードされます。例えば、'elrang:statistics(garbage_collection)' でのGC統計情報取得は、aux_threadで行われます。

通常 erl 起動時に作られるスレッドは以上ですが、他にも、NIF や driver 関係のスレッドがあります。

まとめ

 ・Erlang VM(BEAM)のスレッド構成はSMP/非SMPで大きく異なる
 ・Scheduling Thread の動作を阻害しないために、一部処理が他スレッドにオフロードされる

参考文献

Erlang User's Guide: erl
Erlang/OTP のソースコード (otp/erts)

2014/01/16

Erlang/OTP crypto モジュールエラー on Fedora 19

Fedora 19 環境で自分でインストールした Erlang/OTP を使って、rebar を利用したところ、以下のようなエラーが出ました。

Uncaught error in rebar_core: {'EXIT',
                              {undef,
                                  [{crypto,start,[]},
                                   {rebar_core,run,1},
                                   {rebar,main,1},
                                   {escript,run,2},
                                   {escript,start,1},
                                   {init,start_it,1},
                                   {init,start_em,1}]}}

どうやら、crypto モジュールをstart()した時に問題が発生したようです。
調べてみると、bugzilla にエントリが在りました。

Bug 1023017 - Restore ECC support in Erlang's crypto library

さらに詳しく調べると、ruby でも問題になっていました。

backport r41808(openssl build issue on fedora)

原因は、OpenSSL で OPENSSL_NO_EC が define されていないにも関わらず、OPENSSL_NO_EC2M が define されている環境においてビルドすると、定義されていないシンボル EC_GROUP_new_curve_GF2m (ガロア体GF(2m)上の楕円曲線暗号を扱う関数)を利用してしまうことにあるようです。
解決策として、Erlang/OTP の lib/crypto/c_src/crypto.c の EC_GROUP_new_curve_GF2m を利用している if 節を "ifndef OPENSSL_NO_EC2M" でくくってやれば問題ないことを確認しました。
さて、パッチを送るかと思っていると、既に今より16日前に解決済みであることを知りました。

対策パッチ:
crypto: selective support for GF2m curves

関連Pull Request:
more EC curves

上記のパッチを当てれば、問題は解決されます。

2014/01/08

BEAM(Erlang VM) 参考資料まとめ

はじめに

Erlang/OTP で開発したアプリケーションは、通常 BEAM (Erlang VM)と呼ばれる仮想マシン上で動作させます。BEAMに関する資料は、今のところ世の中にあまり多くないようです。BEAMの情報が得やすくなるよう、ここにまとめておきます。

BEAM(Erlang VM) 参考資料

Hitchhiker’s Tour of the BEAM

Erlang Solutions Ltd. の Robert Virding 氏による BEAM の概要解説。Scheduler, Memory 管理, GC, Async Threads について小気味よくまとまっています。

The evolution of the Erlang VM

同氏による Erlang VM の歴史解説。Erlang VM ごく初期のProlog Interpreter や JAM(Joe's Abstract Machine) からBEAMに至るまでの経緯が書かれています。

Erlang Engine Tuning, Know Your Engine – Part II: the BEAM

ERTS本執筆中のErik (Happi) Stenman氏による BEAM解説。Erlang からコンパイルされたBEAM コードがどう解釈されて動作するのか説明されています。

Inside the Erlang VM

Ericsson の Ludin 氏によるErlang VM のについての解説。 主に Scheduler の実装と SMP 対応について書かれています。

How Erlang does scheduling

Erlang VM の Scheduling 方法についての平易な解説。プロセス優先度設定や、プロセスコンテキストスイッチの契機、他の言語(実行環境)との差異について書かれています。