8. Linuxのシステムプログラミングに関する知識 II

シラバス: 

1. 科目の概要

Linuxにおいてシステムが提供するリソースを活用してプログラムを実行するシステムプログラミングのうち、スレッドプログラミングやプロセス間通信、デバイスファイルの利用、セマフォや共有メモリ、メッセージキューの利用など、比較的高度な話題について解説する。

2. 習得ポイント

本科目の学習により習得することが期待されるポイントは以下の通り。

3. IT知識体系との対応関係

「8. Linuxのシステムプログラミングに関する知識Ⅱ」とIT知識体系との対応関係は以下の通り。

[シラバス:http://www.ipa.go.jp/software/open/ossc/download/Model_Curriculum_05_02.pdf]

<IT知識体系上の関連部分>

4. OSSモデルカリキュラム固有の知識

OSS モデルカリキュラム固有の知識として、Linux という具体的な環境におけるプログ

ラミング手法の知識が含まれる。デバッグ手法、プロセス管理、入出力管理などに関するプログラミング手法について、Linux を通して習得する。

(網掛け部分はIT知識体系で学習できる知識を示し、それ以外はOSSモデルカリキュラム固有の知識を示している)

II-8-1. 複数ソースプログラムのビルド

多くのソフトウェアでは、ひとつのソースコードで全てのコードが記述されるわけではない。ソースコードを分割することのメリットについて説明し、複数のソースコードからアプリケーションをビルドするためのツールであるmakeの概念と利用方法について解説する。

【学習の要点】

* プログラムのソースコードはしばしば複数のソースファイル(または他のファイル)から生成される。

* プログラムは、自分以外の人が書いたライブラリを組み込んで構成することがある。OSによって提供される機能(ライブラリやシステムコール)を利用する場合もこれに相当する。このような場合、ライブラリやシステムコールは、外部のプログラムから利用できるようにヘッダファイルと呼ばれるインタフェースを公開する。

* 外部にインタフェースを公開する以外にも、ソースコードの可読性を向上させたり、モジュール性を持たせたりという目的のために分割することがある。

* makeコマンドは、対象ソースファイル、参照ヘッダファイル、参照ライブラリ、およびコンパイル・リンクオプションの書かれたファイル(makefile)を読み込んで、プログラムのビルドを自動化するツールである。

図II-8-1. makeの流れ

 

【解説】

1) ソースコードを分割することのメリットとデメリット

* メリット

- ヘッダファイルと呼ばれるインタフェースを使って、ライブラリとして外部に公開したとき、汎用性が高まる。

- 個々のモジュールを小さくすることにより、モジュール性を高めることができる。

- デバッグ時に問題の切り分けがより単純になる。また、ソースコードの可読性が向上する。

- 一度ソースファイルをコンパイルすれば、変更を加えない限り再コンパイルする必要がなくなるので、プログラムをコンパイルする合計時間を短縮できる。

* デメリット

- 過度に細かく分割した場合、ソースファイル数が膨れ上がり、可読性は逆に低下してしまう。

2) ヘッダファイル

* 関数プロトタイプ宣言が記述されたファイルのこと。ライブラリのインタフェースとして機能する。

* そのライブラリを使用するソースファイルをコンパイルするとき、ヘッダファイルを使って、関数の呼び出しが正しいかチェックする。

3) makeとは

* 複数のソースファイルからなるプログラムのビルドを、自動化するためのツールのこと。

* CやC++だけでなく、TeXやデータベース管理などにも適用できる。

* メリット

- 不要な再コンパイルを避ける事ができ、ビルド時間を短縮できる。

- コンパイルだけでなく、モジュールやライブラリとのリンク方法も定義できる。

- プログラム自体の複雑さが増大するほど、コンパイルやリンクを手作業で行う際に発生するミスが増える。makeによりそれらを避けることができる。

* makeを使用するためには、プログラムをビルドするためのルールが記されているファイル (ビルドファイル)「makefile」を作成する必要がある。

- 最初のルールは基本的に、ターゲット、必須項目、実行コマンドから構成される。

- それらのルールに、対象ソースファイル、参照ヘッダファイル、参照ライブラリ、およびコンパイル・リンクオプションなどが記述される。

- 書かれた要素間の依存関係をもとに、makeはファイルのタイムスタンプの比較を行うなどして、不必要なコンパイル作業を除去し、ビルドを最適化する。

* Javaにおいてはmakeの代わりに作られたビルドツールとしてAntがある。Antもmakefileと同様にビルドファイルを持つが、それはXMLにより記述される。

