今月の Software Design : 2013/02 (その2)

 「今月の Software Design 」の2月号の投稿第2回目です。

 SD とは全然関係無いのですが、最近元リクルートのミドルエイジの方とお話する機会がありました。

 何をやっているのかを尋ねると「次世代のリーダーを育成する活動」をしているとか。なんとも胡散臭い活動ですが、なんとリーダーを育成するための確立した方法や、更にはそれについて追求している学問があると言います。

 にわかには信じがたい話です。ちなみにその学問というのが「経営行動科学」という分野の学問なんだそうです。初めて聞きましたが『科学』という名前が付いているのがめちゃめちゃ気になります。『科学』というからには、その論理体系の出発点となる何がしかの原理・原則があるはずだが、それは一体何なのか!? これについて質問すると、この分野で名著と呼ばれる本をいくつか紹介され、「これを読んだらわかるよ」的な説明をされました。なんだかいよいよもって怪しい感じがしますが、相手を知らずして批判をするのは良くないので、ここは素直に勧められた本を読んでみようと思います(読んだら感想を書きます)。

 それでは今月も以下のフォーマットで、各記事の要約と感想を書いてゆきます。

 

・記事のタイトル
( 1 行目 ) 記事の要約
( 2 行目以降 ) 感想

 

・第一特集① :なぜ今、シェルスクリプトの習得が必要なのか?

 シェルにフォーカスした UNIX の歴史と哲学の概略についてのお話。パイプとリダイレクトによる個々のツールを組み合わせた処理系のごく基本的な紹介記事になります。

 記事の内容自体はシェルの基本的なお話ですが、こうした基本的な話や歴史的な経緯がさらっと書けちゃう辺り、書き手の素養の深さを感じます。

 他の人が書いた(とりわけ上田さんのような玄人が書いた)シェルスクリプトは非常にためになります。シェルスクリプトにしろ、なまじ特定の言語の記述に慣れてしまうと自分の型にはまってしまいます。特にシェルスクリプトの場合、シンタックスはそれほど多くありませんが、コマンドラインユーティリティの数は無数に存在しており、自分のような素人は、標準で搭載されている (/bin, /usr/bin 以下の) モノののうち 10% も扱えているか疑問です。玄人が書いたコードには、簡単なサンプルコードの中にも思わぬ発見があることもあるので必見です。

 あと、僕のように歴史をあまり知らない人間には、最後のコラムに書かれている内容は慧眼でした。

 

 

・第一特集② :シェルの動作原理を "深く" 理解する

 インタプリタのロード、シェル変数、サブシェルについて、dtrace を用いて fork(2) と execve(2) の呼び出しを確認しながら動作を説明しています。更にインタプリタのロードでは、execve(2) のカーネルコードを参照して、その動作を説明しています。

 タイトルにある通り、シェルの動作について "深く" 書かれているんですが、簡単なことをわざわざ難しく書いているようにも思えます。DTrace についてほとんど説明しないで DTrace スクリプトをホラよと言わんばかりに掲載したりと、かなり強引です。こんな仰々しいことしないでも、この程度の説明だったら "ps --forest" で事足りるんじゃなかろうか。

 あと ps(1) や fork(2)、init(8) などのセクション番号について「このくらい知ってて当然でしょ?」と言わんばかりにサラッと流す辺り素人を寄せ付けません。

 1章では「"ユーザ"もシェルを使おう」といった記述があり、普段シェルを利用しない非プログラマ向けの内容を匂わせていましたが、この章でそうした甘い言葉に釣られた読者達は返り討ちに合ったに違いありません。

 ちなみに、セクション番号というのは、各コマンドの種別を表すもので、シェルで "man man" と実行すると説明が読めます。

 

 

・第一特集③ :シェルスクリプトがファイル入出力に強いわけ

 シェルの標準入出力、標準エラー出力について、DTrace を用いて、発行したシステムコールを見ながらこれらの処理の実態を解説しています。また最後に、メモリファイルシステムを用いてキャッシュについて説明しています。

 さっきからケチつけてばっかりですが "キャッシュとIO" における「キャッシュを意識したプログラミング云々」の話は非常に引っかかります。単なる言い回しの問題かと思いましたが、最後に「メモリファイルシステムを使って明示的に云々」と言っているので、そうでもなさそうです。

 BSD ではどうか知りませんが、GNU/Linux では汎用ファイルシステムにおいてページキャッシュを提供しており、これにはファイルの読み書きの際にファイルのブロックデータが格納されます。なので、ユーザは通常のファイルシステムにおけるすべてのファイルに対するアクセスは (O_DIRECT フラグ付きでオープンされたものでない限り) メモリキャッシュを通して行います。したがって、メモリファイルシステムであろうとなかろうとキャッシュが利用されるので、ユーザはキャッシュを意識したプログラミングをする必要はありません。

 そんなわけで、メモリファイルシステム以外のファイルに対して、記事で書かれた CACHE.SH の処理を実行して、同じ結果が得られるかを検証してみます。まずは、記事で書かれた処理について見てみます。ちなみにスクリプト中の "$$" は、カレントプロセス (シェル) のプロセス ID を表す特殊変数になります。

 

