kakts-log

programming について調べたことを整理していきます

開発におけるドキュメント運用の理想状態と課題

概要

開発におけるドキュメントの運用方法について、自分なりの理想状態についてまとめます。

簡単にまとめると以下の3つとなります。

  1. ドキュメントが容易に見つかる
  2. ドキュメントの正確性
  3. ドキュメントの抜け漏れがないこと

上から順に実行難易度が下がり、容易に対策ができるはずで上から実行することを目指したいと思っています。
もっと項目を洗い出して整理はできると思いますが、一旦は上記を考えています。

それぞれの理想状態の項目についてと、それを実現するにあたり現状課題になりそうなポイントを整理します。

1. ドキュメントが容易に見つかる

  • ドキュメントが書かれているが、歴史的な経緯でドキュメントの場所や管理システム自体がバラバラになったりする。
  • 管理システムの移行が進まずに、古いドキュメントは過去に管理していたドキュメント管理システム上で残り続けてしまう。
  • 結果としてメンバーによってドキュメントの場所がわからなかったり、コードや他のドキュメントでのリンク切れが起きる。

対策

  • GitHubリポジトリのトップディレクトリのREADME.mdなど、リポジトリを最初に見た人が容易に見つけて辿れる状態にする 
    関連する全てのドキュメントはREADMEから容易に辿ることができる。
    リポジトリ外でドキュメント管理している場合でも、リンクを貼るだけでも効果があるため必ずやっておきたい。

  • ドキュメントの階層の構造化
    ドキュメントの置き場所と階層が構造化され、どの機能・システムに関するドキュメントがどこに書かれているか場所が推測できること
    また、その構造化についての情報も開発者のREADMEにまとまっていること
    これにより、近い将来LLMを使ったシステムでドキュメントを扱いやすい状態にし、chatbotなどで聞けば答えれるようにして、そもそもドキュメントを探さない状態に持っていく

2. ドキュメントの正確性

どこで書くか、どういう粒度で決まっており、特定のトピックに関しては特定のドキュメントで運用し続ける状態を目指す。
これが整っていると、メンバーによって同じ内容を別々の場所で書いてしまったり、内容の重複を防ぎ、運用がより回ると考えています。

対策

  • ドキュメントを書く粒度やテンプレートなどが定まっていること
  • ドキュメントの変更履歴がわかるシステム上で管理すること

3. ドキュメントの抜け漏れがないこと

開発案件の大小や、その時の開発チームのメンバーのタスク量によって、ドキュメントを書く・書かないがぶれてしまうことがある

対策

  • そもそも設計・開発においてドキュメントドリブンでの開発体制を敷く
    • 開発担当者個人の頭の中で設計をするのでなく、まずドキュメントにまとめて叩き台を作り、それに対して周りのメンバーのレビューを実施する
      • これによって設計の抜け漏れをなくす。
      • また、メンバー間でのシステム設計の理解度を揃えることができる。

ドキュメント運用上の課題

前述した運用体制を敷いた際に、個人的に運用上の課題になりそうなポイントがいくつかあるので、それについての課題を整理します。

リポジトリを跨いだ大きなシステムの全体構成・設計について

複数リポジトリにまたがるシステムに関するドキュメントはの運用をどうするか
あくまでGitHub内にとどめるなら、該当のGitHub org内にドキュメント用のリポジトリをまとめ、そこで管理するのが良いのか

システムに関する変遷について

開発初期から、アーキテクチャ変更など、途中段階も含めた一連のストーリーを理解する資料があると良さそう
GitHubリポジトリなどではコミットログなどから変更履歴はたどれるが一連の変遷を理解できるようにするとシステムの理解につながると思う。
-> システム上の意思決定の変更についてはArchitectural Decision Records(ADRs)での運用が最適?

さいごに

ドキュメントについて個人的な理想状態と課題を整理しました。

開発においてドキュメントが容易に見つかり、それらのドキュメントが常に正しい内容を維持しており、抜け漏れがない仕組み・体制を作れれば、開発メンバーのキャッチアップが容易になり、開発における心理的不安や、ドキュメント駆動でシステムの設計・開発品質向上のためのメンバー間でのコミュニケーション活発化を目指せるはずです。

チームのメンバーでそれぞれ理想状態と運用コストに関する考え方も異なるので、度々メンバー間で話して、運用方法をブラッシュアップできればと思います。

google/wireでDIする際のgenerics対応について

概要

この記事はQiita Advent Calendar 2023 Go言語の第十七日の記事です。