II-8-2. ソースコード管理と差分情報

複数ファイルから構成されるプログラムを多人数で開発する環境においては、ソースコードをまとめて管理する必要がある。ソースコード管理の考え方を示し、ソースコード管理ツールの代表的なツールであるRCSやSubversionなどを紹介する。

【学習の要点】

* プログラムを構成するファイルを管理するということは、それを構成するファイル一覧とその世代を管理することである。

* 複数人でプログラムを開発するときは、互いに変更点が競合するのを防ぐ必要がある。これには、人的なコミュニケーションが不可欠であるが、これを補助するツールが存在する。RCSやSubversionはその一例である。

* RCSやSubversionといったバージョン管理ツールは、関連するソースコード一覧にタグをつけて範囲を管理する。その上で、範囲内の個々のソースコードの世代間差分を保持・復元することでプロジェクトの管理を行うものである。

図II-8-2. プロジェクト管理

【解説】

1) バージョン管理

* 主にテキストファイルのバージョン(リビジョン)を管理し、必要に応じて過去のバージョンのファイルを取得できるような機構をバージョン管理機能という。

* 有名なバージョン管理システムにRCS (Revision Control System) がある。RCSは、ファイルの新旧を、差分(前のバージョンとの変更点情報)で表す。すべてのバージョンの生のファイルを保持するわけではない。

* RCSは、古いファイルを得る要求を受けると、ヘッド(最新)バージョンから所望のバージョンまで差分情報を元にファイルのコンテンツを計算する。

2) ファイル群の管理

* RCSは、ただひとつのファイルを管理するのであって、ファイル群に対してバージョン管理を行うことはできない。

* リポジトリ (バージョン管理するファイル群の保管場所) は、時間とともに伸縮する(ファイルは増えたり減ったりする)。また同時に、ファイルの内容は時間とともに変化する。近年のバージョン管理システムは、この2つの軸からなる空間から、ファイル群を選択できる能力が求められる。

* CVSは、ファイル群をモジュールとして管理することができ、ファイル一つ一つに独立してリビジョンを振っていく。

* Subversionは、ファイル群をディレクトリにて管理する。CVSと違って、Subversionはリポジトリに対してリビジョン番号が振られる。このため、Subversionでは、ある時点のリポジトリ全体のスナップショットに容易にアクセスすることができる。

3) CVSでのプロジェクトの管理

* CVSでは、リビジョンはファイル毎に付加されるため、ファイル群に対してバージョン管理を行なうには以下のように行うとよい。

- モジュールを定義する

- ある時点のモジュールに対してタグ(スナップショット名)を付ける

* ある時点のファイル群を取り出すには、モジュール名とモジュールのタグを指定する。

* タグを利用しない場合は、モジュール名と日付を指定する。

4) Subversionでのプロジェクトの管理

* Subversionでは、リポジトリ毎にリビジョンが振られる。リポジトリに対してリビジョンを指定すると、そのリビジョン時点での全ファイルを選択できる。

* ある時点が決まれば、あとはファイル群を選択するだけである。Subversionではファイル群はディレクトリで管理するので、ディレクトリ名を指定するだけでよい。

* CVSと同様、タグの概念を導入することもできる。Subversionでは、タグもその名前のついたディレクトリにすぎない。

II-8-3. デバッグの基本

C言語プログラミングにおけるデバッグの方法を概説する。デバッグプリントの挿入、デバッグ情報の埋め込み方に始まり、デバッガの利用方法、デバッグに利用するシンボル情報を有効にするコンパイル、ブレークポイントやステップ実行といったデバッグの基本的な作業について説明する。

【学習の要点】

* プログラムの入出力をプログラマが検証するには、いくつかの方法がある。単純に画面に変数を出力する方法や、デバッガを使ってプログラムの実行を途中で止め、その後はインタラクティブに実行する方法などが用いられる。

* 通常リリース時には、デバッグのための情報を保持している必要はない。gccのデフォルトの動きでは、デバッグ情報は保持されないので、gdbでデバッグする場合には、コンパイル時に明示的にデバッグ情報を保持するオプションをつける必要がある。

* C言語において、gdbでプログラムの動作を途中で止めるには、止めたい部分の関数名を指定してブレークポイントを設定する。ブレークポイントに遭遇すると、その関数を実行する直前で処理が止まり、インタラクティブなモードに移る。

図II-8-3. ステップ実行

【解説】

1) デバッグのパターン

