【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

ファイルシステムの読み込み


id:oraccha:20101101の手順を参考に、UNIX V6をSIMHで動かしてみました。分析のためファイルを取り出そうとしたのですが、TCP/IP以前のOSのためネットワークが使えるのか不明で、ディスクイメージからファイルを取り出す方法も分かりません。仕方ないのでカーネルのソースを読みながらファイルを取り出してみました。

読み込みツールはF#で実装しました。ブラウザ上から実行できるようにSilverlight化しました。id:n7shi:20101004で作成したZIP圧縮コードにより、ファイルシステム全体をZIPファイルとして保存することができます。

【追記:2015.11.12】ローカルで動かすGUI版もあります。(Silverlight非依存)

以下、UNIX V6のC言語ソースを引用しながら簡単に説明します。ソースコードはタブで整形されていますが、レイアウトが崩れるのを防ぐためスペースに置換して引用しました。タブは8文字を想定しているようです。

構造

前掲の図がディスクイメージの構造です。左の番号はブロック番号で、1つのブロックは512バイトです。ブロックは現在セクタと呼ばれている概念と同じもののようです。最初のブロックはMBRと書きましたが、これはx86用語で、当時どう呼ばれていたかは不明です。IPLの機械語コードが生バイナリとして入っていて、0000〜0777(0x0000〜0x01FF)番地に読み込まれて実行されるようです。

スーパーブロック

MBRの次のブロックにはファイルシステムの情報が収められ、スーパーブロックと呼ばれています。filsys構造体として定義されています。

/usr/sys/filsys.h
/*
 * Definition of the unix super block.
 * The root super block is allocated and
 * read in iinit/alloc.c. Subsequently
 * a super block is allocated and read
 * with each mount (smount/sys3.c) and
 * released with unmount (sumount/sys3.c).
 * A disk block is ripped off for storage.
 * See alloc.c for general alloc/free
 * routines for free list and I list.
 */
struct  filsys
{
        int     s_isize;        /* size in blocks of I list */
        int     s_fsize;        /* size in blocks of entire volume */
        int     s_nfree;        /* number of in core free blocks (0-100) */
        int     s_free[100];    /* in core free blocks */
        int     s_ninode;       /* number of in core I nodes (0-100) */
        int     s_inode[100];   /* in core free I nodes */
        char    s_flock;        /* lock during free list manipulation */
        char    s_ilock;        /* lock during I list manipulation */
        char    s_fmod;         /* super block modified flag */
        char    s_ronly;        /* mounted read-only flag */
        int     s_time[2];      /* current date of last update */
        int     pad[50];
};

PDP-11は16bitマシンのため、intは16bit符号あり整数型(int16_t)です。sizeof(filsys)は516バイトで、ブロック(512バイト)より大きいのですが、パディング(pad)が大雑把に定義されているだけで、実際に516バイト使われているわけではないようです。

ブロックやinodeのリストは書き込む際に必要となりますが、今回は読み込みだけを対象としているので、詳細は飛ばします。

in coreという表現がありますが、coreは今で言うメモリを指しているため、「メモリ上」という意味で使われているようです。ディスクイメージを見ると値が書き込まれているため、純粋にメモリ上だけに存在するわけではなく同期しているようです。

実際の読み込みはmain()から呼ばれるiinit()で行われています。日本語のコメントは私の追記です。

/usr/sys/ken/alloc.c
/*
 * iinit is called once (from main)
 * very early in initialization.
 * It reads the root's super block
 * and initializes the current date
 * from the last modified date.
 *
 * panic: iinit -- cannot read the super
 * block. Usually because of an IO error.
 */
