わたしの名はフレイ 作家・デザイナー見習い
神々とともに生きる詩人 一等星シリウスの導きを信じて

Linuxカーネル開発

OSとしての特徴

Linuxは基本的にコマンドラインのOSだが、その特徴として、MS-DOSなどのパソコン向けOSでは使えない、高機能で高度なコマンドラインシェルを使うことが出来る。

また、コマンドシェル・インターフェースは、ネットワークを通して使うことも出来る。

UNIX環境のため、端末として使うコンピュータ、ログインするコンピュータ、ファイル管理をするコンピュータなどを分けて構築することも、比較的容易である。

自動化も比較的容易で、シェルスクリプトスクリプト言語のように、コマンドやシステムコール・Cや他言語の関数などをスクリプト形式で実行することも出来る。

簡単なコマンドなら、EmacsVimのようなテキストエディタからも、コマンドを実行することが出来る。

また、最近はGUIのデスクトップ環境を使って、Windowsなどのパソコン向けOSと同等かそれ以上のデスクトップ機能を使うことも出来る。この場合、X11サーバーを最初に起動し、クライアントのGUIアプリケーションをXサーバーと通信させ、Xlibに対応したツールキットライブラリ(GTK+やQt)とともに使うことになる。

この場合、Windowsとは違って、さまざまなインターフェースを独自に開発することが出来る。代表例は、GNOMEKDEである。

そして、オープンソースで開発されているアプリケーションは、Microsoft製品やAdobe製品などに勝るとも劣らない機能を持っている。(だが、操作性の違いにより戸惑う部分や、未完成で上手く動かないことも多い。)

カーネルを読もう

カーネルでは、構造体とポインタ、そして膨大な関数によって、それぞれ局所的にとても複雑な計算をしています。

ですが、基本的に存在するのはif文とfor文であり、そこにさまざまな関数の呼び出しと構造体の数値の代入が行われています。

読み解くのにはとても技術力の基本力が必要だと思いますが、そんなに高いハードルではないと僕は思います。

C言語のプログラムをどのように書いていくか、ということの参考にもなるので、頑張って読もうとチャレンジしても僕は良いと思います。

Linux上のミドルウェア

LAMPスタックのようにLinux上で、スクリプト言語インタープリタを実行することがありますが、この時、それらのインタープリタは単なるプロセスです。

こうした設計を、コンピュータ業界では「低レベルレイヤー」と「高レベルレイヤー」に分けて考えます。

ですが、単純に、Linuxカーネル上でPerlPHPが動き、そのPerlPHPを利用してそれぞれの掲示板やWebサービスのプログラムが動いていると考えると良いでしょう。

Linuxカーネルは、Cコンパイラであるgccなどでコンパイルされたバイナリであれば、Linuxカーネル上でそのまま動かせますが、JavaPerlPHPのような「間に抽象レイヤーが必要な言語」では、そのソフトウェア(ミドルウェア)を実行した上で、そのミドルウェアの上でプログラムが実行されます。最近ではC/C++でプログラムを作ることが次第に少なくなってきており、JavaPythonなどのミドルウェアLinuxでも良く使われています。

こうした「低レベル」「高レベル」は、言語以外に、X11GUIシステムにも見られます。X11の上にGTKやQtの階層があり、その上でGNOMEKDEが動き、その上でGUIアプリケーションが動きます。LinuxGUIアプリケーションを使う時は、こうした抽象レイヤーの階層図を理解しておく必要があります。

Linuxカーネルは高パフォーマンス

Linuxカーネルの特徴は、なんといってもその高パフォーマンス。商用のUNIXにも勝るほどの効率と性能で、プロセスやメモリを効率的かつ高速で処理する。また、安定性にも優れている。

FreeBSDなども高パフォーマンスであることで知られるが、Linuxカーネルの高パフォーマンスは、リーナスの高い技量と多くのコミュニティの人員が豊富であることが挙げられる。

だが、人員のスキルだけではなく、技術的に高度である。僕も分からないが、詳細を知りたい方は「詳解Linuxカーネル」に詳しいことが書いてある。Windowsなんか、サーバーには使えない。Linuxとは比較にならないほど、Windowsのレベルは低い。

Linuxカーネルがしっかりしているからきちんと動く

