ruby1.9.* で lxc-attach のバインディング書いてみたら動かなかったので カーネルまで追って調べたログ

LXC(Linux Container) で lxc-attach というコマンドが提供されています。シェルを介すると扱いにくい場面があるので rubyから直接扱えるようにC拡張を書いてみました。

....のですが 1.8系だと動作し 1.9系だと動かなかったので原因を追ってみました。rubyの処理系の仕様とLXC, カーネルの実装を調べる事で原因を掴めました。

LXCの実装は 新しめのカーネルでのみ使用できるシステムコールや機能が種々含まれており、調査した際に得る物が大きかったです。のでログを整形して公開します。お勉強におつきあいください。


おことわり

  • LXCうんぬんについての前置きははしょって書きます。ご了承ください。
  • メインラインに取り込まれてないパッチを当てたLinuxカーネルでの検証です
    • lxc-attach もそのパッチあてることで動く
  • 今後のカーネルのリリースで挙動が変わる可能性も考えられます。

初めに結論を書く

  • lxc-attach は内部で setns(2) 呼び出している
  • setns(2) でマウント名前空間を変更しようとする時、スレッドが複数生えてるプロセスの場合は カーネルがEINVAL を返す
  • ruby1.9.* は起動時からネイティブスレッド2本生えてる。よって setns(2) でコケる
  • ruby1.8.* は setns(2) 通る

(検証したカーネルでは) マルチスレッドなプロセスは setns(2) でマウント名前空間を切り替えられない、よって ネイティブスレッドが起動時から複数存在する ruby1.9ではsetns(2) を呼び出す lxc_attach が機能しない、が簡約したまとめとなります

検証した環境

問題の 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_303errno 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の処理系の仕様を確認する

まとめ

  • カーネルのソースまで追わないと分からない挙動だった
  • ruby1.8 のグリーンスレッド、ruby1.9 のネイティブスレッドに関する理解があやふやだった
  • エッジな機能使う時は自己責任乙。