Linux カーネル - ブロック デバイス ドライバーについての深い理解

ブロックデバイスの処理

ブロック デバイス ドライバーのすべての操作には多数のカーネル コンポーネントが関係しており、最も重要なもののいくつかを図 14-1 に示します。たとえば、プロセスがディスク ファイルに対して read() システム コールを発行すると仮定します。書き込みリクエストは基本的に同じ方法で処理されることがわかります。以下は、カーネルがプロセス要求に応答する一般的な手順です。
ここに画像の説明を挿入します

  1. read() システムコールのサービスルーチンは、適切な VFS 関数を呼び出し、ファイル記述子とファイル内のオフセットを渡します。仮想ファイル システムはブロック デバイス処理アーキテクチャの上に位置し、Linux でサポートされるすべてのファイル システムで使用される共通のファイル モデルを提供します。VFS レイヤーについては第 12 章で詳しく紹介しました。
  2. VFS 機能は、要求されたデータがすでに存在するかどうかを判断し、必要に応じて読み取り操作の実行方法を決定します。カーネルはブロック デバイスに対して最近読み書きされたデータのほとんどを RAM に保持しているため、ディスク上のデータにアクセスする必要がない場合があります。第 15 章ではディスク キャッシュ メカニズムを紹介し、第 16 章では VFS がディスク操作を処理し、ディスク キャッシュおよびファイル システムと対話する方法について詳しく説明します。
  3. カーネルがブロック デバイスからデータを読み取ると仮定すると、カーネルはデータの物理的な場所を決定する必要があります。これを行うために、カーネルはマッピング層に依存し、主に次の 2 つのステップを実行します:
    a. カーネルは、ファイルが配置されているファイル システムのブロック サイズを決定し、それに基づいて要求されたデータの長さを計算します。ファイルのブロックサイズ。基本的に、ファイルはいくつかのブロックに分割されていると見なされるため、カーネルは要求されたデータが配置されているブロック番号 (ファイルの先頭への相対インデックス) を決定します。
    b. 次に、マッピング層はファイル システム固有の関数を呼び出し、ファイルのディスク ノードにアクセスし、論理ブロック番号に基づいてディスク上の要求されたデータの場所を決定します。実際、ディスクは多くのブロックに分割されているともみなされるため、カーネルは要求されたデータを保持するブロックの番号 (ディスクまたはパーティションの先頭への相対インデックス) を決定する必要があります。ファイルはディスク上の不連続なブロックに保存される場合があるため、ディスク インデックス ノードに保存されるデータ構造は、各ファイルのブロック番号を論理ブロック番号にマップします (注 1)。第 16 章ではマッピング層の機能を説明し、第 18 章では代表的なディスク ファイル システムをいくつか紹介します。
  4. これで、カーネルはブロック デバイスに読み取りリクエストを発行できるようになります。カーネルは、ジェネリック ブロック層 (ジェネリック ブロック インヤー) を使用して I/O 操作を開始し、要求されたデータを転送します。一般に、各 I/O 操作は、ディスク上の連続したブロックのセットのみを対象とします。要求されたデータは隣接するブロック内にある必要はないため、共通ブロック層はいくつかの I/O 操作を開始する可能性があります。各 I/O 操作は、「ブロック I/O」(略して「バイオ」) 構造によって記述され、要求を満たすために基礎となるコンポーネントが必要とするすべての情報を収集します。共通ブロック層は、すべてのブロック デバイスの抽象的なビューを提供するため、ハードウェア ブロック デバイス間の違いが隠されます。ほとんどすべてのブロック デバイスはディスクであるため、ユニバーサル ブロック層は、「ディスク」または「ディスク パーティション」を記述するためのいくつかの共通データ構造も提供します。共通ブロック層とバイオデータ構造については、この章の「共通ブロック層」セクションで説明します。
  5. 一般ブロック層の下にある「I/O スケジューラ」は、事前定義されたカーネル ポリシーに従って保留中の I/O データ転送要求を分類します。スケジューラの役割は、物理メディア上の隣接するデータ要求をグループ化することです。スケジューラについては、この章の後半の「I/O スケジューラ」セクションで説明します。
  6. 最後に、ブロック デバイス ドライバーは適切なコマンドをディスク コントローラーのハードウェア インターフェイスに送信して、実際のデータ転送を実行します。ユニバーサル ブロック デバイス ドライバーの全体的な構成構造については、後の「ブロック デバイス ドライバー」セクションで紹介します。ご覧のとおり、ブロック デバイスのデータ ストレージには多くのカーネル コンポーネントが含まれており、各コンポーネントは異なる長さのブロックを使用してディスク データを管理します: 6.1. ハードウェア ブロック デバイス コントローラーは、「セクター」と呼ばれる固定長のブロックを使用してディスク データを管理します
    。 。したがって、I/O スケジューラーとブロック デバイス ドライバーはデータ セクターを管理する必要があります。
    6.2. 仮想ファイル システム、マッピング レイヤー、およびファイル システムは、ディスク データを「ブロック」と呼ばれる論理単位に保存します。
    6.3. ブロックは、ファイル システム内の最小のディスク ストレージ ユニットに対応します。
    すぐにわかるように、ブロック デバイス ドライバーはデータの「セグメント」を処理できる必要があります。セグメントとは、ディスク上で物理的に隣接するデータ ブロックを含むメモリ ページ、またはメモリ ページの一部です。
    6.4. ディスク キャッシュはディスク データの「ページ」に作用し、各ページはページ フレームに正確にインストールされます。共通ブロック層は、上位層と下位層のコンポーネントをすべて組み合わせて、データのセクター、ブロック、セグメント、ページを認識します。多くの異なるデータ ブロックがある場合でも、通常は同じ物理 RAM ユニットを共有します。

たとえば、図 14-2 は 4096 バイトのページの構造を示しています。上位レベルのカーネル コンポーネントは、ページを 4 つの 1024 バイトのブロック バッファとして扱います。ブロック デバイス ドライバーはページ内の最後の 3 ブロックを転送しているため、これらの 3 ブロックは最後の 3072 バイトをカバーするセグメントに挿入されます。ハードディスク コントローラーは、このセグメントが 6 つの 512 バイト セクターで構成されていると認識します。
ここに画像の説明を挿入します
この章では、ブロック デバイスを処理する下位レベルのカーネル コンポーネント (一般的なブロック層、I/O スケジューラ、およびブロック デバイス ドライバー) を紹介するため、セクター、ブロック、およびセグメントに焦点を当てます。

セクタ

許容可能なパフォーマンスを達成するために、ハード ドライブおよび同様のデバイスは、連続する数バイトのデータを高速に転送します。ブロックデバイス上の各データ転送操作は、セクターと呼ばれる隣接するバイトのグループに対して操作されます。以下の説明では、バイトがディスク表面に連続して記録され、1 回の検索操作でアクセスできると仮定します。ディスクの物理構造は複雑ですが、ハードディスク コントローラが受信するコマンドは、ディスクを大きなセクタのグループとして扱います。ほとんどのディスク デバイスのセクター サイズは 512 バイトですが、一部のデバイスではより大きなセクター (1024 バイトおよび 2048 バイト) が使用されます。

セクタはデータ転送の基本単位とみなされるべきであることに注意してください。ほとんどのディスク デバイスは同時に複数の隣接するセクタを転送できますが、1 セクタ未満のデータ転送は許可されません。Linux では、セクター サイズは通常 512 バイトに設定されており、ブロック デバイスがより大きなセクターを使用する場合、対応する基礎となるブロック デバイス ドライバーが必要な変更を加えます。したがって、ブロック デバイスに格納されているデータ セットは、ディスク上の位置、つまり最初の 512 バイト セクターのインデックスとセクター数によって識別されます。セクター インデックスは、sector_c 型の 32 ビットまたは 64 ビット変数に格納されます。

ピース

セクタはハードウェア デバイスによるデータ転送の基本単位であり、ブロックは VFS およびファイル システムによるデータ転送の基本単位です。たとえば、カーネルがファイルの内容にアクセスするときは、まずファイルのディスク i ノードを含むブロックをディスクから読み取る必要があります (第 12 章の「i ノード オブジェクト」セクションを参照)。このブロックはディスク上の 1 つ以上の隣接するセクターに対応し、VFS はそれを単一のデータ単位として扱います。Linux では、ブロック サイズは 2 のべき乗である必要があり、1 ページ フレームを超えることはできません。さらに、各ブロックには整数のセクターが含まれている必要があるため、セクター サイズの整数倍でなければなりません。したがって、80×86 アーキテクチャでは、許可されるブロック サイズは 512、1024、2048、および 4096 バイトです。

ブロックデバイスのブロックサイズは一意ではありません。ディスク ファイル システムを作成するとき、管理者は適切なブロック サイズを選択できます。したがって、同じディスク上の複数のパーティションで異なるブロック サイズが使用される場合があります。さらに、ブロック デバイス ファイルに対する各読み取りまたは書き込み操作は、ディスク ファイル システムをバイパスするため、「生」アクセスであり、カーネルは最大のブロック (4096 バイト) を使用してこの操作を実行します。各ブロックには独自のブロック バッファーが必要です。ブロック バッファーは、カーネルがブロックの内容を保存するために使用する RAM メモリの領域です。カーネルがディスクからブロックを読み取るときは、ハードウェア デバイスから取得した値を対応するブロック バッファに書き込みます。同様に、カーネルがブロックをディスクに書き込むときは、関連するブロック バッファの実際の値を使用して、対応するブロック バッファを更新します。ハードウェアデバイス上の隣接するバイトのセット。ブロック バッファのサイズは通常、対応するブロックのサイズと一致します。

バッファヘッダは、各バッファに関連付けられたタイプbuffer_headの記述子です。これには、カーネルがバッファを処理するために知っておく必要があるすべての情報が含まれているため、カーネルは各バッファを操作する前に、まずバッファ ヘッダーをチェックします。バッファ ヘッダー内のすべてのフィールド値については、第 15 章で詳しく説明しますが、この章では、そのうちの一部 (b_page、b_data、b_blocknr、および b_bdev) のみを紹介します。

b_page フィールドには、ブロック バッファーが配置されているページ フレームのページ記述子アドレスが格納されます。ページ フレームがハイ メモリに配置されている場合、b_data フィールドにはページ内のブロック バッファのオフセットが格納され、それ以外の場合、b_data にはブロック バッファ自体の開始リニア アドレスが格納されます。
b_blocknr フィールドには、論理ブロック番号 (ディスク パーティション内のブロック インデックスなど) が格納されます。
最後に、b_bdev フィールドは、バッファ ヘッダーを使用してブロック デバイスを識別します (この章で後述する「ブロック デバイス」セクションを参照)。

