d.sunnyone.org
sunnyone.org

ページ

2014-07-06

VyOS と L2TPv3 でハイパーバイザ内部ネットワーク同士をブリッジ接続する

前回ようやく仮想マシン(VM)達を移設するためのPCの準備ができたのだけど、移設する元のPCには、内部のみのネットワーク(VirtualBoxで言うところのHost-Only Network)があり、そこにいくつかVMがぶら下がっているので、これらも移行しないといけない。

一気に移せば何の問題もないのだけど、徐々に移そうとすると、内部のみのネットワークなので、旧ハイパーバイザに残ったVMと新ハイパーバイザに移し終わったVMで通信ができなくなってしまう。

どうしたらいけるかなと考えた結果、「イーサネットフレームを中継するVMを用意すればよくね?」ということで、それを可能にするL2TPv3と、L2TPv3を簡単に使えそうなVyOS (Vyattaのfork)を使って試してみた。

今回の話を図にすると、以下のとおり。

環境 / 利用ソフトウェア

  • 仮想マシンハイパーバイザ 2台。
  • VyOS v1.1 Helium (開発版なので注意:今回はVyOS-virt-livecd-1407022200-7e8544a-i386.isoを利用)

設定方法

VyOS インストール

まず、それぞれのハイパーバイザにVyOS をインストールする必要があるが、これはISOイメージでブートしてvyos / vyosでログイン、install imageと打てば指示に従うだけでOK。

こちらのブログが絵が張ってあってわかりやすそうなので参考に→『Vyatta』からフォークした『VyOS』をインストールしてみた | 俺的備忘録 〜なんかいろいろ〜

あとはホストとしての設定をする必要があるが、とりあえずこんな感じだけやればSSHでログインできる(ブリッジするだけのホストとして考えているので、インターネットに出していくのは想定していない)。
$ configure
# set interfaces ethernet ethX address '192.168.XXX.YYY/24'  (あとで出てくるので値は省略)
# set system host-name vybridge1    (あるいはvybridge2)
# set service ssh port 22
# commit
# save
以下、saveは省略するが、適宜保存する。

vybridge1の設定

まずeth0インタフェースにIPアドレスを振る。

$ configure
# set interfaces ethernet eth0 address '192.168.100.241/24'

次に「l2tpeth0」インタフェースを構成する。トンネルID/セッションIDは好きに決めてOK.
# set interfaces l2tpv3 l2tpeth0 
# set interfaces l2tpv3 l2tpeth0 local-ip 192.168.100.241
# set interfaces l2tpv3 l2tpeth0 remote-ip 192.168.100.242
# set interfaces l2tpv3 l2tpeth0 tunnel-id 41
# set interfaces l2tpv3 l2tpeth0 peer-tunnel-id 42
# set interfaces l2tpv3 l2tpeth0 session-id 1
# set interfaces l2tpv3 l2tpeth0 peer-session-id 2
# set interfaces l2tpv3 l2tpeth0 source-port 5000
# set interfaces l2tpv3 l2tpeth0 destination-port 5001

いったん適用して様子を見る。
# commit
# exit
$ show interfaces l2tpv3 detail
l2tpeth0:  mtu 1488 qdisc pfifo_fast state UNKNOWN group default qlen 1000
    link/ether XX:XX:XX:XX:XX:XX brd ff:ff:ff:ff:ff:ff
    inet6 XXXX::XXXX:XXXX:XXXX:XXXX/64 scope link
       valid_lft forever preferred_lft forever

    RX:  bytes    packets     errors    dropped    overrun      mcast
             0          0          0          0          0          0
    TX:  bytes    packets     errors    dropped    carrier collisions
           738          7          0          0          0          0
l2tpeth0ができていることを確認する。

