0から作るソフトウェア開発

日々勉強中。。。

Tips
リンク

ファイルシステム >

0から作るLinuxプログラム Ext2ファイルシステムその5 仮想ファイルシステム(VFS)

Ext2ファイルシステム

Linuxのファイルシステムのうち基本的なファイルシステムであるExt2について
みていきます。

それでは、順番に見ていきます。

目次 Ext2ファイルシステム

  1. ファイルシステムの概要

  2. ファイルシステムへのアクセス

  3. カーネルサイドから見たシステムコール

  4. ファイルシステム関連のシステムコール

  5. (現在のページ) 仮想ファイルシステム(VFS)

  6. ステップ0 Ext2ファイルシステムと簡単なモジュールの実装

  7. ステップ1 Ext2ファイルシステムタイプとマウントメソッド

  8. ステップ2 Ext2スーパーブロックの読み込み

  9. ステップ3 スーパーブロック管理情報

  10. ステップ4 マウントとスーパーブロックオブジェクト

  11. ステップ5 スーパーブロックオブジェクトと管理情報のセットアップ

  12. ステップ6 ブロックグループディスクリプターの読み込み

  13. ステップ7 ルートディレクトリのinode読み込み

  14. ステップ8 簡単なディレクトリの読み込み

  15. ステップ9 ディレクトリ読み込みとページキャッシュとaddress_space

  16. ステップ10 inodeオブジェクトのlookupメソッド

  17. ステップ11 inodeオブジェクトのmkdirメソッドとper-cpuカウンター

  18. ステップ12 簡単なブロックの新規割り当て処理

  19. ステップ13 スーパーブロックオブジェクトのwrite_inodeメソッド

  20. ステップ14 Orlov方式による新規ディレクトリのinodeを割り当てるブロックグループ選択

  21. ステップ15 inodeオブジェクトのrmdirメソッドとunlinkメソッド

  22. ステップ16 inodeオブジェクトのrenameメソッド

  23. ステップ17 inodeオブジェクトのcreateメソッドとファイルオブジェクトの汎用メソッド

  24. ステップ18 inodeオブジェクトのlinkメソッド

  25. ステップ19 シンボリックとinodeオブジェクトのsymlinkメソッド

  26. ステップ20 inodeオブジェクトのmknodメソッドとtmpfileメソッド

  27. ステップ21 スーパーブロックオブジェクトのevict_inodeメソッド

  28. ステップ22 スーパーブロックオブジェクトのsync_fsメソッド

  29. ステップ23 スーパーブロックオブジェクトのstatfsメソッドとメモリーバリアー/フェンス

  30. ステップ24 スーパーブロックオブジェクトのremount_fsメソッドとマウントオプション解析

  31. ステップ25 スーパーブロックオブジェクトのshow_optionsメソッドとprocfs

  32. ステップ26 カーネルオブジェクトとsysfs

  33. ステップ27 スーパーブロックオブジェクトのfreeze_fs/unfreeze_fsメソッド

  34. ステップ28 ファイルオブジェクトのioctlメソッド

  35. ステップ29 ファイルオブジェクトのsetattrメソッド

  36. ステップ30 address_spaceのdirect_IOメソッド

  37. ステップ31 リザベーションウィンドウ

  38. ステップ32 拡張アトリビュートその1 ハンドラーの呼び出し

  39. ステップ33 拡張アトリビュートその2 コア処理

  40. ステップ34 POSIX ACLその1 ハンドラーの呼び出し

  41. ステップ35 POSIX ACLその2 コア処理

  42. ステップ36 ディスククォータ

仮想ファイルシステム(VFS)とは

仮想ファイルシステムはファイルシステム関連のシステムコールが最初にアクセスすること
になるカーネル空間のプログラムとなります。

仮想ファイルシステムは最初にSun Microsystems, Inc.(現在はOracle

Corporation)で導入されました。仮想ファイルシステムはVFS(Virtual Filesystem)

とも言います。名前の通り、ファイルシステムを仮想的にとり扱う仕組みとなります。


ファイルシステムにはここで見ていくExt2ファイルシステムやWindowsのNTFS、FATなど

さまざまな種類があります。(Linuxで扱うことができるファイルシステムについては

いろいろなファイルシステムを参照してください)。そして、新しい方式のファイルシステムも

どんどん作られています。ファイルシステムの種類が1つだけ、例えば、FAT32だけのシステム

だと単純にアクセスしたいファイルを指定するだけとなりますが、2つ以上になってくると

ややこしくなってきます。ファイルシステムの種類ごとにファイルにアクセスする方式が違って

きますので、例えばファイルデータの読み込みをしたいときにそのファイルがどのファイル

システムに格納されているのかを知っておく必要があります。つまり、このままだとファイルに

アクセスするときにファイルシステムの種類も指定しておかなければなりません。扱うファイル

システムが2つくらいだと煩わしくはないのですが、ネットワークファイルシステムやExt2、Ext3

そしてExt4と種類が多くなってくると大変になってきます。このため、ファイルにアクセスする

ときにたくさんあるファイルシステムを自動的に切り替えてくれる仕組みがあると楽だなあと

思ってしまいます。まさにこの仕組みを実現しているのが仮想ファイルシステムの役目と

なっています。最初の頃はVFSはVirtual Filesystem Switchと呼ばれていました。

ファイルシステムの切り替えを目的としていることがわかる命名です。

仮想ファイルシステム(VFS)の役割

ファイルシステムの切り替えのために導入された仮想ファイルシステムですが、現在Linux

(や他の汎用OS)では、次のような役割を担っています。

アプリケーションサイドのインターフェース統一

マウント情報などを扱うシステムアプリケーション以外の普段ユーザーが使用するアプリ

ケーションがファイルにアクセスしたいと思ったときに特にファイルシステムについて意識

することはありません。単純にアクセスしたいファイルを指定するだけとなります。例えば

