43
システムプログラミング演 2002/10/02 目次 1 はじめに 2 2 演習の環境 3 2.1 .................................... 3 2.2 ................................ 3 3 mini-web サーバの構造 4 3.1 .................................. 4 3.2 から mini-web サーバ ................... 4 4 基礎 1: ネットワーク (ソケット)API 6 4.1 .................................. 6 4.2 インターネット .......................... 6 4.2.1 プロトコルレイヤ ......................... 6 4.2.2 IP アドレス ............................ 7 4.3 ソケット API .......................... 10 4.4 .................................. 11 4.5 ソケット .......................... 12 4.6 : エラーチェックを怠ら いこ .............. 12 4.7 アドレス ポート 割り ...................... 13 4.8 待ち .................................. 14 4.9 .................................... 15 4.10 データ ............................... 15 4.11 データ ............................... 16 5 課題 1: ソケットの練習 16 6 基礎 2: HTTP HTML 18 6.1 じめに .................................. 18 6.2 HTTP ................................... 18 6.3 telnet : TCP クライアント ..................... 19 6.4 HTML ................................... 20 1

システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

Page 1: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

システムプログラミング演習

田浦健次朗

2002/10/02

目 次

1 はじめに 2

2 演習の環境 3

2.1 基本 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

2.2 巷の情報源 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

3 mini-webサーバの構造 4

3.1 予備知識 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4

3.2 外から見たmini-webサーバの動作 . . . . . . . . . . . . . . . . . . . 4

4 基礎 1: ネットワーク (ソケット)API 6

4.1 基本概念 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

4.2 インターネットの復習 . . . . . . . . . . . . . . . . . . . . . . . . . . 6

4.2.1 プロトコルレイヤ . . . . . . . . . . . . . . . . . . . . . . . . . 6

4.2.2 IPアドレス . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

4.3 ソケットAPIの全体像 . . . . . . . . . . . . . . . . . . . . . . . . . . 10

4.4 共通事項 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11

4.5 ソケットの生成と解放 . . . . . . . . . . . . . . . . . . . . . . . . . . 12

4.6 重要な余談: エラーチェックを怠らないこと . . . . . . . . . . . . . . 12

4.7 アドレスとポートの割り当て . . . . . . . . . . . . . . . . . . . . . . 13

4.8 接続待ち . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14

4.9 接続 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

4.10 データの送信 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15

4.11 データの受信 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

5 課題 1: ソケットの練習 16

6 基礎 2: HTTPとHTML 18

6.1 はじめに . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

6.2 HTTP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18

6.3 telnet : 万能TCPクライアント . . . . . . . . . . . . . . . . . . . . . 19

6.4 HTML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

1

Page 2: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

7 課題 2: 最小webサーバ 22

8 課題 3: シングルスレッド/セッションサーバ 22

9 基礎 3: 並行プログラム (スレッドAPI) 23

9.1 基本概念 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

9.2 共通事項 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

9.3 生成 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25

9.4 終了 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

10 基礎 4: 共有メモリの更新と同期 26

10.1 排他制御 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26

10.2 WaitForSingleObjectについて . . . . . . . . . . . . . . . . . . . . . . 29

10.3 セマフォ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30

10.4 条件変数 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32

11 課題 4: スレッドと同期の練習 36

12 課題 5: マルチスレッドサーバ 36

12.1 概略 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36

12.2 リクエストスレッドと,セッションスレッド間の同期について . . . . 37

13 課題のまとめ 40

A Webサーバの構成法 40

A.1 スレッド vs プロセス . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

A.2 生成 vs プール . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

A.3 CGI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

A.4 非同期 I/O . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42

1 はじめにプログラムを作るには,アルゴリズムやデータ構造に関するスキルも必要だが,実用的には,プラットフォーム (オペレーティングシステムやプログラム言語) が提供する機能を理解し,それを使いこなすスキルが不可欠である.例えばファイルの読み書き,プログラムの起動,ネットワークからのデータの入出力,などは全て,最も下位レベルでは,オペレーティングシステムがその機能を提供し,プログラミング言語がそれを呼び出すためのライブラリを提供し,応用プログラムがそれを呼び出すことによって機能している.上にあげた機能はどれも,実用的なプログラムにとってはあまりにも基本的な機能であり,これらを使わずに,有用なプログラムが書けることはまずない.メールもWWWも,ChatもGnutellaもばっさり単純化した見方をすれば,ネットワークやファイルからデータを読んだり書いたりしているだけとも言える.そこで本演習では,これらの,オペレーティングシステムが提供する基本機能を習得,それを組み合わせて実用的なプログラムを構築することを目標とする.具体

2

Page 3: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

的な課題として,少し変な動作をする “mini-webサーバ”を作ることを目標とする.これはもちろん本物のWebサーバの機能を全て実装するものではないし,実はその基本的な機能すら実装しないのだが,通常のブラウザとWebサーバ間で使われているプロトコルを利用する.したがって構築したWebサーバに,普段利用しているブラウザでアクセスすることが出来る.実装する機能は単純さを優先した,つまらない (しょぼい)ものだが,プログラム全体の構成は,実用的なネットワークサーバでも踏襲できる構成をとっており,一度経験しておくとよいものである (付録として,ネットワークサーバの典型的な構成としてよく用いられる,そのほかの選択肢を述べておく).

2 演習の環境

2.1 基本

Cプログラムが開発できる環境と,動作確認のためのwebブラウザがあれば特別なソフトは必要ない.telnetコマンドが動くことを確認しておくとよいかもしれない.UNIXの gcc, WindowsのVisual C++ (Visual Studio), Cygwinなどで本課題に沿った演習を行うことが出来る.ただし,以下で細かい説明は,LinuxとWindowsのVisual C++に準拠したものになっているかもしれない.

2.2 巷の情報源

これから述べるネットワークプログラミングや,並行 (スレッド)プログラミングについては,多数の参考書が出ている.前者については,「ソケットプログラミング」の類のキーワードのついた本を,後者については,スレッドプログラミング,POSIX

スレッド,Pthreadsなどのキーワードの着いた本を探すとよい.各 APIのもっとも簡単な情報源はオンラインマニュアルである.UNIX (Linux,

Solarisなど)であれば,

man ⟨ API名 ⟩

でマニュアルページを引くことが出来る.Windowsの APIについては,Microsoft

の Visual Studio, Visual C++などの製品についてくる,MSDNライブラリというオンラインマニュアル集が役に立つ (電気の図書では,Visual Studio, Visual C++,

MSDNライブラリとも貸し出している).その中の「キーワード」というタブを選択してAPI名をキーワードとして検索すれば,情報が得られる.どちらも,実際にプログラムで使用するときは,利用前に一度必ず検索して,ついでに使い方のテンプレートをカット& ペーストするのがよい.以下の説明はあくまでAPI全体の概念整理で,ひとつひとつの細かいパラメータの意味などについてはいちいち説明していない.ここでの説明の都合に応じて,· · ·などと省略したり,よく使う値 (たとえば 0など)を勝手に入れてしまっているので,それらが何を意味するかは適宜自分で確認してほしい.

3

Page 4: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索エンジンなどを利用して,そちらも参照されたい.

3 mini-webサーバの構造

3.1 予備知識

ブラウザでWebのデータを閲覧しているとき,そのデータは「Webサーバ」と呼ばれるプログラムから供給されている.例えば,ブラウザで,

http://www.logos.t.u-tokyo.ac.jp/~tau/index.html

というページを閲覧しようとしたときは,www.logos.t.u-tokyo.ac.jp という計算機で動作しているWebサーバにネットワークを通して,「/~tau/index.htmlというページをよこせ」という要求が送られる.その要求を受け取ったWebサーバが“/~tau/index.html”という文字列 (パス名という)を解釈する.解釈の結果,「通常は」パス名があるファイルに対応しており,対応するファイルが読み込まれ,その内容が,いくらかの付随情報 (例えばこの内容は画像です,などの情報)とともにブラウザに送り返される.ブラウザは送り返された情報を読んで,画面に表示したり,さらなるページをサーバに要求したりする.本課題の目標は,上で日本語で述べたプロセスの詳細を理解し,かつプログラムとして実現することである.といっても現代のWebサーバの機能全てを短期間で作ることはできないし,かといって,ファイルを表示するだけのWebサーバを作ってもあまり面白くないので,少し変態的な動作をする (より上品な言葉で言えば「オリジナリティの高い動作をする」) mini-webサーバを作ってみよう.上で,Webサーバが “/~tau/index.html”のような要求を受け取ったとき,「通常は」パス名があるファイルに対応しており,この内容がブラウザに送り返される,と述べた.しかしこれはあくまで普通のWebサーバがそうなっているという慣例に過ぎず,理屈上は,Webサーバはこの文字列を自由に解釈して,その文字列に応じた,勝手な動作をすることが可能である.ブラウザは,Webサーバから送られたデータを素直に表示するだけで,そのデータの「出所」が何なのかは,わからないからである.実際普通のWebサーバも,設定ファイルで指定されたパス名を,コマンドが格納されているプログラム名と解釈して,そのプログラムを起動する (通常,cgiなどと呼ばれている)機能を持っており,これを用いてWebサーバに色々と複雑な動作をさせている.本課題では,送られたパス名を好きなように解釈して,好きな動作をする “mini-

webサーバ”プログラムを作成し,それを実際にブラウザを用いてアクセスする課題を作成する.

3.2 外から見たmini-webサーバの動作

ここでは,「なるべく単純だが,少しは意味のある,それでいて少しもしつこくない」動作として,以下のようなものを考える.これは,「記憶力ゲーム」とでもいうも

4

Page 5: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

ので,ブラウザで,あるページにアクセスすると何桁かの数字 (例えば,297572897)

を表示する.そこにある「ゲームスタート」のリンクをクリックすると,0~9までの数字と,「終了」というリンクを持つページを表示する.ユーザが正しい一桁目の数字を無事クリックすれば再び同じページを表示し,そうでない場合は「エラーページ」を表示する.これを繰り返して,ユーザが正しい全ての数字を正しい順番でクリックし,その後に「終了」をクリックすると「おめでとうページ」を表示する.本題であるネットワークのアクセスの仕方や,並行処理の勉強のために,外から見たサーバの動作は極力単純にした.そのためゲームとしてはいささかサムイゲームであるのは否めない.余力のある人は,より面白い動作を考えて,是非それを実現してみてほしい.例えばこの延長上で,オセロゲームサーバなどを作ることが可能だろう.また,通常のWebサーバの最も基本的な機能である,「パス名をファイル名として解釈して,その内容をブラウザに送り返す」機能も追加してみると良い.上で,「クリック」というユーザの行う動作を述べたが,サーバプログラムにとっては,何がクリックされたかは関係がなく (知ることは出来ず),サーバプログラムに送られるのは「パス名」である.したがってサーバプログラムの動作は,クライアントからのリクエストを読み,その中のパス名を読む.そして,「どのようなパス名が送られてきたら何をするか (どのような文字列をブラウザに送り返すか)」がプログラムされており,それに従い動作するのである.最終課題では,サーバは次の要件を満たすものとする.これにより,ゲームとしては単純でありながらも,プログラムとしてはそれなりにややこしいものになる.これは現実のWebサーバであれば必ず実現しなくてはならない用件である.

• 複数のリクエストに対する処理を並行して行う.すなわち,仮にひとつのリクエスト (Aとする)を処理するのに,運悪く 5秒かかったとする (もちろん我々の「記憶力ゲーム」ではそんなことはありえないが,ここでは一般に,そういうことがありうると仮定するのである).そのときに,その 5秒間の処理中に,次のリクエスト (Bとする)が来たら,Bの処理がなかなか (Aが完了するまで)始まらない,などということがあってはならない.

• 複数のゲームが同時進行可能.つまり,数字列 378827に対してクリックを繰り返す人と,数字列 7689827に対してクリックを繰り返す人が同時に存在してもよく,サーバはその二つを「ごっちゃにしない」.

以降ではまず,本課題を達成するために必要なバックグラウンドを 3つに分けて説明し,各項目に関する小課題を出す.最後にそれらを変更,組み合わせてmini-web

