次のページ 前のページ 目次へ

2. SMP Linux

このドキュメントでは、 SMP Linux システムを並列処理に使う方法について、要点をかいつまんで見ていきます。 SMP Linux の最新の情報は、SMP Linux プロジェクトのメーリングリスト で得られます。majordomo@vger.rutgers.edu に subscribe linux-smp と書いて電子メールを送ると参加できます。

SMP Linux は本当に動作するのでしょうか? 1996 年 6 月に新品の(実はノー ブランド ;-)100MHz 駆動の Pentium を 2 個搭載したシステムを購入しました。 パーツから組み上げたシステムで、プロセッサと Asus のマザーボード、256 K のキャッシュ、32 MB の RAM、1.6 GB のディスク、6 倍速の CD-ROM、ビデオ カードに Stealth 64、15 インチの Acer のモニターがついて、合計 1,800 ドルでした。プロセッサが 1 つのシステムと比べて数 100 ドル高くなりました。 SMP Linux を動かすのは難しいことではなく、単独プロセッサ用の「普通について くる」Linux をインストールして、makefile にある SMP=1 という行 のコメントを外して、カーネルを再コンパイルし(SMP1 に設定とは、ちょっと皮肉ですね ;-)、lilo に新しいカーネルを設定 すればいいだけです。このシステムのパフォーマンスは申し分なく、安定性も 十分です。それ以来メインのワークステーションとして活躍しています。つまり SMP Linux は実際に動いているのです。

次にわからないのは、はたして SMP Linux でどの程度のレベルの高さ で共有メモリを使った並列プログラムをコーディングして実行できるかと いうことでしょう。 1996 年前半では、まだ十分とはいえませんでした。しかし状況は変わりました。 例えば、現在では極めて完成度が高い POSIX スレッドライブラリがあります。

本来の共有メモリ方式と比べてパフォーマンスは劣りますが、SMP Linux システムでは、もともとソケット通信を使ったワークステーション上で動く クラスタ用に開発した並列処理用のソフトウェアのほとんどを利用できます。 ソケット(セクション 3.3 を参照のこと)は、単独の SMP Linux 上で動作し、 クラスタをネットワーク上で組んだ複数の SMP マシンでも動作します。 しかし、ソケットは SMP にとっては必要ないオーバーヘッドがかなり多く あります。オーバーヘッドのかなりの部分はカーネルや割り込み制御にあり ます。これが問題をさらに悪化させています。というのも、通常 SMP Linux は、カーネル内で同時に動作できるプロセッサは 1 つだけで、割り込み コントローラとして設定できるプロセッサもたった 1 つだけなのです。 つまりブートしたプロセッサしか割り込みをかけられません。 それにもかかわらず、標準的な SMP の通信機器は、大部分のクラスタを組んだ ネットワークよりも優れています。クラスタ用に設計されたソフトウェアが、 クラスタ上で動くよりも SMP 上の方がうまく動作することが多いようです。

【訳註:Linux のカーネルが 2.4 になり SMP 対応が改良されました。 2.2 系列以前では、システムコールが呼び出されるとそれが動いているプロセッサ がカーネルを離すまでカーネル全体にロックをかけていました。 つまり他のプロセッサがカーネルを利用しようとしても(システムコールを発行 しても)、カーネルのロックがはずれるまで(現在動いているシステムコールの 処理が終わるまで)処理待ちになっていました。2.4 系列ではカーネル全体ではなく、 カーネル内部でプロセッサ間で共有している資源単位でのロックが可能になりました。 ただしすべてが資源単位でのロックに変更されたわけではなく、依然としてカーネル をロックする部分もあります。 割り込みについても、それぞれのプロセッサからかけることが可能になりました】

このセクションの残りの部分では、SMP のハードウェアについて論じ、Linux で 並列プログラムのプロセスがメモリを共有し合う基本的なしくみを見て行くこと にします。アトミックな処理や変数の保存、資源のロック、キャッシュ・ライン について 少し意見を述べて、最後に他の共有メモリ並列処理についての資料を いくつか紹介したいと思います。

2.1 SMP のハードウェア

SMP システムはもう何年も動き続けていますが、つい最近まで基本的な機能の 実装がそれぞれのマシンで異なる傾向にあったため、オペレーティングシステム のサポートは移植性があるとは言えませんでした。 この状況が変わるきっかけになったのが、MPS と呼ばれる Intel の Multiprocessor Specification です。MPS 1.4 の仕様は、PDF ファイルで http://www.intel.com/design/pro/datashts/242016.htm から利用 できます。また MPS 1.1 についての概要が http://support.intel.com/oem_developer/ial/support/9300.HTM 【訳註:リンク切れ】にあります。ただ、intel の WWW サイトはよく構成を変える ので注意してください。 プロセッサを最大 4 つまで搭載する MPS 互換のシステムは、数多くの ベンダーが構築して います。しかし MPS は理論上もっと多数のプロセッサをサポートしています。

非 MPS で 非 IA-32 なシステムで SMP Linux がサポートしているものは、 Sun4m を載せたマルチプロセッサの SPARC マシンです。SMP Linux は Intel の MPS バージョン 1.1 もしくは 1.4 互換のマシンをサポートしており、16 個までの 486DX、Pentium、Pentium MMX、Pentium Pro、Pentium II をサポート しています。 サポートしていない IA-32 プロセッサには、Intel の 386 と Intel の 486SX/SLC です(SMP のしくみと浮動小数点演算装置をつなぐインタフェースがありません)。 AMD と Cyrix プロセッサ(SMP をサポートするチップが異なるので、この ドキュメントを書いている時点では利用できません)が上げられます。

【訳註:先に述べた KLAT2 は、AMD の Athlon を搭載していて、SWAR として intel の MMX に加えて、AMD のマルチメディア拡張命令である 3DNow! も利用しています】

大切なのは、MPS 互換のシステムのパフォーマンスが大きくばらつくことを 知っておくことです。皆さんの予想通り、プロセッサの速度はパフォーマンス の差に影響を与える要因の 1 つです。クロック速度が速ければ速いほど、より 高速なシステムになる傾向にありますし、Pentium Pro は Pentium よりも高速 です。しかし MPS 自体は共有メモリの実装がどのようであるかについて仕様 を決めているわけではなく、ただソフトウェアの点からどのように実装が機能 すべきなのかを決めているに過ぎません。つまりパフォーマンスとは、共有メモリ の実装がどのように SMP Linux やユーザのプログラムの特性と相互に連係して いるかの結果でもあります。

