LXC(Linux Container) で lxc-attach というコマンドが提供されています。シェルを介すると扱いにくい場面があるので rubyから直接扱えるようにC拡張を書いてみました。
....のですが 1.8系だと動作し 1.9系だと動かなかったので原因を追ってみました。rubyの処理系の仕様とLXC, カーネルの実装を調べる事で原因を掴めました。
LXCの実装は 新しめのカーネルでのみ使用できるシステムコールや機能が種々含まれており、調査した際に得る物が大きかったです。のでログを整形して公開します。お勉強におつきあいください。
おことわり
初めに結論を書く
- lxc-attach は内部で setns(2) 呼び出している
- setns(2) でマウント名前空間を変更しようとする時、スレッドが複数生えてるプロセスの場合は カーネルがEINVAL を返す
- ruby1.9.* は起動時からネイティブスレッド2本生えてる。よって setns(2) でコケる
- ruby1.8.* は setns(2) 通る
(検証したカーネルでは) マルチスレッドなプロセスは setns(2) でマウント名前空間を切り替えられない、よって ネイティブスレッドが起動時から複数存在する ruby1.9ではsetns(2) を呼び出す lxc_attach が機能しない、が簡約したまとめとなります
検証した環境
- Ubuntu 11.10 oneiric
- カーネル 2.6.38.7,
- http://lxc.sourceforge.net/patches/linux/2.6.38/ のパッチを当ててリビルド
- LXCのライブラリもリビルド、 /usr/local/lxc にインストールしてある
- LXCの環境は適宜作成。apt-get install lxc とか。
問題の lxc-attachをバインディンするC拡張のソース
引数の確認などサボってます。rubyistに gemsを投げつけられて死ぬ
- lxc.c
#include "ruby.h" #include "lxc/namespace.h" static VALUE lxc_lxc_attach(VALUE, VALUE); void Init_lxc(void){ VALUE lxc; lxc = rb_define_class("Lxc", rb_cObject); rb_define_method(lxc, "attach", lxc_lxc_attach, 1); } static VALUE lxc_lxc_attach(VALUE self, VALUE name) { char *lxc_name = StringValuePtr(name); pid_t pid; int ret; pid = get_init_pid(lxc_name); if(pid == -1) { rb_raise(rb_eRuntimeError, "container '%s' is not running", lxc_name); } ret = lxc_attach(pid); if(ret == -1) { rb_raise(rb_eRuntimeError, "failed to lxc_attach (pid %d)", pid); } return self; }
コンテナの名前から pid を引き、lxc_attch(pid) でコンテナにアタッチするコードとなります
- extconf.rb
require 'mkmf' dir_config('lxc') if have_header('lxc/namespace.h') and have_library('lxc') and have_func("lxc_attach") $libs = append_library($libs, "lxc") create_makefile("lxc") else raise "lxc_attach is not defined" end
ビルドする時は ↓のような手順で。カレントディレクトリに lxc.so が生成されます
$ ruby -v ruby -vruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-linux] $ ls -1 extconf.rb lxc.c $ ruby extconf.rb --with-lxc-header=/usr/local/lxc/include/ --with-lxc-lib=/usr/local/lxc/lib/ $ make
拡張は初めて書きました。よりよい実装方法あればご指摘いただければ幸いです
拡張を実行してテスト
ビルドした拡張を実行してみます。実際に LXCにアタッチして、hostname を取得するコードを実行してみましょう
- テスト用にコンテナを起動しておく
$ sudo /usr/local/lxc/bin/lxc-execute -n test -f /usr/share/doc/lxc/examples/lxc-empty-netns.conf /bin/bash
ターミナルで別セッション開いて、コンテナにlxc-attachするコードを実行します
- ruby 1.9.3p-0, 1.9.2p290 いずれもコケる
# rbenvで切り替えて実行 $ ruby -r./lxc -e 'Lxc.new.attach("test"); system("hostname");' lxc: Invalid argument - failed to set namespace 'mnt'-e:1:in `attach': failed to lxc_attach (pid 2234) (RuntimeError) from -e:1:in `<main>'
$ sudo ruby1.8 -r./lxc -e 'Lxc.new.attach("test"); system("hostname");' omega
1.8.7 でのみ通りました。この段階で処理系に依存する問題であることが切り分けできました
カーネルのソースまで追って調査したログ
後からまとめた物なので、キレイに整形してあります。実際は もっと泥臭い調べ方してます..
- ruby1.9.* の strace を取ると、syscall_303 が errno 22 返しています
open("/proc/2234/ns/pid", O_RDONLY) = 5 open("/proc/2234/ns/mnt", O_RDONLY) = 6 open("/proc/2234/ns/net", O_RDONLY) = 7 open("/proc/2234/ns/ipc", O_RDONLY) = 8 open("/proc/2234/ns/uts", O_RDONLY) = 9 syscall_303(0, 0x5, 0xffffffffffffffff, 0x7f93caa54a5b, 0, 0x11, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0 x1) = 0 close(5) = 0 syscall_303(0, 0x6, 0xffffffffffffffff, 0, 0x11, 0, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1, 0x1) = -1 (err no 22) write(2, "lxc: ", 5lxc: ) = 5 write(2, "Invalid argument - failed to set"..., 48Invalid argument - failed to set namespace 'mnt') = 48 write(2, "\n", 1 ) = 1
- syscall_303 は setns(2) のこと
- LXCのパッチを当てて追加したシステムコールなので、strace か libc? が対応してなくて名前逆引きできてない
- errno 22 は EINVAL として定義されている
/usr/include/asm-generic/errno-base.h:#define EINVAL 22 /* Invalid argument */
- setns(2) で EINVALを返すコードを探します。 LXCのパッチ 0014-ns-proc-Add-support-for-the-mount-namespace.patch に含まれています。一部抜粋します
+static int mntns_install(struct nsproxy *nsproxy, void *ns) +{ + struct fs_struct *fs = current->fs; + struct mnt_namespace *mnt_ns = ns; + struct path root; + + if (fs->users != 1) + return -EINVAL; +
- fs->users の正確な意味が分からないんだけど...
- fs_struct の参照カウンタとして使われている
- LWP(スレッド)が生えると +1 されてる様子
- fs->users が 1 でない場合は EINVAL
実行したプロセスがマルチスレッドだと動かないってことになる
- ruby1.9.* で スレッドの数見てみたら 最初から2つ生えてる
$ ruby -v ruby 1.9.3p0 (2011-10-30 revision 33570) [x86_64-linux] $ ruby -e 'system "ps -opid,tid,cmd -L | grep ruby"' 25532 25532 ruby -e system "ps -opid,tid,cmd -L | grep ruby" 25532 25558 ruby -e system "ps -opid,tid,cmd -L | grep ruby" $ ruby -e 'puts File.read("/proc/self/status").split("\n").grep(/Thread/)' Threads: 2
ここまで来て、rubyの処理系の仕様を確認する