* 具体的なデバッグの作業は、プログラムのある時点で変数にどのような値がセットされているかどうか(メモリの状態)を見て、その値が正しいかどうか確認することと、期待する値と食い違いが生じたソースコード上の場所を発見することである。

* デバッグの方法にはいくつかのパターンがある。

- 変数出力命令 (デバッグプリント) の埋め込み

最も原始的な方法で、ソースコード内にprintfのような変数の内容を出力する命令を組み込み、実行時に出力される内容を確認する方法である。

- デバッグシンボルの埋め込み

コンパイル時にデバッグシンボルを残す (gccでは-gオプションをつける) 方法。gdbのようなデバッガにてインタラクティブにプログラムをステップ実行し、必要であれば変数の内容やアドレスを出力することができる。

2) ブレークポイント

* デバッガでプログラムを実行する際に、プログラムを途中で止めるよう指定するのがブレークポイントである。gdbでは、ブレークポイントとして、ソースコードの行番号や関数名などが指定できる。

* 統合開発環境などでは、視覚的に行にブレークポイントを設定することができる。しばしば、画鋲(ピン)のアイコンで表される。

* ブレークポイントでプログラムを止めた後は、ステップ実行(一行ずつ実行するなど)を行なって、不良箇所を発見する。

3) ステップ実行

* ブレークポイントでプログラムを止めたら、以下のような操作により、インタラクティブに実行を継続する。

- ステップイン

現在の位置がサブルーチンの呼び出しポイントである場合、サブルーチンの内部へ移動する。

- ステップアウト

現在のサブルーチンの残りの部分を実行し、その呼び出し元へ戻る。

- ステップオーバー

現在の位置から、ソースコードの次の行へ移動する。現在の行がサブルーチンの呼び出しポイントである場合は、そのサブルーチンの実行後の状態となる。

* 不正なメモリ参照を行なった場合などは、ステップ実行は途中で失敗する。失敗が起こると不良箇所は見つけ易い。ステップ実行により実行した部分に不良が存在することが多いからである。

* 論理的な不良をデバッガによって発見したい場合は、ステップ実行によりその都度メモリの状態を検証する必要がある。

II-8-4. プロセスの生成と管理

プロセスの概念を簡単に説明し、forkシステムコールによって新たにプロセスを生成する方法、プロセス管理情報を得る方法、procファイルシステムを利用したプロセス情報の取得など、プロセスの生成と管理を行うプログラミング手法を紹介する。

【学習の要点】

* プロセスとは、カーネルによって管理される実行の単位である。すべてのプロセスはカーネルによってプロセスIDというユニークな番号が与えられる。

* あるプロセスが、新しくプロセスを生成するには、forkシステムコールを用いる。forkによって生成されたプロセスは、それを呼び出したプロセスの子プロセスとなる。

* カーネルはprocインタフェースを使用してプロセス情報をユーザに公開する。通常/procにマウントされる疑似ファイルシステムにあるプロセスIDが名前となったディレクトリ下のファイルから、そのプロセスに関する情報が取得できる。

図II-8-4. プロセスのforkとjoin

【解説】

1) プロセスの生成

* あるユーザプロセスは、forkシステムコールを使って自分自身の複製を生成することができる。複製によって生成されたプロセスは、forkを呼び出したプロセスの子プロセスと呼ばれ、それに対してforkを呼び出した側を親プロセスという。

* Linuxの多くのディストリビューションでは、すべてのプロセスはinitプロセスの子プロセスである。initプロセスは、カーネルが最初に生成するユーザプロセスである。

* 子プロセスは、ファイル記述子などのプロセスリソースを親プロセスと共有することができる。アドレス空間はコピーが作成され、それぞれ独立して管理される。

* 子プロセスは、exec系システムコールにより、親プロセスとは別のプログラムに置き換えることができる。

* fork後は親プロセスと子プロセスの処理は並列して実行されるが、親プロセスは、waitシステムコールにより、子プロセスの終了を待つことができる。

* 子プロセスの生成から終了までの一般的な流れ

- 親プロセス: forkシステムコールにより子プロセスを生成

- 親プロセス: (処理)

- 親プロセス: waitシステムコールにより、子プロセスの終了を待つ

- 子プロセス: execveにより自分自身を他のプログラムに置き換え

- 子プロセス: (処理)

- 子プロセス: exitシステムコールにより終了

- 親プロセス: 子プロセスの終了を検知

