UNIX 6th code reading - unix/sys2.c

はじめに

今回はunix/sys2.cを見ていきます。

ファイルシステム関連のシステムコールがそろっています。

システムコールの説明はユーザマニュアルにも載っているので、合わせて目を通すことをお勧めします。

read( ), write( )

read( )(5711行目)はreadシステムコールのハンドラで、write( )(5720行目)はwriteシステムコールのハンドラです。その名の通りファイルの読み書き処理を行うシステムコールです。

readとwriteはデータの流れる方向が「ユーザ空間←ディスク」「ユーザ空間→ディスク」と逆なだけで、処理自体はほとんど共通です。

共通処理はrdwr( )という関数が行っています。read( )とwrite( )はそれぞれfile構造体のフラグであるFREAD, FWRITEを引数にしてrdwr( )を呼んでいるだけです。

read, writeシステムコールに対する引数は、ファイルディスクリプタ(u.u_ofile[ ]のindex)、読み書きするデータのアドレス、読み書きするバイト数の三つです。ファイルディスクリプタはr0で渡します。読み書きを始めるファイル中のオフセットはfile構造体が持っているので引数で渡す必要はありません。

rdwr( )

rdwr( )(5731行目)はreadシステムコールハンドラとwriteシステムコールハンドラの共通処理を行います。

適切なu.u_base, u.u_count, u.u_offset, u.u_segflgを設定してreadi( ), writei( )を呼びます。

システムコールの返り値として、userのr0には実際に読み書きが行われたbytes数が格納されます。ユーザプログラムはこの値を見ることで正しく読み書き処理が完了したかどうかを確認できます。

open( ), creat( )

open( )(5765行目)はファイルを開くopenシステムコールのハンドラで、creat( )(5781行目)はファイルを新規に作成するcreatシステムコールのハンドラです。

ファイルを開くのも新規に作成するのも、処理の内容がほとんど同じなので共通処理はopen1( )に任せています。

openシステムコールに対する引数は、ファイルのパス名、ファイルのモードです。ファイルのモードは読み、書き、読み書き両方、のいずれかを表します。モードに対応したパーミッションがファイル(inode)に与えられていない場合、エラーとなります。

creatシステムコールに対する引数は、ファイルのパス名、inodeのモードです。inodeのモードはinodeのi_modeに設定されます。ただし010000以上はクリアされてしまうので、例えばIFDIR(このinodeはディレクトリ)は設定できません(5687-5698行目参照)。ただし、パス名に対応するファイル・ディレクトリが既に存在していた場合、i_modeは上書きされません。該当ファイル・ディレクトリのサイズを0にするだけです。

open( )はnamei( )を使用し、パス名に対応したinodeの取得を試みます。取得に失敗すると何もせずreturnします。

creat( )もnamei( )を使用し、パス名に対応したinodeをの取得を試みます。inodeが取得できなかった場合は、そのファイルを新規に作成しようとしているという意味なので、makdnode( )を使って新たにinodeを生成します。このとき、inodeのi_modeはユーザから指定されたものに設定されます。namei( )でinodeが取得できた場合は、そのファイルが既に存在しているという意味なので、この後の処理でそのinodeを使用します。open( )と異なるのは、ファイルの中身がopen1( )の中で初期化されるという点です。

open1( )

open1( )(5804行目)はopenシステムコールハンドラとcreatシステムコールハンドラの共通処理を行います。

既存のファイルを開こうとしている場合(open時、もしくは、creatで既にファイルが存在していた場合)は、access( )を使ってアクセス権限があるかチェックします。

新規にファイルを作成しようとしている場合(creat時。既存のファイルの有無は気にしない)は、itrunc( )を使ってinodeと使用しているデータblock Noを初期化し、ファイルサイズを0にします。

その後、falloc( )を実行してu.u_ofile[ ]の空きエントリとfile[ ]の空きエントリを新規に割当てます。そして新規に割当てたfile[ ]のエントリから、namei( )で取得したinodeを指すようにします。