Linuxは、リーナス・トーバルズが作っているカーネルがしっかりしているから、きちんと動く、ということが言えます。

どんなアプリケーションであれ、OSの上で動くプログラムは、全て、カーネルの上で動いています。

Linux上のシステムは、Apacheであれ、Perlであれ、GCCであれ、カーネルの上で動いています。

システムコールについては、カーネルでは基本的なものしか実装しておらず、多くがlibcなども介して動いています。Perlなどであれば、インタープリタの上で動いています。

ですが、Linuxカーネルがきちんとしているせいで、それらはとてもきちんと安定して動くのです。

Linuxは、マルチタスク・マルチユーザのOSをUNIXを模倣して作りましたが、ここで「UNIXを模倣した」というのがポイントです。リーナスは、単なるUNIXの再実装を開発しただけにすぎません。ですが、UNIXという巨大な遺産を継承したことで、簡単に「一流に動くもの」を作ることができたのです。

ですが、最近はWindowsも頑張っています。Windows NTのNTカーネルを再実装してから、Windowsはあまりフリーズせず、不安定でなくなりました。

なぜ、Windows 9xはそんなに不安定だったのでしょうか。

その理由は、Windowsという製品そのものが、MS-DOSをベースとしており、そのMS-DOSが、他社のCP/Mの互換OSを入手して改良した、「後付けの改良・改造を施したOS」だったからです。

他人が開発したソフトウェアを、別の人間が改造して、ウィンドウシステムのような巨大な機能を付け加えることは、とても難しいことです。Windows Meが不安定だったのは、それにマルチメディア機能などたくさんの機能を付け加えすぎたせいだと思います。

また、Windows 9xは一定の基準や標準が無く、Microsoftが独自に仕様を定めた結果、適当かついいかげんに仕様が膨らんでしまい、DOSのおかしなAPIと相まって、巨大で複雑なカーネルとなってしまったのです。

反面、Linuxカーネルは、リーナス・トーバルズが一から作ったカーネルを、ネット上のみんなで開発したOSです。リーナスが天才だっただけではなく、UNIXの優れたエンジニアが「自ら貢献しようという積極的な動機を持って改良」したのです。彼らは、素晴らしい仕事を「ネット上の協力」を通して行いました。リーナスが、「それが僕には楽しかったから」といったことを言ったのも、彼らとの協力そのものが楽しかったからであり、リーナスだけではなく、参加者が全員楽しかったのだと思います。

また、Linuxカーネルの上で動くコンパイラMinix本を読もう
リーナスは確かに偉大な天才ですが、誰にもできないことをしたわけではありません。

彼は、タネンバウム教授のMinix本を読んで、その通り、Minixを改良して、Minixと同じものを作りながら、Minixを改良したコードだけで動くようになり、最終的にインターネットに発表し、全てのMinixコンポーネントGNUコンポーネントに置き換えて、独自の商用利用を禁じたライセンスはGPLに変更し、さまざまな人の改良パッチを取り入れただけだからです。

そういうわけで、タネンバウム教授のMinix本を読めば、きっとリーナスと同じことは誰でもできるでしょう。

僕も、そろそろMinix本を読むぐらいのレベルに来ているような気がします。家に第二版があるので、そのうち読もうと思っています。

ネットでは、Minix本はOSの仕組みを理解したい人というよりは、OSを作りたい人向けだと言います。やコマンドツールはGNU Projectが開発していますが、彼らのソフトウェアは既にUNIXでの稼働実績があり、成熟していました。そこに、リーナスの作った高品質なカーネルが「上手く組み合わさった」ことで、GNULinuxは融和し、「一から作ろうとしては作れないところを、ネットの仲間やGNUXFree86などの既製品と組み合わせて作り上げた」というOSがLinuxなのです。

Linuxの課題とコミュニティの価値

また、Minix本を書いたタネンバウム教授は、Linuxを「マイクロカーネルではなくモノリシックカーネルで、時代遅れだ」と言っています。現代的なカーネルマイクロカーネルと呼ばれ、本当のカーネルは小さく、プロセス間通信と仮想CPU/仮想メモリの「最低限必要なものだけを実装したカーネル」とし、ファイルシステムやネットワークなどは、マイクロカーネル上の「サーバー」として実装し、いつでもそのサーバーをONにしたりOFFにしたりすることができるものであるべきですが、Linuxカーネルはそうしたカーネルに必要な機能をひとつのプログラムの中にいっしょくたにしています。これは初学者にとっては作りやすいカーネルですが、現代的なカーネルではありません。Linuxは今でもモノリシックカーネルを続けており、設計において技術的な新しい点もあまりなく、「普通のUNIX互換カーネル」でしかありません。