* 親プロセスと子プロセスは、そのままではアドレス空間は共有しない。これらのプロセスが互いに通信するには、シグナルまたはパイプ、共有メモリあるいはソケットを利用する。

2) プロセスの確認

* 通常/procにマウントされるproc疑似ファイルシステムは、現在システム上に存在するプロセスに関する情報を取得するための仕組みを提供する。

* /proc下にあるプロセスIDディレクトリ内には、statusファイルなどが存在し、シェル上からプロセス情報を取得するのに便利である。

* /proc直下には、meminfoやcpuinfoファイルが存在する。これらはそれぞれ、システムに搭載されている物理メモリ/仮想メモリに関する情報と、CPUに関する情報を含む。

* /proc下にあるプロセス毎の情報は、top(1)やps(1)シェルコマンドを使用して一覧表示することができる。

II-8-5. スレッドプログラミング

複数プロセスの利用よりも軽量な並列プログラムを実現するスレッドプログラミングについて解説する。プロセスとスレッドの違いについて述べ、pthreadライブラリを利用してスレッドプログラミングを実現する方法を説明する。

【学習の要点】

* アドレス空間はプロセス毎に割り当てられるため、通常複数のプロセス間でアドレス空間は共有されない。

* スレッドは、プロセス内における実行の単位であり、スレッド同士はアドレス空間を共有することができる。スレッドはしばしば軽量プロセスと呼ばれる。

* スレッドはアドレス空間を共有しながら独立にスケジュール可能なため、並列プログラムを実現する際に用いられる。

* pthreadは、POSIXによって定義された、Linux標準のスレッドライブラリである。

 

プロセス

POSIXスレッド

生成方法

fork

pthread_create

共有されるリソース

記述子テーブル、メモリマッピングなど

プロセスアドレス空間全体

相互通信

パイプ、ソケット、共有メモリなど各種IPC

プロセスアドレス空間の参照

主要な同期方法

セマフォ、共有メモリ上のミューテックス

ミューテックス、条件変数

 

図II-8-5. プロセスとスレッド

 

【解説】

1) プロセスとスレッド

* プロセスとスレッドのどちらも、それを生成した親とは独立してスケジュールが可能である。つまり、親と並列して処理を行なうことができる。

* 子プロセスは、親プロセスとの間でプロセスリソースを共有するが、通常アドレス空間を共有しない(コピーが作成され、それぞれ独立に管理される)。

* あるプロセスにおいてスレッドが生成されると、そのスレッドは親プロセスとの間でアドレス空間を共有することができる。

* スレッドは、プロセス内での実行単位である。このことから軽量プロセス(Light Weight Process [LWS])と呼ばれることがある。

2) スレッドプログラミング

* 複数のプロセスを扱うプログラムと複数のスレッドを扱うプログラムの異なる点は、スレッドの場合はスレッド間でアドレス空間が共有されることである。

* 複数のスレッドが同じアドレス空間にアクセスできるということは、スレッド間で共有リソースに対して適切な同期が必要であることを示唆する。

* スレッド間で共有リソースに対して同期を行なうにはいくつかの方法がある。ミューテックスは最も基本的でよく使用される同期機構である。ミューテックスは、あるアドレス空間にある共有リソースに対して複数のスレッドが同時にアクセスすることを禁止する。

3) POSIXスレッド

* POSIXスレッドは、POSIXの定めるスレッドAPIである。

* POSIXスレッドは、スレッド使用時に必要とされる、ミューテックスやロックの操作に関するインタフェースも定義する。

* Linuxカーネル2.6は、POSIXスレッドを使ってスレッド機構を提供する。これは、NPTL (Native POSIX Thread Library) と呼ばれ、GNU Cライブラリに統合されている。

* POSIXスレッドの定める主な関数

- pthread_create

スレッドを新しく生成する

- pthread_exit

スレッドを終了する

- pthread_join

スレッドを親スレッドに合流させる

- pthread_detach

スレッドを親スレッドからデタッチする。デタッチされたスレッドは、その終了と同時にメモリリソースが解放される。

- pthread_self

現在実行中のスレッドIDを取得する。

II-8-6. シグナルの利用

シグナルを利用して非同期プログラミングを実現する方法を説明する。シグナルの種類やシグナルの取り扱い方法、タイマー割り込みプログラムの作成方法、プログラムの強制停止とシグナルハンドラ等の概念について述べる。

【学習の要点】

* シグナルとは、プロセス間(またはカーネルからプロセス)で、予め定義されたイベントを送受信する仕組みである。