falloc( )の中で実行されるufalloc( )により、新規に割当てられたu.u_ofile[ ]のエントリのindexがuserのr0に格納されています。これがopen, creatシステムコールの返り値となります。

つまり

  fp = fopen( 'hoge.txt', 'r' ) ;

などと実行したとき、fpにはu.u_ofile[ ]のindex(0以上の整数値)が入ります。

このファイルディスクリプタを使用して、ファイルの読み書きやcloseを行います。カーネルはファイルディスクリプタから該当のu.u_ofile[ ]を見て、お目当てのfile構造体を取得することができるのです。

close( )

close( )(5846行目)はcloseシステムコールのハンドラです。openシステムコールやcreatシステムコールで開いたファイルを閉じるためのシステムコールです。

引数にファイルディスクリプタ(u.u_ofile[ ]のindex)が渡され、該当のエントリをNULLにします。

その後にclosef( )を呼び、u.u_ofile[ ]の該当エントリが指していたfile[ ]のエントリの参照カウンタをデクリメントします。

seek( )

seek( )(5861行目)はseekシステムコールのハンドラです。開いているファイルの読み書きオフセット(=file構造体のf_offset)を変更します。

当時のseekシステムコールは、block単位(=512bytes)でオフセットの増減を指定することが可能でした。

参考:ユーザマニュアルより引用

The file descriptor refers to a file open for reading or writing. The read (resp. write) pointer for the file is
set as follows:
if ptrname is 0, the pointer is set to offset.
if ptrname is 1, the pointer is set to its current location plus offset.
if ptrname is 2, the pointer is set to the size of the file plus offset.
if ptrname is 3, 4 or 5, the meaning is as above for 0, 1 and 2 except that the offset is multiplied by 512.
If ptrname is 0 or 3, offset is unsigned, otherwise it is signed.

現在の一般的なOSでは、block単位の指定はできないようです。

ファイルディスクリプタからu.u_ofile[ ]が指しているfile[ ]のエントリを取得し、file構造体のf_offsetの値を変更します。

link( )

link( )(5909行目)はlinkシステムコールのハンドラです。所謂ハードリンクを実現します。当時はシンボリックリンクは未だ存在しません。

linkによって、一つのファイル(inode)に対して、複数の名前をつけることができます。例えば、/var/hogeというファイルに対し/var/homuというリンクを張ると、そのファイルは/var/hoge, /var/homuのどちらのパス名でもアクセスすることができるようになります。

ハードリンクの場合は、新旧どちらの名前も同等の扱いをされ、例えば上記でいう/var/hogeを削除したとしても、/var/homuからまだ見られているのでファイル(inode+data)は削除されません。被リンク数(パス空間の中で、いくつの名前を持っているか)はinodeのi_nlinkで管理され、これが0にならない限りファイルは削除されません。

例:/var/hogeから/var/homuに対しハードリンクを張った時の/varディレクトリの状態

i_number filename
0x10 hoge
0x10 homu

一方シンボリックリンクの場合は、上記で言う/var/hogeを消してしまうとファイル自体が削除され、/var/homuでファイルにアクセスしようとするとファイルがないと言われてしまいます。シンボリックリンクは主従関係があり、従の方はあくまで主のファイルを指しているだけと言えるでしょう。

ただしハードリンクには問題があります。デバイスファイルシステムをまたがってリンクを張ることができません(inode numberが一意にならないから?ディレクトリのエントリはinode numberとファイル名しか保持しておらず、inode numberだけではどのデバイスのどのinodeを示すか判別できない?)。また、ディレクトリに対してリンクを張ることもできません。パス空間の中にループができてしまい、ファイル名の一貫性が崩れてしまうからです。これらの問題に対処するために、後にシンボリックリンクが生まれたようです。

(しかし、ソースを見るとsuper userならばディレクトリに対してリンクを張ることができるようです。最近は、OSによっては問題があるとしてsuper userでもディレクトリにリンクを張れないように仕様を変更しているようです)