次のようにマウントされているファイルシステムに対してアプリケーションではファイルシステム

のタイプを特に意識せずにファイルデータのコピーを行うことができます。


次のプログラムは/cdromにマウントされたISO9660ファイルシステムのファイル

をExt4ファイルシステムのファイルにコピーするプログラム例となります。


#include <stdio.h>
#include <errno.h>

#include <unistd.h>
#include <fcntl.h>

int main( int argc, char *argv[ ] )
{
    int     err, read_fd, write_fd;
    char    read_path[ ]    = "/cdrom/File3";
    char    write_path[ ]   = "/home/user/File1";
    char    buf[ 4096 ];
    ssize_t len;

    if( argc != 1 )
    {
        fprintf( stderr, "usge : %s\n", argv[ 0 ] );
        return( -1 );
    }

    read_fd = open( read_path, O_RDONLY );

    if( read_fd < 0 )
    {
        perror( "open : read_fd : " );
        return( -1 );
    }

    write_fd = open( write_path, O_WRONLY | O_CREAT | O_TRUNC, 0600 );

    if( write_fd < 0 )
    {
        if( ( err = close( read_fd ) ) < 0 )
        {
            perror( "close : read_fd " );
        }
        return( -1 );
    }

    while( ( len = read( read_fd, buf, 4096 ) ) )
    {
        ssize_t write_len;

        if( len < 0 )
        {
            perror( "read " );
            break;
        }
        
        write_len = write( write_fd, buf, len );

        if( write_len < 0 )
        {
            perror( "write " );
            break;
        }
    }

    if( close( read_fd ) < 0 )
    {
        perror( "close : read_fd " );
    }

    if( close( write_fd ) < 0 )
    {
        perror( "close : write_fd " );
    }

    return( 0 );
}



アプリケーションからのVFS操作

このように、アプリケーションプログラムからはIOS9660ファイルシステムと

Ext4ファイルシステムの違いは意識しないで、ただファイルの読み込みと

ファイルへの書き込みを行っています。このように、ファイルシステムの違い

を仮想ファイルシステムでファイルとして扱うことができるようにしています。

このようにアプリケーションからはファイルだけを扱うことで、他の同じような

UNIXシステムにも移植することができますので、プログラムの生産性向上

にも役に立ちます。

カーネルサイドのインターフェース統一

仮想ファイルシステムと各ファイルシステムとのインターフェースはあらかじめ

定義されていますので、新しいファイルシステムを追加するときににも効率

良く実装することができます。例えば、パス名解決ではディスクからディレクトリ

エントリーを探す処理は個々のファイルシステムで独自に実装されている仕事

となりますが、一度ディスクから取得した後は各ファイルシステムで同じ処理

となりますので、これを仮想ファイルシステムで行っています。これにより、

仮想ファイルシステムで個別のファイルシステムで共通となる処理(言い換えると

個別のファイルシステムに依存しない処理)を実装しておくことで、プログラム

の生産性も上がります。


また、個々のファイルシステムはドライバーのようにカーネルモジュールとして

必要なときにカーネルに取り付けたり、不要な時に外したりすることもできます。

VFSと各ファイルシステム

ファイル情報のキャッシュとファイルシステム共通オブジェクト

ディスクに格納されているファイルに初めてアクセスするときにはメモリーにデータ

を読み込む必要があるため、ディスクI/Oが発生します。ディスクからの読み込み

はCPUよりも大変遅いため、頻繁にディスクアクセスするとシステムのパフォーマンス

が落ちてしまいます。そこで、仮想ファイルシステムで一旦読み込みしたファイルの

情報は可能な限りメモリーに留めておくことで、キャッシュするようにしています。

データのアクセスには参照の局所性があります。一回アクセスされたデータは将来

的に再びアクセスされる可能性があるということになります。例えば、テキストエディター

が使う設定ファイルを変更する場合では、設定ファイルの読み込み、設定ファイルの

編集、編集した内容の書き込み、編集された設定ファイルをテキストエディターが

読み込み直すといった具合に、何度も同じファイルにアクセスすることになります。

アクセスするたびに何度もディスクI/Oが発生してしまうと、動作が大変遅くなります。

このため、仮想ファイルシステムではファイルデータなどのファイルシステム情報データを

各ファイルシステムにとって共通となる゛オブジェクト”を導入しています。共通オブジェクト

にはユーザーから参照することができるファイル情報、例えばstatで見ることが

できるファイル属性の情報や、statfsでみることができるファイルシステム情報を

格納しています。このように、各ファイルシステムから取得しておいたデータを共通

オブジェクトに置くことで、ユーザーからはファイルシステムの違いを意識せずにファイル

情報を取り出したり、また、メモリーにオブジェクトを留めることでキャッシュとして利用

することができます。更に、オブジェクトにはメソッドが定義されています。これにより、

各ファイルシステム個別の操作を呼び出すことで、もともとのファイルシステム切り替え

としての機能を実現しています。このように仮想ファイルシステムの機能実現には

ファイルシステムの共通オブジェクトが重要な役割を担っています。


ファイルシステムオブジェクトには次のようなオブジェクトが定義されています。

VFSオブジェクト

疑似ファイルシステム

Linuxでは疑似ファイルシステムで見てきましたように、実際にデータを格納する

デバイスを持たない疑似的なファイルシステムがあります。疑似ファイルでは先ほど

見てきました、ファイルシステムの共通オブジェクトを仮想ファイルシステムで作成する

ことで実現しています。このとき、各オブジェクトにはデバイスとの関連付けは行い

ませんので、疑似ファイルシステムに対する操作は全てメモリー上のみで行われます。



ここまでで、簡単ですが仮想ファイルシステムの役割について見てきました。

それでは、仮想ファイルシステム機能を実際に実現させている、ファイル

システムの共通オブジェクトについてもう少しだけ詳しく見ていきましょう。

ファイルシステム共通オブジェクト