一部

ディスク上の各 I/O 操作は、ディスクといくつかの RAM ユニットの間でいくつかの隣接するセクターの内容を転送することであることがわかっています。ほとんどの場合、ディスク コントローラはデータ転送に DMA を直接使用します [第 13 章の「ダイレクト メモリ アクセス (DMA)」セクションを参照]。ブロック デバイス ドライバーは、いくつかの適切なコマンドをディスク コントローラーに送信するだけでデータ転送をトリガーできます。データ転送が完了すると、コントローラーは割り込みを発行してブロック デバイス ドライバーに通知します。

DMA は、ディスク上の隣接するセクターからデータを転送します。これは物理的な制約です。ディスク コントローラーでは、DMA によるデータの不連続セクターの転送が許可されていますが、ディスク表面上の読み取り/書き込みヘッドの移動が非常に遅いため、この方法での転送速度は非常に遅くなります。古いディスク コントローラは「単純な」DMA 転送のみをサポートしていました。この転送モードでは、ディスクは RAM 内の連続したメモリ セルとの間でデータを転送する必要があります。ただし、新しいディスク コントローラは、いわゆるスキャッター/ギャザー DMA 転送もサポートしています。この方法では、ディスクは不連続なメモリ領域との間でデータを転送できます。

スキャッター/ギャザー DMA 転送を開始するには、ブロック デバイス ドライバーがディスク コントローラーに送信する必要があります。

  1. 開始ディスクセクタ番号と転送される総セクタ数
  2. メモリ領域の記述子のリンク リスト。リンク リストの各エントリにはアドレスと長さが含まれます。ディスク コントローラはデータ転送全体を担当します。たとえば、読み取り操作中に、コントローラは隣接するディスク セクタからデータを取得し、それらを異なるメモリ領域に保存します。スキャッター/ギャザー DMA 転送方式を使用するには、ブロック デバイス ドライバーがセグメントと呼ばれるデータ記憶単位を処理できる必要があります。セグメントは、隣接するディスク セクターからのデータを含むメモリ ページ、またはメモリ ページの一部です。したがって、スキャッター/ギャザー DMA 操作では、複数のセグメントが同時に転送される可能性があります。ブロック デバイス ドライバーは、ブロック、ブロック サイズ、およびブロック バッファーについて知る必要がないことに注意してください。したがって、上位層がセグメントを複数のブロック バッファから構成されるページとして認識しても、ブロック デバイス ドライバーはこれを意識する必要はありません。これまで見てきたように、RAM 内の対応するページ フレームがたまたま連続していて、ディスク上の対応するデータ ブロックも隣接している場合、共通ブロック層は異なるセグメントをマージできます。このマージ方法により生成される大きなメモリ領域を物理セグメントと呼びます。ただし、別のマージ方法がさまざまなアーキテクチャで許可されています。特殊なバス回路 [IO-MMU など。第 13 章の「ダイレクト メモリ アクセス (DMA)」のセクションを参照] を使用して、アドレスと物理間のバス マッピングを処理します。アドレス。

この結合方法により生成されるメモリ領域をハードウェアセグメントと呼びます。ここでは、バス アドレスと物理アドレスの間に動的なマッピングがない 80×86 アーキテクチャに焦点を当てているため、この章の残りの部分では、ハードウェア セグメントが常に物理セグメントに対応していると仮定します。

共通ブロック層

共通ブロック層は、システム内のすべてのブロック デバイスからのリクエストを処理するカーネル コンポーネントです。この層によって提供される関数のおかげで、カーネルは次のことを簡単に行うことができます。

  1. データ バッファをハイ メモリに配置します。CPU がデータにアクセスするときのみ、ページ フレームをカーネルの線形アドレス空間にマップし、データがアクセスされた後にマップを解除します。
  2. いくつかの追加手段を通じて、いわゆる「ゼロコピー」モードが実装され、ディスク データを最初にカーネル メモリ領域 (実際にはカーネルが使用するバッファ) にコピーするのではなく、ユーザー モード アドレス空間に直接保存します。 I/O データ転送の場合、ページ フレームはプロセスのユーザー モード リニア アドレス空間にマッピングされます。
  3. LVM (Logical Volume Manager) や RAID (Redundant Array of Inexpensive Disks) で使用される論理ボリュームなどを管理します。複数のディスク パーティションは、異なるブロック デバイスに配置されている場合でも、単一のパーティションとして認識できます。
  4. 大規模なマザーボードのディスク キャッシュ、強化された DMA パフォーマンス、I/O 転送要求の相対スケジューリングなど、新しいディスク コントローラの高度な機能のほとんどを活用します。

生体構造

一般的なブロック層の中心となるデータ構造は、ブロックデバイスの I/O 操作を記述する bio と呼ばれる記述子です。各バイオ構造には、ディスク記憶領域識​​別子 (記憶領域内の開始セクタ番号とセクタ数) と、I/O 操作に関連付けられたメモリ領域を記述する 1 つ以上のセグメントが含まれています。bio は bio データ構造によって記述され、そのフィールドは表 14-1 に示されています。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
bio の各セグメントは bio_vec データ構造によって記述され、そのフィールドは表 14-2 に示されています。bio の bi_io_vec フィールドは bio_vec データ構造の最初の要素を指し、bi_vcnt フィールドは bio_vec 配列内の現在の要素数を格納します。
ここに画像の説明を挿入します
バイオ記述子の内容は、ブロック I/O 操作中に更新され続けます。たとえば、ブロック デバイス ドライバーがスキャッター/ギャザー DMA 操作でデータ転送全体を完了できない場合、bio の bi_idx フィールドは、転送される最初のセグメントを指すように継続的に更新されます。インデックス bi_idx が指す現在のセグメントから始まる bio 内のセグメントを継続的に繰り返すために、デバイス ドライバーはマクロ bio_for_each_segment を実行できます。一般ブロック層が新しい I/O 操作を開始すると、bio_alloc() 関数が呼び出され、新しい bio 構造が割り当てられます。

通常、バイオ構造はスラブ アロケータによって割り当てられますが、メモリが不十分な場合、カーネルはバックアップのバイオ小規模メモリ プールも使用します (第 8 章の「メモリ プール」セクションを参照)。カーネルは、bio_vec 構造にもメモリ プールを割り当てます。結局のところ、セグメント記述子を割り当てることができなければ、bio 構造を割り当てても意味がありません。同様に、bio_put() 関数は bio の参照カウンタ (bi_cnt) の値をデクリメントし、値が 0 に等しい場合、bio 構造体および関連する bio_vec 構造体が解放されます。

ディスクとディスク パーティションの表現

ディスクは、共通ブロック層によって扱われる論理的なブロックデバイスです。通常、ディスクは、ハードディスク、フロッピー ディスク、光ディスクなどのハードウェア ブロック デバイスに対応します。ただし、ディスクは、複数の物理ディスク パーティション上、または RAM の専用ページ内の一部のメモリ領域上に構築された仮想デバイスである場合もあります。いずれの場合でも、共通ブロック層によって提供されるサービスを使用すると、上位カーネル コンポーネントはすべてのディスク上で同じように動作できます。ディスクは gendisk オブジェクトによって記述され、そのフィールドは表 14-3 に示されています。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
flags フィールドには、ディスクに関する情報が格納されます。これらのフラグの中で最も重要なものは GENHD_FL_UP です。これを設定すると、ディスクが初期化され、使用できるようになります。もう 1 つの関連フラグは GENHD_FL_REMOVABLE です。これは、フロッピー ディスクや CD-ROM などのリムーバブル ディスクの場合に設定されます。gendisk オブジェクトの fops フィールドは、テーブル block_device_operations を指します。このテーブルには、ブロック デバイスの主要な操作用にカスタマイズされたメソッドがいくつか格納されています (表 14-4 を参照)。
ここに画像の説明を挿入します通常、ハードディスクはいくつかの論理パーティションに分割されます。各ブロック デバイス ファイルは、ディスク全体またはディスク上のパーティションを表します。たとえば、メジャー デバイス番号が 3、マイナー デバイス番号が 0 のデバイス ファイル /dev/hda はプライマリ EIDE ディスクを表す場合があり、ディスク内の最初の 2 つのパーティションはデバイス ファイル /dev/hdal と /dev で構成されます。 / hda2 は、メジャー デバイス番号がすべて 3 で、マイナー デバイス番号がそれぞれ 1 と 2 であることを意味します。一般に、ディスク上のパーティションは、連続するマイナー デバイス番号によって区別されます。ディスクが複数のパーティションに分割されている場合、パーティション テーブルは hd_struct 構造体の配列に格納され、配列のアドレスは gendisk オブジェクトのpart フィールドに格納されます。配列には、ディスク内のパーティションの相対インデックスによってインデックスが付けられます。hd_struct 記述子のフィールドを表 14-5 に示します。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
カーネルは、システム内で新しいディスクを検出すると (ブート段階中、リムーバブル メディアがドライブに挿入されたとき、または実行時に外部ディスクが接続されたとき)、alloc_disk() 関数を呼び出します。新しい gendisk オブジェクト。新しいディスクが複数のパーティションに分割されている場合、alloc_disk() は適切な hd_struct タイプの配列も割り当てて初期化します。次に、カーネルは add_disk() 関数を呼び出して、新しい gendisk オブジェクトを一般ブロック層データ構造に挿入します (この章で後述する「デバイス ドライバーの登録と初期化」セクションを参照)。

リクエストを送信する

I/O 操作リクエストを一般ブロック層に送信するときにカーネルによって実行される一連のステップを紹介します。要求されたデータ ブロックはディスク上で連続しており、カーネルはそれらの物理的な位置をすでに知っていると仮定します。最初のステップは、bio_alloc() 関数を実行して新しい bio 記述子を割り当てることです。次に、カーネルはいくつかのフィールド値を設定してバイオ記述子を初期化します。

  1. bi_sector をデータの開始セクター番号に設定します (ブロック デバイスが複数のパーティションに分割されている場合、セクター番号はパーティションの開始位置を基準とします)。
  2. bi_size をデータ全体をカバーするセクター数に設定します。
  3. bi_bdev をブロック デバイス記述子のアドレスに設定します (この章で後述する「ブロック デバイス」セクションを参照)。
  4. bi_io_vec を bio_vec 構造体配列の開始アドレスに設定します。配列の各要素は 1/0 演算のセグメント (メモリ キャッシュ) を記述します。さらに、bi_vcnt を bio のセグメントの総数に設定します。
  5. bi_rw を要求された操作のフラグに設定します。最も重要なフラグは、データ転送の方向、つまり READ (0) または WRITE (1) を示します。
  6. bi_end_io を、bio での I/O 操作が完了したときに実行される完了ルーチンのアドレスに設定します。