linkシステムコールに対する引数は、被リンクファイルのパス名、新しい名前、です。

被リンクのパス名に該当するファイルが存在しなかったり、逆に、新しい名前に該当するファイルが既に存在しているとエラーとなります。また、被リンク数には上限があり127を超えるとエラーとなります。

unlink( )

unlink( )(3510行目)はunlinkシステムコールのハンドラです。unlink( )はunix/sys2.cではなくunix/sys4.cに記述されていますがlink( )と対比させながら読むと理解しやすいのでここで扱います。

unlinkシステムコールはファイルの削除を行います。ディレクトリはsuper userでないと削除することが出来ません。

(一般ユーザはどうやってディレクトリを削除する?v6のlibc相当のrmdir.sを見たところ、普通にunlinkシステムコールを呼んでいるように見える。当時はシステムとしてsuper userでないとディレクトリを削除できない仕様になっていた??)

ファイルの削除は、ディレクトリ中の該当エントリのinode numberを0にすることで実現されます。

あるディレクトリの"hoge.txt"というファイルを削除する場合

削除前

i_number file name
0x10 mado.txt
0x16 homu.txt
0x20 hoge.txt
0x35 fuga.dat

削除後

i_number file name
0x10 mado.txt
0x16 homu.txt
0x00 hoge.txt
0x35 fuga.dat


また、inodeのi_nlinkをデクリメントします。i_nlinkが0になるときそのinodeは解放され、使用していたデータのblock Noも解放されます。

ただし、データの内容自体はクリアされずにディスクにそのまま残るということに注意してください。

つまり、ファイルを削除するということは、データの内容自体に辿る術をなくすことである、と言えるかもしれません。

こちらのエントリで、実験で実際にそのようになることを確かめています。

mknod( )

mknod( )(5962行目)はmknodシステムコールのハンドラです。

ディレクトリや特別なファイルを作成します。最近のOSではディレクトリ作成用のmkdirというシステムコールを別に持っているようです。

このシステムコールはsuper userのみが実行できます。一般ユーザが実行すると何もせずエラーとなります。

(unlinkと同じ話で、一般ユーザはどうやってディレクトリを作成する?)

mknodシステムコールの引数は、作成するファイル・ディレクトリのパス名、モード、アドレスです。モードはinodeのi_modeに設定されます。このパラメータに「ディレクトリである」というフラグを立てることで、ディレクトリを作成することができます。creatシステムコールと異なり、上位bitはクリアされません。これがシステムの中で唯一のディレクトリ作成方法です。

sslep( )

sslep( )(5979行目)はsleepシステムコールのハンドラです。

一定時間プログラムの実行を止めます。

このハンドラは、カーネルの中で使用されるsleep( )とは異なるので注意してください。

sslep( )(sleepシステムコール)は秒数を引数に取り、指定された秒数だけプログラムの実行を止めます。

一方、sleep( )はカーネル中で資源待ちの時などに呼ばれる関数で、実行中のプロセスを寝させます(止めます)。プロセスを起こす(再起動させる)には、別のプロセスがwakeup( )を呼ぶ必要があります。

sslep( )はtoutグローバル変数に再起動する時刻を格納し、sleep( )を呼んで寝ます。これを起こすのはclock( )です。システムのクロックと同期し、定期的にclock( )が呼ばれ、その中でtoutと現在時刻を比較し一致していたらwakeup( )で起こします。

clock( )について書いたエントリを見直すことをお勧めします。

コードメモ

read( )
  • 5711 : FREADを引数にrdwr( )を呼んでいるだけ
write( )
  • 5722 : FWRITEを引数にrdwr( )を呼んでいるだけ