まず MPS に準拠したシステムそれぞれで見なければいけないのは、どのように システムが物理的な共有メモリにアクセスできるのかということです。

それぞれのプロセッサは独自の L2 キャッシュを持っているか?

MPS Pentium の一部やすべての MPS Pentium Pro、Pentium II システムは、 独立した L2 キャッシュを持っています(Pentium Pro や Pentium II はモ ジュールに組み込まれています)。独立した L2 キャッシュは一般的に コンピュータのパフォーマンスを最大限に引き出すと見なされていますが、Linux ではそれほど単純ではありません。複雑にしている第 1 の原因は、現状の SMP Linux のスケジューラがプロセスそれぞれを同じプロセッサに割り当て続け ないようにしている点にあります。これはプロセッサ・アフィニティ と呼ばれている考え方です。 これはすぐにでも変更されるかもしれません。最近「プロセッサ・バインディ ング」というタイトルで、SMP Linux の開発コミュニティでこの考え方が 何度か議論されました。プロセッサ・アフィニティがないと、独立した L2 キャッシュが存在した場合無視できないオーバーヘッドが発生してしま います。それは、あるプロセスが最後に実行していたプロセッサではなく、 別のプロセッサでタイムスライスを割り当てられた場合に発生します。

【訳註:カーネル2.2系列では、特定の CPU に特定のプロセスを割り 当てることは標準ではできませんが、パッチとして PSET - Processor Sets for the Linux kernel( http://isunix.it.ilstu.edu/~thockin/pset/) が用意されています。 2.4 系列では、プロセスを同じプロセッサになるべく処理させるように スケジューリング方法が変更されています。またプロセッサ・バインディング は、割り込みについては実装されており、プロセスについては検討中です】

比較的安価なシステムの多くは、1 つの L2 キャッシュを 2 つの Pentium プロセッサで共有しています。困ったことにキャッシュの競合が発生して、複数 の独立した順次処理をするプログラムに対して深刻なパフォーマンスの低下を引き 起こします。逆に良い点は、並列プログラムの多くが実際に共有したキャッシュ の恩恵を受ける可能性があることです。というのは、両方のプロセッサが 共有メモリの同じラインにアクセスしようとする場合、キャッシュにデータを フェッチしなければならないのは 1 つのプロセッサだけで、バスの競合が避けられ ます。プロセッサ・アフィニティがなければ、共有の L2 キャッシュのデータ 不整合をより低く押えることもできます。つまり並列処理にとって、予想していた ほど、共有の L2 キャッシュに欠点があるとは断定できません。

私達が所有している 2 つの Pentium を搭載した 256 K のキャッシュを持つ システムを使用した経験からすると、パフォーマンスに非常にばらつきがあり、 その原因はカーネルが負う処理のレベルに依存しています。最悪で約 1.2 倍 の速度向上にしかなりません。しかし 2.1 倍まで高速化ができたことから、 計算中心の SPMD スタイルのコードはまさに「フェッチの共有」効果がでて いることになります。

バスの構成?

最近の大部分のシステムでは、プロセッサに 1 つ以上の PCI バスが接続して いて、その PCI バスに 1 つ以上の ISA や EISA バスが「ブリッジ」しています。 これらのブリッジが遅延を引き起こし、さらに EISA と ISA は PCI に比べて帯域 も狭くなります(ISA が最も遅い)。そのためディスクドライブやビデオカード他の 高いパフォーマンスを必要とするデバイスは、普通 PCI バスインタフェース経由 で接続すべきです。

MPS システムでは、PCI バスが 1 つしかなくても計算が多くを占める並列プロ グラムを高速に実行できます。しかし入出力処理は、プロセッサが 1 つのもの より良いとは言えません…恐らくプロセッサによるバスの競合で若干 パフォーマンスが落ちてしまいます。つまり入出力の速度向上に注目するとすれ ば、複数の独立した PCI バスと入出力コントローラがある MPS システムを手に 入れる方が良いということになります(例えば複数の SCSI 接続)。ここで注意 しなければいけないのは、SMP Linux があなたが所有する部品をサポートして いるか、ということです。また最新の SMP Linux が基本的にカーネル上では 常に 1 つのプロセッサしか動いていないということも忘れないでください。 そういう訳で、入出力コントローラには入出力処理それぞれに必要となるカーネル の処理時間が最小のものを選ぶように心がけてください。本当に高性能を目指す ならば、システムコールを使わないで、ユーザ・プロセスから直接 raw デバイス を使って入出力することを考慮してもいいのではないでしょうか。これは思った ほど難しいことではありませんし、セキュリティレベルを下げることにもなり ません(セクション 3.3 で基本的なやり方を説明します)。

バスの速さとプロセッサのクロックの速さの関係がここ数年でいびつになって いることをぜひ知っておいてください。 現状ではシステムの大部分は同一の PCI のクロック速度を使用していますが、 高速なプロセッサのクロックと低速なバスのクロックの組合せは、まれなケース とは言えません。Pentium 133 は Pentium 150 より速いバスを 採用しており、様々なベンチマークで一見不思議な結果を出しているのが良い例 です。これらの影響は SMP システムではさらに大きくなります。より速いバス・ クロックがより重要なのです。

【訳註:Pentium 133 は 66 MHz の 2 倍速、Pentium 150 は 60 MHz の 2.5 倍速で駆動します】

メモリのインタリーブと DRAM の技術?

本来、メモリをインタリーブすることは MPS とは何の関係もありませんが、 MPS システムではよく触れられる話です。というのも、これらのシステムは 往々にしてメモリの帯域幅をより必要とするからです。普通は、2 way もしくは 4 way をインタリーブして RAM を構成します。そのためブロック・アクセスは、 1 つのバンクだけではなく、複数のバンクを使用します。 これによって、より広い帯域幅でメモリにアクセスでき、とりわけキャッシュ のロードやストアに効果があります。

【訳註:インタリーブとは、メモリ上のデータ読み書きの高速化を はかるための方法の 1 つで、搭載されているメモリをグループ分けします。 このグループをバンクと呼び、このバンク単位に並列にアクセスする方式を とります。 2 way は 2 バンク、4 way は 4 バンクにメモリをグループに 分けて並列にアクセスします】

しかしこの効果については、状況が少々混沌としてきました。その理由は、 EDO DRAM と他の様々なメモリ関連技術が同じような種類の操作に改良 を加えてきているためです。DRAM 技術についてとてもよくまとめられた 資料が、 http://www.pcguide.com/ref/ram/tech.htm で見られます。

それでは、2 way でインタリーブしている EDO DRAM とインタリーブして いない SDRAM ではどちらがパフォーマンスが優れているでしょうか? この 質問はとてもいい質問なのですが、簡単には答えらえません。というのも、 インタリーブするのも、魅力ある DRAM 技術も、とても高価になりがちだから です。同じお金をかけて普通のメモリを購入すれば、より多くのメモリを 確保できるのは明らかです。一番遅い DRAM でも、ディスクベースの仮想 メモリより比べものにならないほど高速です…。

【訳註:SDRAM と EDO RAM の大きな違いは、SDRAM は PC のベース クロックと同期して動作するのに対して、EDO RAM はベースクロックとは非 同期に動作する点にあります。しかし SDRAM もはじめのアドレスにあるデータ にアクセスする時には EDO DRAM と同様に遅延(RAS、CASの)が発生します。次 世代メモリと言われる RDRAM もより広いメモリ帯域幅が実現できますが、DRAM を使用することにはかわりなく、やはり遅延が発生してしまいます。 つまり、現在までのメモリの高速化はバーストモードの高速化が中心であって、 DRAM 自体が抱えている遅延の問題を根本的に見直しているわけではありません】

2.2 共有メモリを使ったプログラミングをはじめるにあたって

そろそろ並列処理を SMP で動かすことが、いかに素晴らしいかを理解できたと 思いますが…。どこからはじめたらいいでしょうか? まずは共有メモリ通信が 実際にどのように動くのか、少しばかり勉強してみましょう。

あるプロセッサが値をメモリにストアして、あるプロセッサがそれをロード する―こう言うと単純なことに思われるかもしれませんが、残念ながらそれほど 単純ではありません。例えば、プロセスとプロセッサの関係はきっちり決って いるわけではありません。しかし、プロセッサの個数以上にプロセスが動いて いないなら、プロセスとプロセッサという言葉を入れ換えてしまっても、 おおざっぱに言って同じことです。このセクションの残りの部分では、大きな 問題になると思われるキーポイントを概観して、ポイントを外したままになら ないようにしたいと思います。何を共有すべきかを決めるのに使われるモデルを 2 つと、アトミックな操作、変数の保存のしかた、ハードウェアのロック操作、 キャッシュ・ラインの効果、Linux におけるプロセスのスケジューリングを 扱います。

すべてを共有するのか、一部を共有するのか

共有メモリを使うプログラミングには、根本的に異なる 2 つのモデルがあり ます。それはすべてを共有する場合と一部を共有する場合 です。この 2 つのモデルとも、プロセッサが共有メモリからもしくは共有 メモリへロードやストアをしてやりとりできます。違うところは、すべてを 共有する場合がデータ構造すべてを共有メモリに置くのに対して、一部を共有 する場合は、ユーザがはっきりとどのデータ構造が共有メモリを使用する可能性 があり、どのデータ構造が単一プロセッサのローカルなメモリを使用 するかを示す必要があります。

どちらの共有メモリモデルを使うべきでしょうか? たいていの場合は好みの 問題と言ってもいいと思います。すべてを共有するモデルを好む人々が多いの は、データ構造のどれが宣言されたその時に共有されるべきか、ということを 区別する必要がまったくないからです。ただ競合しそうなアクセスにロックを かけて、共有しているものが 1 つのプロセス(プロセッサ)からしか一時に アクセスしないようにします。繰り返しますが、これも上記のように単純なわけ ではありません。そこで、比較的安全である共有するものを一部にとどめる方法 を選ぶ人々が多数いるのです。

すべてを共有する

すべてを共有する利点は、既にある順次実行プログラムがたやすく手に入ること、 そしてそのプログラムをすべてを共有することを前提にした並列プログラムに、 それほど手間をかけず修正していける点にあります。どのデータが他のプロセッサ によってアクセスされるかを最初から解決する必要はありません。

簡単に言うと、すべてを共有する上で大きな問題となるのは、あるプロセッサが 処理したことすべてが他のプロセッサに影響を与えてしまうかもしれない点 にあります。この問題が顕著になるケースは 2 つあります。

上記の類の問題は、一部を共有する方法をとった場合にはほとんど起こり ません。それは、はっきりと指定したデータ構造だけを共有するためです。 また、すべてを共有する方法が動作するのは、プロセッサすべてが全く同じ メモリ・イメージを実行している場合だけです。これはほとんど疑う余地が ないことです。すべてを共有して複数の異なるコード・イメージを扱うこと はできません(つまり、SPMD は利用できますが、MIMD はできません)。

すべてを共有する場合にプログラムがサポートする典型的なタイプが、スレッド・ライブラリです。 Threads は、本来「軽量(light-weight)」なプロセスとして、通常 の UNIX 上のプロセスとは違ったスケジュールで動作していました。ここで とても大切なのは、1 つのメモリ・マップを共有してアクセスする点にあります。 POSIX Pthreads のパッケージは、移植に対してたいへんな労力を注いできました。 ここで非常に疑問なのは、移植されたものが本当に SMP Linux 上のプログラム のスレッドとして並列に動作するのか、ということです(理想的には、ある プロセッサでそれぞれのスレッドが動作する)。POSIX API は、そこまでを 求めていませんし、 http://www.aa.net/~mtp/PCthreads.html 【訳註:リンク切れ】のようなバージョンは、スレッドを並列に実行することを まったく考慮していません。プログラムから実行されるスレッドはすべて、Linux の 1 つのプロセス内で動き続けます。

SMP Linux の並列処理を最初にサポートしたスレッド・ライブラリは、既に 過去のものになりつつある bb_threads ライブラリで、 ftp://caliban.physics.utoronto.ca/pub/linux/ にあります。これは とても小規模なライブラリで Linux の clone() システムコールを 使い、新たに子プロセスを立ち上げた上で独自のスケジュールで動きます。 ここでは Linux のプロセスすべては 1 つのアドレス空間を共有します。 SMP Linux マシンでは、これらの「スレッド」を複数並行に動作することが できます。理由は各々の「スレッド」が Linux のプロセスに他ならないから です。これと引き換えに、他のオペレーティング・システムが提供している いくつかのスレッド・ライブラリのような「軽量な」スケジュール制御は できません。このライブラリは、C でラッパーしたアセンブリ・コードが少し 含まれていて、メモリ上にそれぞれのスレッドのスタック領域を確保し、 アトミックなアクセス機能をロックの配列(ミューテック、mutex)で実現します。 ドキュメントは、README と短いサンプル・プログラムがあります。

最近になって、clone() を使った POSIX スレッドが開発されてい ます。このライブラリは LinuxThreads で、SMP Linux で使用することを前提にしてすべてを共有することを主目的に したライブラリです。POSIX スレッドはドキュメントが整備されており、 LinuxThreads READMELinuxThreads FAQ はとても良い資料です。現状での主な問題は、POSIX スレッドは内容が非常に濃いのために、きちんとその内容を理解する必要がある こと、LinuxThreads は開発途中であることです。また、POSIX スレッド規格は、 標準化作業中という問題もあり、古くなった規格の初期バージョンで作成しない ように少々注意が必要です。

【訳註:POSIX スレッドは IEEE 1003.1 として標準化されています。 各種スレッドライブラリについては Multithreading Libraries、マルチスレッドのプログラミングについては マルチスレッドのプログラミングが役に立ちます】

一部を共有する

実際のところ一部を共有するということは、「共有する必要があるものだけを 共有する」ということです。このアプローチは、一般的には MIMD(SPMD ではない) に合致していて、それぞれのプロセッサのメモリ・マップ上で同じ位置にある共有 するオブジェクトを扱う点に注意が必要となります。もっと大切なのは、一部を共有 することでパフォーマンスの予測と改善やコードのデバック等が簡単になることです。 ただ問題になるのは、

現状では、Linux のプロセスグループを独立したメモリ空間に置くのに非常に 似かよった手法が 2 つあります。共有するものすべては、そのメモリ空間上で はほんのわずかなセグメントを使用するだけです。Linux システムを設定する 時に「System V IPC」をうっかり外してしまわなければ、 Linux は「System V Shared Memory(共有メモリ)」というとても移植性が高いしくみを利用できる ようになります。他の選択としてメモリのマッピング機能も利用でき、これは いろいろな UNIX システム間でそれぞれ実装されています。それが mmap() システムコールです。マニュアルその他でこれらのシステムコールを学習する ことをお勧めします。簡単な概略はセクションの 2.5 と 2.6 で触れますので、 勉強するきっかけとして利用してください。

アトミックな処理と処理の順序

上記どちらのモデルを使うにしても、結果はそれほど違いません。要は並列 プログラムの中から、すべてのプロセッサがアクセスできる読み書き可能な メモリの一部へのポインタを得るわけです。これが意味するところは、共有 メモリ上のオブジェクトをアクセスする並列プログラムが、そのオブジェクト がローカルなメモリにあるかのごとく扱える、ということなのでしょうか? そうとも言えるのですが、ちょっと違います…

アトミックな処理というのは、ある対象に対する操作を部分に分け たり、割り込みが入ったりすることなく、連続して処理をする考え方です。 残念ながら共有メモリのアクセスでは、共有メモリ上にあるデータに対する すべての操作がアトミックに行われるわけではありません。何か手を打たない 限り、単純なロードやストア操作のようなバス上での処理が 1 回で済む操作 (つまり、8、16、32 ビットにアラインされた操作。アラインされていなかったり、 64 ビットの操作は異なる)だけが、アトミックな処理になります。 さらに都合が悪いことに、GCC のような「賢い」コンパイラでは最適化が働い てしまい、あるプロセッサの処理が完了したことを他のプロセッサが検知する のに必要となるメモリ上の操作を取り除いてしまいがちです。 幸いなことには、これらの問題 2 つとも改善することができます。気になる アクセスの効率とキャッシュ・ラインの大きさの関係をそのままにして置くのです。

しかしこれらの問題点を論じる前に、それぞれのプロセッサがメモリを参照 する時には、前提としてコーディングした順番で参照するということをはっきり しておくことは無駄ではありません。Pentium がそうですが、将来の Intel の プロセッサがそうであるとは限らない、ということも忘れないでください。 そのようなわけで、気に止めておいて欲しいことがあります。それは将来の プロセッサが、保留しているメモリへのアクセスを完了させる命令を通じて 共有メモリにアクセスする必要が出てくるかもしれない、ということです。 つまり、メモリへのアクセスに順番を付ける機能を提供するわけです。CPUID 命令は、その命令によって生じる悪影響に対しての予防処置であることは 明らかです。

変数の保存のしかた

レジスタにある共有メモリのバッファされているオブジェクトの値を GCC が 最適化しないようにするには、共有メモリ上のすべての実体を volatile という属性をつけて宣言する必要があります。こう宣言すると、共有 するオブジェクトに対する 1 ワードだけのアクセスをともなう読み書きは、 アトミックに行われます。例えば、p が整数へのポインタでポインタ と整数両方が共有メモリにあるとすると、ANSI C での宣言は次のようになります。


volatile int * volatile p;

このコードでは、最初の volatileint を参照し、その intp を指しています。次の volatile は、 ポインタそのものを参照しています。ややっこしいのですが、このちょっと した指定で、GCC があちこちで強力に最適化することが可能になります。少なく とも理屈上では、GCC に対して -traditional オプションをつける ことで、最適化をいくぶん犠牲にして正しいコードを間違いなく生成できる と思います。というのは、ANSI が定められる前の K&R の C は、そもそも register と意図的に指定をしない限り、すべての変数を volatile 扱いにしていました。GCC でコンパイルする場合に cc -O6 ... という様にしているなら、本当に必要な部分にははっきりと volatile と指定したくなるに違いありません。

プロセッサのすべてのレジスタが更新されたことを知らせる役目であるアセンブリ 言語のロックを使うと、GCC が適切にすべての変数をフラッシュして、 volatile と宣言したことに関連したコードが「非効率」にコンパイル されることを防ぐ、という噂があります。この裏技は、GCC のバージョン 2.7.0 を使って static で確保した外部変数に対して効果を発揮します…がこれは ANSI C 標準に沿った機能ではありませんし、なおまずいことに読み込みだけ を行う他のプロセッサは、レジスタにずっと値をバッファしてしまいます。つまり、 共有メモリ上の値が変更されていることを全く知りようがありません。 まとめると、望みの動作を行うには volatile を使って変数にアクセス するしか正しく動作することが保証されない、ということです。

通常の変数に対して volatile 属性を指定してタイプキャストをする ことで、volatile なアクセスが行えるということを理解しておいてください。 例えば、通常の int i;*((volatile int *) &i) とすることで、volatile に参照できるようになります。つまり、重要な部分に 対してだけ「オーバーヘッド」をともなう volatile な操作を意図して呼び 出すことができます。

ロック

++i; が、共有メモリにある変数 i に常に 1 を加える と思っているなら、「なんだ、これは」という事態になります。単一な命令 を実行するようにコーディングされていても、ロードやストアは結果的に独立 してメモリに対する命令を発行し、他のプロセッサは i に対する この 2 つの命令の間にアクセスできてしまいます。例えば、2 つのプロセスが 両者から ++i; を実行すると、i は 2 増えるのではなく、 1 増えることになるかもしれません。Intel による Pentium の「Architecture and Programming Manual」によると、LOCK という単語が頭について いると、その後にくる命令が何であっても、メモリ上のデータへのアクセスに 関連する操作はアトミックであることが保証されています。


BTS, BTR, BTC                     mem, reg/imm
XCHG                              reg, mem
XCHG                              mem, reg
ADD, OR, ADC, SBB, AND, SUB, XOR  mem, reg/imm
NOT, NEG, INC, DEC                mem
CMPXCHG, XADD

しかしこれらの命令を全面的に採用することは、良い思い付きとはいえない でしょう。例えば、XADD は 386 プロセッサでは使用できません ので、移植の面で問題が生じてしまうでしょう。

XCHG 命令は、LOCK という単語が頭についていなくても 常にロックをかけますし、この方法は明らかにアトミックな操作と して、より高級なアトミックのしくみであるセマフォやキューの共有よりも 優れています。もちろん C でコーディングしたものを GCC にかけても、この 命令は生成できません…そのかわりに、少しばかりインラインでアセンブリ・ コードの記述が必要になります。1 ワードの volatile なオブジェクトである obj とやはり 1 ワードのレジスタ値である reg を定義 すると、GCC のインラインでのアセンブリ・コードは下記のようになります。


__asm__ __volatile__ ("xchgl %1,%0"
                      :"=r" (reg), "=m" (obj)
                      :"r" (reg), "m" (obj));

GCC のインラインのアセンブリ・コードで、ビット操作を使ってロックを かける例は、 bb_threads library にあります。

しかし、おぼえておいて欲しいことがあります。それはメモリ操作をアトミック に行うと、それなりにコストがかかるということです。ロックをかける操作は かなり大きなオーバーヘッドがかかり、通常の参照がローカルなキャッシュを 利用するのに比べ、他のプロセッサからのメモリ参照は遅延が生じてしまうこと になるでしょう。最適なパフォーマンスを得るには、できるだけロック操作を 使用しないことです。また、これら IA32 でのアトミックな命令は、当然ですが 他のシステムには移植できません。

この他にも解決方法はいろいろあります。通常の命令に対して、同期をいろいろ ととるための実装がいくつも用意されています。その中には 相互排他 (mutual exclusion。略称 mutex))があり、少なくとも 1 つのプロセッサ がいつでも共有しているオブジェクトを更新できることを保証しています。 たいていの OS のテキストでは、これらのテクニックの内のいくつかを論じて います。Abraham Silberschatz 氏 と Peter B. Galvin 氏の共著である、 Operating System Concepts、ISBN 0-201-50480-4 の第 4 版で非常に 優れた議論がなされています。