また、GNUから見ると、LinuxカーネルのおかげでGNU/Linuxシステムは完成しましたが、「GNUの全ての成果が、Linuxの成果であるかのように誤解されている」というところがあります。GNUのソフトウェアはフリーソフトウェアであるため、コピーや再配布が可能ですが、LinuxユーザーはGNUの成果をふんだんに取り入れながら、あたかもその全てをリーナス・トーバルズとコミュニティが開発したかのように人々に勘違いさせています。実際はリーナスやコミュニティはLinuxを開発しただけで、GNUまでは開発していません。GNUストールマンは「GNU/Linuxと呼ぶべきだ」としていますが、Debianぐらいしかこの名称を本気で採用するプロジェクトはありません。そしてそのDebianすら、フリーソフトウェアでない「独占ソフトウェア」を含めて再配布しています。これはGNUから見てあってはならないことです。

そういうわけで、Linuxにはさまざまな課題や問題があり、またそのコミュニティも一様ではありませんが、そこが良いのです。みんなで開発し、みんなが別の理想を持って別の理由から開発したものを、共通の利益として共有していく、このプロジェクトの透明性と健全性こそが、オープンソースコミュニティの大原則です。最近は、Linuxカーネルでも、ボランティアの参加者は8%程度に過ぎず、多くが企業によって金銭的な利益を目的に開発されていますが、同時に、Mozilla.orgのように本当に金儲けではなく開発されているプロジェクトもあります。色んな人がいて、みんなで協力する姿勢は、とても良いものだと僕は思っています。Linuxは既に、昔のLinuxとは全く違うものですが、そうした考え方や理想そのものは変わっていないように感じます。

本当のところを言うと、GNUの言うように、リーナスやその仲間が開発した部分は本当に小さいものです。それは、UNIX由来のツールは、昔から作っていた人々が居て、たとえばviをビル・ジョイが開発したように、あるいはPerlラリー・ウォールが、Rubyまつもとゆきひろが開発したように、さまざまな人間が開発したものの寄せ集めに過ぎないからです。Red Hatなどが「保守と責任のサポートをやる」と言っていますが、彼らのやっていることは昔からUNIXの業者がやっていたこととあまり変わりません。そう、Linuxはまさに「コンピュータ技術を全部一緒にしたかのようなOS」です。そのため、Linuxをやっていると、本当にコンピュータの「歴史」が良く分かります。歴史的なさまざまなイディオムやプラクティスが多く、「ああ、これがまさにコンピュータだな」と感じられます。逆に、Windowsは新しいアーキテクチャ上の進歩が多く、Windowsを使っていると「新しい世界になったのだな」ということが良く分かります。新旧のWindowsLinuxがともに進歩していけば、きっとIT技術の未来は明るいのではないかと思います。

2019.11.16追記:「詳解 Linuxカーネル」を読んでいて、この文章に少し誤りがあることに気付いた。すなわち、歴史上の伝統的なほとんどのUNIXカーネルはモノリシックだということ。Linuxはそれらと同様にモノリシックなカーネルでありながら、モジュール機構などを活用し、カーネルフロッピーディスクに収めるぐらい小さくできる(少なくともこの本の第二版が出版された時点では)。よって、必ずしもマイクロカーネルの設計は必要ない。リーナスも開発者たちもそれを重々承知であると思う。

コンピュータはCPUとメモリが機械語で記述されたOSの膨大な情報をひらすらに計算する

コンピュータは、CPUとメモリが、機械語で記述されたOSの膨大な情報を、ひらすらに計算する機械です。

ただひたすらに、機械語で書かれたOSの膨大な情報を計算します。これが「計算機」です。

Linuxカーネルオープンソースであることは、「コンピュータを自由に使える」ということを意味しています。

プログラムのソースコードが公開されたLinuxカーネルでは、OSの内容を自分で研究したり書き換えたりすることができます。