rdwr( )
  • arguments
    • mode : 読み出しか書き込みか
  • 5736-5738 : getf( )を使用して、ファイルディスクリプタに対応したfile構造体を得る。ファイルディスクリプタはユーザからシステムコールの引数で渡される。file構造体の取得に失敗したらreturn
  • 5739-5742 : file構造体のフラグと引数で渡された読み書きのモードを比較し、権限があるかチェックする。file構造体のフラグはファイルをオープンするときに設定される
  • 5743-5745 : readi( ), writei( )を呼ぶ前にu.u_base, u.u_count, u.u_segflgを設定しておく
  • 5746-5749 : 読み書きしようとしているファイルがパイプだった場合の処理。パイプ関係の処理を読み解くときに論理を確認する予定。今は置いておく
  • 5750-5756 : 読み書きしようとしているファイルが通常ファイルだった場合の処理。u.u_offsetにfile構造体のオフセットをセットしておく。これで必要なパラメータの設定が完了したので、後は引数modeに応じてreadi( )かwritei( )のどちらかを呼ぶだけ。readi( ), writei( )実行後、dpadd( )を呼んでfile構造体のf_offsetに実際に読み書きしたbyte数をプラスする。普通の+演算子ではなくdpadd( )を使っているのは、f_offsetは16bits以上のデータのため
  • 5758 : ユーザへの返り値としてuserのr0に読み書きを行ったbyte数を格納する
open( )
  • 5770-5772 : namei( )を使ってパス名に対応したinodeを取得する。パス名はシステムコールの引数としてユーザから渡される。inodeの取得に失敗した場合何もせずにreturn
  • 5773 : このインクリメントはopenシステムコールの引数で渡すmodeの仕様と、カーネル内で使用されているfile構造体のmodeの値の差分を吸収している
参考:マニュアルよりopenシステムコールで使用されるmode引数の説明を抜粋

Open opens the file name for reading (if mode is 0), writing (if mode is 1) or f
or both reading and writing
(if mode is 2).

参考:カーネル中で使用されている、file構造体のmodeを抜粋

#define FREAD 01
#define FWRITE 02
#define FPIPE 04
  • 5774 : open1( )を呼んでopen( )とcreat( )の共通処理を実行
creat( )
  • 5786 : namei( )を呼んで、パス名に対応したinodeを取得する。パス名はシステムコールの引数としてユーザから渡される
  • 5787 : namei( )でinodeを取得できなかったら。新たにinodeを生成することを試みる
    • 5788-5789 : namei( )でエラーがあったのでinodeを取得できなかった場合は何もせずreturn
    • 5790-5792 : maknode( )を実行してinodeを新たに取得する。maknode( )の引数にユーザから渡されたmodeの引数を使用する。ただし、上位bit(010000以上)はクリアするので、例えばディレクトリ(IFDIR=040000)などは作成できない。また、スティッキービットもクリアされる。maknode( )でinodeが取得できなかったらエラーでreturn
    • 5793 : 取得したinodeを引数にopen1( )を呼ぶ
  • 5794 : namei( )でinodeを取得できたら、このinodeを利用する
    • 5795 : open1( )を呼んでopen( )とcreat( )の共通処理を実行する