サーバを完成させる.

• ネットワーク (ソケット)API

• Webの仕組み (HTTPとHTML)

• 並行処理 (スレッド)API

5

Page 6: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

4 基礎1: ネットワーク (ソケット)API

4.1 基本概念

最初にネットワーク入出力の話に入る.UNIXでもWindowsでも,ネットワーク入出力のためのAPIとしてはほとんど共通の,ソケットというAPIが用意されている.ここではこのソケットを用いて,インターネットにつながれたコンピュータ同士が通信を行うための手順を学習する.細かい注意をしておくと,「ソケット」と「インターネット上の通信」は,本来全く直交した概念である.ソケットは,プロセス間通信をするために設計されたAPI (プログラムに対して提供するインタフェース)であり,プログラム上ソケットを用いていても,実際に使われている通信の仕組み (プロトコル)はインターネットとは限らない.例えばひとつのUNIXコンピュータ内の二つのプロセス間の通信もソケットAPIを用いてプログラムされるし,その通信プロトコルはインターネットとは関係がない (単にOSがプロセスのメモリ間でデータをコピーしているだけだろう).逆に,インターネットプロトコルは純粋に通信プロトコル (コンピュータ間でどのようなパケットがやり取りされるかを規定する) であって,それをプログラムから使うのにソケット以外のAPIというのも考えられる.しかしここではそのような区別にはこだわらず,「ソケットはインターネットを使う手段,インターネットを使うにはソケットを使ってプログラムを書く」という理解 (誤解)をしたまま説明をする.それは実際,今時のプログラマが日々生活する環境の近似としては間違っていない.なぜ上のような注意をしたかというと,「ソケットはインターネット以外にも使えるように設計されている」ということを知っておかないとソケットのAPIがとても回りくどくてわかりにくいものに思えるかもしれないからである.本題に入る.まず生成のための API (socket)があり,その結果としてソケット

(ネットワークへの口と思えばよい)が返される.そのソケットを指定して,データの読み書きをするAPI (send と recv)を用いてデータのやり取りを行う.片方のソケットに書き込んだデータが接続されたもう一方のソケットに届く.使い終わったソケットは close (UNIX)ないし,closesocketというAPIを用いて破棄する.以下ではこのAPIを詳細に解説する前にインターネットの基本的な概念を復習しておこう.

4.2 インターネットの復習

4.2.1 プロトコルレイヤ

IP インターネットの最も下層には,「指定されたあて先 (IPアドレス)に向けてパケットをできるだけ届ける」というレイヤ (インターネットプロトコル; IP)がある.ポイントとしては,

• パケットは送られた順序で到着するとは限らないし,そもそも到着するという保証すらない.

• パケットは固定長であり,大きなデータは多数のパケットに分けて送られる.

6

Page 7: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

• あて先は IPアドレスのみであって,同じコンピュータ内の複数のあて先を区別する手段 (たとえばwebブラウザとメールソフト)は,このレイヤには存在しない

これ以上の機能は以下に述べるUDPおよびTCPというレイヤで実現される.

UDP IPプロトコルと同じ到着保証 (無保証というべきか)を持つ.つまりパケットは到着するとは限らないし,順番も保証されない.パケット長も固定である.唯一 IPよりも高機能なのは,あて先として,IPアドレスとポート番号 (0から 65,536

までの整数)の組を指定する点である.つまり,ひとつの計算機がネットワークへの口を複数もち,それをこの番号で区別できるという点である.

TCP Web, メール, リモートログイン,ファイル転送,chatなど,ありとあらゆるネットワークアプリケーションを構築する基礎になっているのが,このTCPである.ポイントは,

• UDP同様,ポート番号を用いて 1計算機内の複数の口を区別できる.

• 信頼性のある (reliable),順序を保存する (ordered)通信を提供する.つまり,送られたデータは失われることなく必ず到着し,かつ送り出された順に到着する.

• いきなりデータを送ることはできない.通信を始める前に接続と呼ばれる手順が必要である.

IPという信頼性のない通信レイヤの上に,どうやって信頼性のある通信を実現するんだろう,と疑問に思った人は頭が働いている証拠である.インターネットに関する講義で学んでいただきたい.ソケットAPIでは,上記 3つのいずれのレイヤも利用可能だが,一般ユーザに利用可能なのは後者二つだけである.ここではさらに,まったく pragmaticな観点から,一般プログラマとして最初に覚えるべき,もっとも重要な通信手段はほとんどの場合TCPである,とばっさり言い切ってしまおう.そして以下ではソケットAPI

を用いてTCP通信を行う方法を説明する.なにしろ,送ったデータがとどかないかもしれない,という前提でアプリケーションを書くのは難しく,何か意図がない限り,好んでそのような前提でプログラムを書くことはないからだ.ちなみに,言葉の問題だが,インターネットプロトコル (IP)は技術的にはここで述べた,信頼性のない,最下層の通信プロトコルのことである.しかし通常インターネットの通信プロトコルといった場合には,IPの他にUDPとTCPを含む.そしてTCPがあまりにも重要なので,TCP/IPという言葉が良く使われる.実際にソケットAPIにたどりつくまで,もう少し辛抱してインターネットの基礎を復習しておこう.

4.2.2 IPアドレス

何はなくとも最も基礎である,IPアドレス.現状で広く使われている IPv4と,時期バージョンの IPv6があるという話を聞いたことはあると思うが,ここでは IPv4

7

Page 8: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

の話をする.IPv4では IPアドレスは 32 bitの情報で,それを通常は 4つの 8 bit整数 (0 · · · 255)に分割して,133.11.23.16のように表示する.世界中のコンピュータにひとつかふたつくらいの IPアドレスをふって,とにかく IPアドレスさえ指定すれば,そのコンピュータが世界のどこでどうつながれていようと通信が出来る,というのがインターネットのそもそものアイデアだった.そのために,一般プログラマの知らないところでルータと呼ばれる機械がパケットを右から左へたらいまわししている.これがどういう仕組みで動いているか,まさか世の中に存在するコンピュータを全て管理している事務室があるわけではあるまいし,と不思議に思った人は頭が動いている証拠で,インターネットの授業をまじめに聞いてほしい.自分のコンピュータが「インターネットにつながっている (例えばブラウザでasahi.com

が読める)」時には,必ず IPアドレスが振られている.そのアドレスは,Windows

であれば ipconfig, UNIXであれば/sbin/ifconfig -aというコマンドでたしかめられる.これはインターネットにつながらないときのトラブルシューティングの手段の一つでもあるので,一度は使ってみること.

GOEDEL:system-programming% ipconfig

Windows IP Configuration

Ethernet adapter ワイヤレス ネットワーク接続:

Connection-specific DNS Suffix . :

IP Address. . . . . . . . . . . . : 10.14.20.8

Subnet Mask . . . . . . . . . . . : 255.255.0.0

Default Gateway . . . . . . . . . : 10.14.0.1

GOEDEL:system-programming%

この例では,10.14.20.8というアドレスが割り振られていた.隣に友達がいたら,友達のと確かに違うということを確認しておこう.1

このアドレスを自分で設定した,という場合もあるだろうが,多くの場合自分でアドレスを設定してはいないはずである.「自分は何も設定していないのに,いつ誰がこのアドレスを決めたんだろう?」と思った人は頭が動いている証拠で,これはDHCPというプロトコルによって,コンピュータがインターネットに物理的に接続したときに,自動的に重複なく割り振られたものである.さて,こうしてお互いの計算機の IPアドレスがわかったら,あとはソケットAPI

を覚えれば,「好きなコンピュータ間でデータを送る」ことができるはずである.それが本来のインターネットのはずだ.

ローカルアドレス 残念ながら話はそう単純ではなく,世の中のアドレスに 2種類存在する.ひとつは,文字通り世界中どこでも通用するグローバルアドレス, もう

1いや,実を言うと同じということもありうる.それは以降の説明を読んでほしい.

8

Page 9: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

ひとつは,そうではない,ローカルアドレス (プライベートアドレス)である.二つの区別は簡単で,以下のアドレスはローカルアドレスであると決められている.

• 10.*.*.* – 10.255.255.255

• 172.16.0.0 – 172.31.255.255

• 192.168.0.0 – 192.168.255.255

ローカルアドレスに向けられたパケットは,インターネットの通常のルールに反して「世界中のどこへでも」旅をしない.そうではなく,同じLANの中にのみ配達され,それ以上は旅をしない.LANの厳密な定義もまたやろうとすると,循環した定義になってしまいそう (要するにあるあて先のパケットが到達する範囲が LANを規定している. . . )なのでやらないが,さしあたって同じネットワーク機器に直接つながっているコンピュータ,無線であれば近くにいる物同士,といい加減に理解しておこう.結果として,日本のどこかに 192.168.0.1というコンピュータがあるのと同時に,フランスのどこかで,同じアドレス 192.168.0.1を持つコンピュータがいても混乱はおきない.その一方で,「IPアドレスさえわかれば通信できる」というインターネットの美しい世界はここで壊れている.そして実は,現在 ISP (プロバイダ)に加入したときに,パソコンに振られるアドレスは 10中 8, 9がこのローカルアドレスである.なぜこんなことになってしまったのか? おそらくもともとは診断やテストのために予約されたアドレスが,インターネットの普及とともに,「すべてのコンピュータに,世界中に通用する名前を,重複なく」割り当てるのが難しくなってしまった,ということであまりに広く使われすぎている,ということだろう.という話は色々あるのだが,要するにネットワークで実験をする際に,コンピュータに上のようなアドレスが割り当てられていたら,要注意なのである.その場合にどういう通信が可能かを正確に理解するのは結構難しい.近似として,

• グローバルアドレスを持つ計算機に対して接続 (TCP)することは (securityポリシーなどで明示的にブロックされていなければ)出来る.

• 同じ LAN内であればどちらからでも相手に接続できる.

• ひとたびTCPの接続が確立したら,双方向に通信できる.

と理解しておけばよい.最もよくあるケースは,自宅のコンピュータと,友達の自宅のコンピュータ同士で,直接接続できない,よって,ある人の自宅のコンピュータにwebサーバをおいて,それを友達に見せるということが出来ない,などというケースである.一方,asahi.comなどの webサイトを使うことはどのコンピュータでも出来るのである.ここで最後の,「ひとたびTCPの接続が確立したら,双方向に通信できる」のはなぜかと疑問に思った人は頭が働いている.TCPとて IPの上に構築されている.したがって TCPでデータを送るとはいっても,下のレベルでは,ローカルアドレス(例えば,10.14.0.2)というアドレスを持つコンピュータに向かって IPパケットが

9

Page 10: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

送られているはずである.しかしそのパケットは LANの外に出て行かないはずである. . .実はここが汚いところなのだが,ローカルアドレスのみを持つコンピュータ(p)からグローバルアドレスを持つコンピュータ (gとしよう)に向かって通信をする際に,p(つまり送信者)のアドレスは,それがつながっている別の機器 (ルータ)の持つアドレス (それはグローバル)に変換して送られる.gからのパケットは,当然ルータのグローバルアドレスに向かって届けられるが,そこからルータが,もともとの差出人に向かってパケットを転送するのである.この変換をNetwork Address

Translation (NAT)と呼んでいて,もしADSLなどに加入していれば,電話とコンピュータの間にはさむ機器 (ブロードバンドルータ)が,大概その役目をしている.さて,我々の実験環境でそれが問題になるか? 大学のwireless環境や,情報基盤センターにつなげている場合,その中同士では,この問題を気にする必要はない.また,これから作成するネットワークプログラムを,すべてひとつのホスト内で動かしている分には何も問題はない (それだとあまりネットワークプログラムっぽくなくて面白くないのだが).複数のホスト間で接続する場合に唯一問題をややこしくすることがあるとすると,

VMWareを使う場合である.VMWareの中でOS (例えば Linux)を動かし,かつ,

VMWareのネットワーク構成をNAT構成にしている場合