qiita.com

google/wireについて

github.com

google/wireとは、GoでのDependency Injection用のコード生成ツールとなります。
wire自体の使い方は、公式のチュートリアルとしてまとめられているため、そちらをご確認ください。 github.com

google/wireの現状について簡単にまとめると以下の通りになります。

Wire is currently in maintenance mode (i.e. not accepting new features) and investigation is needed for how much work is involved. Full support of go generics will probably not land anytime soon, but we welcome comments and fixes.

Goにおけるgenerics対応済みのDIツールは他に存在しますが、今回はgoogle/wireにおけるgenericsを扱う場合のワークアラウンドの方法についてまとめます。
 

genericsを用いたコードのDIでエラーが出る例

雑コードですが、下記のV interfaceを定義し、 Message 構造体にint型のフィールドを定義します

type V interface {
    int | float32
}

func NewV[T V]() T {
    return T(2)
}
package modelgen

import "fmt"

type Message struct {
    Msg string
    V   int
}

func NewMessage(v int) Message {
    return Message{Msg: "Hi there!", V: v + 1}
}

type Greeter struct {
    Message Message // <- adding a Message field
}

func NewGreeter(m Message) Greeter {
    return Greeter{Message: m}
}

func (g Greeter) Greet() Message {
    return g.Message
}

type V interface {
    int | float32
}

func NewV[T V]() T {
    return T(2)
}

type Event struct {
    Greeter Greeter // <- adding a Greeter field
}

func NewEvent(g Greeter) Event {
    return Event{Greeter: g}
}

func (e Event) Start() {
    m := e.Greeter.Greet()
    fmt.Println(m.Msg, m.V)
}

これをDIさせるために、wire.goで下記のように設定します。 Vインタフェースの実装に関するプロバイダ関数であるNewVをwire.Buildに型パラメータの指定なしで渡してみます。

func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, NewV)
    return modelgen.Event{}
}

この状態でwireコマンドを実行すると、下記のエラーとなり、型パラメータTが指定されていないとエラーが出ます。

$ make wire
wire
wire: /Users/hoge/Documents/code/go-wire-sandbox/wire.go:128:2: cannot infer T (/Users/hoge/Documents/code/go-wire-sandbox/src/modelgen/generics_models.go:30:11)
wire: generate failed
make: *** [wire] Error 1

次に、NewV[int]と型パラメータTにintを指定すると、unknown patternエラーがでて、 wireでは認識できないシンタックスエラーとなります。

// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, modelgen.NewV[int])
    return modelgen.Event{}
}
➜  go-wire-sandbox git:(main) ✗ make wire
wire
wire: /Users/hoge/Documents/code/go-wire-sandbox/wire.go:128:74: unknown pattern
wire: github.com/kakts/go-wire-sandbox: generate failed
wire: at least one generate failure
make: *** [wire] Error 1

解決策: 型パラメータを指定してDIさせる方法

上記の問題がありますが、下記の通り、型パラメータを指定して関数を呼び出した結果を返すプロバイダー関数(NewVInt)を定義し、それをwire.Buildに渡してあげると解決します。

func NewVInt() int {
        // intを型パラメータに指定してNewVを実行し、その戻り値を返す
    return modelgen.NewV[int]()
}

// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    wire.Build(modelgen.NewEvent, modelgen.NewGreeter, modelgen.NewMessage, NewVInt)
    return modelgen.Event{}
}
$ make wire
wire
wire: github.com/kakts/go-wire-sandbox: wrote /Users/hoge/Documents/code/go-wire-sandbox/wire_gen.go

wireによって生成されたコードは下記のようになり、 型パラメータで指定したint型の値をNewMessageの引数に渡していることを確認できます。

func NewVInt() int {
    return modelgen.NewV[int]()
}


// Generics動作確認用
func InitializeGenericsEvent() modelgen.Event {
    int2 := NewVInt()
    message := modelgen.NewMessage(int2)
    modelgenGreeter := modelgen.NewGreeter(message)
    event := modelgen.NewEvent(modelgenGreeter)
    return event
}

これでwire.Buildでgenericsを使ったプロバイダー関数を定義し、型パラメータを指定した状態でDIさせることができました。

最後に

google/wireにおけるgenericsの対応についての説明は以上となります。

序盤の概要にまとめましたが、google/wireは現在メンテナンスモードになっており、今後のgoの新機能に追従することはないようです。しかし上述したようにgenericsをどうしても使いたい場合の解決策があることがわかりました。