先ほど見てきましたように、仮想ファイルシステムには5つの共通オブジェクト

がありました。ファイルシステムタイプ、スーパーブロック、inode、dエントリー、

ファイルオブジェクトの5つです。ファイルシステム共通オブジェクトは仮想ファイル

システムで管理している情報となりますので、ユーザー空間から直接見ることは

なかなか難しく、どのような働きをしているのか想像できない部分が多いです。

もう少しだけ共通オブジェクトについて詳しく見ていきます。

ファイルシステムタイプオブジェクト

仮想ファイルシステムでは、処理を任せるファイルシステムにどのようなファイル

システムがあるのか知っておく必要があります。ファイルシステムがわかっていないと

そもそもマウントすることができません。ファイルシステムタイプオブジェクトでは、

どのようなファイルシステムがあるのかを事前にカーネルが把握しておくために

あります。

ファイルシステムタイプオブジェクトが管理するデータ

ファイルシステムオブジェクトは各ファイルシステムの初期化時に自分がどのような

ファイルシステムであるのかを登録します。登録することにより、カーネルは処理

できるファイルシステムを認識することができます。

があります。ファイルシステムモジュールがカーネルにビルトインされている場合は

起動時にファイルシステムタイプがカーネルに登録されます。ファイルシステムモジュール

がカーネルモジュールとして作成されている場合は、モジュールをロードしたときに

ファイルシステムタイプがカーネルに登録されます。

ファイルシステムタイプオブジェクトのメソッド

ファイルシステムタイプオブジェクトのメソッドには次のようなものがあります。

ファイルシステムをマウントするには、まずファイルシステムタイプオブジェクトの

マウントメソッドを呼び出しすることから始まります。

スーパーブロックオブジェクト

仮想ファイルシステムで扱うスーパーブロックオブジェクトでは、ファイルシステムを

管理するデータと各ファイルシステムの固有の処理を呼び出すスーパーブロックの

メソッドが定義されています。(おそらくここに記載している情報だけでは理解できない

かと思います。より詳しくはExt2ファイルシステムで見ていきたいと思います。)

スーパーブロックオブジェクトが管理するデータ

スーパーブロックオブジェクトでは大まかに次のような管理データを扱います。

などがあります。この情報は主に、各ファイルシステムがデバイスから読みだした

ファイルシステム制御ブロック(Ext2ではスーパーブロック)の情報を格納します。

スーパーブロックオブジェクトのメソッド

スーパーブロックオブジェクトのメソッドは大まかに次のようなものがあります。

などがあります。ここで、再マウントメソッドが出てきますが、マウントメソッドがあり

ません。マウントメソッドはファイルシステムタイプオブジェクトにあります。

inodeオブジェクト

inodeはindex nodeでUNIXで実装されていたファイルシステムがその起源

となります。ファイル制御ブロックともいい、ファイルがファイルシステムのどこに

データがあるのか、またタイムスタンプなどのメタデータが格納されています。

仮想ファイルシステムではディスクから読み込んだinode情報をinodeオブジェクト

に格納し、キャッシュとして利用します。このため、inodeキャッシュともいいます。

また、ディスク上のブロックに格納されてるいるinode情報と区別するために、

vnode(Virtual node)とも呼ぶことがあります。Linuxでは単にinodeキャッシュ

またはinodeと言うことが多いです。

inodeはファイルの場所を示すオブジェクトとなります。ファイルの場所はファイル

システム上で一意に決まっている必要があります。UNIXで実装されていたファイル

システムではinode番号で特定していました。このため、inodeオブジェクトが持つ

inode番号も同じファイルシステム内で一意になっている必要があります。inode

番号がファイルシステム内で一意に決まっているため、inodeオブジェクトを探す

ときには、ハッシュテーブルのキー値としてinode番号とファイルシステムタイプ

を指定して高速検索を行います。


ユーザーからのファイル作成の要求やファイルデータ操作の要求はinodeオブジェクト

に対してメソッド呼び出しを行います。


仮想ファイルシステムで扱うinodeは次のような管理データとメソッドが定義

されています。(ここではあまり詳細に触れていません。おそらくなんのことだろうと

思われている方が多いかと思いますが、Ext2ファイルシステムの説明で詳細に

見ていこうかと思います)。

inodeオブジェクトが管理するデータ

inodeオブジェクトでは大まかに次のような管理データを扱います。

などがあります。この情報は各ファイルシステムがデバイスから読みだしたファイル

制御ブロック(Ext2のinodeブロックに格納されている情報)の情報を格納します。

inodeオブジェクトのメソッド

inodeオブジェクトのメソッドは大まかに次のメソッドが定義されています。

inodeオブジェクトのメソッドはinodeに対しての操作となります。

などがあります。ここで、ファイルの作成やシンボリックリンク作成などのメソッドが

ありますが、これはディレクトリのinodeオブジェクトのみに必要なメソッドとなります。

通常ファイルのinodeオブジェクトには必要ありません。


これに加えてinodeオブジェクトにはファイル操作メソッドが定義されています。

inodeオブジェクトのファイル操作メソッド

inodeオブジェクトのファイル操作メソッドはファイルデータに対する操作となります。

次のようなメソッドがあります。

ファイル操作は皆さんにおなじみの操作ばかりだと思います。

dエントリーオブジェクト

dエントリーオブジェクトはその名の通りディレクトリエントリーを表したオブジェクトと

なります。仮想ファイルシステムでは、ディレクトリに格納されているファイル名を

ディスクからから読み込んだときに、dエントリーオブジェクトに格納してキャッシュと

して使用します。このため、dエントリーキャッシュとも言います。

ユーザーがパスを指定してシステムコールを発行したときに、そのパスに含まれる

パスの要素1つ1つについてdエントリーが作成されていきます。


dエントリーオブジェクトはディレクトリのエントリーを表していますので、ディレクトリ

構成についても表現しています。つまり、親ディレクトリのdエントリーオブジェクトを