キャッシュ・ラインの大きさ

アトミックな操作に関して必須で、SMP のパフォーマンスに劇的な影響を あたえることがもう 1 つあります。それはキャッシュ・ラインの大きさです。 MPS 規格ではキャッシュが使われていてもいなくても、同期するための参照が 必要とされています。しかし 1 つのプロセッサがメモリのあるラインに書き 込みを行うと、以前書き込まれたライン上にあるキャッシュしてあるものは、 すべてを無効にするか更新する必要があります。2 つ以上のプロセッサどれもが データを同じラインの違う部分に書くとすると、キャッシュやバスに多量のやり 取りが発生してしまうかもしれません。これは事実上キャッシュ・ラインから 受渡しが起こることを意味しています。この問題は、偽共有(false sharing) と言われています。解決するのはいたって単純で、並列にアクセスする 場合は、それぞれのプロセスに対して、データをできるだけ異なるキャッシュ・ ラインから取ってくるようにデータを構成することです

偽共有は、CPU が共通して使用する L2 キャッシュを持っているシステムには 関係ない問題と思われるかもしれませんが、独立して存在する L1 キャッシュ を忘れてはいけません。キャッシュの構成や独立したレベルの数は両者とも 変動するもので、Pentium の L1 のキャッシュ・ラインの大きさは 32 バイトで 外部キャッシュ・ラインの典型的な大きさは 256 バイト程度です。仮に 2 つの 要素のアドレス(物理、仮想どちらでも)が ab として、 プロセッサ毎の最大のキャッシュ・ラインの大きさが c としてそれが 2 の累乗の大きさとします。厳密に言うと、もし ((int) a) & ~(c - 1)((int) b) & ~(c - 1) と等しければ、どちらも同じキャッシュ・ラインを参照することになりま す。共有するオブジェクトが並列に参照される場合、少なくとも c バイト離れた位置にあるならば、異なるキャッシュ・ラインに置かれるべきだ、 というのが原則です。