今後新規で開発をする場合は、運用上困らないように他のDIツールの検討をしてみても良いかもしれません。

備考リンク

Update dependencies to support Generics by efueyo · Pull Request #360 · google/wire · GitHub

C: Pthreadによるスレッド作成とデタッチ

概要

CにおいてPthread(POSIX Thread library)ライブラリを使い、pthread_create()によりスレッドの作成をした後、別のスレッドからpthread_join()を実行し、作成したスレッドの終了を待ち、終了状態を得ることが可能です。

場合によっては、作成したスレッドの終了状態を得る必要がない場合もあります。
この場合はスレッドの終了状態が残り続けるよりも、システム側が終了したスレッドを自動的に破棄する方がよいです。
そこで、pthread_detach()を実行することで、スレッドをデタッチ、つまりスレッドの終了状態を破棄することができます。

ここでは、Pthreadのpthread_create()を用いた基本的なスレッド作成方法について説明した上で、pthread_join()によって指定したスレッドの終了を待つ方法や、pthread_detach()を実行した際に、別スレッドからpthread_join()を実行しても終了状態が取れなくなることを確認します。

pthread_create()によるスレッドの作成

pthread_create()について

pthread_create()により、実行したプロセス内でスレッドを新規作成します。

pthreadライブラリでの関数定義は下記のとおりです。 詳細はリンクを確認ください。
Ubuntu Manpage: pthread_create - create a new thread

       #include <pthread.h>

       int pthread_create(pthread_t *restrict thread,
                          const pthread_attr_t *restrict attr,
                          void *(*start_routine)(void *),
                          void *restrict arg);

新規作成されたスレッドは、第3引数のstart_routineで指定した関数を実行します。 第4引数に、start_routineの関数の引数を指定します。

スレッドの作成例

ここではシンプルに、pthread_create()を実行してスレッドを作成する例を示します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed\n");
        return -1;
    }
    printf("pthread_create succeeded\n");
    return 0;
}

基本的なスレッドの作成方法はこのとおりです。

スレッドの終了を待つ。

前項で紹介したコードは、main()でスレッドを作成して、指定した関数を実行したシンプルな処理となります。
fork()におけるwait()のように、スレッドにおいても、作成したスレッドの終了を待って、終了状態を得るためのpthread_join()関数が用意されています。
作成されたスレッドとは別のスレッドから実行することで、該当のスレッドの終了を待つことができます。

pthread_join()について

pthread_join()を別のスレッドで実行することで、指定したスレッドの終了を待って、終了状態を得ることができます。

Ubuntu Manpage: pthread_join - join with a terminated thread

       #include <pthread.h>

       int pthread_join(pthread_t thread, void **retval);

第1引数 threadに、終了を待つ対象のスレッド情報を渡します。 第2引数には、 スレッドによって実行された関数の戻り値を受けるポインタを渡します。

戻り値は、0ならば正常で、それ以外の場合はerrnoを示します。

スレッドの終了後、pthread_join()を実行するまで、システム内部でこの終了したスレッドの情報が残り続けます。 スレッドで実行させる処理によっては、pthread_join()でスレッドの終了を待つ必要がない場合があり、その場合は終了したスレッドの情報は不要となります。

不要な情報が残っているとシステムでリソースを消費してしまうため、スレッド終了時に破棄させるように設定することで、この問題を解決できます。
スレッドの情報を破棄させる方法の1つとして、pthread_detach()の実行があります。

pthread_detach()について

pthread_detach()は、スレッドに割り当てられるリソースが、終了時に回収可能であることを知らせるために使われます。

Ubuntu Manpage: pthread_detach - detach a thread

       #include <pthread.h>

       int pthread_detach(pthread_t thread);

引数に、対象のスレッドの情報を渡します。

前述したように、スレッド終了後に他のスレッドが終了状態を必要としない場合に使用するべきものです。

スレッドのデタッチする・しない場合の挙動について

ここで、スレッドを作成後、スレッドをデタッチした場合とそうでない場合にどういう挙動になるかを説明します。

例1 スレッドをデタッチせず、pthread_join()を使った場合

こちらはスレッドを利用した際に、終了状態を待つ必要がある場合の一般的な例となります。

大まかな流れは下記のとおりです。

  1. メインスレッドでptheread_create()を実行し、スレッドを作成
  2. 作成されたスレッドではpthread_create()で指定した関数を実行し、終了する
  3. メインスレッド側で2のスレッドの終了をpthread_join()を実行して待つ