は要注意である.VMWareの NAT構成とは,VMWare自身が上述の NAT機器のような役目を果たし,VMWare内で走るOSにローカルアドレスを割り振り,ホストOS外との通信の際には,あたかもホストOSからのパケットであるかのように見せかける構成である.一方,VMWareのネットワーク構成を “bridged”構成にしている場合は,ホスト

OS同士が通信できれば,VMWare内のOS同士もできると思っておいて良いだろう.従って,好き好んでNAT構成を選ぶ理由はないのだが,やっかいなことに,

ホストコンピュータがwirelessで通信している場合,bridged構成は使えない

という問題があり,ようするにwirelessでネットワークにつなげていて,かつVMWare

内のOSで実験をしている人は要注意なのである.実際に通信が出来る喜びの前に,汚い現実のことを話しすぎてしまったかもしれない.あまりに長かった基礎の復習を終えて,APIの説明に入る.

4.3 ソケットAPIの全体像

TCPで通信をする場合,2台のコンピュータのうち,

• どちらかが,相手からの接続を受付け,

• もう一方が,相手に接続をする.

電話を受ける方とかける方の関係である.前者を通常サーバ,後者をクライアントと呼ぶが,これもあまり意味のある言葉ではない.普通サーバとは,あるサービスを提供する側,クライアントはそのお客さんというニュアンスで使われる.インター

10

Page 11: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

ネットの場合,ウェブサーバ,メールサーバなど,ほとんどの「サービス提供」をするプログラムは,お客,つまりウェブブラウザやメール表示プログラムからの接続を受け付けて動作するので,近似として「サービスを提供するプログラム≈接続を受け付けるプログラム」という関係が成り立っているから,そのような言葉遣いになったのだろう.以下でも,便利だからこの用語を利用する.この用語の元で,TCP通信のフローチャートは以下のようになる.カッコ内はAPI名である.

サーバ側 1. ソケットを生成する (socket).

2. ソケットに,IPアドレスとポートを割り当てる (bind).

3. そのソケットの上で,接続待ち状態になる (listen; accept).

4. クライアントからの接続が来て,通信可能状態になる.

クライアント側 1. ソケットを生成する (socket).

2. サーバのアドレスとポートにめがけて,接続を行う (connect).

3. この時点で,サーバが接続待ち状態であれば,接続が確立され,通信可能状態になる.

クライアント側が接続を行ったときに,サーバ側が接続待ち状態になければエラーとなる.世の中で,サーバ側が接続待ち状態であることを保証する方法はなく,接続してみてダメだったらあきらめる,ということしかない.もちろんこの実験ではサーバをまず立ち上げて,待機状態に入ったことを (適切なprint文で)確認して,クライアントを立ち上げる.

4.4 共通事項

ソケットAPIを使うプログラムでは全て,UNIXでは以下の 3つ:

#include <sys/socket.h>

#include <netinet/in.h>

#include <netdb.h>

Windowsでは,以下のひとつ:

#include <windows.h>

のファイルを includeしておく (上記の行をファイルの先頭に書いておく).また,Windows上でVisual C++ (Visual Studio)でプログラムをコンパイルして実行可能ファイルを生成するとき,wsock32.libをリンクする.例えば,

cl myprogram.c wsock32.lib

のようにする.これを忘れると,

server.obj : error LNK2001: 外部^^ef^^bd^^bc^^ef^^be^^9d^^ef^^be^^8e^^ef^^be^^9e^^ef^^be^^99 "_listen@8" は未解決です

11

Page 12: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

以下での表記 UNIXでの型 Windowsでの型socket type int SOCKET

size type ssize_t int

表 1: 型の表記の共通化

などのメッセージが色々と表示されるであろう.統合環境では,とくに何もする必要はない (たぶん).また,Windowsと UNIXでは,APIはほとんど同じだが,返されるデータの型などが微妙に異なる.例えば以下で述べる socketというAPIが返すデータの型はUNIXでは int, Win32では SOCKETである.それらは本質的な違いではなく,単なる呼び方の違いに過ぎず,それらをいちいち異なるAPIとして別々に記述するのが面倒なため,以下では,socket type という表記で,UNIXでは int, WindowsではSOCKETをあらわすものとする.また,size type という表記でUNIXでは ssize_t,

Windowsでは intをあらわすものとする (表 1).

4.5 ソケットの生成と解放

生成

socket type s = socket(AF_INET, SOCK_STREAM, 0);

socketはその名のとおりソケットを生成して返す APIである.返された値はその後,接続を確立したり,データを通信をするためにAPI に渡される.socketは,生成に失敗すると-1 (UNIX),INVALID_SOCKET (Win32)を返す.

解放

UNIX: close(s);

Win32: closesocket(s);

close (UNIX)および closesocket (Win32)はソケット s の接続を遮断して,利用をやめるためのAPIである.注意としては,Win32にも closeというAPIが存在するが,別の用途のもので,ソケットに対して呼び出すと正しくない動作をするので,特にUNIXでのプログラミングの経験者は注意.

4.6 重要な余談: エラーチェックを怠らないこと

APIを呼び出したら,エラーが返されていないことを必ずチェックすること.エラーが起きているにもかかわらずその後の処理を行うと,プログラムは先へ進んでいるのになぜか動作はおかしいとか,一見して関係ないところで突如プログラムが落ちるなどの動作を引き起こす.エラーを起こしたらすぐに捕まえることをしておかないと,デバッグに非常な時間を浪費することになる.開発段階では,通常ありえないエラーを起こしたときには,その原因を表示してプログラムを終了させるのがよい.そのための一般的な方法を,socketを例にして以下に示す.

12

Page 13: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

UNIX:

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

if (-1 == s) {

perror("socket:");

exit(1); /* 終了したければ */

}

Win32ソケット関連:

SOCKET s = socket(AF_INET, SOCK_STREAM, 0);

if (INVALID_SOCKET == s) {

int e = WSAGetLastError();

char * lpMsgBuf;

FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM |

FORMAT_MESSAGE_IGNORE_INSERTS,

NULL,

e,

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR) &lpMsgBuf, 0, NULL);

printf("ERROR: %s\n", lpMsgBuf);

exit(1); /* 終了したければ */

}

そのほかのWin32 API: もしエラーを起こしたAPIがソケット関連 (つまり,この節で説明する API) 以外の API であれば,上記の WSAGetLastError の部分を,GetLastErrorとする.APIを呼び出すたびに,毎回上のようなコードを書き加えるのは,面倒くさくなって,そのうちやめてしまうので,ひとつの関数にまとめてしまうのがよい.

4.7 アドレスとポートの割り当て

bindというAPIの呼び出し方と,その典型的な使い方を示す.

struct sockaddr_in a;

a.sin_family = AF_INET;

a.sin_addr.s_addr = INADDR_ANY;

a.sin_port = htons(p);

int ok = bind(s, (struct sockaddr *)&a, sizeof(a));

このコード断片全体で,「ソケット sとポート pを結びつける」という動作をする.その意味は,後に acceptによって「接続待ち状態」に入ったときに,クライアントか

13

Page 14: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

らの接続を受け付けるポート番号が pであるということである.早い話が,「このソケットは今後このポート pで接続を待ちます」ということを宣言している.上の断片をもう少し解説すると,bindには,接続を受け付けたい IPアドレスとポート番号を sockaddr_inという構造体の中に指定する.

a.sin_family = AF_INET;

は,一種のおまじないで,インターネット経由の通信をする場合はこのようにする.ここで,ソケットがインターネット以外の通信のためにもつかわれるということが現れている.

a.sin_addr.s_addr = INADDR_ANY;

は,接続を受け付けたい IPアドレスを指定する.INADDR_ANYは,この計算機に割り当てられている任意のアドレスでよい,ということを指定しており,ひとつしかIPアドレスを持たない計算機 (ほとんどの計算機がそうである) の場合,常にこうしておけばよい.最後に,

a.sin_port = htons(p);

は,接続を受け付けるポート番号 pを指定している.htonsはおまじないと思っていてよいが,忘れずつけること.bindは成功すれば0を返す.失敗したら,−1 (UNIXの場合)またはSOCKET_ERROR

(Win32の場合)を返す.

4.8 接続待ち

int ok = listen(n);

socket type t = accept(s);

先に acceptを説明する.acceptはその名のとおり「接続要求を受け付ける」.もしクライアントからの接続要求がまだ来ていなければ,来るまで待機 (ブロック)する.無事接続が成立すると,新しいソケットが返され,そのソケットが,接続してきたクライアントと通信をするための口となる.acceptの引数として渡したソケット sは,今後も,あくまで,接続要求を受け付けるためのソケットに過ぎず,このsを用いてクライアントと通信するわけではないので,注意しよう.listenは,未処理の接続待ちをOSが n個まで受け付ける (ためておく)ことを指定する.つまり,サーバが acceptを発行せずに,多数のクライアントが接続要求をしてきたときに,n個までその要求をOSがためておく,ということである.n+ 1

個目以降の接続要求は,OSによって,クライアントに直ちにエラーが返される.もちろん,サーバがその後 acceptを実行したらその中のひとつの接続要求が成立し,新しい接続要求を受け付けられるようになる.本課題では,この値にあまり注意深くなる必要はなく,適当に 16などとしておけばよいだろう.listenは成功したら0,失敗したら−1 (UNIX)またはSOCKET_ERROR (Win32)を返す.acceptは成功したら新しいソケット,失敗したら−1 (UNIX)またはINVALID_SOCKET

(Win32)を返す.

14

Page 15: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

4.9 接続

struct sockaddr_in a;

struct hostent * hp = gethostbyname(sv);

if (0 == hp) { /* error処理 (省略). exit(1); */ }

a.sin_family = AF_INET;

a.sin_addr.s_addr = *((unsigned long *)hp->h_addr);

a.sin_port = htons(p);

int ok = connect(s, (struct sockaddr *)&a, sizeof(a));

上の断片は,svという名前の計算機の IPアドレスを検索し (gethostbyname),その IPアドレスとを持つ計算機上のポート p めがけて接続要求を行う (connect).svは文字列で,たとえば,"www.yahoo.com"のようなDNS名でもよいし,"133.11.23.10"のような,IPアドレスを文字列で書いたものでもよい.特別な名前として,"localhost"という名前が常に,このプログラムを実行している計算機自身をあらわしている.したがって,一台の計算機の中で実験をするときによく使う名前である.gethostbynameが成功すると,その名前を持つ計算機に関する情報へのポインタが返される.失敗すると 0が返される.返されたデータの構造は解説する気がしないので興味がある人は適切なマニュアルを参照すること.(Linuxでも同じでよいか?)

上のプログラムの中では,

a.sin_addr.s_addr = *((unsigned long *)hp->h_addr);

によって,IPアドレスを取り出して,それを構造体 aの中にに格納している.上のコード片は普通,覚えるのは不可能なので一度書いたら後はそれを繰り返しコピーして使うのである.

4.10 データの送信

size type n = send(s, m, l, 0);

mがメッセージの先頭のアドレス (ポインタ,配列,文字列など)で,そこから lバイトを sに向かって転送する.エラーを検出する−1 (UNIX)または SOCKET_ERROR

(Win32)が返される.成功の場合,送られたバイト数が返される.注意としては,sendが終了しても,それは実際にデータが相手に到着したことを意味しない.OSが,以降送り届けるという約束をしただけである.もちろんこの約束はほとんどの場合守られる (TCPが信頼性のある通信であるといわれる所以)が,それでもそれ以降,送信者,受信者,中間のルータのクラッシュなどで,接続そのものが切れて,送信に失敗することはあり得る.実際に相手にデータが届いたことを知ろうと思えば,相手からの確認応答を得るしかない.

15

Page 16: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

4.11 データの受信

size type n = recv(s, m, l, 0);

ソケット sから受信したメッセージを最大で lバイト,アドレスmを先頭として書き込む.失敗すれば−1 (UNIX)または SOCKET_ERROR (Win32) が返される.データが来ないまま,先方が (closeまたは closesocketにより)ソケットを閉じたら 0が返される.それ以外の場合,受信したバイト数が返され,これは 1以上,l以下である.とくに,lバイト受信する前に (たとえその後データがさらにやってくるとしても) recvが終了しうることに注意が必要である.