Linux のスケジューラの問題点

並列処理で共有メモリを使用する際には、あらゆる部分で OS のオーバー ヘッドを避けなければいけませんが、OS のオーバーヘッドは通信それ自体 以外のところから発生する可能性があります。既にこれまでに述べてきました が、プロセスの数はマシンに付けてあるプロセッサ数以下にするように 構成すべきです。でもどうやって動かすプロセス数を正確に決められるので しょうか?

最高のパフォーマンスを得るには、並列プログラムで動かすプロセス 数は、そのプログラムで起動するプロセスが同時に異なるプロセッサで 動かせると思われる数と同じにすべきです。例えば、4 つのプロセッサ を持つ SMP システムでは、通常 1 つのプロセッサは何か別の目的(例えば WWW サーバ)に使われているとすると、3 つのプロセッサだけで並列プログ ラムを動かすことになるはずです。 おおまかにどのくらいのプロセスがシステムでアクティブなのかは、 uptime コマンドで見られる「load average(平均負荷)」を見れば わかります。

これとはまた別の方法があり、並列プログラムで使っているプロセスの実行 優先順位を引き上げることが可能です。例えば、renice コマンドや nice() システムコールです。優先度を上げるには、特権が必要に なります。このアイディアは単純で、他のプロセスをプロセッサから追い出 して、自分のプロセスをすべてのプロセッサで同時に動かしてしまおうと いう考えです。この考えをプロトタイプ版の SMP Linux を使用してもう 少しきちんと実現したものが http://www.rtlinux.org/ で、リアルタイムなスケジューリングを提供しています。