この流れで、3でメインスレッド側でpthread_join()を実行し、スレッドが終了するまで待ち、スレッドの終了状態を取得します。


#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        printf("pthread_create failed");
        return -1;
    }
    // 作成したスレッドが終了するのを待つ
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // スレッドによって実行された関数の戻り値を&resに格納する
    s = pthread_join(t1, &res);
    if (s != 0) {
        printf("pthread_join failed");
        return -1;
    }

    printf("Thread returned %ld\n", (long) res);
    return 0;
}

main()関数でスレッド作成後、sleep(3)を実行し、作成したスレッドが終了するのを待った後、pthread_join()を実行します。 第2引数に渡したresに関数の実行結果が渡るので、実行後その結果を表示しています。

# ./simple_thread
Hello world
Message from main()
Thread returned 12

スレッドの関数に Hello world\n を渡したので、その分の文字列長を表示できているのがわかります。

例2 自スレッドで終了を待たずにpthread_detach()を実行した場合

作成したスレッド側で、終了前にpthread_detach()を実行させた後、別のスレッドでpthread_join()をした際にどうなるのかを確認します。

例1とは大きくコードは変わらず、pthread_create()で指定した関数内で、終了前にpthread_detach()を実行しています。
自スレッドで実行するため、引数には時スレッドの情報を取得するpthread_self()を渡します。

#include <pthread.h>
#include <stdio.h>

static void * threadFunc(void *arg)
{
    char *s = (char *) arg;
    printf("%s", s);

    int res;
    // 自スレッドをデタッチする
    res = pthread_detach(pthread_self());
    if (res != 0) {
        print("pthread_detach failed");
        return -1;
    }
    
    return (void *) strlen(s);
}

int main(int argc, char *argv[])
{
    pthread_t t1;
    void *res = NULL;

    int s; 

    // スレッドの作成
    // 作成されたスレッドは第三引数の関数に第4引数の値を渡して実行する
    s = pthread_create(&t1, NULL, threadFunc, "Hello world\n");

    // スレッド作成失敗
    if (s != 0) {
        errExitEN(s, "pthread_create");
    }
    sleep(3);
    printf("Message from main()\n");

    // 作成したスレッドの終了を待つ
    // 作成したスレッドが実行する関数内でpthread_detach()を実行しているため
    // ここでエラーが出る
    s = pthread_join(t1, &res);
    if (s != 0) {
        errExitEN(s, "pthread_join");
    }

    printf("Thread returned %ld\n", (long) res);
    exit(EXIT_SUCCESS);
}

この例だと、スレッドで実行する関数でpthread_detach()を実行しており、 メインスレッド側でpthread_join()を実行する際にはすでに作成したスレッドはデタッチされているため、スレッドの終了状態が取れずにエラーとなります。

# ./simple_thread_detach 
Hello world
Message from main()
ERROR [EINVAL Invalid argument] pthread_join

実行結果はこのとおりです。

pthread_create()を実行時にスレッド属性としてデタッチをするように設定もできますが、今回はpthread_detach()によるスレッドのデタッチを行い、デタッチした際の挙動について確認できました。

stdioバッファの方式について

概要

fork()による子プロセスを作成するプログラムにおいて、printfなどのstdioライブラリの関数を実行した場合、標準出力が端末とファイルの場合で出力結果が異なる場合があります。
この挙動について整理します。

(参考: LINUXプログラミングインタフェース25.4)

fork()とprintf()を利用した例

下記のように、fork()の前にprintfで文字列出力させ、その後fork()によって子プロセスを作成するプログラムを例とします。

#include "tlpi_hdr.h"
#include <stdio.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    printf("Hello world\n");
    write(STDOUT_FILENO, "Ciao\n", 5);

    if (fork() == -1)
        errExit("fork");

    // 親子プロセスともにここまで実行する
    exit(EXIT_SUCCESS);
}

これを、標準出力を端末にして実行すると

$ ./fork_stdio_buf
Hello world
Ciao

という、予想通りの結果が得られます。

しかし、標準出力をファイルにしてリダイレクトすると、挙動が変わります。

root@3d890c1239c5:/tlpi/procexec# ./fork_stdio_buf >a.txt
root@3d890c1239c5:/tlpi/procexec# cat a.txt 
Ciao
Hello world
Hello world

ファイルに出力された結果を確認すると Hello worldが2回出力されてしまいます。

printf()実行時に内部で保持しているstdioバッファの扱いが変わるようです。

解説

上記の挙動を理解するために、まっずstdioバッファのバッファリング方式の違いを知る必要があります。