参照できたり、自分の配下にあるファイルのリストを持っています。これによりディレクトリ

の親子関係を関連付けています。


dエントリーオブジェクトはファイル名を表していますので、ファイル名に対応する

inode情報(フィルシステム内でファイルを特定するのに必要です)と関連付け

されています。言い換えるとdエントリーオブジェクトはユーザーが指定したファイル名

からinodeオブジェクトを取得する役目があります。


仮想ファイルシステムで扱うdエントリーオブジェクトは次のような管理データと

メソッドが定義されています。

dエントリーオブジェクトが管理するデータ

dエントリーオブジェクトでは大まかに次のようなデータを管理しています。

などがあります。

dエントリーオブジェクトのメソッド

dエントリーオブジェクトのメソッドは特にファイル名の比較操作がわかりやすいかと

思います。特にFATでは仕様上、大文字小文字の区別なくファイル名の比較を

行う必要があります。このようなファイルシステムでは、dエントリーオブジェクトの比較

メソッドを実装することで、そのファイルシステム独自の比較操作が仮想ファイル

システムから呼び出されるようになります。


dエントリーオブジェクトのメソッドには次のようなものがあります。(今回Ext2ファイル

システムではすべて仮想ファイルシステムレイヤーでの操作となりますので、Ext2

ファイルシステム独自のdエントリーオブジェクトのメソッドは実装しません。)

Ext2ファイルシステムではdエントリーオブジェクトのメソッドは実装しませんので、

少しだけ詳しく見ていきます。今回は必要ありませんので、読み飛ばして頂いても

問題ありません。


次のようにカーネルソースに定義されています。(カーネルバージョンが変われば

変わっている可能性があります)。


struct dentry_operations {
        int (*d_revalidate)(struct dentry *, unsigned int);
        int (*d_weak_revalidate)(struct dentry *, unsigned int);
        int (*d_hash)(const struct dentry *, struct qstr *);
        int (*d_compare)(const struct dentry *, const struct dentry *,
                        unsigned int, const char *, const struct qstr *);
        int (*d_delete)(const struct dentry *);
        void (*d_release)(struct dentry *);
        void (*d_prune)(struct dentry *);
        void (*d_iput)(struct dentry *, struct inode *);
        char *(*d_dname)(struct dentry *, char *, int);
        struct vfsmount *(*d_automount)(struct path *);
        int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;



dエントリーオブジェクトのメソッド
関数名 操作 説明
d_revalidate dエントリーの再検証 dエントリーを使用する前にそのオブジェクトがまだ有効なエントリーかどうか検証します。ネットワークファイルシステムでは、ネットワークごしのシステムでどのような状態となっているかわかりませんので、inodeがまだ有効であるか確認したりします。通常ローカルファイルシステムでは特に何も行いません。また、仮想ファイルシステム標準のメソッドも特にありません。
d_weak_revalidate dエントリーの弱再検証 パス名解決の最終処理でdエントリーオブジェクトがまだ有効なエントリーかどうか検証します。ネットワークファイルシステムでは、同様にどのような状態となっているかわかりませんので、inodeがまだ有効であるか確認したりします。通常ローカルファイルシステムでは特に何も行いません。また、仮想ファイルシステム標準のメソッドも特にありません。
d_hash dエントリーハッシュ関数 dエントリーのハッシュリストからdエントリーオブジェクトを高速検索するときに使用します。ファイルシステム独自でハッシュ値を計算したいときに実装します。通常は仮想ファイルシステムで用意されているハッシュ関数が呼び出されます。
d_compare dエントリーの比較 先ほどのFATの例のように、大文字小文字を区別しないように比較するなどのファイルシステムの独自実装を行いたいときに実装します。通常は仮想ファイルシステムの比較処理が呼び出されます。仮想ファイルシステムでは、通常の文字列比較を行います。
d_delete dエントリーの削除 dエントリーオブジェクトの参照カウンターが0(すなわちもう誰も参照していない状態)となったときに、そのdエントリーと関連する(例えばinodeなど)処理が必要な場合に実装します。通常のファイルシステムと特に実装していないものもあります。仮想ファイルシステムの標準メソッドもありません。
d_release dエントリーの解放 dエントリーオブジェクト用のメモリーが一旦メモリーアロケーターに戻されるときにファイルシステム独自の処理を呼び出します。通常のファイルシステムでは特に実装していないものもあります。仮想ファイルシステムの標準メソッドもありません。
d_iput inodeの参照解放 dエントリーに関連付けられているinodeへの参照を取り下げます。inodeが削除されたときに呼び出されます。仮想ファイルシステムの標準メソッドではinodeオブジェクトを解放します。
d_dname dエントリー名取得 dエントリーオブジェクトに格納されているファイル名を取得するときにファイルシステム独自のプレフィックスをつけたいときなどに実装します。通常のファイルシステムでは特に使用しません。仮想ファイルシステムの標準メソッドもありません。
d_automount オートマウント パス名解決などでマウントポイントに到達したときに自動的にマウントを行います。マウントはファイルシステム独自の実装が必要なため、仮想ファイルシステムの標準メソッドはありません。
d_manage オートマウント管理 指定されたdエントリーオブジェクトについてオートマウントが必要かどうか判断します。autofsで使用しています。仮想ファイルシステムの標準メソッドはありません。


となります。

ファイルオブジェクト

ファイルオブジェクトはプロセスが現在オープンしているファイルについての情報を

管理するオブジェクトとなります。ユーザー空間ではファイルディスクリプターを指定

してプロセスが現在オープンしているファイルに対して操作を行うことができます。

ファイルディスクリプターはファイルオブジェクトを管理するファイルディスクリプター

テーブルのインデックスとして使用します。ファイルディスクリプターテーブルは

struct files_structでプロセスごとに管理します。

ファイル管理構造体files_struct

ファイルディスクリプターテーブルを管理する構造体でプロセスごとに管理を行います

(構造体のメンバーなど載せていますがここで詳しく知っておく必要はありません。

面倒くさければ適当に読み流してください)。


/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
        atomic_t                count;
        struct fdtable __rcu    *fdt;
        struct fdtable          fdtab;
  /*
   * written part on a separate cache line in SMP
   */
        spinlock_t              file_lock ____cacheline_aligned_in_smp;
        int                     next_fd;
        unsigned long           close_on_exec_init[1];
        unsigned long           open_fds_init[1];
        struct file __rcu       *fd_array[NR_OPEN_DEFAULT];
};