iinit()
{
        register *cp, *bp;

        (*bdevsw[rootdev.d_major].d_open)(rootdev, 1); /* rootdevを開く */
        bp = bread(rootdev, 1); /* 1はブロック番号 */
        cp = getblk(NODEV); /* メモリ確保, malloc(512)に相当 */
        if(u.u_error)
                panic("iinit");
        bcopy(bp->b_addr, cp->b_addr, 256); /* memcpy()に相当, 256はワード長 = 512バイト */
        brelse(bp); /* 読み込んだブロックのデータを解放 */
        mount[0].m_bufp = cp;
        mount[0].m_dev = rootdev;
        cp = cp->b_addr; /* 確保されたメモリの生ポインタを取り出す */
        cp->s_flock = 0;
        cp->s_ilock = 0;
        cp->s_ronly = 0;
        time[0] = cp->s_time[0];
        time[1] = cp->s_time[1];
}

s_flock, s_ilock, s_ronlyは読み込み後に初期化していることから、ディスク上では同期していないようです。

inode

スーパーブロックの次にはinode(アイノード)のリストが来ます。inodeというのはファイルに対するヘッダのようなものですが、ハードリンクにより複数のファイルから1つのinodeを参照することもあるため、1対1でファイルと対応しているわけではありません。そのためinodeにはファイル名の情報が含まれていません。

inode全体で使えるブロックサイズは filsys.s_isize で示されています。1ブロックが512バイトで、1つのinodeが32バイトであることから、ファイルシステム上のinode数の上限は s_isize * 16 個となります。

inodeは1から数えます(0は「なし」を意味します)。最初のinode(inode番号1)がルートディレクトリです。inodeの抽出はiget()で行われています。ldivは除算、lremは剰余です。

/usr/sys/ken/iget.c: iget()より抜粋・追記
        ip = bread(dev, ldiv(ino+31,16)); /* (ino+31)/16 */
        ip1 = ip->b_addr + 32*lrem(ino+31, 16); /* 32*((ino+31)%16) */

inode番号2の場合、ino=2, (2+31)/16=2, 32*((2+31)%16)=32 となり、ディスク上のブロック番号2の32バイト目に存在します。

構造体

inode構造体はino.hとinode.hの2箇所で定義されていますが、ino.hはディスク上の格納形式を示すためだけに含まれているヘッダで、実際にはコードから使用されていないようです。

/usr/sys/ino.h
/*
 * Inode structure as it appears on
 * the disk. Not used by the system,
 * but by things like check, df, dump.
 */
struct	inode
{
        int     i_mode;
        char    i_nlink;
        char    i_uid;
        char    i_gid;
        char    i_size0;
        char    *i_size1;
        int     i_addr[8];
        int     i_atime[2];
        int     i_mtime[2];
};

/* modes */
#define IALLOC  0100000
#define IFMT    060000
#define         IFDIR   040000
#define         IFCHR   020000
#define         IFBLK   060000
#define ILARG   010000
#define ISUID   04000
#define ISGID   02000
#define ISVTX   01000
#define IREAD   0400
#define IWRITE  0200
#define IEXEC   0100

構造体の個々のフィールドの意味は、inode.hの方に書いてあります。i_flagなどディスク上に存在しないヘッダが付いています。

/usr/sys/inode.h
struct  inode
{
        char    i_flag;
        char    i_count;        /* reference count */
        int     i_dev;          /* device where inode resides */
        int     i_number;       /* i number, 1-to-1 with device address */
        int     i_mode;
        char    i_nlink;        /* directory entries */
        char    i_uid;          /* owner */
        char    i_gid;          /* group of owner */
        char    i_size0;        /* most significant of size */
        char    *i_size1;       /* least sig */
        int     i_addr[8];      /* device addresses constituting file */
        int     i_lastr;        /* last logical block read (for read-ahead) */
} inode[NINODE];

i_modeの下位12ビットはいわゆるパーミッションで、chmodで使われる4桁の8進数(0755, 0644等)が格納されています。IFDIRのビットが立っていればディレクトリを示します。

i_addrにはディスク上のデータの位置がブロック番号で格納されています。ブロック番号が16bit整数で管理されていることから、サポートされるディスクサイズの上限が 65536 * 512 = 32MB となります。断片化をサポートするため複数のブロックが指定できるようになっています。9ブロック以上使用しているとi_addr[8]に入りきらなくなりますが、そのときの処理はデータブロックの節で説明します。