バイオ記述子が適切に初期化されると、カーネルは generic_make_request() 関数を呼び出します。これは、一般ブロック層への主要なエントリ ポイントです。この関数は主に次の操作を実行します。

  1. bio->bi_sector がブロックデバイスのセクタ数を超えていないことを確認してください。それを超えた場合は、bio->bi_flags を BIO_EOF フラグに設定し、カーネル エラー メッセージを出力し、bio_endio() 関数を呼び出して終了します。bio_endio()は、bio記述子のbi_sizeとbi_sectorの値を更新し、bioのbi_end_ioメソッドを呼び出します。bi_end_io 関数の実装は基本的に、I/O データ転送をトリガーするカーネル コンポーネントに依存します。次の章で、bi_end_io メソッドの例をいくつか示します。
  2. ブロック デバイスに関連付けられたリクエスト キュー q を取得します (この章で後述する「リクエスト キュー記述子」セクションを参照)。そのアドレスはブロック デバイス記述子の bd_disk フィールドに格納され、その中の各要素は bio-disk によってポイントされます。 >bi_bdev。
  3. block_wait_queue_running() 関数を呼び出して、現在使用中の I/O スケジューラを動的に置き換えることができるかどうかを確認します。そうであれば、新しい I/O スケジューラが開始されるまで現在のプロセスをスリープさせます (次のセクション「I/O スケジューラ」を参照) )。
  4. blk_partition_remap() 関数を呼び出して、ブロック デバイスがディスク パーティションを参照しているかどうかを確認します (bio->bi_bdev は bio->bi_dev->bd_contains と等しくありません。この章で後述する「ブロック デバイス」セクションを参照してください)。その場合は、bio->bi_bdev からパーティションの hd_struct 記述子を取得して、次のサブ操作を実行します:
    a. データ転送の方向に従って、read_sectors を更新して値を読み取るか、write_sectors を更新して hd_struct 記述子に値を書き込む。
    b. bio->bi_sector 値を調整して、パーティションに対する相対的な開始セクター番号をディスク全体に対する相対的なセクター番号に変換します。
    c. bio->bi_bdev をディスク全体のブロックデバイス記述子に設定します (bio->bd_contains)。今後、一般的なブロック層、I/O スケジューラ、およびデバイス ドライバーはディスク パーティションの存在を忘れ、ディスク全体で直接動作するようになります。
  5. q->make_request_fn メソッドを呼び出して、バイオリクエストをリクエストキュー q に挿入します。
  6. 戻る。
    make_request_fn メソッドの一般的な実装については、この章の後半の「I/O スケジューラのリクエスト」セクションで説明します。

I/Oスケジューラ

ブロック デバイス ドライバーは一度に 1 つのセクターを転送できますが、ブロック I/O 層はディスク上のアクセスされたセクターごとに個別の I/O 操作を実行しないため、ディスクのパフォーマンスが低下する可能性があります。ディスク表面上のセクターの位置を特定するのは非常に時間がかかります。その代わりに、カーネルは可能な限り、複数のセクターをマージして全体として処理しようとし、平均ヘッド移動時間を短縮します。カーネル コンポーネントがディスク データの読み取りまたは書き込みを行う場合、実際にはブロック デバイス リクエストが作成されます。基本的に、要求には、要求されているセクターと、そのセクターに対して実行される操作の種類 (読み取りまたは書き込み) が記述されます。ただし、カーネルは要求が発行されてもすぐに要求を満たしません。I/O 操作は単にスケジュールされ、実行は延期されます。この人為的な遅延は、ブロック デバイスのパフォーマンスを向上させるための重要なメカニズムです。

新しいデータ ブロックが要求されると、カーネルは、待機していた前の要求をわずかに延長することで新しい要求を満たすことができるかどうか (つまり、さらなるシーク操作を行わずに新しい要求を満たすことができるかどうか) をチェックします。ディスクアクセスはほとんどシーケンシャルであるため、この単純なメカニズムは非常に効率的です。リクエストが遅延すると、ブロックデバイスの処理が複雑になります。
たとえば、プロセスが通常のファイルを開き、ファイル システム ドライバーが対応するインデックス ノードをディスクから読み取るとします。ブロック デバイス ドライバーはリクエストをキューに入れ、inode を保持するブロックが転送されるまでプロセスを一時停止します。ただし、同じディスクにアクセスしようとする他のプロセスもブロックされる可能性があるため、ブロック デバイス ドライバー自体はブロックされません。ブロック デバイス ドライバーがハングしないようにするために、各 I/O 操作は非同期的に処理されます。

特に、ブロック デバイス ドライバーは割り込み駆動型です (第 13 章の「I/O 操作の監視」セクションを参照)。一般的なブロック層は I/O スケジューラーを呼び出して、新しいブロック デバイス リクエストを生成するか、既存のブロック デバイス リクエストを拡張します。デバイス要求をブロックし、終了します。次に、アクティブ化されたブロック デバイス ドライバーは、いわゆるストラテジ ルーチンを呼び出して保留中の要求を選択し、その要求を満たすための適切なコマンドをディスク コントローラーに発行します。

I/O 操作が終了すると、ディスク コントローラは割り込みを生成し、必要に応じて、対応する割り込みハンドラがポリシー ルーチンを呼び出してキュー内の別の要求を処理します。各ブロック デバイス ドライバーは、デバイスに対する保留中の要求のリンク リストを含む独自の要求キューを維持します。ディスク コントローラが複数のディスクを処理している場合、通常は物理ブロック デバイスごとにリクエスト キューが存在します。各要求キューで個別に I/O スケジューリングを実行すると、ディスクのパフォーマンスを向上させることができます。

リクエストキュー記述子

リクエスト キューは大規模なデータ構造 request_queue で表され、そのフィールドは表 14-6 に示されています。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します

本質的に、リクエスト キューは二重リンク リストであり、その要素はリクエスト記述子 (つまり、リクエスト データ構造。次のセクションを参照) です。リクエスト キュー記述子の queue_head フィールドには、リンク リストの先頭 (最初の疑似要素) が格納され、リクエスト ディスクリプタの queuelist フィールドのポインタは、リクエストをリンク リストの前および次の要素にリンクします。
キュー リスト内の要素の順序は各ブロック デバイス ドライバーに固有ですが、I/O スケジューラには、要素を順序付けるための事前に決定された方法がいくつか用意されています。これについては、セクションで説明する「I/O スケジューリング アルゴリズム」で後述します。backing_dev_info フィールドは、backing_dev_info タイプの小さなオブジェクトで、基本ハードウェア ブロック デバイスの I/O データ トラフィックに関する情報を保存します。たとえば、先読みに関する情報やリクエスト キューの輻輳ステータスに関する情報が保持されます。

リクエスト記述子

各ブロックデバイスの保留中のリクエストはリクエスト記述子によって表され、表 14-7 に示すリクエストデータ構造に格納されます。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します
各リクエストには 1 つ以上のバイオ構造が含まれます。最初に、汎用ブロック層はバイオ構造のみを含むリクエストを作成します。次に、I/O スケジューラは、最初のバイオに新しいセグメントを追加するか、別のバイオ構造をリクエストにリンクすることによって、リクエストを「拡張」します。新しいデータがリクエスト内にすでに存在するデータに物理的に隣接している状況が発生する可能性があります。リクエスト記述子のバイオ フィールドはリクエスト内の最初のバイオ構造を指し、バイオテール フィールドは最後のバイオ構造を指します。rq_for_each_bio マクロはループを実行し、それによってリクエスト内のすべての bio 構造を走査します。
リクエスト記述子のいくつかのフィールド値は動的に変更される場合があります。たとえば、バイオで参照されているすべてのデータ ブロックが転送されると、バイオ フィールドはリクエスト リスト内の次のバイオを指すように直ちに更新されます。この期間中に、新しいバイオがリクエスト リストの最後に追加される可能性があるため、バイオテールの値も変化する可能性があります。ディスク データ ブロックの転送中に、要求記述子の他のいくつかのフィールドの値が I/O スケジューラまたはデバイス ドライバによって変更されます。

たとえば、nr_sectors はリクエスト全体でまだ送信する必要があるセクターの数を格納し、current_nr_sectors は現在のバイオ構造でまだ送信する必要があるセクターの数を格納します。表 14-8 に示すように、flags には多くのフラグが格納されます。これまでのところ、最も重要なフラグは REQ_RW で、データ転送の方向を決定します。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
ここに画像の説明を挿入します

リクエスト記述子の割り当てを管理する

負荷が高く、ディスク操作が頻繁に行われる場合、固定量の動的空きメモリが、リクエスト キュー q に新しいリクエストを追加するプロセスにとってボトルネックになります。この問題を解決するために、各 request_queue 記述子には、次のような request_list データ構造が含まれています。

  1. 要求された記述子のメモリ プールへのポインタ (第 8 章の「メモリ プール」セクションを参照)。
  2. 2 つのカウンタは、READ リクエストと WRITE リクエストに割り当てられたリクエスト記述子の数を記録するために使用されます。
  3. 読み取りまたは書き込みリクエストの割り当てが失敗したかどうかをマークするために使用される 2 つのフラグ。
  4. 2 つの待機キューには、空き読み取りおよび書き込み要求記述子を取得するためにスリープしているプロセスが格納されます。· リクエスト キューが更新される (空になる) のを待っているプロセスを格納する待機キュー。

blk_get_request() 関数は、特定のリクエスト キューのメモリ プールから空きリクエスト記述子を取得しようとします。メモリ領域が不十分でメモリ プールが使い果たされた場合、現在のプロセスを一時停止するか、NULL を返します (カーネル制御が無効な場合)。パスをブロックすることはできません)。割り当てが成功すると、リクエスト キューの request_list データ構造のアドレスがリクエスト記述子の r1 フィールドに格納されます。blk_put_request() 関数はリクエスト記述子を解放します。記述子の参照カウンタの値が 0 の場合、記述子は元のメモリ プールに返されます。

