次のページ 前のページ 目次へ

3. 仮想ファイルシステム (VFS)

3.1 Inode キャッシュとDcache との相互作用

複数のファイルシステムをサポートするため、 Linux は VFS (Virtual Filesystem Switch) という特別なカーネルインターフェースレベルを持っています。 これは SVR4 から派生した OS に見られる vnode/vfs インターフェースに良く似ています (もともとは、BSD と Sun に由来の実装からきています)。

Linux の inode キャッシュは、一つの 977 行からなるファイル fs/inode.c に実装されています。 特筆すべき事に、このファイルは、この 5-7 年の間ほとんど変更がありませんでした。 一番最後に更新されたのは、そう 1.3.42 の時なのです。

Linux の inode キャッシュの構造は以下の通りです。

  1. 各 inode がスーパーブロックポインタと 32bit の inode 番号の値によってハッシュされるグローバルハッシュテーブル inode_hashtable。 スーパーブロックの無い inode (inode->i_sb == NULL) は、代りにanon_hash_chainで始まるダブルリンクリストへとつながれる。匿名 inodeの例として、fs/inode.c:get_empty_inode()を呼ぶことで、net/socket.c:sock_alloc()によって作られるソケットがある。
  2. i_count>0 かつ i_nlink>0 となる inode からなるグローバルタイプの in_use_list(inode_in_use)。inode は、get_empty_inode()get_new_inode() により新たに割り当てられ、inode_in_use リストへつながれる。
  3. i_count = 0であるような グローバルタイプの未使用リスト (inode_unused)。
  4. i_count>0i_nlink>0そしてi_state & I_DIRTY であるような inode によるスーパーブロック毎の汚れたリスト(sb->s_dirty)。inodeが dirtyの印がつけられたら、それはやはりハッシュになっているsb->s_dirtyリストに追加される。スーパーブロック毎に inode の dirty リストを管理することで、inode の同期をすばやく行うことができるようになる。
  5. 厳密な意味の inode キャッシュ -- inode_cachepを呼び出す SLAB キャッシュ。inode オブジェクトが割り当てられたり、解放されたりするときは、この SLAB キャッシュから取得されたり、返されたりする。

それぞれのタイプリストは、inode->i_listから指し示され、ハッシュテーブルは、inode->i_hashから結び付けられています。各 inode は それぞれハッシュテーブルと1種類のみのタイプリスト(in_use, unused, または dirty)に入ります。

このすべてのリストは一つのスピンロックinode_lockにより保護されています。

inode キャッシュサブシステムは、inode_init()関数が、init/main.c:start_kernel()から呼び出された時に初期化されます。この関数は、__initとマークされており、これはその後、このコードが解放されてしまうことを意味しています。この関数は一つの引数 -- システムの物理ページ数を渡します。このことから、inode キャッシュは利用可能なメモリ量に依存して決められていることがわかります。つまり、もし十分な量のメモリがあれば、より大きなハッシュテーブルを作成するということです。

inode キャッシュに関するの唯一の統計的情報は、利用されていないinode数です。これは、inodes_stat.nr_unusedに格納され、ファイル/proc/sys/fs/inode-nrproc/sys/fs/inode-stateを通してユーザプログラムから見ることができます。

そのため、生きたカーネルで実行する gdb からリストを取出すことができます。


(gdb) printf "%d\n", (unsigned long)(&((struct inode *)0)->i_list)
8
(gdb) p inode_unused
$34 = 0xdfa992a8
(gdb) p (struct list_head)inode_unused
$35 = {next = 0xdfa992a8, prev = 0xdfcdd5a8}
(gdb) p ((struct list_head)inode_unused).prev
$36 = (struct list_head *) 0xdfcdd5a8
(gdb) p (((struct list_head)inode_unused).prev)->prev
$37 = (struct list_head *) 0xdfb5a2e8
(gdb) set $i = (struct inode *)0xdfb5a2e0
(gdb) p $i->i_ino
$38 = 0x3bec7
(gdb) p $i->i_count
$39 = {counter = 0x0}

ここで、include/linux/list.hlist_entry()マクロの定義にしたがってstruct inodeのアドレス (0xdfb5a3e0) を得るために、アドレス 0xdfb5a2e8 から8を引いていることに注意しましょう。

inode キャッシュがどのように働くか理解するために、ext2 ファイルシステムの通常のファイルで、 オープン、クローズされるときの inode の一生を見ていきましょう。


fd = open("file", O_RDONLY);
close(fd);

open(2) システムコールは fs/open.c:sys_open() 関数に定義されていて、実体は fs/open.c:file_open() 関数が行っています。この関数は2つの部分に分けられ、

  1. open_namei(): dentryとvfsmount構造体からなるnameiデータ構造体を埋める。
  2. dentry_open(): dentryやvfsmountを与える。この関数は、新しいstruct fileを割り当て、相互にリンクする。また、inode が(dentry->d_inodeから inode を与える) open_namei() で読み込まれる時 inode->i_fop にセットされているファイルシステムに特有の f_op->open() メソッドを呼び出す。

open_namei() 関数は、ファイルシステム毎のinode_operations->lookup() メソッドを起動する real_lookup()をさらに呼び出す path_walk() によって、 dentry キャッシュと相互作用を起こします。 このメソッドの役割は、親ディレクトリで名前の一致するエントリを探すことで、その inode を取得するため iget(sb, ino) を呼び出しています。この関数は inode キャッシュへアクセスします。 inode が読み込まれたら、 d_add(dentry, inode) により dentry のインスタンスが作成されます。 ここで注意するのは、上記の間で、オンディスク inode 番号というコンセプトを持つUNIX スタイルのファイルシステムのために、そのエンディアンを現在の CPU フォーマットにマップするのがルックアップメソッドの仕事だということです。 たとえば、ファイルシステムに特有な raw ディレクトリエントリの inode 番号がリトルエンディアンの32ビットフォーマットであれば、


unsigned long ino = le32_to_cpu(de->inode);
inode = iget(sb, ino);
d_add(dentry, inode);

というコードを実行します。