sendと recvの対応について 上記の recvの動作 (指定した lバイトよりも少ないバイト数で recvが終了することがある)についてさらに注意しておこう.一般に,recvの呼び出し結果は以下のどれかになる.

• エラーで終了する

• 0を返す.この場合,ソケットが閉じられたことを意味する.

• 1以上 l以下の数を返す.この場合,返されたバイト数だけのデータが受信され,その後ソケットにまだデータが来るのか,閉じられるのかは一切不明である.

とくに陥りやすい錯覚は,Xバイトの sendを行ってら,recv(· · · , · · · , X, 0)を発行すれば,Xバイトのデータを受け取る,という錯覚である.もちろん,このXが大きい場合を除くと,実は多くの場合そうなる (パケットはしょせんある塊で送受信される) のだが,それを勝手に仮定してはいけない.もし,Xバイト受信したいのであれば,以下のようなループを書くのが厳密に正しいやり方である.以下で buf

は,少なくともXバイトのデータを格納できる (char *) 型のポインタである.

received = 0;

while (received < X) {

t = recv(s, buf + received, X - received, 0);

if (t == -1) { error; /* 省略 */ }

else if (t == 0) { socket closed prematurely; /* 詳細省略 */}

else received += t;

}

5 課題1: ソケットの練習肩慣らしと,今後,より複雑なプログラムを開発するためのツール作成を兼ねて,ソケットを用いた例題を作成してみよう.

• ソケットを作り,適当なポート (重要なサービスと衝突しないために,大きな数 (たとえば 5000以上, 65535以下の適当な番号を用いよ)で接続を受け付ける (socket, bind, listen, acceptを用いる).

16

Page 17: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

• 接続が成立したら,recvによって,1000バイト (以内)のデータを受け取り,受け取ったらすぐに接続を閉じる (UNIXならばclose, Windowsならばclosesocket).

• 受け取ったデータをそのまま printfを用いて表示する.

これによって,例えばブラウザがページを取り出す要求する際にどのようなメッセージをサーバに送っているのかを調べることが出来る.このプログラムが出来たら以下のようにしてみよう.

• サーバを実行する計算機の IPアドレスを ipconfig, ifconfig -aなどで調べる.例えばそれが,192.168.0.1 だったとする.サーバが接続を受け付けているポート番号が 5000だったとする.

• サーバを立ち上げ,接続待ち状態 (acceptを呼び出した状態) に入ったことを確認する.

• ブラウザの,URLを記入する欄に以下のように記入する.

http://192.168.0.1:5000/hogehoge.html

または,もし同じサーバを立ち上げたのと同じホストでブラウザを使うのであれば,IPアドレスを調べなくても,単に,

http://localhost:5000/hogehoge.html

でよい.

するとブラウザはサーバに接続を依頼し,メッセージを送る.詳細は環境によって異なるが,おそらくサーバは以下のようなメッセージを表示するだろう.

GET / HTTP/1.1

Accept: image/gif, image/x-xbitmap, image/jpeg, ...

Accept-Language: ja

Accept-Encoding: gzip, deflate

User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)

Host: 192.168.0.1:5000

Connection: Keep-Alive

ブラウザの方は,リクエストを送った後,本来ならば返事が返ってくるところが,それがないままに接続が閉じられるので,何らかの通信障害が発生した,というエラーを表示することだろう.

17

Page 18: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

注: 上でブラウザに記入した URLはあまり見慣れないものかもしれない.まず,www.yahoo.comのようなシンボリックな名前の変わりに IPアドレスを直接用いている.次に,IPアドレスの後ろに,:5000などというものがついている.前者について.このシンボリックな名前はDNS名とよばれ,覚えにくい IPアドレスの代わりに利用することを意図して作られたものである.実際には,DNS

名が与えられた場合には,それをまず対応する IPアドレスに変換してから,やはり IPアドレスを用いてアクセスしているのである.そのためのAPIは,口述するgethostbynameというAPIで,コマンドとしては,nslookupというコマンドが用意されている.DNSの仕組みについてはインターネット関係の講義などを参考にすること.いずれにせよ,ここでは,通常用いているシンボリックな名前も,まず IP

アドレスに変換されていて,通信に使うのはあくまで IPアドレスである,ということだけを理解しておけばよい.次に:5000という表記について.これは想像でわかると思うが,接続に使うポート番号を指定するための記法である.何も書かなければ 80を用いるが,そのほかのポートで待つサーバにはこの記法を用いるのである.

6 基礎2: HTTPとHTML

6.1 はじめに

本節では,本来のwebブラウザとwebサーバの間のプロトコル (HTTP)と,正しいページとしてブラウザに理解されるための,ページの記述言語 (HTML)について説明する.もちろん詳細には立ち入らず,必要最低限の事項のみを説明する.

6.2 HTTP

HTTP (Hyper Text Transfer Protocol)は,webサーバとそのクライアントの間のプロトコルである.基礎となる部分は非常に単純であり,クライアントが取り出したいページを要求するためのメッセージの書式と,それに対するサーバの返事の書式を覚えれば事足りる.まず,以下がクライアントがwebサーバに,ページを要求するためのメッセージの書式である.

GET ⟨path⟩ HTTP/1.0⟨CR⟩⟨LF⟩

⟨options. . . ⟩

. . .

⟨CR⟩⟨LF⟩

前節の最後の実験で表示された文字列 (ブラウザから送られてきたメッセージ)が,確かにこの書式になっていたことを確認しておこう.⟨CR⟩⟨LF⟩は,それぞれいわゆる改行,復帰文字であり,もちろん表示はされない.文字コードとしては,⟨CR⟩ = 13, ⟨LF⟩ = 10である.C言語を含む多くの言語で,13を’\r’, 10を’\n’と表記できる.最後の空行で,リクエストメッセージの終わりを示していることに注意.

18

Page 19: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

6.3 telnet : 万能TCPクライアント

さて,前節の最後では,ブラウザから送られたメッセージがHTTPの書式に沿っていることを確認したが,逆に,通常のwebサーバが確かにこのメッセージに正しく受け答えることも確認しておこう.それには,前節で書いた簡易サーバの逆の簡易クライアント,つまり,サーバに接続し,HTTPリクエストを送り,返された文字列を表示する,というプログラムを書けばよいのだが,幸いなことにそうする必要もない.Windowsにも UNIX にも,telnetと呼ばれるプログラムが用意されており,今の我々の目的に利用可能である.telnetは以下のように起動する.

telnet ⟨サーバ名 ⟩ ⟨ポート名 ⟩

サーバ名は,www.logos.t.u-tokyo.ac.jpのようなDNS名でもいいし,133.11.23.10のような IPアドレスでもよい.例えば以下のようにすれば,www.yahoo.comのweb

サーバーにつながる.

telnet www.yahoo.co.jp 80

次にここに文字列を打ち込むと,それがwebサーバに送られる.先ほど覚えた,ページを要求するためのメッセージを送ってみよう.

GET / HTTP/1.0⟨リターンキー ⟩

⟨リターンキー ⟩

ここでは一切のオプションを省略して,GET行と最後の空行だけを入力する.パス名としては,/,つまり,いわゆるトップレベルページ (ブラウザにhttp://www.yahoo.co.jp/

と入力したときに表示されるページ) を要求している.うまくいけば,サーバからの答えが返される.スクロールしてしまうので見えないかもしれないが,以下のようになっている.

HTTP/1.1 200 OK⟨CR⟩⟨LF⟩Date: Wed, 18 Sep 2002 04:21:24 GMT⟨CR⟩⟨LF⟩Cache-Control: private⟨CR⟩⟨LF⟩Pragma: no-cache⟨CR⟩⟨LF⟩Connection: close⟨CR⟩⟨LF⟩Content-Type: text/html;charset=euc-jp⟨CR⟩⟨LF⟩⟨CR⟩⟨LF⟩<HTML>

<HEAD>

· · ·

19

Page 20: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

返事は,リクエストの可否などを示すヘッダと,要求されたページの中身 (body)

とからなり,両者の間は空行で区切られている.上の例では 6行目までがヘッダで,7行目が空行,それ以降が中身である.中身がどのような書式であるかは,HTTPの定めるところではない.上の例では,

<HTML>. . .で始まる中身が送られてきているので,おそらく,HTML (Hyper Text

Markup Language)という,webページを作成するときのもっとも標準的な書式が送られてきている.また,ブラウザにそれを教えるために,ヘッダの最後に,

Content-Type: text/html;charset=euc-jp⟨CR⟩⟨LF⟩

という行が書かれており,この行が,これから送られてくる中身はHTML形式であるということを告げている.おのおののヘッダ行の意味については解説しない.この演習で我々にとって重要なのは,ブラウザが最低限動作するために,サーバがどのような文字列をブラウザに返したらよいかということで,さしあたって以下のようにしておけばよい.

HTTP/1.1 200 OK⟨CR⟩⟨LF⟩Content-Type: text/html⟨CR⟩⟨LF⟩⟨CR⟩⟨LF⟩⟨中身 ⟩(次節で解説する)

ところで,telnetは通常,他の計算機にログインする手段として使われていたが,このように任意のポートに接続することが出来,HTTPのみならず他のあらゆるTCPの上のアプリケーションレベルプロトコル (SMTP, POPなど)の動作を理解するのに用いることが出来る便利なプログラムである.今,telnetはセキュリティ上問題があるため,他の計算機へのログイン手段としては使われなくなっている (例えばログイン時にパスワードをそのままネットワークに流す).

6.4 HTML

さて,ブラウザに理解される「中身」の書き方について,やはり最低限のことを解説しておこう.もっともすでにホームページなどを自分で書いたことがあって,詳しい人はたくさんいることだろう.Web上に存在するページの多くは HTMLという書式で書かれている.これは通常のテキストの中に,多少の修飾機能 (フォントを大きくするなど)や,他の文章へのリンクを埋め込むことが出来るものである.本演習のためには,以下程度のページが作れれば十分である.

<html>

<p>これは html文書です</p>

<p>Yahooが見たい人は<a href=http://www.yahoo.co.jp/>こちら</a></p>

</html>

ここで覚えておくべき文法は,

20

Page 21: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

• htmlファイルは必ず<html>ではじめ,</html>で終わる.

• 文章の一段落は,<p>と</p>で囲む.

• 他のページへのリンクは,<a href=リンク先>と</a> で囲む.ブラウザには,「リンク先」自身は表示されず,<a href=リンク先> と</a>で囲まれた文字列がクリック可能であるように表示される.

ためしに上の文書を,エディタ (emacsなど)で作成し,a.htmlという名前で保存し,ブラウザで表示してみよ (Windowsであれば単にそのファイルのアイコンをクリックすればよい).<p>などのタグがあるのとないのとでブラウザの表示がどう違うかを観察してみるとよい.

相対リンク さて,上の例では,リンク先として,http://www.yahoo.co.jp/という文字列を使ったが,ファイル名の指定と同様,リンク先を現在のページからの相対で指定することも出来る.2例えば,

http://www.logos.t.u-tokyo.ac.jp/~tau/doctor-enshu/2002/index.html

というページ内に,

<a href=x.html>X</a>

というリンクがあれば,それはブラウザによって,

http://www.logos.t.u-tokyo.ac.jp/~tau/doctor-enshu/2002/x.html

のことであると解釈される.すなわち現在のページの末尾の要素 (index.html) を取り除いた上で,x.htmlに置き換わったページと解釈される.ファイルシステムのパス名と同様,..という記号で,階層構造を上に上る.例えば,

<a href=../y.html>Y</a>

とあれば,/~tau/doctor-enshu/2002/から一段上ったところの,y.html, つまり,

http://www.logos.t.u-tokyo.ac.jp/~tau/doctor-enshu/y.html