* プロセスは各シグナルを受信したときのデフォルトの動作を持っている。これを上書きするには、signalシステムコールを用いてハンドラ関数を登録する。ただし、プロセスを強制終了させるなどの一部のシグナルについてはこれを上書きすることはできない。

* singalをプロセス間で用いると、互いにプロセス間通信を行うことができる。

* alermシステムコールとpauseシステムコール、SIGALRMシグナルハンドラを組み合わせると、タイマー割り込みプログラムを作成することができる。

SIGHUP

Hangup

SIGSTKFLT

Stack fault

SIGINT

Interrupt

SIGCLD (SIGCHLD)

Child status has changed

SIGQUIT

Quit

SIGCONT

Continue

SIGILL

Illegal instruction

SIGSTOP

Stop, unblockable

SIGTRAP

Trace trap

SIGTSTP

Keyboard stop

SIGABRT

Abort

SIGTTIN

Background read from tty

SIGIOT

IOT trap

SIGTTOU

Background write to tty

SIGBUS

BUS error

SIGURG

Urgent condition on socket

SIGFPE

Floating-point execution

SIGXCPU

CPU limit exceeded

SIGKILL

Kill, unblockable

SIGXFSZ

File size limit exceeded

SIGUSR1

User-defined signal 1

SIGVTALRM

Virtual alarm clock

SIGSEGV

Segmentation violation

SIGPROF

Profiling alarm clock

SIGUSR2

User-defined signal 2

SIGWINCH

Window size change

SIGPIPE

Broken pipe

SIGPOLL (SIGIO)

Pollable event occurred

SIGALRM

Alarm clock

SIGPWR

Power failure restart

SIGTERM

Termination

SIGSYS

Bad system call

図II-8-6. signal.hに記載のあるシグナル

【解説】

1) シグナルの送受信

* シグナルは、あるプロセスに対して、カーネル、他のプロセス、自プロセス、あるいはユーザが特定のイベントを送信するソフトウェア割り込みである。

* プロセスに対して送信されたシグナルは、そのプロセスの用意するシグナルハンドラによって捕捉される。プロセスが明示的にシグナルハンドラを用意しない場合は、デフォルトのアクションが実行される。

* シグナルハンドラを用意することで、プロセスがシグナルに対するデフォルトのアクションを上書き、拒否、または無視することを可能にする。

* カーネルの送信する一部のシグナルは、シグナルハンドラによって捕捉できない。これらは、SIGSTOPとSIGKILL の2つのシグナルであり、プロセスの暴走時に強制的に終了させる手段を残すためである。

2) シグナルを使ったプログラミング

* プロセスがシグナルを捕捉するようにハンドラを用意するには、signalシステムコールを用いる。signalは、受け取るべきシグナルの種類を示す整数値と、ハンドラ関数へのポインタを取り、以前のハンドラ関数を返す。呼び出しに失敗するとSIG_ERRを返す。

* Linuxでは、ハンドラ関数ポインタとsignalシステムコールは、以下のように定義されている。

typedef void (*sighandler_t)(int);

sighandler_t signal(int signum, sighandler_t handler);

* このとき、ハンドラ関数handleは以下のようにしてsignalシステムコールに渡すことができる。

sighandler_t h = &handle;

signal(SIGBUS, h);

* POSIX.1 (IEEE Std 1003.1) 標準のシステムでは、signalよりも一般的なsigactionシステムコールを使うことができる。これらのシステムではsignalはsigactionを用いて実装されている。sigactionは、シグナルの生成とハンドラの呼び出しに関してより細かい制御が可能である。

* kill, killpgは、それぞれ他のプロセス、プロセスグループに対してシグナルを送信するのに使うシステムコールである。

3) シグナルを使ったプログラミングの用途

* シグナルはもともと、プロセス間通信を行なうために設計されたものではないが、固定的なイベントを送受信するだけである場合、その目的に使うことができる。

* 実装例として、シグナルSIGALRMの送信と、pauseシステムコール (POSIX.1システムではsigsuspendを使用可能) を組み合わせたタイマーアプリケーションが考えられる。

II-8-7. パイプによるプロセス間通信

プロセス間通信を実現する手法のひとつとして、パイプを利用した通信手順を紹介する。パイプの作成と入出力制御、名前付きパイプの利用方法などについて、関連する関数を利用してプロセス間通信を実現するための具体的な手順を説明する。

【学習の要点】

* パイプは、一次元バイト列であり、プロセス間でプロセス間通信を行うひとつの方法である。