open1( )
  • arguments
    • ip : inode pointer
    • mode : ファイルを読もうとしているのか、書こうとしているのか
    • trf : 以下のいずれかを示す
      • 0 : 既存のファイルを開こうとしている
      • 1 : 新規にファイルを作成しようとしている。ただし該当のファイルは既に存在している
      • 2 : 新規にファイルを作成しようとしている。該当ファイルは存在していない
  • 5813 : ファイルが既に存在していたら。アクセス権限をチェックする
    • 5814-5815 : ファイルを読もうとしていた場合、access( )を使ってinodeに読み出しパーミッションがついているかチェックする
    • 5816-5820 : ファイルを書こうとしている場合、access( )を使ってinodeに書き込みパーミッションがついているかチェックする。ただしディレクトリには書き込みを許さない。ディレクトリだった場合はu.u_errorにEISDIR(ディレクトリに書こうとしている)をセット。このu.u_errorは5822行目で拾われる
  • 5822-5823 : u.u_errorをチェックし、値が入っていたらoutに飛んでエラー処理。u.u_errorはaccess( )内でアクセス権限がないときに値を取りうる。また前述のとおりディレクトリに書き込もうとしているときにも値を取りうる。この部分は5813行目から始まるif文の中に入れてもいいような……?
  • 5824-5825 : trfが1か2なら(つまりcreatシステムコール実行中ならば)itrunc( )を呼んで、inodeが保持しているデータblock Noを全てフリーリストに戻す。本来trfが2の場合はこの処理は不要。maknod( )で新たに取得したinode(=解放されてフリーリストにいるinode)はデータblock Noが全て解放されていることが保証されているため
  • 5826 : prele( )を呼んでinodeのロックを解放。このロックはどこでかけられる?
  • 5827-5828 : falloc( )を呼んで、u.u_ofile[ ]のエントリとfile[ ]のエントリを新たに割り当て。失敗したらoutに飛んでエラー処理
  • 5829-5830 : falloc( )で取得したfile[ ]のエントリであるfile構造体の初期設定
  • 5831 : falloc( )内で実行されるufalloc( )で、userのr0にはu.u_ofile[ ]の割り当てられたエントリのindexが格納されている。これをローカル変数iに格納しておく。このiは、後の処理でエラーが起きたとき、割り当てられたu.u_ofile[ ]のエントリを解放するために使用される(5835行目)
  • 5832 : openi( )を実行。通常ファイルの場合は何もしない
  • 5833-5834 : openi( )内でエラーがなかった場合、return. 処理の成功
  • 5835-5839 : openi( )内でエラーがあった場合、割り当てられたu.u_ofile[ ]のエントリを解放し、割り当てられたfile構造体の参照カウンタをデクリメントし、iput( )を呼んでinodeの参照カウンタをデクリメントする
close( )
  • 5850-5852 : getf( )を呼んで、ユーザから渡されたファイルディスクリプタに該当するfile構造体をu.u_ofile[ ]から取得する。失敗したらエラー
  • 5853 : ファイルディスクリプタで示されたu.u_ofile[ ]のエントリを解放
  • 5854 : closef( )を呼んで、file構造体の参照カウンタをデクリメント
seek( )
  • 5866-5868 : getf( )を使って、ユーザから渡されたファイルディスクリプタに該当するfile構造体をu.u_ofile[ ]から取得する。失敗したらエラー
  • 5869-5872 : パイプに対してはseekできない
  • 5874-5878 : ユーザから与えられた引数modeが3以上ならば、ユーザから与えられたオフセット増減の値をblock単位(512bytes)に変換。modeが3のときはオフセット増減値はunsignedなので、数値の部分だけ抽出する
  • 5879 : modeが2以下の場合は、オフセット増減値はbyte単位。nに格納しておく。ただし、modeが1, 2の場合、値はsignedなので負の値の場合はnの上位bitであるn[0]を-1にして、n全体で負値になるようにする
  • 5885 : ここからmodeによって分岐。(0, 3), (1, 4), (2, 5)がセット。file構造体のオフセットに代入するnの値を決定する
    • 5887-5891 : ファイルポインタが現在示しているオフセットを基準に値を増減する
    • 5893-5896 : ファイルサイズ(ファイルの後端)を基準に値を増減する
    • 5897-5899 : ファイルの先頭を基準に値を増減する。よってswitch文の中では何もしない
  • 5901-5902 : nの値をfile構造体のf_offsetに代入する