リクエストキューの混雑を回避する

各リクエスト キューには、処理できるリクエストの最大数があります。リクエストキュー記述子の nr_requests フィールドには、データ転送方向ごとに処理可能なリクエストの最大数が格納されます。デフォルトでは、キューには最大 128 個の保留中の読み取りリクエストと 128 個の保留中の書き込みリクエストがあります。

保留中の読み取り (書き込み) リクエストの数が nr_requests 値を超える場合、リクエスト キュー記述子の queue_flags フィールドに QUEUE_FLAG_READFULL (QUEUE_FLAG_WRITEFULL) フラグを設定することによってキューは満杯としてマークされ、使用可能な転送にリクエストを追加しようとします。ブロッキングプロセスは、request_list 構造に対応する待機キューに配置され、スリープします。リクエスト キューがいっぱいになると、I/O データ転送が完了するまで待機している間に多くのプロセスがスリープ状態になるため、システムのパフォーマンスに悪影響を及ぼします。したがって、特定の転送方向の保留中のリクエストの数がリクエスト記述子の nr_congestion_on フィールドに格納されている値 (デフォルト値は 113) を超えると、カーネルはキューが混雑していると見なし、キューの作成を遅くしようとします。新しいリクエストのレート。

保留中のリクエストの数が nr_congestion_off の値 (デフォルト値は 111) よりも少ない場合、混雑したリクエスト キューは混雑しなくなります。blk_congestion_wait() 関数は、すべてのリクエスト キューが混雑しなくなるか、タイムアウトが経過するまで、現在のプロセスを一時停止します。

ブロックデバイスドライバーを有効にする

これまでに見たように、ブロック デバイス ドライバーのアクティブ化を遅らせると、隣接するブロックへのリクエストが集中するという利点があります。この遅延は、いわゆるデバイス挿入およびデバイス抽出テクノロジーによって実現されます (注 2)。ブロック デバイス ドライバーがプラグインされている場合、ドライバー キューに保留中の要求があっても、ドライバーはアクティブ化されません。blk_plug_device() 関数の機能は、ブロック デバイスを、より正確には、ブロック デバイス ドライバーによって処理される要求キューに挿入することです。

基本的に、この関数はリクエストキュー記述子のアドレス q を引数として受け取ります。q->queue_flags フィールドに QUEUE_FLAG_PLUGGED ビットを設定し、q->unplug_timer フィールドで埋め込み動的タイマーを再起動します。blk_remove_plug() はリクエスト キューを削除します。 q: QUEUE_FLAG_PLUGGED フラグをクリアし、q->unplug_timer 動的タイマーの実行をキャンセルします。すべてのマージ可能なリクエストが「目に入った」リクエスト キューに追加されたときに、カーネルはこの関数を明示的に呼び出します。さらに、リクエスト キュー内の保留中のリクエストの数が、リクエスト キュー記述子の unplug_thresh フィールドに格納されている値 (デフォルト値は 4) を超える場合、I/O スケジューラはリクエスト キューも削除します。

デバイスが q->unplug_lay (通常 3ms) の時間間隔にわたって接続されたままの場合、blk_plug_device() 関数によってアクティブ化された動的タイマーが期限切れになるため、blk_unplug_timeout() 関数が実行されます。したがって、カーネル スレッド kblockd によって操作されるワーク キュー kblockd_workqueue を起動します (第 4 章の「ワーク キュー」セクションを参照)。kblockd は blk-unplug_work() 関数を実行し、そのアドレスは q->unplug_work 構造体に格納されます。次に、関数はリクエスト キュー内の q->unplug_fn メソッドを呼び出します。このメソッドは通常、generic_unplug_device() 関数によって実装されます。

generic_unplug_device() 関数の機能は、ブロック デバイスを切断することです。
まず、リクエスト キューがまだアクティブかどうかを確認し、
次に blk_remove_plug() 関数を呼び出します。
最後に、ポリシー ルーチン request_fn メソッドを実行して、次のリクエストの処理を開始します。要求キュー (この章の「デバイス ドライバーの登録と初期化」セクションを後述する)。

IOスケジューリングアルゴリズム

新しいリクエストがリクエスト キューに追加されると、汎用ブロック層は I/O スケジューラを呼び出して、リクエスト キュー内の新しいリクエストの正確な位置を決定します。I/O スケジューラは、セクターごとにリクエストをキューに入れようとします。処理対象のリクエストがリンクリストから順番に抽出される場合、ヘッドはランダムに移動するのではなく、内側のトラックから外側のトラックへ(またはその逆に)直線的に移動するため、ヘッドシークの回数は大幅に減少します。あるトラックから別のトラックに移動したり、別のトラックにジャンプしたりできます。これはエレベーター アルゴリズムからインスピレーションを得たもので、エレベーター アルゴリズムは異なる層からの上り要求と下り要求を処理することを思い出してください。エレベータは一方向に移動し、一方向の最後の予定階に到達すると、エレベータは方向を変えて反対方向に移動を開始します。したがって、I/O スケジューラはエレベーター アルゴリズムとも呼ばれます。

負荷が高い場合、セクター番号の順序に厳密に従う I/O スケジューリング アルゴリズムはあまり適切に機能しません。この場合、データ転送の完了時間は主にディスク上のデータの物理的な位置によって決まります。したがって、デバイス ドライバーによって処理されるリクエストがキューの先頭 (セクター番号が小さい) にあり、セクター番号が小さい新しいリクエストが常にキューに追加されると、キューの最後尾のリクエストは簡単に枯渇してしまいます。 。したがって、I/O スケジューリング アルゴリズムは非常に複雑になります。
現在、Linux 2.6 は 4 つの異なるタイプの I/O スケジューラまたはエレベーター アルゴリズム、つまり「Anticipatory」アルゴリズム、「Deadline」アルゴリズム、「CFQ (Complete Fairness Queueing)」アルゴリズム、および「Noop (No Operation)」アルゴリズムを提供しています。アルゴリズム。ほとんどのブロック デバイスの場合、カーネルで使用されるデフォルトのエレベーター アルゴリズムは、カーネル パラメーターエレベーター = を使用してブート時にリセットできます。値は、as、deadline、cfq、および noop のいずれかになります。ブートパラメータが指定されていない場合、カーネルはデフォルトで「予想される」I/O スケジューラを使用します。つまり、デバイス ドライバーはデフォルトのエレベーター アルゴリズムを任意のスケジューラで置き換えることができ、デバイス ドライバーは I/O スケジューリング アルゴリズムをカスタマイズすることもできますが、これはまれです。さらに、システム管理者は、実行時に特定のブロック デバイスの I/O スケジューラを変更できます。たとえば、最初の IDE チャネルのプライマリ ディスクで使用される I/O スケジューラを変更するには、管理者は、エレベーター アルゴリズムの名前を sysfs 特殊ファイル システムの /sys/block/hda/queue/scheduler ファイルに書き込むことができます。 (第 13 章の「sysfs ファイル システム」セクションを参照)。

リクエスト キューで使用される I/O スケジューリング アルゴリズムは、タイプ elevator_t のエレベータ オブジェクトによって表され、オブジェクトのアドレスはリクエスト キュー記述子のエレベータ フィールドに格納されます。エレベーター オブジェクトには、エレベーターのすべての可能な操作 (エレベーターのリンクと切断、キューへのリクエストの追加とマージ、キューからのリクエストの削除、キュー内の次の保留中のリクエストの取得など) をカバーするいくつかのメソッドが含まれています。エレベーター オブジェクトには、リクエスト キューの処理に必要なすべての情報が含まれるテーブルのアドレスも格納されます。さらに、各リクエスト記述子には、リクエストを処理するために I/O スケジューラによって使用される追加のデータ構造を指すエレベーター_プライベート フィールドが含まれています。

ここで、4 つの I/O スケジューリング アルゴリズムを簡単なものから難しいものまで簡単に紹介します。I/O スケジューラの設計は、CPU スケジューラの設計 (第 7 章を​​参照) と非常に似ていることに注意してください。使用されるヒューリスティックと定数値は、テストとベンチマーク拡張の結果です。一般に、すべてのアルゴリズムはディスパッチ キュー (ディスパッチ キュー) を使用し、キューに含まれるすべてのリクエストは、デバイス ドライバーが処理する順序で並べ替えられます。つまり、デバイス ドライバーによって次に処理されるリクエストは、通常、ディスパッチ キューです。の最初の要素をキューに入れます。

ディスパッチキューは、実際にはリクエストキュー記述子の queue_head フィールドによって決定されるリクエストキューです。ほとんどすべてのアルゴリズムは、追加のキューを使用してリクエストを分類し、順序付けします。これらにより、デバイス ドライバーは既存のリクエストにバイオ構造を追加し、必要に応じて 2 つの「隣接する」リクエストをマージできます。

「ヌープ」アルゴリズム

これは最も単純な I/O スケジューリング アルゴリズムです。ソートされたキューはありません。通常、新しいリクエストはディスパッチ キューの先頭または末尾に挿入され、次に処理されるリクエストは常にキュー内の最初のリクエストになります。

「CFQ」アルゴリズム

「CFQ (Completely Fair Queuing)」アルゴリズムの主な目的は、I/O リクエストをトリガーするすべてのプロセス間でディスク I/O 帯域幅を公平に分配することです。この目標を達成するために、アルゴリズムはさまざまなプロセスによって発行されたリクエストを格納する多くのソート キューを使用します。

アルゴリズムがリクエストを処理するとき、カーネルはハッシュ関数を呼び出して、現在のプロセスのスレッド グループ識別子 (通常はその PID に対応します。第 3 章の「プロセスの識別」セクションを参照) をキューのインデックス値に変換します。このアルゴリズムは、キューの最後に新しいリクエストを挿入します。したがって、同じプロセスからのリクエストは通常​​、同じキューに挿入されます。ディスパッチ キューを再設定するために、アルゴリズムは基本的にポーリング方式で I/O 入力キューをスキャンし、最初の空でないキューを選択し、そのキュー内の一連のリクエストをディスパッチ キューの最後に移動します。

「デッドライン」アルゴリズム