br0を構成し、eth1とブリッジする。
$ configure
# set interfaces bridge br0
# set interfaces ethernet eth1 bridge-group bridge br0
# set interfaces l2tpv3 l2tpeth0 bridge-group bridge br0
# commit
# exit
$ show interfaces
Codes: S - State, L - Link, u - Up, D - Down, A - Admin Down
Interface        IP Address                        S/L  Description
---------        ----------                        ---  -----------
br0              -                                 u/u
eth0             192.168.100.241/24                u/u
eth1             -                                 u/u
l2tpeth0         -                                 u/u
lo               127.0.0.1/8                       u/u
                 ::1/128
$ show bridge br0
bridge name     bridge id               STP enabled     interfaces
br0             0000.xxxxxxxxxxxx       no              eth1
                                                        l2tpeth0
br0ができていて、eth1とl2tpeth0が入っていることがわかる。
なお、eth1にはIPを振っていない。

これで、vybridge1の準備は終了。

vybridge2の設定

vybridge2の設定もほぼ同様。

$ configure
# set interfaces ethernet eth0 address '192.168.100.242/24'

vybridge2の「l2tpeth0」インタフェースには、基本的にはvyatta1と対照にする。
# set interfaces l2tpv3 l2tpeth0 
# set interfaces l2tpv3 l2tpeth0 local-ip 192.168.100.242
# set interfaces l2tpv3 l2tpeth0 remote-ip 192.168.100.241
# set interfaces l2tpv3 l2tpeth0 tunnel-id 42
# set interfaces l2tpv3 l2tpeth0 peer-tunnel-id 41
# set interfaces l2tpv3 l2tpeth0 session-id 2
# set interfaces l2tpv3 l2tpeth0 peer-session-id 1
# set interfaces l2tpv3 l2tpeth0 source-port 5001
# set interfaces l2tpv3 l2tpeth0 destination-port 5000

br0を構成し、eth1とブリッジする。
$ configure
# set interfaces bridge br0
# set interfaces ethernet eth1 bridge-group bridge br0
# set interfaces l2tpv3 l2tpeth0 bridge-group bridge br0
# commit

これでおしまい。あとは、新ハイパーバイザのVMから、旧ハイパーバイザのVMに通信してみて通信できればOK。

備考

実際のところ、今回の要件では一気に移行すればいいだけだったので、こんなことは必要ない。必要な場合であっても、Production環境でこのような使い方をするのであれば、L2VPNはオーバーヘッドが大きいのでパフォーマンスの検証をすべきと思う。(そもそも今のところVyOSの開発版についている機能のようなので、安定版では使えないのだけど)

なお、今回の構成では暗号化はされないので、もし必要ならIPSecの上に乗せるなど、別途考える必要がある。

しかしながら、かなりお手軽にL2ブリッジを構成できるので覚えておいて損はないと思う。

参考



2014-07-03

KVMの仮想マシンをWebブラウザから管理する

自分用にLinux KVM (Kernel-based Virtual Machine) で検証用他の仮想マシン達を立てている。その仮想マシンの管理に、今まではvirshコマンドと仮想マシンマネージャー (virt-manager)を使っていたが、各VMの画面が見たいとなると、VNCでXにログインして... という感じで煩わしかった。そこで、ハイパーバイザとなるPCのリプレースを契機に、Web管理ツールのWebVirtMgrを使って、Webブラウザから管理できるようにしてみた。

こんな感じの画面がWebブラウザから使えるようになる。


環境/前提

  • Ubuntu 14.04 Server
  • libvirtでKVMの仮想マシンを起動できる状態
    • (BIOS設定他+apt-get install qemu-kvm libvirt-bin的な状態)

Web側の設定


WebVirtMgrはPython + Djangoで書かれたWebアプリケーションなので、それが動くようにする。基本的には、https://github.com/retspen/webvirtmgr/wiki/Install-WebVirtMgr に書かれている手順の通り。このページには、RedHat/CentOS/Debianなんかのときの方法も書いてある。

Python他のインストール

apt-getで入れる。

$ sudo apt-get install git python-pip python-libvirt python-libxml2 novnc supervisor nginx 

アプリケーションのダウンロードと設定

gitで落としてきて、いくつか設定する。
$ git clone git://github.com/retspen/webvirtmgr.git
$ cd webvirtmgr

