システムプログラミング 演習5(ファイルシステム)


FUSE (Filesystem in Userspace)

FUSEは,カーネルを変更することなく ユーザプログラムで新たなファイルシステムを作るためのLinuxの仕組みである

FUSEの概要

FUSEライブラリを用いてプログラムを作り, そのプログラムを実行することでOSに新たなファイルシステムを追加することができる. 例えば,後の例題のプログラム simplefs.c を:

$ cc -lfuse simplefs.c -o simplefs
のようにコンパイルし, あらかじめ空のディレクトリ/mnt/aを作っておいてから スーパーユーザ(root)権限でそのディレクトリに書き込める ユーザの権限で
$ ./simplefs  /mnt/a
と実行すると,このプログラムで定義する新たなファイルシステムが /mnt/aにマウント(接続) され,このファイルシステム内のファイルが/mnt/a/ファイル名 として読み書きできるようになる. simplefsコマンドは, ファイルシステムがマウントされている間,ずっと実行しており,例えば
$ cp simple.c  /mnt/a/simple.c

のようにこのファイルシステムの中にファイルを作ったり書き込んだりして open, writeなどのシステムコールが実行されると,そのシステムコールの内容が カーネル内のモジュールを通じてsimplefsプログラムに通知される. simplefsから処理結果を返すと,逆の経路を辿ってcpなどの プログラムへ返される.

FUSE

ファイルシステムプログラムの構造

FUSEを用いてファイルシステムを定義するプログラムは, 以下のような形で作る.
#define FUSE_USE_VERSION 26
#define _FILE_OFFSET_BITS 64

#include <fuse.h>

/*
... open, read, write などの操作を実行する関数定義を
ここに書く ...
*/

static struct fuse_operations myfs_oper = {
  .open		= myfs_open,
  .read		= myfs_read,
  .write	= myfs_write,
  .mknod	= myfs_mknod,
};

int main(int argc, char *argv[])
{
  return fuse_main(argc, argv, &myfs_oper, NULL);
}

ファイルシステムの機能を実現するための処理関数を作り, それらの関数へのポインタを struct fuse_operation 型の構造体の中に 入れて,fuse_main関数に渡してやる. カーネルとやりとりをして,システムコールに相当する関数を呼び出したり, 結果をカーネルに返す作業はfuse_mainが行なってくれる.

struct fuse_operation 構造体に定義すべき操作(関数へのポインタ) の種類としては,以下のようなものが定義されている. (詳細は/usr/local/include/fuse/fuse.hを参照.)

struct fuse_operations {
    int (*getattr) (const char *, struct stat *);
    int (*mknod) (const char *, mode_t, dev_t);
    int (*mkdir) (const char *, mode_t);
    int (*unlink) (const char *);
    int (*rmdir) (const char *);
    int (*rename) (const char *, const char *);
    int (*chmod) (const char *, mode_t);
    int (*chown) (const char *, uid_t, gid_t);
    int (*truncate) (const char *, off_t);
    int (*open) (const char *, struct fuse_file_info *);
    int (*read) (const char *, char *, size_t, off_t, struct fuse_file_info *);
    int (*write) (const char *, const char *, size_t, off_t,
                  struct fuse_file_info *);
    int (*opendir) (const char *, struct fuse_file_info *);
    int (*readdir) (const char *, void *, fuse_fill_dir_t, off_t,
                    struct fuse_file_info *);
    int (*access) (const char *, int);
    int (*utimens) (const char *, const struct timespec tv[2]);
    /* 以下省略 */
};

これら全てを定義する必要はないが,定義しないとファイルシステムの機能が限定される. 以下の例(simplefs)では,getattr(ファイルが存在するか,普通のファイルかディレクトリか などの情報を得る), readdir(ディレクトリ内のファイル一覧を得る), mknod(ディレクトリでない 普通のファイルを作る), open(既に存在するファイルを開く), read, write のみを定義している.

このファイルシステムでは,

などの操作は行なえない.また,ファイルの持ち主のユーザや ファイルに書き込んだ時刻などの情報は持っていない. ファイルのデータはメモリ上に持っているだけでディスクには書き込まないので, アンマウントするとデータが消えてしまう.

それぞれの関数は,対応するシステムコールに応じて,成功すると0を返すもの (getattr, readdir, openなど)や,バイト数を返すもの(read, writeなど)がある. 負の値を返すとエラーの意味になり,その場合は返す値がエラーコードを表す. たとえば,-ENOENTを返すと「No such file or directory」のエラーになる. (エラーコードの詳細は/usr/include/asm/errno.hやシステムコールのマニュアルを参照.)

ファイル simplefs.c

#define FUSE_USE_VERSION 26
#define _FILE_OFFSET_BITS 64

#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <fcntl.h>
#include <stdarg.h>

#define INITIAL_SIZE 1024

/* 1個の普通のファイルを表す構造体 */
struct file_metadata {
  int mode;     // ファイルの種類・読み書きのモード
  int nlink;    // ファイルのリンクカウント
  int length;   // ファイル内のデータのバイト数
  int capacity; // data配列の大きさ
  char *data;   // データを保持する配列へのポインタ
};

/* ディレクトリを表すリストの要素 */
struct directory_entry {
  char *name;                   // ファイル名
  struct file_metadata *file;   // ファイルの本体
  struct directory_entry *next; // 次の要素へのポインタ
};

struct directory_entry *root = NULL;   // このFSのルートディレクトリ