ディスパッチ キューに加えて、Deadline アルゴリズムでは 4 つのキューが使用されます。2 つのソート キューにはそれぞれ読み取りリクエストと書き込みリクエストが含まれており、リクエストは開始セクター番号に従ってソートされます。他の 2 つのデッドライン キューには同じ読み取りリクエストと書き込みリクエストが含まれていますが、これは「デッドライン」に従って順序付けされます。これらのキューは、最後に処理されたリクエストに最も近いリクエストを優先するエレベーター ポリシーによりリクエストが長期間無視された場合に発生するリクエスト スタベーションを回避するために導入されました。リクエストの期限は基本的に、リクエストがエレベーター アルゴリズムに渡されたときに開始されるタイムアウトです。

デフォルトでは、読み取りリクエストのタイムアウトは 500 ミリ秒、書き込みリクエストのタイムアウトは 5 秒です。読み取りリクエストは通常​​、要求プロセスをブロックするため、読み取りリクエストが書き込みリクエストよりも優先されます。期限により、たとえソートキューの最後にあるとしても、スケジューラーが長時間待機しているリクエストを処理することが保証されます。アルゴリズムがディスパッチ キューを補充したい場合、まず次のリクエストのデータ方向を決定します。読み取りリクエストと書き込みリクエストの両方が同時にスケジュールされている場合、(書き込みリクエストの枯渇を避けるため)「書き込み」方向が何度も放棄されない限り、アルゴリズムは「読み取り」方向を選択します。次に、アルゴリズムは、選択された方向に関連付けられたデッドライン キューをチェックします。キュー内の最初のリクエストのデッドラインが過ぎている場合、アルゴリズムはリクエストをディスパッチ キューの最後に移動します。また、そのリクエストから開始することもできます。タイムアウトしました。一連のリクエストをソートキューから移動します。グループの長さは、移動するリクエストがディスク上で物理的に隣接している場合には長くなり、そうでない場合には短くなります。

最後に、リクエストのタイムアウトがない場合、アルゴリズムはソートされたキューからの最後のリクエストの後に一連のリクエストをスケジュールします。ポインタがソートされたキューの最後に到達すると、検索は再び最初から始まります (「一方向アルゴリズム」)。

「予想」アルゴリズム

「予想」アルゴリズムは、Linux が提供する最も複雑な I/O スケジューリング アルゴリズムです。基本的に、これは「デッドライン」アルゴリズムの進化版であり、「デッドライン」アルゴリズムの基本メカニズム (2 つのデッドライン キューと 2 つの並べ替えキュー) を借用しており、I/O スケジューラは読み取りと書き込みの間で対話的にスキャンおよび並べ替えを行います。読み取りリクエスト。リクエストがタイムアウトしない限り、スキャンは基本的に継続的に行われます。読み取りリクエストのデフォルトのタイムアウトは 125 ミリ秒、書き込みリクエストのデフォルトのタイムアウトは 250 ミリ秒です。ただし、アルゴリズムは追加のヒューリスティック ガイドラインにも準拠します。
1. 場合によっては、アルゴリズムはソート キュー内の現在位置の後のリクエストを選択するため、ヘッドが後ろから検索することを強制されることがあります。これは通常、このリクエストの後の検索距離が、ソート キュー内の現在位置からのこのリクエストの検索距離の半分未満である場合に発生します。
2. このアルゴリズムは、システム内の各プロセスによってトリガーされた I/O 操作の種類をカウントします。特定のプロセス p によって発行された読み取りリクエストをスケジュールした後、アルゴリズムはソート キュー内の次のリクエストが同じプロセス p からのものであるかどうかを直ちにチェックします。その場合は、すぐに次のリクエストをスケジュールしてください。それ以外の場合は、プロセス p に関する統計を確認します。プロセス p がすぐに別の読み取りリクエストを発行する可能性があると判断された場合は、短期間 (デフォルトは約 7ms) 遅延します。
したがって、アルゴリズムは、プロセス p によって発行された読み取りリクエストと、スケジュールされたばかりのリクエストがディスク上で「近隣」にある可能性があると予測します。

I/O スケジューラにリクエストを送信する

この章の前半の「リクエストの送信」セクションで説明したように、 generic_make_request() 関数は、リクエスト キュー記述子の make_request_fn メソッドを呼び出して、I/O スケジューラにリクエストを送信します。通常、このメソッドは __make_request() 関数によって実装されます。この関数は request_queue タイプ記述子 q と bio 構造記述子 bio をパラメータとして受け取り、次の操作を実行します。

  1. 必要に応じて、blk_queue_bounce() 関数を呼び出してバウンス バッファを作成します (後述)。バウンス バッファが作成された場合、__make_request() 関数は元の bio 構造ではなくバッファ上で動作します。
  2. I/O スケジューラの elv_queue_empty() 関数を呼び出して、リクエスト キューに保留中のリクエストがあるかどうかを確認します。ディスパッチ キューは空の場合もありますが、I/O スケジューラの他のキューには保留中の要求が含まれている可能性があることに注意してください。保留中のリクエストがない場合は、blk_plug_device() 関数を呼び出してリクエスト キューを挿入し (この章の前半の「ブロック デバイス ドライバーのアクティブ化」セクションを参照)、ステップ 5 に進みます。
  3. 挿入されたリクエスト キューには保留中のリクエストが含まれています。I/O スケジューラの elv_merge() 関数を呼び出して、新しい Bio 構造を既存のリクエストにマージできるかどうかを確認します。この関数は 3 つの可能な値を返します:
    3.1. ELEVATOR_NO_MERGE: 既存のリクエストにバイオ構造を含めることはできません; この場合は、ステップ 5 に進みます。
    3.2. ELEVATOR_BACK_MERGE: バイオ構造は最後のバイオとしてリクエスト req に挿入できます;
    この場合、関数は q->back_merge_fn メソッドを呼び出してリクエストを展開できるかどうかを確認します。そうでない場合は、ステップ 5 に進みます。それ以外の場合は、req リストの最後に bio 記述子を挿入し、req の対応するフィールド値を更新します。次に、関数はこのリクエストとそれに続くリクエストをマージしようとします (2 つのリクエストの間に新しいバイオが設定される場合があります)。
    3.3. ELEVATOR_FRONT_MERGE: バイオ構造はリクエスト req の最初のバイオとして挿入できます; この場合、関数は q->front_merge_fn メソッドを呼び出してリクエストを展開できるかどうかを確認します。そうでない場合は、ステップ 5 に進みます。それ以外の場合は、バイオ記述子を req リストの先頭に挿入し、req の対応するフィールド値を更新します。次に、リクエストを前のリクエストとマージする試みが行われます。
  4. bio が既存のリクエストに組み込まれている場合は、ステップ 7 に進んで機能を終了します。
  5. bio を新しいリクエストに挿入する必要があります。新しいリクエストディスクリプタを割り当てます。空きメモリがない場合、現在のプロセスは、bio->bi_rw の BIO_RW_AHEAD フラグが設定されるまで一時停止されます。これは、この I/O 操作が先読みであることを示します (第 16 章を参照); この場合、関数は次の関数を呼び出します。 bio_endio() を実行して終了します。この時点ではデータ転送は実行されません。bio_endio() 関数の説明については、generic_make_request() 関数のステップ 1 を参照してください (前の「リクエストの送信」セクションを参照)。
  6. リクエスト記述子のフィールドを初期化します。a.セクター
    数、現在のバイオ、現在のセグメントなどのバイオ記述子の内容に従って各フィールドを初期化します。
    b. フラグ フィールドに REQ_CMD フラグを設定します (標準の読み取りまたは書き込み操作)。
    c. 最初のバイオ セグメントのページ フレームがローエンド メモリに格納されている場合は、バッファ フィールドをバッファのリニア アドレスに設定します。
    d. rq_disk フィールドを bio->bi_bdev->bd_disk のアドレスに設定します。
    e. プロフィールをリクエスト リストに挿入します。
    f. start_time フィールドを jiffies の値に設定します。
  7. すべての操作が完了しました。ただし、終了する前に、bio->bi_rw の BIO_RW_SYNC フラグが設定されているかどうかを確認してください。その場合、generic_unplug_device() 関数が「リクエスト キュー」で呼び出され、デバイス ドライバーがアンインストールされます (この章の前半の「ブロック デバイス ドライバーのアクティブ化」セクションを参照)。
  8. 機能が終了します。

__make_request() 関数を呼び出す前にリクエスト キューが空でない場合は、リクエスト キューがアンプラグされているか、まもなくアンプラグされる予定です。保留中のリクエストを持つすべての挿入リクエスト キュー q には、動的タイマー q->unplug_timer が実行されているためです。一方、リクエスト キューが空の場合は、__make_request() 関数がリクエスト キューに挿入します。後で (最悪の場合はアンプラグ タイマーの期限切れ時)、または早期 (bio の BIO_RW_SYNC フラグが設定されている場合、__make_request() から終了するとき) のいずれかで、リクエスト キューはアンプラグされます。いずれの場合も、ブロック デバイス ドライバーのポリシー ルーチンは最終的にディスパッチ キュー内の要求を処理します (この章で後述する「デバイス ドライバーの登録と初期化」セクションを参照)。

blk_queue_bounce() 関数

blk_queue_bounce() 関数の機能は、q->bounce_gfp のフラグと q->bounce_pfn のしきい値を調べて、バッファーバウンスが必要かどうかを判断することです。これは通常、リクエスト内の一部のバッファがハイメモリに配置されており、ハードウェア デバイスがそれらのバッファにアクセスできない場合に発生します。

ISA バスで使用される古い DMA 方式は、24 ビットの物理アドレスのみを処理できます。したがって、バウンス バッファの上限は 16 MB に設定されます。これは、ページ フレーム番号が 4096 であることを意味します。ただし、古いデバイスを扱う場合、ブロック デバイス ドライバーは通常、バウンス バッファーに依存せず、ZONE_DMA メモリ ゾーンに DMA バッファーを直接割り当てることを好みます。ハードウェア デバイスがハイ メモリ内のバッファを処理できない場合、blk_queue_bounce() 関数は、バイオ内の一部のバッファを本当にバウンスする必要があるかどうかをチェックします。