このように、ファイルを開くときには、実体がiget4(sb, ino, NULL, NULL) である iget(sb, ino) をたたきます。これは、

  1. inode_lockの保護のもと、ハッシュテーブルの中から、スーパブロックと inode 番号が一致する inode を見つけようとする。もし、inode が見つかれば、その参照カウンタ (i_count) を増分する。 もし、増加の前の値が 0 であったらば、inode は dirty ではないので、どのタイプのリスト(iode->i_list)からであれ削除する。これは、現在 on (それはもちろんinode_unusedリストでなければならない)であり、inode_in_useタイプのリストに挿入される。最後に、inodes_stat.nr_unused が減少される。
  2. もしinodeが現在ロックされていたら、ロックが解除されるまで待つ。つまり、iget4()はロックされていない inode を返すことが保証される。
  3. もし inode がハッシュテーブルに見つからなければ、この inode に初めて出会ったということになる。よって、get_new_inode()を呼び出し、ハッシュテーブルの挿入すべき位置をポインタで渡す。
  4. get_new_inode() は新しい inode をinode_cachep SLAB キャッシュから割り当てる。しかし、この操作は( GFP_KERNEL割り当てで)ブロックできるため、ハッシュテーブルを守る inode_lock spinlock をドロップしないといけない。spinlock をドロップしてしまったため、後で再びハッシュテーブルで inode を検索してみなくてはならない。再度それが見つかれば、ハッシュテーブルで見つけたものを復帰し、(__iget によって参照数を増やした後に)、新しく割り当てられた inode を破棄する。ハッシュテーブルでまだ見つからなければ、今さっき割り当てた新しい inode が、使われるべきものとなる。したがって、それは要求された値に初期化される。そして、ファイルシステムに特有の sb->s_op->read_inode() メソッドは、残りの inode を使うために起動される。 ということで、inode キャッシュバックのコードから、ファイルシステムコードへ見に行かなければならない。 ここで、ファイルシステムに特有の lookup() メソッドが iget()を起動したために、 inode キャッシュのコードを読んでいることを思い出してほしい。 s_op->read_inode() メソッドがディスクよりinodeを読み込む間、inode はロックされる。(i_state = I_LOCK()) read_inode() メソッドが復帰した後にロック解除される。そして、inode ロックを待っている全てが起こされる。
さて、このファイルディスクリプタをcloseしたときに、なにが起こるかをみていきます。close(2) システムコールは、do_close(fd, 1) を呼び出す fs/open.c:sys_close() 関数として実装されています。do_close(fd, 1) は、プロセスのファイルディスクリプタテーブルの記述子を剥ぎ取り(NULL で置き換える)、ほとんどの作業を行う filp_close() 関数を呼び出します。 fput()ではおもしろいことが起こります。これがファイルの最後のリファレンスであるならば、__fput()を呼びだす fs/file_table.c:_fput() が呼び出され、dcache と相互作用を起こします。(しかも inode cache とも作用します -- dcache は inode キャッシュのマスターであった!) fs/dcache.c:dput() は、 我々を iput(inode)を経由してinode キャッシュへ引き戻してくれるdentry_iput()を実行します。 したがって、fs/inode.c:iput(inode)は次のように理解することができます。

  1. もし渡されたパラメータが NULL ならば、本当に何もせず戻る。
  2. もし、fs特有の sb->s_op->put_inode() メソッドがあれば、(ブロックできるため)スピンロックを取得することなくすぐに起動される。
  3. inode_lockスピンロックが取得され i_count が1つ減少される。 もしこれがこの inode への最後のリファレンスで*なけれ*ば、単純にリファレンスが多すぎないかどうかを確認する。 これは、i_count が割り当てられた 32bit 以内に納めるためである。もし、リファレンスが多すぎる場合は警告を表示して返る。 ここで、inode_lock スピンロックを取得しているときに printk() を呼び出すが、これは大丈夫である。というのは、printk()はブロックされることはなく、したがっていついかなるコンテキスト(割り込みハンドラでさえ)からでも呼び出すことができるためだ。
  4. もし最後のアクティブなリファレンスであった場合には、必要な処理がある。

最後の inode リファレンスでの iput() が行う処理は、やや複雑なものです。そのため、リストでもその最後に分けてあります。

  1. もし i_nlink == 0 (例: ファイルがオープンしているときに unlink された)場合は、 inode はハッシュテーブルとtype list から削除される。もし、この inode のページキャッシュになにかデータページが保持されていたら、truncate_all_inode_pages(&inode->i_data) により削除される。 そして、ファイルシステム特有の s_op->delete_inode()メソッドが呼び出される。 これは通常は、ディスク上の inode を削除する。もし、s_op->delete_inode()メソッドがファイルシステムにより登録されていない(例: ramfs)場合には、clear_inode(inode)を呼び出し、これはもし登録されていればs_op->clear_inode()を呼び出す。また、inode が特定のブロックデバイスに関連していれば、デバイスのリファレンスカウントをbdput(inode->i_bdev)で落とす。
  2. もし i_nlink != 0 なら、同じハッシュバケットの他の inode があるかチェックする。そして、なければ inode は dirty ではないため、その type list から削除できる。 そして、inode_unusedリストへ追加し、inodes_stat.nr_unusedを増分する。 もし、これが匿名 inode であった(NetApp .snapshot)なら、タイプリストから削除し、完全にクリア/破壊する。

3.2 ファイルシステム登録/登録解除

Linuxカーネルは、新しいファイルシステムを最小限の努力で書くことができる機構を提供しています。その歴史的な理由とは次のようなものです。

  1. 人々がLinux以外のオペレーティングシステムを使っている世界で、古いソフトウエアへの投資を保護するために、Linuxは大変多くの異なったファイルファイルシステムのサポートにより相互運用性を提供しなければならなかった。そのため、多くのファイルシステムはそれ自身の存在はそれほど重要ではないが、現存するLinux以外のオペレーティングシステムがとの互換性をとるためだけの目的で存在する。
  2. ファイルシステムを記述するインターフェースはとてもシンプルなものでなければならなかった。 現存するプロプラエタリなファイルシステムを、読込みのみ可能なファイルシステムを書くことによってリバースエンジニアリングできるようにするために、ファイルシステムを記述するインターフェースはとてもシンプルなものでなければならなかった。 したがって、LinuxのVFSは読込み専用ファイルシステムの実装が非常に容易になっている。ファイルシステムのサポートを書く仕事のの95%は、書き込みのフルサポートを追加して完成させることに費やされるのである。具体的な例としては、私がLinux用に読込み専用のBFSファイルシステムを書くのに約10時間でできたのに対して、完全な書き込みサポートを追加して完成させるのに数週間を要したことを挙げられる(しかも、現在でも 完璧主義者から、"これはcompactificationサポートを持たないため"完成していないと主張されるのだ)。
  3. VFS インターフェースはエキスポートされており、したがってすべてのLinuxファイルシステムはモジュールとして実装できる。

ここで、Linuxでファイルシステムの実装に必要なステップを考えてみましょう。ファイルシステムを実装するコードは、動的にロードされるモジュールでも、静的にカーネルにリンクされるようにも構成することができます。 そして、 Linuxの元で行われる方法は非常に明白です。必要なことは、struct file_system_type構造体に情報を埋めて、これをVFSへregister_filesystem()関数によって登録することだけなのです。fs/bfs/inode.cの例だと、以下のようになります。


#include <linux/module.h>
#include <linux/init.h>

static struct super_block *bfs_read_super(struct super_block *, void *, int);

static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

module_init()/module_exit()マクロは、BFSがモジュールとしてコンパイルされたときに、関数init_bfs_fs()exit_bfs_fs()がそれぞれ、init_module()cleanup_module()になるようにします。