ここで本来の手順は「sudo pip install -r requirements.txt」なのだけど、パッケージ管理されないファイルが増えてしまう。requirements.txtを見ると、書いてあるライブラリがUbuntu 14.04のバージョン的にいけそうだったのでパッケージで入れることにした。
$ sudo apt-get install python-django gunicorn python-lockfile

スクリプトで初期設定をする。
$ ./manage.py syncdb
You just installed Django's auth system, which means you don't have any superusers defined.
Would you like to create one now? (yes/no): yes
Username (leave blank to use 'ユーザ名'): 
Email address: user@example.org
Password: パスワード
Password (again): パスワード
Superuser created successfully.
$ ./manage.py collectstatic

ディレクトリをまるっと移動する。
$ sudo mkdir /var/www
$ cd ..
$ sudo mv webvirtmgr /var/www/
$ sudo chown -R www-data:www-data /var/www/webvirtmgr

バックグラウンド起動の設定

バックグラウンドで起動するようにsupervisorを設定する。
/etc/supervisor/conf.d/webvirtmgr.confに以下の内容を書く。
[program:webvirtmgr]
command=/usr/bin/python /var/www/webvirtmgr/manage.py run_gunicorn -c /var/www/webvirtmgr/conf/gunicorn.conf.py
directory=/var/www/webvirtmgr
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/webvirtmgr.log
redirect_stderr=true
user=www-data

supervisorのリスタート(これで8000/tcpで立ち上がる)
$ sudo service supervisor restart 

フロントのWebサーバの設定

フロントに立つnginxを設定する。プロキシしているだけなので、Apacheでもいいと思うが、サンプルがこうなっているのでそうした。

/etc/nginx/sites-available/webvirtmgrに以下を記述する。
server {
    listen 8008 default_server;

    server_name $hostname;
    #access_log /var/log/nginx/webvirtmgr_access_log; 

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-for $proxy_add_x_forwarded_for;
        proxy_set_header Host $host:$server_port;
        proxy_set_header X-Forwarded-Proto $remote_addr;
    }
}

$ sudo ln -s /etc/nginx/sites-available/webvirtmgr /etc/nginx/sites-enabled/webvirtmgr
$ service nginx restart 

これで http://hostname:8008/ にアクセスすると画面が見えるようになる。

novncの設定

ブラウザからコンソールを使うためのnovncの設定。この設定ファイルもwebvirtmgrに入っているので、使うだけでOK.

$ cd /var/www/webvirtmgr
$ sudo cp conf/initd/webvirtmgr-novnc-ubuntu /etc/init.d/webvirtmgr-novnc
$ sudo service webvirtmgr-novnc start
$ sudo update-rc.d webvirtmgr-novnc defaults

(コメントにnova-novncproxyとか書いてあったのでOpenStack Novaから持ってきたのであろう...)

libvirtの設定

WebVirtMgrはlibvirtdにTCP接続するので、その設定をする(なので、ハイパーバイザは別ホストでもOK)
ここの設定は https://www.webvirtmgr.net/docs/ を参考にした(証明書切れてる...)。

/etc/default/libvirt-binのlibvirtd_optsの行に-lを足して、tcpでlistenするようにする。
libvirtd_opts="-d -l"

/etc/libvirt/libvirtd.conf を以下のように編集する。今回はハイパーバイザが同一ホストなので127.0.0.1でlistenさせる。
listen_tls = 0
listen_tcp = 1
listen_addr = "127.0.0.1"

libvirt用のパスワードを設定する。
$ sudo apt-get install sasl2-bin
$ sudo saslpasswd2 -a libvirt ユーザ名
Password: 
Again (for verification): 
$ sudo sasldblistusers2 -f /etc/libvirt/passwd.db 
ユーザ名@ホスト名: userPassword

libvirtdを再起動する。
$ service libvirt-bin restart

以下のようにして接続できることを確認する。