のことと解釈される.これは,ページ内にwebサーバの名前を直接埋め込まなくてよいので,便利である.注意としては,サーバ自身が起点となるページを覚えているのではなく,あくまでブラウザがそれを覚えて,GETの次に書くべきパス名を計算するということである.例えば,上の../y.htmlのリンクをクリックした際には,ブラウザが,そのリンクが/~tau/doctor-enshu/2002/を起点にしていることから,完全なパス名/~tau/doctor-enshu/y.htmlを計算し,サーバに,GET /~tau/doctor-enshu/y.html

というリクエストを送る.したがって,サーバは送られたパス名は常に完全な (絶対)パス名であることを仮定してよいのである.

2正確には現在のページの相対とは限らないが,ここではそう思っておいてよい.

21

Page 22: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

7 課題2: 最小webサーバここまでの理解で,GETリクエストを読み込んで,いつも同じHTMLを返す,最小webサーバを作ってみよう.

1. 適当なポート番号で接続待ち状態に入り,以下を繰り返す.

2. 接続を受け付けたらHTTPのGETリクエストを読み込み,要求されているパス名を抽出する.

3. 適当なHTMLページをその場で生成して,HTTPにしたがった書式で返事として返す.このとき,きちんとパス名を抽出できていることを示すために,返すHTMLページ内にパス名を埋め込んでおくとよいだろう.

通常のwebサーバでは,HTMLページは通常ファイルとして格納されており,サーバは要求されたパス名に対応するファイルを読み込んで返すが,ここで作るwebサーバは,その場で適当なHTMLのフォーマットに沿った文字列を生成して返すものとする.実際,現在の複雑なwebサイトでは,要求に応じて異なる複雑な動作をさせるため,多くの場合にこのようにHTMLページをリクエストに対してその場で生成している.本課題は,それのもっとも単純なものを作ってみようというのである.

8 課題3: シングルスレッド/セッションサーバ上の課題ができたら,いよいよ「記憶力ゲーム」サーバの作成に取り掛かる.ここでは簡略版として,シングルセッション (ゲーム)を仮定して,シングルスレッドで動作するサーバを作成する.つまり,進行中のゲームはひとつしかなく,かつ複数のリクエストが同時に多数やってくることはない (あったとしても,それは順に処理すればよい),と仮定する.動作は以下のとおりとする.

• 適当なポート番号 (勝手に決めてよい)で接続待ち状態に入る.

• 接続を受け付けたらHTTPのGETリクエストを読み込む.そのパス名によって以下のような動作を行う.以下で,xxx は_, /を含まない任意の文字列をあらわす.

1. パス名が’/’であれば,ゲームを始めるためのリンクを持つHTML ページを生成してクライアントに返す.そのリンクのパス名は,/new_xxx/の形とする.ここで xxxの部分は今は使わないが,必要に応じてアプリケーションが必要な情報を埋め込めるようにしておく.例えば今回の記憶力ゲームサーバでも,この部分で記憶すべき数の桁数などを指定できるようにしておいてもよいだろう.

2. /new_xxx/の形のパス名であれば,それは「新しいゲームの始まり」を意味するものとしよう.覚えるべき数字を適当に生成し,それを表示するHTMLページを返す.「ゲームスタート」のリンクも含ませておく.その

22

Page 23: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

パス名 動作 返されるページ中のリンク/ トップレベルページ表示 新しいゲーム/new_xxx/ 覚えさせる数字の表示 そのゲームの開始/xxx_S/ 次の数字を選択させるページの表示 数字および「終了」/xxx_i/ 数字 iの正しさを検査, 数字および「終了」

正しければ次の数字を選択させるページの表示,間違っていればエラーページを表示して終了

/xxx_E/ ゲーム終了の検査,正しければおめでとうページの表示して終了,間違っていればエラーページを表示して終了

リンクのパス名は,/xxx_Sとしておこう.ここでやはり xxx は今は使わない (任意の適当な文字列でよい)が,後に複数のゲームが同時進行する場合に複数のゲームを区別するために使う.

3. /xxx_Sの形のパス名に対しては,0~9までの数字のリンクと,終了を示すリンクを持つHTMLページを生成して返す.数字 iのリンクのパス名は,/xxx_i/ の形,終了のリンクのパス名は,/xxx_E/ とする.xxx は上と同様,後に複数のゲームの同時進行を許すときに複数の進行中のゲームを区別するために用いる.

4. /xxx_i/の形のパス名であれば,iが正しい数字かどうかをチェックし,正しければ上と同じ,0~9までと終了を示すリンクを持つHTMLページを返す.そうでなければ,その旨表示するページを示してサーバプログラム自体を終了する.

5. /xxx_E/の形のパス名であれば,数字の場合と同様,それが正しい入力か(つまり全ての桁が既に入力されているか)を検査し,どちらの場合もその結果を表示するHTMLページを返し,サーバプログラム自体を終了する.

6. ここに書いていない以外の色々な理由で,エラーが発生したときは,それをブラウザに伝えるべく,その旨書かれたエラーページを返す.どこでエラーが発生したかなどの情報を書いておくと,デバッグがしやすくなる.

パス名とそれに応じた動作を表にまとめた.ここまでを,なるべく全員の人に達成してほしい課題とする.次の節からより本格的なマルチスレッドサーバ (複数のリクエストの並行処理や,複数のゲームの同時進行を許す)を作成しよう.次節ではそのための準備として,並行プログラムの基礎とスレッドAPIについて解説する.

9 基礎3: 並行プログラム (スレッドAPI)

9.1 基本概念

スレッドは,CPUを使用したいプログラムに,オペレーティングシステムが用意したAPIであり,プログラム実行中にスレッドを生成すると,もともとの処理と並

23

Page 24: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

行して,新しいスレッドが動き出す.もっともスレッドAPIを使わなくても,プログラムの起動時には暗黙的にひとつのスレッド (C言語であればmain 関数を実行するスレッド)が作られるので,直接スレッドAPIを使ったことのある人は少ないかもしれない.その場合も,ひとつ下のレベルでは,多数のスレッドが作られ,それがオペレーティングシステムによって順番に実行されているのである.複数のCPUがあれば,本当に複数のスレッドが同時に動作し,結果として台数効果 (複数のスレッドを使うと処理全体が速くなる)が得られるが,仮にひとつのCPU

であってもスレッドを使うことには色々な意味がある.

I/O遅延時間の有効利用: プログラム上はよどみない一連の処理を記述していても実際には diskやネットワークからのデータを待つ,などの理由でスレッドはたまにブロックしている.複数のスレッドがあれば,ひとつのスレッドがブロックしても,オペレーティングシステムによって他のスレッドが自動的に実行される.

例えばファイルを読む処理Aと,ネットワークからのデータを読む処理 Bが別々の処理を行っていたとする.このとき,処理Aと処理 Bに別々のスレッドを割り当てれば,プログラムは以下のように自然に書け,

A: B:

... read file ...; ... read network ...;

かつ,“read file”の部分でスレッド Aがディスクからのデータを待つ状態に入ったら,自動的にスレッド Bが実行される.逆に Bがネットワークからのデータ待ち状態に入れば自動的にAが実行される.

AとB二つの処理を一つのスレッドを使って記述すると,運悪く “read file”のところで待ちが発生したら,本来処理可能な Bの部分までもが先へ進まなくなる.結果としてCPUに無駄が生ずるのである.

Webサーバに限らず,典型的なサーバは,多くの時間をファイルやネットワークからのデータの入出力に費やしているので,1 CPUの計算機でもマルチスレッド化することでスループットの向上が達成できる.

プログラムの書きやすさ: 結局は上記の項目と同じことだが,論理的なひとつの処理 (プログラマが自然にそう思う一連の手続き)はひとつのスレッドに任せて,別々の処理は別々のスレッドの処理させることでプログラムが簡潔なものになる.

上記の処理Aと処理Bの場合もそうだが,より具体的な例としては,ブラウザで,すでに取り出されたページの描画を行うスレッドと,その中で参照しているさらなるデータ (例えば画像)をサーバから取り寄せるスレッド,さらにユーザのアクション (取り消しボタンなど)を監視するスレッド,などをおのおの別のスレッドとして書くというのは典型的である.

24

Page 25: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

スレッドはいいことずくめのようだが,生成するのに一定のコストがかかる点に注意する必要がある.わずかな量の仕事を別々のスレッドにやらせると,余分なオーバーヘッドを発生する.最悪の場合スレッドを大量に作りすぎて,物理メモリを食いつぶし,ある限界を超えたところでサーバの性能が急速に悪化するということにもありうる.そういう心配事はさておき,APIの説明に入ろう.

9.2 共通事項

スレッドを提供するAPIとしてよく使われるAPIには少なくとも 3種類ある.

Pthreads: Posix Threadsとも呼ばれ,ほとんどの UNIX (Linux, Solaris, Digital

UNIXなど)で提供されている.

Solaris Threads: ほぼ Pthreadsと同様のAPIで,Solarisに固有のAPIである.

Win32 Threads: Windowsで提供されているAPIである.

本節と次節で解説するAPIを使うに当たっては以下のファイルを includeする.

Pthreads: #include <pthread.h>

Solaris Threads: #include <thread.h>

#include <sync.h>

Win32 Threads: #include <windows.h>

また,ライブラリをリンクするために,実行ファイルを生成するコマンドラインの最後に以下を追加する.

Pthreads: -lpthread

Solaris Threads: -lthread

9.3 生成

Pthreads: int err = pthread_create(p, · · · , f, x);

Solaris: int err = thr_create(· · · , · · · , f, x, · · · , p);

Win32: HANDLE h = CreateThread(· · · , · · · , f, x, · · · , p);

どのAPIも,f(x)を実行するスレッドをひとつ生成する.∗pに生成されたスレッドの ID が格納される.これは後に pthread_join, thr_joinなどのAPIに渡される.

pthread_create, thr_createは成功すると0,失敗すると-1を返す.CreateThreadは,生成されたスレッドのハンドル (HANDLE型の値.実体は整数)を返す.失敗するとNULLを返す.このハンドルは後にこのスレッドに対して操作を行うときにAPI

に渡すもので,さしあたってWindowsがスレッドにつけた名前と考えておけばよい.

25

Page 26: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

余談 Win32 APIではスレッドに限らず,およそAPIを用いて生成される全てのデータは,この HANDLE型の値としてユーザプログラムに渡される.後に述べるmutex

データ構造,プロセス,など,すべてのものがHANDLE型である.Win32 APIではそれらのデータを総称して,カーネルオブジェクトと呼んでいる.つまり,CreateThreadで作られるスレッドはカーネルオブジェクトの一種,後に述べる CreateMutex で作られるmutexデータ構造もカーネルオブジェクトの一種である.

9.4 終了

Pthreads: pthread_exit(· · ·);

Solaris: thr_exit(· · ·);

Win32: ExitThread(· · ·);

スレッドを終了するときにこのAPIを呼ぶことになっているが,実はスレッド生成時に指定した関数 f が実行を終了 (return)すると,自動的にこれを呼んだのと同じ効果がある.

10 基礎4: 共有メモリの更新と同期pthread_create, thr_create, CreateThreadなどによって生成されたスレッドは,それを生成したスレッドと同じプロセス内で動作し,メモリを共有する.より詳しく言うと,同一プロセス内で動作するスレッド T , Uがあったとすると,T がアドレス aに 100を書き込み,その後U がアドレス aを読めば,U は 100を読むことになる.この現象を「スレッド T , U はメモリを共有する」という.メモリを共有しているので,スレッドは共有メモリを介して通信・協調動作 (例:

あるスレッドが計算した結果を他のスレッドが利用する,ひとつの仕事を分割して,スレッドT がある部分の仕事をし,スレッドUがそれと違う別の部分の仕事をする,など)をすることができる.これは簡単なように見えて,大変なこともあり,気をつけないと非常に発見しにくいバグの元になる.その理由は,スレッドはOSによってスケジュールされるため,複数のスレッドが実行される順番や,交互実行されるタイミングが予測できないことにある.したがってスレッド間の通信・協調を行う際には,OSがどのようにスレッドを実行しようとも正しい動作になるように,スレッド実行のタイミングにある種の制約をつけるAPIを用いる必要がある.そのようなスレッド間の「タイミングの調整」をスレッドの同期 と呼ぶ.以下に,我々の課題に必要な,同期のためのAPIを解説する.