// ディレクトリ内のファイルを探す補助関数
static struct directory_entry *search_file(const char *name)
{
  struct directory_entry *p;
  for (p = root; p != NULL; p = p->next) {
    if (strcmp(p->name, name + 1) == 0) {
      return p;
    }
  }
  return NULL;
}

static int simple_getattr(const char *path, struct stat *stbuf)
{
  int res = 0;
  struct directory_entry *p;

  memset(stbuf, 0, sizeof(struct stat));
  if (strcmp(path, "/") == 0) {
    stbuf->st_mode = S_IFDIR | 0755;
    stbuf->st_nlink = 2;
  } else if (p = search_file(path)) {
    stbuf->st_mode = p->file->mode;
    stbuf->st_nlink = p->file->nlink;
    stbuf->st_size = p->file->length;
  } else
    res = -ENOENT;

  return res;
}

static int simple_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
			off_t offset, struct fuse_file_info *fi)
{
  struct directory_entry *p;

  if (strcmp(path, "/") != 0)
    return -ENOENT;

  filler(buf, ".", NULL, 0);   // fillerを呼ぶと,返す一覧情報に1つ要素を追加する.
  filler(buf, "..", NULL, 0);
  for (p = root; p != NULL; p = p->next) {
    if (filler(buf, p->name, NULL, 0)) {
      break;
    }
  }

  return 0;
}

static int simple_open(const char *path, struct fuse_file_info *fi)
{
  struct directory_entry *p;

  if ((p = search_file(path)) == 0) {
    return -ENOENT;
  }

  fi->fh = (long)p->file;
  
  return 0;
}

static int simple_read(const char *path, char *buf, size_t size, off_t offset,
		     struct fuse_file_info *fi)
{
  struct file_metadata *f = (struct file_metadata *)(long)fi->fh;

  if (offset > f->length) {
    return 0;
  }
  if (offset + size > f->length) {
    size = f->length - offset;
  }
  memcpy(buf, f->data + offset, size);

  return size;
}

// 十分に大きなdata配列を確保する補助関数.data配列は2倍ずつ大きくしていく.
static int assure_size(struct file_metadata *f, int size)
{
  int new_capacity = f->capacity;
  char *new_data;

  while (new_capacity < size) {
    new_capacity *= 2;
  }
  new_data = calloc(new_capacity, 1);
  if (new_data == NULL) {
    return -1;
  }
  memcpy(new_data, f->data, f->length);
  free(f->data);
  f->data = new_data;
  f->capacity = new_capacity;
  return 0;
}

static int simple_write(const char *path, const char *buf, size_t size, off_t offset,
                      struct fuse_file_info *fi)
{
  struct file_metadata *f = (struct file_metadata *)(long)fi->fh;
  int block_num, begin, end;
  char *block;

  if (assure_size(f, offset + size) < 0) {
    return -ENOSPC;
  }
  if (offset + size > f->length) {
    f->length = offset + size;
  }
  memcpy(f->data + offset, buf, size);

  return size;
}

static int simple_mknod(const char *path, mode_t mode, dev_t device)
{
  struct directory_entry *p;

  if (search_file(path)) {
    return -EEXIST;
  }
  p = malloc(sizeof(struct directory_entry));
  if (p == NULL) {
    return -ENOSPC;
  }
  p->file = calloc(sizeof(struct file_metadata), 1);
  if (p->file == NULL) {
    return -ENOSPC;
  }
  p->file->mode = mode;
  p->file->nlink = 1;
  p->file->length = 0;
  p->file->capacity = INITIAL_SIZE;
  p->file->data = calloc(INITIAL_SIZE, 1);

  p->name = strdup(path + 1);
  if (p->name == NULL) {
    return -ENOSPC;
  }

  p->next = root;
  root = p;

  return 0;
}

static struct fuse_operations simple_oper = {
  .getattr	= simple_getattr,
  .readdir	= simple_readdir,
  .open		= simple_open,
  .read		= simple_read,
  .write	= simple_write,
  .mknod	= simple_mknod,
};

int main(int argc, char *argv[])
{
  return fuse_main(argc, argv, &simple_oper, NULL);
}

FUSEを使う準備

  1. fuse-2.7.4.tar.gzをダウンロード し, USBメモリ等を用いてFedora 7にコピーする.
  2. 以下のコマンドで,アーカイブを解凍する
    $ tar xvfz fuse-2.7.4.tar.gz
    
  3. 以下のコマンドで,プログラムをビルドする.
    $ cd fuse-2.7.4
    $ ./configure
    $ make
    
  4. スーパーユーザ権限で,プログラムやライブラリをインストールする.
    # make install
    

演習課題

  1. 上のファイルsimplefs.cをコンパイルし,マウントして動作を確認しなさい. 既に存在するファイルをcpコマンドでコピーしてみて,diffコマンドで元のファイルと 比較し,正しくコピーできていることを確認しなさい.また,mv, rm, mkdirはエラーになる ことを確認しなさい.
  2. rename 関数を追加し,mvコマンドでファイル名を変更できるようにしなさい. エラーチェックは省略して良い.
  3. truncate 関数を追加し,ファイルの大きさを変更できるようにしなさい. ファイルが大きくなることもあることに注意. 第2引数が負ならば,-EINVALを返し,ファイルが多き過ぎてメモリが足りない ならば -ENOSPC を返すようにしなさい. cp コマンドで既に存在するファイルを上書きできることを確かめなさい.
  4. (オプション問題)余裕のある人は, unlink 関数を追加し,ファイルを削除できるようにしなさい. 存在しないファイル名を渡された時には -ENOENT を返すようにしなさい. rm コマンドでファイルが削除できることを確かめなさい.
  5. 以下を提出しなさい.