$ virsh -c qemu+tcp://127.0.0.1/system nodeinfo
Please enter your authentication name: ユーザ名@ホスト名
Please enter your password: 
CPU model:           x86_64
CPU(s):              8
CPU frequency:       800 MHz
CPU socket(s):       1
Core(s) per socket:  4
Thread(s) per core:  2
NUMA cell(s):        1
Memory size:         16318576 KiB

Web画面にログインしての設定

http://hostname:8008/ にアクセスすると、ログイン画面が表示されるので、最初の「syncdb」を実行したときのユーザ/パスワードでログインする。

あとは画面ベースなので詳細は省略するが、以下のように設定すれば使えるようになる。

  • Servers List画面で127.0.0.1を追加する。
  • Storage Pools画面で、仮想マシン用のディレクトリorLVMプールとISOを配置するディレクトリを追加する。
  • Networks画面でネットワークを追加する。
  • Instances画面でインスタンスを追加する。

感想

OpenStackをこのようにVMを管理するために使おうと検証してみたのだけど、LVMのストレージやOpen vSwitchのブリッジを自前でIDを振って管理したりしていて、トラブルがあったときに追いづらくなるデメリットのほうが大きそうだったのでやめた。一方で、このWebVirtMgrはlibvirtやLVMをそのまま見せる設計思想なので、何かあったときにも追いやすく、1台~数台の管理ならかなり向いていそうだった。

2014-07-01

GPT パーティションテーブルを別のディスクにコピーする

sdaからsdbにパーティションテーブルをコピーするには、sgdiskを使うのが簡単。コマンドを先に書くとこう。不用意に実行するとさっくり壊れるので注意。
# sgdisk -R=/dev/sdb /dev/sda
# sgdisk -G /dev/sdb
sgdisk --backup sda.table /dev/sdaとかしておいたほうが安心かも?
参考: http://askubuntu.com/questions/57908/how-can-i-quickly-copy-a-gpt-partition-scheme-from-one-hard-drive-to-another

sgdiskがないとき

GPT版のfdiskであるところのgdiskをインストールする。
# apt-get install gdisk
cfdisk, sfdiskがあるように、cgdisk, sgdiskも入ってくる。ただし、コマンドラインオプションは違うので注意。
正直デフォルトで入っていて欲しい...

コマンドの説明