その場合、バイオ記述子をコピーしてバウンスバイオを作成し、セグメント内のページフレーム番号が q->bounce_pfn の値以上の場合、次の操作を実行します。

  1. 割り当てられたフラグに従って、ZONE_NORMAL または ZONE_DMA メモリ領域にページ フレームを割り当てます。
  2. 新しいページ フレームの記述子を指すように、リバウンド バイオの中央にある bv_page フィールドの値を更新します。
  3. bio->bio_rw が書き込み操作を表す場合、 kmap() を呼び出してハイエンド メモリ ページをカーネル アドレス空間に一時的にマップし、次にハイエンド メモリ ページをローエンド メモリ ページにコピーし、最後に kunmap( ) マッピングを解除します。次に、blk_queue_bounce() 関数は、バウンス バイオに BIO_BOUNCED フラグを設定し、それに特定の bi_end_io メソッドを初期化し、最後にバウンス バイオの bi_private フィールド (初期バイオへのポインタ) に格納します。リバウンド バイオ上の I/O データ転送が終了すると、関数は bi_end_io メソッドを実行してデータをハイエンド メモリ領域 (読み取り操作にのみ適しています) にコピーし、リバウンド バイオ構造を解放します。

ブロックデバイスドライバー

ブロック デバイス ドライバーは、Linux ブロック サブシステムの最下位レベルのコンポーネントです。これらは I/O スケジューラからリクエストを取得し、必要に応じてそれらのリクエストを処理します。もちろん、ブロック デバイス ドライバーはデバイス ドライバー モデルの一部です (第 13 章の「デバイス ドライバー モデル」セクションを参照)。したがって、各ブロック デバイス ドライバーは、device_driver タイプの記述子に対応し、さらに、デバイス ドライバーによって処理される各ディスクは、デバイス記述子に関連付けられます。ただし、これらの記述子には特別な点はありません。ブロック I/O サブシステムは、システム内の各ブロック デバイスの追加情報を格納する必要があります。

ブロックデバイス

ブロック デバイス ドライバーは複数のブロック デバイスを処理できます。たとえば、IDE デバイス ドライバーは、それぞれが別個のブロック デバイスである複数の IDE ディスクを処理できます。さらに、通常、各ディスクはパーティション化されており、各パーティションは論理ブロック デバイスとして見ることができます。明らかに、ブロック デバイス ドライバーは、ブロック デバイスに対応するブロック デバイス ファイルに対して発行されるすべての VFS システム コールを処理する必要があります。各ブロックデバイスは、block_device 構造体の記述子によって表され、そのフィールドは表 14-9 に示されています。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
すべてのブロック デバイス記述子はグローバル リンク リストに挿入され、リンク リストの先頭は変数 all_bdevs で表され、リンク リストのリンクに使用されるポインタはブロック デバイス記述子の bd_list フィールドに配置されます。ブロック デバイス記述子がディスク パーティションに対応する場合、 bd_contains フィールドはディスク全体に関連付けられたブロック デバイス記述子を指し、 bd_part フィールドは hd_struct パーティション記述子を指します (前述の「ディスクとディスク パーティションの表現」セクションを参照)この章では)。それ以外の場合、ブロック デバイス記述子がディスク全体に対応する場合、bd_contains フィールドはブロック デバイス記述子自体を指し、bd_part_count フィールドはディスク上のパーティションが開かれた回数を記録するために使用されます。bd_holder フィールドには、ブロック デバイス ホルダーを表すリニア アドレスが格納されます。ホルダーは、I/O データ転送を行うブロック デバイス ドライバーではなく、デバイスを使用し、固有の権限を持つカーネル コンポーネントです (たとえば、ブロック デバイス記述子の bd_private フィールドを自由に使用できます)。
通常、ブロック デバイスの所有者は、デバイスにマウントされたファイル システムです。もう 1 つの一般的な問題は、ブロック デバイス ファイルが排他的アクセス用に開かれたときに発生します。ホルダーは対応するファイル オブジェクトです。bd_claim() 関数は bd_holder フィールドを特定のアドレスに設定し、逆に bd_release() 関数はフィールドを NULL にリセットします。ただし、同じカーネル コンポーネントが bd_claim() 関数を複数回呼び出し、呼び出しのたびに bd_holders の値が増加する可能性があることに注意してください。ブロックデバイスを解放するには、カーネルコンポーネントは bd_release() 関数を bd_holders 回呼び出す必要があります。
図 14-3 はディスク全体に対応しており、ブロック デバイス記述子がブロック I/O サブシステムの他の重要なデータ構造にどのようにリンクされているかを示しています。

アクセスブロックデバイス

カーネルがブロック デバイス ファイルを開く要求を受け取ると、まずデバイス ファイルがすでに開いているかどうかを判断する必要があります。実際、ファイルがすでに開いている場合、カーネルは新しいブロック デバイス記述子を作成して初期化する必要はなく、代わりに既存のブロック デバイス記述子を更新する必要があります。ただし、実際の複雑さは、メジャー番号とマイナー番号が同じでパス名が異なるブロック デバイス ファイルは、VFS では別のファイルとして認識されますが、実際には同じブロック デバイスを指していることです。したがって、カーネルは、オブジェクトの i ノード キャッシュ内のブロック デバイス ファイルの存在をチェックするだけでは、対応するブロック デバイスがすでに使用されているかどうかを判断できません。
ここに画像の説明を挿入します
メジャー デバイス番号とマイナー デバイス番号、および対応するブロック デバイス記述子の間の関係は、bdev 特殊ファイル システムを通じて維持されます (第 12 章の「特殊ファイル システム」セクションを参照)。各ブロック デバイス記述子は、bdev 特殊ファイルに対応します。ブロック デバイス記述子の bd_inode フィールドは、対応する bdev インデックス ノードを指します。インデックス ノードは、ブロック デバイスのメジャー デバイス番号とマイナー デバイス番号、および対応する記述子のアドレスをエンコードします。bdget() は、ブロック デバイスのメジャー デバイス番号とマイナー デバイス番号をパラメータとして受け取ります。bdev ファイル システムで関連するインデックス ノードを検索します。そのようなノードが存在しない場合は、新しいインデックス ノードと新しいブロック デバイス記述子が割り当てられます。いずれの場合も、この関数は、指定されたメジャー デバイス番号とマイナー デバイス番号に対応するブロック デバイス記述子のアドレスを返します。ブロック デバイスの記述子が見つかると、カーネルは bd_openers フィールドの値をチェックして、ブロック デバイスが現在使用されているかどうかを判断します。値が正の場合、ブロック デバイスは既に使用されています (おそらく別のデバイス ファイルを通じて)。同時に、カーネルは、開かれたブロック デバイス ファイルに対応する i ノード オブジェクトのリンク リストも維持します。リンク リストはブロック デバイス記述子の nd_inodes フィールドに格納され、インデックス ノード オブジェクトの i_devices フィールドには、リンク リスト内の前後の要素をリンクするために使用されるポインタが格納されます。

デバイスドライバーの登録と初期化

ここで、ブロック デバイス用の新しいドライバーの設計に含まれる基本的な手順を説明しましょう。明らかに、説明は比較的単純ですが、ブロック I/O サブシステムによって使用される主要なデータ構造をいつ、どのように初期化するかを理解するのに役立ちます。ブロック デバイス ドライバーに必要な手順はすべて省略しましたが、第 13 章ですでに説明されています。たとえば、ドライバー自体を登録する手順はすべてスキップしました (第 13 章の「デバイス ドライバー モデル」セクションを参照)。通常、ブロックデバイスはPCIやSCSIなどの標準的なバスアーキテクチャに属しており、カーネルは対応する補助機能を提供しており、その補助機能としてドライバーをドライバーモデルに登録することになります。

カスタムドライバー記述子

まず、デバイス ドライバーには、ハードウェア デバイスの駆動に必要なデータを保持する foo_dev_t タイプのカスタム記述子 foo が必要です。この記述子には、デバイスの動作に使用される I/O ポート、デバイスが割り込みを発行する IRQ ライン、デバイスの内部ステータスなど、各デバイスに関する関連情報が格納されます。また、ブロック I/O サブシステムに必要ないくつかのフィールドも含まれています。

struct foo_dev_t {
[...]
spinlock_t lock;
struct gendisk *gd;
[...]
} foo;

ロック フィールドは、foo 記述子のフィールド値を保護するために使用されるスピン ロックであり、そのアドレスは通常、ドライバー固有のブロック I/O サブシステムのデータ構造を保護するためにカーネル ヘルパー関数に渡されます。gd フィールドは、このドライバーによって処理されるブロック デバイス (ディスク) 全体を記述する gendisk 記述子へのポインターです。

サブスクリプションマスター番号

デバイス ドライバーは、自身でメジャー デバイス番号をサブスクライブする必要があります。従来、この操作は register_blkdev() 関数を呼び出すことによって行われます。

err = register_blkdev(FOO_MAJOR,"foo");
if(err)goto error_major_is_busy;

この関数は、第 13 章の「デバイス番号の割り当て」セクションで説明した register_chrdev() 関数に似ています。メジャー デバイス番号 FOO_MAJOR を予約し、それにデバイス名 foo を割り当てます。同等の register_chrdev_region() 関数がないため、これではマイナー番号の範囲を割り当てることができず、さらに、サブスクライブされたメジャー番号とドライバーのデータ構造の間にリンクが確立されないことに注意してください。register_blkdev() 関数によって生じる唯一の目に見える効果は、/proc/devices 特殊ファイル内の登録されたメジャー デバイス番号のリストに新しいエントリが追加されることです。

カスタム記述子の初期化

ドライバーを使用する前に、foo 記述子のすべてのフィールドを適切に初期化する必要があります。
ブロックI /O サブシステムに関連するフィールドを初期化するために、デバイス ドライバーは主に次の操作を実行します
ドライバーは最初にスピン ロックを初期化し、次にディスク記述子を割り当てます。前に図 14-3 で示したように、gendisk 構造には他の多くのデータ構造が含まれるため、gendisk 構造はブロック I/O サブシステムで最も重要なデータ構造です。alloc_disk() 関数は、ディスク パーティション記述子を格納するための配列も割り当てます。この関数で必要なパラメータは、配列内の hd_struct 構造体の要素の数です。16 は、ドライバーが 16 個のディスクをサポートし、各ディスクに 15 個のパーティションを含めることができることを意味します (0 個のパーティションは使用されません)。

Gendisk記述子の初期化