まず、stdioバッファは、カーネル空間でなく、ユーザ空間で扱われるデータとなります。

そして下記のように標準出力先によって、バッファリング方式が変わります。

  • 端末: 行バッファリング
  • ファイル: ブロックバッファリング

行バッファリングの場合、上記のプログラムでprintfに対して改行文字を含んだ文字列を渡しているため、改行によりその場で表示されます。
逆に、ブロックバッファリングの場合だと、改行があっても表示されず親プロセスのstdioバッファにデータが止まります。

そして、fork()の実行により子プロセスにも、親プロセスのstdioバッファの内容が複製されます。
最後に、親子プロセス両方でexit()を実行する際に、それぞれがもっているstdioバッファの内容をフラッシュすることになり、結果として同じ文字列が二度表示されることになります。

write()による出力では、データはユーザ空間でなく、カーネルのバッファへ直接送信されることになります。 fork()では、カーネルのバッファの内容は複製されないため、2度出力されることはありません。

まとめ

まとめると、同じファイルに対するstdioの関数とシステムコールの併用をする場合には注意が必要となります。 stdioがユーザ空間で扱っているバッファの扱いと、カーネル空間のバッファを直接扱うシステムコールの挙動の理解が重要です。

C: シグナルハンドラ内で非リエントラントな関数を扱った際の挙動

概要

Cにおいて、シグナルハンドラ関数内で非リエントラントな関数を実行した際に意図しない挙動が生じる問題についてかんたんに整理します。

リエントラント: Reentrant 再入可能 - マルチスレッド安全なもの
非リエントラント: Non-Reentrant 再入不可 - リエントラントとは逆で、複数のスレッドから同時に実行した場合、予期せぬ結果を引き起こす可能性があるもの

シグナルハンドラで不整合が起きる例

シグナルハンドラ関数内で非リエントラントな関数であるcrypt(3)を実行した例を挙げ、この場合の注意点について整理します。

crypt(3)について

Cの標準ライブラリで用意されているcrypt(3)は、文字列の暗号化を行う関数で、非リエントラントな関数となります。

crypt(3)実行時、関数内部でstaticな変数を操作するため、複数のスレッドで同時に実行された際に、そのstaticな変数を同時に操作されることにより、結果として意図せぬ結果となります。

crypt_r(3)は、crypt(3)のリエントラント対応版の関数となります。

Man page of CRYPT

シグナルハンドラでcrpyt(3)を使った例

ここでシグナルハンドラ関数内とmain関数内でcrypt(3)を使用した際に生じる不具合を確認します。

/**
 * @file nonreentrant.c
 * 21-1 main()とシグナルハンドラの双方でnon-reentrant関数を呼び出す
 */

// 600以上の値で定義するとSUSv3関連の定義とC99での定義が追加で公開される
#if ! defined(_XOPEN_SOURCE) || _XOPEN_SOURCE < 600
#define _XOPEN_SOURCE 600
#endif

#include <unistd.h>
#include <signal.h>
#include <string.h>
#include "../lib/tlpi_hdr.h"

// argv[2]の文字列
static char *str2;

// ハンドラ実行回数
static int handled = 0;

/**
 * シグナルハンドラ
 * str2 をcrypt()で暗号化する
 */
static void handler(int sig)
{
    crypt(str2, "xx");
    handled++;
}

int main(int argc, char *argv[])
{
    char *cr1;

    int callNum, mismatch;
    struct sigaction sa;

    // 引数チェック
    if (argc != 3) {
        usageErr("%s str1 str2\n", argv[0]);
    }

    // argv[2]をstatic変数に代入し、ハンドラから使用可能にする
    str2 = argv[2];
    
    // staticに割り当てられた文字列を別メモリへコピー
    cr1 = strdup(crypt(argv[1], "xx"));

    if (cr1 == NULL) {
        errExit("strdup");
    }

    sigemptyset(&sa.sa_mask);
    sa.sa_flags = 0;
    sa.sa_handler = handler;
    if (sigaction(SIGINT, &sa, NULL) == -1) {
        errExit("sigaction");
    }

    // argv[1]を繰り返しcrypt()で暗号化する。
    // シグナルハンドラが割り込むと、crypt()内部のstatic memoryがargv[2]の暗号化結果に上書きされ、
    // strcmp()がcr1との不一致を検出する

    for (callNum = 1, mismatch = 0; ; callNum++) {
        
        // すでにargv[1]を暗号化した文字列cr1 と 再度argv[1]を暗号化した文字列を比較する
        if (strcmp(crypt(argv[1], "xx"), cr1) != 0) {
            mismatch++;
            printf("Mismatch on call %d (mismatch=%d handled=%d)\n", callNum, mismatch, handled);
        }
    }
}