前提として、こんなディスクが二つあるとする。
$ sudo sgdisk -p /dev/sda
Disk /dev/sda: 16777216 sectors, 8.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 3102653 sectors (1.5 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02
   2            4096        13676543   6.5 GiB     FD00

$ sudo sgdisk -p /dev/sdb
Creating new GPT entries.
Disk /dev/sdb: 16777216 sectors, 8.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 65906D3D-B672-4373-AB55-F0C473EC52ED
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 16777149 sectors (8.0 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name

ここで以下を実行することでパーティションテーブルをコピーできる。
$ sudo sgdisk -R=/dev/sdb /dev/sda
The operation has completed successfully.

これだけでコピーされているのだが、ディスクとパーティションに振られた固有IDまでコピーされてしまっている。

以下はディスク本体と2番目のパーティションの状態。
$ sudo sgdisk /dev/sda -p -i 2
Disk /dev/sda: 16777216 sectors, 8.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 3102653 sectors (1.5 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02
   2            4096        13676543   6.5 GiB     FD00
Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID)
Partition unique GUID: CEA27178-6FB4-4358-B921-C75352CE33D7
First sector: 4096 (at 2.0 MiB)
Last sector: 13676543 (at 6.5 GiB)
Partition size: 13672448 sectors (6.5 GiB)
Attribute flags: 0000000000000000
Partition name: ''

$ sudo sgdisk /dev/sdb -p -i 2
Disk /dev/sdb: 16777216 sectors, 8.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): 1519615B-B82D-4CA6-9849-28F3198A90E7 ←同じ
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 3102653 sectors (1.5 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02
   2            4096        13676543   6.5 GiB     FD00
Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID)
Partition unique GUID: CEA27178-6FB4-4358-B921-C75352CE33D7 ←同じ
First sector: 4096 (at 2.0 MiB)
Last sector: 13676543 (at 6.5 GiB)
Partition size: 13672448 sectors (6.5 GiB)
Attribute flags: 0000000000000000
Partition name: ''

そこで、sgdisk -Gで振りなおす。

$ sgdisk -G /dev/sdb

そうすると、IDが新しくなっていることがわかる。
$ sudo sgdisk /dev/sdb -p -i 2
Disk /dev/sdb: 16777216 sectors, 8.0 GiB
Logical sector size: 512 bytes
Disk identifier (GUID): CCC50747-7D41-4662-9FFE-93E7EE7644A3 ←変わっている
Partition table holds up to 128 entries
First usable sector is 34, last usable sector is 16777182
Partitions will be aligned on 2048-sector boundaries
Total free space is 3102653 sectors (1.5 GiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048            4095   1024.0 KiB  EF02
   2            4096        13676543   6.5 GiB     FD00
Partition GUID code: A19D880F-05FC-4D3B-A006-743F0F84911E (Linux RAID)
Partition unique GUID: 29394950-7433-4913-8BA9-64E36B979394 ←変わっている
First sector: 4096 (at 2.0 MiB)
Last sector: 13676543 (at 6.5 GiB)
Partition size: 13672448 sectors (6.5 GiB)
Attribute flags: 0000000000000000
Partition name: ''

2014-06-14

Haswell Refreshで新しいPCを組んだ

KVMでVMをいくつもホストしている自宅のPCが壊れてしまった(といってもおそらくファンだけなのだけど)ので、ケースを捨てるのはたいへんだという理由で中身を入れ替えることにした。備忘の意味で構成を記録しておく。

CPUとHDD以外の部品は秋葉原のツクモの店員さんに相談しながら決めた。アキバのPCショップの店員さんの知識は信じられないくらいマニアックでおもしろいので、荷物は重くなるけれど、通販はしないことにしている。

CPU
Core i7 4790S (Haswell Refresh)
M/B
GIGABYTE GA-Z87X-UD3H
Memory
CFD ELIXIR W3U1600HQ-8GC11 (8GBx2)
HDD
ST3000DM001 x2
電源
Corsair HX650 CP-9020030-JP (650W)
CPU Cooler
Thermalright TRUE Spirit 120M(BW)


ATXマシンを作るのは壊れたこのマシンを作ったとき以来で、壊れたマシンを作ったのはPhenom初代なのでPhenom IIが出る2008年より前、つまり4年以上前になる。そしてずっとAMDでやってきたので、Intelに戻ってきたのはCoppermine以来、つまり10年以上前、下手すると15年くらい前。時が経つのはおそろしい…

このPCを新しいVMの親玉にすべくOpenStackを調べているのだけど、どうもそんなチープな世界を想定したプロダクトではないらしく、おそろしく仰々しい。見ていると時代の進化を追えておもしろいのだけど、たぶん本番はもっとローレベルなものになるはず。

2014-06-03

PSCmdletでもasync/awaitを使いたい

PowerShellコマンドレットをC#で作ること自体は簡単で、単にSystem.Management.Automationアセンブリを参照してCmdletかPSCmdletクラスを継承したクラスを含むクラスライブラリを作るだけなのだけど(このへんを参照→How to Write a Simple Cmdlet) うっかりasync/awaitしたあとにWriteObjectでオブジェクトを出力しようとしたりすると、以下のように怒られてしまう。

Get-SampleCmdlet : WriteObject メソッドと WriteError メソッドは、BeginProcessingメソッド、ProcessRecord メソッド、および EndProcessing メソッドの上書きの外側から呼び出すことはできず、同じスレッド内からだけ呼び出すことができます。コマンドレットで呼び出しが正しく作成されていることを確認するか、または Microsoft カスタマー サポート サービスにお問い合わせください。

Microsoft カスタマー サポート サービスにお問い合わせしたらこんなことも教えてくれるの?と思うところだけど、まぁこういう状況ではSynchronizationContextを提供してあげればよくね?ということでオレオレSynchronizationContextを準備して提供するようにしたのがこのAwaitablePSCmdletクラス

使い方は簡単で、PSCmdletクラスの代わりにこのAwaitablePSCmdletクラスを継承して、BeginProcessing/ProcessRecord/EndProcessingの代わりにAsyncをつけたBeginProcessingAsync/ProcessRecordAsync/EndProcessingAsyncを実装すればいいだけ。
例えばこんな感じ。


本当はStopProcessingにCancellationTokenまわりとか実装しないといけないのだろうけど、たぶんあとは微調整できるレベルだと思うので、必要に応じて。

2014-05-11

MSILでわかるC# のラムダ式

そういえばLINQ to Objectsでコードを書いたときに裏でどういう動きになっているのかなぁ、とILでイメージができなかったので読んでみたら、単にC#のラムダ式がどうなっているかという話だけだったのでまとめる。

長いのでサマリ:
  • ラムダ式を使うと、基本的に入れ子クラスが作られ、ラムダ式が使う変数をとっておかれる。
  • ラムダ式の中身に書いたものは、基本的に入れ子クラスのメソッドとして定義される。
  • 入れ子クラスは不要なときには作られない。

評価スタックでピンとこない方は「C#でHelloWorldプログラムを作成する」を読んでおくのをおすすめする。

今回の説明用のサンプルコードはこちら。「.Count(x => x == val)」がどうなっていくのか、というお話。
namespace ConsoleApplicationLinq
{
    class Program
    {
        static void Main(string[] args)
        {
            int val = 100;
            var format = "Count of {0}: {1}";
            
            var array = new int[0];
            var count = array.Count(x => x == val);

            System.Console.WriteLine(format, val, count);
        }
    }
}

サイズ0のint配列に100がいくつあるか調べてWriteするというコード(0に決まっている)。説明の都合、順番などが不自然な感じになっている。

このソースをILに逆アセンブルした結果がこちら。
https://gist.github.com/sunnyone/0d3ed8285b5d91495399
別ウィンドウで開きながら説明を見るといいかも。

さて、これからILを見ていく。書いた部分がコンパイルされているであろうMainメソッドの実装を見ようとすると、そのMainメソッドの前に見知らぬ「<>c__DisplayClass1」という入れ子クラスが作られていることがわかる。

そのクラスの内容はこうなっている。

.class auto ansi sealed nested private beforefieldinit '<>c__DisplayClass1'
         extends [mscorlib]System.Object
  {
    (CompilerGeneratedAttributeの部分は省略)
    
    .field public int32 val // valフィールド
    
    (ここにコンストラクタの定義があるが省略)
    
    // <Main>b__0メソッドの定義
    .method public hidebysig instance bool 
            '<Main>b__0'(int32 x) cil managed
    {
      .maxstack  8
      
      // xをロード(評価スタックにpush)
      IL_0000:  ldarg.1
      
      // valフィールドをロード
      IL_0001:  ldarg.0
      IL_0002:  ldfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
      
      // 二つの値=xとvalの値を比較し、同じなら1・異なるときは0
      IL_0007:  ceq
      
      // return
      IL_0009:  ret
    } // end of method '<>c__DisplayClass1'::'<Main>b__0'

  } // end of class '<>c__DisplayClass1'

このクラスは要は「x => x == val」の部分を<Main>b__0というメソッドに実装し、加えてvalフィールドを持っている。

次に本体のMainメソッド。Mainメソッドはこの入れ子クラスを活用して動作する。

まずメソッドとローカル変数の定義。format, array, countのほかに、先ほどの<>c__DisplayClass1が「CS$<>8__locals2」として用意されているのがわかる。valはないことに注意。
.method private hidebysig static void  Main(string[] args) cil managed
  {
    .entrypoint
    .maxstack  3

    .locals init ([0] string format,
             [1] int32[] 'array',
             [2] int32 count,
             [3] class ConsoleApplicationLinq.Program/'<>c__DisplayClass1' 'CS$<>8__locals2')

ここから処理開始だが、記述したC#コードに対応する部分に先立って、<>c__DisplayClass1がnewされ、ローカル変数に入る。
// <>c__DisplayClass1クラスのインスタンスを生成してローカル変数3番目にセット
    IL_0000:  newobj     instance void ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::.ctor()
    IL_0005:  stloc.3

次に「int val = 100;」の部分。ここがポイント。
C#コード上ではローカル変数に見えているが、実際には内部の<>c__DisplayClass1クラスのフィールドになっている。
入れ子になったクラスにあるラムダ式の実体がこの変数を使うためにフィールドに入れている。
// CS$<>8__locals2と「100」をロードして、CS$<>8__locals2のvalにセット
    IL_0006:  ldloc.3
    IL_0007:  ldc.i4.s   100 
    IL_0009:  stfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val

次に「var format = "Count of {0}: {1}";」の部分。このように、ラムダ式と関係ない部分はふつうのローカル変数になる。
// 説明省略
    IL_000e:  ldstr      "Count of {0}: {1}"
    IL_0013:  stloc.0

次に「var array = new int[0];」だが、ここもローカル変数に入れるだけ。
// 説明省略
    IL_0014:  ldc.i4.0
    IL_0015:  newarr     [mscorlib]System.Int32
    IL_001a:  stloc.1

次に実際のラムダ式が登場する「var count = array.Count(x => x == val);」の部分。
先ほどvalのために生成したDisplayClassのメソッドを使ってFuncオブジェクトを生成し、Countメソッドに渡している。

// ローカル変数1番目:arrayをロード
    IL_001b:  ldloc.1
    
    // <Main>b__0メソッドのポインタをロード
    IL_001c:  ldloc.3
    IL_001d:  ldftn      instance bool ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::'<Main>b__0'(int32)
    
    // <Main>b__0メソッドのポインタを使って、Funcオブジェクトを生成
    IL_0023:  newobj     instance void class [mscorlib]System.Func`2::.ctor(object,
                                                                                        native int)
                                                                                        
    // System.Linq.Enumerable::CountメソッドにarrayとFuncオブジェクトを渡す
    IL_0028:  call       int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
                                                                                 class [mscorlib]System.Func`2)

    // ローカル変数2番目にセット
    IL_002d:  stloc.2

最後に「System.Console.WriteLine(format, val, count);」の部分。
ポイントはvalを使うのにラムダ式用の<>c__DisplayClass1を使っているところ。
// ローカル変数0番目: formatをロード
    IL_002e:  ldloc.0
    
    // ローカル変数3番目: CS$<>8__locals2のvalフィールドをロード、intなのでboxingする
    IL_002f:  ldloc.3
    IL_0030:  ldfld      int32 ConsoleApplicationLinq.Program/'<>c__DisplayClass1'::val
    IL_0035:  box        [mscorlib]System.Int32
    
    // ローカル変数2番目: countをロード, boxing
    IL_003a:  ldloc.2
    IL_003b:  box        [mscorlib]System.Int32
    
    // WriteLine
    IL_0040:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object,
                                                                  object)
    IL_0045:  ret
  } // end of method Program::Main

