450 likes | 710 Views
■ext2 ファイルシステム構造と操作プリミティブ - ファイルシステムのレイアウト. BSD のファイルシステムおよびその論文を大いに参考にして設計されており、殆ど同様の構成と なっている。ファイルシステムを構成する各種データは、関連の深いもの同士をなるべく物理的に 近くに配置できるように配慮されている。 詳しくは、 BSD UNIX の解説書および、論文を参照。. boot sector. super block. BLOCK GROUP 1. block group descriptors. BLOCK GROUP 2. data-block bitmaps.
E N D
■ext2ファイルシステム構造と操作プリミティブ - ファイルシステムのレイアウト■ext2ファイルシステム構造と操作プリミティブ - ファイルシステムのレイアウト BSDのファイルシステムおよびその論文を大いに参考にして設計されており、殆ど同様の構成と なっている。ファイルシステムを構成する各種データは、関連の深いもの同士をなるべく物理的に 近くに配置できるように配慮されている。 詳しくは、BSD UNIXの解説書および、論文を参照。 boot sector super block BLOCK GROUP 1 block group descriptors BLOCK GROUP 2 data-block bitmaps BLOCK GROUP 2 inode bitmaps BLOCK GROUP N-1 inode-table BLOCK GROUP N Data-blocks
一つのファイルシステムは物理的に複数の領域(ブロックグループ) に分割して管理されている。 これは関係の深いデータを同じブロックグループ内から確保すれば、ディスクヘッドのシーク時間の 無駄が少なくなるだろうという発想である。 スーパブロックはファイルシステムの全体の構造を管理している。 主な情報は、次に述べるブロックグループの配置と、このファイルシステム内に存在するフリーiノード 総数とフリーブロック総数の管理である。 各ブロックグループは、ブロックグループディスクリプタ (このブロックグループの管理情報領域)、 iノード領域、データブロック領域、 iノード領域の閉塞管理用ビットマップ、データブロック領域の 閉塞管理用ビットマップから構成される。 スーパブロック/ブロックグループディスクリプタは全てのブロックグループの先頭に配置されているが、 最初のブロックグループ内に配置されているもののみを利用している。 (二つ目以降のブロックグループ内のスーパブロックは、fsck時に一つ目のスーパブロックが 読み出せない時に、利用することができる。 umount時に何もディスクに書き戻していないので、レイアウト情報のみを利用しているようだ)
■ext2ファイルシステム構造と操作プリミティブ - ファイル構造■ext2ファイルシステム構造と操作プリミティブ - ファイル構造 フラグメントを入れようとした痕跡はあるが、実装はされていない。非常に伝統的な構造をしている。 「Design Of UNIX」の世界そのままである。詳しくはそちらを参照のこと。 ファイルを形作るディスク上のiノードとデータブロックの関係は、下図に示す構造になっている。 これはレギュラーファイルでもディレクトリファイルでもシンボリックリンクファイルでも全て同じである。 ext2のディスク上のiノードのi_block[]には12個の直接ブロックを登録できるようになっている。 12個のブロック以上の大きさのファイルの場合は、i_block[12]に間接ブロックを登録し、 13個目のデータブロックはこの間接ブロックに登録する。 更にファイルが大きくなった時は、 i_block[13]に間接ブロックを管理する間接ブロック (二段間接ブロック) を登録する。 同様にi_block[14]には三段間接ブロックが登録される。ファイルサイズはi_sizeに記録されている。 i_block[12]に間接ブロックを登録されているからと言って、 i_block[0]~i_block[11]の全てに 直接ブロックが登録されているとはかぎらない。
ext2 disk inode i_node i_uid i_size i_blocks i_block [ ] direct blocks indirect double indirect triple indirect ディスク上のiノードが参照するときは、 メモリ上のiノードに情報をコピーした後利用する。 システム共通の情報はメモリiノードの共通部分へ、 ファイルシステム固有の部分は メモリiノードの union部分へコピーする。
■ext2ファイルシステム構造と操作プリミティブ - ディレクトリ構造■ext2ファイルシステム構造と操作プリミティブ - ディレクトリ構造 ext2fsのディレクトリファイルの構造は、BSD UNIXの形式を周到している。 一つ一つのディレクトリエントリの大きさは可変長である。ただし実装上の都合により、1ディスクブロック を越える大きさのディレクトリエントリは作成不可能である。 最新のext2fsディレクトリエントリは、BSDより一つだけ余計に情報を格納している。 ディレクトリエントリ情報としてファイル属性を持っている。従来ファイル属性はinodeを見なければ 分からなかったが、 ext2fsではディレクトリを参照しただけで、わかるような作りになっている。 (ただし、まだ積極的に利用してはいない。 fsckでのみの利用か?) ino rec _len name _len file _type name [ ] ino rec _len name _len file _type name [ ] disk block disk block
各ファイルシステムの論理構造は、ディスク上に物理的に展開されている。 rootファイルシステムの inodeとディレクトリの関係は以下のようになっている。 これも「Design Of UNIX」の世界そのままである。 root inode ino=2 Directory data block 2 . 2 .. 3 lost+found 26 vmlinuz 32 usr inode inode ino=32 Directory ino=26 Regular data block data block Linux Kernel object 32 . inode 2 .. ino=17 Regular 17 bin data block 53 src 17 . 18 spool 32 .. inode inode 42 yacc ino=18 Regular ino=68 Regular 68 gdb data block data block “/var/spool”
■ext2ファイルシステム構造と操作プリミティブ - スーパブロックとブロックグループ操作■ext2ファイルシステム構造と操作プリミティブ - スーパブロックとブロックグループ操作 メモリ上のスーパブロックと、ディスク上のext2ファイルシステムのデータ構造の関係を示す。 ディスク上のデータはバッファキャッシュ上に読み出され、メモリ上のスーパブロックとリンクされ管理される。 ディスク上のデータのうちスーパブロック域とブロックグループディスクリプタ域は、ファイルシステムの マウント時に読みだされ、アンマウントするときまで解放されない。 (更新されたものはときどきディスクへ書き戻される) データブロック域のビットマップ、iノード域のビットマップは必要に応じてメモリ上に読み込まれる。 メモリ上のスーパブロックにキャッシュできるのは8つまでとなっており、使用頻度の少ないものから キャッシュから追い出される。しかし、メモリ上のスーパブロックのキャッシュ域から追い出されても、 ビットマップ自体アクセス頻度が高いため、バッファキャッシュ域から落ちてしまうことは殆どなさそうである。
super_block super block ext2part block group descriptors s_sbh s_group_desc s_block_bitmap data-block bitmap inode bitmap inodetable s_inode_bitmap data-blocks ............ ..... super block block group descriptors ............ ..... data-block bitmap inode bitmap ............ ..... inodetable data-blocks ............ .....
■ext2ファイルシステム構造と操作プリミティブ - スーパブロックとブロックグループ操作■ext2ファイルシステム構造と操作プリミティブ - スーパブロックとブロックグループ操作 主な処理関数 ext2_get_group_desc()関数 メモリ上に読み込んだブロックグループディスクリプタテーブルの中から、 目的とするブロックグループのエントリを見つける load_block_bitmap()関数 ブロックのビットマップを得る。もしキャッシュ上に見つからない時は read_block_bitmap 関数を用いて、ディスク上から読み込みを行う。 load_inode_bitmap()関数 iノードのビットマップを得る。もしキャッシュ上に見つからない時は read_inode_bitmap 関数を用いて、ディスク上から読み込みを行う。
■ext2ファイルシステム構造と操作プリミティブ - iノード確保アルゴリズム■ext2ファイルシステム構造と操作プリミティブ - iノード確保アルゴリズム ext2fs上に新しいファイルを生成する場合、まずext2_new_inode関数にて新しいinodeを確保する。 この関数はディスクブロック上のinodeの確保と、そのiノードに対応するメモリiノードの読み込みを行う。 ディスク上のiノード確保にあたっては、以下のポリシーを用いる。 ディレクトリ用iノードの確保ポリシー 1.平均以上のinodeがあるブロックグループの中で、最も空きブロックが残っているもの 通常ファイル用のiノード確保ポリシー 1.親ディレクトリのinodeブロックと同じブロックグループ内に確保 2.同じグループに取れないときは、quadratic hash検索 (隣、その二つ隣、その四つ隣、その八つ隣....) 3.それでも駄目なら、全リニアサーチ
■ext2ファイルシステム構造と操作プリミティブ - iノード確保アルゴリズム■ext2ファイルシステム構造と操作プリミティブ - iノード確保アルゴリズム アルゴリズム詳細 以下にその大雑把なアルゴリズムを示す。 アルゴリズム中に付いている◆印は同期書き込みを、◇印は遅延書き込みを表す ext2_new_inode(親ディレクトリのiノード, 生成するファイルタイプ) メモリiノードを確保(get_empty_inode関数) if(生成するファイルがディレクトリ?) { 1ブロックグループあたりの平均フリーinode数を計算 for(全てのブロックグループ) { ブロックグループディスクリプタ読み込み(ext2_get_group_desc関数) 平均以上のinodeがあり、最も空きブロックの多いブロックグループを求める。 } } else { 親ディレクトリが属するブロックグループディスクリプタ読み込み(ext2_get_group_desc関数) if(同じブロックグループ内にフリーinodeがある) { このブロックグループを候補とする。 } else { for(ブロックグループをquadratic hash検索) { ブロックグループディスクリプタ読み込み(ext2_get_group_desc関数) 空きiノードがあるブロックグループを候補とする } } if(それでも候補のブロックグループが見つからない) { for(全てのブロックグループ) { ブロックグループディスクリプタ読み込み(ext2_get_group_desc関数) 空きiノードがあるブロックグループを候補とする } } }
候補のブロックグループのiノードビットマップを読み込む(load_inode_bitmap関数)候補のブロックグループのiノードビットマップを読み込む(load_inode_bitmap関数) iノードビットマップ内のフリーなビットを見つけ、予約する。 ◇iノードビットマップの遅延書き込み要求(mark_buffer_dirty) if(SYNCマウント?) { ◆iノードビットマップをディスクに書き込み(ll_rw_block関数、wait_on_buffer関数) } ブロックグループディスクリプタが管理するフリーiノード数を1減らす ◇ブロックグループディスクリプタの属するバッファを遅延書き込み要求(mark_buffer_dirty) スーパブロックが管理するフリーiノード数を1減らす ◇スーパブロックを遅延書き込み要求(mark_buffer_dirty) メモリiノード初期化し、iノードハッシュに登録 ◇メモリiノードの遅延書き込み要求(mark_inode_dirty) return メモリiノード 問題点など 上記ディスクiノード確保アルゴリズムの問題点は、 syncモードのmountであっても新規作成した iノードの 同期書き込みが行われていない点にある。 iノードの書き込み前に、このiノードが ディレクトリに 登録された場合、一時的にディスク上のデータ構造に矛盾が生じる。 このタイミングでシステムがクラッシュするとDUPブロックが 発生する可能性がある。 メモリ上のスーパブロック内にブロックグループのビットマップ (iノード用、ブロック用)はそれぞれ 8つまで登録できるようになっている。 大きなファイルシステムになると、そこに全ては 乗り切らなくなり LRUで追い出されることになる。 inodeビットマップ域などのアクセスが遅くなる可能性もあるが、 アクセス頻度が高いため、 案外バッファキャッシュから落ちない ような気がする。
逆にファイル削除に伴うiノードの解放は、ext2_free_inode関数によって行われる。逆にファイル削除に伴うiノードの解放は、ext2_free_inode関数によって行われる。 単純に解放対象となったiノードに対応するiノードビットマップのビットをクリアするのみである。 メモリiノード自体の解放は、呼び出し側(vfs)で行っている。 ext2_free_inode(メモリiノード) iノードビットマップを読み込む(load_inode_bitmap関数) ビットマップ上のiノードに対応するビットをクリア ブロックグループディスクリプタ読み込み(ext2_get_group_desc関数) ブロックグループディスクリプタが管理するフリーiノード数を1増やす ◇ブロックグループディスクリプタの属するバッファを遅延書き込み要求(mark_buffer_dirty) スーパブロックが管理するフリーiノード数を1増やす ◇スーパブロックを遅延書き込み要求(mark_buffer_dirty) ◇iノードビットマップを遅延書き込み要求(mark_buffer_dirty) if(SYNCマウント?) { ◆iノードビットマップをディスクに書き込む(ll_rw_block関数、wait_on_buffer関数) } 問題点など SYNCモードのマウントをした場合、 iノードの解放時にiノードビットマップを同期書き込みしているが 実はこの処理は必須ではない。 もし同期書き込みをせずマシンが クラッシュしてもこのビットが示すiノードが誰からも参照できなく なる だけであり、ファイルシステム破壊を引き起こすことはない。 性能の面から考えても、ここは非同期書き込みで十分だと思われる。
■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム ブロック確保ポリシー ファイルを構成するデータブロックをなるべく物理的に近くに配置するため、 「ブロック確保関数 ext2_alloc_block」を呼び出す処理では、どの付近にブロックを確保したいかを ヒント値として渡さなければならない。 プリアロケート機能なしでコンパイルされている場合、この関数では何もせず、 即 ext2_new_block関数を呼び出す。 与えられたヒント値を元に、以下のポリシーを用いてディスクブロックを確保する。 1.ヒント値で与えられたヒント番目のブロックそのもの 2.ヒント値から32ブロック以内のフリーブロックを検索 3.同じブロックグループ内のフリーブロックを検索 4.全てのブロックグループ内のフリーブロックを検索
■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム アルゴリズム詳細 以下にその大雑把なアルゴリズムを示す。アルゴリズム中に付いている◆印は同期書き込みを、 ◇印は遅延書き込みを表す。 v2.2とことなりこの処理中で確保したブロックに対応するバッファのクリアを行わないようになった。 ext2_new_block(iノード, ヒント値, ....) (上記アルゴリズムを用いて、空きブロックを検索) 候補のブロックグループのブロックビットマップを読み込む(load_block_bitmap関数) ブロックビットマップ内のビットを予約する。 プリアロケートの処理(後述のプリアロケート機能に関する説明参照) ◇ブロックビットマップの遅延書き込み要求(mark_buffer_dirty関数) if(SYNCマウント?) { ◆ブロックビットマップをディスクに書き込む(ll_rw_block関数、wait_on_buffer関数) } ブロックグループディスクリプタが管理するフリーブロック数を1減らす ◇ブロックグループディスクリプタの属するバッファを遅延書き込み要求(mark_buffer_dirty) スーパブロックが管理するフリーブロック数を1減らす ◇スーパブロックを遅延書き込み要求(mark_buffer_dirty)
データブロックの解放は連続したブロックをまとめて行う。 ext2_free_blocks(iノード, ブロック番号, ブロック数) ブロックが属するブロックビットマップを読み込む(load_block_bitmap関数) ブロックビットマップ内のビットを解放する ブロックグループディスクリプタが管理するフリーブロック数を解放要求分増やす ◇ブロックグループディスクリプタの属するバッファを遅延書き込み要求(mark_buffer_dirty) スーパブロックが管理するフリーブロック数を1増やす ◇スーパブロックを遅延書き込み要求(mark_buffer_dirty) if(SYNCマウント?) { ◆ブロックビットマップをディスクに書き込む(ll_rw_block関数、wait_on_buffer関数) }
■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム■ext2ファイルシステム構造と操作プリミティブ - ディスクブロック確保アルゴリズム プリアロケート機能 これは、ファイル拡張時に将来の再拡張を見越して連続するブロックを予約してしまう機能である。 次回のファイル拡張の高速化と、ファイルブロックが並ぶことによる、ファイルアクセスの高速化を 期待できる。 ext2_alloc_block(iノード, ヒント値) ・既にプリアロケートしたものがあればそれを利用する。 なければext2_new_block()で、 フリービットマップから確保し、 iノードに登録する。(メモリ上のみ) プリアロケートしたもの があってもヒント値付近のものでなければ、一度ext2_discard_preallocで一度捨ててしまう ・先行拡張は通常ファイルのみが対象 ・プリアロケート機能のコンパイルフラグが立っていない時は、 ここでは何もせず 即ext2_new_block関数を呼び出す ext2_new_block(iノード, ヒント値) ・プリアロケートされたブロックが無くなった時のみ、 ext2_alloc_block関数から呼び出される ・ヒント値を元にあるブロックを確保した後、 それに続くブロック列を先行確保する。 先行確保の最大値はスーパブロックに指定された数分(mkfs時に指定?) である。 未指定のときは8ブロックになる。 ext2_discard_prealloc(iノード) ・ファイルの参照が0になったとき(close等)に呼び出される。 ・プリアロケートしておいたブロックをフリービットマップに戻す。 (ext2_free_blocks関数を呼び出す)
ext2_new_block()にて先行確保されたブロックは下図のようにメモリiノードの中のリザーブ域にのみext2_new_block()にて先行確保されたブロックは下図のようにメモリiノードの中のリザーブ域にのみ 登録される。次に ext2_alloc_block()が呼び出された時は、このリザーブ域からブロックを取り出し、 iノードに直接ブロックまたは間接ブロックとして登録する。先行確保されたブロックはメモリ上にのみ 存在する。 free block bitmap memory inode data block prealloc ext2 disk inode 問題点など SYNCモードのマウントをした場合、 ブロックの解放時にブロックビットマップを同期書き込みしているが 実はこの処理は必須ではない。もし同期書き込みをせずマシンが クラッシュしてもこのビットが示す ブロックが誰からも参照できなく なるだけであり、ファイルシステム破壊を引き起こすことはない。 性能の面から考えても、ここは非同期書き込みで十分だと思われる。 ブロックのプリアロケート機能(8ブロック先行確保)があるが、 システムがクラッシュしてもプリアロケート したブロックが 失われる方向に倒れるため、信頼性には問題がない。 プリアロケートしたブロックは フリービットマップ上から削除され、 ファイルに組み込まれるまでの間はメモリ上にしか存在しないため、 DUPブロックとなる危険性はない。大きなファイルの生成と読み出しにはそこそこの効果が期待できそう。
■ext2ファイルシステム構造と操作プリミティブ■ext2ファイルシステム構造と操作プリミティブ - ファイルオフセットからブロック番号への変換 ext2ファイルシステムでは、ext2_getblk関数がファイルオフセットからブロックへの変換処理と データブロック割り当て処理を同時に行っているが、ページI/Oなどでは、vfsから直接ページと ブロック間の I/Oが起動される。 このとき必要となる純粋にファイルオフセットに対応するブロック番号を求める関数 (ext2_bmap関数、ext2_get_block関数) も用意している。 (処理本体は、ext2_get_block関数。ext2_bmapを呼び出すとファイル拡張無しでext2_get_block関数が 呼び出される) あるファイルのファイル内オフセットに対応するブロックのバッファを獲得する関数が ext2_getblk関数である。 この関数は、指定されたファイル内オフセットに対応するブロックが割り当てられていない時、 自動的にブロック割当を行う。 つまりext2_get_block関数をファイル拡張ありで呼び出す。 (自動確保するかどうかは、引き数により指定可能)
ext2_get_block(iノード、オフセット、返却値設定用バッファヘッド、createフラグ)ext2_get_block(iノード、オフセット、返却値設定用バッファヘッド、createフラグ) if(ファイル拡張指定でない場合) { ファイルオフセットをブロック番号に変換(ext2\_block\_map関数) 得られたブロック番号を指定されたバッファヘッドに設定 return バッファヘッド } if(ファイルオフセットが直接ブロックの範囲) { データブロックのバッファ = inode_getblk(iノード, iノード内のブロック登録エントリ番号, ...); } else if(ファイルオフセットが一段間接ブロックの範囲) { 間接ブロックのバッファ = inode_getblk(iノード, iノード内の間接ブロックの登録エントリ番号, ...); データブロックのバッファ = block_getblk(iノード, 間接ブロックのバッファ, 間接ブロック内のデータブロック登録エントリ番号, ...); } else if(ファイルオフセットが二段間接ブロックの範囲) { 一段目の間接ブロックのバッファ = inode_getblk(iノード, iノード内の一段目の間接ブロックの登録エントリ番号, ...); 二段目の間接ブロックのバッファ = block_getblk(iノード, 一段目の間接ブロックのバッファ, 一段目の間接ブロック内の二段目の間接ブロック登録エントリ番号, ...); データブロックのバッファ = block_getblk(iノード, 二段目の間接ブロックのバッファ, 二段目の間接ブロック内のデータブロック登録エントリ番号, ...); } else { /* 三段間接 */ 一段目の間接ブロックのバッファ = inode_getblk(iノード, iノード内の一段目の間接ブロックの登録エントリ番号); 二段目の間接ブロックのバッファ = block_getblk(iノード, 一段目の間接ブロックのバッファ, 一段目の間接ブロック内の二段目の間接ブロック登録エントリ番号); 三段目の間接ブロックのバッファ = block_getblk(iノード, 二段目の間接ブロックのバッファ, 二段目の間接ブロック内の三段目の間接ブロック登録エントリ番号); データブロックのバッファ = block_getblk(iノード, 三段目の間接ブロックのバッファ, 三段目の間接ブロック内のデータブロック登録エントリ番号); } バッファにブロックを登録 return データブロックのバッファ;
下記は、ファイル拡張が指定されていない場合に呼び出される、純粋なファイルオフセットから下記は、ファイル拡張が指定されていない場合に呼び出される、純粋なファイルオフセットから ブロック番号への変換ルーチンである。 ext2_block_bmap(iノード、オフセット) if(ファイルオフセットが直接ブロックの範囲) データブロックのブロック番号 = inode_bmap(iノード, iノード内のブロック登録エントリ番号); return データブロックのブロック番号; if(ファイルオフセットが一段間接ブロックの範囲) { 間接ブロックのブロック番号 = inode_bmap(iノード, iノード内の間接ブロックの登録エントリ番号); データブロックのブロック番号 = block_bmap(iノード, 間接ブロックのブロック番号, 間接ブロック内のデータブロック登録エントリ番号); return データブロックのブロック番号; } if(ファイルオフセットが二段間接ブロックの範囲) { 一段目の間接ブロックのブロック番号 = inode_bmap(iノード, iノード内の一段目の間接ブロックの登録エントリ番号); 二段目の間接ブロックのブロック番号 = block_bmap(iノード, 一段目間接ブロックのブロック番号, 一段目の間接ブロック内の二段目間接ブロック登録エントリ番号); データブロックのブロック番号 = block_bmap(iノード, 二段目の間接ブロックのブロック番号, 二段目の間接ブロック内のデータブロック登録エントリ番号); return データブロックのブロック番号; }
一段目の間接ブロックのブロック番号 = inode_bmap(iノード, iノード内の一段目の間接ブロックの登録エントリ番号); 二段目の間接ブロックのブロック番号 = block_bmap(iノード, 一段目間接ブロックのブロック番号, 一段目の間接ブロック内の二段目間接ブロック登録エントリ番号); 三段目の間接ブロックのブロック番号 = block_bmap(iノード, 二段目間接ブロックのブロック番号, 二段目の間接ブロック内の三段目間接ブロック登録エントリ番号); データブロックのブロック番号 = block_bmap(iノード, 三段目の間接ブロックのブロック番号, 三段目の間接ブロック内のデータブロック登録エントリ番号); return データブロックのブロック番号; inode_bmap関数は、単純に指定されたメモリiノードの指定されたエントリに登録されている ブロック番号を読み出すだけ。 block_bmap関数は、単純に指定されたメモリ上の間接ブロックの指定されたエントリに登録されている ブロック番号を読み出すだけ。
inode_getblk関数は、直接iノードに継っているブロック (直接データブロック、および一段目の間接ブロック)に対応する バッファを獲得する関数である。もし指定されたブロックがiノードに登録されていなければ、フリーブロックを見つけて来て iノードに登録する。アルゴリズム中に付いている◆印は同期書き込みを、◇印は遅延書き込みを表す。 inode_getblk(iノード、iノード内オフセット) if(iノード内オフセットで指定された箇所にブロックが登録されている) { if(間接ブロックアクセスなら) { そのブロックに対応したバッファを確保(getblk関数) return バッファ } else { return ブロック番号 } } 空きブロックを確保(ext2_alloc_block関数) if(間接ブロックアクセスなら) { 確保したブロックに対応するバッファを確保(getblk関数) バッファがI/O中なら完了を待ち合わせる。 バッファをクリア(memset関数) バッファを有効にする(mark_buffer_update関数) ◇バッファの遅延書き込み要求をだす(mark_buffer_dirty関数) } else { 確保したブロックを返却値に登録 } iノードにこの確保したブロックを登録 iノードの登録ブロック数情報更新(i_blocks) if(SYNC属性 または、同期モードopen?) { ◆iノードをディスクに書き込む(ext2_sync_inode関数) } else { ◇iノードの遅延書き込み要求を出す(mark_inode_dirty関数) } return 確保したブロックのバッファ;
block_getblk関数は、間接ブロックに継っているブロック (データブロック、または多段の間接ブロック)に対応する バッファを獲得する関数である。もし指定されたブロックが間接ブロックに登録されていなければ、フリーブロックを 見つけて来てiノードに登録する。 block_getblk(iノード, 間接ブロックのバッファ, 間接ブロック内オフセット) if(間接ブロック内オフセットで指定された箇所にブロックが登録されている) { if(間接ブロックアクセスなら) { そのブロックに対応したバッファを確保(getblk関数) return バッファ } else { return ブロック番号 } } 空きブロックを確保(ext2_alloc_block関数) if(間接ブロックアクセスなら) { 確保したブロックに対応するバッファを確保(getblk関数) バッファがI/O中なら完了を待ち合わせる。 バッファをクリア(memset関数) バッファを有効にする(mark_buffer_update関数) ◇バッファの遅延書き込み要求をだす(mark_buffer_dirty関数) } else { 確保したブロックを返却値に登録 } 間接ブロックに確保したブロックを登録 ◇間接ブロックの遅延書き込み要求をだす(mark_buffer_dirty関数) if(SYNC属性 または、同期モードopen?) { ◆間接ブロックをディスクに書き込む(ll_rw_block関数,wait_on_buffer関数) } iノードの登録ブロック数情報更新(i_blocks) ◇iノードの遅延書き込み要求を出す(mark_inode_dirty関数) 間接ブロックのバッファの解放(brelse関数) return 確保したブロックのバッファ
■ext2ファイルシステム構造と操作プリミティブ-ファイルオフセットからブロック番号への変換■ext2ファイルシステム構造と操作プリミティブ-ファイルオフセットからブロック番号への変換 ext2_getblk関数アルゴリズム 以下にその大雑把なアルゴリズムを示す。アルゴリズム中に付いている ◆印は同期書き込みを、◇印は遅延書き込みを表す。 ext2_getblk(iノード、オフセット, ...) ファイルオフセットをブロック番号に変換(ext2_get_block関数) 空のバッファを確保(getblk関数) if (新規割り当てれたブロックであった場合) { バッファをクリア(memset関数) バッファを有効にする(mark_buffer_update関数) ◇バッファの遅延書き込み要求をだす(mark_buffer_dirty関数) } バッファを返却
下図は、ちょうど二段間接ブロックが作成される時の、データの更新順を表したものである。下図は、ちょうど二段間接ブロックが作成される時の、データの更新順を表したものである。 ◆の後ろに書かれた番号が更新順を表している。 ext2 disk inode i_size i_blocks direct block 6 indirect double indirect 4 triple indirect 2 3 5 1 free block bitmap
問題点など 1.SYNCモードのマウント時でも、iノードと間接ブロック(一段~三段)の 更新順序が逆。 iノードに近い方から更新されているが、 その更新途中でシステムクラッシュすると、 不定の値を持つ 間接ブロックが登録されてしまうことがある。 この間接ブロックの値によっては致命的なファイルシステム 破壊を引き起こす可能性がある。 2.SYNCモードのマウント時、ファイル拡張が連続すると、 その度ブロック確保とi ノード(間接ブロック)への登録のため、 毎回iノード(間接ブロック)の同期書き込みが 発生するため、 I/Oネックとなることが予想される。
■ext2ファイルシステム構造と操作プリミティブ-ファイルオフセットからブロック番号への変換■ext2ファイルシステム構造と操作プリミティブ-ファイルオフセットからブロック番号への変換 ext2_bread関数アルゴリズム ext2_bread関数は、ブロックの確保、ブロックに対応したバッファの確保、ブロック内容のバッファへの 読み込みまでを行う。 create指定がある場合、ファイルに対応するブロックがない場合、ファイル拡張 を行う。 ext2_bread(iノード、オフセット、createフラグ) オフセットに対応するブロックのバッファを獲得(ext2_getblk関数) if(ディレクトリの拡張があった && ディレクトリプリアロケーションモード) { さらに一つブロックを先行拡張する(ext2_getblk関数) スーパブロックに指定された数分だけブロックを先行拡張する(ext2_getblk関数) } if(バッファの内容が有効) return バッファ バッファへブロックデータを読み込む(ll_rw_block関数、wait_on_buffer関数) return バッファ 問題点など この処理は、ディレクトリ拡張時に複数ブロックまとめて確保して しまうことを意味している。 必要がないときは ディレクトリプリアロケーション機能が動かないようにして おかねばならない。 (現時点においてはmkfsではデフォルトでこの機能 をOFFに初期化しており, 指定する方法もないようだ)
■ext2ファイルシステム構造と操作 - iノードとデータブロックの結合の解除■ext2ファイルシステム構造と操作 - iノードとデータブロックの結合の解除 ファイルのトランケート処理は以下の場合に呼び出される。 明示的なtruncateシステムコール呼び出しでは、iノード内の i_sizeで示されるところまで解放する。 ・明示的なtruncateシステムコール呼び出し ・トランケートモードのopen時 ・ファイルの削除時 注意 2000.6.16現時点では、open/truncateシステムコールの延長では truncate処理は呼び出されない。 VFSレベルではinodeサイズのみ変更しており、実際のファイル切り詰め処理を行う下記関数は 呼び出されない。 ユーザ見え的にはファイルは小さくなったように見えるので問題無いが、フリーブロックが増えない という小さい問題は残る。 再度ファイル拡張が発生したとき処理が高速になるというメリットはある。
ext2_truncate(iノード) 先行拡張したブロックの解放(ext2_discard_prealloc関数) while(ブロックが残っている間) { 直接ブロックの解放(trunc_direct関数) 間接ブロックの解放(trunc_indirect関数) 二段間接ブロックの解放(trunc_dindirect関数) 三段間接ブロックの解放(trunc_tindirect関数) if(全ての解放が完了したら) break if(SYNC属性 かつ iノードがDIRTY) { ◆iノードのディスクへの書き込み(ext2_sync_inode関数) } スケジューラを呼び出し少し待つ(schedule関数) } iノード更新時間更新 ◇iノードの遅延書き込み要求(mark_inode_dirty関数)
trunc_direct(iノード) iノードのファイルサイズから解放する直接ブロックの先頭エントリを求める while(iノードに解放する直接ブロックある間) { if(その直接ブロックエントリが空?) continue; iノードのそのブロックが登録されていたエントリをクリア iノードに登録されているブロック数を減らす(メンバi_blocks) ◇iノードの遅延書き込み要求(mark_inode_dirty関数) 連続分したブロックをまとめて解放(ext2_free_blocks関数) } 連続分したブロックをまとめて解放(ext2_free_blocks関数) return 全て解放できたら0、失敗したら1
trunc_indirect(iノード, 間接ブロックが登録されているエントリ番号, 間接ブロックが登録されているエントリ) if(間接ブロックがない) return 0; 間接ブロックを読み出す(bread関数) while(間接ブロックに解放するブロックがある間) { if(解放しようとしているブロックのバッファが使用中) continue; 間接ブロックのそのブロックが登録されていたエントリをクリア iノードに登録されているブロック数を減らす(メンバi_blocks) ◇iノードの遅延書き込み要求(mark_inode_dirty関数) ◇間接ブロックの遅延書き込み要求(mark_buffer_dirty関数) 連続分したブロックをまとめて解放(ext2_free_blocks関数) } 連続分したブロックをまとめて解放(ext2_free_blocks関数) 間接ブロック自体の解放(check_block_empty関数) return 全て解放できたら0、失敗したら1
trunc_dindirect(iノード, 二段間接ブロックが登録されているエントリ番号, 二段間接ブロックが登録 されているエントリ) if(二段間接ブロックがない) return 0; 二段間接ブロックを読み出す(bread関数) while(二段間接ブロックに解放する間接ブロックがある間) { 間接ブロックに登録されているデータブロックの解放(trunc_indirect関数) } 二段間接ブロック自体の解放(check_block_empty関数) return 全て解放できたら0、失敗したら1 trunc_tindirect(iノード) if(三段間接ブロックがない) return 0; 三段間接ブロックを読み出す(bread関数) while(三段間接ブロックに解放する二段間接ブロックがある間) { 二間接ブロックに登録されている間接ブロックと データブロックの解放(trunc_dindirect関数) } 三段間接ブロック自体の解放(check_block_empty関数) return 全て解放できたら0、失敗したら1
check_block_empty(inode, bh, ind_bh) if(間接ブロックのバッファがI/O中なら) I/O完了を待ち合わせる if (間接ブロックに何か残っているなら) { if(SYNC属性 かつ 間接ブロックのバッファがDIRTY) { ◆間接ブロックの書き込み(ll_rw_block関数, wait_on_buffer関数) } 間接ブロックのバッファの解放(brelse関数) return 0; } if (他から、この間接ブロックのバッファが参照されている) return 1; if (間接ブロックが空なら) { iノード(または別の間接ブロック)から、この間接ブロックの登録を消す iノードに登録されているブロック数を減らす(メンバi_blocks) ◇iノードの遅延書き込み要求(mark_inode_dirty関数) 間接ブロックのバッファを無効にする(bforget関数) 間接ブロック自体を解放(ext2_free_blocks関数) }
下図は、ファイルトランケート処理の順番を図に表した物である。 ◆は同期書き込み、◇は遅延書き込みを表している。 ◆◇の後ろの数字は、その処理の順番を示している。 free block bitmap 2 ext2 disk inode 1 i_size i_blocks 4 4 3 direct block 5 3 indirect double indirect triple indirect 6 10 8 7 9 11 free block bitmap
問題点など SYNCモードでマウントしたときの問題点 1.iノード/間接ブロックの更新より、フリービットマップへの登録が 先に動作する。 フリービットマップの方が先に更新されると フリービットマップとファイル構造の両方に登録された 状態になり、 DUPブロックが発生する可能性がある。 さらに、iノード/間接ブロックの更新前に、 解放されたブロックがフリービットマップから別のファイル構造に 組み込まれてしまう可能性もある。 2.iノードと間接ブロックの書き込み順が逆である。 間接ブロックのほうが先に解放されるため、iノードが 解放済みの間接ブロックを参照している タイミングが存在する。 これもシステムクラッシュ時のDUPブロックの原因となる。 3.ファイル削除時は、既に全てのディレクトリからの参照は あり得ないが、解放するiノードや間接 ブロックの 同期書き込みが動作することがある。(単純に性能上の問題) 4.これ自体、ファイルシステムの構造に影響を与えるものでは無いが、 ファイルの先頭に近い方の ブロックから解放しているのは 気になる。 5.ファイルトランケート時に、ファイル生成時間まで 設定し直しているが、 POSIX的にはこれは正しいのだろうか?
■ext2ファイルシステム構造と操作 - ディレクトリ操作■ext2ファイルシステム構造と操作 - ディレクトリ操作 ディレクトリから、ファイル名で指定されるファイルのエントリを見つけ出す。 アクセス高速化のため、ディレクトリブロック用のバッファを先行して読みだしている。 (現在は8ブロック先行読みだし) ext2_find_entry(ディレクトリのiノード, ファイル名, ... ) ディレクトリの先頭ブロックから8つ分(NAMEI_RA_SIZE)のバッファを確保(ext2_getblk関数) for(ディレクトリブロックがある間) { if(確保したバッファにまだディレクトリデータが読み込まれていないなら) まとめて読み込む(ll_rw_block関数) 次に検索するディレクトリブロックの入ったバッファのI/O完了を待つ(wait_on_buffer関数) while(ディレクトリブロック中にディレクトリエントリがある間) { if(ディレクトリエントリとファイル名が一致) { 先行確保したバッファを解放(brelse関数) return 見つかったディレクトリエントリ } } 検索が終わったバッファを解放(brelse関数) 次の読み込みに利用する代わりのバッファを確保(ext2_getblk関数) } バッファを全て解放(brelse関数) return NULL(発見できず)
ext2_add_entry関数は、ディレクトリにファイル名で指定されるファイルのエントリを追加する。ext2_add_entry関数は、ディレクトリにファイル名で指定されるファイルのエントリを追加する。 ext2_find_entry関数と異なりディレクトリブロックの先行読み込み処理は行わず、1ブロックずつ ゆっくりと読み込み処理を行っている。 ext2_add_entry(ディレクトリのiノード, ファイル名, ... ) 先頭のディレクトリブロックを読み込む(ext2_bread関数) while(1){ if(このブロック中のディレクトリエントリを全て検索しつくした) { ディレクトリブロックを読み込んだバッファを解放(brelse) 次のディレクトリブロックを読み込む(ext2_bread関数) if (ディレクトリが拡張された) { 拡張されたブロックをディレクトリブロックとして初期化 iノード中のディレクトリサイズ情報を変更 ◇ディレクトリのiノードの遅延書き込み要求(mark_inode_dirty関数) } } if (既に存在しているエントリと同じ名前なら) return EEXISTエラー if (ディレクトリブロック内にファイル名を登録可能なスペースが見つかった){ ディレックブロックを読み込んだバッファ内に、目的のファイルを登録する。 (空エントリならそこに登録する) (大きなエントリなら、二つのエントリに分割し、そこに登録する) (ディレクトリエントリに、登録するiノード番号を書き込む) ◇ディレクトリiノードの遅延書き込み要求(mark_inode_dirty関数) ◇ディレクトリブロックの遅延書き込み要求(mark_buffer_dirty関数) if(SYNC属性) { ◆ディレクトリブロックの書き込み(ll_rw_block関数, wait_on_buffer関数) } } } ディレクトリのデータブロックを読み込んだバッファを解放(brelse)
ディレクトリブロックの先頭のディレクトリエントリ以外は、空のディレクトリエントリは置かず、削除されたディレクトリブロックの先頭のディレクトリエントリ以外は、空のディレクトリエントリは置かず、削除された ディレクトリエントリは一つ前のディレクトリエントリにマージしてしまう。次にディレクトリエントリを追加する 場合は、これら大きな空きスペースを持ったディレクトリエントリを探し出し、二つに再分割し利用する。 下図は、ファイル"baa"をディレクトリに登録/削除する時の動きを示している。 4 “foo” rec _len name _len file _type rec _len name _len file _type ino name [ ] ino ext2_add_entry() ext2_delete_entry() 4 “foo” 18 “baa” rec _len name _len file _type rec _len name _len file _type rec _len name _len file _type name [ ] ino name [ ] ino ino
ext2_delete_entry(ディレクトリエントリ, ディレクトリエントリが含まれるディレクトリブロックのバッファ) ディレクトリブロックのバッファの中から、目的のエントリを削除する (途中のエントリの削除は、一つ前のエントリと結合することにより実現) (先頭のエントリの削除は、iノード番号フィールドに0を入れることで実現) ◇ディレクトリブロックの遅延書き込み要求(mark_buffer_dirty関数) if(SYNC属性) { ◆ディレクトリブロックの書き込み(ll_rw_block関数, wait_on_buffer関数) } 問題点、注意点 1.ディレクトリ拡張が発生した場合、ディレクトリブロックを初期化し ディスクに書き込むタイミングと、 iノードのファイルサイズを ディスクに書き込みタイミングは、上位モジュールの動作によって は、 ディレクトリブロックより先にiノードが書き込まれてしまう。 これは、このタイミングでシステムクラッシュすると不正な ディレクトリブロックが残る可能性がある。 (注)上位モジュールの 作りによる。 2.ディレクトリ拡張が発生した場合、ディレクトリブロックの割当は ext2_bread関数の延長で行われる が、 ディレクトリのiノードそのものの更新は遅延書き込みとなっている。 つまり、このタイミングで システムがクラッシュすると ファイルサイズ情報が更新されていないため、ディレクトリに 新規登録 したファイルを参照できなくなってしまう。 3.ディレクトリからのファイル名検索、ディレクトリへのファイル登録は、 毎回ディレクトリ先頭からの 線形探索となる為、大きなディレクトリは アクセス速度の面で非常に不利である。 補足 ext2_add_entry関数は、エントリ名の重なりチェックは完全には行わない。呼び出し側で考慮しなければならない。
■ext2ファイルシステム構造と操作 - その他、主なプリミティブ関数群■ext2ファイルシステム構造と操作 - その他、主なプリミティブ関数群 ext2_read_inode(iノード) ディスクiノードを読みだし、メモリiノードに値を設定する ext2_write_inode(iノード) ext2_update_inode(iノード, 0) を見よ ext2_sync_inode(iノード) ext2_update_inode(iノード, 1) を見よ ext2_update_inode(iノード, フラグ); メモリiノードを書き戻す。フラグが1の時は即ディスクまで 書き戻すが、フラグが0の時は バッファキャッシュまで 書き戻すのみ。 ext2_delete_inode(iノード) iノードに登録されているデータブロックを解放した後、 iノードを解放(ext2_free_inode関数)する。