i_atime, i_mtimeはinode.hの方には含まれていませんが、それぞれ最終アクセス日時・更新日時を示します。1970年1月1日午前0時を起点とする秒数が32bitで格納されています。いわゆるUNIX時間です。intが16bitのため、32bitは2つのintで表現されています。上位・下位はPDP-11特有のミドルエンディアンで表現されています。

ファイルサイズはi_size0(上位), i_size1(下位)で示されます。これは全体で24bitの数値を示しているため、扱えるファイルサイズの上限は16MBとなります。i_size1はcharのポインタ型ですが、ポインタではなく16bit符号なし整数型(uint16_t)として使われています。id:oraccha:20101106によれば当時のC言語(pre K&R)には符号なし整数型がなかったため、ポインタで代用していたとのことです。

ミドルエンディアン

PDP-11は16bitをリトルエンディアンで表現しますが、32bitは上位ワードを先に置くミドルエンディアンと呼ばれる形式を使用しています。具体的には以下の通りです。

エンディアン 0x12345678
ビッグ 12 34 56 78
リトル 78 56 34 12
ミドル 34 12 78 56

あくまで想像ですが、PDP-11のCPU上の実装というよりも、ビッグエンディアンの16bitマシンを念頭に書かれたC言語のコードをリトルエンディアンのマシンで動かした結果、ミドルエンディアンが発生したように感じます。

64bit以上はどう表現されるか気になる所ですが、UNIX V6のソースコードで64bitを扱っている箇所はないようです。

データブロック

inode.i_addrでデータブロックの番号が示されます。ブロック番号は1から始まるため、0は「なし」を意味します。連続したブロックでも1つずつ示されます。

使用しているブロックが9個以上でi_addr[8]に収まりきらない場合、imodeにILARGフラグが立ち、i_addrが指すブロックにはブロックマップが来ます。ブロックマップにはデータブロックの番号が1つずつ格納されています。ちなみにILARGのIはinodeの接頭辞で、LARGはlargeの略のようです。

【追記】id:takahirox:20110815:1313398103より。ILARGの場合、i_addr[0〜6]は上記説明の通りですが、i_addr[7]が指すブロックは「ブロックマップのブロックマップ」という二重ポインタとなっています。これによりディスク上限(65536 * 512 = 32MB)をやや上回る(7 * 256 * 512 + 256 * 256 * 512 = 32.875MB)サイズのファイルが理論上マッピング可能になりますが、サイズ自体が24bitのため16MB以上は使用不可となります。

8ブロック以内
  • i_addr → データブロック
9ブロック以上
  • i_addr[0〜6] → ブロックマップ → データブロック
  • i_addr[7] → ブロックマップのブロックマップ → ブロックマップ → データブロック

ディレクト

前述のようにinode.i_modeにIFDIRフラグが立っているinodeがディレクトリです。データブロックにはそのディレクトリに含まれるファイルやディレクトリのinode番号と名前が列挙されています。これをディレクトリエントリと呼びます。ファイル名はディレクトリエントリにしか含まれないため、inode単体からファイル名を知ることはできません。

カーネルにはディレクトリエントリの構造体は見当たりません。lsコマンドではユーザー側でディレクトリのデータブロックを読み取ってディレクトリエントリを列挙しています。ファイル名の上限が14文字だということが分かります。

/usr/source/s1/ls.c: readdir()より抜粋
        static struct {
                int     dinode;
                char    dname[14];
        } dentry;

ディレクトリエントリの数はinode.i_nlinkで示されますが、削除されたディレクトリエントリは dinode = 0 の状態でそのまま残っているため、残骸を飛ばしながら列挙する必要があります。

まとめ

イメージからファイルシステムを読み込む手順は以下のようになります。

  1. ルートディレクトリのinodeからデータブロックを取得
  2. データブロックに含まれるinodeを取り出す
  3. inodeがディレクトリなら再帰的に2に戻る

これを実装したのが冒頭のデモです。