カーネルだけではなく、GCCのようなツールチェイン(コンパイラや開発ツールの集合)がGPLでライセンスされているため、コンピュータの動作を自在に、どこまでも詳細に変更できます。

まさに、Linuxは「ハッキングのためのOS」であると言えます。

また、最近のIntel CPUの進歩は目覚ましく、数十年前のSunやHPなどのワークステーション以上の能力を、64bitのIntelプロセッサが持っています。これにLinuxを組み合わせることで、「現代版ワークステーション」を操作することができます。

レジスタとメモリ

実際のところ、レジスタとはCPU内部の高速なメモリに過ぎません。ですが、レジスタ機械語で命令の処理に使われるため、小さく、少なく、そしてプログラムの実行にとって重要です。そのため、レジスタは少ない領域のものを、プロセスを切り替えるたびに「読み込んだり退避したり」します。

これに対して、メモリは低速で、大容量です。このため、それぞれの領域に分割して、プロセスがそれぞれのアドレス空間を持ち、機械語の命令としてメモリにプロセスがアクセスしようとした時は、カーネルが仲介して論理アドレス物理アドレスに翻訳するのです。

そのため、コンテキストの切り替えは比較的容易です。ソフトウェアだけで実現できます。

メモリ管理を行うためには、ハードウェアの機能を用いてカーネルが仲介し、論理アドレスをリニアアドレス・物理アドレスに変換します。この時、ハードウェアのセグメンテーション回路とページング回路を利用します。また、それぞれのメモリ領域はページと呼ばれる単位でカーネルによって管理されます。

メモリ管理ユニット (MMU)

メモリ管理ユニット (MMU)はCPUのメモリへのアクセスを制御するハードウェア部品のひとつで、仮想アドレスを物理アドレスに変換したり、メモリ保護やキャッシュやバスを管理する機能がある。

現代的なコンピュータでは、MMU内蔵のCPUとOSが連携して仮想記憶を実現している。

割り込みとは

要するに、ハードウェアの要求をカーネルが待っていると遅いので、カーネルが待っている間別の処理をし、ハードウェア側で何かあったら「割り込み」としてカーネルの処理に割り込んでくる、という仕組み。

注意されたい。

さまざまな割り込み

割り込みはこれ以外にもある。

マウスやキーボードのイベントを取りこぼすことなく伝えるのも割り込みだし、ハードウェアの割り込みとは別にソフトウェアの割り込みもある。また、周辺機器に障害が発生した場合や例外処理の効率化、それに正確なタイミングでの処理をタイマー割り込みを行って行うこともある。

プロセス間通信とは

Linuxにおいて、プロセスには個別のメモリ空間が与えられる。

これに対して、スレッドはプロセスよりも軽量であり、プロセスの中で同じメモリ空間を共有する。

スレッドにおいては、並列処理を行うコードは適切に「ロック」すなわち排他制御をし、競り合い状態が発生しないスレッドセーフなものにする必要がある。

また、プロセスにおいては、「プロセス間通信」(IPC)という仕組みを行うことで、異なるプロセスの中でデータを共有できる。

LinuxにおけるIPCには、共有メモリ、セマフォ、マップドメモリ、パイプ、ソケット通信などがある。

Linuxファイルシステムの種類

基本的にext2, ext3, ext4, JFS, XFS, Btrfs, FAT, FAT32などがある。

Linuxでしかファイルシステムを使わない場合は、一番優れていて安心できるのはext4LinuxのルートファイルシステムLinuxのシステムをインストールするファイルシステム)にはext4を使うのが無難。Bツリーファイルシステムはまだまだ未熟である。

LinuxでもWindowsでも読み書きできるようにファイルシステムをフォーマットしたい時はFAT形式がおすすめである。USBハードディスクなどを使う場合でも、WindowsからもLinuxからも読む場合はFAT形式を使おう。

ジャーナリング

ext3ext4(現在のLinuxの標準)などのファイルシステムジャーナリングファイルシステムと呼ばれ、ジャーナリングによってシステムの整合性を保つ機能が搭載されている。

ジャーナリングに対応していないと、不意に異常終了した時にファイルシステムが破損する場合がある。

VFS

LinuxではVFSという機構を使うことで、各ファイルシステムの差異を吸収することが出来る。

