Pick up

xv6 で見るファイル操作 〜システムコール open ・ close〜

tak

こんにちは、DWS2人目の大島です。
今日は、Linux のファイルの open ・ close の操作について、少し低レイヤの部分をのぞいてみたいと思います。

はじめに

まず、初めに「ファイルってなんなの?」っていう部分を念の為確認しておきましょう。

ファイルとは、プログラムからブロックデバイス(ストレージ)に保存されている情報にアクセスするための一つの管理単位のようなものです。

プログラムから扱うデータは、永続的に保存するためにはブロックデバイスに保存しなければなりません。
ただ、ブロックデバイスへのアクセスはメモリからのアクセスと比較して非常に時間がかかるため、永続化されたブロックデバイス上のデータにアクセスしていては、パフォーマンスが非常に悪くなってしまいます。

そこで、データを操作する場合、ブロックデバイス上からメモリの上に持ってきてプログラムから扱います。

今回は、このブロックデバイス上のデータをファイルとしてメモリ上管理するための仕組みがどうなっているのか実際にプログラムを使って確認していきます!

今回利用する xv6 について

今回利用するのは、xv6 という unix OS です。
様々な大学で、OS の学習のために広く使われている OS で、コード量が少ない非常にシンプルなOSです。OS 初心者がコードリーディングをしようと思った場合に一番最初に候補に上がる OS ではないかと思います。

今回の記事を読む前に
  • プロセスについては、特に詳細には触れません
  • xv6 はマルチスレッド対応はしていないため、スレッドについては取り扱いません
  • xv6 のビルド方法・デバッグ方法については触れません

プログラムがファイルを扱うための全体像

まずは全体像を確認しておきましょう!

プログラムがファイルを取り扱うための実装はこんな感じになっています。

一つのプログラムが実行されると、一つのプロセスとして裏側では処理がされます。

この画像を見て「全然わからない」となっても問題ないです。
一つ一つ順番に追いかけていきます。

全体像


一応、簡単に登場要素を以下にまとめておきます。

名前存在場所説明
proc 構造体メモリプロセスの情報を管理するための構造体
file 構造体メモリプロセスが開いているファイル情報を管理するための構造体
inode 構造体メモリファイルのメタ情報を管理するための構造体
buf 構造体メモリファイルの実体データを書き込むためのバッファの構造体
inodeブロックデバイスブロックデバイス上でファイルのメタ情報を管理するためのデータ
ブロックブロックデバイスブロックデバイス上のファイルの実体データが書き込まれる場所

では最初から、どうやってプログラムからファイルを開いて管理しているのか確認してきましょう!

proc 構造体(プロセス)

まずは、proc構造体です。
proc構造体とは、プロセスの状態を管理する構造体です。
一つのプログラムを実行すると、一つのプロセスとなって起動され、proc構造体として管理されます。

このproc構造体というのは、xv6 においては、ptableというグローバル変数として管理されます。

proc と ptable

以下が proc構造体の定義となります。

情報量が多くて嫌になるので、今回はofile[]という要素にだけ着目しましょう。

// Per-process state
struct proc {
  uint sz;                     // Size of process memory (bytes)
  pde_t* pgdir;                // Page table
  char *kstack;                // Bottom of kernel stack for this process
  enum procstate state;        // Process state
  int pid;                     // Process ID
  struct proc *parent;         // Parent process
  struct trapframe *tf;        // Trap frame for current syscall
  struct context *context;     // swtch() here to run process
  void *chan;                  // If non-zero, sleeping on chan
  int killed;                  // If non-zero, have been killed
  struct file *ofile[NOFILE];  // Open files
  struct inode *cwd;           // Current directory
  char name[16];               // Process name (debugging)
};