files_struct構造体
メンバー 説明
atomic_tcount 参照カウンターです。
struct fdtable __rcufdt ファイルディスクリプターテーブルを更新するときにRCUで排他制御を行います。
struct fdtablefdtab ファイルディスクリプターテーブルです。ここでプロセスが現在オープンしているファイルを参照することができます。
spinlock_tfile_lock ファイルディスクリプターテーブルを更新するためのスピンロックです。____cacheline_aligned_in_smpはなるべくキャッシュミスしないように配置するメモリーアドレスをL1キャッシュサイズにアラインします。
intnext_fd 次に割り当てるファイルディスクリプターの値を格納します。
unsigned long []close_on_exec_init[1] 初期化時にclose-on-execフラグをセットするファイルディスクリプターの集合(ビットで表します)です。
unsigned long []open_fds_init[1] 初期化時にセットするデフォルトオープンファイルディスクリプターの集合です。
struct file __rcu *[]fd_array[NR_OPEN_DEFAULT] 初期化時にセットするファイルディスクリプター(のポインター配列)です。NR_OPEN_DEFAULTlong型のビット数で定義されています。


ファイルディスクリプターテーブルstruct fdtable

struct files_structでファイルディスクリプターテーブルを管理します。

ファイルディスクリプターテーブルはstruct fdtableで管理していきます。


struct fdtable {
        unsigned int        max_fds;
        struct file __rcu   **fd;      /* current fd array */
        unsigned long       *close_on_exec;
        unsigned long       *open_fds;
        struct rcu_head     rcu;
};



fdtable構造体
メンバー 説明
unsigned intmax_fds このプロセスがオープンできるファイルディスクリプターの最大数です。
struct file __rcu **fd 実際にファイルオブジェクトが格納される配列です。オープンするファイルディスクリプターが多くなってくると動的に拡張していきます。初期化時にはfiles_structfd_arrayの領域を使用します。
unsigned long *close_on_exec close-on-execフラグがセットされているファイルディスクリプターをビットで表します。
unsigned long *open_fds 有効なオープンファイルディスクリプターをビットで表します。
struct rcu_headrcu ファイルディスクリプターテーブルを排他制御してアクセスするためにRCUを使用します。


プロセスとファイルディスクリプターテーブルとファイルオブジェクトは次のような

関係になっています。

ファイルディスクリプターテーブル

ファイルオブジェクトが管理するデータ

ファイルオブジェクトはオープンしているファイルを管理します。次のような管理データが

あります。

ファイルオブジェクトのメソッド

inodeのファイル操作メソッドと同じとなります。

マウント関連のファイルシステム共通補助オブジェクト

ここまで見てきましたファイルシステム共通オブジェクトを使ってマウントポイントや

プロセスのルートディレクトリを管理するための補助的な共通オブジェクトがあります。


ファイルシステムは木構造で表現されています。そのファイルシステム木構造の

なかでどこにファイルシステムがマウントされているのかといった情報を管理する

必要があります。また、Linuxではマウントポイントに複数回マウントを重ねる

ことができたり、ファイルシステムの一部部分を別のマウントポイントにマウント

したりできます。更に、Linuxではプロセスによってマウントしているファイルシステム

を個別に見せるための、マウント名前空間も実装しています。


このような情報も仮想ファイルシステムで管理を行っています。


次のような共通補助オブジェクトがあります。

mnt_namespace(マウント名前空間)オブジェクト

マウント名前空間オブジェクトをプロセスごとに管理することで、各プロセスが個別に違う

マウントポイントが見えるようになります。

mnt_namespaceについては呼び飛ばして頂いて問題ありません)。

				
struct mnt_namespace {
        atomic_t                count;
        unsigned int            proc_inum;
        struct mount            *root;
        struct list_head        list;
        struct user_namespace   *user_ns;
        u64                     seq;    /* Sequence number to prevent loops */
        wait_queue_head_t       poll;
        u64                     event;
};



mnt_namespace構造体
メンバー 説明
atomic_tcount 参照カウンターです。
unsigned intproc_inum procfsのinode番号です。
struct mount *root マウント名前空間のルートマウントポイントです。
struct list_headlist マウント名前空間に所属するマウントポイント(mountオブジェクト)を連結リストします。
struct user_namespace *user_ns このマウント名前空間に適用されるユーザー名前空間(プロセスごとのユーザーIDやグループIDを管理します)です。
u64seq マウント名前空間のシーケンス番号です。名前空間を新しく生成するたびに連続した番号を付与していきます。これにより古いマウント名前空間から新しく生成した名前空間へのバインドマウントを検出します。
wait_queue_head_tpoll マウント名前空間でイベント(マウントなど)発生時にそのイベントをポーリングしているプロセスを起動させるときに、現在のプロセスをスケジューラーのウェイトキューに入れます。
u64event マウント名前空間で発生したイベント(マウントなど)数です。


マウント名前空間でプロセスごとにマウントされたファイルシステムが別々のように

見えます。

マウント名前空間

mountオブジェクト