ここでは主に以下の処理を行っています。

  1. 実行時に文字列を2つ受け取る。str1, str2とする
  2. main関数内でstr1に対してcrypt(3)を実行し、変数に代入する
  3. SIGINTに対するシグナルハンドラ関数内で、str2に対してcrypt(3)を実行する
  4. main関数内のループで、2とは別にstr1に対してcrypt(3)を実行した結果と、2で変数に代入した文字列を比較する。
  5. プログラム実行中に、SIGINTを送信し、4の処理で不整合が起きるかをチェックする

SIGINTを受け取った際、シグナルハンドラは別スレッドで実行された際に、crypt(3)が実行されます。
このときcrypt(3)内で扱っているstaticな変数に対する競合が発生し、4で実行しているcrypt(3)が意図しない結果となり、不整合が起きることを確認するものです。 (Linuxプログラミングインタフェース 21-1参照)

コード実行後、SIGINTを投げるたびに文字列が一致せず、ログに出ていることが確認できます。

root@8db89fb5ba52:/tlpi/signals# ./nonreentrant a b
^CMismatch on call 352950 (mismatch=1 handled=1)
^CMismatch on call 1839887 (mismatch=2 handled=2)

非同期シグナルセーフな関数

シグナルハンドラを扱うにあたって上述した問題が発生しないことを保証するために、Linuxの標準仕様において、非同期シグナルセーフな関数が指定されています。

docs.oracle.com

www.jpcert.or.jp

シグナルハンドラ関数を扱う際の注意点まとめ

上記のような問題が発生するため、シグナルハンドラ関数では、以下を満たすように注意が必要です

  • シグナルハンドラ関数内ではリエントラントであるようにする、もしくは上述した非同期シグナルセーフな関数のみを使用する。
  • メインプログラムが下記の処理を行う際は、シグナルをブロックする
    • 非同期シグナルセーフでない関数の実行
    • シグナルハンドラも変更するグローバルデータの変更

inotify_add_watch()によるファイル・ディレクトリ変更検知イベントを受信した際の挙動について

概要

linuxシステムコールでのinotify_add_watch(),によるファイル、ディレクトリの変更検知を行った際の挙動を整理する。

監視対象として特定のディレクトリを指定した場合、変更イベントの内容を保持するinotify_event構造体のname とlenにそれぞれ変更があったファイル名とファイル名の長さが入る。
しかし、監視対象としてディレクトリでなく特定のファイルを指定した場合、inotify_event->nameとlenには値が入らない

参考

Man page of INOTIFY

監視対象のディレクトリ内のオブジェクトに対してイベントが発生した場合、 inotify_event 構造体で返される name フィールドは、ディレクトリ内のファイル名を表す。

検証

inotify_add_watchの第2引数に、それぞれ特定のファイル・特定のディレクトリを指定した場合にinotify_event構造体の値がどうなるかを検証する

2つの例を挙げるが、主に異なるのは、inotify_add_watchの引数となる

wd = inotify_add_watch(inotifyFd, WATCH_TARGET_FILE, IN_ALL_EVENTS);

1: 特定のディレクトリを監視対象に指定した場合