プログラマやプログラムは各ファイルシステムの実装の詳細を知らなくても、VFSインターフェイスだけを使ってあらゆるファイルシステムに対応することができる。

Btrfs

Btrfsは現在Linuxカーネルで開発されている先駆的なファイルシステム。機能が豊富で、まだ未熟だが進歩の期待度が大きい。いつか標準になる日が来るかもしれない。

機能的には、SunのZFSに近い。ZFSGPLとの兼ね合いでLinuxでは使えないため、Btrfsに期待が集まっている。

そもそもはOracleによって開発された。Oracleは信頼できるデータベースの会社であるため、Oracleとさまざまな会社が協力することで、信頼性のあるファイルシステム技術に成長することを願っている。

リーナスはZFSが嫌い

Linuxカーネル開発リーダーのリーナス・トーバルズは、ZFSを公式のLinuxカーネルにはマージしない姿勢を改めて強調している。ZFSを「バズワード」と明言し、起訴好きのOracleGPLにライセンスでもしない限りはマージしないとのこと。

これにより、ZFSを使いたいLinuxユーザーは、Ubuntuなどの独自に対応したディストリビューションを使うか、FreeBSDなどの他のUNIX互換OSを使う必要が改めて浮き彫りになった。

BIOS

BIOSは、起動時に最初に実行されてブートローダを読み込むだけではなく、接続された機器のI/Oを直接操作し、その操作の方法を共通したインターフェースを通じてOSが行うための基本的なI/O操作方式を提供する。

現代的なOSでは、こうしたI/Oの共通インターフェースの提供はデバイスドライバを通じて行うことが多く、BIOSによるI/O操作はあまり行われなくなっている。

デバイスドライバ

デバイスドライバは、この世界にあまたあるハードウェアデバイスそれぞれを共有のインターフェースで操作する仕組みで、デバイスドライバが用意されていないハードウェアについてはOSは操作することができない。Linuxカーネルの大部分のコードはデバイスドライバに充てられている。Linuxはプリンターなど一部のデバイスについては今でもドライバが十分ではなく、プリンターについては「ドライバレス」と呼ばれる新しい仕組みも開発されている。

最近はLinuxカーネルX11もモジュールを自動でロードするので、自分でデバイスドライバを導入したり設定したりすることは少なくなっている。組み込みシステム向けにLinuxカーネルを使う場合などは、カーネルを手動コンパイルすることで、必要のないモジュールを全て消去できる。また、リアルタイム向けのパッチを当てることで、プリエンティブルなRTOSの機能(要求が起きてからタスクを実行する時間を最短にする)を実現できる。特に自動車などLinux+Pythonの環境を組み込みでも使いたい場合など(人工知能を乗せるなど)の時は、PC向けの要らない機能(デバイスドライバだけではなくファイルシステムなど)は全部消去させることができる。

ブロック型デバイスとキャラクタ型デバイス

デバイスドライバには、大きく分けてブロック型デバイスとキャラクタ型デバイスがある。

基本的に、ブロック型デバイスというのは、ハードディスクやCD-ROMのような「データ」を扱うためのもので、キャラクタ型デバイスというのは、モデムやプリンタやキーボードやマウスといった「やり取り」を行うためのもの。

ブロック型とキャラクタ型の違いは、「好きな時に好きなところにアクセスできるかどうか」。

また、このどちらにも当たらないネットワークインターフェースのデバイスドライバがあり、通信に利用する。デバイスファイルからの読み書きはできない。

デバイスドライバによってデバイスがシステムに認識されると、現在のLinuxではudevを用いて/devディレクトリにデバイスファイルが自動作成される。

プログラムがデバイスファイルにアクセスすると、デバイスドライバに対応するI/Oデバイスを用いて、デバイスを直接触ることができる。

ソケット

ソケットは、ユーザにとってのデータの出入り口で、ソケットに対してデータを書き込んだり読み込んだりするだけで通信の行える、UNIXにおけるネットワークのインターフェース。

2つ以上のソケットが互いに関係を持つと有効になる。

互いに関係を持ったソケットでは、片方に書き込むとそのデータがもう片方のソケットから出ていく。

複雑なネットワークの通信プロトコルを意識する必要はない。

