2018/02/21


Twitter で「読みたい」と呟いたら著者の武内覚さんから献本しましょうかとお声を掛けて頂いたので即答でお願いしました。

僕はいつも Linux でしか動作しないソフトウェアを Windows に対応させるパッチを書いたりしているので、普段 Windows しか触っていないと思われがちですが、実は僕が Linux を触り始めたのは 1996 年にトッパンから出版された「Linux 入門」くらい昔だったりします。ちょうど Linux 2.0 が出た頃だったと思います。その頃の Linux はようやく SMB カーネルが出た頃で、まだまだお遊び感のある OS で不安定でもありました。ディストリビューションもほぼ Slackware くらいしか無かったかもしれません。

あの頃の Linux はインターネットを検索しても殆ど情報が出て来ず、本気で調べるにはソースコードを読むしかありませんでした。当時、父親から譲り受けたノート PC に躊躇いもなく Linux を入れたはいいが、SiS チップセット用ディスプレイドライバが OpenGL を実行すると重たくなるという現象に出くわし、なんとか問題を解消したくて何日も何日もソースを読んでハックしていました。あれから比べれば Linux は巨大になりました。ユーザ数も信じられないくらい増えました。今やサーバといえば Linux が一般的ですし、こんなに手軽に Linux が起動出来る世の中になるとは思っていませんでした。そして何よりもインターネットには様々な Linux の情報が溢れる様になりました。

ただしブログ等で情報が溢れた事で、適当な情報を鵜呑みにしてしまいやすいという悪い側面も出て来てしまいました。正しい情報とは実際に手を動かした人から得たい物です。また、知らない事を解決するのにインターネット検索を使ってしまうと、その知りたかった情報だけを得てしまうので、知りたくなかった情報には行き届かない事もあります。

例えば UNIX の time コマンドは引数のコマンドを実行した後 real、user、system の数値を表示しますが、個々の意味をちゃんと理解していないプログラマも少なからずいるのではないでしょうか。free コマンドの出力を見て、今 OS がどういう状態なのか理解していない人もいるのではないでしょうか。理解したつもりになっているだけだったりしないでしょうか。

最近の OS は色々な最適化が施されておりそれ故に複雑になってきています。プログラムが遅いという事象だけから原因を突き止めるのは本当に難しいです。だからこそボトルネックを疑う為の材料が多く必要なのです。知識がないと高価な CPU やメモリを買い足すことしか出来なくなります。

本書はこういった基本的な知識を埋める所から章が始まります。今までなんとなくしか理解していなかった Linux の内部を、図解で細かく説明してくれています。OS 上でメモリがどの様に管理されているのか、OS はファイルシステムをどの様に扱っているのか等を、誰でも分かる(であろう)レベルで書いてあります。

こういった書籍の多くは、難しい用語を並べて読者を振り切ってしまう事が多いのですが、本書では始めの数章を使って知識を固め、徐々に知識レベルを上げていってくれているお陰で挫折する方も少ないのでと思います。

ちなみに僕が特にワクワクしたのは第8章の「ストレージデバイス」について書かれている章でした。検証コードを使ってシーケンシャル/ランダムアクセスを HDD と SDD の両方で試し、read/write がサイズの変化によってどのくらい性能に影響が出るかを実験しています。まだ読んだだけで試していないので、後で実際に試してみようと思っています。

この他にも本書では検証コードを使った実験が沢山あります。「これこんな事やったらどうなるんだろ」と思っていた実験も自分でソースをコンパイルして試せる様になっています。おそらく読者が実行する環境で結果も異なってくるでしょう。だからこそ実際に手を動かして得られた情報は貴重なのです。本書はこの様な知見の詰まったとても良い本だと思いました。


2018/01/24


今日たまたま見つけた gotest というプログラムの修正を行った際にドハマリした。

GitHub - rakyll/gotest: go test with colors
https://github.com/rakyll/gotest

gotest は go test の出力の PASSFAIL といった定型の文字列を見つけて緑や赤に色付けする小さなプログラム。仕組みも簡単で以下の様なコードになっている。

func main() {
    setPalette()
    enableOnCI()
    gotest(os.Args[1:])
}

func gotest(args []string) {
    r, w := io.Pipe()
    defer w.Close()

    args = append([]string{"test"}, args...)
    cmd := exec.Command("go", args...)
    cmd.Stderr = w
    cmd.Stdout = w
    cmd.Env = os.Environ()

    go consume(r)

    if err := cmd.Run(); err != nil {
        if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
            os.Exit(ws.ExitStatus())
        }
        os.Exit(1)
    }
}

func consume(r io.Reader) {
    reader := bufio.NewReader(r)
    for {
        l, _, err := reader.ReadLine()
        if err == io.EOF {
            return
        }
        if err != nil {
            log.Fatal(err)
        }
        parse(string(l))
    }
}

cmd.Run() はブロックするので gorutine で bufio.Reader を読みながら色付けしてる。ただこれ、cmd.Run() のブロックが解け、os.Exit() が呼び出されるまでに goroutine 側が処理しないと出力をロストしてしまう事になる。そこで僕は以下の様に修正した。

