Assyのリベラル文学研究所もご覧ください。

Linuxカーネル

Linuxカーネル

Linuxカーネルについて書きたいと思う。

マルチタスク

LinuxマルチタスクのOS

まず、LinuxカーネルマルチタスクなOSのカーネルである。
マルチタスクとは、「複数のプログラムを同時に並列実行できる」ということ。

プロセスとスレッド

ここで、プログラムにはプロセスとスレッドがあることが重要である。
プロセスは、それぞれに個別の独立したメモリ領域が与えられる。
それに対して、スレッドはプロセスの中に存在する並列処理であり、
同じメモリ領域を参照することができる。
このため、マルチスレッド環境では、排他制御を行う「ロック」が肝心となる。
ロックを適切に行うことで、スレッドセーフな関数を作らなければならない。
プロセスにおいても、プロセス間通信(IPC)と呼ばれる機構を使うことで、
メモリを共有したり、簡単な排他制御を行う(セマフォ)ことが可能となっている。

コンテキスト切り替え

プログラムがもしあったとして、それをマルチタスクで実行するためにどうするか。
プログラムは、実際、機械語でCPU命令とメモリアドレスが記述されているため、
単純に考えれば、CPUとメモリがそれぞれのプログラムに必要となる。
しかしながら、マルチタスクのOSでは、
プログラムの命令を、とても小さな時間で切り替えながら実行することで、
CPUの仮想化を実現する。
つまり、プログラムの実行しているレジスタの内容や、プログラムカウンタなどを、
task_structと呼ばれる構造体に退避させて、
プログラムを「安全に停止させる」ことができればいい。
この上で、プログラムを次々に停止・再実行する。
これを「コンテキスト切り替え」と呼ぶ。

プロセスディスクリプタ

カーネルはプロセスを管理するために、「プロセスディスクリプタ」と呼ばれるプロセスの情報によってプロセスを管理する。
カーネルがプロセスの実行を停止すると、CPUレジスタ内のさまざまな情報がプロセスディスクリプタの中に退避される。
この情報には、プログラムカウンタや汎用レジスタ浮動小数レジスタ、プロセッサ制御レジスタ、メモリ管理レジスタなどが含まれる。
カーネルがプログラムの再開を決めると、プロセスディスクリプタに退避された情報を使って、停止されたCPUレジスタを復旧する。

仮想アドレス空間

また、メモリ領域については、
カーネルがハードウェアのMMUという機構を上手く使うことで、
プログラムが実際のメモリ領域(論理アドレスやリニアアドレスと呼ぶ)にアクセスした段階で、
カーネルがそれを物理アドレスに翻訳する。
このようにすることで、カーネルはメモリ領域を全てのプログラムに「個別に与える」ことができる。
このような仕組みを「仮想アドレス空間」と呼ぶ。

ページアドレッシング

Linuxはメモリアドレスをページと呼ばれる小さな単位で管理している。
Linuxでは効率化と使用メモリ領域の削減のために、
三段階のページアドレッシングを採用しており、
大きなものから順に、
ページグローバルディレクトリがあり、
その中にページミドルディレクトリがあり、
その中にページテーブルがあり、
ページテーブルにおいて実際のページ領域をマッピングする。

アドレス空間の構造

アドレス空間には、
・テキスト領域(機械語のプログラムコードが入る)
・ヒープ領域(mallocが管理する)
・スタック領域(関数の引数やローカル変数)
などがあり(ほかにデータ領域やBSS領域がある)、プロセスが作られると割り当てられる。
基本的に、プロセスを実行すると、そのプロセスに対してテキスト領域、ヒープ領域、スタック領域が割り当てられることを知っておけば良い。

割り込み

カーネルは、IOデバイスにアクセスする時点で、
IOデバイスの処理の終了を待機するが、
IOデバイスはプログラムの命令よりも動作が遅いので、
いちいちIOデバイス処理を待っていたのでは効率が悪い。
そのため、カーネルはIOデバイス処理を待つ間に、
必要とあれば別の処理を行う。
そして、IOデバイスの処理が終わった段階で、
カーネルに対して「割り込み」を行う。
割り込まれた時、カーネルはIOデバイスの処理の完了を知り、
その後の命令を実行する。

デバイスドライバ

