複数のファイルシステムをサポートするため、 Linux は VFS (Virtual Filesystem Switch) という特別なカーネルインターフェースレベルを持っています。 これは SVR4 から派生した OS に見られる vnode/vfs インターフェースに良く似ています (もともとは、BSD と Sun に由来の実装からきています)。
Linux の inode キャッシュは、一つの 977 行からなるファイル fs/inode.c
に実装されています。
特筆すべき事に、このファイルは、この 5-7 年の間ほとんど変更がありませんでした。
一番最後に更新されたのは、そう 1.3.42 の時なのです。
Linux の inode キャッシュの構造は以下の通りです。
inode_hashtable
。 スーパーブロックの無い inode (inode->i_sb == NULL
) は、代りにanon_hash_chain
で始まるダブルリンクリストへとつながれる。匿名 inodeの例として、fs/inode.c:get_empty_inode()
を呼ぶことで、net/socket.c:sock_alloc()
によって作られるソケットがある。
i_count>0
かつ i_nlink>0
となる inode からなるグローバルタイプの in_use_list(inode_in_use
)。inode は、get_empty_inode()
と get_new_inode()
により新たに割り当てられ、inode_in_use
リストへつながれる。
i_count = 0
であるような グローバルタイプの未使用リスト (inode_unused
)。
i_count>0
、i_nlink>0
そしてi_state & I_DIRTY
であるような inode によるスーパーブロック毎の汚れたリスト(sb->s_dirty
)。inodeが dirtyの印がつけられたら、それはやはりハッシュになっているsb->s_dirty
リストに追加される。スーパーブロック毎に inode の dirty リストを管理することで、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-nr
やproc/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.h
の list_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つの部分に分けられ、
open_namei()
: dentryとvfsmount構造体からなるnameiデータ構造体を埋める。
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)
をたたきます。これは、
inode_lock
の保護のもと、ハッシュテーブルの中から、スーパブロックと inode 番号が一致する inode を見つけようとする。もし、inode が見つかれば、その参照カウンタ (i_count
) を増分する。
もし、増加の前の値が 0 であったらば、inode は dirty ではないので、どのタイプのリスト(iode->i_list
)からであれ削除する。これは、現在 on (それはもちろんinode_unused
リストでなければならない)であり、inode_in_use
タイプのリストに挿入される。最後に、inodes_stat.nr_unused
が減少される。
iget4()
はロックされていない inode を返すことが保証される。
get_new_inode()
を呼び出し、ハッシュテーブルの挿入すべき位置をポインタで渡す。
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 ロックを待っている全てが起こされる。
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)
は次のように理解することができます。
sb->s_op->put_inode()
メソッドがあれば、(ブロックできるため)スピンロックを取得することなくすぐに起動される。inode_lock
スピンロックが取得され i_count
が1つ減少される。
もしこれがこの inode への最後のリファレンスで*なけれ*ば、単純にリファレンスが多すぎないかどうかを確認する。
これは、i_count
が割り当てられた 32bit 以内に納めるためである。もし、リファレンスが多すぎる場合は警告を表示して返る。
ここで、inode_lock
スピンロックを取得しているときに printk()
を呼び出すが、これは大丈夫である。というのは、printk()
はブロックされることはなく、したがっていついかなるコンテキスト(割り込みハンドラでさえ)からでも呼び出すことができるためだ。
最後の inode リファレンスでの iput()
が行う処理は、やや複雑なものです。そのため、リストでもその最後に分けてあります。
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)
で落とす。
i_nlink != 0
なら、同じハッシュバケットの他の inode があるかチェックする。そして、なければ inode は dirty ではないため、その type list から削除できる。 そして、inode_unused
リストへ追加し、inodes_stat.nr_unused
を増分する。
もし、これが匿名 inode であった(NetApp .snapshot)なら、タイプリストから削除し、完全にクリア/破壊する。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;
};
それぞれのメンバは、次のように説明されます。
filesystemsproc
ファイルへ現われます。そして、ファイルシステムを名前から見つけるときのキーとして使われます。これと同じ名前は、mount(2)へ指定するファイルシステムのタイプとしても使われ、ファイルシステムごとに一意でなければなりません。
FS_REQUIRES_DEV
は、ブロックデバイス上にだけマウント可能なファイルシステムという意味。FS_SINGLE
は、唯1つのスーパーブロックをもつファイルシステム、FS_NOMOUNT
はユーザスペースからマウントできない、つまりmount(2)システムコールでマウントできないファイルシステム。しかしこれは、内部的にはkern_mount()
インターフェースを使えばマウントできる (例 pipefs)。
FS_SINGLE
の場合を除いて常に失敗し、
fs_type->kern_mnt->mnt_sb
を(fs_type->kern_mnt = NULL
)のように参照しようとして、get_sb_sinle()
でOops エラーが発生する。
THIS_MODULE
によって自動で適切に設定される。
FS_SINGLE
ファイルシステムのみ利用。これはkern_mount()
が設定する。(TODO: kern_mount()
はFS_SINGLE
が設定されていないファイルシステムのマウントを拒否すべきである)。
file_systemsによる
片方向リストの先頭へのリンク (fs/super.c
を参照)。リストは、file_systems_lock
読み書きスピンロックにより保護されており、register/unregister_filesystem()
関数は、リストからエントリを取り外したり、入れたりすることで変更を行う。read_super()
関数の仕事は、スーパーブロックのフィールドを埋めることです。
inodeの場所を割り当て、マウントされたファイルシステムのインスタンスに関連づけられたfs固有の情報を初期化します。
bread()
関数を使って、スーパーブロックをsb->s_dev
項で指示されるデバイスから読み取る。sb->s_op
をstruct super_block_operations
構造体を指すように初期化する。この構造体は、「inodeを読み取る」とか、「inodeを削除する」などのような操作を実装しているファイルシステム特有の関数からなる。
d_alloc_root()
を用いてルートinodeとルートdentryを割り当てる。
sb->s_dirt
を1に設定し、スーパーブロックからなるバッファをdirtyとマークする(TODO: なぜこれが必要?BFSでは、MINIXがそうしていたのでやっているが...)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_fds
のfd
ビットがセットされます。これらのことは全て、current->files->file_lock
読み書きスピンロックの書き込み保護のもとで行われます。もし、デスクリプタがcloseされれば、 current->files->open_fds
のfd
ビットはクリアされ、このプロセスがファイルをつぎにオープン使用としたときに、最初の未使用のデスクリプタを見つけるためのヒントとして、current->files->next_fd
はfd
と同じになるよう設定されます。
ファイル構造体は 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
のメンバを見ていきましょう。
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
スピンロックによって保護される。
open_namei()
がnameidataを探すとき(あるいは、これを呼び出すpath_walk()
が実行されたとき)に作られる。しかし、実際のfile->f_dentry
メンバはdentry_open()
によって、このようにして見つかったdentryにセットされる。
vfsmount
ファイルシステム構造体を指す。これは、dentry_open()
によって設定されるが、open_namei()
(ないしは、これを呼び出す path_init()
) により nameidata の一部として見ることができる。
file_operations
を指す。これは、nameidataの検索中にファイルシステム特有のs_op->read_inode()
メソッドによって作られた、inode->i_fop
からコピーされる。このセクションの後の方で、file_operations
メソッドは詳細に見ていくことにしよう。
get_file/put_filp/fput
により操作されるリファレンスカウンタ。
dentry_open()
によって(filp_open()
が若干の変更を加えて)コピーされるopen(2)システムコールからのO_XXX
フラグ。O_CREAT
、O_EXCL
、O_NOCTTY
、O_TRUNC
をクリアした後はこれらのフラグを長い間保存しておく意味はない。なぜなら、F_SETFL
や F_GETFL
fcntl(2) システムコールで変更されることも、問い合わせられることもないからである。
dentry_open()
によって設定される。変換のポイントは、読み書きのアクセスを別のビットへ格納することで、(f_mode & FMODE_WRITE)
や(f_mode & FMODE_READ)
のようなチェックを容易にすることができる。
long long
型、つまり64ビット値になる。
SIGIO
機構を通じて受け取るファイルI/Oの所有者。(fs/fcntl.c:kill_fasync()
参照)
get_empty_filp()
によって作られたときにファイルを開いたプロセスのユーザIDとグループIDに設定される。ファイルがソケットのときは、ipv4ネットフィルタが使う。
fs/nfs/file.c
で設定され、mm/filemap.c:generic_file_write()
でチェックされる。
event
を使って)f_pos
が変ったときにいつも増分される。
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 *);
};
THIS_MODULE
に設定する必要があり、モジュールのカウントは、ドライバがopen/release時に制御する必要があるのに対して、mount/umountのときに制御されるため、ファイルシステムは幸運なことに無視できる。
fs/read_wirte.c:default_llseek()
が使われる。これは正しい方法である。(TODO: default_llseekを使うために現在これにNULLを設定しなければならなくなっている。このときは、llseek()
でif()
を保存している)。
read(2)
システムコールの実装。ここでは、ファイルシステムは、mm/flemap.c:generic_file_read()
を通常のファイルに、fs/read_write.c:generic_read_dir()
(単純に-EISDIR
を返す)をディレクトリに使用できる。
mm/filemap.c:generic_file_write()
を通常のファイルに使用でき、ディレクトリは無視できる。
FIBMAP
や FIGETBSZ
、 FIONREAD
のようなioctl は、高レベルで実装されておりf_op->ioctl()
メソッドを読むことがない。
dentry_open()
が呼ぶ。ファイルシステムは滅多に使わない。例えば、coda はオープンしたときにファイルをローカルにキャッシュしようとする。release()
メソッド参照) これを使うファイルシステムというのは、唯一NFSのクライアントで、全てのdirtyページを書き出す。ここで、close(2)システムコールから、ユーザ空間に送り返されるエラーを返すことがある。
file->f_count
が0であるときだ。整数を返すと定義されているにも関わらず、VFSでは返り値は無視される。(fs/file_table.c:__fput()
を参照)
file = fget(fd)
)、および inode->i_sem
セマフォを操作することは行う。Ext2 ファイルシステムは最後の引数を無視するので、fsync(2) と fdatasync(2) のシステムコールは全く同じ働きをする。
file->f_flags & FASYNC
が真であるときに使われる。
posix_lock_file()
)なものが呼ばれる前に呼ばれたとき、もし成功したが通常のPOSIXロックコードが失敗したら fs 依存レベルでは、ロック解除できなくなるということである。
Linux においては、マウントされたファイルシステムについての情報は2つに分かれた構造体 super_block
とvfsmount
で保持されています。これは 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
構造体には様々なメンバがあります。
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
メンバを利用すべきでないということは、これまで何度も提案されてきた。
lock_super()/unlock_super()
によって現在ロックされているかどうかを示す。
struct file_system_type
を指す。fs 特有のread_super()
が成功したときに VFS fs/super.c:read_super()
が設定し、失敗したとき NULL にリセットするため、ファイルシステムの read_super()
メソッドは設定する必要がない。
super_operations
構造体へのポインタ。s_op
を正しく初期化するのは、ファイルシステムの read_super()
メソッドの仕事である。
d_alloc_root()
を呼んでdentryを割り当てて実体化するために渡すのは、read_super()
の仕事である。
inode->i_state & I_DIRTY
)いれば、スーパーブロック特有のinode->i_list
経由でリンクされた dirty リストにおかれる。
sb->s_files
リストをみて、もしファイルが書き込みオープンされている(file->f_mode & FMODE_WRITE
)か、unlinkが保留されている(inode->i_nlink == 0
)ファイルがあれば、再マウントを拒否するfs/file_table.c:fs_may_remount_ro()
を参照すること。
FS_REQUIRES_DEV
のために、これは ファイルシステムがマウントされているデバイスを表す block_device 構造体を指している。
vfsmount
構造体のリスト。マウントされたこのスーパーブロックの各インスタンスの???
スーパーブロックの操作は、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 *);
};
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_op
とinode->i_fop
では、VFSレベルが、inode や関連するファイルでなんの操作が行われるかを知っている
read_inode()
を実装しないファイルシステムは、ramfsとpipefsである。たとえば、ramfsは、必要になったときに全てのinode の操作が呼ぶ、自身のinode生成関数ramfs_get_inode()
を持っている。
read_inode()
と似て、ディスクに同様なブロックを割り当て、mark_buffer_dirty(bh)
を呼ぶことで、バッファキャッシュと作用する。このメソッドは、inodeが、それぞれやファイルシステム全体の同期の一部なりで同期される必要があるとき、dirty な inode (mark_inode_dirty()
によって dirty と印をつけられている)に対して呼ばれる。
inode->i_count
とinode->i_nlink
が0になるときは常に呼ばれる。ファイルシステムは、オンディスクのinodeのコピーを削除し、VFSでclear_inode()
を呼び出して、「完全に根絶する」。
-->
brelse()
し、フリーブロックやinodeなどのために割り当てられたビットマップを全てkfree()
する。
sb-private
領域にある)とmark_buffer_dirty(bh)
があるブロックを見つけなければならない。これはまた、sb->s_dirt
フラグをクリアする。
struct stats
へのポインタは、ユーザポインタではなくカーネルポインタであるため、ユーザ空間へのI/Oが不必要であることに注意すること。もし実装されていなければ、statfs(2)
はENOSYS
で失敗する。
clear_inode()
から呼ばれる。プライベートデータをinode構造体へ(generic_ip
メンバを経由して)割り当てるファイルシステムは、ここで解放されなければならない。
それでは、on-disk (FS_REQUIRES_DEV
)ファイルシステムをマウントするときに起ることを見ていきましょう。
mount(2)システムコールの実装は、fs/super.c:sys_mount()
にあり、これは実際の処理を行う do_mount()
関数にオプションとファイルシステムタイプやデバイス名をコピーする単なるラッパーになっています。
do_mount()
がget_fs_type()
を呼ぶときに1回、read_super()
が成功したならば、get_sb_dev()
がget_filesystem()
を呼ぶときに1回である。1つめの増分は、read_super()
の内部において、モジュールがアンロードされるのを避けるためであり、2つめの増分は、マウントされたインスタンスによってモジュールが使用中であることを示すためのものである。明らかに、各マウント後の全体でのカウントは1つだけ増えることから、do_mount()
は戻る前にカウンタを減少させるのである。
fs_type->fs_flags & FS_REQUIRES_DEV
が真であるなら、ブロックデバイスへのリファレンスを取得し、ファイルシステムのread_super()
メソッドと連携してスーパーブロックを埋めるget_sb_bdev()
を呼ぶことで、スーパーブロックが初期化される。全てがうまく行けば、super_block
構造体は初期化され、ファイルシステムモジュールへのextra参照と、下にあるブロックデバイスへの参照を得ることになる。
vfsmount
構造体が割り当てられ、sb->s_mounts
リストとグローバルなvfsmntlist
リストへリンクされる。vfsmount
のメンバのmnt_instances
は全ての同じスーパーブロックにマウントされているインスタンスを見つけられるようにする。
mnt_list
メンバは、システム全体の全てのスーパーブロックに対して、全てのインスタンスを見つけることができるようにする。mnt_sb
メンバはこのスーパーブロックを指し、mnt_root
はsb->s_root
dentry への新しい参照を持つ。マウントにブロックデバイスが必要ない簡単な 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_type
がfle_systems
リストにリンクされることになります。つまり/proc/filesystems
を読み出して、FS_REQUIRES_DEV
が設定されていないことを示す「nodev」フラグつきで「pipefs」が登録されているのを見つけられることになります。/proc/filesystems
ファイルは、新しいFS_
フラグを全てサポートするよう、まさに機能強化されるべきです(そして、私はそのパッチを作りました)。しかし、これを使う全てのユーザのアプリケーションが動かなくなるため、今だ実行することができません。
Linux カーネルインターフェースが随時変更されている(単に良くするために)にも関わらず、ユーザ空間での互換性に及ぶとなると、Linux はとても保守的なオペレーティングシステムとなり、長い期間再コンパイルをしないで多くのアプリケーションが使えるようにするのです。
kern_mount()
の結果は次のようになります。
unnamed_dev_in_use
ビットマップのビットが設定される。もし割り当てるビットがなければ、kern_mount()
はEMFILE
で失敗する。
get_empty_super()
によって、新たなスーパーブロック構造体が割り当てられる。get_empty_super()
関数は、super_block
によって先頭を示されるスーパーブロックのリストを見て、s->s_dev == 0
となる空のエントリーを探す。
pipe_fs_type->read_super()
メソッド(つまりpipefs_read_super()
だが)が起動され、ルートのinodeとルートのdentryとなる sb->s_root
を割り当て、sb->s_op
を&pipefs_ops
になるよう設定する。
kern_mount()
が 新しいvfsmount
構造体を割り当て、vfsmntlist
とsb->s_mounts
へリンクするadd_vfsmnt(NULL, sb->s_root, "none")
を呼ぶ。
pipe_fs_type->kern_mnt
は新しいvfsmount
構造体に設定されて戻る。kern_mount()
返り値がvfsmount
構造体になっているのは、FS_SINGLE
のファイルシステムでさえ複数回マウントできることから、mnt->mnt_sb
はkern_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_fop
はrdwr_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_fops
とwrite_pipe_fops
それぞれの file->f_op
操作は持てません。
簡単なディスク上の 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->flags
を FS_REQUIRES_DEV
に設定され、BFS がマウントする実際のブロックデバイスが必要であることを示しています。
モジュールの初期化関数はファイルシステムと(BFSがモジュールとして設定されたときのみ存在する)登録解除を行う解除関数をVFSに登録します。
ファイルシステムの登録を行うことで、マウントを進めることができます。マウントではfs/bfs/inode.c:bfs_read_super()
に実装されている fs_type->read_super()
メソッドが起動されます。この関数は次のような処理を行います。
set_blocksize(s->s_dev, BFS_BSIZE)
: ブロック型デバイス層とバッファキャッシュを通して相互作用しようとしていることから、二つ三つの初期化を行う必要がある。すなわち、ブロックサイズを設定して、さらに s->s_blocksize
と s->s_blocksize_bits
メンバを通じて VFS へ通知するのだ。
bh = bread(dev, 0, BFS_BSIZE)
: デバイスのブロック0を読み、s->s_dev
を通じて渡す。このブロックはファイルシステムのスーパーブロックである。
BFS_MAGIC
番号に対して正当性を確認し、sbプライベートメンバs->su_sbh
へ格納する(実際はs->u.bfs_sb.si_sbh
である)。
kmalloc(GFP_KERNEL)
を使って割り当て、1に設定される最初の2つを除く全てのビットを0に設定する。これは、inode 0と1をずっと割り当てないことを示すためである。inode 1はルートであり、関連するビットはいずれ数行後に1に設定される。というのは、ファイルシステムはマウントされるときには必ず妥当なルート inode を持ってるべきものだからだ。
s->s_op
を初期化する。これはs_op->read_inode()
が起動される結果としてiget()
を通じて、この時点から inode キャッシュを呼び出すことができる。これは、(inode->i_ino
とinode->i_dev
による)特定のinode からなるブロックを見つけて読み込む。
もしルート inode の取得が失敗したら、inode ビットマップを解放し、バッファーキャッシュの背後にあるスーパーブロックバッファーを解放して、NULL を返す。もしルート inode の読み込みがOKであれば、(ルートとなる)/
という名前のdentryを割り当て、この inode を実体化する。
iput()
を通じて戻される。そう、必要以上に長く参照を保持できないのだ。
s->s_dirt
フラグを設定する(TODO: なぜこれをやるのか? 普通は、これはminix_read_super()
がやるからなのだが、minixもBFS もread_super()
でスーパーブロックを書き換えていないようなのだ)。
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_op
とinode->i_fop
の事前設定を行うことになります。ファイルのオープンは、inode->i_fop
からfile->f_op
へ伝搬します。
さて、link(2)システムコールのコードの通り道をみていきましょう。システムコールの実装はfs/namei.c:sys_link()
にあります。
getname()
関数を使ってコピーされる。
path_init()/path_walk()
を使って nameidata に変換され、dcache と相互作用する。結果は old_nd
とnd
構造体へ格納される。
old_nd.mnt != nd.mnt
であったら、「クロスデバイスリンク」EXDEV
が返される。ファイルシステムを超えたリンクはできないが、Linux ではそれをファイルシステムのマウントされたインスタンス(あるいは特定のファイルシステム間)を超えたリンクができないと解釈している。
nd
へlookup_create()
によって関連づけられる。
vfs_link()
関数が呼ばれ、ディレクトリに新しいエントリーを作ることができるかどうかチェックする。そして、dir->i_op->link()
メソッドを呼び出す。これはファイルシステムに特有のfs/bfs/dir.c:bfs_link()
関数を呼び出すことになる。
bfs_link()
の内部では、ディレクトリをリンクしようとしていないかチェックし、もしそうであればEPERM
エラーで拒否する。これは標準ファイルシステム(ext2)を同じ挙動である。
bfs_add_entry()
を呼び出して新しいディレクトリエントリを特定のディレクトリに追加しようとする。この関数は、使用していないスロット(de->ino == 0
)を探して全てのエントリーを見ていき、見つかったならば、 name/inode の対を関連するブロックへ書き出して、 dirtyの印を(スーパブロック以外の優先度で)つける。
inode->i_nlink
を増分し、inode->i_ctime
を更新して、inode と一緒にインスタンス化した新しいdentryと同様に、この inode を dirty とマークする。unlink()/rename()
のような他の関連 inode 操作も、同じようなやり方で働きます。したがって、あまりメリットがないため、ここで全ての詳細を扱うことはしません。
Linux は、ディスクからのユーザアプリケーションバイナリの読み込みをサポートしています。もっと面白いことには、そのバイナリは、他のフォーマットで保存されていても構いません。プログラムに対するシステムコールを通じたオペレーティングシステムの反応は、 他の UNIX に見られるフォーマット(COFFなど)をエミュレートし、さらに他のタイプ(Solaris, Unixwareなど)のシステムコールの振る舞いをエミュレートするために、必要ならば、標準(Linux の振る舞いをする標準)から外れることができます。これが、実行ドメインとバイナリフォーマットが必要になる理由になっています。
各 Linux のタスクは、task_struct
(p->personality
)のなかにそれぞれのパーソナリティを持っています。現在(正式なカーネルも追加パッチでも含め)存在するパーソナリティには、FreeBSD、Solaris、UnixWare、OpenServerのサポートを含み、さらに他のメジャーなオペレーティングシステムもあります。
その current->personality
の値は2つの部分に分かれます。
STICKY_TIMEOUTS
, WHOLE_SECONDS
などパーソナリティの変更により、私たちはオペレーティングシステムがシステムコールを取り扱う方法を変えることができます。たとえば、STICKY_TIMEOUT
をcurrent->personality
へ与えることで、select(2)
システムコールは、残り時間を保存するかわりに、最後の引数(タイムアウト)の値を維持します。プログラムの中には(Linuxにはない)バグのあるオペレーティングシステムに依存したバグのあるものが存在するため、ソースコードが入手できずバグを改修できない場合のために、Linux はバグをエミュレートする方法を提供しているのです。
実行ドメインは、パーソナリティの延長線上にあり、一つのモジュールとして実装されています。通常一つの実行ドメインは一つのパーソナリティを実装していますが、なかには、それほど多くない条件を満たすことで一つのモジュールの中に「閉じた」複数のパーソナリティを実装することもできます。
実行ドメインはkernel/exec_domain.c
に実装され、2.4 カーネルで 2.2.x から完全に書き直されました。サポートしているパーソナリティの種類に加え、カーネルが現在サポートしている実行ドメインのリストは、/proc/execdomains
ファイルを読み出すことで得ることができます。PER_LINUX
を除く実行ドメインは、動的に読み込まれるモジュールとして実装できます。
ユーザインターフェースは、personality(2)システムコールを通じて、現在のプロセスのパーソナリティを設定できるようになっています。あるいは、もし引数が存在しないパーソナリティ 0xffffffff であったらcurrent->personality
をただ返します。明らかにこのシステムコールの振る舞いは、パーソナリティとは独立です。
実行ドメイン登録のカーネルインターフェースは2つの関数になっています。
int register_exec_domain(struct exec_domain *)
: 読み書きスピンロックexec_domain_lock
による書き込み保護のもと、単リンクリストのexec_domains
へ実行ドメインをリンクすることで、登録する。成功すれば0を返し、失敗なら非0となる。
int unregister_exec_domain(struct exec_domain *)
: exec_domians
リストからはずすことにより実行ドメインを登録解除する。登録と同様、exec_domian_lock
スピンロックを書き込みモードで利用する。成功すれば0を返す。exec_domains_lock
が読み書きロックである理由は、登録と登録解除要求のみがリストを書き換え、cat /proc/filesystemsがfs/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.S
のENTRY(lcall7)
に移します。なぜならこれは、arch/i386/kernel/traps.c:trap_init()
に準備されているためです。
適切なスタック割り当ての変換のあと、entry.S:lcall7
はcurrent
から 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
を実装したものになっています。