次に、ドライバーは gendisk 記述子のいくつかのフィールドを初期化します:
foo.gd->private_data =&foo;
foo.gd->major = FOO_MAJOR;
foo.gd->first_minor = 0;
foo.gd->minors = 16;
set_capacity ( foo.gd,foo_disk_capacity_in_sectors);strcpy(foo.gd->disk_name,"foo");
foo.gd->fops =&foo_ops;
foo 記述子のアドレスは gendisk 構造体の private_data フィールドに格納されているため、ブロック I/ O サブシステムによってメソッドとして呼び出される低レベル ドライバー関数は、ドライバー記述子をすばやく見つけることができます。ドライバーが複数のディスクを同時に処理できる場合、このアプローチにより効率が向上します。set_capacity() 関数は、容量フィールドを 512 バイト セクターのディスク サイズに初期化します。この値は、ハードウェアをプローブしてディスク パラメーターを要求するときに決定されることもあります。

ブロックデバイス動作テーブルの初期化

gendisk 記述子の fops フィールドは、カスタム ブロック デバイス メソッド テーブルのアドレスに初期化されます (この章の前半の表 14-4 を参照) (注 3)。同様に、デバイス ドライバーの foo_ops テーブルには、デバイス ドライバー固有の関数が含まれています。たとえば、ハードウェア デバイスがリムーバブル ディスクをサポートしている場合、汎用ブロック層は media_changed メソッドを呼び出して、ブロック デバイスが最後にマウントまたは開かれてからディスクが変更されているかどうかを確認します。このチェックは通常、いくつかの低レベル コマンドをハードウェア コントローラーに送信することによって行われるため、各デバイス ドライバーによって実装される media_changed メソッドは異なります。同様に、ioctl メソッドは、ジェネリック ブロック層が ioctl コマンドの処理方法を知らない場合にのみ呼び出されます。たとえば、このメソッドは通常、ioctl() システム コールがディスク構成、つまりディスクで使用されるシリンダ、トラック、セクタ、およびヘッドの数をクエリするときに呼び出されます。したがって、各デバイス ドライバーによって実装される ioctl メソッドも異なります。

リクエストキューの割り当てと初期化

私たちの勇敢なデバイス ドライバー設計者は、処理を待っているリクエストを保持するリクエスト キューをセットアップします。次の手順を実行すると、リクエスト キューを簡単に作成できます。

foo.gd->rq = blk_init_queue(foo_strategy,&foo.lock);
if(!foo.gd->rq)goto error_no_request_queue;
blk_queue_hardsect_size(foo.gd->rd,foo_hard_sector_size);
blk_queue_max_sectors(foo.gd->rd,foo_max_sectors);
blk_queue_max_hw_segments(foo.gd->rd,foo_max_hw_segments);
blk_queue_max_phys_segments(foo.gd->rd,foo_max_phys_segments);

blk_init_queue() 関数は、リクエスト キュー記述子を割り当て、そのフィールドの多くをデフォルト値に初期化します。受け取るパラメータは、デバイス記述子のスピン ロックのアドレス (foo.gd->rq->queue_lock フィールド値) とデバイス ドライバーのポリシー ルーチンのアドレス (次のセクション「ポリシー ルーチン」を参照) (foo .gd- >rq->request_fn フィールド値)。
この関数は、foo.gd->rq->elevator フィールドも初期化し、ドライバーにデフォルトの I/O スケジューリング アルゴリズムの使用を強制します。デバイス ドライバーが別のスケジューリング アルゴリズムを使用したい場合は、後でエレベーター フィールドのアドレスをオーバーライドできます。次に、いくつかのヘルパー関数を使用して、リクエスト キュー記述子のさまざまなフィールドをデバイス ドライバーの特性値に設定します (同様のフィールドについては表 14-6 を参照)。

割り込みハンドラの設定

第 4 章の「I/O 割り込み処理」セクションで説明したように、デバイス ドライバーはデバイスの IRQ ラインを登録する必要があります。これは次のように行うことで実現できます。

request_irq(foo_irq,foo_interrupt, SA_INTERRUPTISA_SHIRQ,"foo",NULL);

foo_interrupt() 関数はデバイスの割り込みハンドラであり、その機能の一部については、この章の後半の「割り込みハンドラ」セクションで説明します。

ディスクを登録する

最後に、デバイス ドライバーのすべてのデータ構造の準備が整いました。初期化フェーズの最後のステップは、ディスクを「登録」してアクティブ化することです。これは、次の操作を実行するだけで簡単に実現できます: add_disk(foo.gd);
add_disk() 関数は、gendisk 記述子のアドレスをパラメーターとして受け取り、主に次の操作を実行します。

  1. gd->flags の GENHD_FL_UP フラグを設定します。
  2. kobj_map() を呼び出して、デバイス ドライバーとデバイスのメジャー番号 (関連する範囲のマイナー番号) との間の接続を確立します (第 13 章の「キャラクター デバイス ドライバー」セクションを参照してください。この場合、以下に注意してください。マッピング ドメインは bdev_map 変数で表されます)。
  3. デバイス ドライバー モデルの gendisk 記述子に kobject 構造体を、デバイス ドライバーが処理する新しいデバイス (/sys/block/foo など) として登録します。
  4. 必要に応じて、ディスク上のパーティション テーブルをスキャンし、見つかったパーティションごとに、foo.gd->part 配列内の対応する hd_struct 記述子を適切に初期化します。また、デバイス ドライバー モデルにパーティションを登録します (例: /sys/block/foolfool)。
  5. デバイス ドライバー モデルのリクエスト キュー記述子に埋め込まれた kobject 構造体を登録します (例: /sys/block/foo/queue)。add_disk() が返されると、デバイス ドライバーは動作する準備が整います。初期化関数が終了し、ポリシー ルーチンと割り込みハンドラーが、I/O スケジューラーによってデバイス ドライバーに渡された各要求の処理を開始します。

戦略ルーチン

ポリシー ルーチンは、ハードウェア ブロック デバイスと対話して、ディスパッチ キューにアセンブルされた要求を満たすブロック デバイス ドライバーの関数または関数のセットです。ストラテジ ルーチンは、前のセクションで紹介した foo_strategy() 関数など、リクエスト キュー記述子の request_fn メソッドを通じて呼び出すことができます。I/O スケジューラ層は、リクエスト キュー記述子のアドレス q をこの関数に渡します。前述したように、ポリシー ルーチンは通常、新しいリクエストが空のリクエスト キューに挿入された後に開始されます。ブロック デバイス ドライバーがアクティブ化されている限り、キュー内のすべての要求はキューが空になるまで処理される必要があります。ポリシー ルーチンの簡単な実装は次のとおりです。スケジューリング キュー内の各要素について、ブロック デバイス コントローラーと対話してリクエストを処理し、データ転送が完了するまで待機し、サービスされたリクエストをキューから削除して続行します。スケジュールを処理するためのキュー内の次のリクエスト。この実装はあまり効率的ではありません。データ転送に DMA を使用できると仮定しても、I/O 操作が完了するまでポリシー ルーチン自体を一時停止する必要があります。これは、ポリシー ルーチンが専用のカーネル スレッドで実行される必要があることを意味します (無関係なユーザー プロセスにペナルティを与えたくありません)。さらに、このようなドライバーは、複数の I/O データ転送を同時に処理できる最新のディスク コントローラーをサポートできません。したがって、多くのブロック デバイス ドライバーは次の戦略を採用しています。

  1. ポリシー ルーチンはキュー内の最初のリクエストを処理し、データ転送の完了時に割り込みを生成できるようにブロック デバイス コントローラーを設定します。その後、ポリシー ルーチンは終了します。
  2. ディスク コントローラが割り込みを生成すると、割り込みコントローラはポリシー ルーチンを再度呼び出します (通常は直接、場合によってはワーク キューをアクティブにすることによって)。ポリシー ルーチンは、現在のリクエストに対して別のデータ転送を開始するか、リクエストのすべてのデータ ブロックが転送されたら、ディスパッチ キューからリクエストを削除して次のリクエストの処理を開始します。
    リクエストは複数のバイオ構造で構成され、各バイオ構造は複数のセグメントで構成されます。基本的に、ブロック デバイス ドライバーは次の 2 つの方法で DMA を使用します。
  3. ドライバーは、要求された各バイオ構造の各セグメントにサービスを提供するために、さまざまな DMA 転送方法を確立します。
  4. ドライバーは、要求されたすべての BIOS のすべてのセグメントにサービスを提供するために、単一のスキャッター/ギャザー DMA 転送を確立します。
    最後に、デバイス ドライバー ポリシー ルーチンの設計は、ブロック コントローラーの特性に依存します。各物理ブロック デバイスには、他の物理ブロック デバイスとは異なる固有の特性があるため (たとえば、フロッピー ディスク ドライバーはトラック上のブロックを複数のトラックにグループ化し、単一の I/O 操作でトラック全体を転送します)、デバイスにとって重要です。各リクエストがどのように処理されるべきかについて一般的な仮定をするのはあまり意味がありません。

この例では、 foo_strategy() 戦略ルーチンは次のことを行う必要があります。

  1. I/O スケジューラの補助関数 elv_next_request() を呼び出して、ディスパッチ キューから現在のリクエストを取得します。ディスパッチ キューが空の場合は、このポリシー ルーチンを終了します。 req = elv_next_request(q);if(!req)return;
  2. blk_fs_request マクロを実行して、リクエストの REQ_CMD フラグが設定されているかどうか、つまりリクエストに標準の読み取りまたは書き込み操作が含まれているかどうかを確認します。
if(!blk_fs_request(req))
	goto handle_special_request;
  1. ブロック デバイス コントローラーがスキャッターギャザー DMA をサポートしている場合は、リクエスト全体のデータ転送を実行し、転送完了時に割り込みを生成するようにディスク コントローラーをプログラムします。blk_rq_map_sg() ヘルパー関数は、データ転送を開始するためにすぐに使用できるスキャッター/ギャザー リンク リストを返します。
  2. それ以外の場合、デバイス ドライバーはデータを 1 つずつ転送する必要があります。この場合、戦略ルーチンは 2 つのマクロ、rq_for_each_bio と bio_for_each_segment を実行して、それぞれのバイオのバイオ リンク リストとセグメント リンク リストを横断します。