このproc構造体の中の、ofile[]がプロセスが開いているファイルの情報を管理している部分になります。(ofile[] には、file構造体へのポインタが格納されて、現在プロセスが開いているファイルの情報が保存されています。

ファイルディスクリプタ

ちなみに、このofile[]に入るインデックスが「ファイルディスクリプタ」と呼ばれるものになります。名前は聞いたことがある人が多いのではないでしょうか?

プロセスが生成されると、標準入力・標準出力・標準エラー出力は、それぞれ 0, 1, 2 番目のファイルとしてofile[]に格納されます。
つまり、ファイルディスクリプタとは、プロセスが開いているファイルを単に連番で管理をしているだけのものとなります。

プログラムからアクセスしているファイルを利用する場合、
このファイルディスクリプタを指定することで、ofile[]で管理されている何番目のファイルにアクセスしたらいいか、判別することができるわけです。

では、次にfile[]構造体を確認してみましょう!

file 構造体(プロセスが開いているファイル情報)

file構造体は、プログラムが開いているファイルの状態を管理するための構造体です。
ftableというグローバル変数の中で管理されています。

file と ftable

以下がfile構造体の中身です。
この file 構造体、名前からファイルの実体を指しているのかと思えば、全然そんなことはありません。後ほど出てくるinode構造体と交えて話しますので、一旦、ここではinodeという要素にだけ着目しましょう。

// https://github.com/mit-pdos/xv6-public/blob/eeb7b415dbcb12cc362d0783e41c3d1f44066b17/file.h#L1

struct file {
  enum { FD_NONE, FD_PIPE, FD_INODE } type;
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe;
  struct inode *ip;
  uint off;
};

このfile構造体は、inode構造体を参照しています。
では次は、inode構造体を見ていきましょう!

inode 構造体

inode も言葉としてなんとなく聞いたことある、という方も多いかもしれません。
inode とは UNIX において、ファイルシステム上の各ファイルやディレクトリに対して一意に割り当てられる番号です。
そして、inode構造体は、実際の inode 番号や inode と紐づくファイルの情報を管理するための xv6 上のデータ構造です。

inode構造体は、 icacheというグローバル変数上で管理されます。

inode と icache

ここで「ファイルの情報ってさっきのfile構造体とどう違うの?」という疑問が湧くかもしれません。
inode構造体の中身を確認してみましょう。

// https://github.com/mit-pdos/xv6-public/blob/eeb7b415dbcb12cc362d0783e41c3d1f44066b17/file.h#L12C1-L26C3

// in-memory copy of an inode
struct inode {
  uint dev;           // Device number
  uint inum;          // Inode number
  int ref;            // Reference count
  struct sleeplock lock; // protects everything below here
  int valid;          // inode has been read from disk?

  short type;         // copy of disk inode
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+1];
};

ここでfile構造体の中身をもう一度みてみましょう。

// https://github.com/mit-pdos/xv6-public/blob/eeb7b415dbcb12cc362d0783e41c3d1f44066b17/file.h#L1

struct file {
  enum { FD_NONE, FD_PIPE, FD_INODE } type;
  int ref; // reference count
  char readable;
  char writable;
  struct pipe *pipe;
  struct inode *ip;
  uint off;
};


file構造体には、readableや writableといった項目がありますね。
名前からわかる通り、これらの値はファイルを「読み取り状態で開いているのか?」「書き込み可能状態で開いているのか?」といった情報を管理しています。
つまり、file構造体には「プログラムがファイルを開いている状態」が保存されています。

一方、inode構造体の方には devaddrs と言った要素があります。dev は device の省略で、ファイルの実体のデータが保存されているブロックデバイスを示しており、addrs は address の略称で、該当のブロックデバイスにおけるデータの格納されている場所を示しています。
つまり、inode 構造体には「ファイル本体のメタ情報」が保存されている訳です。

さて、ここで inode 構造体の中身のコードに、

// in-memory copy of an inode

というコメントが残っています。
日本語にすると、この inode 構造体は inode のメモリ上のコピーと書かれてますね。
実は inode はブロックデバイス上に本来保存されているもので、inode 構造体はメモリ上のキャッシュです。(inode はファイルの実体データの管理をしている情報ですので、永続化しないとファイルの情報が消えてしまいますから、当たり前と言えば当たり前ですが)

デバイス上の inode

このようにメモリ上に inode をキャッシュする理由は xv6 のコードのコメントに記載があります。

// The kernel keeps a cache of in-use inodes in memory
// to provide a place for synchronizing access
// to inodes used by multiple processes. The cached
// inodes include book-keeping information that is
// not stored on disk: ip->ref and ip->valid.

カーネルは、複数のプロセスから使用される inode へのアクセスを同期させるための場所を提供するために、メモリ上に使用中の inode のキャッシュを保持します
(メモリ上に)キャッシュされた inode は、ディスクには保存されない、管理情報を含みます