* パイプはpipeシステムコールを用いて生成する。

* パイプで通信できるプロセス同士は、互いに記述子を共有できる間柄(多くの場合、親子関係)になければならない。

* 親子関係にないプロセス同士がパイプを用いて通信を行うには、FIFO(名前付きパイプとも呼ばれる)を使用できる。この場合は、ファイルシステム上のスペシャルファイルを共有することによって同期が取られる。

図II-8-7. 親子プロセス間でパイプを作成

【解説】

1) パイプ

* パイプは、名前を持たないカーネル内のバッファであり、pipeシステムコールを用いて作成する。pipeは、2つのファイル記述子を作成する。それぞれ、読み込み用と書き出し用で、プロセスはこれらの記述子に対するreadやwriteシステムコールを通してパイプにアクセスする。作成例を以下に示す。

int p[2];

pipe(p);

* pipeによって作成されたファイル記述子を、プロセス間で共有することで、2つのプロセスはカーネル内の同じバッファ領域にアクセスすることが可能である。これは、これら2つのプロセスが互いに任意のデータをやり取りできることを意味する。

* forkによって親子関係を持つプロセス同士では記述子の共有が容易であるため、ほとんどの場合パイプはこの状況下で使用される。

* ひとつのパイプは単方向のデータの流れを提供する。これに従って、forkによって記述子を共有するプロセスは、それぞれ一方は呼び込み用記述子をクローズ、他方は書き出し用記述子をクローズする。

* 双方向のデータの流れが必要な場合は、パイプを二つ用意する。

* パイプはシェルで最もよく利用される。これは、読み込み用の記述子を標準入力、書き込み用の記述子を標準出力にそれぞれマップすることで行なう。

2) FIFO

* FIFO (First In, First Out) は、パイプに似た機能を提供する特殊ファイルである。スペシャルファイルであることから、これはファイルシステムの名前空間上にマップされる。このため、これを名前付きパイプと呼ぶことがある。

* FIFOは、mkfifoシステムコールを用いて作成する。

mkfifo(“/tmp/myfifo”, mode);

* パイプと違い、ファイルシステムのパスと関連付けられる(名前を持つ)ため、関係のないプロセス同士が名前で同一FIFOにアクセスすることができる。

3) パイプ、FIFOに共通の性質

* パイプもFIFOもカーネル内に置かれるバッファである。バッファの長さはシステムに依存し、POSIX.1では最低512バイトを要求する。

* パイプやFIFOのサイズに収まるデータをwriteによって書き込んだ場合、writeは不可分性を保証する。もし、パイプやFIFOのサイズ以上のデータを書き込んだ場合にはこれは保証されない。

* パイプやFIFOは、何もしなければブロッキングモードのファイル記述子を使用することになる。fcntlを使用して記述子を非ブロッキングモードに設定することで、パイプまたはFIFOの処理を非ブロッキングモードで実行できる。

II-8-8. デバイスファイルの扱い方

キーボード、ディスプレイ、プリンタ、ディスクといった全てのデバイスをファイルとして一元的に取り扱う考え方を説明する。また、入出力先を変更するリダイレクトについて述べ、デバイスファイルを簡単に利用する方法を紹介する。

【学習の要点】

* 各種ハードウェアデバイスは、カーネル内のデバイスドライバによって操作される。ユーザは、スペシャルファイル(デバイスファイル)を通してカーネル内のデバイスドライバの機能にアクセスできる。

* スペシャルファイルは、カーネルによって特別に扱われる一方、ユーザからは通常のファイルのように見える。このためプログラムからは、open, read, writeなどのシステムコールを用いて記述子を取得し、アクセスすることができる。

* ある記述子に関連づけられたストリームを、別の記述子に関連づけることを入出力リダイレクションと呼ぶ。通常標準出力に出力されるストリームをファイルに出力するには、端末デバイスの記述子1を閉じ、続いて目的のファイルを開いて新たに記述子1を獲得することで行われる。

図II-8-8. dup2によるリダイレクト

 

【解説】

1) デバイスファイルの扱い方

* Linuxカーネルは、ハードウェアデバイスに対するデバイスドライバを実装する。

* デバイスファイルは、ユーザプロセスが、カーネル内のデバイスドライバにアクセスすることができるようにカーネルが提供するインタフェースである。これはスペシャルファイルとも呼ばれる。ファイルシステム上に名前を持ち、ユーザプロセスは通常のファイルと同じread, writeなどのシステムコールを用いてアクセスできる。