`
|-- inotify_logger_directory
|-- log.txt
|-- target // 監視対象ディレクトリ
    `-- target.txt
    `-- target2.txt

コード:

tlpi/tlpi/inotify/inotify_logger_directory.c at master · kakts/tlpi · GitHub

/**
 * inotifyを使ったファイル変更検知ロガー
 * 
 * inotifyによる監視対象
 * 特定のファイルを指定
 */

#include <sys/inotify.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include "../lib/tlpi_hdr.h"

#define WATCH_TARGET_DIR "./target"
#define LOG_FILE "./log.txt"

/**
 * 指定したファイルディスクリプタが指すファイルにinotify_event構造体の内容を書き込む
 */
static void writeToLog(int fd, struct inotify_event *p)
{
    printf("writeToLog\n");
    printf("p->len:%d\n", p->len);
    printf("p->name:%s\n", p->name);
    ssize_t numWrite;

    // write()を使ってp->nameをファイルに書き込む
    numWrite = write(fd, p->name, p->len);
    if (numWrite == -1) {
        errExit("write");
    }

    // 改行文字を書き込む
    numWrite = write(fd, "\n", 1);
    if (numWrite == -1) {
        errExit("write");
    }

    printf("Write %ld bytes to log file. name:%s\n", (long) numWrite, p->name);
}

/**
 * read()に指定したバッファサイズ小さく、次のinotify_event構造体を読み込めない場合がある
 * これを回避するために、inotify_eventを最低でも1つは保持できるだけのサイズ(sizeof(struct inotify_event) + NAME_MAX + 1)を確保すれば良い
 */
#define BUF_LEN (10 * sizeof(struct inotify_event) + NAME_MAX + 1)


int main(int argc, char *argv[])
{
    int inotifyFd, logFd, wd, j;
    char buf[BUF_LEN];
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    // inotifyインスタンスを作成
    inotifyFd = inotify_init();
    if (inotifyFd == -1) {
        close(inotifyFd);
        errExit("inotify_init");
    
    }

    // 変更検知対象のディレクトリを指定
    // 実行ファイルと同一ディレクトリのファイルを監視する場合は、nameが入らない
    wd = inotify_add_watch(inotifyFd, WATCH_TARGET_DIR, IN_ALL_EVENTS);
    if (wd == -1) {
        errExit("inotify_add_watch");
    }
    printf("Watching %s using wd %d\n", "target.txt", wd);

    // ログ出力用のファイルを開く
    // 書き込む場合はファイル末尾へ追加する
    logFd = open(LOG_FILE, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (logFd == -1) {
        errExit("open");
    }



    // イベント処理用の無限ループ
    for (;;) {
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0) {
            fatal("read() from inotify fd returned 0!");
        }

        if (numRead == -1) {
            errExit("read");
        }

        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        // 読み込んだバッファの内容をinotify_event構造体にキャストして表示
        event = (struct inotify_event *) buf;
        writeToLog(logFd, event);
    }

    exit(EXIT_SUCCESS);
}

出力結果は下記となる。 ./target ディレクトリ配下のファイルを操作すると、操作したファイルの名前の情報がinotify_eventに渡っているのを確認できます。

Read 32 bytes from inotify fd
writeToLog
p->len:16
p->name:target.txt
Write 1 bytes to log file. name:target.txt
Read 32 bytes from inotify fd

....
writeToLog
p->len:16
p->name:target2.txt
Write 1 bytes to log file. name:target2.txt
Read 48 bytes from inotify fd
writeToLog
p->len:32

2: 特定のファイルを監視対象に指定した場合

root@a0cd1290306f:/tlpi/inotify# tree
.
|
|-- inotify_logger
|-- log.txt // log出力先
`-- target.txt // 監視対象ファイル

上記のような構成で、./target.txtに対して変更があった場合、変更情報を./log.txtに出力させます。

コード tlpi/tlpi/inotify/inotify_logger.c at master · kakts/tlpi · GitHub

/**
 * inotifyを使ったファイル変更検知ロガー
 * 
 * inotifyによる監視対象
 * 特定のファイルを指定
 */

#include <sys/inotify.h>
#include <stdio.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#include "../lib/tlpi_hdr.h"

#define WATCH_TARGET_FILE "./target.txt"
#define LOG_FILE "./log.txt"

/**
 * 指定したファイルディスクリプタが指すファイルにinotify_event構造体の内容を書き込む
 */
static void writeToLog(int fd, struct inotify_event *p)
{
    printf("writeToLog\n");
    printf("p->len:%d\n", p->len);
    printf("p->name:%s\n", p->name);
    ssize_t numWrite;

    // write()を使ってp->nameをファイルに書き込む
    numWrite = write(fd, LOG_FILE, strlen(LOG_FILE));
    if (numWrite == -1) {
        errExit("write");
    }

    // 改行文字を書き込む
    numWrite = write(fd, "\n", 1);
    if (numWrite == -1) {
        errExit("write");
    }

    printf("Write %ld bytes to log file. name:%s\n", (long) numWrite, p->name);
}

/**
 * read()に指定したバッファサイズ小さく、次のinotify_event構造体を読み込めない場合がある
 * これを回避するために、inotify_eventを最低でも1つは保持できるだけのサイズ(sizeof(struct inotify_event) + NAME_MAX + 1)を確保すれば良い
 */
#define BUF_LEN (10 * sizeof(struct inotify_event) + NAME_MAX + 1)