https://github.com/mit-pdos/xv6-public/blob/master/fs.c#L108

キャッシュをする理由は大きく2つです。

  • メモリ上にキャッシュをすることで、アクセスを高速にする
  • inode に対してロックをかけられるよう制御を行う情報を追加し、複数のプロセスからも安全にデータを扱えるようにする
    • dinodeが、ディスク上の inode を表す構造体ですが、これにはロックの情報はありません

このように、inode はブロックデバイス上のものをメモリに読み込んでキャッシュして利用しています。
メモリ上にファイルの情報がキャッシュされていなかったり、ファイルの情報が最新のものでなかったりした場合、os はメモリ上に対象の inode の情報をブロックデバイスから読み込んできてファイルのメタ情報へのアクセスを行います。

では最後に inode からファイルの実体となるデータが格納されている場所を確認しましょう!

バッファとブロック


先ほども少し触れましたが、inode 構造体には、ファイルの実体が格納されているブロックのアドレスを示す要素が格納されています。
↓の inode 構造体の中に addrs という要素がありますね、こちらがその要素です。

// https://github.com/mit-pdos/xv6-public/blob/eeb7b415dbcb12cc362d0783e41c3d1f44066b17/file.h#L12C1-L26C3

// in-memory copy of an inode
struct inode {
  uint dev;           // Device number
  uint inum;          // Inode number
  int ref;            // Reference count
  struct sleeplock lock; // protects everything below here
  int valid;          // inode has been read from disk?

  short type;         // copy of disk inode
  short major;
  short minor;
  short nlink;
  uint size;
  uint addrs[NDIRECT+1];
};

ブロックデバイスは、利用できるデータ格納の単位をブロックとして区切り、それを複数利用する形でファイルは構成されています。
このブロックを複数組み合わせることで、ファイルのデータが構成されます。
inode はこのブロックを複数束ねて管理するためのデータというわけです。

以下のような形で inode で管理されるアドレスのブロックにファイルの実体データは格納されます。

バッファとブロック


ファイルのデータとブロック

inode に格納されているブロックのアドレスはブロックデバイス上、連なるものではなく、分散されたブロックに格納されます。

これは、連続した場所にアドレスを確保するような作りの場合、ファイルのサイズが削除されたときに、ファイルとファイルの間に空いた容量の上限が削除されたファイル固定されてしまい、削除されたファイル以上のサイズのものを保存することができなくなってしまいます。つまり、ストレージの容量が十分に活かせません。

一方で、ブロック単位でストレージを区切り、連続してないブロックに自由に分割してファイルを書ける場合、ちょっとした隙間も活かすことができ、ストレージを最大限活用することができます。

このため、inode という形でアドレスを束ねて、ストレージを無駄なく使うようにしているのです。


ようやくプロセスからファイルの実体データまで辿り着きました。これでデータが読み込めます!

ただ、ファイルのデータを読み書きするたびにストレージにアクセスしていては、アクセスが遅くなってしまいます。
プロセスからデータを読み書きする場合、メモリ上の値に書き込んだ方が圧倒的に早いですから、ファイルの実体もメモリ上に一旦持ってきて操作をする方が効率が良いです。

xv6 においてももちろんその仕組みは用意されています。
ブロックデバイスのそれぞれのブロックに応じて、buf 構造体と呼ばれるバッファが用意されます。
buf 構造体は bcache というグローバル変数で管理されます。

ファイルを読み書きをする際には実際のデータはメモリ上にあるこのバッファに対してアクセスを行うことで、アクセスを高速化することができます。
言ってしまえば、ファイルを利用する際には、本来ブロックデバイス上に存在する inode とストレージ領域を、メモリ上にキャッシュとして用意してしまうわけです。

完成

この buf 構造体は、随所でブロックデバイスへフラッシュされ、ファイルデータを永続化することになります。

ここまでで、ようやく全体の流れを把握しました。
では実際に xv6 をデバッグしながら動きを見てみましょう。

下準備について

まずは、xv6 の下準備をしていきます。

xv6 は、gdb というデバッガを利用することで、デバッグすることができますので、こちらを利用します。

xv6 のリポジトリは↓となります。

参考
xv6-public
xv6-public

こちらのリポジトリをクローンしてビルドをして、gdb にてデバッグをして動作を確認します。
xv6 のビルド・起動 〜 gdb によるデバッグは他にもいろいろな記事が出ているのでここでは触れません。