10.1 排他制御

もっとも頻出する同期に排他制御がある.これは,複数のスレッドがどのような順番で実行されようとかまわないが,同時にある部分を実行してはならない,とい

26

Page 27: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

う場合に用いる.実際には,複数のスレッドによって使われるデータの更新は,大概の場合,排他制御を考慮する必要がある.単純な例として以下を考える.

1: int x = 0;

2:

3: f()

4: {

5: x = x + 1;

6: }

7:

8: g()

9: {

10: x = x + 2;

11: }

今二つのスレッド Tf, Tgが,それぞれ f, gを実行しているとする.両方のスレッドが終了した後で xを読めば 3が読み出されると思うかもしれないが,それは一般には正しくない.なぜならば,以下のような実行が考えられるからである.

1. Tfが 5行目で x = 0を読む.

2. Tgが 10行目で x = 0を読む.

3. Tfが 5行目で x = 0 + 1を書く.

4. Tgが 10行目で x = 0 + 2を書く.

この場合最終的に xには値 2が残る.プログラマの意図はおそらく,1と 2を足して 3を得たいというもので,3そのためには,5行目と 10行目が同時に実行されてはいけなかったのである.複数のCPUを搭載する計算機では Tfと Tgが違うCPUによって実行され,Tfと

Tgが偶然にも,ほぼ同時に実行されればこのようなことが起こる.CPUがひとつしかなくても,OSが Tfが 5行目で x = 0を読んだ直後にスレッドを Tgに切り替えれば同じ不幸がおきる.細かいことだが,後者の場合,それらの二つが「同時に」おこったという表現は正確ではないかも知れない.上のプログラムが運良く動く条件のより正確な表現は,fの x = x + 1と,gの x = x + 2が「時間的に重なることなく実行される」という表現である.通常このことを,(それらの処理が)「排他的に実行される」と表現している.プログラマから見ると,x = x + 1および x = x + 2という処理が排他的に実行されれば問題は起きなかった.それらが排他的であれば,x = x + 1という「一連の処理」と,x = x + 2という一連の処理の,どちらが先に来ようとも,最終結果が 3であることが保証される.

3もちろんたまに 1や 2になってもいい,というのが彼の意図かも知れず,それは本人に聞かない限りわからないが,今はそうではなかったとしよう.

27

Page 28: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

このように,ある一連の処理を,他の処理と排他的に実行したい,という要求はしばしば発生し,それを保証するのが,排他制御である.一般に排他制御は,mutexと呼ばれるデータ構造と,それに対する lock/unlockという二つの操作からなる.Lockを acquire, unlockを releaseと呼ぶこともある.以下が排他制御のAPIである.

mutexの型:

Pthreads: pthread_mutex_t

Solaris: mutex_t

Win32: HANDLE

生成・初期化:

Pthreads: int err = pthread_mutex_init(m, 0);

Solaris: int err = mutex_init(m, USYNC_PROCESS, 0);

Win32: HANDLE h = CreateMutex(NULL, FALSE, NULL);

消去:

Pthreads: int err = pthread_mutex_destroy(m);

Solaris: int err = mutex_destroy(m);

Win32: BOOL ok = CloseHandle(m);

Lockの獲得:

Pthreads: int err = pthread_mutex_lock(m);

Solaris: int err = mutex_lock(m);

Win32: DWORD r = WaitForSingleObject(m, INFINITE);

Lockの解放:

Pthreads: int err = pthread_mutex_unlock(m);

Solaris: int err = mutex_unlock(m);

Win32: BOOL ok = ReleaseMutex(m);

基本的な使い方は,どの二つも互いに時間的に重なってはならない操作A1, · · · , An

があった時に,ひとつの mutexを用意し,それぞれの操作を実行する直前にそのmutexに対する lock操作,終了直後に unlock操作を入れるというものである.今の我々の例では,

28

Page 29: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

1: int x = 0;

2:

3: f()

4: {

5: pthread_mutex_lock(mp);

6: x = x + 1;

7: pthread_mutex_unlock(mp);

8: }

9:

10: g()

11: {

12: pthread_mutex_lock(mp);

13: x = x + 2;

14: pthread_mutex_unlock(mp);

15: }

などと書くことになる.Pthread, Solarisでは,mutexは使われる前にそれぞれpthread_mutex_init,thr_mutex_initによって初期化される必要がある.Win32では,CreateMutexがこれを行う.Lock操作が行われるとmutexデータ構造の中に,「そのmutexは現在ロックされている」ということが記録される.それ以降,unlockされる以前にmutexに対してlock操作が行われると,unlockされるまでそれを試みたスレッドはブロックする.

10.2 WaitForSingleObjectについて

上では,この APIはロック操作である,と書いたがその言い方は正確ではなく,実際にはこれらは様々なカーネルオブジェクトに対して,その種類に応じて異なる動作をする.WaitForSingleObject(h)の hは,mutexに限らない一般のカーネルオブジェクトへのハンドルである.一般にこれらのAPIは渡されたカーネルオブジェクトが「ある状態」になるまで待つという動作をする.この「ある状態」(Win32 API用語では,「シグナル状態」と呼ばれている)というのはカーネルオブジェクトごとに異なり,それがmutexの場合は「unlockされた状態」だったわけである.どのような種類のオブジェクトに対して,「シグナル状態」がどういう状態かは,WaitForSingleObjectのマニュアルページにまとめて書いてある.WaitForMultipleObjects(n, ha, .., ..)は,その名が示すとおり,複数のカーネルオブジェクト ha[0], · · · , ha[n− 1]に対して,それらがシグナル状態になるのを待つ,というAPIである.パラメータの指定の仕方によって,「どれかひとつ」のカーネルオブジェクトがシグナル状態になるのを待つことも出来るし,「全て」のカーネルオブジェクトがシグナル状態になるのを待つことも出来る.ha[0], ha[1], · · ·の中には色々な種類のカーネルオブジェクトが混ざっていても良い.このテキストでは以降,様々な種類のカーネルオブジェクトが登場するが,そのつど,その種類に応じた WaitForSingleObjectの意味を解説する.同じ名前のAPI

29

Page 30: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

が複数登場して,毎回説明が異なることになるので混乱しないように注意してほしい.WaitForMultipleObjectsも同様に,以降で述べる様々なカーネルオブジェクトに適用可能だが,その意味は上に述べたとおりで尽きているので,そのつど繰り返し述べることはしない.

10.3 セマフォ

セマフォは,mutexオブジェクトを少し一般化したものである.mutexの動作を比喩的に述べるならば,mutexの中にはひとつのボールが入っており,lock操作が,ボールを取り出す (すでにボールがなければそのボールが返されるまで待つ),unlock操作は (過去に取り出したはずの)ボールを返す操作といえる.セマフォはこれを一般化して,このボールの数を一般に c個にしたものである.この APIは SolarisとWin32には存在するが,Pthreadには存在しない.Pthreadでは,次節で述べる条件変数を用いれば,セマフォを実現できる.以下がそのAPIである.

セマフォの型:

Solaris: sema_t

Win32: HANDLE

生成・初期化:

Solaris: int err = sema_init(s, c, USYNC_THREAD, 0);

Win32: HANDLE h = CreateSemaphore(NULL, i, c, NULL);

消去:

Solaris: int err = sema_destroy(s);

Win32: BOOL ok = CloseHandle(s);

獲得:

Solaris: int err = sema_wait(s);

Win32: DWORD ok = WaitForSingleObject(s, · · ·);

解放:

Solaris: int err = sema_post(s);

Win32: DWORD ok = ReleaseSemaphore(s, 1, NULL);

獲得・解放操作の意味を詳述すると以下のようになる.セマフォは内部にひとつの整数値 xを保持している.その元で,

30

Page 31: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

獲得: x = 0であれば,x > 0になるまで待機.そして xを 1減らす.

解放: xに 1を足す.待っているスレッドがあればそのひとつを起こす.

という動作をする.もちろん各処理は,排他的に実行される.cはxとして許される値の最大値でセマフォの容量と呼ぶ.Win32 APIではxの初期値を生成時に CreateSemaphoreの引数 i(≤ c)で指定できる.Solarisでは,xの初期値は cである.しかし,初期値 cで初期化した後に,必要な回数 (c−i回)sema_wait

を実行すれば,実質初期値 iを指定できたことになる.セマフォは,i個存在し,今後さらに資源が追加されて最大で c個になりうる資源へのアクセスを調停する APIである.振り返ってmutexは,1個しかない資源へ,同時にアクセスが起こらぬよう調停するAPIであった.それがセマフォでは,「i個の資源が用意されておりそのどれかが使えればよい」というプロセスが多数あったときに,それらのプロセスが行儀よく,つまり一度に i+1以上のプロセスが同時に資源を獲得しないように,調停する.xが「現在まだ残されている資源の数」だと思えば上の動作がまさに正しい調停を行っているというのが納得できるだろう.本課題では後に,有限のメモリ領域を用いてスレッド間でデータを正しく受け渡すためにセマフォを用いる.この問題は生産者消費者同期と呼ばれ,頻繁に出現するスレッド間の同期の典型例である.問題を正確に述べると以下のようになる.

スレッドP は多数のデータを継続的に生産し,スレッドCがそれを順にひとつずつ消費する.そこでP からCに,共有メモリを介してデータを受け渡したい.特に,以下の二つの要件を満たす.

• 生産されて消費されていないデータがない (0個)ときは,P が次のデータを生産するまでCはブロックする.

• データを受け渡すための領域がデータN個分に限られており,生産されて消費されていないデータがN 個のときは,C がひとつデータを消費するまで P はブロックする.

Cの立場から見ると,すでに生産されて,まだ消費されていないデータが,セマフォで調停されるべき「資源」に相当する.つまり,内部の整数値が,そのようなデータの個数であるようなセマフォを作ればよい.逆にP の立場から見ると,「資源」に相当するのは「データを書き込んでよい領域」であり,内部の整数値がそのような領域の数であるようなセマフォを作ればよい.P , Cがデータをそれぞれ生産・消費するループは以下のようになるだろう.

1: r = 容量 N, 初期値 0のセマフォ;

2: w = 容量 N, 初期値 Nのセマフォ;

3: B = 容量 Nの (N個までデータを書き込める)バッファ;

4:

5: P()

6: {

7: for (..;..;..) {

8: データをひとつ生産;

31

Page 32: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

9: sema_wait(w);

10: Bにデータを書き込む;

11: sema_release(r);

12: }

13: }

14:

15: C()

16: {

17: for (..;..;..) {

18: sema_wait(r);

19: Bからデータを取り出す;

20: sema_release(w);

21: 読んだデータを使う;

22: }

23: }

10.4 条件変数

セマフォの獲得は,内部の整数値 xが,「0ならば待機,正ならば 1減らす」という処理を行い,一方の解放は xを,「1増加させ,0が 1になったところであれば,待機しているスレッドを起こす」というものであった.一般に,複数のスレッドがデータ構造を更新するときは,スレッドが以下のような一連の動作を行うことが多い.

• データ構造に対してある「条件」(セマフォであれば x > 0)を検査

• 条件が成り立たなければ,成り立つまでブロック.

• 成り立てばそのまま続行し,データの更新を行う.

条件変数はこれらを実現するために用いる比較的低水準なプリミティブで,具体的には,上で「条件が成り立つまでブロック」する操作 (wait)と,そうしてブロックしたスレッドを起こす操作 (signal)から構成される.前節で説明したように,我々の課題には生産者消費者同期が必要で,それはセマフォを使えば簡単に実現できるのだが,実はPthreadにはセマフォAPIが存在しない.そして,Linuxではスレッドパッケージとしては Pthreadが事実上唯一の選択肢となる.結果として,Linuxでは本節の条件変数を用いてセマフォを実現しなくてはならない.条件変数は Pthreadおよび Solarisで提供される.

条件変数の型:

Pthreads: pthread_cond_t

Solaris: cond_t

32

Page 33: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

初期化:

Pthreads: pthread_cond_init(c, 0);