int main(int argc, char *argv[])
{
    int inotifyFd, logFd, wd, j;
    char buf[BUF_LEN];
    ssize_t numRead;
    char *p;
    struct inotify_event *event;

    // inotifyインスタンスを作成
    inotifyFd = inotify_init();
    if (inotifyFd == -1) {
        close(inotifyFd);
        errExit("inotify_init");
    
    }

    // 変更検知対象のファイルを指定
    // 実行ファイルと同一ディレクトリのファイルを監視する場合は、nameが入らない
    wd = inotify_add_watch(inotifyFd, WATCH_TARGET_FILE, IN_ALL_EVENTS);
    if (wd == -1) {
        errExit("inotify_add_watch");
    }
    printf("Watching %s using wd %d\n", "target.txt", wd);

    // ログ出力用のファイルを開く
    // 書き込む場合はファイル末尾へ追加する
    logFd = open(LOG_FILE, O_RDWR | O_CREAT | O_APPEND, S_IRUSR | S_IWUSR);
    if (logFd == -1) {
        errExit("open");
    }



    // イベント処理用の無限ループ
    for (;;) {
        numRead = read(inotifyFd, buf, BUF_LEN);
        if (numRead == 0) {
            fatal("read() from inotify fd returned 0!");
        }

        if (numRead == -1) {
            errExit("read");
        }

        printf("Read %ld bytes from inotify fd\n", (long) numRead);

        // 読み込んだバッファの内容をinotify_event構造体にキャストして表示
        event = (struct inotify_event *) buf;
        writeToLog(logFd, event);
    }

    exit(EXIT_SUCCESS);
}

出力結果は下記となる。

writeToLog
p->len:0
p->name:
Write 1 bytes to log file. 

特定のファイルを指定した場合はlen, nameに値が入ってこないのを確認できました。

Golangでファイル監視を行う

概要

Goでファイル監視の方法についての記事です。
Goの標準パッケージではファイル監視の機能は提供されていないが、 github.com/fsnotify/fsnotify を使ったファイル監視のやり方をまとめます。

fsnotifyについて

github.com/fsnotify/fsnotify
各プラットフォーム・OSに対応したファイル変更通知機能(inotifyシステムコールなど)を提供するライブラリです。

主な使い方

ざっくり整理すると、以下の流れになります。

  • ファイル監視用のwatcherを作成する。
  • watcher.Addにより監視対象とするパスを追加
    1. ディレクトリを指定した場合: その配下のファイル全てが監視対象となる
    2. 特定のファイルを指定した場合: 複数のツールがファイルをアトミックに更新するため、一般的に推奨されない この場合、特定のファイルのみを監視対象とする場合、1のディレクトリ指定を行い、ファイル変更イベントが発生した際に、Event.Nameに変更があったファイルのパスが入るため、それをチェックして処理を行うのが推奨されます。
  • watcherがEvents, Errorsというチャネルを持っており、特定のファイルに対するイベントが発生した場合、チャネルにメッセージが送信されるのでそれを受信して処理を行う。

下記から、指定したディレクトリ配下の特定のファイルへのイベントを監視するコードについて説明します。

特定のファイルのみ更新したい場合

上記にあるように、watcher.Addで特定のファイルを指定するのでなく、そのファイルが属するディレクトリを指定し、Eventを監視し、Event.Nameをチェックして必要なファイルのみフィルタリングしてください。

./tmp ディレクトリ配下のtarget.txt への変更があった場合にEventを表示させる例を示します。

// fsnotifyで特定のファイルを監視

package main

import (
    "log"

    "github.com/fsnotify/fsnotify"
)

func main() {
    watcher, err := fsnotify.NewWatcher()
    if err != nil {
        log.Fatal(err)
    }
    defer watcher.Close()

    go func() {
        for {
            select {
            case event, ok := <-watcher.Events:
                if !ok {
                    log.Println("watcher.Events is not ok")
                    return
                }

                // 特定のファイルへイベントが発生した場合
                if event.Name == "tmp/target.txt" {
                    log.Println("Event: ", event)
                } else {
                    log.Println("not target file", event.Name)
                }

            case err, ok := <-watcher.Errors:
                if !ok {
                    log.Println("watcher.Errors is not ok")
                    return
                }
                log.Println("error:", err)
            }
        }
    }()

    err = watcher.Add("./tmp")
    if err != nil {
        log.Fatal(err)
    }
    <-make(chan struct{})
}

case event, ok := <-watcher.Events: でwatcher.Eventsのchannelからのメッセージを受信した際の処理で、eventを受けとるので、ここからevent.Nameをチェックしています。

といった感じで指定したファイルへの変更イベントを拾うことができました。