xv6 は非常にシンプルな構成となっているため、利用できるコマンドも非常に限られています。
今回確認したい動作は、ファイルの open ・ close ですので、それを確認できるようのコマンドを事前に用意をしておきます。

xv6 にて、独自のユーザープログラムを実行する方法はこちらのブログより確認させていただきました。

参考
Adding a User Program to xv6
Adding a User Program to xv6

今回用意するプログラムは、以下の通りです。

このプログラムは単純に myfile1.txt というファイルを開いて(最初に開く場合は作成)、1ブロック分書き込みを行い、ファイルを閉じるだけのシンプルなプログラムです。

このファイルを 1blockfile.c として保存しています。

#include "types.h"
#include "stat.h"
#include "user.h"
#include "fcntl.h"

int main() {
    const char *filename = "myfile1.txt";
    int fd;

    // ファイルを開く(存在しない場合は作成)
    fd = open(filename, O_CREATE | O_WRONLY);
    if (fd < 0) {
        printf(2, "open %s failed\n");
        exit();
    }

    printf(1, "File '%s' opened successfully, fd = %d\n");

    for(int i=0; i < 512; i++){
        write(fd, "a", 1);
    }

    // ファイルを閉じる
    if (close(fd) < 0) {
        printf(2, "close %s failed\n");
        exit();
    }

    printf(1, "File '%s' closed successfully\n");
    exit();
}


まずは、qemu を使用して、デバッグが可能な状態で xv6 を起動します。

$ make qemu-nox-gdb
*** Now run 'gdb'.
qemu-system-i386 -nographic -drive file=fs.img,index=1,media=disk,format=raw -drive file=xv6.img,index=0,media=disk,format=raw -smp 2 -m 512  -S -gdb tcp::26000

別ターミナルで gdb の準備をします。


// ターミナルを別で起動し、gdb 起動 
$ gdb kernel
GNU gdb (Ubuntu 9.2-0ubuntu1~20.04.1) 9.2
(中略)

// kernel をシンボルファイルとして読み込む
(gdb) symbol-file kernel
Load new symbol table from "kernel"? (y or n) y
Reading symbols from kernel...

// xv6 に接続
(gdb) target remote localhost:26000
Remote debugging using localhost:26000
0x0000fff0 in ?? ()

// xv6 の処理をスタート
(gdb) c
Continuing.

xv6 側で操作ができる状態になっているかと思いますので、
sys_opensys_close にブレークポイントをセットします。(sys_open sys_close が xv6 において、ユーザープログラムからシステムコールの open・close を呼んだ時にカーネル側で呼び出される関数です)

// ctrl + c で処理を中断
^C
Thread 1 received signal SIGINT, Interrupt.
0x801038e5 in mycpu () at proc.c:45
45	  apicid = lapicid();

// ブレークポイントを設置して、処理を継続
(gdb) b sys_open
Breakpoint 1 at 0x801051c0: file sysfile.c, line 287.
(gdb) b sys_close
Breakpoint 2 at 0x80104df0: file sysfile.c, line 95.
(gdb) c
Continuing.

コマンドを実行すると、gdb のターミナルで sys_open をキャッチするので、la src コマンドにてソースを確認できます。

ここまでで準備は完了です。実際に、変数を確認してきましょう!

実行前の状態の確認

ではここから実際にファイルの open ・ close の動作を確認していきます。
今回は、proc, file, inode を確認します。バッファについては、出力量が多いので今回は見ていきません。

proc 構造体は、グローバル変数である ptable.proc のなかで管理されています。
※ gdb でデバッグ中、p コマンドで対象の変数の中身を確認することができます
※少し読みづらいので整形してます

現在 init プロセスと sh プロセスの2つだけが存在しているのがわかりますね