実際のところ、カーネルはデバイスコントローラの詳細を
デバイスドライバという小さなソフトウェアモジュールによって知っている。
カーネルにおいて、実際のデバイスの詳細を知っているのは、
デバイスドライバだけである。
バイスには、キャラクタ型デバイスとブロック型デバイスがある。
キャラクタ型デバイスは、その時その時の状況を、
バイト列(文字列)によって知らせるようなデバイスであり、
システムコールのストリームを通じて操作できる。
ブロック型デバイスは、内部にファイルシステムが存在するデバイスであり、
ファイル処理APIを通じてアクセスする。
キャラクタ型デバイスは、ランダムアクセス(好きな時に好きな場所のデータを知ること)ができないが、
ブロック型デバイスは、ランダムアクセスができる。

ファイルシステム

Linuxカーネルでは、モノリシックカーネルであるため、
ファイルシステム処理がカーネルに統合されている。
ブロック型ファイルシステムext2ext4が標準的に使われるが、
B-Tree型のファイルシステムであるReiserFS, XFS, JFSなども使うことができる。
また、次世代の(おそらくZFSを超える)ファイルシステムであるBtrfsも開発されている。
LinuxカーネルではVFSの仕組みにより、
ファイルシステムが変わってもシステムコールAPIは変わらない。
どのファイルシステムにも、同じようにプログラムからアクセスできる。
また、ジャーナリングに対応したファイルシステムでは、
一度マシンを強制終了しても、ファイルやデータが消え去ることがない。
また、内部的にはi-nodeと呼ばれるインデックス処理を行っている。
全てのファイルにはi-nodeから参照できる。

ネットワーク

Linuxカーネルはネットワークをサポートしており、
BSDソケットインターフェースからネットワーク通信ができる。
BSDソケットは、互いに2つのソケットが繋がると有効になり、
片方に書き込むと片方から読み込むことができる。
TCPUDPもサポートされている。
TCPでは、再送制御などの多くの機能がある。
UDPには、そうした機能がない代わり、ビデオや音声を送ったりする時に、
誤りを無視して高速に通信することができる。
BSDソケットは多くの場合サーバー・クライアントシステムであり、
片方のサーバが待ち受けをし、そのサーバに対してクライアントが接続する。

入出力とシステムコール

また、Linuxでは端末を用いた入出力が可能である。
端末のデバイスファイルは、多くの場合/dev/tty1が第一の仮想コンソールである。
Linuxにおいては、標準入力、標準出力、標準エラー出力
多くの場合プロセスに与えられるが、
このほかにも、openで開いたファイルやソケットなどにも文字列を入出力できる。
システムコールについては、open(), close(), write(), read()が、
ファイルに対しての読み書きとして使える。
ファイルディスクリプタと呼ばれる整数が、それぞれのストリームの識別に使われる。
しかしながら、多くの場合、システムコールを使うよりも、
C言語のstdioというライブラリを使った方が高速であり、また便利である。
システムコールに比べて、stdioはバッファリングを行うため、
何度も同じデータにアクセスする時の速度が高速になる。
また、システムコールのread()やwrite()は固定長の読み書きを行うが、
stdioは文字単位や行単位の読み書きが可能であり、
printfのようなフォーマット出力と呼ばれる便利な仕組みが存在する。
stdioではファイルディスクリプタを生に触るのではなく、
FILE型のポインタと呼ばれるラッパーを使う。

シグナル

プログラムを強制終了するためには、シグナルを使う。
シグナルにはいくらかの種類があるが、
カーネルがシグナルを処理する際のデフォルトの挙動は、
「無視する」「強制終了」「強制終了してコアダンプを吐く」「一時停止する」「一時停止を再開する」の5種類。
しかしながら、シグナルはプロセス側で処理を書き換えることができる。
これを「シグナルを捕捉する」あるいは「トラップする」と言う。
シグナルを上手くプロセス側で捕捉することで、
デフォルトで無視されるシグナルも役に立つのである。

カーネルシステムコールを待ち受けるだけ

このようなカーネルをどのように開発するか、という問題だが、
Linuxカーネルは、基本的にシステムコールを待ち受けるだけの
イベント駆動なOSである。
よって、Linuxカーネルは自分からは何もしない。
何かの要求があった時にだけ、働くように作られている。

開発には英語力が必要

Linuxカーネルの開発には英語力が必要である。
カーネル内部のコメントも、
メンテナンスされているドキュメントも、
全て英語で書かれている。
この英語が読めれば、きちんと開発できるようになる。