Solaris: cond_init(c, USYNC_PROCESS, 0);

消去:

Pthreads: pthread_cond_destroy(c);

Solaris: cond_destroy(c);

待機:

Pthreads: pthread_cond_wait(c,m);

Solaris: cond_wait(c,m);

シグナル:

Pthreads: pthread_cond_broadcast(c);

Solaris: cond_broadcast(c);

PthreadとSolarisで,対応するAPIの意味は全く同じなので,以下ではPthreadの方で説明する.pthread_cond_waitの引数cは条件変数へのポインタ (pthread_cond_t

型へのポインタ)で,mはmutexへのポインタである.そして,mはこれを呼び出した時点ですでに lockされていることが仮定されている.pthread_cond_wait(c,m)

を実行すると以下の動作が排他的に起こる.

1. mが unlockされる.

2. 呼び出したスレッドが c上で待機状態に入る.

一方,pthread_cond_broadcast(c)を実行すると以下の動作が起こる.

1. c上で待機しているスレッドがあればそれらを全て起こす.

2. 起こされたスレッドはその後再びmをロックしに行く

したがって,pthread_cond_wait(c,m)が成功した後は,すでにmの lockを自分が獲得している状態である.条件変数を用いてデータ構造Xがある条件 f(X)を満たすまで待つコードの雛形は以下のようになる.

1: pthread_mutex(m);

2: while (!f(X)) {

3: pthread_cond_wait(c,m);

4: }

33

Page 34: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

5: · · · /* Xを読んだり書いたり */

6: /* 場合によって,pthread_cond_broadcast(c) */

7: pthread_mutex_unlock(m);

上記コードを簡単に解説する.

• 1行目で,Xの更新と他のアクセスが排他的に行われぬよう,mutex mを lock

する.

• 2-4行目で,条件 f(X)の成立を待つ.

• 3行目で待機状態に入るときに,mの lockが解放される.これによって皇族のスレッドがXをアクセスできるようになる.実際こうしないと,Xが更新されることもおそらくなく,結果として (今偽である) f(X)が今後成立するべくもない.

• 4行目は,誰かがどこかでpthread_cond_broadcastを実行し,その後このスレッドが再びmutexmを取得している状態である.ただし,ここでf(X)が成立していると決め付けるのは,一般には早計である (理由は下で述べる).したがって2-4行目のループをまわして条件が成立するまで必要なだけpthread_cond_wait

を繰り返す.

• 5行目は必要に応じてXを読んだり書いたりする.

• 6行目で pthread_cond_broadcastが必要なのは,自分がX を更新したことによって,何か他のスレッドが待機している条件が今成立した可能性があるときである.

一番説明を要するのは,2-4行目のループが何故必要か,であろう.つまり何故,より簡単な,

1: if (!f(X)) {

2: pthread_cond_wait(c,m);

3: }

ではいけないか? 上のコードは以下のことを仮定している.

どこかで誰かがpthread_cond_broadcastを実行して,c上で待つスレッドを全て起こし,やがて上記のスレッド 3 行目に達したときに,必ず条件 f(X)が成立している.

これは様々な状況で正しくないことがある.例えば,

• c上で,色々なスレッドが色々な条件—あるスレッドは f(X), あるスレッドはg(X), という具合—で待機する場合.この場合 pthread_cond_broadcast は,f(X),g(X)などのうちのひとつが今成立したときに呼ばれることになるだろう.その場合,pthread_cond_waitは実際には f(X)が成立していないにもかかわらず一旦起こされることになる.

34

Page 35: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

• c上で複数のスレッドが同時に待機しうる場合.仮に全てのスレッドが同じ条件で待つとしても,f(X)が成立したとき,pthread_cond_broadcastによって複数のスレッドが同時に起こされるとすると,そのうち最初にmutexを lock

できるスレッドは当然のことながらひとつだけである.したがって 2番目のスレッドが 3行目に達するのは,1番目のスレッドが再びデータを更新した後で,このときも f(X)が成り立つとは保証されない.

もちろん上のコードでOKという場合も存在するのだが,あまりややこしいことは考えずに,while (!f(X))というループをまわすのを基本としておくのが安全なのである.以下は前節の,容量N のバッファを使った生産者消費者同期を条件変数を使って書いたものである.

1: c = 条件変数;

2: m = mutex;

3: B = 容量 Nの (N個までデータを保持できる)バッファ;

4:

5: P()

6: {

7: for (..;..;..) {

8: データをひとつ生産;

9: pthread_mutex_lock(m);

10: while (B中の要素数 == N) pthread_cond_wait(c, m);

11: データを書き込む;

12: if (B中の要素数 == 1) pthread_cond_broadcast(c);

13: pthread_mutex_unlock(m);

14: }

15: }

16:

17: C()

18: {

19: for (..;..;..) {

20: pthread_mutex_lock(m);

21: while (B中の要素数 == 0) pthread_cond_wait(c, m);

22: データを読み出す;

23: if (B中の要素数 == N - 1) pthread_cond_broadcast(c);

24: pthread_mutex_unlock(m);

25: 読んだデータを使う;

26: }

27: }

35

Page 36: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

11 課題4: スレッドと同期の練習スレッドと同期の練習として,mainスレッドがたくさんの「子分スレッド」と,

「入力供給スレッド」を作り,入力供給スレッドが多数のデータを供給し,子分スレッドはそのデータを取り出して計算をし,それをmainスレッドに受け渡すというプログラムを書いて見よう.

• ある容量N の有限バッファ(配列)をふたつ用意する (A,Bとする).

• mainプログラムはW 個の子分スレッドと,ひとつの「入力供給スレッド」

• 入力供給スレッドはAにたくさん (≫ N)の入力データ (適当に生成する)を詰め込んでいく.もちろんバッファがあふれないように生産者消費者同期を行う.

• 各workerスレッドはAから一個データを取り出しては,そのデータに対して何かの計算を行い,Bに返す,ということを繰り返す.もちろんAが空だったり,Bが満杯のときのために,生産者消費者同期を行う.

• mainスレッドはスレッド生成後,Bからデータを受け取る.

各子スレッドにどういうデータを供給してどんな計算をさせるかは適当に工夫してほしい.意味のない計算でもよいが,実行時間が調節できるものがよい.短ければ多数のスレッドがAやBにほぼ同時にアクセスをしにいく確率が高くなるし,長ければバッファのあふれが常に起こるようになる.実行時間やW,N をいろいろ変えて,どんなパラメータでも正しく動くようにテストを行うこと.

12 課題5: マルチスレッドサーバ

12.1 概略

さて,いよいよ複数のゲームの同時進行が出来るマルチスレッドサーバである.8

節のシングルスレッドサーバと概略は同じだが以下のようにする.

• トップレベルのスレッド (mainの実行を開始したスレッド)が,すべてのリクエストの受け取る.

• トップレベルスレッドがリクエストを受けたら,「そのリクエストを処理するため」の新しいスレッド (以下,リクエストスレッド)を生成する.そのスレッドは以下のように動作する.

1. パス名が’/’であれば,課題 2と同様.

2. /new_xxx/の形のパス名であれば,そのスレッドが,「そのゲーム担当のスレッド」(以下,セッションスレッド) として動作を開始する.そのゲームを識別するユニークな IDをつくり,Gとする.Gは最も簡単には,そのスレッドのスタック上の,適当な変数のアドレスを用いればよい.他のスレッドと同じ値にならなければ十分だからである.

36

Page 37: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

セッションスレッドは課題 2と同様のページを返す.ただし,そのページに埋め込むリンク (/xxx_S)の中の,xxx の部分はGにしておく.これで,後にこのリンクがクリックされたときに,パス名を見れば,そのリクエストが「どのゲーム用のものか」がわかる.

しかる後に,セッションスレッドはリクエストスレッドからの,さらなるリクエストを待つ状態に入る.

3. /xxx_zzz の形のあらゆるパス名に対しては (課題 2によれば,ここで zzz

は,’S’, ’E’, または一文字の数字の場合があり得た),xxx という IDを持つセッションスレッドにリクエストを届ける.この際に zzz を付随情報として渡す.後はそれを受け取ったスレッドが,zzz の内容に応じて,課題 2 に書かれているような処理を行う.ただし,エラーが発生しても,サーバ全体を終了させるのではなく,そのスレッドだけが終了する.

12.2 リクエストスレッドと,セッションスレッド間の同期について

この課題を達成するに当たっては,/xxx_zzz というタイプのリクエストを,セッションスレッドに誤りなく受け渡すための同期処理がもっとも微妙な部分だろう.実際にはこのゲームサーバのユーザはほとんどの場合一人 (作者自身のみ)だけで,

10秒に一回程度のリクエストをサーバに送るだけなので,これから述べるような同期処理をいい加減に書いたとしても動いてしまうかもしれないし,逆に本当にここまで細かいことを考えてコードを書かなくてはいけないのが,「うざく」感じられるかもしれないが,ここは想像力を働かせて,君の書いたサーバがインターネットに公開され,1万人のユーザが同時に君のサーバにアクセスしてゲームをして遊ぶという状態を想定してコードを書いてみてほしいのである.まず,/xxx_zzz のタイプのリクエストを適切なスレッドに送り届けるために必要な項目には最低限以下の項目がある.

• 情報 zzz を書き込むための,セッションスレッドごとにひとつずつ用意されたメモリ領域.セッションスレッド xxx のためのこの領域をBxxxと呼ぶ.

• その領域を使って,正しく生産者消費者同期を行うこと.すなわち,

– セッションスレッドは,Bxxxに値が書かれていないのに,そこを誤って読むことはない.

– リクエストスレッドは,Bxxxに書かれた値がまだセッションスレッドによって読まれていないのに,そこに次のリクエストを書き込むことはない.

これはまさしく,10.3, 10.4節で述べた,容量 1の有限バッファを用いた生産者消費者同期の問題である.

• リクエストスレッドが,スレッド ID xxx から,領域Bxxxや,それに付随する同期のためのデータ (mutexや semaphore/condition variable)を見つけることが出来るようなデータ構造.これをセッション表と呼ぶことにする.

37

Page 38: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

• セッションスレッドの生成や消滅に伴って,セッション表にスレッドの IDと付随するデータ構造を表に書き込んだり,表から消去したりすること.それを誤りなく行うための同期 (排他制御).

セッション表の構造を極力単純なものとするために,単なる固定長の配列を大域変数として採用しよう.配列の各要素は次のようなデータ構造とする.以下はWindows

の場合である.

typedef struct session_table_entry

{

HANDLE mx; /* このエントリの排他制御 */

int n_freed; /* 後述 */

int occupied; /* このエントリが使われていたら 1 */

void * session; /* このエントリを使っているセッションスレッドの ID */

HANDLE get_sema; /* 生産書消費者同期のためのセマフォ(1だったら以下の sと argから読み込み可能) */

HANDLE put_sema; /* 生産書消費者同期のためのセマフォ(1だったら以下の sと argへの書き込み可能) */

/* 以下がセッションスレッドへ実際に渡される情報 */

SOCKET s; /* このリクエストの返事を返すソケット */

char arg[PATHLEN]; /* zzzを格納する */

} session_table_entry, * session_table_entry_t;

そして以下の配列 stableに,現在進行中の全てのセッションの情報を記録しておくとともに,それらのセッションにリクエストを届けるための領域として利用する.

session_table_entry stable[MAX_SESSIONS];

新しくセッションスレッドを生成したら,配列 stable中から,使われていない領域 (occupied == 0の要素)を見つけ出し,そこに正しく情報を書き込む.そのコードをただしく排他制御して,ほぼ同時に二つのセッションが出来たときの処理が正しく行われるように正しいコードを書こう.ところで,この配列 stableを固定長としたために,この配列の要素数を超える数のセッション (ゲーム)を同時進行させることは出来ない.そういう事態が発生したら,正しくエラーページが返るようにするのも重要な仕事である (過負荷状態での正しい挙動).リクエストスレッドが xxx_zzz タイプのパス名をリクエストとして受け取ったら,フィールド sessionが xxx と一致するようなエントリを配列 stableから探す.最終的には返事を返すべきソケットと,zzz を,そのエントリ中のフィールド sと arg

