panda's tech note

Advent Calendar 2019: advfs

Day 5: ディレクトリツリーの管理

今日からは /test 以外のファイルも扱える少しまともな ramfs を開発していきます。最上位ディレクトリから再度実装し直すため,昨日実装した open, read, write, truncate は一旦 -ENOENT を返すようにします。

ファイルシステム固有データ

昨日まではグローバル変数にいろいろとファイルシステム情報を保存しましたが,実装としてきれいではありません。今日からは,ファイルシステム固有のデータ構造は全て advfs_t 構造体に入れ,各関数内で扱います。そのため,まず main() 関数内で

    advfs_t advfs;

を定義します。そして,この変数へのポインタを以下のように fuse_main() 関数の第4引数に渡します。

    return fuse_main(argc, argv, &advfs_oper, &advfs);

この第4引数で渡されたポインタは,FUSE のコンテキスト情報内に保存されます。各 FUSE API 関数の中からこの変数を取得するには,以下のように, fuse_get_context() 関数でコンテキストを取得し,そのメンバー変数である private_data を参照することで取得できます。

    struct fuse_context *ctx;
    advfs_t *advfs;
    ctx = fuse_get_context();
    advfs = ctx->private_data;

今回は,まずファイルシステムのエントリ(inodeに相当)タイプとして,未使用,通常ファイル,ディレクトリの3種類を以下の通り enum 型の advfs_entry_type_t として定義します。

typedef enum {
    ADVFS_UNUSED,
    ADVFS_REGULAR_FILE,
    ADVFS_DIR,
} advfs_entry_type_t;

通常ファイル固有の情報として,ファイルの中身を保存するバッファおよびそのサイズを持つ構造体 advfs_entry_file_t を以下の通り定義します。

typedef struct {
    uint8_t *buf;
    size_t size;
} advfs_entry_file_t;

ディレクトリは,通常ファイルまたはディレクトリ(まとめてエントリ advfs_entry_tとして定義)を子に持つので,それらの数およびエントリインデックス配列 children を持つ構造体 advfs_entry_dir_t として定義します。子のエントリインデックス配列は,エントリへのポインタの配列でも良いのですが,実際のブロックデバイスへの実装を考え,エントリがブロックデバイスの一部に予め配列として予約されているものとして,今回はその配列へのインデックスで定義しています。

typedef struct _advfs_entry advfs_entry_t;
typedef struct {
    int nent;
    int *children;
} advfs_entry_dir_t;

エントリは名前とエントリタイプ,パーミッション,各種タイムスタンプおよびエントリタイプ固有の情報を持つ構造体として定義します。

struct _advfs_entry {
    char name[ADVFS_NAME_MAX];
    advfs_entry_type_t type;
    int mode;
    time_t atime;
    time_t mtime;
    time_t ctime;
    union {
        advfs_entry_file_t file;
        advfs_entry_dir_t dir;
    } u;
};

ファイルシステム固有の構造体として,最上位ディレクトリのエントリのインデックス root および エントリ配列 entries を持つ構造体 advfs_t を以下のように定義します。

typedef struct {
    int root;
    advfs_entry_t *entries;
} advfs_t;

ディレクトリの初期化

今回は2日目同様,最上位ディレクトリのみを実装します。

上記で定義した構造体を用いて,最上位ディレクトリの初期化を行います。コードは以下の通りです。

    advfs_t advfs;
    int i;
    struct timeval tv;

    /* Allocate entries */
    advfs.entries = malloc(sizeof(advfs_entry_t) * ADVFS_NUM_ENTRIES);
    if ( NULL == advfs.entries ) {
        return -1;
    }
    for ( i = 0; i < ADVFS_NUM_ENTRIES; i++ ) {
        advfs.entries[i].type = ADVFS_UNUSED;
    }

    /* root directory */
    gettimeofday(&tv, NULL);
    advfs.root = 0;
    advfs.entries[advfs.root].type = ADVFS_DIR;
    advfs.entries[advfs.root].name[0] = '\0';
    advfs.entries[advfs.root].mode = 0777;
    advfs.entries[advfs.root].atime = tv.tv_sec;
    advfs.entries[advfs.root].mtime = tv.tv_sec;
    advfs.entries[advfs.root].ctime = tv.tv_sec;
    advfs.entries[advfs.root].u.dir.nent = 0;
    advfs.entries[advfs.root].u.dir.children = NULL;