SMP システムを並列処理マシンをただ一人で扱うユーザでないなら、2 つ以上 の並列プログラムを同時に実行しようとすると、プログラム間で衝突が起こる 恐れがあります。普通これを解決する方法は、ギャングスケジューリング (gang scheduling)をとります。すなわち、スケジューリングの優先度を 操作して、いつでもただ 1 つの並列プログラムに関するプロセスだけが動いて いるようにします。しかし思い出してみてください。並行処理を行えば行うほど 結果を得られにくくなり、スケジューラにオーバーヘッドが加わります。つまり 例えば、4 つのプロセッサを持つマシンが、強制スケジュールを使って 2 つの プログラムをそれぞれ 4 つのプロセスで動かすよりも、2 つのプログラムを それぞれ 2 つのプロセスで動かす方が効率良いのは確かです。

さらにもう 1 つこの問題をややこしくしている事があります。あるマシンで プログラムを開発していて、そのマシンは終日とても忙しく動いているが、 夜間は並列処理に丸々使えるとします。プロセスすべてを立ち上げて正しく動く ようにコードを書いて、それをテストしなければいけませんが、昼間のテストは 遅いことを思い知らされると思います。実のところとても遅くなり ます。もし現在動いていない(他のプロセッサ上の)他のプロセスが変更した共有 メモリ上の値をビジーウエイトしているプロセスがあるならば。同様 の問題は、プロセッサが 1 つしかないシステムでコードを開発したりテストし たりする時にも起こります。

