yunomuのブログ

趣味のこと

研究室の思い出 印刷された大量のソースコード

 2004年頃、大学4年で研究室に配属され、OSとネットワークの研究室でちょっと専門外の機械学習の研究をしていた。配属直後なので研究というより概念のお勉強から始まる。

 その研究室では既に10年続くプロジェクトもあり、打ち合わせのために作られた大量の検討資料がファイルに綴じられて本棚に刺さっていた。しかしよく見ると半数以上のラベルがプロジェクト名ではなく、「FreeBSD src/sys/alpha」とか「FreeBSD src/sys/boot」とか、何かのパス名みたいなものだった。そういうファイルが本棚の1列分以上ある。なんだこれ、と思って先輩に聞いてみた。

「あのファイル何なんですか?」
「あーそれ、俺が印刷した」

 この研究室は私が配属される2年前に教授が他の大学から移られてきてできたものであり、先輩は何もわからないままその研究室に入った人だった。教授が前にいた大学に研究室の先輩と言える人はいたものの、そう頻繁に連絡が取れるわけでもなく、先輩のいない研究室でいきなりOSの研究を引き継ぐことになった。当初はマシンの起動やカーネルのビルドで手こずって遠方の先輩に電話で助けてもらったり出張してきてもらったりもしていたらしい。

 そんなこんなで先輩はOSの研究に加わった。前任者の関係だったか何か忘れたけど、提案手法の実装評価はFreeBSDを改造してやることになっていたため、そもそもの構造の勉強をFreeBSDをベースに行おうとした。行おうとしたが、そもそも何をしたらいいのか取っ掛かりがない。どうしたらいいか先生にたずねてみても「ソースコード読め」くらいしか言われない。とりあえず読みやすいように印刷して読んでみるも、当然のように何もわからない。当時もetagsのようにソースコードを解析して定義を参照できるようなツールはあったものの、当時の先輩はそれを知らないし、カーネルソースコード全体を解析できるようなものではない。関数マクロは解析しきれないし、その後いろいろ理解してから解析対象を絞っても2日くらいはかかった。

 先輩は先生に相談してみる。
「どうしたらいいですか」
「関係ありそうなところを読めばいいよ」
「それがどこかわからないんですが」
「じゃあ全部読め」
「印刷していいですか、読みやすいので」
「いいよ」
「本当に? 大量にありますよ? かなり紙を使いますよ?」
「紙はいっぱいあるから」
というようなやり取りを経て、印刷作業が始まったらしい。

 ちょっとした脅しというか、面倒だから教えてほしいと思っていたのに「やってみれば」みたいな対応をされたのでそれに対抗して、でもまあやるしかないかということでソースコードの先頭(アルファベット順)のファイルから印刷開始。ところがソースコードの量は思っていた量の何倍どころか何十倍も多く、数日経ってもsys以下のディレクトリ1つか2つしか消化できていない。でもやれと言うんだからやってやると意地になって1〜2週間、ひたすらプリンタを専有して印刷し続けて、さすがに頭が冷えたらしい。紙も時間も無駄だし、そもそもそれだけやり続けてもまだ1割も行ってない気がするし、そもそもこれを全部読むのかと。

 結局その後、きちんと調べるべき部分を調べ直し、ようやく研究を開始することができ、この時に印刷したソースコードは記念にファイリングして取っておいている、ということらしい。先輩は「いやーあの頃は若かったなー」と言っていたが1年前の話であるし、実際には「きちんと調べるべき部分を調べ直し」の部分の方が大変だったであろうことは想像できる(私もこの1年後に似たようなことをやったので)。

 余談だが、FreeBSDソースコードはsrc/sys以下にあるが、この中にはデバイスドライバや各アーキテクチャ依存のコードもフラットに置かれているため、先輩が本来読むべきコードは "i386", "kern", "ufs"あたりであり、特に研究テーマがファイルシステムに関係が深かったために最も重要なのは"ufs"であった。アルファベット順に読んでいると大変なことになっていただろうし、実際に頭文字cのディレクトリの数個目で止まっていた。cで始まるディレクトリは多い割にほとんど本筋ではない。危ないところだった。

 ということで、この話は面白すぎるので新しく研究室配属されてきた人に「あのファイル何ですか」と聞かれる度に語り部をやっていたが、語る相手もいなくなったので放流することにする。

Goでsubcommandsを使う

gitkubectl みたいなサブコマンドを実装したい時。

 たぶん一番楽なのはsubcommandsライブラリを使う方法だと思います。

subcommands package · pkg.go.dev

 mainは後回しにして、subcommand側から。 Command 型を作って、subcommands.Command インタフェースを実装します。

package subcmd

import (
        "context"
        "flag"
        "fmt"

        "github.com/golang/subcommands"
)

type Command struct {}

// これがサブコマンド名になる
func (c *Command) Name() string { return "sbcmd" }

// コマンド一覧で出てくるサブコマンドの説明
func (c *Command) Synopsis() string { return "subcommands example" }

// helpとかで出てくる使い方
func (c *Command) Usage() string { return "subcmd [args]" }

// flagライブラリでオプションの処理をするやつ
func (c *Command) SetFlags(f *flag.FlagSet) {
}

// 本体
func (c *Command) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
        fmt.Println("ノ˘ω˘) You just don't have enough fight in you!")

        return subcommands.ExitSuccess
}

 mainでこのCommandを登録します。

package main

import (
    "context"
    "flag"
    "os"

    "github.com/google/subcommands"

    "yunomu.net/subcmdex/subcmd" // Commandを定義したパッケージ
)

// 別にmain()でもよい
func init() {
        // commands, flags, helpというサブコマンドはプリセットがあるのでせっかくだから登録する
    subcommands.Register(subcommands.CommandsCommand(), "help")
    subcommands.Register(subcommands.FlagsCommand(), "help")
    subcommands.Register(subcommands.HelpCommand(), "help")

    subcommands.Register(&subcmd.Command{}, "")

        // subcommands.Registerの後に実行する
        // 全体で1回だけ実行しないとおかしなことになる
    flag.Parse()
}

func main() {
    ctx := context.Background()

    subcommands.Execute(ctx)
}

 これでサブコマンドが実行できる。