struct file_system_typeは、include/linux/fs.hで定義されます。


struct file_system_type {
        const char *name;
        int fs_flags;
        struct super_block *(*read_super) (struct super_block *, void *, int);
        struct module *owner;
        struct vfsmount *kern_mnt; /* For kernel mount, if it's FS_SINGLE fs */
        struct file_system_type * next;
};

それぞれのメンバは、次のように説明されます。

read_super()関数の仕事は、スーパーブロックのフィールドを埋めることです。 inodeの場所を割り当て、マウントされたファイルシステムのインスタンスに関連づけられたfs固有の情報を初期化します。

  1. バッファキャッシュbread()関数を使って、スーパーブロックをsb->s_dev項で指示されるデバイスから読み取る。
  2. スーパブロックが正しいマジックナンバーからなっているか確認し、全体的に正常であるかを「見」る。
  3. sb->s_opstruct super_block_operations構造体を指すように初期化する。この構造体は、「inodeを読み取る」とか、「inodeを削除する」などのような操作を実装しているファイルシステム特有の関数からなる。
  4. d_alloc_root()を用いてルートinodeとルートdentryを割り当てる。
  5. もしファイルシステムが読み取り専用でマウントされていなければ、sb->s_dirtを1に設定し、スーパーブロックからなるバッファをdirtyとマークする(TODO: なぜこれが必要?BFSでは、MINIXがそうしていたのでやっているが...)

3.3 ファイルデスクリプタ管理

Linuxにおいては、ユーザファイルデスクリプタとカーネルのinode構造体の間に間接的なレベルが存在しています。プロセスがopen(2)システムコール呼び出すとき、カーネルは、このファイルを次のI/O操作で使用する小さい正の整数を返します。この整数は、struct fileへのポインタの配列のインデックスになっています。各ファイル構造体は、file->f_dentryを通して、dentryを指しています。そして各dentryはdentry->d_inodeを通じてinodeを指しています。

各タスクは、include/linux/sched.hで定義されているstruct files_structを指しているtsk->filesフィールドを持っています。


/*
 * Open file table structure
 */
struct files_struct {
        atomic_t count;
        rwlock_t file_lock;
        int max_fds;
        int max_fdset;
        int next_fd;
        struct file ** fd;      /* current fd array */
        fd_set *close_on_exec;
        fd_set *open_fds;
        fd_set close_on_exec_init;
        fd_set open_fds_init;
        struct file * fd_array[NR_OPEN_DEFAULT];
};

file->countは参照カウンタであり、(通常、fget()から呼び出される)get_file()で増分され、fput()put_filp()で減少されます。fput()put_filp()の違いは、fput()が一般のファイルに通常必要な、ファイルロックの開放やdentryの開放などの追加の処理を行うのに対し、put_filp()は単にファイルテーブル構造体を操作するに過ぎないところにあります。つまり、カウンタの減少はfile_lockスピンロックの保護のもと、anon_listからファイルを削除し、それをfree_listへ追加するということです。

もし子のスレッドがクローンフラグ引数にCLONE_FILESを設定してclone()システムコールを呼んで作られたものであれば、tsk->filesは親と子の間で共有することができます。これは、(do_fork()によって呼ばれる)kernel/fork.c:copy_files()において見ることができ、これは、もしCLONE_FILESが、伝統的な従来の古典的UNIXのfork(2)における通常のファイルデスクリプタテーブルのコピーのかわりに設定されていれば、単にfile->countを増分させるだけです。

ファイルが開かれるとき、それに割り当てられるファイル構造体は、current->files->fd[fd]スロットへ組み込まれます。そしてビットマップcurrent->files->open_fdsfdビットがセットされます。これらのことは全て、current->files->file_lock読み書きスピンロックの書き込み保護のもとで行われます。もし、デスクリプタがcloseされれば、 current->files->open_fdsfdビットはクリアされ、このプロセスがファイルをつぎにオープン使用としたときに、最初の未使用のデスクリプタを見つけるためのヒントとして、current->files->next_fdfdと同じになるよう設定されます。

3.4 ファイル構造体管理

ファイル構造体は include/linux/fs.hで定義されています。


struct fown_struct {
        int pid;                /* pid or -pgrp where SIGIO should be sent */
        uid_t uid, euid;        /* uid/euid of process setting the owner */
        int signum;             /* posix.1b rt signal to be delivered on IO */
};

struct file {
        struct list_head        f_list;
        struct dentry           *f_dentry;
        struct vfsmount         *f_vfsmnt;
        struct file_operations  *f_op;
        atomic_t                f_count;
        unsigned int            f_flags;
        mode_t                  f_mode;
        loff_t                  f_pos;
        unsigned long           f_reada, f_ramax, f_raend, f_ralen, f_rawin;
        struct fown_struct      f_owner;
        unsigned int            f_uid, f_gid;
        int                     f_error;

        unsigned long           f_version;
  
        /* needed for tty driver, and maybe others */
        void                    *private_data; 
};

では struct file のメンバを見ていきましょう。

  1. f_list: このメンバは次のリストのうち一つ(そしてただひとつ)のファイル構造体をリンクする。a) このファイルシステムの全てのオープンしているファイルのsb->s_filesリスト、もし関連するinodeが匿名でなければ、(filp_open()からよばれる)dentry_open()は、このリストにファイルをリンクする。b) fs/file_table.c:free_list, 未使用のファイル構造体からなる。c) fs/file_table.c:anon_list, get_empty_filp()によって新しいファイル構造体が作られたときにこのリストにおかれる。すべてのリストはfiles_lockスピンロックによって保護される。
  2. f_dentry: このファイルに関連づけられるdentry。dentryは、open_namei()がnameidataを探すとき(あるいは、これを呼び出すpath_walk()が実行されたとき)に作られる。しかし、実際のfile->f_dentryメンバはdentry_open()によって、このようにして見つかったdentryにセットされる。
  3. f_vfsmnt: そのファイルのvfsmountファイルシステム構造体を指す。これは、dentry_open()によって設定されるが、open_namei()(ないしは、これを呼び出す path_init()) により nameidata の一部として見ることができる。
  4. f_op: このファイルで起動されるさまざまなメソッドからなるfile_operationsを指す。これは、nameidataの検索中にファイルシステム特有のs_op->read_inode()メソッドによって作られた、inode->i_fopからコピーされる。このセクションの後の方で、file_operationsメソッドは詳細に見ていくことにしよう。
  5. f_count: get_file/put_filp/fputにより操作されるリファレンスカウンタ。
  6. f_flags: dentry_open()によって(filp_open()が若干の変更を加えて)コピーされるopen(2)システムコールからのO_XXXフラグ。O_CREATO_EXCLO_NOCTTYO_TRUNCをクリアした後はこれらのフラグを長い間保存しておく意味はない。なぜなら、F_SETFLF_GETFL fcntl(2) システムコールで変更されることも、問い合わせられることもないからである。
  7. f_mode: ユーザ空間のフラグとモードの組み合わせ。dentry_open()によって設定される。変換のポイントは、読み書きのアクセスを別のビットへ格納することで、(f_mode & FMODE_WRITE)(f_mode & FMODE_READ)のようなチェックを容易にすることができる。
  8. f_pos: 次回のファイルの読み書きにおける現在のファイル位置。i386では、long long型、つまり64ビット値になる。
  9. f_reada, f_ramax, f_raend, f_ralen, frawin: readaheadをサポートする。 -- 複雑すぎて人間には議論できない ;)
  10. f_owner: 非同期I/O通知をSIGIO機構を通じて受け取るファイルI/Oの所有者。(fs/fcntl.c:kill_fasync()参照)
  11. f_uid, f_gid: ファイル構造体がget_empty_filp()によって作られたときにファイルを開いたプロセスのユーザIDとグループIDに設定される。ファイルがソケットのときは、ipv4ネットフィルタが使う。
  12. f_error: 書き込みエラーを返すためにNFSクライアントが利用する。これはfs/nfs/file.cで設定され、mm/filemap.c:generic_file_write()でチェックされる。
  13. f_version: キャッシュを取り消すためのバージョン管理メカニズム。(グローバル変数のeventを使って)f_posが変ったときにいつも増分される。
  14. private_data: ファイルシステムが使うことができる(例えば、coda はここに 証明書を格納する) 非公開のファイル毎のデータ。ないしは、デバイスドライバが使う。(devfsにある)デバイスドライバは、このメンバを、file->f_dentry->d_inode->i_rdevに符号化される、古典的なマイナー番号のかわりに、複数のインスタンス間を区別するのに使える。