このように、ラムダ式は、必要な変数を入れ子クラスのインスタンスにとっておいて、ラムダ式に書いた内容のメソッドが実行される、という形で実装されている。
しかし、ラムダ式があれば必ず入れ子クラスが作られるかというとそうではなく、変数をとっておく必要がない場合、違う形にコンパイルされる。

たとえば、先のコードのint val = 100;にconstをつけてconst int val = 100;にするだけで、ILはこうなってしまう。

.class private auto ansi beforefieldinit ConsoleApplicationLinq.Program
       extends [mscorlib]System.Object
{
  // ProgramクラスそのものにFuncのフィールドが用意される
  .field private static class [mscorlib]System.Func`2<int32,bool> 'CS$<>9__CachedAnonymousMethodDelegate1'
  
  (CompilerGeneratedAttributeの部分は省略)
  
  .method private hidebysig static void  Main(string[] args) cil managed
  {
    // ローカル変数の定義などなど
    .entrypoint
    .maxstack  3
    .locals init ([0] string format,
             [1] int32[] 'array',
             [2] int32 count)
    IL_0000:  ldstr      "Count of {0}: {1}"
    IL_0005:  stloc.0
    IL_0006:  ldc.i4.0
    IL_0007:  newarr     [mscorlib]System.Int32
    IL_000c:  stloc.1
    IL_000d:  ldloc.1
    
    // CS$<>9__CachedAnonymousMethodDelegate1をロードして、存在すればこの先の処理まで飛ばす
    IL_000e:  ldsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0013:  brtrue.s   IL_0026

    // なければProgramクラスに定義された<Main>b__0メソッド(「x => x == val」の実装)を使ってFuncオブジェクトを作る
    IL_0015:  ldnull
    IL_0016:  ldftn      bool ConsoleApplicationLinq.Program::'<Main>b__0'(int32)
    IL_001c:  newobj     instance void class [mscorlib]System.Func`2::.ctor(object,
                                                                                        native int)
    // 作ったらフィールドに格納して、ロードしておく
    IL_0021:  stsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    IL_0026:  ldsfld     class [mscorlib]System.Func`2 ConsoleApplicationLinq.Program::'CS$<>9__CachedAnonymousMethodDelegate1'
    
    // 以下同じ
    IL_002b:  call       int32 [System.Core]System.Linq.Enumerable::Count(class [mscorlib]System.Collections.Generic.IEnumerable`1,
                                                                                 class [mscorlib]System.Func`2)
    IL_0030:  stloc.2
    IL_0031:  ldloc.0
    IL_0032:  ldc.i4.s   100
    IL_0034:  box        [mscorlib]System.Int32
    IL_0039:  ldloc.2
    IL_003a:  box        [mscorlib]System.Int32
    IL_003f:  call       void [mscorlib]System.Console::WriteLine(string,
                                                                  object,
                                                                  object)
    IL_0044:  ret
  } // end of method Program::Main

  (コンストラクタの定義は省略)
  
  .method private hidebysig static bool  '<Main>b__0'(int32 x) cil managed
  {
    (CompilerGeneratedAttributeの部分は省略)
    
    .maxstack  8
    IL_0000:  ldarg.0
    IL_0001:  ldc.i4.s   100 // 「100」はここに埋め込まれるので、フィールドから取る必要がない
    IL_0003:  ceq
    IL_0005:  ret
  } // end of method Program::'<Main>b__0'

} // end of class ConsoleApplicationLinq.Program

こっちのコードだとご丁寧にもFuncオブジェクトをstaticフィールドにキャッシュしている。

ちなみに、valをローカル変数ではなくフィールドに持つようなクラスを作った場合も上述のconstの形に近くなり、入れ子クラスは作られない(Funcオブジェクトのキャッシュはしなくなる)。

まぁ、この差異はプログラム全体からしたらたいしたことはないと思うので、書くときに意識することはないと思うが、知っておいても悪くない…かな?
(一応、array.Count(x => x == val)を10000000回実行したら、valがconst/フィールド/ローカル変数それぞれの場合でStopwatchクラス読みで240ms/240ms/300msだった)

こうしてILコードを見ていると、ラムダ式がどういうものなのかしっくりきたのだけど、みなさまはどうだろうか?

2014-04-24

MSBuild Launcher 0.2.3 リリース

MSBuild Launcher 0.2.3をリリースしました。

今回の大きな変更点の一つ目はパフォーマンス改善です。生産性が上がるというレベルの変化なのでぜひ上げてください。処理待ちよりもログ出力待ちのほうが多いなんてビルドをされている方には、体感でわかるレベルで違うはずです。PowerShellで適当に出力して確認していたら、2倍ぐらい速くなっていました。


確認に使っていたビルドファイル:


もうひとつはキャンセルをするときに再帰的にプロセスを停止するようにしたことです。今まではエンジン用プロセスだけ停止していたので、Execで起動するようなケースではプロセスが残ってしまったりしたのですが、子供から殺すのでいなくなるはずです。