% go run main.go subcmd
ノ˘ω˘) You just don't have enough fight in you!

 さらにsubcmdのサブコマンドも作れる。CommandインタフェースのSetFlagを以下のように書き換える。

// (省略)

type Command struct {
        cdr *subcommands.Commander // Commanderを保存できるようにする。
}

// (省略)

func (c *Command) SetFlags(f *flag.FlagSet) {
        // SetFlagsはCommandがRegisterで登録された時に呼び出されるのでCommanderはここで初期化する
        cdr = subcommands.NewCommander(f, "")

        cdr.Register(&subsubcmd.Command{})

        // サブコマンドにもcommands, flags, helpがあるのでついでに登録しておくといい
        cdr.Register(cdr.CommandsCommand(), "help")
        cdr.Register(cdr.FlagsCommand(), "help")
        cdr.Register(cdr.HelpCommand(), "help")

        c.cdr = cdr
}

func (c *Command) Execute(ctx context.Context, f *flag.FlagSet, args ...interface{}) subcommands.ExitStatus {
        // Commanderを実行する
        return c.cdr.Execute(ctx, args...)
}
// (省略)

 これで何段階でも階層化したコマンドを作ることができる。

 それはそうと kubectlgcloud コマンドはsubcommandsではなくCobraを使っています。ソースコードを生成するタイプなのでそれなりの命名規則などのクセはあるもののこれはこれで便利。

GitHub - spf13/cobra: A Commander for modern Go CLI interactions

glide getやglide upが動かない

 glide getで新しいライブラリをインストールしようとするとこんなエラーが出た。