マウントポイントを表現するオブジェクトです。次のように定義されています。

				
struct mount {
        struct hlist_node mnt_hash;
        struct mount *mnt_parent;
        struct dentry *mnt_mountpoint;
        struct vfsmount mnt;
        union {
                struct rcu_head mnt_rcu;
                struct llist_node mnt_llist;
        };	
#ifdef CONFIG_SMP
        struct mnt_pcp __percpu *mnt_pcp;
#else
        int mnt_count;
        int mnt_writers;
#endif
        struct list_head mnt_mounts;    /* list of children, anchored here */
        struct list_head mnt_child;     /* and going through their mnt_child */
        struct list_head mnt_instance;  /* mount instance on sb->s_mounts */
        const char *mnt_devname;        /* Name of device e.g. /dev/dsk/hda1 */
        struct list_head mnt_list;
        struct list_head mnt_expire;    /* link in fs-specific expiry list */
        struct list_head mnt_share;     /* circular list of shared mounts */
        struct list_head mnt_slave_list;/* list of slave mounts */
        struct list_head mnt_slave;     /* slave list entry */
        struct mount *mnt_master;       /* slave is on master->mnt_slave_list */
        struct mnt_namespace *mnt_ns;   /* containing namespace */
        struct mountpoint *mnt_mp;      /* where is it mounted */
        struct hlist_node mnt_mp_list;  /* list mounts with the same mountpoint */
#ifdef CONFIG_FSNOTIFY
        struct hlist_head mnt_fsnotify_marks;
        __u32 mnt_fsnotify_mask;
#endif
        int mnt_id;                     /* mount identifier */
        int mnt_group_id;               /* peer group identifier */
        int mnt_expiry_mark;            /* true if marked for expiry */
        struct hlist_head mnt_pins;
        struct path mnt_ex_mountpoint;
};



mount構造体
メンバー 説明
struct hlist_nodemnt_hash マウントオブジェクトのハッシュリストにつなげるノードです。
struct mount *mnt_parent マウントオブジェクトの親マウントオブジェクトとなります。
struct dentry *mnt_mountpoint マウントポイントのdエントリーです。
struct vfsmountmnt マウントポイントにマウントされているファイルシステムを表します。
struct rcu_headmnt_rcu RCUコールバックでマウントオブジェクトを解放します。
struct llist_nodemnt_llist
struct mnt_pcp __percpu *mnt_pcp マウントオブジェクト更新中のプロセス数をカウントします。
struct list_headmnt_mounts マウントポイントの子マウントポイントの連結リストです。
struct list_headmnt_child 親マウントポイントのマウントオブジェクトのmnt_mountsに連結するノードです。
struct list_headmnt_instance マウントポイントにマウントされているスーパーブロックオブジェクトのs_mountsに連結するノードです。
const char *mnt_devname マウントするデバイス名が格納されます。mountsource引数で指定したデバイス名となります。
struct list_headmnt_list マウント名前空間mnt_namespacelistに連結するノードです。
struct list_headmnt_expire 未使用期間の期限が来たときに自動アンマウントするマウントオブジェクトを管理するリストに連結します。NFSなどで使用します。
struct list_headmnt_share マウントオブジェクトをcloneするときに複製元と複製先で共有するための連結リストです。 共有マウントで使用します。
struct list_headmnt_slave_list スレーブマウントオブジェクトの連結リストとなります。スレーブのマウントポイントを作成するときに使用します。
struct list_headmnt_slave マウントオブジェクトをmnt_slave_listに連結するためのリストノードです。
struct mount *mnt_master スレーブのマウントオブジェクトからマスターのマウントオブジェクトを参照します。
struct mnt_namespace *mnt_ns このマウントオブジェクトが属するマウント名前空間となります。
struct mountpoint *mnt_mp マウントポイントを管理します。次のような構造体となっています。

				
struct mountpoint {
        struct hlist_node m_hash;
        struct dentry *m_dentry;
        struct hlist_head m_list;
        int m_count;
};



m_listにマウントオブジェクトのmnt_mp_listを連結して、同じマウントポイントに複数のマウントを行ったときのマウントオブジェクトの管理を行います(同じマウントポイントに複数回マウントを行うと最後に行ったマウントのみが見えるのはここで管理を行っているからです)。
struct hlist_nodemnt_mp_list マウントポイントmnt_mpで複数マウントを管理する連結リストmnt_mp->m_listに連結するノードとなります。
struct hlist_headmnt_fsnotify_marks fanotifyでマークをつけたマウントポイントを管理するリストに使用するノードです。
__u32mnt_fsnotify_mask fanotifyで指定したマスクを格納します。
intmnt_id マウントオブジェクトのマウントIDです。
intmnt_group_id マウントオブジェクトのマウントグループIDです。
intmnt_expiry_mark 未使用期間の期限が来たときに自動アンマウントを行うかどうかのフラグです。
struct hlist_headmnt_pins マウントオブジェクトがアンマウントされたときにピンに挿入されている処理を呼び出します。処理を呼び出すためにfs_pin構造体のリストノードをmnt_pinsに連結します。例えば、ACCTファイルはこれを利用してアンマウント時にファイルの内容をフラッシュするようにしています。
struct pathmnt_ex_mountpoint 名前空間をアンロックするときまでに親マウントポイント情報を保存しておくために使用しています。


マウントオブジェクトは次のようにマウントの親子関係を表します。

マウントオブジェクト

vfsmountオブジェクト

マウントしたファイルシステムを表します。

				
struct vfsmount {
        struct dentry      *mnt_root;   /* root of the mounted tree */
        struct super_block *mnt_sb;     /* pointer to superblock */
        int                 mnt_flags;
};



vfsmount構造体
メンバー 説明
struct dentry *mnt_root マウントされたファイルシステムのルートdエントリーオブジェクトです。
struct super_block *mnt_sb マウントされたファイルシステムのスーパーブロックオブジェクトです。
intmnt_flags vfsmountの制御フラグです。


vfsmountオブジェクトは次のようにマウントされたファイルシステムを表します。

vfsmountオブジェクト

共通オブジェクトが使用される例

それではここで、各ファイルシステム共通オブジェクトについて、

  1. ファイルシステムのマウント

  2. マウントしたファイルシステムでファイルをオープン