解決方法は、コードに呼び出しを組み込むことです。どんな場合でも、他の プロセスからの動きをループを使って待つようにします。そうすると Linux は、他のプロセスが動く機会を与えることができます。私は C のマクロを 使って IDLE_ME のように呼び出しています。テストで動かす 時には、cc -DIDLE_ME=usleep(1); ... とコンパイルし、 「製品」として動かす場合には cc -DIDLE_ME={} ... と コンパイルしています。usleep(1) を呼び出すと、1 ミリ秒間スリープ するようになり、Linux のスケジューラがそのプロセッサ上で別のプロセスを 選べるようになります。プロセスが使用可能なプロセッサの 2 倍以上の数に なると、コードの中で usleep(1) を使うと、使わない場合の 10 倍 速くなることも珍しくありません。

2.3 bb_threads

bb_threads (「Bare Bones」(必要最低限の) threads)ライブラリは、 ftp://caliban.physics.utoronto.ca/pub/linux/ にある非常に 簡素なライブラリです。Linux の clone() システムコールを使って 実装されています。gzip tar ファイルでたった 7 KB です! この ライブラリは、2.4 で論じた LinuxThreads ライブラリが世に出たことで 過去のものになってしまいましたが、bb_threads はまだまだ役に立ち ます。小さく、シンプルで、とりあえず最初に Linux のスレッドを使用する には十分です。このソース・コードを読んでも、LinuxThreads のソースを眺めた 時のように威圧感を感じることはないのは確かです。まとめてみると、 bb_threads ライブラリは最初に試してみるのには良いライブラリですが、 正直なところ大きなプロジェクトのコーディングで使用するのには適していません。

bb_threads ライブラリを使った基本的なプログラムの構築方法は、 下記の通りです。

  1. プログラムは、1 つのプロセスとしてスタートさせること
  2. それぞれのスレッドで必要になるスタック空間の最大値を見積ること。 大きく見積っても害はない(仮想記憶がそうであるように ;-)。 しかし忘れないで欲しいことは、スタックすべては単独の仮想アドレス 空間に由来しているため、大きいことはいいことだ、というわけにはいかないこと である。64 K がお試しの大きさである。 bb_threads_stacksize(b) とすると、 b バイトに設定できる
  3. 次のステップは、必要となるすべてのロックを初期化することである。 ロックのしくみはこのライブラリに組み込まれていて、0 から MAX_MUTEXES までの番号が設定でき、i という ロックを初期化するには、bb_threads_mutexcreate (i) と設定する
  4. 新しいスレッドを起こすには、ライブラリのルーチンを呼び出して何の 関数をスレッドとして実行し、その関数に渡される引数は何かを指定する。 新しいスレッドを開始するには、void を返り値とする関数である f を単独の arg を引数として持つ必要があり、 bb_threads_newthread(f, &arg) という感じで指定することになる。ここで fvoid f(void *arg, size_t dummy) というように宣言して おくべきである。引数が 1 つ以上になる場合は、引数の値で初期化してある 構造体へのポインタを指定してする
  5. 並列コードを実行する。実行に当っては、 bb_threads_lock(n)bb_threads_unlock(n) を慎重に使用する こと。n は整数で、どのロックを使用するかを指定する。この ライブラリでのロックをかけることとロックを解除する操作は、アトミック にバスをロックする命令を使って、ありふれたスピン・ロックで実現している。 この方法では必要以上にメモリ参照の輻輳が生じ、プロセスを公平に実行する ことが保証できない bb_threads についているデモ・プログラムは正しくロックを扱えず、 printf()fnnmainなどの関数内で同時 に実行されてしまう。そのようなわけでこのデモは常に動くとは限らない。 デモを非難するつもりはないが、ロックには細心の注意が必要で あることを強調しておきたい。また、LinuxThreads を使えばわずかではあるが 使いやすくなることも付け加えたい
  6. あるスレッドが返り値を戻す時点で、そのプロセスが実際に 破棄される…しかし、ローカルなスタック・メモリは自動的に解放されない。 正確に言うと Linux はメモリの解放をサポートしておらず、そのメモリ空間は malloc() のメモリの空きリストに自動的に戻されない。つまり、親 プロセスは bb_threads_cleanup(wait(NULL)) を 呼び出して、死んでしまった子プロセスのスペースを再利用のために再宣言し なければいけない