さて、ファイルについて起動できるメソッドからできている file_operations構造体を見てみましょう。inode->i_fopからコピーされることや、それがs_op->read_inode()メソッドによって設定されることなどを覚えているでしょうか。これは、include/linux/fs.hで定義されています。


struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char *, size_t, loff_t *);
        int (*readdir) (struct file *, void *, filldir_t);
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
        int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, struct dentry *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
        ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
};

  1. owner: 論点となっているサブシステムを所有しているモジュールを指す。ドライバのみがこれをTHIS_MODULEに設定する必要があり、モジュールのカウントは、ドライバがopen/release時に制御する必要があるのに対して、mount/umountのときに制御されるため、ファイルシステムは幸運なことに無視できる。
  2. llseek: lseek(2)システムコールを実装している。通常、これは省略され、fs/read_wirte.c:default_llseek()が使われる。これは正しい方法である。(TODO: default_llseekを使うために現在これにNULLを設定しなければならなくなっている。このときは、llseek()if()を保存している)。
  3. read: read(2)システムコールの実装。ここでは、ファイルシステムは、mm/flemap.c:generic_file_read()を通常のファイルに、fs/read_write.c:generic_read_dir()(単純に-EISDIRを返す)をディレクトリに使用できる。
  4. write: write(2)システムコールの実装。ここでは、ファイルシステムは、mm/filemap.c:generic_file_write()を通常のファイルに使用でき、ディレクトリは無視できる。
  5. readdir: ファイルシステムが使う。通常のファイルは無視され、ディレクトリにはreaddir(2)getdents(2) システムコールが実装される。
  6. poll: poll(2)select(2) システムコールが実装される。
  7. ioctl: ドライバやファイルシステム特有の ioctl を実装している。一般のファイルに対する FIBMAPFIGETBSZFIONREAD のようなioctl は、高レベルで実装されておりf_op->ioctl() メソッドを読むことがない。
  8. mmap: mmap(2) システムコールを実装している。ファイルシステムは generic_file_mmapを一般のファイルに対してここで使うことができ、ディレクトリに対しては無視される。
  9. open: open(2) したときにdentry_open() が呼ぶ。ファイルシステムは滅多に使わない。例えば、coda はオープンしたときにファイルをローカルにキャッシュしようとする。
  10. flush: ファイルの各close(2)ごとに呼ばれる。必ずしも最後に呼ばれる必要はない。(以下のrelease()メソッド参照) これを使うファイルシステムというのは、唯一NFSのクライアントで、全てのdirtyページを書き出す。ここで、close(2)システムコールから、ユーザ空間に送り返されるエラーを返すことがある。
  11. release: ファイルの最後のclose(2)で呼ばれる。つまり、file->f_countが0であるときだ。整数を返すと定義されているにも関わらず、VFSでは返り値は無視される。(fs/file_table.c:__fput()を参照)
  12. fsync: fsync(2)/fdatasync(2)システムコールに直接対応する。最後の引数でfsyncになるかfdatasyncになるかを指定する。VFS はその際ほとんど何も仕事をしないが、ファイルディスクリプタを file 構造体に対応させること (file = fget(fd))、および inode->i_sem セマフォを操作することは行う。Ext2 ファイルシステムは最後の引数を無視するので、fsync(2)fdatasync(2) のシステムコールは全く同じ働きをする。
  13. fasync: このメソッドは、file->f_flags & FASYNC が真であるときに使われる。
  14. lock : ファイルシステムに固有なPOSIXのfcntl(2)のファイル領域ロック機構。ここでの唯一のバグは、fs 非依存(posix_lock_file())なものが呼ばれる前に呼ばれたとき、もし成功したが通常のPOSIXロックコードが失敗したら fs 依存レベルでは、ロック解除できなくなるということである。
  15. readv: readv(2)システムコールの実装。
  16. writev: writev(2)システムコールの実装。

3.5 スーパーブロックとマウントポイント管理

Linux においては、マウントされたファイルシステムについての情報は2つに分かれた構造体 super_blockvfsmount で保持されています。これは Linux は同じファイルシステム(ブロックデバイス)を複数のマウントポイントへマウントする、つまり同じsuper_block が複数のvfsmount構造体へ関連づけられるという意味ですが、これが許されていることが、2つに分かれて管理される理由になっています。

include/linux/fs.h で定義される struct super_block を最初にみていきます。


struct super_block {
        struct list_head        s_list;         /* Keep this first */
        kdev_t                  s_dev;
        unsigned long           s_blocksize;
        unsigned char           s_blocksize_bits;
        unsigned char           s_lock;
        unsigned char           s_dirt;
        struct file_system_type *s_type;
        struct super_operations *s_op;
        struct dquot_operations *dq_op;
        unsigned long           s_flags;
        unsigned long           s_magic;
        struct dentry           *s_root;
        wait_queue_head_t       s_wait;

        struct list_head        s_dirty;        /* dirty inodes */
        struct list_head        s_files;

        struct block_device     *s_bdev;
        struct list_head        s_mounts;       /* vfsmount(s) of this one */
        struct quota_mount_options s_dquot;     /* Diskquota specific options */

