panda's tech note

Advent Calendar 2019: advfs

Day 4: ファイルの読み書き

昨日に引き続き ramfs を FUSE 上に実装していきます。今日は,昨日追加した /test の読み書きを行います。読み書きを行うために実装するAPIは,openreadwrite の3つですが,今回はリダイレクトでの上書きや vim などのエディタでの書き込みもサポートするために,truncate も実装します。

初期化

fuse_main を呼び出す前に,/test ファイルの実体 ramfs_file_buf のメモリを確保します。具体的には以下のようにします。単純に malloc でメモリを確保しているだけです。

int
main(int argc, char *argv[])
{
    ramfs_file_buf = malloc(ramfs_file_max_size);
    if ( NULL == ramfs_file_buf ) {
        return -1;
    }

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

open

まずファイルを開く open を実装します。今回は /test ファイルのみを扱うので,これ以外のファイル名が指定された場合は -ENOENT を返します。open() システムコールは,VFS内部では vnode,アプリケーションにはファイルディスクリプタを返し,それらを用いて read, write などの関数(やシステムコール)を呼び出しますが,FUSE ではパスのまま扱います。また,ファイルの生成もカーネルで create に変換されるため,open 必要なのはパーミッションチェックとキャッシュの取り扱いのみです。今回は,以下のように /test に対しては何も行わないようにしています。

int
advfs_open(const char *path, struct fuse_file_info *fi)
{
    if ( strcmp(path, ramfs_file_path) != 0 ) {
        return -ENOENT;
    }

    return 0;
}

パーミッションチェックをする場合は,第二引数の struct fuse_file_info *fifi->flags に保存されるフラグをチェックします。

read

read は以下のように実装します。ファイルパスは第1引数で渡されるため, open と同様に /test 以外のファイルに対しては -ENOENT を返します。 open で指定されたオプションは第5引数で渡される fi に保存されているため, fi->flags を確認することで,このファイルが読み込みモード( O_RDONLY または O_RDWR )で開かれたことを確認します。第2引数,第3引数は,読み込みバッファとバッファサイズであり,ここにファイルの中身を書き込みます。第4引数の offset は読み込むファイルのオフセットが指定されます。この関数は read() 関数と同様に読み込んだバイト数を返します。今回は /test の中身は ramfs_file_buf に保存しているため,この offset バイト目から size バイト分またはファイルの終端までをコピーします。

int
advfs_read(const char *path, char *buf, size_t size, off_t offset,
           struct fuse_file_info *fi)
{
    int perm;

    if ( strcmp(path, ramfs_file_path) != 0 ) {
        return -ENOENT;
    }
    /* Permission check */
    perm = fi->flags & 3;
    if ( perm != O_RDONLY && perm != O_RDWR ) {
        return -EACCES;
    }

    if ( offset < (off_t)ramfs_file_size ) {
        if ( offset + size > ramfs_file_size ) {
            size = ramfs_file_size - offset;
        }
        (void)memcpy(buf, ramfs_file_buf + offset, size);
    } else {
        size = 0;
    }

    return size;
}

write

writeread と同様にファイルの書き込みモードのチェックを行い,ramfs_file_buf に書き込むだけです。単純なコードなので以下のコードを参照してください。 ramfs_file_buf には ramfs_file_max_size バイト分のみメモリを確保しているので,このサイズを超える場合はクオータサイズを超えたことを表すエラー -EDQUOT を返します。

int
advfs_write(const char *path, const char *buf, size_t size, off_t offset,
            struct fuse_file_info *fi)
{
    int perm;

    if ( strcmp(path, ramfs_file_path) != 0 ) {
        return -ENOENT;
    }

    /* Permission check */
    perm = fi->flags & 3;
    if ( perm != O_WRONLY && perm != O_RDWR ) {
        return -EACCES;
    }
    if ( size <= 0 ) {
        return 0;
    }

    if ( offset + size > ramfs_file_max_size ) {
        return -EDQUOT;
    }
    (void)memcpy(ramfs_file_buf + offset, buf, size);
    if ( ramfs_file_size < offset + size ) {
        ramfs_file_size = offset + size;
    }

    return size;
}

fuse_operations

open, read, write, truncate を実装したので,それらの関数を以下のように fuse_main() に渡す struct fuse_operations 構造体のメンバ変数に追加します。

static struct fuse_operations advfs_oper = {
    .getattr    = advfs_getattr,
    .readdir    = advfs_readdir,
    .statfs     = advfs_statfs,
    .open       = advfs_open,
    .read       = advfs_read,
    .write      = advfs_write,
    .truncate   = advfs_truncate,
};

この時点での具体的な実装は src/main.c を参照してください。

実行

昨日同様に実装したプログラムをビルド・実行するために

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

を実行し,

$ docker exec -it advfs_advfs_1 bash

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

以下のように /mnt/test に対して echo コマンドの出力をリダイレクトし, cat コマンドで出力することで,ファイルの読み書きが正常に行われていることが確認できます。

root@b2cf2d9bacc9:/usr/src# ls -al /mnt/
total 4
drwxrwxrwx 2 root root    0 Jan  1  1970 .
drwxr-xr-x 1 root root 4096 Dec  4 13:01 ..
-rw-rw-rw- 1 root root    0 Dec  3 10:50 test
root@b2cf2d9bacc9:/usr/src# cat /mnt/test
root@b2cf2d9bacc9:/usr/src# echo "test1234" > /mnt/test
root@b2cf2d9bacc9:/usr/src# cat /mnt/test
test1234
root@b2cf2d9bacc9:/usr/src# echo "test56789" >> /mnt/test
root@b2cf2d9bacc9:/usr/src# cat /mnt/test
test1234
test56789
root@b2cf2d9bacc9:/usr/src# echo "a" > /mnt/test
root@b2cf2d9bacc9:/usr/src# cat /mnt/test
a

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

今日は open, read, write, truncate の4 API関数を実装しました。これで, ramfs 上の /test ファイルを読み書きできるようになりました。明日は,特定のファイル(/test)以外も扱えるようにしていこうと思います。