[ERROR] Error scanning github.com/golang/protobuf/ptypes/duration: open /Users/yunomu/.glide/cache/src/https-github.com-golang-protobuf-ptypes-duration: no such file or directory
[ERROR] This error means the referenced package was not found.
[ERROR] Missing file or directory errors usually occur when multiple packages
[ERROR] share a common dependency and the first reference encountered by the scanner
[ERROR] sets the version to one that does not contain a subpackage needed required
[ERROR] by another package that uses the shared dependency. Try setting a
[ERROR] version in your glide.yaml that works for all packages that share this
[ERROR] dependency.

 私がメインで使っているMacだとこうなるけど、他の環境ではこのエラーは起きない。

 そもそもエラーメッセージの中では https-github.com-golang-protobuf-ptypes-duration を探そうとしているが私が探しているのは https-github.com-golang-protobuf/ptypes/duration である。パスの切り方が違うので当然ながらファイルは見つからない。

 バグかと思ったけどもそういうissueも登録されていないのでコードを見てみる。 https://github.com/Masterminds/glide/tree/v0.12.3

 件のエラーメッセージが dependency/resolver.go の543行目あたりに書かれている。 https://github.com/Masterminds/glide/blob/v0.12.3/dependency/resolver.go

 543             } else if strings.Contains(errStr, "no such file or directory") {
 544                 r.hadError[dep] = true
 545                 msg.Err("Error scanning %s: %s", dep, err)
 546                 msg.Err("This error means the referenced package was not found.")
 547                 msg.Err("Missing file or directory errors usually occur when multiple packages")
 548                 msg.Err("share a common dependency and the first reference encountered by the scanner")
 549                 msg.Err("sets the version to one that does not contain a subpackage needed required")
 550                 msg.Err("by another package that uses the shared dependency. Try setting a")
 551                 msg.Err("version in your glide.yaml that works for all packages that share this")
 552                 msg.Err("dependency.")

エラーメッセージに “no such file or directory” という文字列が入っているかどうかでエラーを判断しているあたり大胆だ。

 そのエラーの原因がここ。デバッグメッセージ付きで実行してみると、同ファイルのこの部分の時点で 493行目の r.Handler.PkgPath(dep)https-github.com-golang-protobuf-ptypes-duration を返しており、成功するはずがない。

 492         // Here, we want to import the package and see what imports it has.
 493         msg.Debug("Trying to open %s (%s)", dep, r.Handler.PkgPath(dep))
 494         var imps []string
 495         pkg, err := r.BuildContext.ImportDir(r.Handler.PkgPath(dep), 0)

 次に PkgPath の定義を見る。interfaceなので定義は2つあるが repo/installer.go の方。 https://github.com/Masterminds/glide/blob/v0.12.3/repo/installer.go

 600 // PkgPath resolves the location on the filesystem where the package should be.
 601 // This handles making sure to use the cache location.
 602 func (m *MissingPackageHandler) PkgPath(pkg string) string {
 603     root, sub := util.NormalizeName(pkg)
 604
 605     // For the parent applications source skip the cache.
 606     if root == m.Config.Name {
 607         pth := gpath.Basepath()
 608         return filepath.Join(pth, filepath.FromSlash(sub))
 609     }
 610
 611     d := m.Config.Imports.Get(root)
 612     if d == nil {
 613         d = m.Config.DevImports.Get(root)
 614     }
 615
 616     if d == nil {
 617         d, _ = m.Use.Get(root)
 618
 619         if d == nil {
 620             d = &cfg.Dependency{Name: root}
 621         }
 622     }
 623
 624     key, err := cache.Key(d.Remote())
 625     if err != nil {
 626         msg.Die("Error generating cache key for %s", d.Name)
 627     }
 628
 629     return filepath.Join(cache.Location(), "src", key, filepath.FromSlash(sub))
 630 }

本来、最後629行目で key=https-github.com-golang-protobuf, filePath.FromSlash(sub)=ptypes/dulation にならなければならないが key=https-github.com-golang-protobuf-ptypes-duration になっている。全然ダメだ。 そもそも603行目のutil.NormalizeName()が root=github.com/golang/protobuf/ptypes/duration, sub= を返している。 このライブラリのリポジトリhttps://github.com/golang/protobuf で、リポジトリ内のサブディレクトリは ptypes/dulation なので、ここでは root=github.com/golang/protobuf, sub=ptypes/dulation となってほしい。

ということで util.NoralizeName() を見る。 https://github.com/Masterminds/glide/blob/v0.12.3/util/util.go

300 // NormalizeName takes a package name and normalizes it to the top level package.
301 //
302 // For example, golang.org/x/crypto/ssh becomes golang.org/x/crypto. 'ssh' is
303 // returned as extra data.
304 //
305 // FIXME: Is this deprecated?
306 func NormalizeName(name string) (string, string) {
307     // Fastpath check if a name in the GOROOT. There is an issue when a pkg
308     // is in the GOROOT and GetRootFromPackage tries to look it up because it
309     // expects remote names.
310     b, err := GetBuildContext()
311     if err == nil {
312         p := filepath.Join(b.GOROOT, "src", name)
313         if _, err := os.Stat(p); err == nil {
314             return toSlash(name), ""
315         }
316     }
317
318     name = toSlash(name)
319     root := GetRootFromPackage(name)
320     extra := strings.TrimPrefix(name, root)
321     if len(extra) > 0 && extra != "/" {
322         extra = strings.TrimPrefix(extra, "/")
323     } else {
324         // If extra is / (which is what it would be here) we want to return ""
325         extra = ""
326     }
327
328     return root, extra
329 }

嫌な感じのコメントが書いてある。 それはいいとして、312行目でGOROOTから該当のライブラリを探している。 私の場合、 GOROOT=/usr/local/go なので、 /usr/local/go/src/github.com/golang/protobuf/ptypes/duration が存在するかどうかを確認している。実際に見てみると、あった。なんであるんじゃい。

 このファイル(ディレクトリ)が存在することで314行目でreturnしてしまって、最終的におかしなURLにライブラリの更新確認に行って死んでいた模様。この後にパスからリポジトリの種類を判別してURLを作るのだがそもそも/usr/local以下はgitはgitでもbrewの配下である。

% sudo rm -rf /usr/local/go/src/github.com

で事なきを得た。そもそもなんでこんなところにこんなものが入っていたのかよくわからない。

 このあたりを探っているとGoogle内部のリポジトリやパッケージ管理法とGitとの相性の悪さで外の人たちが割を食ってる感じがしてちょっと面白い。自分が悩む方でなければ。

 あとGoは読むのが楽で助かる。

cabalでビルドをカスタマイズする

 cabalファイルだけでは足りない事がある。どうしても単一のプログラムではできないこともあるし、TemplateHaskellでも生成できなくてやむなくソースコードを自動生成するスクリプトを書かざるを得ない事もあります。自動生成なんかやると何かに負けた気分になりますが、cabalでなければ普通にやっている事なので、何も問題は無い気がします。MakefileとかAntとか。

 具体的には、
(1) Haskell製コード生成プログラムをビルド
(2) シェルスクリプト経由でコード生成プログラムを実行
(3) 本体のプログラムをビルド
という事をやりたい。

 ところがcabalには直接的に外部のコマンドを実行してビルドする方法が無いみたいです。無いみたいなんですが、ビルドを細かく制御する方法はいくつか用意されています。
(A) Build-Type: Configureでautoconfで頑張る
(B) Build-Type: MakeでMakefileを書いて頑張る
(C) Build-Type: CustomでSetup.hsを書いて頑張る

 はっきり言って(A)で何でもできるんですが、それなりに面倒くさい。(B)は楽そうだけど、全部のターゲット(http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Make.html)を定義するのは結構面倒だし、そんなに色々出来る必要は無い。cabalの機能もそれなりに使いたいし。どうでもいいけど、Makeのときにcabal configureの時にconfigureファイルが無いとコケるのはバグかな。空のconfigureファイルがあればその後は問題なく通っちゃうし、configureスクリプト使ってないっぽいのでバグっぽい。誰も使ってないんだろうか。まあ(A)と(C)があれば使わないよな。

 そんなわけで、余程の事がなければCustomで足りると思うのでCustomを使って行こうと思います。

 ここで、こんな感じのプロジェクトを作ったと仮定します。この例はHaskellのCabalのマニュアルからのコピペ(http://www.haskell.org/ghc/docs/7.0.3/html/Cabal/authors.html)のBuild-TypeをCustomに変えただけのものです。

Name:           TestPackage
Version:        0.0
Cabal-Version:  >= 1.2
License:        BSD3
Author:         Angela Author
Synopsis:       Small package with two programs
Build-Type:     Custom

Executable program1
  Build-Depends:  HUnit
  Main-Is:        Main.hs
  Hs-Source-Dirs: prog1

Executable program2
  Main-Is:        Main.hs
  Build-Depends:  HUnit
  Hs-Source-Dirs: prog2
  Other-Modules:  Utils

 今回はこれを、program1をビルドするためにprogram2とシェルスクリプト(build.sh)が必要だということにしよう。

 これに加えて、cabal initした時に作られるSetup.hsがあります。
 こんなの。

import Distribution.Simple
main = defaultMain

 このままの場合は、Build-Type: Simpleの時と全く同じ動きをします。
 ちなみにimportの部分をDistribution.Makeに変えるとBuild-Type: Makeと同じ動きになります。

 つまり、cabalコマンドを実行した時にこのmainから始まるプログラムが実行されるわけなので、例えば

main = return ()

などとやると何をしても何も起こらなくなります。いや、helpは大丈夫ですけど。

 じゃあconfigureとかbuildとかtestの判別はどうやってるんだというと、単にdefaultMainの中でgetArgsしているだけなので、

import System.Environment
main = getArgs >>= print

なんてやってみると、このSetup.hsというものがどれだけ何でもアリなのかがわかるんじゃないかな。ただ残念ながらcabalに定義されてないサブコマンドを作る事まではできません。

 というわけで、getArgsの中身を見ながら処理を書いてもいいんですが、それだとMakefileと変わりません。ここではcabalが知り得る色々な情報を用意してくれるCabalパッケージのライブラリを使って処理を書くことができます。
Cabal: A framework for packaging Haskell software | Hackage

 具体的には、例えばbuildサブコマンドの前に処理を挟みたい場合はこう

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{preBuildHook = hook}

hook :: Args -> BuildFlags -> IO HookedBuildInfo
hook args flags = ...

何ができるかは、おおむねUserHooks型のマニュアルを見ればわかるかと思います。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple.html#t:UserHooks

 これの中の、例えばbuildHookという、これはhookというよりはbuildコマンドを実行した時に行われる処理そのものなんですけど、そのbuildHook関数の定義がこうなってる。

buildHook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()

この中で、

  • PackageDescriptionは*.cabalファイルの中の全体に関わるNameとかDescriptionの部分
  • LocalBuildInfoはExecutable節とかLibrary節みたいな部分
  • UserHooksは定義したUserHooksそれ自身
  • BuildFlagsはサブコマンドに渡したターゲットやオプション

になっています。
 まるっきり、なんの工夫もなく、cabalの実行時に得られる情報そのものです。
 そのものなので、例えばbuildコマンド実行時にinstallを実行したりもできます。

import Distribution.PackageDescription (HookedBuildInfo)
import Distribution.Simple
import Distribution.Simple.Setup (BuildFlags, defaultInstallFlags)

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = hook}

hook :: PackageDescription -> LocalBuildInfo -> UserHooks -> BuildFlags -> IO ()
hook desc info hooks _flags = (instHook hooks) desc info hooks defaultInstallFlags

これでbuildだけを実行することはできなくなり、buildするとinstallを必ず実行するようになりますが、installコマンドを実行すると無限ループするので使い物にはなりません。

 ここで最初の例に戻ります。
 program1をビルドするためには(1)program2をビルドして(2)build.shを実行した後に(3)program1をビルドする必要があります。
 まあ、これだけの情報があればこれくらいの処理は簡単に作れそうに見えます。

 ところが、これが意外に面倒くさい。
 hooksに渡される情報はあくまでもコマンドに渡される引数とcabalファイルの情報だけなので、これからビルドするターゲットがどれかという情報は含まれていない。program1をビルドしようとしているのかprogram2をビルドしようとしているのかの区別は存在しないのです。
 なので、(3)program1をビルドする前に(1)program2をビルドして(2)build.shを実行する、みたいな順序付けは非常に面倒くさいのです。順序付けならcabalのBuild-Tools: program2でできますけど、プログラムの実行ができない。

 ただ、build処理内でprogram1とprogram2の区別はされていないものの、一応何を実行しようとしているかは知ることができます、BuildFlagsのbuildArgsで。
http://hackage.haskell.org/package/Cabal-1.20.0.2/docs/Distribution-Simple-Setup.html#t:BuildFlags
 このフィールドには、buildサブコマンドに渡されたターゲットが格納されています。

% cabal build program1

を実行すれば["program1"]が、

% cabal build program1 program2

を実行すれば["program1", "program2"]が、

% cabal build

を実行すれば[]が入ってきます。
 お察しの通り最後のが厄介で、buildはターゲットを指定しないと全てのターゲットをビルドするんですが、その時は空リストが格納されています。でも落ち着いてPackageDescriptionのexecutablesから全ターゲットのリストが取れるので、それを使えばいいと思います。こんな感じで。

import Distribution.PackageDescription (HookedBuildInfo, executables, exeName)
import Distribution.Simple

main :: IO ()
main = defaultMainWithHooks simpleUserHooks{buildHook = \desc _ _ _ ->
    print (map exeName (executables desc))}

 あとは、buildArgsの中身を見ながら、program1がビルドされるかどうかを判断してprogram2をビルドしたり、System.Processモジュールを使って外部コマンドを実行したりすればいいんじゃないかと思います。最後は力技です。面倒くさいです。

 さらに面倒なことに、これら面倒な事をパッケージ化してライブラリとして公開したとしても、cabalにはSetup.hsをビルドするための依存ライブラリを記述する機能が無いので、そのライブラリが既にglobalかsandboxにインストールされていないと本体(program1とか)のビルドがコケる。
 これなんとかならないのかな。もはやCabalに機能追加するしか無い気がする。
 いやtargetのどれかのBuild-Dependsに書けば大丈夫なんですけど、targetに関係無い依存関係書きたくないよねぇ。私が使っているのは今のところprocessとdirectoryで、これらは本体でも使っているので問題は起きていませんけど。悩ましい。

Javaの例外のスタックトレースを操作する(しない)

 Javaで、例外を作成するメソッドを作りたくなることが数年に1回くらいある。で、大抵はそんなこと実はやらないほうがよくて、やらないんですけど、まあやることになったとします。やるなよ。

 そんでこんなメソッドを作る。

    private Exception makeException() {
        return new Exception();
    }

まあ本来はメソッドで切り分けようというくらいだから、例外のメッセージを作るのが面倒くさいとか、単純にnewで作って済まないとか、いろんな事情があるとは思うんですけど省略。

 このメソッドを、どこか別の機能から呼び出して使う。

    public void testFunction() {
        // 何か処理
        throw makeException();
    }

    private static Exception makeException() {
        return new Exception();
    }

こんな風にやった時、スタックトレースの一番上はtestFunctionになるのかmakeExceptionになるのか。
 つまりスタックトレースってThrowableオブジェクトがnewされた時に作られるのか、throwされた時に突っ込まれるのか。そういえば知らない。
 一応、Throwable#setStackTraceというメソッドがあるので、どちらも可能といえば可能。

 throwされた時に突っ込まれるなら例外の発生場所を正確に把握することができるんだけど、でもcatchした例外を再度throwしてもスタックトレースは変わったりしないので、実は答えは「newされた時」しかない。
 ただ、newされた時にスタックトレースが作られると、makeExceptionみたいなメソッドを作った時に、スタックトレースのトップがmakeExceptionになってしまう。
 いや別にmakeExceptionが一番上でもその次の要素を見ればいいじゃんという話もあるんだけど、ライブラリをいくつか経由してたりするとcauseのcauseのcauseとかになってたりして、下の方に行くほど重要な筈なのに何故か下に行くほど省略されていて、「え、スタックトレース4つしか表示されないの、これほとんどフレームワークの中じゃん」みたいになる。だからスタックトレースはできるだけ短くしておきたい。

 ということで、makeExceptionみたいなメソッドを作ろうとするとこういう感じになるわけです。

    private static Exception makeException() {
        Exception e = new Exception();
        StackTraceElement[] stackTrace = e.getStackTrace();
        e.setStackTrace(Arrays.copyOfRange(stackTrace, 1, stackTrace.length));
        return e;
    }

例外をnewした後にスタックトレースの一番上を取り除いて返す。これで例外の発生源はmakeExceptionの中ではなく、少なくともmakeExceptionを呼び出した場所になります。
 実際にはRuntimeExceptionを投げたくなったりとか色々あるのでGenericsを使って定義すると思うんですが、それはまあいい。
 途中でスタックトレースのサイズが1以上であることを決め打ちしたコードがあるけど、Throwableオブジェクトはnewした時点で絶対にスタックトレースは1以上になるので、まあいいじゃないですかこれくらい。length<=0でIllegalStateExceptionやErrorを投げるとかでもいいんですが。

 で、大抵はこれを使ってコードを書いている途中とかで、このメソッドが不要であることに気付いたり、余計に話をややこしくしている事に気付いたりして、もうちょっと上のレベルから設計を見直す事になります。
 気付かなかった事にして続けます。

 Exceptionの基底クラスのThrowableのコンストラクタの実装はこんな感じになっていて、

public class Throwable implements Serializable {
//(略)
    private static final StackTraceElement[] UNASSIGNED_STACK = new StackTraceElement[0];
//(略)
    public Throwable() {
        fillInStackTrace();
    }
//(略)
    public synchronized Throwable fillInStackTrace() {
        if (stackTrace != null ||
            backtrace != null /* Out of protocol state */ ) {
            fillInStackTrace(0);
            stackTrace = UNASSIGNED_STACK;
        }    
        return this;
    }    

    private native Throwable fillInStackTrace(int dummy);
//(略)
}

まあnativeの先までは見てないんですけど、dummyって何だよという。それはどうでもいいんですが、ここでおそらくスタックトレースを作っていて、いかにもスタックトレースが格納されていそうなstackTraceにはUNASSIGNED_STACKを入れている。このUNASSINED_STACKという定数は空配列なので、なんだこりゃということになる。

 getStackTraceの実装を見てみる。

    public StackTraceElement[] getStackTrace() {
        return getOurStackTrace().clone();
    }

    private synchronized StackTraceElement[] getOurStackTrace() {
        // Initialize stack trace field with information from
        // backtrace if this is the first call to this method
        if (stackTrace == UNASSIGNED_STACK ||
            (stackTrace == null && backtrace != null) /* Out of protocol state */) {
            int depth = getStackTraceDepth();
            stackTrace = new StackTraceElement[depth];
            for (int i=0; i < depth; i++)
                stackTrace[i] = getStackTraceElement(i);
        } else if (stackTrace == null) {
            return UNASSIGNED_STACK;
        }
        return stackTrace;
    }
//(略)
    native int getStackTraceDepth();
//(略)
    native StackTraceElement getStackTraceElement(int index);

getOurStackTraceを読んでいて、実際のスタックトレース取得処理はgetOurStackTraceがやっている。ちなみにprintStackTraceもgetOurStackTraceを呼んでいる。
 で、stackTraceが空じゃなければstackTraceを返して、空だった場合にはまたgetStackTraceDepthやgetStackTraceElementというnativeメソッドでどこからか取ってきている。
 なんでfillInStackTraceと同時にこの辺やらないんだろうと思ったけど、まあJNIの仕様的に配列をそのまま取得はできないし、コンストラクタで必ずfillInStackTraceが呼ばれるから内部状態エラーが起きる事もないし、ややこしいけどこれでいいんだろう。
 とにかく、これでめでたくスタックトレースJavaコードの中に出現することになりました。

 スタックトレースの構築方法がなんとなく想像ついたところで、よく見るとfillInStackはpublicメソッドなので、外から呼べる事に気づきます。というかjavadocに書いてあります。
 なので、なんか上の方で書いたmakeExceptionの中での怪しいスタックトレース操作なんてやらなくても、

    Exception e = makeException();
    e.fillInStackTrace();
    throw e;

とやれば、fillInStackTraceを呼んだ行が一番上に来るようなスタックトレースが構築されます。ただし、これだとfillInStackTraceを2回呼び出すことになるので、それが少々気になる。結構重いし。
 いや、そもそもこんなことやらなくていいんですけど。
 設計を見直そう。

 ところで、fillInStackTraceはfinalにはなっていないので、オーバーライドしてしまえばExceptionをnewしてもスタックトレースが作られなくなって、その分newがかなり軽くなります。
 スタックトレースが必要無い時に便利なテクニックです。

 Play FrameworkのHTTPレスポンスを返す時に使うRedirectやResultの基底クラスのFastRuntimeExceptionクラスは、こんな風にfillInStackTraceを潰してありますね。
play1/framework/src/play/utils/FastRuntimeException.java at master · playframework/play1 · GitHub
 かしこい。みんなもソース読もう。

 しかしこういう事をすると、前半でmakeExceptionの中でスタックのトップを消す処理をしていましたが、ああいうところでIndexOutOfBoundExceptionが出てしまうようになります。
 なのでスタックトレースが0やnullになることは無いなんて甘い予測を立てたりせずに、真面目にスタックトレースの深さをチェックしておいた方が良いでしょう。プログラミングって難しい。
 まあでもこの場合は、さすがにスタックトレースがnullになることはありませんよね。たぶん。いや、どうだろう。プログラミングって難しい。

ファイルとファイルディスクリプタとファイルのようなものたち

 プログラムからファイルを読もうとしたらどうするか。
 まずディスクを選んで、ディスクに書き込まれているファイルシステムを調べる。次にファイルシステムの管理情報に基づいて目的のファイルのデータブロックを探す。具体的にはファイルシステムの形式に従ってディレクトリを辿ったりテーブルを検索したりする。
 見つかったデータブロックからデータを読む。
 以上おわり。

 でも普通、というかLinuxMacでプログラムからファイルを読む時に、ディスクを指定したり、ファイルシステムの形式に従って管理情報やディレクトリの内容を読んだりはしません。
 大抵は、ファイルシステムext4(Linux)だろうがISO 9660(CD ROM)だろうがFAT(フロッピーディスクやメモリーカードなど)だろうが、だいたいパス名をopen()で指定して、read()で読むとかそういう感じだと思います。いわゆるopen/close/read/writeというやつ。

 これがどうなっているのかというと、単にファイルシステムのドライバがopen/close/read/writeという共通インタフェースを提供するようになっていて、ファイルシステム毎に適切なドライバを選んで呼び出しているというだけです。
 この共通インタフェースを仮想ファイルシステムといいます。

 ファイルシステムを仮想ファイルシステムから使えるようにする処理をマウントといいます。mountコマンドでやるアレです。
 マウントでは、ファイルシステムの種類やマウントポイントなどの情報をどこかのテーブルに登録しています。このテーブルはだいたいOSの内部にあって、あれ、なんでOS内にあるんだっけ? まあとにかくUnixLinux系OSでは大抵仮想ファイルシステムというのはOSカーネルに実装されています。FreeBSDのソースパス上でもsys/kern/vfs_xxx.cとかにあるので、結構中核にある感じですよね。まあ、権限とか排他制御の関係でOS内部にあるわけなんですが。
 そんなわけで、以降はUnix/Linux系OSでの実装を元に話します。

 マウントポイントというのは、マウントポイントです。
 Unix/Linux系OSの仮想ファイルシステムでは、ディスクやファイルシステムが違っていてもその全てのファイルシステムを"/"から始まるパス名で、あたかも一つのファイルシステムであるかのように扱っています。
 なので、mountコマンドを引数無しで実行すると出てくるように、"/"はext4でディスク1だけど"/var"はディスク2、"/mnt/cdrom"はISO9660でCD-ROMとか、そういうことになっています。open()でファイル名を指定した時に、このテーブルと照合されて、適切なデバイスとファイルシステムドライバが選択されるようになっています。

 つまり、open()というのは、「どのデバイスを」「どのファイルシステムドライバを使って」「どのファイルに対して」readやwriteなどの処理を実行するか、という情報を決定する処理なのです。
 そして、その対応表というか、決定した情報は、OS内部の管理表に登録されて、その表のエントリ番号が、いわゆるファイルディスクリプタになります。この管理表というやつがプロセスごとにあって、だから1プロセスが開くことができるファイル数の上限というものがあるわけですね。

 ところで、ファイルディスクリプタというものは、例えば標準入力は0、標準出力は1、標準エラー出力は2と決まっていたり、パイプやソケットのようなプロセス間通信やネットワーク通信なんかでも使いますし、その他にもデバイスを扱う時にも出てきて、こいつら全然ファイルじゃないじゃんみたいな事になります。
 ファイルディスクリプタというのは、要はデバイスやファイルシステムドライバやパス名を記録したエントリのキーでしかなくて、ここで登録されているパス名やデバイスというものは、別に具体的なディスクやファイルである必要は無くて、ファイルシステムドライバに、パス名でリソースを指定する機能(=open)と、readやwriteみたいな機能がありさえすれば良いわけです。
 あ、でもパイプやソケットはopen無いですね。まあこれらはパス名とは違う方法でリソースを指定してると思ってください。ソース見た方が早いしわかりやすいと思います。pipe()やsocket()の中で似たような事をやってます。

 要するにファイルシステムドライバにopenやreadやwriteという機能があれば、ファイルがあろうがなかろうが仮想ファイルシステムにマウントすることができます。
 そうやってマウントされたものの例としては、/devだったり/procだったり/sysだったりとか、ああいうOSにログインすると存在するんだけどディスク上には存在しないファイル群があります。

 /dev以下のファイルはハードディスクそのものとか、USBポートとか、コンソールとか、そういう入出力デバイスにつながっていて、これらに読み書きするとハードディスクに書き込んだり、USBポートに繋いだデバイスを制御できたりします。
 例えば以下のプログラムをLinux上で実行するとたぶんディスクの1セクタ目が読めます。Macだとパス名を"/dev/disk0"とかにするといいかも。あとたぶんroot権限が要ります。

#include <stdio.h>
int
main()
{ 
    int fd, i, size;
    unsigned char buf[512];
    fd = open("/dev/hda", 0);
    if (fd == -1) {
        perror("open");
        return -1;
    }
    size = read(fd, buf, 512);
    for (i = 0; i < size; i++) {
        printf("%02x ", buf[i]);
    }
    printf("\n");
    close(fd);
    return 0;
}

最後の2バイトが"55 aa"になっていたら、「このディスクから起動できます」みたいな印です。余談ですけど。

 それと、/dev/ttyというデバイスは、読むとキーボード、書くとディスプレイという事になっているので、さっきのプログラムのパス名を"/dev/tty"に変えて実行すると、改行なりCtrl+Dなりを実行するまでのキー入力をそのままprintf()で書きます。
 ソケットやパイプはちょっと面倒なんだけど、同じくファイルディスクリプタを取得してread/writeすると、他のプロセスやネットワーク上のどこかのデータを読んだり書いたり、つまりデータをやりとりすることができます。
 /procなんかは、OS内部のプロセス情報をあたかもファイルのように見せている。読むとOS内部のメモリから情報をとってきたりしています。/sysも同じ。

 これらも単にハードディスクやキーボードのドライバ、もしくはプロセス間通信のモジュールに、read/writeなどの機能が実装されていて、そのモジュールがファイルディスクリプタのテーブルに登録されているだけです。データのやりとり、入力出力というのは、突き詰めればread/writeがあればなんとかなる事が多いので、まあファイルと同じように扱えれば気楽だよねという発想なんじゃないかと思います。

 標準入力、標準出力、標準エラー出力は、プロセス生成時にあらかじめ適当なデバイスを開いておいたというもので、シェルのパイプやリダイレクトなんかはこの辺のファイルディスクリプタを別なものに付け替える処理をしています。

 FUSEなんかも、仮想ファイルシステムでマウントして使えるようなプログラムを書くためのフレームワークみたいなものです。プロセスをファイルシステムとしてマウントしているみたいな感じ。

 なんか何の話をしてるのかわからなくなってきた。
 ソースかなんか見ながらやった方が面白かったかな。

ディスク拡張したのにディスク容量が増えないんだけど(ファイルシステムの話)

 例えばAmazon EC2のEBSのように、ディスクが仮想化されるようになり、そうするとディスクの容量を拡張するというよくわかんない事がお手軽にできるようになり、「ディスク容量増やしたんだけどOSから見ると増えてない。なんで」みたいな事が起こり、「ディスク容量を増やしてもファイルシステムの容量は増えないんだよ」というのを説明するのも面倒くさい。
 良い時代になりました。

 まだ二次記憶装置はハードディスクが多いだろうし、単に私が知ってるというのと、私の習慣でディスクって言葉を使います。が、たぶんCDもDVDもSSDもだいたい同じことです。

 ディスクにはメモリと同じようにデータ格納位置にアドレスが振ってあって、メモリは1バイトごとにアドレスが振られていますが、ディスクの場合は512バイトごとに振らてています。なぜ512バイトなのかは知りません。この512バイトをセクタとかディスクブロックと呼びます。

 ディスクは大抵アクセスは遅いけど大容量で不揮発なので、消えると困るデータはだいたいディスクに保存する事になるんですが、どうやって保存するのか。
 どうやってもなにも単にディスク上のアドレスを指定して書き込めばいいんです。

 例えばディスクに書き込むdk_write(addr, data)みたいな関数があったとしたら

    dk_write(0, "Yahho-");

みたいにやればディスクの0番地にデータを書き込んで、めでたくあなたのコンピュータは起動しなくなります。まあ起動しなくなるかどうかは構成しだいではありますし、ここでは全然関係ないんですけど、単に0番地にはだいたいOSの起動に必要な情報が書かれている事が多いので。ブートセクタとかMBRってやつ。
 dk_writeの中では、out命令とかでハードディスクに対してアドレスとデータが格納されたメモリ上の位置を送信しているだけです。暇だったらATAデバイスドライバのコードとか見てみるとわかるかもしれませんが、DMAとか書き込みスケジューリングとか色々あってわかりづらいかもしれません。私は面倒なのでATAの仕様を調べました。

 それで、この関数があれば一応データは保存できるようになりますが、0番地にはこのデータ、1番地にはこのデータ、みたいに覚えておくのは大変だし、512バイトを超えるデータはどうするんだという話になります。まあ、512バイトを超える場合は連続した領域に書き込めばいいんですが、それにしてもじゃあどこからどこまでが一つのデータなのかを覚えないといけなくなるし、外部断片化が起きるようになります。

 なんにしてもディスク上のデータを管理するなんらかの機構が必要です。
 そのなんらかの機構というやつがファイルシステムです。

 つまり、ファイルシステムというのは、ディスク上のデータの『どこからどこまでがどのデータか』という情報を管理しておく機構で、そのデータの管理単位のことをファイルといいます。
 そして、ここまででOSの話がほとんど出てきていないことからわかるように、ファイルシステムというものはディスク上のデータ管理の話であって、本来OSとはあまり関係ありません。厳密にはというか、狭義では。
 そんなわけで、LinuxでもNTFS用のドライバがあればWindowsのファイルを読むことはできますし、逆にWindowsでもext4とかのドライバがあればLinuxのファイルを読むことができます。

 ファイルシステムがやっている管理というのは、本でいうところの目次を作っているようなものです。本にはページ1つ1つに番号がついていて、目次には章節のタイトルとページ番号の対応が書いてある。基本的にはそれと同じです。この目次に書いてあるタイトルが、ファイルシステムではファイル名になります。

 具体的に、UFS/FFSやext2のようなやつの話をします。以降はFFSの事だと思ってください。FAT系やNTFSは全然違うみたいですし。
 で、FFSでは、ファイルシステムではディスクをデータ用の領域と管理用の領域とに分けています。

 データ用の領域には、データが格納されています。そのまんまですが、データ、つまりファイルの中身です。中身は中身だ。それ以上の情報は無い。
 あ、一応『ここでデータは終わりです』みたいな情報は入っています。
 ファイルシステム上のデータは、ディスク上の複数のセクタをまとめたブロックで管理されています。なぜまとめるかというと、ディスクは物理的な特性上、連続したセクタを読むのは速いけども飛び飛びの領域を読むのは非常に遅いので、できるだけデータはまとめておきたい。512バイトでは小さすぎる。それで4KBなり8KBなりのサイズでまとめて管理することで、読み書きを高速化しようとしています。
 ただあまり大きいと内部断片化か起きるので、適当に小さい必要もあって、その速さと空間効率のバランスを取った結果が8KBくらいだったらしいです。
 ちなみに2004年頃に発表された資料によると、初期のGoogle File Systemのブロックサイズは4MBだったらしいです。当時はデカいと思ったけど、今はそうでもないような気もするけども、アクセス速度を考えるとやっぱりデカい気もする。どうせストライピングしてるだろうから問題ないのかなぁ。おそらく今も変わらないか、小さくなるか、逆にめちゃくちゃデカくなってるか、いやよくわかんない。
 4KBやその倍数や4MBなのはメモリ管理の都合じゃないかな。メモリ管理的に、4KBより小さいのや4MBより大きいのは、現状ではあまり意味が無い気がします。

 管理用の領域には、ファイルシステムの大きさや種類などの基本的な情報や、inodeが格納されています。
 inodeというのは、目次の1行みたいなものです。ファイルのサイズ、中身のデータの格納場所(ブロック番号のリスト)、ファイルの種類(通常ファイル、ディレクトリ、シンボリックリンクなど)、パーミッションなどが記録されたテーブルです。基本的に1エントリが1ファイルを表しています。ただここにはファイル名は入っていません。
 inodeのエントリは固定長なので、番号でアクセスすることができます。この時の番号をinode番号と呼びます。inode番号は"ls -i"などのコマンドで見ることができます。
 ちなみにinodeはIndex-Nodeの略だという人もいますが、実のところは何の略だかよくわかってないらしいです。どうでもいいですけども。

 で、ディレクトリというのがあります。
 ディレクトリというのは、ファイルの一種です。
 ディレクトリ・ファイルは、簡単に言うとファイル名とinode番号が格納されたテーブルです。ファイル名はディレクトリに保存してあります。
 つまり、"/bin/ls"というファイルを読もうとした時には、以下の様な手順を踏みます。
(1) "/"のディレクトリを指すinodeを読む("/"のinode番号だけはファイルシステムの管理領域に書いてある)
(2) (1)で読んだinodeから"/"ディレクトリの内容が格納されたブロックを特定し、読む
(3) (2)の内容から、"bin"のエントリを見て、"bin"のinode番号を特定する
(4) "bin"のinodeを読む
(5) "bin"の内容が格納されたブロックを読む
(6) (5)から"ls"のエントリを見て、"ls"のinode番号を特定する
(7) "ls"のinodeを読む
(8) "ls"の内容が格納されたブロックを読む(終わりだよ〜)

 まあ結構面倒くさいわけです。DNSとだいたい同じです。
 ただ、これでファイル名があればファイルの中身にたどり着くことができるようになりました。
 おおむね、ファイルシステムはこんな感じの仕組みになっています。

 このような仕組みになっているので、以下の様な現象が起きます。

 lnコマンドで作れるハードリンクというものがあります。これはファイルの実体は同じものなんだけど、パスや名前が違うファイルを作ることができる機能です。これは違うファイル名に対応するinode番号を同じものにすることで実現することができます。
 ハードリンクはどっちがリンク先リンク元という区別はなくなり、どちらもまったく同じ立場のファイルになります。
 ただし、inode番号というのは定義上ファイルシステムに固有のものなので、違うファイルシステム上のファイルにハードリンクを張ることはできません。

 mvコマンドによるファイルの移動は、同じファイルシステム内であればinode番号は変わらずに、移動元と移動先のディレクトリの情報を書き換えるだけです。ディスク上のデータの格納位置も変わりません。

 ファイルの削除というのは、ディレクトリ内のファイル名とinode番号のエントリを削除したり、inodeからブロック群への参照を削除したりする処理です。『ファイルを削除してもデータは残る』と言われる所以はこれです。それと、ファイル削除のことをunlinkと言ったりするのもこれが理由です。というか、ファイルシステムのインタフェース的にも削除じゃなくてunlinkです。複数のハードリンクがあると消えませんし。

 ところでパーティション(スライス)というのはディスクを複数に分割する機能で、それぞれに異なるファイルシステムを構築することができます。
 なのでパーティションが違うと違うファイルシステムとみなされて、ハードリンクを張ることはできません。
 また、mvコマンドでディスクやパーティションをまたいでファイルを移動させようとすると、移動ではなくてコピーになります。いや最近はコピーした後に移動元を削除してくれますけども。

 ちなみにシンボリックリンクというのはリンク先のファイル名が書かれたファイルなので、リンク先が他のパーティションだろうが他のホストだろうが一時的にしか存在してなかろうがファイル名でアクセスできる限りなんでもアクセスできますが、ファイル名が変わったりするとアクセスできなくなります。

 で、えー、なんだ。話を戻すと、
 ファイルシステムは管理領域とデータ領域に分かれていて、管理領域の中のinodeは固定長で、番号でアクセスできるようになっている。データ領域も8KBだかのブロックに分かれていて、これも番号が振られている。
 この構造自体がディスクに書き込まれている情報なので、一度作ってしまうと、例えばデータ領域に一切変更を加えずに管理領域の容量を増やしたり減らしたりするのは難しい。管理領域のサイズやデータ領域の構造に少しでも変更を加えると全データが使えなくなる可能性がありますし、普通は管理に必要な領域以外は全部データ領域にするので、変更の余地が無いわけです。
 この、ファイルシステムの構造をディスクに書き込む処理の事を『フォーマット』といいます。ファイルシステムの構造を作るのでフォーマットといいます。フォーマットでファイルシステムの内容が全部消えるのはこのせいです。

 そして、inodeは中盤でちょっと触れたように、1エントリが1ファイルに対応しています。そして、ファイルシステム作成時に管理領域の大きさも決めてしまわないといけないので、inodeのエントリ数もファイルシステム作成時に決めてしまわないといけなくなります。
 つまり、ファイルシステム作成時に、そのファイルシステムで管理できるファイル(ディレクトリ等含む)の最大数を決める必要があります。inode数を多くするとファイル数を増やせますが、管理領域が増えるのでデータ領域が減ります。小さいファイルを多数作るならそれもアリでしょう。

 また、データ領域のブロックサイズも作成時に決める必要があります。
 これもまた、大きいと読み書きは速くなりやすくなりますが、小さいファイルが多いと内部断片化しやすくなります。小さいと効率は良くなりますが、大きいファイルを扱う時に外部断片化が起きやすくなって、アクセスは遅くなりやすくなります。デフラグすりゃいいんですが、大変ですし。
 デフラグというのは、この場合は飛び飛びになったデータを並び替えてできるだけ一箇所にまとめることです。一箇所にまとめた方がディスク上の効率が良い。SSDだと関係ないんですけど。

 そんなわけで、実はファイルシステムを作る時というのは結構色々な事を考えないといけないのです。どれくらいの大きさのファイルをどれくらいの数作るか。
 最近はディスクが安いからかなんなのか、そこら辺はあまり神経質に考える必要がなくなっています。が、mkfsコマンドのオプションを見るとまあ一応色々細かく指定することができるようにはなっています。
 昔はOSをインストールするといったらこんな風にまず用途を考えてファイルシステムのパラメータを色々と調整するところから始めないといけなかったそうです。時代は変わって、特に何も考えなくてもOSをインストールできるようになりました。素敵ですね。

 そう、で、だから、ディスク容量を増やしてもinodeの数も増えないしデータ領域のブロック数も増えないからファイルシステム上の容量は増えないわけです。
 ただ、これらの管理情報を後から拡張する方法がファイルシステムに用意されていれば容量を増やすことはできますし、単に新しく作ったファイルシステムに情報をコピーしても増やすことはできます。どちらが簡単かはそれこそファイルシステムによりますね。
 減らすのが難しいのもこういう構造だからです。

 それと途中で「狭義のファイルシステム」みたいな事を書いたと思いますが、じゃあ広義ってなんだというと、ディスク上の構造ではなく、インタフェースとしてのファイルシステムという概念があります。
 この辺から、OSとかユーザ寄りの話になってきて、これはこれでかなり面白いんですが、別の話なので別な時に。
 キーワードは仮想ファイルシステムとか、まあ単にファイルシステムでもいいです。あとはreadカーネルコールの処理を追いかけてみるとか。