$ cat cache-memory-fs.sh
#!/bin/bash
 
tmp=/tmp/$$
 
dd if=/dev/zero of=${tmp}-out bs=1024x1024x10 count=100
dd if=/dev/zero of=${tmp}-out2 bs=1024x1024x10 count=2000
 
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null

 

$ ./cache-memory-fs.sh
100+0 レコード入力
100+0 レコード出力
1048576000 バイト (1.0 GB) コピーされました、 2.20463 秒、 476 MB/秒
2000+0 レコード入力
2000+0 レコード出力
20971520000 バイト (21 GB) コピーされました、 46.8089 秒、 448 MB/秒
time cat /tmp/28125-out > /dev/null
real  0m2.156s
user  0m0.012s
sys   0m0.528s
time cat /tmp/28125-out > /dev/null
real  0m0.207s
user  0m0.004s
sys   0m0.200s

 

 おおよそ記事と同じような結果が得られました。今度は、ストレージに格納されている通常のファイルシステム (ext4) にて同じ処理を行います。

 

$ cat cache-local-fs.sh
#!/bin/bash
 
tmp=~/$$
 
dd if=/dev/zero of=${tmp}-out bs=1024x1024x10 count=100
dd if=/dev/zero of=${tmp}-out2 bs=1024x1024x10 count=2000
 
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null

 

$ ./cache-local-fs.sh 
100+0 レコード入力
100+0 レコード出力
1048576000 バイト (1.0 GB) コピーされました、 2.25739 秒、 465 MB/秒
2000+0 レコード入力
2000+0 レコード出力
20971520000 バイト (21 GB) コピーされました、 47.1223 秒、 445 MB/秒
time cat /home/ohyama/28125-out > /dev/null
real  0m2.056s
user  0m0.000s
sys   0m0.444s
time cat /home/ohyama/28125-out > /dev/null
real  0m0.196s
user  0m0.008s
sys   0m0.184s

 

 メモリファイルシステムで行った場合とほぼ同じ結果になっています。ちなみに、記事では最初の dd で生成したファイルをキャッシュから追い出すために、次の行で大きなファイルを生成していますが。GNU/Linux では /proc/sys/vm/drop_caches に対して 1 を書き込むと、ページキャッシュがクリアされるので、以下のようにしても同じ結果が得られます。

 

$ cat cache-local-fs2.sh
#!/bin/bash
 
tmp=~/$$
 
dd if=/dev/zero of=${tmp}-out bs=1024x1024x10 count=100
#dd if=/dev/zero of=${tmp}-out2 bs=1024x1024x10 count=2000
echo 1 > /proc/sys/vm/drop_caches
 
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null
echo -n "time cat ${tmp}-out > /dev/null"
time cat ${tmp}-out > /dev/null

 

# ./cache-local-fs2.sh
100+0 レコード入力
100+0 レコード出力
1048576000 バイト (1.0 GB) コピーされました、 2.15693 秒、 486 MB/秒
time cat /home/ohyama/32018-out > /dev/null
real  0m2.046s
user  0m0.008s
sys   0m0.456s
time cat /home/ohyama/32018-out > /dev/null
real  0m0.205s
user  0m0.000s
sys   0m0.204s

 

 

・第一特集④ :シェルスクリプトのエラーハンドリングとデバッグ

 ここまで BSD 系の話でしたが、ここで bash 固有の話が頻出します。シェルの終了ステータスコードの話と、それらを取得する方法。そして bash の trap コマンドによるエラーハンドリングの話。そして、パイプライン中のログ出力について解説しています。

 bash 固有の話が多いですが非常に面白いです。内容もこれまでの素養としてのお話というよりも、実用的なお話で、特に trap コマンドや -xv パラメータについては目から鱗が落ちました。