       union {
                struct minix_sb_info    minix_sb;
                struct ext2_sb_info     ext2_sb;
                ..... all filesystems that need sb-private info ...
                void                    *generic_sbp;
        } u;
       /*
         * The next field is for VFS *only*. No filesystems have any business
         * even looking at it. You had been warned.
         */
        struct semaphore s_vfs_rename_sem;      /* Kludge */

        /* The next field is used by knfsd when converting a (inode number based)
         * file handle into a dentry. As it builds a path in the dcache tree from
         * the bottom up, there may for a time be a subpath of dentrys which is not
         * connected to the main tree.  This semaphore ensure that there is only ever
         * one such free path per filesystem.  Note that unconnected files (or other
         * non-directories) are allowed, but not unconnected diretories.
         */
        struct semaphore s_nfsd_free_path_sem;
};

super_block 構造体には様々なメンバがあります。

  1. s_list: 全てのアクティブなスーパーブロックのダブルリンクリスト。ここで、「全てのマウントされたファイルシステム」と言わないのは、Linux では、一つのスーパブロックに関連づけられたマウントされたファイルシステムの複数のインスタンスを持てるためである。
  2. s_dev: マウントされるのにブロックを必要とするファイルシステム、つまり FS_REQUIRES_DEV なファイルシステムにとっては、このメンバはそのブロックデバイスの i_dev である。その他の (匿名ファイルシステムと呼ばれる) ファイルシステムに対しては、これは MKDEV(UNNAMED_MAJOR, i) で表される整数である。ただし i は、unnamed_dev_in_use ビット配列の中で最初の OFF ビットを指す、1 から 255 の範囲の値である。fs/super.c:get_unnamed_dev()/put_unnamed_dev() を参照のこと。匿名ファイルシステムが s_dev メンバを利用すべきでないということは、これまで何度も提案されてきた。
  3. s_blocksize, s_blocksize_bits: ブロックサイズとlog2(ブロックサイズ)。
  4. s_lock: スーパーブロックが、lock_super()/unlock_super() によって現在ロックされているかどうかを示す。
  5. s_dirt: スーパーブロックが変更されたときに設定され、ディスクへ書き戻されたときはいつもクリアされる。
  6. s_type: 関連するファイルシステムの struct file_system_type を指す。fs 特有のread_super()が成功したときに VFS fs/super.c:read_super() が設定し、失敗したとき NULL にリセットするため、ファイルシステムの read_super() メソッドは設定する必要がない。
  7. s_op: fs 特有のread/write inodeなどのメソッドからなる super_operations 構造体へのポインタ。s_op を正しく初期化するのは、ファイルシステムの read_super() メソッドの仕事である。
  8. dq_op: ディスククオタ操作。
  9. s_flags: スーパーブロックフラグ。
  10. s_magic: ファイルシステムのマジック番号。minixファイルシステムでは、それ自身の複数バージョンを区別するために使われる。
  11. s_root: ファイルシステムのルートのdentry。ルートの inode をディスクから読み、d_alloc_root()を呼んでdentryを割り当てて実体化するために渡すのは、read_super() の仕事である。
  12. s_wait: スーパーブロックがロック解除されるのを待つプロセスの待ちキュー。
  13. s_dirty: 全てのdirty な inode のリスト。もし inode が汚れて(inode->i_state & I_DIRTY)いれば、スーパーブロック特有のinode->i_list経由でリンクされた dirty リストにおかれる。
  14. s_files: このスーパーブロックでオープンしているファイル全てのリスト。ファイルシステムを read-only で再マウントできるかを判断するときに有用。sb->s_files リストをみて、もしファイルが書き込みオープンされている(file->f_mode & FMODE_WRITE)か、unlinkが保留されている(inode->i_nlink == 0)ファイルがあれば、再マウントを拒否するfs/file_table.c:fs_may_remount_ro()を参照すること。
  15. s_bdev: FS_REQUIRES_DEV のために、これは ファイルシステムがマウントされているデバイスを表す block_device 構造体を指している。
  16. s_mounts: 全ての vfsmount 構造体のリスト。マウントされたこのスーパーブロックの各インスタンスの???
  17. s_dquot: ディスククオタ用。

スーパーブロックの操作は、include/linux/fs.h で定義されている super_operations 構造体に記述されている。


struct super_operations {
        void (*read_inode) (struct inode *);
        void (*write_inode) (struct inode *, int);
        void (*put_inode) (struct inode *);
        void (*delete_inode) (struct inode *);
        void (*put_super) (struct super_block *);
        void (*write_super) (struct super_block *);
        int (*statfs) (struct super_block *, struct statfs *);
        int (*remount_fs) (struct super_block *, int *, char *);
        void (*clear_inode) (struct inode *);
        void (*umount_begin) (struct super_block *);
};

  1. read_inode: inodeをファイルシステムから読み込む。iget4()(したがって、iget())を通して、fs/inode.c:get_new_inode() からのみ呼ばれる。もしファイルシステムがiget()を使いたいならば、read_inode()は実装されないといけない。そうでなければ、get_new_inode()はパニックになる。 inodeが読まれている間はロックされる(inode->i_state = I_LOCK)。関数から復帰するとき、全てのinode->i_waitの待ちは起こされる。ファイルシステムのread_inode()メソッドの仕事は、読み込まれる inode からなるディスクブロックを割り当て、バッファーキャッシュ bread()関数を使って読み込み,inode 構造体の様々なフィールドが初期化される. 例えばinode->i_opinode->i_fop では、VFSレベルが、inode や関連するファイルでなんの操作が行われるかを知っている read_inode()を実装しないファイルシステムは、ramfsとpipefsである。たとえば、ramfsは、必要になったときに全てのinode の操作が呼ぶ、自身のinode生成関数ramfs_get_inode()を持っている。
  2. write_inode: inode をディスクに書き戻す。read_inode()と似て、ディスクに同様なブロックを割り当て、mark_buffer_dirty(bh)を呼ぶことで、バッファキャッシュと作用する。このメソッドは、inodeが、それぞれやファイルシステム全体の同期の一部なりで同期される必要があるとき、dirty な inode (mark_inode_dirty()によって dirty と印をつけられている)に対して呼ばれる。
  3. put_inode: リファレンスカウンタが減少させられるときにいつも呼ばれる。
  4. delete_inode: inode->i_countinode->i_nlinkが0になるときは常に呼ばれる。ファイルシステムは、オンディスクのinodeのコピーを削除し、VFSでclear_inode()を呼び出して、「完全に根絶する」。 -->
  5. put_super: umount(2)システムコールの最終段階で呼ばれ、ファイルシステムに対して、このインスタンスについてファイルシステムが保持しているいかなる内部の情報を解放しなければならないことを通知する。典型的にはこれは、スーパーブロックがあるブロックをbrelse()し、フリーブロックやinodeなどのために割り当てられたビットマップを全てkfree()する。
  6. write_super: スーパーブロックをディスクに書き戻す必要があるときに呼ばれる。これは、スーパブロック(通常sb-private領域にある)とmark_buffer_dirty(bh)があるブロックを見つけなければならない。これはまた、sb->s_dirtフラグをクリアする。
  7. statfs: fstatfs(2)/statfs(2)システムコールの実装。引数として渡されるstruct statsへのポインタは、ユーザポインタではなくカーネルポインタであるため、ユーザ空間へのI/Oが不必要であることに注意すること。もし実装されていなければ、statfs(2)ENOSYSで失敗する。
  8. remount_fs: ファイルシステムを再マウントするときには必ず呼ばれる。
  9. clear_inode: VFSレベルclear_inode()から呼ばれる。プライベートデータをinode構造体へ(generic_ipメンバを経由して)割り当てるファイルシステムは、ここで解放されなければならない。
  10. umount_begin: なにもファイルシステムをビジーにしないことを確かめるのに全力を尽くすことができるように、あらかじめファイルシステムへ通知するため、umountを強制する間に呼ばれる。現在、NFSでのみ使われる。これは、一般的なVFSレベルの強制umountサポートとの関係はない。