link( )
  • 5914-5916 : namei( )を使って、ユーザから渡されたパス名に対応するinodeを取得する。失敗したらエラー
  • 5917-5920 : link数には制限(127まで)があるので、それ以上増える場合にはEMLINK(リンク数がover)エラー
  • 5921-5922 : ディレクトリに対しては、super userしかリンクを張れない。super userでなければエラー
  • 5926 : 先ほど取得したinodeのロックを解放。inodeはnamei( )の中でロックされている?
  • 5927-5932 : ユーザから渡された二つ目のパス名に対応したinodeの取得を試みる。新たにこの名前で一つ目のパス名に対応したinodeを指すようにしたいので、inodeが取得できたらEEXIST(ファイルが既に存在している)エラー。iput( )で取得できてしまったinodeを解放する
  • 5933-5934 : u.u_errorに値が入っていればエラー処理。namei( )の中か、inodeを取得できてしまった場合に値が入りうる
  • 5935-5939 : 異なるデバイス間ではリンクを張れない。もし張ろうとしていた場合にはエラー処理。u.u_pdirは5928行目で実行されるnamei( )の中で設定される。二つ目のパス名が指すファイルの親ディレクトリが格納される
  • 5940 : 一つ目のパス名に対応したinodeを、二つ目のパス名が指すファイルの親ディレクトリにエントリを追加する。ただし、ファイル名は二つ目のパス名に対応した名前がつけられる。その名前は5929行目で実行されるnamei( )の中でu.u_dentに格納されている
  • 5941-5942 : 一つ目のパス名に対応したinodeの被リンク数(i_nlink)をインクリメントし、inodeの更新フラグを立てる
  • 5945 : iput( )を呼んで、inodeの参照カウンタをデクリメント。どこで参照カウンタがインクリメントされる?namei( )の中のiget( )?
unlink( )
  • 3515-3517 : namei( )を使って、ユーザから渡されたパス名に対応したファイルの親ディレクトリのinodeを取得する。inodeが取得できなかったらエラー
  • 3518 : prele( )を呼んで、inodeのロックを解除する。namei( )でロックされる?
  • 3519-3521 : iget( )を使って、パス名に対応したファイルを取得。失敗したらpanic
  • 3522-3523 : ディレクトリを削除する場合、カレントユーザはsuper userでなければならない。super userでなかったらエラー
  • 3524-3528 : u.u_offset, u.u_base, u.u_count, u.u_dent.u_inoに適切な値を入れてwritei( )を呼び、ディレクトリの該当エントリのinode numberを0にクリア
  • 3529-3530 : inodeの被リンク数をデクリメントし、更新フラグを立てる
  • 3533-3534 : iput( )を使って、親ディレクトリ、該当ファイルinodeを解放
mknod( )
  • 5957 : カレントユーザがsuper userならば
    • 5958 : namei( )を使って、ユーザから渡されたパス名に対応するinodeを取得。新規にファイル・ディレクトリを作成しようとしているので、既にパス名に対応したinodeがあったらエラー
  • 5964-5965 : super userでなかったら(suser( )内でu.u_errorに値が入る)、もしくは、namei( )でエラーがあったらエラー処理
  • 5966-5968 : maknode( )を使って新たにinodeを取得。inodeのi_modeは、ユーザから引数で渡されたものになる。maknode( )が失敗したらエラー
  • 5969 : i_addr[0]にユーザから引数で渡された値を格納。ディレクトリを作成する場合は0が格納される
  • 5972 : iput( )を呼んでinodeの参照カウンタをデクリメント
sslep( )
  • 5983 : spl7( )を呼んでプロセッサ優先度を上げる(なぜ?)
  • 5984-5986 : d変数に、現在時刻を表すtime変数の値にユーザから渡された秒数を足したものを格納する
  • 5988-5995 : toutに値を設定して、時間が来るまで寝る。時間が来たらclock( )が起こす。clockは定期的にtoutをチェックして、時間になったかどうかを判断している。while文で囲まれているのは、時間が来る前に起こされてしまったときのための対処?どういうときに起こりうる?
  • 5996 : 時間が来たらプロセッサ優先度を元に戻してプロセスの再起動

終わりに

ファイルシステム周りのシステムコールを見てきました。前回までの内容を理解できていれば、今回の内容を理解するのはそんなに難しくないと思います。

しかし、システムコール実行からディスクへのアクセスまでの流れが見えづらい気がしています。全体像を整理したエントリを別途書くかもしれません。

次回はunix/sys3.cを見ていく予定です。