つまり、送信側が「書き」、受信側が「読む」ことだけでどんなに複雑なプロトコルを用いた通信も可能となる。

ネットワークインターフェースデバイスNIC)の操作やOSIプロトコル手順やIPアドレスMACアドレスやルーティングなど、複雑な処理は全部カーネルがやってくれる。

ソケットはファイルとして扱われるため、普通のファイルと同じようにファイルディスクリプタを用いて操作する。

リアルタイムOSとは

組み込みなど、小さなハードウェアに組み込まれたRTOSでは、リアルタイムに反応することが求められる。

RTOSでは、イベントが発生してからCPUは必ず決められた時間内に処理を行う。

そのために「優先順位」(レイテンシ)を使うことで、一番優先度が高いタスクは他の全てのタスクを無視できる。

Linuxではrt(Real Time)関係の開発が行われており、RT-Preemptパッチを当てることで、Linuxを完全にプリエンティブルなカーネルに書き換えることができる。

RT-LinuxはRaspberryPiなどでも試すことができる。

カーネルの手動コンパイルの方法

Linuxカーネルは、手動でコンパイルすることで、さまざまなオプション機能を有効にしたり、無効にしたりすることが出来る。各機能は有効・無効あるいは動的にいつでも読み込めるモジュールにすることができる。

カーネルヘッダー

自分でLinuxのプログラムをコンパイルしたい時に、カーネルヘッダーが必要となる時がある。カーネルAPIを使うプログラムをコンパイルするために必要。

カーネルヘッダーは、少し前の状況だと、カーネルがメジャーアップデートされた場合(2.4から2.6への移行の時など)、プログラムが正常に動かなくなったり、コンパイルできなくなる時があった。なので、大切な基盤システムなどでは、注意してアップデートすること。

カーネルヘッダーだけではなく、gcc, glibc, pythonなどの重要コンポーネントは、メジャーアップデートに失敗すると悲惨なことになる。Red Hatの開発者などでも、注意して開発している。特に、Pythonなどは本当に動かなくなる(Python2とPython3には全く互換性がない)。気を付けてシステムを管理しよう。

マイクロカーネルの特徴

ファイルシステムデバイスドライバのような準カーネル機能を、モノリシックカーネルでは一番下の巨大なカーネルの一部として実装するが、マイクロカーネルではユーザー空間で動く「サーバ」として実装する。このことによって、カーネルは小さく(マイクロ)になり、それぞれの個別のサーバはアプリケーションとして動かすことができる。

Linuxではカーネルの一部としてファイルシステムデバイスドライバを実装しているが、Machなどではカーネルは最低限のプロセス管理やメモリ管理だけを行い、Hurdサーバによってファイルシステムなどが実現されている。

マイクロカーネルの優位性

マイクロカーネルの優位性はいくつかある。

まず、マイクロカーネルでは、必要のない機能はサーバを停止するだけでいつでもON/OFFできる。そのため、ファイルシステムやネットワークなどが必要ない組み込みシステムやリアルタイムOSでは、大きなメリットになる。

それから、メモリ管理の効率が良い。全てのカーネル機能をメモリに置かず、必要になった段階でサーバーを起動すれば良いため、効率的なメモリ管理ができる。

デメリットとしては、カーネルの設計が複雑になる。特に、スレッドやポートを用いてメッセージによるタスクの管理を行うMach/Hurdのモデルは、設計上複雑すぎて、きちんと動かない。動作速度も遅く不安定になる。

こうしたデメリットについては、L4のように、「注意深く設計することで高性能なIPC性能をたたき出す」というマイクロカーネルも誕生している。

逆に、サーバのON/OFFやメモリ効率性のメリットについては、Linuxや*BSDなどはモジュールによってカーネルにいつでも機能を組み込むことができるようにすることで、モノリシックカーネルでも可能となる。

GNU Hurdがなかなか完成しなかったことについて、ストールマンは「マイクロカーネルデバッグが予想以上に難しかった」という点を挙げており、サーバーとユーザーランドのプロセスが相互に通信し合うマイクロカーネルの設計は、デバッグや開発も難しい。

Hurdは未完成

これだけGNU/Linuxが一般的に使われるようになっても、GNUマイクロカーネルHurdはまだ完成していない。ストールマンは、「マイクロカーネルデバッグが予想以上に難しかった」と語っている。