まず,ADVFS_NUM_ENTRIES 個の要素を持つエントリ配列のメモリを確保します。そして,すべてのエントリを未使用 ADVFS_UNUSED とします。

その後,最上位ディレクトリのエントリを初期化します。最上位ディレクトリはディレクトリ名を持たないので,名前を表す name は空,パーミッションは 0777,各種タイムスタンプは現在時刻とします。また,このディレクトリは空ディレクトリとするので u.dir.nent0 とします。

パスからエントリの解決

昨日までに説明してきた通り,FUSE では,各種APIはパス名でファイルを識別します。このパス名からエントリを解決する関数を以下の通り作成します。

advfs_entry_t *
advfs_path2ent(advfs_t *advfs, const char *path)
{
    advfs_entry_t *e;

    if ( '/' != *path ) {
        return NULL;
    }
    path++;

    /* Root */
    e = &advfs->entries[advfs->root];
    if ( '\0' == *path ) {
        return e;
    }

    return NULL;
}

今回は最上位ディレクトリのみの対応であるため,パスが / であれば最上位ディレクトリのエントリを,それ以外であれば NULL を返すようにします。

getattr および readdir の実装

パス名からエントリが解決できれば,昨日までの知識で getattr および readdir APIが実装できます。今回は以下の通り実装しました。

int
advfs_getattr(const char *path, struct stat *stbuf)
{
    struct fuse_context *ctx;
    advfs_t *advfs;
    advfs_entry_t *e;
    int status;

    /* Reset the stat structure */
    memset(stbuf, 0, sizeof(struct stat));

    /* Get the context */
    ctx = fuse_get_context();
    advfs = ctx->private_data;

    e = advfs_path2ent(advfs, path);
    if ( NULL == e ) {
        /* No entry found */
        return -ENOENT;
    }
    if ( e->type == ADVFS_DIR ) {
        /* Directory */
        stbuf->st_mode = S_IFDIR | e->mode;
        stbuf->st_nlink = 2 + e->u.dir.nent;
        stbuf->st_uid = ctx->uid;
        stbuf->st_gid = ctx->gid;
        status = 0;
        stbuf->st_atime = e->atime;
        stbuf->st_mtime = e->mtime;
        stbuf->st_ctime = e->ctime;
#ifdef HAVE_STRUCT_STAT_ST_BIRTHTIME
        stbuf->st_birthtime = e->ctime;
#endif
    } else {
        status = -ENOENT;
    }

    return status;
}
int
advfs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
              off_t offset, struct fuse_file_info *fi)
{
    struct fuse_context *ctx;
    advfs_t *advfs;
    advfs_entry_t *e;

    /* Get the context */
    ctx = fuse_get_context();
    advfs = ctx->private_data;

    e = advfs_path2ent(advfs, path);
    if ( NULL == e || e->type != ADVFS_DIR ) {
        /* No entry found or non-directory entry */
        return -ENOENT;
    }

    filler(buf, ".", NULL, 0);
    filler(buf, "..", NULL, 0);

    return 0;
}

実行

いつものように実装したプログラムをビルド・実行するために

$ docker-compose build && docker-compose up -d

を実行し,

$ docker exec -it advfs_advfs_1 bash

と実行し, /mnt に実装したファイルシステムをマウントしたコンテナの bash を起動します。

2日目同様,ls -al /mnt を実行します。2日目とは違い,今回は最上位ディレクトリのタイムスタンプを起動時刻にしたため,下記のように . のタイムスタンプが起動時の時刻になっていることが確認できるかと思います。

root@0c7ab5397d27:/usr/src# ls -al /mnt
total 4
drwxrwxrwx 2 root root    0 Dec  5 14:41 .
drwxr-xr-x 1 root root 4096 Dec  5 14:41 ..

今日のまとめと明日の予定

今日は最上位ディレクトリを実装しました。明日からはこのディレクトリ化にファイルを追加し,読み書きできるようにしていこうと思います。