* デバイスドライバ側は、ユーザプロセスからのシステムコールを受けて、それらに対応するオペレーション関数を実装している。この中には、通常のファイルに対しては使用しないような、ioctl, pollシステムコールに対応するオペレーション関数も含まれる。

2) デバイスファイルを使ったプログラミング例

* Linuxには、/dev/rtcというハードウェアクロックを制御するためのキャラクタデバイスファイルが存在する。このデバイスファイルを使ったプログラミングの例を示す(エラー処理は省略してある)。

* /dev/rtcは、デバイスファイルなのでopenシステムコールでオープンすることができる。ファイル記述子を得る。

int rtcfd;

rtcfd = open("/dev/rtc", O_RDONLY);

* ioctlシステムコールは、主にデバイスを設定するのに使用される。この例では、ioctlで割り込み周期(tick)を8192に設定する。

ioctl(rtcfd, RTC_IRQP_SET, 8192);

* ioctlで制御できる項目と引数の型は、デバイスドライバ毎に異なる。通常デバイスドライバのヘッダファイル(この例では、/usr/include/linux/rtc.h)で知ることができる。

* readシステムコールで、デバイスファイルの内容を読む。この例では割り込みを待つ(読み込み可能になるまでブロックされる)。値を読み込むことができるとreadが返る。

unsigned int data;

read(rtcfd, &data, sizeof(data));

* デバイスファイルの使用を終えたら、closeシステムコールでクローズする。

close(rtcfd);

3) デバイスファイルの入出力リダイレクト

* シェルのリダイレクトを利用して、デバイスファイルに対して通常のファイルのように、入出力先を変更することができる。例えば以下の例は、シリアル端末 (/dev/ttyS0) からの情報をターミナルエミューレータの画面で表示する入力リダイレクトの例である。

cat < /dev/ttyS0

II-8-9. セマフォ、共有メモリ、メッセージキューの利用

プロセス間通信で必要な手段であるセマフォ、共有メモリ、メッセージキューについてそれぞれの概念を理解させる。セマフォを利用した排他制御、共有メモリを利用したプロセス間のデータ転送、メッセージキューを利用したプロセス間通信の具体的な方法を解説する。

【学習の要点】

* プロセス同士が通信する際には、一方のプロセスが、他方のプロセスの処理の完了を待つ必要がある場合がある。これを、互いのプロセスの同期を取るという。

* プロセスを同期する代表的な手法に、セマフォを用いる方法がある。よく使われるバイナリセマフォは、0と1の状態を持ち、プロセスが処理を続行可能であるか否かの判断を行うために用いられる。

* プロセスはそれぞれが独自のアドレス空間を持ち、原則として互いのメモリ領域に踏み入ることはできない。例外として、共有メモリは二つ以上のプロセスがアドレス空間を共有することを可能にする。

* メッセージキューはカーネルによって保持されるキューで、プロセスやスレッドはメッセージを置いたり取り出したりといった操作を非同期で行うことができる。

図II-8-9. 2つのスレッドが共有リソースにアクセスする例

【解説】

1) 同期とセマフォ

* 複数のプロセス間、またはスレッド間でデータを共有する場合、一方のプロセスまたはスレッドがデータの内容を変更している間、他方のプロセスまたはスレッドはその変更の完了を待つことが必要である。このことを同期をとるという。

* Linuxは複数の同期手段を提供する。ミューテックスと条件変数を使った同期、セマフォを使った同期が比較的よく利用される。

* セマフォを使った同期の基本的な概念は、カーネル内またはファイルシステム名前空間上に存在する単一のセマフォを、複数のプロセスが監視することである。この流れは概ね以下の通りである。

- プロセス1と2は、セマフォへの参照を作成する。

- プロセス1が、自分の処理が続行可能かどうかをセマフォに問い合わせる。

- セマフォはプロセス1の処理の続行を許し、それを記録する。

- プロセス2が自分の処理が続行可能かどうかをセマフォに問い合わせる。

- セマフォはプロセス1が処理中にプロセス2の処理の実行を許すかどうか決定する(このポリシーはセマフォの種類により異なる)。

- セマフォが処理の続行を許せばプロセス2は処理を続行する。そうでなければプロセス1の処理の完了を待つ。

2) 共有メモリ

* ひとつのプロセス内の複数のスレッドは、それぞれがプロセスのアドレス空間を共有することができるが、異なるプロセス同士は通常これができない。