下記の C プログラムはセクション 1.3 で論じたアルゴリズムを元に、2 つの bb_threads を使って円周率の近似値を求めています。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include "bb_threads.h"

volatile double pi = 0.0;
volatile int intervals;
volatile int pids[2];      /* Unix PIDs of threads */

void
do_pi(void *data, size_t len)
{
  register double width, localsum;
  register int i;
  register int iproc = (getpid() != pids[0]);

  /* set width */
  width = 1.0 / intervals;

  /* do the local computations */
  localsum = 0;
  for (i=iproc; i<intervals; i+=2) {
    register double x = (i + 0.5) * width;
    localsum += 4.0 / (1.0 + x * x);
  }
  localsum *= width;

  /* get permission, update pi, and unlock */
  bb_threads_lock(0);
  pi += localsum;
  bb_threads_unlock(0);
}

int
main(int argc, char **argv)
{
  /* get the number of intervals */
  intervals = atoi(argv[1]);

  /* set stack size and create lock... */
  bb_threads_stacksize(65536);
  bb_threads_mutexcreate(0);

  /* make two threads... */
  pids[0] = bb_threads_newthread(do_pi, NULL);
  pids[1] = bb_threads_newthread(do_pi, NULL);

  /* cleanup after two threads (really a barrier sync) */
  bb_threads_cleanup(wait(NULL));
  bb_threads_cleanup(wait(NULL));

  /* print the result */
  printf("Estimation of pi is %f\n", pi);

  /* check-out */
  exit(0);
}

2.4 LinuxThreads