(gdb) p ptable.proc
$2 = {
{sz = 12288, pgdir = 0x8dfbb000, kstack = 0x8dfff000 "", 
    state = SLEEPING, pid = 1, parent = 0x0, tf = 0x8dffffb4, 
    context = 0x8dfffefc, chan = 0x80112d54 <ptable+52>, killed = 0, ofile = {
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 
      0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>, 
    name = "init\000\000de\000\000\000\000\000\000\000"},
 {sz = 16384, 
    pgdir = 0x8df73000, kstack = 0x8dffe000 "", state = SLEEPING, pid = 2, 
    parent = 0x80112d54 <ptable+52>, tf = 0x8dffefb4, context = 0x8dffee6c, 
    chan = 0x8010ffa0 <input+128>, killed = 0, ofile = {
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 
      0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>, 
    name = "sh", '\000' <repeats 13 times>}, 
{sz = 0, pgdir = 0x0, 
    kstack = 0x0, state = UNUSED, pid = 0, parent = 0x0, tf = 0x0, 
    context = 0x0, chan = 0x0, killed = 0, ofile = {0x0 <repeats 16 times>}, 
    cwd = 0x0, name = '\000' <repeats 15 times>} <repeats 62 times>}

file 構造体を管理している ftable.file もみてみます。
FD_INODE が利用されているファイルになるので、現在一つのファイルが使われている状態で、もう一つ利用済みで解放された後のファイルが存在しています。

(gdb) p ftable.file
$1 = {
{type = FD_INODE, ref = 6, readable = 1 '\001', writable = 1 '\001', 
    pipe = 0x0, ip = 0x80110aa4 <icache+196>, off = 20}, 
{type = FD_NONE, 
    ref = 0, readable = 1 '\001', writable = 1 '\001', pipe = 0x0, 
    ip = 0x80110aa4 <icache+196>, off = 0}, 
{type = FD_NONE, ref = 0, 
    readable = 0 '\000', writable = 0 '\000', pipe = 0x0, ip = 0x0, 
    off = 0} <repeats 98 times>}

さらにメモリ上の inode 構造体を管理している icache.inode も確認してみます。
inode 構造体は現在2つ使用中のものがあり、1つ inode 番号が残ってはいるがすでに利用済みで解放された inode 構造体が残っています。

(gdb) p icache.inode
$3 = {
{dev = 1, inum = 1, ref = 2, lock = {locked = 0, lk = {locked = 0, 
        name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 2148538563, 
          2148540033, 2148540487, 2148553230, 2148551277, 2148555585, 
          2148554883, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 1, 
    type = 1, major = 0, minor = 0, nlink = 1, size = 512, addrs = {59, 
      0 <repeats 12 times>}},
 {dev = 1, inum = 21, ref = 1, lock = {
      locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, 
        pcs = {0, 2148532906, 2148536368, 2148552038, 2148551277, 2148555585, 
          2148554883, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, 
    valid = 1, type = 3, major = 1, minor = 1, nlink = 1, size = 0, addrs = {
      0 <repeats 13 times>}},
 {dev = 1, inum = 13, ref = 0, lock = {
      locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, 
        pcs = {0, 2148538563, 2148535329, 2148554152, 2148551277, 2148555585, 
          2148554883, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, 
    valid = 1, type = 2, major = 0, minor = 0, nlink = 1, size = 27852, 
    addrs = {379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 
      391}},
 {dev = 0, inum = 0, ref = 0, lock = {locked = 0, lk = {
        locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 0, 0, 
          0, 0, 0, 0, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, 
    valid = 0, type = 0, major = 0, minor = 0, nlink = 0, size = 0, addrs = {
      0 <repeats 13 times>}} <repeats 47 times>}

これで、プログラムを実行する前の現在の状態が確認できました。

2つのproc, 1つの file, 2つの inode と廃棄された 1つの file, 1つの inode

コマンド実行〜sys_open()

プログラム実行前の状態が確認できたので、xv6 を実行しているターミナルで、1blockfile コマンドを実行します。(gdb 側で c コマンドを忘れずに)

$ 1blockfile

すると、先ほどブレークポイントを設定した sys_open の部分で止まります。(la src コマンドを打っておくとわかりやすいです)

ptable.proc を確認すると、1blockfile という名前の proc がしっかり一つ追加されていますね。

(gdb) p ptable.proc
$1 = {
{sz = 12288, pgdir = 0x8dfbb000, kstack = 0x8dfff000 "", state = SLEEPING, pid = 1, parent = 0x0,
    tf = 0x8dffffb4, context = 0x8dfffefc, chan = 0x80112d54 <ptable+52>, killed = 0, ofile = {0x8010fff4 <ftable+52>,
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>,
    name = "init\000\000de\000\000\000\000\000\000\000"}, 
{sz = 16384, pgdir = 0x8df73000, kstack = 0x8dffe000 "",
    state = SLEEPING, pid = 2, parent = 0x80112d54 <ptable+52>, tf = 0x8dffefb4, context = 0x8dffeefc,
    chan = 0x80112dd0 <ptable+176>, killed = 0, ofile = {0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>,
      0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>,
    name = "sh", '\000' <repeats 13 times>}, 


{sz = 12288, pgdir = 0x8df23000, kstack = 0x8dfbe000 "", state = RUNNING,
    pid = 3, parent = 0x80112dd0 <ptable+176>, tf = 0x8dfbefb4, context = 0x8dfbef2c, chan = 0x0, killed = 0, ofile = {
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>},
    cwd = 0x80110a14 <icache+52>, name = "1blockfile\000\000\000\000\000"}, 


{sz = 0, pgdir = 0x0, kstack = 0x0,
    state = UNUSED, pid = 0, parent = 0x0, tf = 0x0, context = 0x0, chan = 0x0, killed = 0, ofile = {
      0x0 <repeats 16 times>}, cwd = 0x0, name = '\000' <repeats 15 times>} <repeats 61 times>}

次に ftable.file を確認します。
まだ 1blockfile が実行される前の状態ですので、ファイルは現時点では変化はありません。

(gdb) p ftable.file
$2 = {
{type = FD_INODE, ref = 9, readable = 1 '\001', writable = 1 '\001', pipe = 0x0, ip = 0x80110aa4 <icache+196>,
    off = 31},
 {type = FD_NONE, ref = 0, readable = 1 '\001', writable = 1 '\001', pipe = 0x0,
    ip = 0x80110aa4 <icache+196>, off = 0}, 
{type = FD_NONE, ref = 0, readable = 0 '\000', writable = 0 '\000',
    pipe = 0x0, ip = 0x0, off = 0} <repeats 98 times>}

icache.inode も先ほどと変化なしですね。

(gdb) p icache.inode
$3 = {
{dev = 1, inum = 1, ref = 3, lock = {locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0,
        pcs = {0, 2148538563, 2148540033, 2148540487, 2148534955, 2148554152, 2148551277, 2148555585, 2148554883, 0}},
      name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 1, major = 0, minor = 0, nlink = 1, size = 512, addrs = {
      59, 0 <repeats 12 times>}},
 {dev = 1, inum = 21, ref = 1, lock = {locked = 0, lk = {locked = 0,
        name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 2148536391, 2148552038, 2148551277, 2148555585,
          2148554883, 0, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 3, major = 1, minor = 1,
    nlink = 1, size = 0, addrs = {0 <repeats 13 times>}},
 {dev = 1, inum = 19, ref = 0, lock = {locked = 0, lk = {
        locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 2148538563, 2148535329, 2148554152,
          2148551277, 2148555585, 2148554883, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 2,
    major = 0, minor = 0, nlink = 1, size = 15520, addrs = {696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706,
      707, 708}}, 
{dev = 0, inum = 0, ref = 0, lock = {locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock",
        cpu = 0x0, pcs = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 0, type = 0,
    major = 0, minor = 0, nlink = 0, size = 0, addrs = {0 <repeats 13 times>}} <repeats 47 times>}

現段階では、プロセスが追加されただけの状態ですね。プログラムを先に進めてみましょう。

1blockfile のプロセスが追加される

sys_close()

continue すると sys_close の部分で止まります。

ptable については特に変化がありません。

(gdb) p ptable.proc
$1 = {
{sz = 12288, pgdir = 0x8dfbb000, kstack = 0x8dfff000 "", state = SLEEPING, pid = 1, parent = 0x0,
    tf = 0x8dffffb4, context = 0x8dfffefc, chan = 0x80112d54 <ptable+52>, killed = 0, ofile = {0x8010fff4 <ftable+52>,
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>,
    name = "init\000\000de\000\000\000\000\000\000\000"}, 
{sz = 16384, pgdir = 0x8df73000, kstack = 0x8dffe000 "",
    state = SLEEPING, pid = 2, parent = 0x80112d54 <ptable+52>, tf = 0x8dffefb4, context = 0x8dffeefc,
    chan = 0x80112dd0 <ptable+176>, killed = 0, ofile = {0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>,
      0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>}, cwd = 0x80110a14 <icache+52>,
    name = "sh", '\000' <repeats 13 times>}, 


{sz = 12288, pgdir = 0x8df23000, kstack = 0x8dfbe000 "", state = RUNNING,
    pid = 3, parent = 0x80112dd0 <ptable+176>, tf = 0x8dfbefb4, context = 0x8dfbef2c, chan = 0x0, killed = 0, ofile = {
      0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x8010fff4 <ftable+52>, 0x0 <repeats 13 times>},
    cwd = 0x80110a14 <icache+52>, name = "1blockfile\000\000\000\000\000"}, 


{sz = 0, pgdir = 0x0, kstack = 0x0,
    state = UNUSED, pid = 0, parent = 0x0, tf = 0x0, context = 0x0, chan = 0x0, killed = 0, ofile = {
      0x0 <repeats 16 times>}, cwd = 0x0, name = '\000' <repeats 15 times>} <repeats 61 times>}

ftable.file は2つ目のファイルの type FD_INODE(使用中)へと変更になりました。
file の中の off という要素に着目してみると、512 という値に変化しているのが確認できます。
これは、ファイルの読み書きの開始位置を表すもので、現在 512 byte 分書き込みを起こったので、しっかりとオフセットが更新されているわけですね。

ブロックのサイズ

xv6 におけるブロックサイズは 512 byte のため、addrs には一つだけ値が格納されています。

(gdb) p ftable.file
$5 = {
{type = FD_INODE, ref = 9, readable = 1 '\001', writable = 1 '\001', pipe = 0x0, ip = 0x80110aa4 <icache+196>,
    off = 72}, 

// 差分
{type = FD_INODE, ref = 1, readable = 0 '\000', writable = 1 '\001', pipe = 0x0,
    ip = 0x80110b34 <icache+340>, off = 512},


{type = FD_NONE, ref = 0, readable = 0 '\000', writable = 0 '\000',
    pipe = 0x0, ip = 0x0, off = 0} <repeats 98 times>}

inode 構造体では、ref という「プロセスからの参照数を表す要素」が0であった(=使用されていないデータ) inum=19 のエントリが、inum=22 になっています。
size を見ると、512 となっており、addrs にも値が追加されています。つまり、今回新しくファイルが作成されて inode が割り当てられ、使用されていない inode 構造体にキャッシュされたことがわかりますね。

(gdb) p icache.inode
$6 = {
{dev = 1, inum = 1, ref = 3, lock = {locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0,
        pcs = {0, 2148538563, 2148551427, 2148553356, 2148551277, 2148555585, 2148554883, 0, 0, 0}},
      name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 1, major = 0, minor = 0, nlink = 1, size = 512, addrs = {
      59, 0 <repeats 12 times>}},
 {dev = 1, inum = 21, ref = 1, lock = {locked = 0, lk = {locked = 0,
        name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 2148536561, 2148552150, 2148551277, 2148555585,
          2148554883, 0, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 3, major = 1, minor = 1,
    nlink = 1, size = 0, addrs = {0 <repeats 13 times>}}, 

// 差分
{dev = 1, inum = 22, ref = 1, lock = {locked = 0, lk = {
        locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 2148536561, 2148552150, 2148551277,
          2148555585, 2148554883, 0, 0, 0, 0}}, name = 0x801071f2 "inode", pid = 0}, valid = 1, type = 2, major = 0,
    minor = 0, nlink = 1, size = 512, addrs = {760, 0 <repeats 12 times>}}, 


{dev = 0, inum = 0, ref = 0, lock = {
      locked = 0, lk = {locked = 0, name = 0x80107744 "sleep lock", cpu = 0x0, pcs = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
      name = 0x801071f2 "inode", pid = 0}, valid = 0, type = 0, major = 0, minor = 0, nlink = 0, size = 0, addrs = {
      0 <repeats 13 times>}} <repeats 47 times>}

これでファイルの oepn・close の際の一連の動きが確認できました。
前半戦で確認していった通り、file 構造体や inode 構造体によってファイル操作が実現されていますね。

file と inode が新しく増える

まとめ

お疲れ様でした!

ほんのさわりの部分ですが、どのようにファイルシステムが実装されているのか確認する手助けとなればと思います。
興味が湧けば、ぜひご自身でも xv6 のコードリーディングやデバッグを行ってみてください。

AUTHOR
tak
tak
記事URLをコピーしました