それでは、on-disk (FS_REQUIRES_DEV)ファイルシステムをマウントするときに起ることを見ていきましょう。 mount(2)システムコールの実装は、fs/super.c:sys_mount() にあり、これは実際の処理を行う do_mount() 関数にオプションとファイルシステムタイプやデバイス名をコピーする単なるラッパーになっています。

  1. ファイルシステムのドライバは必要になったときにロードされ、モジュールの参照カウントが増分される。ここで、マウントの最中にファイルシステムのモジュールについての参照カウントについては、2つ増分されることに注意が必要である。do_mount()get_fs_type()を呼ぶときに1回、read_super()が成功したならば、get_sb_dev()get_filesystem()を呼ぶときに1回である。1つめの増分は、read_super()の内部において、モジュールがアンロードされるのを避けるためであり、2つめの増分は、マウントされたインスタンスによってモジュールが使用中であることを示すためのものである。明らかに、各マウント後の全体でのカウントは1つだけ増えることから、do_mount()は戻る前にカウンタを減少させるのである。
  2. 我々の場合、fs_type->fs_flags & FS_REQUIRES_DEVが真であるなら、ブロックデバイスへのリファレンスを取得し、ファイルシステムのread_super()メソッドと連携してスーパーブロックを埋めるget_sb_bdev()を呼ぶことで、スーパーブロックが初期化される。全てがうまく行けば、super_block構造体は初期化され、ファイルシステムモジュールへのextra参照と、下にあるブロックデバイスへの参照を得ることになる。
  3. 新しいvfsmount構造体が割り当てられ、sb->s_mountsリストとグローバルなvfsmntlistリストへリンクされる。vfsmountのメンバのmnt_instancesは全ての同じスーパーブロックにマウントされているインスタンスを見つけられるようにする。 mnt_listメンバは、システム全体の全てのスーパーブロックに対して、全てのインスタンスを見つけることができるようにする。mnt_sbメンバはこのスーパーブロックを指し、mnt_rootsb->s_root dentry への新しい参照を持つ。

3.6 仮想ファイルシステムの例: pipefs

マウントにブロックデバイスが必要ない簡単な Linux ファイルシステムの例として、fs/pipe.cからpipefsを取り上げましょう。ファイルシステムの前提部分はかなり簡単で、ちょっとした説明で十分です。


static DECLARE_FSTYPE(pipe_fs_type, "pipefs", pipefs_read_super,
        FS_NOMOUNT|FS_SINGLE);

static int __init init_pipe_fs(void)
{
        int err = register_filesystem(&pipe_fs_type);
        if (!err) {
                pipe_mnt = kern_mount(&pipe_fs_type);
                err = PTR_ERR(pipe_mnt);
                if (!IS_ERR(pipe_mnt))
                        err = 0;
        }
        return err;
}

static void __exit exit_pipe_fs(void)
{
        unregister_filesystem(&pipe_fs_type);
        kern_umount(pipe_mnt);
}

module_init(init_pipe_fs)
module_exit(exit_pipe_fs)

このファイルシステムは、ユーザ空間からマウントできず、システム全体で一つのスーパーブロックのみ持てることを意味するFS_NOMOUNT|FS_SINGLE型になっています。 FS_SINGLEであるファイルは同様に、register_filesystem()を使って登録成功したあとに、kern_mount()を使ってマウントされなければならないことを意味しています。これは、まさに、init_pipe_fs()で行われることです。この関数の唯一のバグは、もしkern_mount()が失敗した場合(例えば、kmalloc()add_vfsmnt()中で失敗したときなど)に、ファイルシステムは登録された状態で残りますが、モジュールの初期化は失敗になることです。 これは、cat /proc/filesystemsの Oops エラーの原因になります。(丁度 Linus に、これを知らせるパッチを送った。pipefs はモジュールとしてコンパイルできないためこれは真のバグではないが、将来モジュールになるときを考えて書かれるべきだと思う)。

register_filesystem()の結果として、pipe_fs_typefle_systemsリストにリンクされることになります。つまり/proc/filesystemsを読み出して、FS_REQUIRES_DEVが設定されていないことを示す「nodev」フラグつきで「pipefs」が登録されているのを見つけられることになります。/proc/filesystemsファイルは、新しいFS_フラグを全てサポートするよう、まさに機能強化されるべきです(そして、私はそのパッチを作りました)。しかし、これを使う全てのユーザのアプリケーションが動かなくなるため、今だ実行することができません。 Linux カーネルインターフェースが随時変更されている(単に良くするために)にも関わらず、ユーザ空間での互換性に及ぶとなると、Linux はとても保守的なオペレーティングシステムとなり、長い期間再コンパイルをしないで多くのアプリケーションが使えるようにするのです。

kern_mount()の結果は次のようになります。

  1. 新たな無名の(anonymous)デバイス番号が割り当てられ、unnamed_dev_in_useビットマップのビットが設定される。もし割り当てるビットがなければ、kern_mount()EMFILEで失敗する。
  2. get_empty_super()によって、新たなスーパーブロック構造体が割り当てられる。get_empty_super()関数は、super_blockによって先頭を示されるスーパーブロックのリストを見て、s->s_dev == 0となる空のエントリーを探す。
  3. ファイルシステム特有のpipe_fs_type->read_super()メソッド(つまりpipefs_read_super()だが)が起動され、ルートのinodeとルートのdentryとなる sb->s_rootを割り当て、sb->s_op&pipefs_opsになるよう設定する。
  4. そして kern_mount() が 新しいvfsmount構造体を割り当て、vfsmntlistsb->s_mounts へリンクするadd_vfsmnt(NULL, sb->s_root, "none") を呼ぶ。
  5. pipe_fs_type->kern_mntは新しいvfsmount構造体に設定されて戻る。kern_mount()返り値がvfsmount構造体になっているのは、FS_SINGLEのファイルシステムでさえ複数回マウントできることから、mnt->mnt_sbkern_mount()の複数回の呼び出しからの戻りであったとしても同じものを指して戻るためである。