だが、Hurdは興味深いプロジェクトである。そもそも、Linuxカーネルの最大の問題点はモノリシックカーネルであること。カーネルが単独の巨大プログラムとして動いている。これは設計上古くて、美しくない。

最近では、HurdMachではなくL4など別のマイクロカーネルを採用して作れないかという意見も出ている。Machは性能の問題があり、モノリシックカーネルと比べてとても遅いが、L4の開発者はこれを「設計上の問題」であるとしてL4を開発し、高速なIPC性能を叩き出した。

L4

マイクロカーネルOS。Mach/Hurdよりも性能が高く、「マイクロカーネルが性能に問題があるとしたのは、Machの設計や実装が誤っていただけだ」とした。

オリジナルのL4カーネルインターフェース(ABI)やその後継がいくつも再実装されており、「L4マイクロカーネルファミリー」と知られている。

ストリームとは

UNIXにおけるプログラミングは、プロセスとファイルシステムが「ストリーム」をやりとりすることで行われる。

ストリームは要するに、バイト列の流れ道のことであり、UNIXでは標準入力やファイルを表すのにストリームという概念を使う。読みだすことを「読む」、書き込むことを「書く」という。ファイルだけではなく、デバイスやプロセスについても同様。デバイスファイルはストリームの「とっかかり」になる。

そもそもが単純なバイト列の流れの操作が多かった、UNIXシステム由来の概念です。

システムコールとライブラリ関数

LinuxC言語によるAPIの基本は、システムコールとライブラリ関数です。

システムコールLinuxカーネルによる機能で、ストリーム、ファイルシステム、ネットワーク、プロセス、メモリ管理などで、カーネルの機能を使う時に使用します。

C言語のライブラリ関数は、主にglibcなどによる「標準Cライブラリ」です。

システムコールとライブラリ関数では、似たような機能を提供する関数もあります。

たとえば、システムコールのread(2), write(2), open(2), close(2)は、stdio版として、fopen(3), fclose(3), fgetc(3), fputc(3), getc(3), putc(3), getchar(3), putchar(3), ungetc(3), fgets(3), fputs(3), puts(3), printf(3), fprintf(3), scanf(3), fread(3), fwrite(3)などなど、さまざまなライブラリ関数があります。

ここで、(2)や(3)は、システムコール(2)かライブラリ関数(3)かを表しています。

システムコールは、バッファリングを行わないため、動作が遅いです。バッファリングだけの問題ではなく、ライブラリ関数よりもシステムコールの方がずっと遅いです。ライブラリ関数の方が普遍的なため、通常はライブラリ関数を優先して使いましょう。

四大システムコール

システムコールでは、ファイルの情報はファイルディスクリプタと呼ばれる整数値のデータを使って保持します。これはOSがストリームの識別のために使います。

以下は四大システムコール

API 説明
open(2) ファイルを開く。ファイルに接続するストリームを用意する。
close(2) ファイルを閉じる。ストリームを始末する。
使い終わったストリームはclose()で後始末しないといけない。
read(2) ファイルを読み込む。ストリームからバイト列を読み込む。
write(2) ファイルを書き込む。ストリームにバイト列を書きこむ。

ファイルと言うよりは、ストリームを読み書きします。ですので、標準入出力に読み書きすることも可能です。たとえばcatコマンドを作ったり出来ます。

また、四大システムコールの美しさを保つために、それ以外の雑多な処理は全てioctl()で行う。

stdio

stdioでは、バッファリングを行うため通常はシステムコールよりも動作速度が速い。また、固定長バイトだけではなく、文字単位(バイト単位)や行単位での入出力ができる。

これらのCライブラリ関数では、FILE型のポインタを使ってファイルの情報を格納します。

FILE型はファイルディスクリプタのラッパーで、stdioバッファの情報も含まれています。

API 説明
fopen(3),
fclose(3),
fread(3),
fwrite(3)
ライブラリ関数の版。
バッファリングを行うため、通常はシステムコールより動作が速い。
printf(3) printfはフォーマットされた文字列を出力出来ます。
任意の文字列や変数の中身を展開して表示する場合など、良く使います。
scanf(3) scanfはフォーマットを指定して入力できる。
バッファオーバーフローを起こす潜在的な危険がある(文字幅指定で回避できる)。
getchar(3) バイト単位(一文字単位)で文字を入力できる。
putchar(3) バイト単位(一文字単位)で文字を出力できる。