に書き込んでセッションスレッドに渡すのだが,ただ書き込むだけではダメで,正しく生産者消費者同期を行ってやる必要がある.つまり,書き込むためには,その前のリクエストがすでにセッションスレッドに読まれており,もはや同じ領域を書きつぶしても問題ない,ということを確認しなくてはならないのである.この同期をしそこなった場合にそれが間違った挙動として顕在化する確率はまずない.というのも,何も考えずにリクエストスレッドが sと argに書き込んだとして,それが問題になるのは,その時点までに前のリクエストが処理されていないと

38

Page 39: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

いう場合だけだからである.つまり,ある時点でリクエストが届き,それがセッションスレッドに届いた.そして,セッションスレッドがそのリクエストを読む前に,どういうわけか次のリクエストが同じセッションスレッドに届いてしまった,という場合である.これは,このゲームの通常の遊び方では起こりえない.通常の遊び方では,あるひとつのゲームに関するリクエストをブラウザが送ったら,その結果表示されるページを見てからしか,同じゲームに関するリクエストは送られない.しかしそれでも,ユーザが,こちらが想定した遊び方だけをすると考えてサーバを書くのは危険であり,任意のアクセスを受け付けるプログラムは,いつなんどきどんなパス名のリクエストがやってくるかわからない,という仮定で書いておくべきで,さもないと思わぬセキュリティホールを作ることになる (もっともこの記憶力ゲームではあまり重要な情報は扱わないが).HTTPリクエストはブラウザからではなく,どんなプログラムからでも送れることに注意.最後に,もっとも微妙で,気づきにくく,そしてまず起こらない同期の問題を指摘しておこう.それは,セッションスレッドにメッセージを送る際,何気なく次のように表を探索するコードを書くと発生する.

for (i = 0; i < MAX_SESSIONS; i++) {

if (stable[i].occupied && stable[i].session == xxx) { /* (1) */

/* 生産者消費者同期を行って,

stable[i].s と stable[i].arg に書き込む */

...

}

}

このコードだと,(1)の実行終了直後に,当該のスレッドが終了した場合に,存在しないセッションスレッドにリクエストを送ってしまうことになる.当然このリクエストは永遠に処理されないから,ブラウザから見ると,いつまでたっても返事が返ってこないということになる.それだけならまだしも,最悪のケースは,(1)の直後に当該スレッドが終了し,その直後に同エントリが新しいセッションスレッドのために再利用された場合,セッションスレッドが間違ったメッセージを受け取ることにもなる.ここでもやはり,通常の遊び方でそんなことが問題になるかという疑問はあるが,やはりどんな場合でも安全であるようにサーバを書いておく必要がある.より現実的な問題としては,もしユーザが長時間クリックをしてこなかったらセッションスレッドはタイムアウトして終了すべきであり,その場合はこの競合状態は,通常の遊び方でも発生しうることになる (それでも,稀にしか起こらないことには変わりないが).あまりにも細かい議論でイヤになったかもしれないが,この手の「稀にしかおきないエラー」こそが並行プログラムの難しいところである.エラーがあったらそれが直ちに顕在化するのであればプログラムのデバッグは楽なのである.並行プログラムは,通常の条件ではめったにおこらないエラーが潜入しやすいところが難しい.しかもそれが,高負荷などの特殊な条件下では出やすくなり (例えば高負荷時にはスレッドの切り替えの発生頻度が高くなるため競合状態が顕在化しやすくなる),なおさら再現困難な挙動を示す.そして,サーバプログラムの品質 (安定性)はこのような高負荷時の挙動で決まるのである.

39

Page 40: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

これを直す方法は意欲ある人の課題とする.

13 課題のまとめ実力と意欲に応じて以下の中のどのレベルの課題を選んでもよい.レベルの高い課題を達成する人は,それより下の課題をする必要はない.

レベル 0 5節の課題 1と 11節の課題 4

レベル 1 7節の課題 2と 11節の課題 4

レベル 2 8節の課題 3と 11節の課題 4

レベル 3 12節の課題 5

レベル > 3 レベル 2~3を踏まえたうえで,自由に考える.

• 記憶力ゲームよりも「面白い」挙動を考えて実装してみる (例: コンピュータオセロ).

• 通常のwebサーバと同様のファイル読み出し機能を加える.

• ブラウザを通してではなく,自動的にサーバとのやり取りを行うクライアントプログラムを作成し,かつ複数のクライアントプログラムを同時に立ち上げてサーバにいっせいに,ひっきりなしにリクエストを送る.そして,サーバの限界処理能力 (requests/sec)を計測する.ただし,クライアントとサーバを同じCPU上で動作させても意味のある結果にならないので,複数のコンピュータまたは,複数のCPUを搭載するコンピュータを用意する必要がある.

A Webサーバの構成法本課題の内容を理解すると,およそどんなネットワークサーバを作るに当たっても必要となる,基本プリミティブを理解したことになる.つまり,ネットワークと,複数のクライアントからの要求を受け付けるために必須の並行処理である.したがって,例えば実用的なwebサーバも非常に大まかに言ってここで述べた構造にしたがって作ることが出来る.ここでは興味のある人のために,本課題で述べたサーバの基本的設計以外に,安定性,頑強性,移植性などの観点からよく用いられる設計について簡単に述べておく.将来実際にサーバ (より広くは,ネットワークを用いた分散処理,協調処理,並列処理)を調査・実装したりする際の参考にしてほしい.

40

Page 41: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

A.1 スレッド vs プロセス

まず,本課題では並行処理プリミティブとしてスレッドを用いた.つまり,複数リクエストを処理するために,ひとつのリクエストに対してひとつのスレッドを生成して割り当てた.その代わりにプロセスを用いる方法も一般的である.これは,サーバと同じアドレス空間の中にスレッドを作るのではなく,別のアドレス空間を持ったプロセスを生成する.結果として,サーバ本体とリクエストを行うスレッドの間ではデータをメモリ上で共有することはなくなる.本課題のゲームを作るためには,これは不便である (例えばセッション表を共有することは出来なくなる)が,これをソケットや (説明していないが)パイプを用いた明示的な通信に置き換えれば,この構成をとることも可能である.利点としては各リクエストの処理と,サーバ本体が分離されているため,あるリクエスト処理のバグなどによって,サーバ本体の続行が不可能になる可能性が少なくなるという点がある.また,初期のUNIXはそもそもスレッドという概念がなかっため,歴史的にはこれが唯一の選択肢だったこともあり,UNIXに限ればこの方式はそれなりに移植性が高い.欠点はスレッドの生成に比べてプロセスの生成は重い (メモリを多く消費する) ことである.

A.2 生成 vs プール

本課題ではリクエストが来るたびに新しいスレッドを生成するという設計を述べた.これは設計を単純にするが,高負荷時の性能や安定性に問題がある.非常に速い (つまり,リクエストの処理に匹敵するような)レートでリクエストが到着した場合,サーバ内に生成されて終了していないスレッドが大量に溜まることになる.本課題では各リクエストの処理は非常に短く一瞬で終わる (し,きっとそんなに速いレートでアクセスが来ることはない :-)ので,あまり問題とならないが,各スレッドがそれなりのメモリを消費しながら長時間 (といっても例えば数秒のオーダーは十分長時間といえる)実行する場合,あまりにも大量のスレッドが (OSによって)変わりばんこに実行するのは時として重大な性能の低下を引き起こす.メモリを圧迫してTLBやページフォルトを引き起こすこと (意味がわからなければOSの授業をまじめに聞こう)が主な原因である.そのような場合後から来たリクエストは直ちに処理を開始せず,先行スレッドが終了するまで (たとえ若干それに時間がかかっても),「じっと待っててもらう」のが次善の策である.つまり,1000人のスレッドが細かく変わりばんこに実行するよりも,もっとおおまかな交通整理 (100人実行して,それが終わったら次の 100人,という具合)をしないと,システムがパンクしてしまうのである.それ以外にも,毎回スレッドを生成するのはそもそも無駄,という理由もあって,スレッドないしプロセスをあらかじめ固定数生成しておいて,リクエストがきたらそのうちのどれかのスレッドにそのリクエストを渡すという構造がよくとられる.これはスレッドをあらかじめ「スレッド溜め」に溜めておいて,必要に応じてそこから一個取り出して使う,というニュアンスで「スレッドプール」と呼ばれる.

41

Page 42: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

この構造では高負荷時には自然にリクエストだけがたまっていき,スレッドは前の処理が終わったらそれを順に処理していくことになる.難しいのはスレッド数をいくつにしたらよいかということで,多すぎれば問題は解決しないし,少なすぎれば,全てのスレッドが I/Oでブロックしたときに CPUを有効利用できない.最適な数はシステムの容量 (メモリ,CPU),リクエストの処理時間,必要メモリ量,リクエストのうち I/O待ちなどでブロックする頻度や時間,などに応じて決まる.

A.3 CGI

本課題では,「記憶力ゲーム」などという普通webサーバの機能とは考えらない機能を実装した.通常の webサーバでは,webサーバそのものを変更することなく,この手のアプリケーションを作ることが出来る枠組みが様々用意されている.古くからある代表的なものはCGIというもので,平たく言えばあるパス名にリクエストが送られてきたら,そのプログラムが起動される,という枠組みである.そのプログラムの中で好きなことを書けば色々と変なアプリケーションが実現できる.したがってここで述べたようなゲームのみならず,e-shoppingなどのwebサイトはwebサーバそのものを改造することなく実現されている.ただし CGIの枠組みは,ひとつのリクエストに対して一回プログラムを起動し,そのリクエストが終わったらそのプロセスは消滅する,という枠組みで,たとえば,本課題でやったようにセッションスレッド (プロセス)を立ち上げっぱなしにしておいて,以降のリクエストをその指定したプロセスに届ける,などの機能は存在しない.これは,一連のメッセージのやり取りからなるセッション (ここでいえばひとつのゲーム)の状態をメモリ上に保持しておくという (おそらく誰もが自然だと思う)方法でプログラムを書くことが出来ないということを意味している.そのようなことがしたければ,一回のリクエストごとに状態を全てディスクにセーブし,後続のリクエストが来たら,またそれをディスクから読みださなくてはならないのである.これは不便なようだが,Webサーバの基本構造がこのようになっているのには様々な理由が存在する.Webサーバが時々クラッシュしても上位が失われない,複数のコンピュータに負荷を分散させているwebサイトで,どのコンピュータからでも状態を読めるなどが主な理由である.したがって,複雑な (セッションや,それに付随する状態の概念を持つ)応用のためには,ディスクにおかれた大量のデータを簡単に,効率的に取り出せるインフラストラクチャが重要になり,それがいわゆるデータベースというものである.これについては,該当する講義や演習で学んでください.

A.4 非同期 I/O

スレッドを各リクエストに対して生成することの欠点は,高負荷時の性能の劣化である.一方,固定数のスレッドでやりくりすることの欠点は I/O待ちなどでスレッドがブロックした際にCPUを有効利用できない (つまり,リクエストは待っているのに,それにスレッドが割り当てられない)可能性がある点である.より込み入った解法として,スレッド数は固定だが,決してスレッドが I/Oでブロックしないようにする,という解法がある.それには,(上では説明していない)

42

Page 43: システムプログラミング演習 - 東京大学tau/lecture/system...最後に,プログラミングマニュアル的な情報はwebにもあふれているので,検索

「非同期 I/O」という概念を用いる.これは概念は単純で,たとえば diskやネットワークからデータを読むときに,データが来ていなければ,データが来るまでブロックする代わりに,単にその旨が通知されて処理が終わる,というだけのものである.もちろん最終的にはそのデータを読むまでそのリクエストは,「先へは進めない」が,このときに,そのスレッドが他のリクエストに手を伸ばして実行する余地が残されている,という点が異なる.概念的には単純だが,プログラミングは複雑になる.また,ページフォルトなど,非同期 I/Oがあってもまぬがれないブロックも存在する.

43