rq_for_each_bio(bio,rq)
bio_for_each_segment(bvec,bio,i){
/* transfer the i-th segment bvec */
local_irq_save(flags);
addr = kmap_atomic(bvec->bv_page,KM_BIO_SRC_IRQ);
foo_start_dma_transfer(addr+bvec->bv_offset,bvec->bv_len);
kunmap_atomic(bvec->bv_page,KM_BIO_SRC_IRQ);
local_irq_restore(flags);

転送するデータがハイエンド メモリにある場合は、kmap_atomic() 関数と kunmap_atomic() 関数が必要です。foo_start_dma_transfer() 関数は、DMA データ転送を開始し、I/O 操作の完了時に割り込みを生成するようにハードウェア デバイスをプログラムします。
5. 戻ります。

割り込みハンドラ

ブロック デバイス ドライバーの割り込みハンドラーは、DMA データ転送の終了時にアクティブ化されます。要求されたすべてのデータ ブロックが転送されたかどうかをチェックします。そうである場合、割り込みハンドラーはポリシー ルーチンを呼び出して、ディスパッチ キュー内の次の要求を処理します。それ以外の場合、割り込みハンドラーは要求記述子の対応するフィールドを更新し、ポリシー ルーチンを呼び出して保留中のデータ転送を処理します。デバイス ドライバー foo の割り込みハンドラーの一般的なスニペットは次のとおりです。

irqreturn_t foo_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
	struct foo_dev_t *p =(struct foo_dev_t *)dev_id;
	struct request_queue *rq= p->gd->rq;
	[...]
	if(!end_that_request_first(rq,uptodate,nr_sectors)){
		blkdev_dequeue_request(rq);
		end_that_request_last(rq);
	}
	rq->request_fn(rq);
	[...]
	return IRQ_HANDLED;
}

2 つの関数 end_that_request_first() と end_that_request_last() は、リクエストを終了するタスクを共有します。end_that_request_first() 関数によって受信されるパラメータは、要求記述子、DMA データ転送の正常な完了を示すフラグ、および DMA によって転送されたセクタ数です (end_that_request_chunk() 関数は、関数が転送されたバイト数。セクタ数ではありません)。基本的に、リクエスト内のバイオ構造と各バイオのセグメントをスキャンし、リクエスト記述子のフィールド値を次のように更新します。

  1. リクエスト内の最初の未解決のバイオ構造を指すようにバイオ フィールドを変更します。
  2. 最初の未完成セグメントを指すように、未完成のバイオ構造の bi_idx フィールドを変更します。
  3. 未完了セグメントの bv_offset フィールドと bv_len フィールドを変更して、まだ送信する必要があるデータを指定します。この関数は、データ転送が完了した各バイオ構造上で bio_endio() 関数も呼び出します。end_that_request_first() は、リクエスト内のすべてのデータ ブロックが送信された場合は 0 を返し、それ以外の場合は 1 を返します。戻り値が 1 の場合、割り込みハンドラーはポリシー ルーチンを再度呼び出して、リクエストの処理を続行します。それ以外の場合、割り込みハンドラーはリクエストをリクエスト キューから削除し (主に blkdev_dequeue_request() によって行われます)、次に end_that_request_last() ヘルパー関数を呼び出し、ポリシー ルーチンを再度呼び出してディスパッチ キュー内の次のリクエストを処理します。end_that_request_last() 関数の機能は、一部のディスク使用量統計を更新し、I/O スケジューラー rq->elevator のスケジューリング キューから要求記述子を削除し、要求記述子の完了を待っているスリープ状態のプロセスをウェイクアップして、解放することです。その記述子を削除しました。

ブロックデバイスファイルを開く

この章は、ブロック デバイス ファイルが開かれたときに VFS によって実行される操作について説明して終了します。ファイル システムがディスクまたはパーティションにマップされるたび、スワップ パーティションがアクティブ化されるたび、およびユーザー モード プロセスがブロック デバイス ファイルに対して open() システム コールを発行するたびに、カーネルはブロック デバイス ファイルを開きます。すべての場合において、カーネルは基本的に同じ操作を実行します。つまり、ブロック デバイス記述子を探し (ブロック デバイスが使用されていない場合は新しい記述子を割り当て)、次のデータ転送用のファイル操作メソッドを設定します。第 13 章の「デバイス ファイルの VFS 処理」セクションで、デバイス ファイルが開かれるときに dentry_open() 関数がファイル オブジェクトのメソッドをカスタマイズする方法について説明しました。その f_op フィールドはテーブル def_blk_fops のアドレスに設定されます。その内容は表 14-10 に示されています。
ここに画像の説明を挿入します
ここに画像の説明を挿入します
dentry_open() 関数によって呼び出される open メソッドのみを考慮します。blkdev_open() は、インデックス ノードとファイル オブジェクトのアドレスをそれぞれ格納する inode と filp をパラメータとして受け取ります。この関数は基本的に次の操作を実行します。

  1. bd_acquire(inode) を実行して、ブロックデバイス記述子 bdev のアドレスを取得します。この関数は、インデックス ノード オブジェクトのアドレスを受け取り、次の主な手順を実行します:
    a. インデックス ノード オブジェクトの inode->i_bdev フィールドが NULL かどうかを確認し、NULL であれば、ブロック デバイス ファイルが開かれていることを示します。このフィールドには、対応するブロック記述子のアドレスが格納されます。この場合、ブロックデバイスに関連付けられた bdev 特殊ファイルシステムの inode->i_bdev->bd_inode インデックス ノードの参照カウンタをインクリメントし、記述子のアドレス inode->i_bdev を返します。
    b. ブロックデバイスファイルが開かれていません。ブロック デバイス ファイル (この章の「ブロック デバイス」セクションを参照) のメジャー デバイス番号とマイナー デバイス番号に従って、 bdget(inode->i_rdev) を実行してブロック デバイス記述子のアドレスを取得します。記述子が存在しない場合、 bdget() は記述子を割り当てますが、他のブロック デバイス ファイルがブロック デバイスにアクセスしているなど、記述子がすでに存在している可能性があることに注意してください。
    c. ブロック デバイス記述子のアドレスを inode->i_bdev に保存して、同じブロック デバイス ファイルを開く操作を高速化します。
    d. inode->i_mapping フィールドを、bdev インデックス ノードの対応するフィールドの値に設定します。このフィールドはアドレス空間オブジェクトを指します。これについては、第 15 章の「address_space オブジェクト」セクションで説明します。
    e. bdev->bd_inodes によって確立されたブロック デバイス記述子の開いたインデックス ノード リストにインデックス ノードを挿入します。
    f. 記述子 bdev のアドレスを返します。
  2. filp->i_mapping フィールドを inode->i_mapping の値に設定します (上記のステップ 1d を参照)。
  3. このブロック デバイスに関連する gendisk 記述子のアドレスを取得します: disk = get_gendisk(bdev->bd_dev,&part); 開かれたブロック デバイスがパーティションの場合、返されたインデックス値はローカル変数の part に格納されます; それ以外の場合、part はローカル変数の0. get_gendisk() 関数は、kobject マップ ドメイン bdev_map で kobj_lookup() を呼び出し、デバイスのメジャー番号とマイナー番号を渡すだけです (この章の前半の「デバイス ドライバーの登録と初期化」セクションを参照)。
  4. bdev->bd_openers の値が 0 に等しくない場合は、ブロック デバイスがオープンされていることを示します。bdev->bd_contains フィールドを確認します:
    a. 値が bdev と等しい場合、ブロック デバイスはディスク全体です: ブロック デバイス メソッド bdev->bd_disk->fops->open (定義されている場合) を呼び出してから、 bdev->bd_invalidated フィールドで、必要に応じて rescan_partitions() 関数を呼び出します (後述のステップ 6a および 6c を参照)。
    b. bdev と等しくない場合、ブロックデバイスはパーティションです: bdev->bd_contains->bd_part_count カウンタの値を増やします。その後、ステップ 8 に進みます。
  5. ここでブロックデバイスに初めてアクセスします。bdev->bd_disk を gendisk 記述子のアドレスディスクに初期化します。
  6. ブロック デバイスがディスク全体 (部分が 0) の場合、次のサブステップを実行します:
    a. ディスク -> fops -> オープン ブロック デバイス メソッドが定義されている場合、それを実行します: このメソッドはブロック デバイス ドライバーによってカスタマイズされます。特定の最後の初期化を実行します。
    b. ディスク->キュー要求キューのhardsect_sizeフィールドからセクター サイズ(バイト数)を取得し、この値を使用して bdev->bd_block_size および bdev->bd_inode->i_blkbits フィールドを適切に設定します。また、bdev->bd_inode->i_size フィールドに、disk->capacity から計算されたディスク サイズを設定します。
    c. bdev->bd_invalidated フラグが設定されている場合は、 rescan_partitions() を呼び出してパーティション テーブルをスキャンし、パーティション記述子を更新します。このフラグは、check_disk_change ブロック デバイス メソッドによって設定され、リムーバブル デバイスにのみ適用されます。
  7. それ以外のブロック デバイスがパーティションである場合は、次のサブステップを実行します:
    a. bdget() を再度呼び出して (今回は、disk->first_minor マイナー デバイス番号を渡します)、ディスク全体のブロック記述子アドレスを取得します。
    b. ディスク全体のブロックデバイス記述子に対して手順 3 ~ 6 を繰り返し、必要に応じて記述子を初期化します。
    c. bdev->bd_contains をディスク記述子全体のアドレスに設定します。
    d. ディスク パーティションでの新しいオープン操作を考慮して、whole->bd_part_count の値を増やします。
    e. bdev->bd_part に、disk->part[part-1] の値を設定します (これは、パーティション記述子 hd_struct のアドレスです)。同様に、kobject_get(&bdev->bd_part->kobj)を実行して、パーティション参照カウンタの値を増やします。
    f. ステップ 6b と同様に、パーティション サイズとセクター サイズを表すフィールドをインデックス ノードに設定します。
  8. bdev->bd_openers カウンタの値を増やします。
  9. ブロック デバイス ファイルが排他的に開かれている場合 (filp->f_flags の O_EXCL フラグが設定されている場合)、 bd_claim(bdev,filp) を呼び出してブロック デバイスのホルダーを設定します (この章の前半の「ブロック デバイス」セクションを参照)。 。エラーの場合、ブロック デバイスにはすでに所有者がいます。ブロック デバイス記述子は解放され、エラー コード -EBUSY が返されます。
  10. 終了するには 0 (成功) を返します。

blkdev_open() 関数が終了すると、open() システム コールは通常どおり続行されます。今後、オープン ファイルに対して発行される各システム コールは、デフォルトのブロック デバイス ファイル操作をトリガーします。第 16 章で説明するように、ブロック デバイスへの各データ転送は、一般ブロック層にリクエストを送信することで効率的に実行できます。

おすすめ

転載: blog.csdn.net/x13262608581/article/details/132353858