* プロセスのアドレス空間は、通常自身のプロセスからのみ参照を許すが、共有メモリの仕組みを利用すると、プロセスは自分のプロセス空間のある範囲を他のプロセスに公開することができる。

* 共有メモリ領域は、参照を許されたそれぞれのプロセスから直接に(カーネル内バッファを経由せずに)読み書きできるため、最も高速なプロセス間通信といえる。

* それぞれのプロセスは並列に動作するにも関わらず共有メモリ領域を参照できるので、この領域を不正に変更されることのないよう適切な同期をとる必要がある。

3) メッセージキュー

* メッセージキューは、リンク構造を持ったカーネル内の構造体であり、非同期のプロセス間通信を実現する方法である。

* あるプロセスがメッセージキューにメッセージを置くと、構造化されたデータ(レコード)としてカーネル内に保持される。次に別のプロセスは、このレコードを読むことができるようになる。

* それぞれのメッセージには優先度を設定することができる。

* メッセージを読む側のプロセスが、キュー内のメッセージの有無を知る方法は、ブロックして待つ(同期をとる)か、ブロックせずにポーリングするか、シグナルによって通知を受けるかのいずれかである。

II-8-10. ソケットによるネットワーク通信

ネットワークを超えた通信を可能とするソケットの概念を解説する。さらにソケットを用いたネットワーク通信の具体的な手順を示し、サーバプログラムの簡単な動作やホストバイトオーダとネットワークバイトオーダの違いといった関連する話題についても触れる。

【学習の要点】

* ソケットは、ネットワークを超えてプロセス同士が互いに通信を行うことを可能にする。

* ソケットAPIは扱うネットワークに依存しない。カーネル内で定義されているドメインを指定することで扱うネットワークを指定できる。

* ソケットは、socketシステムコールにより生成され、bindシステムコールによりドメイン内で有効な名前を付与する。

* バイトオーダとは、複数バイトを保存する際に、1バイトずつをどういう順序で保存するかを決定するものである。ネットワークを介して通信する際にはネットワークバイトオーダを使用する。

図II-8-10. TCPサーバとクライアントの接続

【解説】

1) ソケットAPI

* ソケットAPIは、ホスト内のプロセス間通信だけでなく、ネットワークを越えてホスト間で通信を行なえるように設計されたものである。

* ソケットAPIはネットワークのプロトコルに依存しないように設計されている。Linuxでは、UNIXドメイン、INETドメイン、BLUETOOTHドメインなどがソケットAPIを使用する。

* ソケットAPIで「ソケット」とは、通信の端点(エンドポイント)を意味する。ソケットは、socketシステムコール(厳密にはLinuxではシステムコールではない)を用いて作成することができる。

* エンドポイントを識別するために、ソケットにはプロトコルアドレスが割り当てられる。プロトコルアドレスはsockaddr構造体で総称(抽象化)され、その実装はプロトコルに依存する。

* ユーザプロセスは、ソケットAPIの提供する一連の関数を用いてカーネルとデータをやりとりする。カーネルは、下位のネットワークサブシステムを呼び出し、実際の通信を行なう。

2) TCPソケット

TCP通信を実現することは、おそらく最もよくあるソケットAPIの利用例である。TCP通信を行なうための使用する主な関数は以下の通りである。

- socket

システムコールによりソケットを作成する。このときTCPプロトコルでの通信を行なうよう、適切な引数を指定する。

int sock = socket(AF_INET, SOCK_STREAM, 0);

- connect

TCPクライアントがサーバに接続する際に用いる。ソケット、サーバのTCPプロトコルアドレス(IPアドレスとポート)を指定する。

connect(sock, (struct sockaddr *) &srv, sizeof(srv));

- bind

ソケットにプロトコルアドレスを割り当てる。通常クライアントではこれを省略する。サーバでは、自身のローカルアドレスを割り当てる。

bind(sock, (struct sockaddr *) &local, sizeof(local));

- listen

TCPサーバがクライアントのconnect要求を待つ。サーバは接続してきたクライアントをキューに入れて保持する。

listen(sock, 5);

- accept

TCPサーバが接続確率済みキューからクライント接続を取り出す。

accept(sock, (struct sockaddr *) &client, sizeof(client));

3) バイトオーダ

バイトオーダとは、複数バイトを保存する際に、1バイトずつをどういう順序で保存するかを決定するものである。ホストによってバイトオーダは異なる場合があるため、ネットワークを介して通信する際には、一貫してネットワークバイトオーダを使用する。