これでファイルシステムは登録され、私たちが使えるようにカーネル内にマウントされました。pipefsファイルシステムのエントリポイントは、pipe(2)システムコールとなり、これはアーキテクチャ依存のsys_pipe()へ実装されていますが、実際にはアーキテクチャ非依存のfs/pipe.c:do_pipe()関数で処理されます。これからdo_pipe()を見ていこうと思います。pipefs との相互作用は、do_pipe()get_pipe_inode()を新しい pipefs の inode を割り当てるために呼び出したときに起こります。この inode のために、inode->i_sbは、pipefs のスーパーブロックpipe_mnt->mnt_sbへ設定されます。そしてファイルの操作i_foprdwr_pipe_fopsに設定されて、(inode->i_pipeに保持される)読み書きする者の数は 1 に設定されます。fs-private 共用体へ保存する代りに、別の inode メンバ i_pipe がある理由は、pipe と FIFO が同じコードを共有しているためと、同じ共用体の中の他のアクセスパスを使う複数の FIFO が他のファイルシステム上に存在できるようにあうるためです。これはとても悪い C の使い方であり、純粋に運だけで動きます。そのため、そう、2.2.x カーネルは、運だけで動いているのです。inode のフィールドをすこし調整すればすぐに処理を停止してしまうことになるでしょう。

pipe(2)システムコール毎に、pipe_mntマウントインスタンスの参照カウンタは増分されます。

Linux においては パイプは対称(双方向あるいはSTREAM pipe)ではなく、つまりファイルの両側で、read_pipe_fopswrite_pipe_fopsそれぞれの file->f_op 操作は持てません。

3.7 ディスクファイルシステムの例:BFS

簡単なディスク上の Linux ファイルシステムの例としてBFSを考えてみましょう。BFSモジュールのプリアンブルはfs/bfs/inode.cにあり、


static DECLARE_FSTYPE_DEV(bfs_fs_type, "bfs", bfs_read_super);

static int __init init_bfs_fs(void)
{
        return register_filesystem(&bfs_fs_type);
}

static void __exit exit_bfs_fs(void)
{
        unregister_filesystem(&bfs_fs_type);
}

module_init(init_bfs_fs)
module_exit(exit_bfs_fs)

となっています。

特別なfstype 定義マクロDECLARE_FSTYPE_DEV()が使われて、fs_type->flagsFS_REQUIRES_DEVに設定され、BFS がマウントする実際のブロックデバイスが必要であることを示しています。

モジュールの初期化関数はファイルシステムと(BFSがモジュールとして設定されたときのみ存在する)登録解除を行う解除関数をVFSに登録します。

ファイルシステムの登録を行うことで、マウントを進めることができます。マウントではfs/bfs/inode.c:bfs_read_super() に実装されている fs_type->read_super()メソッドが起動されます。この関数は次のような処理を行います。

  1. set_blocksize(s->s_dev, BFS_BSIZE): ブロック型デバイス層とバッファキャッシュを通して相互作用しようとしていることから、二つ三つの初期化を行う必要がある。すなわち、ブロックサイズを設定して、さらに s->s_blocksizes->s_blocksize_bits メンバを通じて VFS へ通知するのだ。
  2. bh = bread(dev, 0, BFS_BSIZE): デバイスのブロック0を読み、s->s_dev を通じて渡す。このブロックはファイルシステムのスーパーブロックである。
  3. スーパーブロックはBFS_MAGIC番号に対して正当性を確認し、sbプライベートメンバs->su_sbhへ格納する(実際はs->u.bfs_sb.si_sbhである)。
  4. そして、 inode ビットマップをkmalloc(GFP_KERNEL) を使って割り当て、1に設定される最初の2つを除く全てのビットを0に設定する。これは、inode 0と1をずっと割り当てないことを示すためである。inode 1はルートであり、関連するビットはいずれ数行後に1に設定される。というのは、ファイルシステムはマウントされるときには必ず妥当なルート inode を持ってるべきものだからだ。
  5. そしてs->s_opを初期化する。これはs_op->read_inode()が起動される結果としてiget()を通じて、この時点から inode キャッシュを呼び出すことができる。これは、(inode->i_inoinode->i_devによる)特定のinode からなるブロックを見つけて読み込む。 もしルート inode の取得が失敗したら、inode ビットマップを解放し、バッファーキャッシュの背後にあるスーパーブロックバッファーを解放して、NULL を返す。もしルート inode の読み込みがOKであれば、(ルートとなる)/という名前のdentryを割り当て、この inode を実体化する。
  6. さて、ファイルシステムの全ての inode を見て、内部の inode ビットマップの関連するビットを設定するために、それを全て読み込む。そして、最後の inode のオフセットや最後のファイルの開始/終了ブロックのようなその他の内部パラメータを計算する。読み込む各 inode は inode キャッシュへ iput()を通じて戻される。そう、必要以上に長く参照を保持できないのだ。
  7. もしファイルシステムが読み込み専用でマウントされなければ、スーパーブロックバッファをdirtyと印をつけ、s->s_dirtフラグを設定する(TODO: なぜこれをやるのか? 普通は、これはminix_read_super()がやるからなのだが、minixもBFS もread_super()でスーパーブロックを書き換えていないようなのだ)。
  8. 全てがうまく行けば、VFS層の呼び出し、つまりfs/super.c:read_super()へ、この初期化されたスーパーブロックを返す。

read_super()関数が成功して戻った後、fs/super.c:get_sb_bdev()get_filesystem(fs_type)呼び出しを通じて VFS はファイルシステムモジュールへの参照とブロックデバイスへの参照を得ます。

ここで、ファイルシステムへ I/O を行うときになにが起るかを考えてみましょう。すでに iget()が呼ばれるときにinodeがどのように読まれるか、またiput()でどのように解放されるかは見てきました。 inode の読み込みは、他のことに混じってinode->i_opinode->i_fopの事前設定を行うことになります。ファイルのオープンは、inode->i_fopからfile->f_opへ伝搬します。