LinuxThreads( http://pauillac.inria.fr/~xleroy/linuxthreads/) は、POSIX 1003.1c のスレッド標準規格に基づいて「すべてを共有する」ことを申し分なくしっかり と実装しています。他の POSIX 準拠のスレッドの移植と違い、LinuxThreads は カーネルのスレッド機能(clone())を使っており、これは bb_threads と同様な手法をとっています。POSIX 互換だとスレッドを 使ったアプリケーションを他のシステムへ移植することが他スレッドと比べて かなり容易になり、チュートリアル類もいろいろ利用できます。つまり、Linux で大規模にスレッド化されたプログラムを開発するなら、間違いなくこの パッケージが適切です。

LinuxThreads ライブラリを使った基本的なプログラムの構築のしかたは、 下記の通りです。

  1. プログラムは、1 つのプロセスとしてスタートさせること
  2. 次のステップは、必要となるあらゆるロックを初期化することである。 bb_threads のロックが番号を指定するのとは異なり、 pthread_mutex_t lock という変数を宣言するのが POSIX 流である。pthread_mutex_init(&lock,val) を使って、必要なそれぞれのスレッドを初期化する
  3. bb_threads と同様に、新しいスレッドを起こすにはライブラリの ルーチンを呼び出し、何の関数をスレッドとして実行し、その関数に渡される 引数は何かを指定する。しかし、POSIX ではユーザが pthread_t という変数を宣言してスレッドを区別する。 スレッドを起こすには、pthread_t threadf() を動かすには、pthread_create(&thread,NULL,f,&arg) を呼び出す
  4. 並列コードを動かすには、 pthread_mutex_lock(&lock)pthread_mutex_unlock(&lock) 適切なところで 慎重に使用すること
  5. pthread_join(thread,&retval) を使って、スレッド の後始末をすること
  6. C のコードをコンパイルする時には、-D_REENTRANT を使うこと

次にあげるのは、円周率の計算を LinuxThreads を使って並列に行う例です。 セクション 1.3 で使ったアルゴリズムが使用されていて、bb_threads と同様に 2 つのスレッドを並列に実行します。


#include <stdio.h>
#include <stdlib.h>
#include "pthread.h"

volatile double pi = 0.0;  /* Approximation to pi (shared) */
pthread_mutex_t pi_lock;   /* Lock for above */
volatile double intervals; /* How many intervals? */

void *
process(void *arg)
{
register double width, localsum;
register int i;
register int iproc = (*((char *) arg) - '0');

/* Set width */
width = 1.0 / intervals;

/* Do the local computations */
localsum = 0;
for (i=iproc; i<intervals; i+=2) {
register double x = (i + 0.5) * width;
localsum += 4.0 / (1.0 + x * x);
}
localsum *= width;

/* Lock pi for update, update it, and unlock */
pthread_mutex_lock(&pi_lock);
pi += localsum;
pthread_mutex_unlock(&pi_lock);

return(NULL);
}

int
main(int argc, char **argv)
{
pthread_t thread0, thread1;
void * retval;

/* Get the number of intervals */
intervals = atoi(argv[1]);

/* Initialize the lock on pi */
pthread_mutex_init(&pi_lock, NULL);

/* Make the two threads */
if (pthread_create(&thread0, NULL, process, "0") ||
  pthread_create(&thread1, NULL, process, "1")) {
fprintf(stderr, "%s: cannot make thread\n", argv[0]);
exit(1);
}

/* Join (collapse) the two threads */
if (pthread_join(thread0, &retval) ||
  pthread_join(thread1, &retval)) {
fprintf(stderr, "%s: thread join failed\n", argv[0]);
exit(1);
}

/* Print the result */
printf("Estimation of pi is %f\n", pi);

/* Check-out */
exit(0);
}

2.5 System V の共有メモリ

System V の IPC(プロセス間通信(Inter-Process Communication))は、 システムコールを数多く提供してます。メッセージ・キューやセマフォ、 共有メモリといったしくみです。もちろん本来このしくみは、複数の プロセスをプロセッサが 1 つのシステムで動かすことを目的としていました。 しかし、SMP Linux で複数のプロセス間での通信にも使えるはずです。たとえ どんなプロセッサであっても。

これらのシステムコールの使い方の説明に入る前に、System V の IPC に含まれて いるセマフォやメッセージ通信のようなシステムコールを使用すべきではないこと を理解しておいてください。どうしてでしょうか? SMP Linux ではこれらの機能は 概して処理が遅く、順次実行されるからです。説明はこれで十分ですね。

共有メモリのセグメントに一様にアクセスするために、プロセスのグループを作成 する基本的な手続きは次の通りです。

  1. プログラムは、1 つのプロセスとしてスタートさせること
  2. 普通、それぞれの並列処理のプログラムを自分の共有メモリのセグメント を使って動かしたいかと思われるので、その場合は shmget() を呼んで必要な大きさのセグメントを新しく作成する。またこのシステム コールは、既存の共有メモリの ID を得るためにも使われる。どちらの場合も 返り値は共有メモリの ID か、エラーを表す -1 である。例えば、b バイトの大きさの共有メモリを作るなら、shmid = shmget(IPC_PRIVATE, b, (IPC_CREAT | 0666)) と呼び出す
  3. 次のステップはプロセスにこの共有メモリをアタッチして、文字通り このプロセスの仮想メモリマップに加える。 プログラマーは shmat() を呼ぶことで、仮想アドレス上のどこに 共有メモリのセグメントがあるのかがわかる。しかしそのアドレスはページ境界に 合っていなければならない(すなわち getpagesize() が返す大きさは ページの大きさの倍数になっていて、通常は 4096 バイトである)。また、以前 そのアドレスをメモリにマップしたものは無効になる。 つまり以前マップしていたもののかわりに、そのアドレスを使用するように システムに指示するのである。どちらの場合も返り値はマップされたセグメント の仮想アドレスへのポインタである。コードは、shmptr = shmat(shmid, 0, 0) となる 注意すべき点は、共有メモリセグメントに置く共有する static 変数はすべて 構造体のメンバーにして宣言することで確保できる、ということだ。 そして shmptr をその構造体のポインタにする。このテクニックを 使うと、xshmptr->x と参照 できる
  4. この共有メモリのセグメントを解放しなければならない時は、この セグメントにアクセスする最後のプロセスが終了するかデタッチする場合である。 shmctl() を呼び出して、初期状態に設定する必要がある。コードは、 shmctl(shmid, IPC_RMID, 0) のようになる
  5. Linux 標準の fork() システムコールを使って、必要なプロセス を立ち上げる…。それぞれのプロセスは共有メモリのセグメントを継承する
  6. プロセスが共有メモリのセグメントを使い終ったら、共有メモリの セグメントを必ずデタッチする。これは shmdt(shmptr) とすれば良い

このようにいくつかのシステムコールが設定の際に必要となりますが、一度 共有メモリのセグメントが確保されれば、どのプロセッサがメモリ上の値を変え ても自動的にすべてのプロセッサから見ることができます。最も大切なことは、 システムコールのオーバーヘッドなしに操作ができることです。

System V の共有メモリのセグメントを使った C のプログラム例です。これ は円周率を計算するもので、セクション 1.3 で使ったのと同じアルゴリズム を利用しています。


#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ipc.h>
#include <sys/shm.h>

volatile struct shared { double pi; int lock; } *shared;

inline extern int xchg(register int reg,
volatile int * volatile obj)
{
  /* Atomic exchange instruction */
__asm__ __volatile__ ("xchgl %1,%0"
                      :"=r" (reg), "=m" (*obj)
                      :"r" (reg), "m" (*obj));
  return(reg);
}

main(int argc, char **argv)
{
  register double width, localsum;
  register int intervals, i;
  register int shmid;
  register int iproc = 0;;

  /* Allocate System V shared memory */
  shmid = shmget(IPC_PRIVATE,
                 sizeof(struct shared),
                 (IPC_CREAT | 0600));
  shared = ((volatile struct shared *) shmat(shmid, 0, 0));
  shmctl(shmid, IPC_RMID, 0);

  /* Initialize... */
  shared->pi = 0.0;
  shared->lock = 0;

  /* Fork a child */
  if (!fork()) ++iproc;

  /* get the number of intervals */
  intervals = atoi(argv[1]);
  width = 1.0 / intervals;

  /* do the local computations */
  localsum = 0;
  for (i=iproc; i<intervals; i+=2) {
    register double x = (i + 0.5) * width;
    localsum += 4.0 / (1.0 + x * x);
  }
  localsum *= width;

  /* Atomic spin lock, add, unlock... */
  while (xchg((iproc + 1), &(shared->lock))) ;      
  shared->pi += localsum;
  shared->lock = 0;

  /* Terminate child (barrier sync) */
  if (iproc == 0) {
    wait(NULL);
    printf("Estimation of pi is %f\n", shared->pi);
  }

  /* Check out */
  return(0);
}

この例では、IA32 のアトミックな交換(exchange)命令を使ってロックを実現 しています。 パフォーマンスと移植性をさらに良くするには、バスをロックする命令を回避 するような同期のしくみに置き換えてください(セクション 2.2 で論じたように)。

コードをデバッグする場合は、現在使われている System V の IPC 機能の状態 をレポートする ipcs コマンドを知っておくと便利です。

2.6 メモリマップ・システムコール

システムコールはファイル入出力をともなうと重くなります。実際に、ユーザ レベルでのバッファを介したファイル入出力ライブラリ(getchar()fwrite() 等)があるのはこのためです。しかし、ユーザレベルでの バッファは、複数のプロセスが同じ書き込み可能なファイルにアクセスした場合 には役立ちません。またこれを管理するオーバーヘッドもばかになりません。 BSD UNIX はこの問題を解決するため、システムコールを 1 つ追加しました。 それはファイルの一部をユーザのメモリ空間にマップして、仮想メモリの ページング機構を利用して更新を行うものです。Sequent がこれと同様のしくみ を何年も前から自社の共有メモリを使った並列処理の機能として提供してきました。 (とても古い)man を見ると、とても否定的なコメントが散見されますが、Linux では少なくとも基本的な機能のいくつかは正しく動作します。このシステムコール が複数のプロセスで利用できる共通のメモリ上のセグメントをマップするのに 使用されるのはまれです。

本質的に Linux の mmap() は、セクション 2.5 の基本的な手続きの 2、3、4 で説明した System V の共有メモリの枠組みで置き換えることができ ます。


shmptr =
    mmap(0,                        /* system assigns address */
         b,                        /* size of shared memory segment */
         (PROT_READ | PROT_WRITE), /* access rights, can be rwx */
         (MAP_ANON | MAP_SHARED),  /* anonymous, shared */
         0,                        /* file descriptor (not used) */
         0);                       /* file offset (not used) */

munmap() は System V の共有メモリの shmdt() システム コールと同じ機能です。


munmap(shmptr, b);

私見ですが、System V の共有メモリのかわりに mmap() を使っても、 それほどメリットはないと思います。


次のページ 前のページ 目次へ