セキュリティの問題があるgets()は廃止されました。

後日注記:printfはコンソールへの出力、fprintfはファイルへの出力によく使います。

mallocの使い方

malloc()の使い方は、

int *data;
data = (int *)malloc(sizeof(int) * 5);

のように、配列のサイズを指定したmalloc()関数を実行し、その返り値を型キャストして配列のポインタに代入する。

動的に生成したメモリ領域は、使わなくなった段階で必ずfree()で解放すること。そうでなければメモリリークが起きてしまう。

バッファ決め打ちを使う場合もある

bufという変数をバッファとして使うような場合、バッファは4096とサイズを決め打ちにすることがありますが、fgets()とfputs()を繰り返すのであれば、決め打ちでも構いません。

grepコマンドの実装であれば、fgets()やfputs()を使って一行ずつ文字列を読み出してbufに格納し、書き出しを行いますが、catコマンドの実装などにおいては、システムコールのread()/write()やstdioのfread()/fwrite()など、固定長バイトでの読み書きを行うこともあります。この際にも、こうした一時バッファの固定文字列に一時的にデータを格納する、という手法は良く使われます。

このようにすることで、巨大なメモリ領域を確保しなくても全文字列を書き出せるため、使用メモリの削減にもなります。仕組みは違いますが同じ目的に使えるものとして、Pythonのyield returnがあります。Pythonではyield returnを使うことで一行ごとにデータを読み、一行分のメモリだけでデータを書き出すことができます。

またregcomp()とregexec()はC言語での正規表現のために使います。

straceコマンド

straceコマンドを使うと、実際に動作中のプログラムが読んだシステムコールをその場で表示してくれます。

$ strace ./hello >/dev/null

fork(2)

fork()を呼び出すと、カーネルはプロセスを複製して、2つのプロセスに分裂させる。これによって新しいプロセスを作り出す。

分裂された時点で、複製前と複製後のプロセスは、どちらもfork()を呼び出した状態になっている。

2つのプロセスの両方にfork()の呼び出しが戻って、両方のプロセスでfork()以後のコードが実行される。

この時、元のプロセスを親プロセス、複製されたプロセスのことを子プロセスと呼ぶ。

後日注記:fork()のエラー処理を行ったり、子プロセスと親プロセスで違った処理をさせるためには、fork()の返り値(通常はpidなどの変数に格納する)をif文で-1や0と比較する。fork()は、失敗した時は-1、成功した時は子プロセスの場合は0、親プロセスの場合は子プロセスのPIDを返す。fork()した後で、親プロセスが子プロセスの終了を待つためにはwait()を実行する。あるいはそれぞれのプロセスを個別に終了するためには_exit()あるいはexit()を実行する(子プロセスの場合は_exit()、親プロセスの場合はexit()を使う)。

スレッドプログラミング

スレッドを用いたプログラミングをCとLinuxでするのであれば、NPTL(Native POSIX Thread Library)のpthread_create()という関数を使えば良い。

libcの重要性

libcは、その名の通りCの標準ライブラリの実装。

カーネルコンパイラだけでは、Cのプログラムは動かない。カーネルシステムコールコンパイラの言語処理系と合わせて、「C言語の関数を実行できる」ようなライブラリが必要である。

libcはカーネルと並んでとても重要で、さまざまなCのライブラリ関数を使うためにはカーネルとlibcがタッグを組む必要がある。その上でgccなどの言語処理系が必要。Linuxではこれらは全てフリーソフトウェアで入手できる。GNUプロジェクトがglibcを開発し、Linuxカーネルの開発者と緊密に協力した結果である。

Linuxで利用できるlibcには、GNUによるglibcのほかさまざまな軽量版libcが存在する。また、FreeBSDNetBSDなどもそれぞれのOSカーネルと合わせてlibcを開発しており、組み込みシステムなどではglibcではなくNetBSDのlibcを使うなどといった事例もある。

C++については

また、C++で開発を行うためにも、同様にC++の標準ライブラリが必要である。LinuxではGCCプロジェクトのlibstdc++などが利用できる。