さて、link(2)システムコールのコードの通り道をみていきましょう。システムコールの実装はfs/namei.c:sys_link()にあります。

  1. ユーザ空間の名前はカーネル空間へエラーチェックを行うgetname()関数を使ってコピーされる。
  2. これらの名前はpath_init()/path_walk()を使って nameidata に変換され、dcache と相互作用する。結果は old_ndnd構造体へ格納される。
  3. old_nd.mnt != nd.mntであったら、「クロスデバイスリンク」EXDEVが返される。ファイルシステムを超えたリンクはできないが、Linux ではそれをファイルシステムのマウントされたインスタンス(あるいは特定のファイルシステム間)を超えたリンクができないと解釈している。
  4. 新しい dentry が作られndlookup_create()によって関連づけられる。
  5. 一般的なvfs_link()関数が呼ばれ、ディレクトリに新しいエントリーを作ることができるかどうかチェックする。そして、dir->i_op->link()メソッドを呼び出す。これはファイルシステムに特有のfs/bfs/dir.c:bfs_link()関数を呼び出すことになる。
  6. bfs_link()の内部では、ディレクトリをリンクしようとしていないかチェックし、もしそうであればEPERMエラーで拒否する。これは標準ファイルシステム(ext2)を同じ挙動である。
  7. へルパー関数bfs_add_entry()を呼び出して新しいディレクトリエントリを特定のディレクトリに追加しようとする。この関数は、使用していないスロット(de->ino == 0)を探して全てのエントリーを見ていき、見つかったならば、 name/inode の対を関連するブロックへ書き出して、 dirtyの印を(スーパブロック以外の優先度で)つける。
  8. もし、ディレクトリエントリの追加が成功したら、操作が失敗することはないため、inode->i_nlinkを増分し、inode->i_ctimeを更新して、inode と一緒にインスタンス化した新しいdentryと同様に、この inode を dirty とマークする。

unlink()/rename()のような他の関連 inode 操作も、同じようなやり方で働きます。したがって、あまりメリットがないため、ここで全ての詳細を扱うことはしません。

3.8 実行ドメインとバイナリフォーマット

Linux は、ディスクからのユーザアプリケーションバイナリの読み込みをサポートしています。もっと面白いことには、そのバイナリは、他のフォーマットで保存されていても構いません。プログラムに対するシステムコールを通じたオペレーティングシステムの反応は、 他の UNIX に見られるフォーマット(COFFなど)をエミュレートし、さらに他のタイプ(Solaris, Unixwareなど)のシステムコールの振る舞いをエミュレートするために、必要ならば、標準(Linux の振る舞いをする標準)から外れることができます。これが、実行ドメインとバイナリフォーマットが必要になる理由になっています。

各 Linux のタスクは、task_struct(p->personality)のなかにそれぞれのパーソナリティを持っています。現在(正式なカーネルも追加パッチでも含め)存在するパーソナリティには、FreeBSD、Solaris、UnixWare、OpenServerのサポートを含み、さらに他のメジャーなオペレーティングシステムもあります。 その current->personality の値は2つの部分に分かれます。

  1. 高位の3バイト - バグエミュレーション: STICKY_TIMEOUTS, WHOLE_SECONDSなど
  2. 低位の1バイト - パーソナリティに固有な番号

パーソナリティの変更により、私たちはオペレーティングシステムがシステムコールを取り扱う方法を変えることができます。たとえば、STICKY_TIMEOUTcurrent->personalityへ与えることで、select(2)システムコールは、残り時間を保存するかわりに、最後の引数(タイムアウト)の値を維持します。プログラムの中には(Linuxにはない)バグのあるオペレーティングシステムに依存したバグのあるものが存在するため、ソースコードが入手できずバグを改修できない場合のために、Linux はバグをエミュレートする方法を提供しているのです。

実行ドメインは、パーソナリティの延長線上にあり、一つのモジュールとして実装されています。通常一つの実行ドメインは一つのパーソナリティを実装していますが、なかには、それほど多くない条件を満たすことで一つのモジュールの中に「閉じた」複数のパーソナリティを実装することもできます。

実行ドメインはkernel/exec_domain.cに実装され、2.4 カーネルで 2.2.x から完全に書き直されました。サポートしているパーソナリティの種類に加え、カーネルが現在サポートしている実行ドメインのリストは、/proc/execdomainsファイルを読み出すことで得ることができます。PER_LINUX を除く実行ドメインは、動的に読み込まれるモジュールとして実装できます。

ユーザインターフェースは、personality(2)システムコールを通じて、現在のプロセスのパーソナリティを設定できるようになっています。あるいは、もし引数が存在しないパーソナリティ 0xffffffff であったらcurrent->personalityをただ返します。明らかにこのシステムコールの振る舞いは、パーソナリティとは独立です。

実行ドメイン登録のカーネルインターフェースは2つの関数になっています。

exec_domains_lockが読み書きロックである理由は、登録と登録解除要求のみがリストを書き換え、cat /proc/filesystemsfs/exec_domain.c:get_exec_domain_list()を呼ぶことだけが、リストの読み出しを行うためです。新しい実行ドメインの登録により、「lcall7 ハンドラ」とシグナル番号の変換マップを定義されます。実際、ABIパッチは、この実行ドメインの考え方を拡張して、(ソケットオプションやソケットタイプ、アドレスファミリィやエラー番号マップといった)追加の情報を持つようにしています。

バイナリフォーマットは同じように実装されています。つまり、単リンクリストフォーマットをfs/exec.cで定義し、読み書きロックのbinfmt_lockで保護します。exec_domains_lockのように、binfmt_lockは、バイナリフォーマットの登録や登録解除のときを除くほとんどの場合に読み込みのみになっています。 新しいバイナリフォーマットを登録することで、core_dump()ができるのと同様に、新しいload_binary/load_shlib()関数によって、execve(2)システムコールは機能拡張されます。load_binary()メソッドが、execve(2)システムコールを実装しているdo_execve()からsearch_binary_handler()によって呼ばれているときに、load_shlib()メソッドは古いuselib(2)システムコールによってのみ使わます。

プロセスのパーソナリティは、若干の発見的手法をつかって対応するフォーマットのload_binary()メソッドから読み込まれたバイナリーフォーマットによって決定されます。 UnixWare7バイナリを決定する例としては、まずelfmark(1)ユーティリティを使って、ELFのへッダーのe_flagsに、マジック値 0x314B4455に設定し、印をつけます。 このマジック値は、ELFのロード時に検出され、current->personalityにPER_UW7を設定します。

一度パーソナリティ(と、従ってcurrent->exzec_domain)が分かれば、システムコールは次のように取り扱います。プロセスが lcall7 ゲート命令を使ってシステムコールを発行したとしましょう。 これは制御を arch/i386/kernel/entry.SENTRY(lcall7)に移します。なぜならこれは、arch/i386/kernel/traps.c:trap_init()に準備されているためです。 適切なスタック割り当ての変換のあと、entry.S:lcall7current から exec_domain へのポインタと、そして(アセンブラで4にハードコードされており、struct exec_domainのCの定義で、handlerメンバを移動できないのであるが)exec_domain中の lcall7 ハンドラのオフセットを得て、そこへジャンプします。そうです、Cでは、次のようになります。


static void UW7_lcall7(int segment, struct pt_regs * regs)
{
       abi_dispatch(regs, &uw7_funcs[regs->eax & 0xff], 1);
}

abi_dispatch()は、関数ポインタの表へのラッパーであり、それはこのパーソナリティのシステムコールのuw7_funcsを実装したものになっています。


次のページ 前のページ 目次へ