を行ったときに共通オブジェクトがどのように使われるのかを少し詳しく見ていきたいと

思います。

1.ファイルシステムのマウント

マウントはmountシステムコールで行います。mountはファイルシステムを

ルートファイルシステムツリーに新しいファイルシステムを接合します。これにより初めて

ファイルシステムを利用することができるようになります。mountシステムコールの

引数にはデバイスパスを指定するsource引数とファイルシステムをマウントする

パスを指定するtarget引数があります。また、マウントするファイルシステムの

タイプをfilesystemtypeに指定します。


今回はmountシステムコールの説明で作成したプログラム例を使用して、

プログラムの実行例と同じようにsource/dev/ram0targetにカレント作業

ディレクトリに存在する/home/user/targetディレクトリ、filesystemtyperamfsを指定して

システムコールを呼び出した場合を見ていきます。


mountの動作概略は次のようになります。

mountは実際にはもっと複雑な処理を行っていますが、ここではマウント

名前空間などの処理は省いています)。

  1. ファイルシステムタイプオブジェクトの取得

  2. targetで指定されたパス名の解決

  3. mountオブジェクトを(つまりvfsmountオブジェクトも)メモリーから割り当て

  4. filesystemtypeオブジェクトのmountメソッドでファイルシステムの

    スーパーブロックを読み込み、ファイルシステムのdエントリーを取得

  5. マウントツリーに新しくマウントするmountオブジェクトを追加する。

となります。この手順はおおよその操作で細かい手順は省いています。



まずは、これらの引数を指定してmountシステムコールがどのように動作して

いくのかを見ていきましょう。

1−a.filesystemtypeオブジェクトの取得

mountシステムコールの引数filesystemtypeで指定されたファイルシステム

タイプオブジェクトを取得します。オブジェクトの取得といってもオブジェクトの

ポインターを取得するだけとなります。目的のファイルシステムタイプオブジェクトは

ユーザーが指定したfilesystemtype引数との文字列を比較して探すだけとなります。

指定された文字列が見つかれば該当オブジェクトのポインターを取得して憶えて

おきます。これにより、目的のファイルシステムのmountメソッドを呼び出すことができます。



ファイルシステムタイプオブジェクトの取得

1−b.targetで指定されたパス名の解決

mountシステムコールに限らず、パスを指定するシステムコールは多い

です。パス名の解決は実に多くのプロセスが必要とする処理となりますので、

仮想ファイルシステムでさまざまな工夫が凝らされています。その1つとして

dエントリーオブジェクトとinodeオブジェクトのキャッシュとしての役割がありますが、



今回は説明上まだルート以外のディレクトリのdエントリーがまだないと仮定して

いきます。(本当はmkdirコマンドを行ったときや、lsコマンドを行った

ときにdエントリーのキャッシュが作成されていますが、ここでは説明のために

システム上で初めてパス名解決を行ったことにします。)



targetには/home/user/targetを指定していました。

このときに作成されるdエントリーは指定されたパスの各要素について生成されます。

また、dエントリーが生成された後には対応するinodeが存在するかディレクトリ内の

ファイル(ディレクトリのエントリー)検索メソッドを行います。これにより、ファイルシステム

固有のディレクトリエントリー探索処理が開始されます。そして対応するエントリーが

あれば、そのエントリー(ファイル)のinodeを取得してdエントリーと結合します。

そして、そのinodeは次のシステムコールで使用される可能性が高いため、

いったんdエントリーと関連付けておけば、次に同じエントリーにアクセスするときには

ディスクアクセスすることなくinodeキャッシュにアクセスすることができます。

パス名解決

もしこのとき、エントリーが見つからなければ、dエントリーに対応するinodeも無い

というわけですから、このときこのdエントリーはネガティブ(負の)dエントリーといいます。

これは、一見して役に立たないオブジェクトのように思いますが、Linuxではこの

ネガティブdエントリーをほかのdエントリーキャッシュと同様になるべくメモリーに留めて

おくようにしています。これは、プログラムでは再度同じようなパスを指定してシステム

コールが発行されてくる可能性が高いためであるからです。例えば、プログラムを作成

するときに管理ディレクトリを作成してその配下に管理するユーザーごとのディレクトリ

を作って、そのディレクトリの中にユーザーごとの設定ファイルを作成する場合を考えて

みます。このとき、最初の管理ディレクトリの作成に失敗した後でも、エラーをチェック

せずにいると、各ユーザーごとのディレクトリを作成しようとパスを指定してきます。

このとき、最初の管理ディレクトリのdエントリーはネガティブdエントリーとなっているため

ディスクアクセスすることなく、すぐにその後のシステムコールについてエラーを返すことが

できます。このように、ネガティブdエントリー自体も"ファイルが無い"ということをキャッシュ

することができます。

ネガティブdエントリー

1−c.mountオブジェクトをメモリーから割り当て

mountオブジェクトはカーネルメモリーから動的に割り当てます。

メモリーはスラブ(デフォルトではSLOBで、またはSLAB)アロケーターから

割り当てを行います。スラブアロケーターによるメモリー割り当ては特に

仮想ファイルシステムの共通オブジェクトのように、小さなメモリーを頻繁に

割りあてたり、開放したりするような時にキャッシュから効率良くメモリーを

割り当てることができるメモリーアロケーターとなります。(スラブアロケーター

についてはもう少し後で実際に使用していきます。インターフェースも含め

後でもう少しだけ詳しく見ていきたいと思います)。仮想ファイルシステム

オブジェクトは基本的にスラブアロケーターを利用してオブジェクトの割り当て

や解放を行います。先ほどの手順1−b.で見てきましたパス名の解決での

dエントリーオブジェクトやinodeオブジェクトの割り当てや解放もそうです。

mountオブジェクト割り当て

1−d.filesystemtypeオブジェクトのmountメソッドでスーパーブロック読み込み

