デバイスコントローラ#
デバイス間の違いを隠すために、各デバイスにはデバイスコントローラ(Device Control)と呼ばれるコンポーネントがあります。例えば、ハードディスクにはハードディスクコントローラ、ディスプレイにはビデオコントローラがあります。
コントローラには三種類のレジスタがあり、それぞれ ** ステータスレジスタ(Status Register)、コマンドレジスタ(Command Register)、およびデータレジスタ(Data Register)** です。以下の図のようになります:
これら三つのレジスタの役割:
- データレジスタ:CPU が I/O デバイスに転送する必要があるデータを書き込みます。例えば、印刷する内容が「Hello」の場合、CPU は最初に H 文字を対応する I/O デバイスに送信します。
- コマンドレジスタ:CPU がコマンドを送信し、I/O デバイスに入力 / 出力操作を行うよう指示します。これにより I/O デバイスが作業を開始し、タスクが完了すると、ステータスレジスタ内の状態が完了としてマークされます。
- ステータスレジスタ:CPU に現在作業中または作業が完了したことを知らせる役割があります。作業中の場合、CPU がデータやコマンドを送信しても無駄であり、前の作業が完了し、ステータスレジスタが完了としてマークされるまで、CPU は次の文字やコマンドを送信できません。
CPU はデバイスコントローラ内のレジスタを読み書きすることでデバイスを制御します。これは CPU が直接入力出力デバイスを制御するよりも便利で標準的です。
さらに、入力出力デバイスは二つの大きなカテゴリに分けられます:ブロックデバイス(Block Device)とキャラクタデバイス(Character Device)。
- ブロックデバイス:データを固定サイズのブロックに保存し、各ブロックには独自のアドレスがあります。ハードディスクや USB は一般的なブロックデバイスです。
- キャラクタデバイス:文字単位で文字ストリームを送受信します。キャラクタデバイスはアドレス指定ができず、尋ねる操作もありません。マウスは一般的なキャラクタデバイスです。
ブロックデバイスは通常、大量のデータを転送するため、コントローラは読み書き可能なデータバッファを設けています。
- CPU がコントローラのバッファにデータを書き込むと、バッファ内のデータが一定量に達した時点でデバイスに送信されます。
- CPU がコントローラのバッファからデータを読み取る際も、バッファが一定量に達した時点でメモリにコピーされます。
これはデバイスへの頻繁な操作を減らすためです。
では、CPU はどのようにデバイスの制御レジスタやデータバッファと通信するのでしょうか?二つの方法があります:
- ポート I/O:各制御レジスタには I/O ポートが割り当てられ、特別なアセンブリ命令を使用してこれらのレジスタを操作できます。例えば、in/out のような命令です。
- メモリマッピング I/O:すべての制御レジスタをメモリ空間にマッピングすることで、メモリを読み書きするようにデータバッファを読み書きできます。
I/O 制御方式#
割り込み方式は、頻繁にデータを読み書きするディスクにはあまり適しておらず、CPU が頻繁に中断され、大量の時間を消費する可能性があります。この種のデバイスの問題を解決する方法は、DMA(Direct Memory Access)機能を使用することです。これにより、CPU が関与せずにデバイスが自動的に I/O データをメモリに配置できます。DMA 機能を実現するには「DMA コントローラ」ハードウェアのサポートが必要です。
デバイスドライバ#
デバイスコントローラは多くの詳細を隠しますが、各デバイスのコントローラのレジスタ、バッファなどの使用方法は異なります。したがって、「デバイスコントローラ」の違いを隠すために、デバイスドライバが導入されました。
デバイスコントローラはオペレーティングシステムの範疇には属さず、ハードウェアに属しますが、デバイスドライバはオペレーティングシステムの一部です。オペレーティングシステムのカーネルコードは、ローカル呼び出しコードのようにデバイスドライバのインターフェースを使用できます。デバイスドライバはデバイスコントローラ向けのコードであり、デバイスコントローラを操作する命令を発信した後にのみ、デバイスコントローラを操作できます。
異なるデバイスコントローラは機能が異なりますが、デバイスドライバはオペレーティングシステムに統一されたインターフェースを提供します。これにより、異なるデバイスドライバは同じ方法でオペレーティングシステムに接続できます。
前述の通り、割り込みに関する多くのことがありました。デバイスが作業を完了すると、オペレーティングシステムに通知するために割り込みを送信します。オペレーティングシステムはこの割り込みを処理する場所が必要であり、その場所はデバイスドライバ内にあります。デバイスドライバはコントローラからの割り込み要求に迅速に応答し、この割り込みの種類に応じて対応する割り込み処理プログラムを呼び出して処理します。
一般的なブロック層#
ブロックデバイスに対して、異なるブロックデバイスの違いによる影響を減らすために、Linux は統一された一般的なブロック層を通じて異なるブロックデバイスを管理します。
一般的なブロック層はファイルシステムとディスクドライバの間に位置するブロックデバイスの抽象層であり、主に二つの機能があります:
- 一つ目の機能は、上位のファイルシステムやアプリケーションに対してブロックデバイスへの標準インターフェースを提供し、下位ではさまざまな異なるディスクデバイスを統一されたブロックデバイスに抽象化し、カーネルレベルでこれらデバイスのドライバを管理するフレームワークを提供します。
- 二つ目の機能は、一般的な層がファイルシステムやアプリケーションからの I/O 要求をキューに入れ、次にキューを再整理し、要求を統合するなどの方法、つまり I/O スケジューリングを行います。主な目的はディスクの読み書き効率を向上させることです。
Linux メモリは 5 種類の I/O スケジューリングアルゴリズムをサポートしており、それぞれは次の通りです:
- スケジューリングアルゴリズムなし:ファイルシステムやアプリケーションの I/O に対して何の処理も行いません。このアルゴリズムは仮想マシン I/O でよく使用され、ディスク I/O スケジューリングアルゴリズムは物理マシンシステムに委ねられます。
- 先入先出スケジューリングアルゴリズム
- 完全公平スケジューリングアルゴリズム:各プロセスの I/O スケジューリングキューを維持し、時間スライスに従って各プロセスの I/O 要求を均等に分配します。
- 優先度スケジューリング
- 最終期限スケジューリングアルゴリズム:読み取りおよび書き込み要求のために異なる I/O キューを作成し、機械ディスクのスループットを向上させ、最終期限の要求が優先的に処理されるようにします。これは I/O 負荷が高いシナリオ、例えばデータベースなどに適しています。
ストレージシステムの I/O ソフトウェア層#
Linux ストレージシステムの I/O は上から下に三つの層に分けることができ、ファイルシステム層、一般的なブロック層、デバイス層です。これらの層の関係は以下の図のようになります:
これら三つの層の役割は:
- ファイルシステム層:仮想ファイルシステムや他のファイルシステムの具体的な実装を含み、上位のアプリケーションに対して標準的なファイルアクセスインターフェースを提供し、下位では一般的なブロック層を通じてディスクデータを保存および管理します。
- 一般的なブロック層:ブロックデバイスの I/O キューと I/O スケジューラを含み、ファイルシステムの I/O 要求をキューに入れ、次に I/O スケジューラを通じて次の層のデバイス層に I/O を選択して送信します。
- デバイス層:ハードウェアデバイス、デバイスコントローラ、およびドライバを含み、最終的な物理デバイスの I/O 操作を担当します。
ファイルシステムインターフェースがあれば、ファイルシステムのコマンドラインを通じてデバイスを操作するだけでなく、アプリケーションを通じて read、write 関数を呼び出し、ファイルを読み書きするようにデバイスを操作できます。したがって、Linux ではデバイスも特別なファイルに過ぎません。
しかし、読み書き操作に加えて、特定のデバイスの機能や属性を確認する必要があります。したがって、ioctl インターフェースが必要です。これは入力出力制御インターフェースを表し、特定のデバイス属性を構成および変更するための一般的なインターフェースです。
さらに、ストレージシステムの I/O はシステム全体で最も遅い部分であるため、Linux は I/O の効率を向上させるために多くのキャッシュメカニズムを提供しています。
- ファイルアクセスの効率を向上させるために、ページキャッシュ、インデックスノードキャッシュ、ディレクトリエントリキャッシュなどのさまざまなキャッシュメカニズムを使用し、ブロックデバイスへの直接呼び出しを減らすことを目的としています。
- ブロックデバイスのアクセス効率を向上させるために、バッファを使用してブロックデバイスのデータをキャッシュします。
キーボードで文字を入力するとき、何が起こるのか?#
CPU 内部のメモリインターフェースは、システムバスと直接通信し、その後システムバスは I/O ブリッジに接続されます。この I/O ブリッジは、もう一方の端でメモリバスに接続され、CPU とメモリが通信します。さらに、もう一方の端は I/O バスに接続され、キーボードやディスプレイなどの I/O デバイスを接続します。
ユーザーがキーボードの文字を入力すると、キーボードコントローラはスキャンコードデータを生成し、それをキーボードコントローラのレジスタにバッファリングします。次に、キーボードコントローラはバスを介して CPU に割り込み要求を送信します。
CPU が割り込み要求を受け取ると、オペレーティングシステムは中断されたプロセスの CPU コンテキストを保存し、次にキーボードの割り込み処理プログラムを呼び出します。
キーボードの割り込み処理プログラムは、キーボードドライバが初期化される際に登録されます。このキーボード割り込み処理関数の機能は、キーボードコントローラのレジスタのバッファからスキャンコードを読み取り、スキャンコードに基づいてユーザーがキーボードで入力した文字を見つけることです。入力された文字が表示文字であれば、スキャンコードを対応する表示文字の ASCII コードに翻訳します。例えば、ユーザーがキーボードで入力したのが A 文字であれば、それは表示文字であり、スキャンコードを A 文字の ASCII コードに翻訳します。
表示文字の ASCII コードを取得した後、それを「読み取りバッファキュー」に格納します。次に、表示文字を画面に表示する必要があります。表示デバイスのドライバは定期的に「読み取りバッファキュー」からデータを読み取り、「書き込みバッファキュー」に格納し、最終的に「書き込みバッファキュー」のデータを一つずつ表示デバイスのコントローラのレジスタ内のデータバッファに書き込み、最後にこれらのデータを画面に表示します。
結果が表示された後、中断されたプロセスのコンテキストを復元します。