func main() {
    setPalette()
    enableOnCI()
    os.Exit(gotest(os.Args[1:]))
}

func gotest(args []stringint {
    r, w := io.Pipe()
    defer w.Close()

    args = append([]string{"test"}, args...)
    cmd := exec.Command("go", args...)
    cmd.Stderr = w
    cmd.Stdout = w
    cmd.Env = os.Environ()

    var wg sync.WaitGroup
    wg.Add(1)
    defer wg.Wait()

    go consume(&wg, r)

    if err := cmd.Run(); err != nil {
        if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
            return ws.ExitStatus()
        }
        return 1
    }
    return 0
}

func consume(wg *sync.WaitGroup, r io.Reader) {
    defer wg.Done()
    reader := bufio.NewReader(r)
    for {
        l, _, err := reader.ReadLine()
        if err == io.EOF {
            return
        }
        if err != nil {
            log.Print(err)
            return
        }
        parse(string(l))
    }
}

sync.WaitGroup を使い、goroutine の終了を待ち、gotest の関数の戻り値として外部コマンドの終了コードを渡し、main 関数で os.Exit() を呼び出す。Go 言語を良く使う方なら良く見る、そして割かし綺麗と言われるソースの書き方だと思います。ただこのコード、以下の様に panic で落ちます。

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc0420083fc)
        c:/go/src/runtime/sema.go:56 +0x40
sync.(*WaitGroup).Wait(0xc0420083f0)
        c:/go/src/sync/waitgroup.go:129 +0x79
main.gotest(0xc04203e260, 0x0, 0x0, 0x1)
        c:/dev/go/src/github.com/rakyll/gotest/main.go:54 +0x3bf
main.main()
        c:/dev/go/src/github.com/rakyll/gotest/main.go:33 +0x74

最初これを目にした時には直ぐに原因が分からず「これは Go のバグだ。issue を書くぞ!」という行動を起こす手前で原因に気がついた。

このコード、処理される順に着目して欲しい。io.Pipe() で作られた書き込み用のパイプ w は、関数 gotest を抜ける際に Close される。goroutine consume を待機する為の wg は関数 gotest を抜ける際に Wait が呼び出される。関数 consume 内の reader.ReadLine は、パイプ w が Close されないとブロックを解かない。なので w.Close() が先に呼び出される必要がある。そう、これデッドロックでした。defer 文は、関数内で defer を指定した順とは逆に実行されるので、本来ならば

  • cmd.Run() のブロックが解ける
  • w.Close() が呼び出される
  • wg.Wait() を呼び出す

という順にしないといけない訳です。以下の様に修正して正しく動作する様になりました。

func main() {
    setPalette()
    enableOnCI()
    os.Exit(gotest(os.Args[1:]))
}

func gotest(args []stringint {
    var wg sync.WaitGroup
    wg.Add(1)
    defer wg.Wait()

    r, w := io.Pipe()
    defer w.Close()

    args = append([]string{"test"}, args...)
    cmd := exec.Command("go", args...)
    cmd.Stderr = w
    cmd.Stdout = w
    cmd.Env = os.Environ()

    go consume(&wg, r)

    if err := cmd.Run(); err != nil {
        if ws, ok := cmd.ProcessState.Sys().(syscall.WaitStatus); ok {
            return ws.ExitStatus()
        }
        return 1
    }
    return 0
}

func consume(wg *sync.WaitGroup, r io.Reader) {
    defer wg.Done()
    reader := bufio.NewReader(r)
    for {
        l, _, err := reader.ReadLine()
        if err == io.EOF {
            return
        }
        if err != nil {
            log.Print(err)
            return
        }
        parse(string(l))
    }
}

いやはやお恥ずかしいというかなんというか。本来なら無限ループしてもおかしくない所をちゃんと「deadlock」ってメッセージ付きで panic になってくれたんですから、Go 言語素晴らしいって話でした辛い。


2018/01/17


仕事でサーバを運用していると某国からのアタックがそこそこ多いのだけど、出来れば早急にブロックしてしまいたかったので IP アドレスから whois を引いて CIDR 形式で表示出来るツールを作った。昔はこういうの Perl で書いた気がする。

GitHub - mattn/iputil
https://github.com/mattn/iputil

ライブラリとして作ったけど、コマンドも用意してあります。

$ go get github.com/mattn/iputil/cmd/iprange

実行結果のイメージはこんな感じ。

$ iprange [IPアドレス]
XXX.XXX.XXX.XXX/16
YYY.YY.YYY.YY/16
...

あとはこの結果を使って ufw (Universal FireWall) を使っているのであれば

$ iprange [ヤバいIPアドレス] | xargs -n 1 sudo ufw insert 1 deny from

とかすれば一発でアクセス元をブロック出来る。(はず)

みんなのGo言語[現場で使える実践テクニック] みんなのGo言語[現場で使える実践テクニック]
松木雅幸, mattn, 藤原俊一郎, 中島大一, 牧大輔, 鈴木健太
技術評論社 / (2016-09-09)
 
発送可能時間: