技術部門のBlog

Code & Daily thoughts.

オペレーティングシステムの基本構成、とLinuxにおけるローレベルなシステムコールの仕組み

仕事帰りの新幹線でこの文書を書いています。

最新ののぞみ号では窓側席の下に自由に使える電源ソケットがあったり、無線LANも飛んでいたりと、仕事をする上では非常に快適な環境で移動が苦になりません。

そんな新幹線も今年で開業50周年だそうです。長いと感じるか短いと感じるかは人それぞれだと思いますが、私は案外短いんだなと感じました。このような技術を作ったそんなに昔の人でもない先人たちの偉業を思うたびに頭が下がる今日このごろです。

さて、今回のブログではオペレーティンシステムの授業シリーズ第2回目ということで「オペレーティングシステムの基本構成」について書きたいと思います。授業では扱いきれないローレベルなシステムコールについてのおまけも最後についています。

導入

前回の授業では「オペレーティングシステムとは何か?」という議論をしました。

オペレーティングシステムとは、ソフトウェアを「応用(アプリケーション)ソフトウェア」と「基本ソフトウェア」に分解した後者の部分であり、

  • アプリケーションソフトウェアとハードウェアの間に存在し
  • アプリケーションソフトウェアとは共通化されているAPIによって区分される

部分でした。

80年代に起こったコンピュータ利用の一般化とアプリケーションソフトウェア産業の隆盛に伴い、様々なハードウェアを持ったコンピュータに広くアプリケーションソフトウェアを流通させるために導入され、普及したという背景についても紹介しました。

今日はそのオペレーティングシステムの誕生の歴史を追いながら、現代の一般的なオペレーティングシステムの持つ基本機能について紹介します。

初期のコンピュータ

まずはこの写真を見てください。

Photo of ENIAC from wikipedia.org

これはENIACと呼ばれる初期のコンピュータです。

現在では半導体で実現されている論理回路部分に真空管が使われているため非常に巨大ですね。

この写真は、ENIACの「プログラミング風景」なのですが、この時代のコンピュータは論理回路間の配線(写真の中に見える蜘蛛の巣のようなもの)を差し替えることによってプログラムしていました。現在「プログラム作業」というとコンピュータの前でキーボードを叩くイメージですが、当時はなかなか重労働だったのです。

余談ですが、この写真、男性と女性が写っており、一見、男性のほうがプログラマで女性が補助しているようにも見えますが、実はプログラマは女性の方で、その指示を受けて動いているのが男性です。ENIACは6人の女性プログラマによってプログラムされていたという記録が残っています。初期のプログラミング言語で広く使われた「COBOL」を設計開発したのも女性です。この業界、案外女性が強いのです。

プログラミングが現代のようなキーボードを叩く形に変化したのは、数学者のフォン・ノイマンが考案した「ノイマン型コンピュータ」以降です。ノイマン型コンピュータでは、コンピュータをメモリとCPUの2部分から構成し、CPUに「メモリから順次データを読みだしてそれを命令として実行せよ」という初期命令を与えます。このようにすることで、従来は配線として物理的に実現されていたプログラムをメモリ上の情報として格納することができます。このアイディアによってキーボードで入力した情報としてコンピュータをプログラミングすることが可能になったのです。「プログラムをメモリに格納する」ことが特徴であることからノイマン型コンピュータはストアド・プロシジャー方式とも呼ばれます。

ノイマン型コンピュータは、プログラムの実行のたびにメモリへの読み出しを行うため、メモリの高速性がコンピュータの処理速度に大きく影響することから、一般にCPUに高速にアクセスできる半導体メモリを組合せた構成が採られます。また、半導体メモリは電源を切ると消えてしまう揮発性があり、大きな容量を実現することが難しいことから、それを補うためのHDDなどの補助記憶装置を組み合わせる構成が用いられるようになりました。

オペレーティングシステムの誕生

高性能なコンピュータを実現する上でCPUの処理能力が重要なのはもちろんですが、ノイマン型コンピュータではメモリの性能も大きなポイントになります。

メモリにはプログラムと同時にそのプログラムが処理したデータが格納されます。複雑で高機能なプログラムを実行したり、大量のデータを保存するためには大きなメモリが必要です。プログラムの実行のたびにメモリの読み出しが行われるため、メモリの高速性もコンピュータの処理速度に大きく影響します。しかしながら、メモリの高速性と大容量化は一般にトレードオフの関係があり、高速で大容量のメモリは実現することはできません。

上記の問題を解決するのが「仮想メモリ」というオペレーティングシステムの仕組みです。仮想メモリではHDDなどの補助記憶装置を仮想的に半導体メモリの一部として使うことで、メモリの見かけ上の大容量化を実現します。

補助記憶装置には様々な種類があります、現在、一般に用いられているHDDもそうですし、CD-ROMも補助記憶装置の一種です。昔は、ビデオテープのような巻き取り型の磁気記憶装置も使われていました。オペレーティングシステムには、これらの記憶装置のハードウェアの差を意識することなく使えるように「ディスク管理」の機能も実装されています。

初期のコンピュータは、大学などの研究機関で使われていましたが、非常に高価なものでした。現在は各家庭に複数のコンピュータがあるのが当たり前ですが、当時は1研究機関に1台あるかないかという状況でした。そのような状況ではコンピュータを如何に効率的に稼働させるかという「プロセス管理」が重要になります。また、コンピュータを同時に使うことができる「マルチタスク」の技術も開発されました。これらの機能は現代のオペレーティングシステムにも利用のカタチをかえて(当時に比べたらはるかに贅沢な利用法を想定して)実装されています。

上記のようなコンピュータを便利で効率的に使うための各機能を最初に実装したのは、IBMがSystem/360というコンピュータのために開発したOS/360というソフトウェアだったと言われています。このソフトウェアはその名の通り、Operating Systemという名前の元祖にもなっています。

UNIX、POSIXとLinux

OS/360はメインフレームと呼ばれる、企業の業務処理などを行うコンピュータのために開発されたソフトウェアであり、実行できるのもIBMのコンピュータに限られていました。

AT&T(米国最大手の電話会社、日本のNTTのようなもの)の研究所では、C言語という、特定のCPUに依存した機械語を使わないでプログラムを記述することができるシステム開発言語が開発されると同時に、C言語で記述されたオペレーティングシステム「UNIX」が開発されます。大学などの研究機関ではこのUNIXオペレーティングシステムが普及すると同時に、様々なハードウェア上で実行できるように移植作業が行われました。

オリジナルのUNIXはAT&T UNIXと呼ばれますが、UNIXから派生した亜種には、カリフォルニア大学バークレー校で開発されたBSD UNIXや、サン・マイクロシステムズが開発したSunOS(Solaris)、ヒューレットパッカードが開発したHP-UNIX、MacOSの元になったカーネギーメロン大学で開発されたMachなどがあります。

同じ祖先を参考にして作られたUNIXの亜種たちですが、同じような機能を実装しているにもかかわらずAPIの厳密な共通化は行われておらず、アプリケーションソフトウェアはそれぞれUNIX上で多少の改変を行わなければ実行することができませんでした。

これでは不便である、ということで、IEEE(米国電気工学会)が取りまとめ役となり、厳密なAPIの共通化が行われます。これがPOSIX(Portable Operating System Interface for UNIX)です。

POSIXの登場により、UNIX用に開発されたアプリケーションソフトウェアがほぼ改変なしで流通できるようになり、非常に便利になりました。それと同時に、POSIX規格の誕生の翌年、非常に重要なソフトウェアが思わぬ副産物として誕生します。

以下のネットニュース(掲示板)への投稿を見てください。

From: [email protected] (Linus Benedict Torvalds)
Newsgroups: comp.os.minix
Subject: Gcc-1.40 and a posix-question
Message-ID: <[email protected]>
Date: 3 Jul 91 10:00:50 GMT
Hello netlanders,
Due to a project I'm working on (in minix), I'm interested in the posix
standard definition. Could somebody please point me to a (preferably)
machine-readable format of the latest posix rules? Ftp-sites would be
nice.

この文書は1991年にヘルシンキ大学のLinus Torvaldsという学生がPOSIX規格文書の電子版の入手について質問した投稿です。

Linus Torvaldsは、この投稿の後、入手したPOSIXの規格文書を元にして、LinuxというPOSIX互換オペレーティングシステムを開発して、ネット上でフリー配布を始めます。

それまで開発されていたUNIXオペレーティングシステムの亜種は、どれもオリジナルのAT&T UNIXのソースコードを参照して開発されていたため、フリーのものもあったものの、営利企業であるAT&Tの商用ライセンスに依存する部分がありました。それに対してLinuxというオペレーティングシステムは、フリーで参照できるPOSIXの規格文書「のみ」を参照してゼロから開発された、完全にフリーなオペレーティングシステムです。

Linuxのライセンス上の特性と、そのメンテナンスと拡張作業をコミュニティを巻き込んで精力的に続けた(現在も続けている)Linus Torvaldsの人格からLinuxは特にサーバ向けOSとして爆発的に普及します。

コンシューマ向けオペレーティングシステム

大学などの研究機関においてはPOSIX互換のオペレーティングシステムが早い段階から利用されましたが、POSIX規格はコンシューマ市場(専門家でない一般のコンピュータ利用者市場)ではオーバスペックでした。POSIXのフル規格を実装したオペレーティングシステムは、まだ高価だった高機能なコンピュータでしか利用することができません。コンシューマ市場向けには、ディスク管理機能と簡単なプロセス管理機能のみを持った縮小版のオペレーティングシステムが開発されました。それらのオペレーティングシステムはDisk Operating System (略してDOS)とも呼ばれます。CP/MやMS−DOSはこれらディスク管理機能のみを持った縮小版のオペレーティングシステムです。MS-DOSは当時創業して間もないマイクロソフト社が開発したDOSです。IBMのコンシューマ向けコンピュータにOEM採用されたことをきっかけにマイクロソフト社はコンシューマ向けオペレーティング市場の巨大な存在となっていきます。

現在では、多くのコンシューマ向けオペレーティングシステムがPOSIXのフル規格をサポートしています。Linuxはもちろんのこと、MacOSとLinuxをベースに開発されたAndroidもPOSIX互換です。Windowsは厳密にはPOSIX互換ではありませんが、対応するすべての機能を持っています。iPhoneの中に入っているiOSも多くの互換関数を持ちます。

この授業(ブログ)では、オペレーティングシステムの基本としてPOSIX規格で定義されたAPIを概観するとともに、APIとハードウェアの間で働くオペレーティングシステムの機能を紹介していきます。

新幹線は間もなく目的の停車駅に着きそうです。この新幹線「全輪駆動」というすべての車輪にモータをつけて駆動輪とした初めての列車です。全輪駆動では、特定の車両が客車を引っ張る形だったそれまでの列車とは異なる複雑な制御が必要となります。新幹線を制御システムとしてみると、各車両にエンジンをつけたバスが至近距離で縦列走行しているようなものです、お客さんが乗っている車両や乗っていない車両もあるのですから衝突しないように息を合わせるのは大変な作業です。それに加えて、現在は省エネルギーの回生ブレーキなども導入されているといいます。これらのシステムを運用するソフトウェアの開発は大変な作業です。列車と同様の制御複雑化・高機能化は自動車でも起こっており、オペレーティングシステムが広く導入され始めています。このような制御システムに特化した要件(時間に厳密、安全機能は死守しつつ高機能化にも対応できるように)を満たすように設計されたオペレーティングシステムは特にリアルタイムオペレーティングシステムと呼ばれます。このブログのどこかの回でも紹介したいと思っています。

おまけ(Linuxのローレベルシステムコール)

授業では「オペレーティングシステムはAPIとハードウェアの間にあるソフトウェアのことである」と説明しているものの、アプリケーションとオペレーティングシステムを区分するAPIの境界部分はよくよく考えると実は結構あやふやです。

POSIX規格はC言語の関数名とその引数のコレクションとして定義されていますが、バイナリ形式に変換されたアプリケーションプログラムは、どのようにしてオペレーティングシステムの機能を呼び出しているのでしょうか?

ここではPOSIXで標準化された関数の一つである「printf」の実装を追いかけて、流れを見てみたいと思います。

このprintf、その実装をLinuxのソースコードに探しても見つけることができません。実は、実装は「libc」というライブラリの中で行われています。

http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/printf.c

printf関数は単純にvfprintf関数を呼び出すのみで、その実体はvfprintfに書かれています。

http://sourceware.org/git/?p=glibc.git;a=blob;f=stdio-common/vfprintf.c

見てわかるように、printfのほとんどの機能はlibcに実装されています。

printf関数は各種文字列加工を行った後、最終的に「outstring」マクロを呼び出します。 outstringマクロは、「PUT」マクロを呼び出し、PUTマクロは「_IO_sputn」マクロの別名です。

しばらくマクロを追いかけます。「IO_sputn」は以下のヘッダで定義されたIO_XSPUTNのマクロで、その実体は_xsputn関数です。

http://sourceware.org/git/?p=glibc.git;a=blob;f=libio/libioP.h

どんどん追いかけていく(途中は省略)と最終的には

http://sourceware.org/git/?p=glibc.git;a=blob;f=libio/fileops.c

に記述されているマクロの中で「write」関数が呼ばれます。write関数は「システムコール」と呼ばれる基底関数で機械語で直接記述されています。その内容は、「レジスタeaxにwriteシステムコールのID番号である4を代入して、ハードウェア割り込みの0x80番をコールする」というものです。ここからlibcの世界を抜けてLinuxの世界に入ります。

Linux側では、システムコールを受けてID番号に対応する関数が呼ばれます。その対応表が以下のファイルに定義されています。

https://github.com/torvalds/linux/blob/master/arch/x86/syscalls/syscall_32.tbl

最終的に呼び出されるのが、Linuxカーネル内で定義されるこの関数です。

https://github.com/torvalds/linux/blob/master/fs/read_write.c

以上追いかけていったように、POSIX APIの境界からは直接システムコールが呼び出されるわけではなく、libcが機能の大きな部分を担当しています。libcはライブラリであり、メモリ空間上のユーザスペースで動作します(カーネルスペースに実行が移るのはシステムコール以降です)。この部分をオペレーティングシステムの一部として考えるべきかどうかはよくよく考えると悩ましい問題ですが、授業の中では分かりやすさ優先でオペレーティングシステムの一部として教えてしまっています。

Comments