スーパーブロックは通常ファイルシステムの制御ブロックに格納されています。

ファイルシステムの制御ブロックには各ファイルシステムでそれぞれ格納して

いる情報が違います。そこで、mountメソッドでファイルシステム

固有のマウント処理を行うことになります。ファイルシステムのマウント処理

では最終的にルートディレクトリのinodeも読み込み、dエントリーの生成

まで行います。今回例で見るramfsはスーパーブロックの読み込みでディスク

アクセスは発生しません。ramfsではあらかじめモジュールで定義された値

でスーパーブロックオブジェクトを初期化します。

mountメソッド呼び出し

1−e.マウントツリーに新しくマウントするmountオブジェクトを追加する。

mountメソッドで取得したルートディレクトリのdエントリーオブジェクト

からramfs用のmountオブジェクトを設定します。

mountオブジェクトの設定

次にマウントしたramfsでファイルをオープン(作成)したときの例を見ていきます。

2.マウントしたファイルシステムでファイルをオープン

この例ではopenシステムコールのパスに先ほどマウントを行いましたramfs上の

ファイルパスを指定します。また、ファイルを新規作成(今回はまだファイルが無いという

例となります)するためにオープンフラグとしてO_CREATを指定する例となります。

O_CREATフラグを指定したファイルオープンはopenシステムコールのプログラム例

参照してください。


先ほどのマウントを行ったときの例で見てきましたように、オープンするファイルを指定

したときのパス解決は、dエントリーオブジェクトとinodeオブジェクトのキャッシュで

高速に処理を行うことができます。

ここでは、オープンする(作成する)ファイルのパスpathname/home/user/target/file

指定した場合について見ていきます。

openの動作概略は次のようになっています。

openはもっと複雑な動作をしていますが、共通オブジェクト以外の処理や

パーミッションチェック処理はここでは省略しています)。

  1. ファイルディスクリプターテーブルに空きがあればそこからファイルディスクリプターを取得

    (ファイルディスクリプターテーブルに空きが無い場合はテーブルを拡張します。

    ここではその処理は省きます)。


  2. pathnameのパス名解決。

  3. (パス名解決でファイルが無くて、O_CREATが指定されている場合、

    ファイルを作成します。)作成するファイルの親ディレクトリのinodeのファイル作成

    メソッドを呼び出し、ファイルを作成。


  4. ファイルオブジェクトの初期化を行いファイルディスクリプターテーブルに登録

それでは、順番に見ていきます。

2−a.ファイルディスクリプターテーブルに空きがあればそこからファイルディスクリプターを取得

仮想ファイルシステムでは、まずユーザーから指定されたオープンフラグ(新規作成や追記モードなど)を

確認してから(この処理はここでは省略しています)ファイルディスクリプターテーブルに空きがあるか

確認します。空きが無い場合、現在のテーブルより大きいメモリーを新しく割り当てて、ファイル

ディスクリプターテーブルを拡張します。空きがある場合そのファイルディスクリプターを使用します。


次の図ではファイルディスクリプターテーブルを配列として表していますが、実際にはビットマップで

ファイルディスクリプターが空いてるかどうかを管理しています。

ファイルディスクリプターを取得

2−b.pathnameのパス名解決

ここでは、mount時に/home/user/targetの名前解決が行われていますので、dエントリー

オブジェクトはキャッシュとして動作することになります。つまり、この段階ではすべて仮想

ファイルシステムないで処理が行われ、個別のファイルシステムにアクセスはしませんので

非常に高速に処理できることになります。ファイル名のfileはこの段階でファイル

として存在していませんので、dエントリーオブジェクトの生成までを行うことになります。

openのパス名解決

2−c.作成する親ディレクトリのinodeのファイル作成メソッドを呼び出し、ファイルを作成

ここまで、fileのdエントリーオブジェクトとその親ディレクトリであるマウントした

ファイルシステムのルートディレクトリのdエントリーとinodeオブジェクトを取得できています。

ファイルを作成するにはディレクトリinodeのファイル作成メソッドの呼び出し、ファイルシステム

固有のファイル作成処理を行います。

ramfsファイル作成メソッド呼び出し

2−d.ファイルオブジェクトの初期化を行いファイルディスクリプターテーブルに登録

ramfsではVFSでオープン処理を行うだけとなりますので、後はファイルオブジェクトを適切に

初期化してファイルディスクリプターテーブルに登録するだけとなります。ただし、ディスクベースの

ファイルシステムではクォータ機能があるときにクォータをチェックする必要がありますので、別途

ファイルオブジェクトからファイルシステム固有のファイルオープンメソッドを呼び出す必要があります。


ここまでで、仮想ファイルシステムを大まかに知ることができましたが、この段階で内容的に理解

できていなくて問題ありません。ファイルシステム関連のシステムコールで仮想ファイルシステムの

処理が行われ、仮想ファイルシステムの共通オブジェクトのメッソドから各ファイルシステム固有の

処理が呼び出されるということが理解いただければそれでよいかと思っています。また、スーパー

ブロックやinodeなどまだ詳しく見ていませんので、Ext2ファイルシステムの実装で更に詳しく

見ていきましょう。


説明ばかりでしたので、次は少し実装面を見ていきたいと思います。いよいよファイルシステム

を実装していきます。

PR 説明

ディベロッパー・エクスペリエンス Linux Ext2ファイルシステム
0から作るLinuxプログラム Linux Ext2ファイルシステムの完全説明版(Kindleのみ)となります。実装するステップ数は36ステップあります。少しずつステップを実装して、実際に動かして学習していきます。ファイルシステムの機能を呼び出すシステムコールの実装から、仮想ファイルシステムが呼び出すExt2ファイルシステムの実装を行っていきます。ファイルシステムの開発体験を通すことで、実際にカーネルが行っているファイルシステムの基礎的な動作を理解することができます。ぜひお試しください!

inserted by FC2 system