Linux カーネルによるユーザー空間のメモリー・アクセス

Linux メモリーおよびユーザー空間 API の紹介

カーネルとユーザー空間はそれぞれに異なる仮想アドレス空間に存在することから、この 2 つの間でデータを移動する際には特別な考慮が必要な事項があります。この記事では、仮想アドレス空間の背後にある概念、そしてユーザー空間との間でデータを移動するためのカーネル API の詳細を探るとともに、メモリーのマッピングに使用できるその他のマッピング手法についても学びます。

M. Tim Jones, Independent author

M. Tim JonesM. Tim Jones は組み込みソフトウェアのエンジニアであり、『Artificial Intelligence: A Systems Approach』、『GNU/Linux Application Programming』現在、第 2 版です) や『AI Application Programming』(こちらも現在、第 2 版です)、それに『BSD Sockets Programming from a Multilanguage Perspective』などの著者でもあります。技術的な経歴は静止軌道衛星用のカーネル開発から、組み込みシステム・アーキテクチャーやネットワーク・プロトコル開発まで、広範にわたっています。また、コロラド州ロングモン所在のEmulex Corp. の顧問エンジニアでもあります。



2010年 8月 11日

Tim とつながるには

Tim は developerWorks で人気の高いお馴染みの著者の 1 人です。Tim が書いたすべての developerWorks 記事を閲覧してみてください。また、My developerWorks では、Tim のプロフィールを調べることや、彼やその他の著者、そして他の読者とつながることができます。

Linux® 内でアドレス指定できる最小のメモリー単位はバイトかもしれませんが、メモリー抽象化として管理の対象となるのはページです。この記事ではまず Linux 内でのメモリー管理について説明し、その上で、カーネルからユーザー・アドレス空間を操作する方法を詳しく見ていきます。

Linux メモリー

Linux では、ユーザー・メモリーとカーネル・メモリーは互いに独立しており、それぞれに異なるアドレス空間に実装されます。これらのアドレス空間は仮想化されたものです。つまり、(この後すぐに詳しく説明するプロセスにより) 物理メモリーから抽象化されたアドレスが作成されます。アドレス空間は仮想化されていることから、いくつものアドレス空間が存在することが可能です。実際、カーネル自体は 1 つのアドレス空間に置かれ、各プロセスはそれぞれに固有のアドレス空間に置かれます。これらの仮想アドレス空間内のメモリー・アドレスは仮想であるため、独自のアドレス空間を持つ多数のプロセスが、大幅にサイズが下回る 1 つの物理アドレス空間 (マシン内の物理メモリー) を共通して参照することができます。これは便利なだけでなく、セキュアでもあります。なぜなら、各アドレス空間は独立していて、互いに分離されているためです。

その一方、このセキュリティーにはコストが伴います。それぞれのプロセス (およびカーネル) がまったく同じアドレスで異なる物理メモリー領域を参照できることから、メモリーをそのまますぐには共有できないというコストです。幸い、この問題に対してはいくつかのソリューションがあります。例えば、ユーザー・プロセスの間でメモリーを共有するには、POSIX (Portable Operating System Interface for UNIX®) の共有メモリー・メカニズム (shmem) を使用することができます。ただし注意すべき点として、このソリューションでは、プロセスごとの仮想アドレスが違っていても、参照する物理メモリーの領域は同じである可能性があります。

仮想メモリーと物理メモリーのマッピングは、ベースとなるハードウェアに実装されたページ・テーブルで行われます (図 1 を参照)。マッピングを指定するのはハードウェア自体ですが、ページ・テーブルとその構成を管理するのはカーネルです。下図に示されているように、プロセスは大きなアドレス空間を持つことができますが、このアドレス空間は疎な空間になっています。つまり、アドレス空間の小さな領域 (ページ) がページ・テーブルを介して物理メモリーを参照するということです。したがって、プロセスは任意の時点で必要となるページのみを対象に定義した、大規模なアドレス空間を持つことができます。

図 1. 仮想アドレスを物理アドレスにマッピングするページ・テーブル
仮想アドレスを物理アドレスにマッピングするページ・テーブル

このように、各プロセス用のメモリーをそれぞれに疎な空間として定義できるということは、実際の物理メモリーをオーバーコミットできるということです。ページングと呼ばれるプロセス (Linux では、一般にスワップと呼ばれます) により、あまり使用されていないページは、アクセスする必要のある他のページを格納するために、速度の遅いストレージ・デバイス (ディスクなど) に動的に移されます (図 2 を参照)。この振る舞いによってコンピューター内の物理メモリーは、アプリケーションですぐに必要となるページを提供すると同時に、必要性の低いページをディスクに移動して物理メモリーをより効率的に使用できるようにします。ページによってはファイルを参照することができますが、その場合、ページがダーティーであれば (ページ・キャッシュを通じて) データをフラッシュし、ページがクリーンであれば単純にデータを破棄することができます。

図 2. あまり使用されていないページを速度が遅く、コストの低いストレージに移動すると、スワップ処理によって物理メモリー空間をより有効に使用することができます
あまり使用されていないページを速度が遅く、コストの低いストレージに移動すると、スワップ処理によって物理メモリー空間をより有効に使用することができます

MMU のないアーキテクチャー

すべてのプロセッサーに MMU があるわけではありません。そのため、uClinux ディストリビューション (マイクロコントローラー向け Linux) ではオペレーション用に単一のアドレス空間をサポートしています。このアーキテクチャーには、MMU が提供するようなメモリー保護はありませんが、別の種類のプロセッサー上で Linux を実行することができます。uClinux についての詳細は、「参考文献」セクションを参照してください。

ストレージにスワップするページを選択するプロセスは「ページ置換アルゴリズム」と呼ばれ、さまざまなアルゴリズム (LRU (Least Recently Used) など) を使って実装することができます。このプロセスが行われるのは、メモリー・ロケーションがリクエストされ、そのメモリー内にページがない場合 (メモリー管理ユニット (MMU) にマッピングが存在しない場合) です。このイベントは「ページ・フォルト」と呼ばれます。ページ・フォルトはハードウェア (MMU) によって検出され、ページ・フォルト割り込み発生後はファームウェアによって管理されます。このスタックについては、図 3 を参照してください。

Linux には、いくつかの有用な特性を備えた興味深いスワップ機能が実装されています。Linux スワップ・システムでは、複数のスワップ・パーティションおよび優先順位を作成して使用することができるため、パフォーマンス特性がそれぞれに異なるストレージ・デバイスに対してスワップの階層を構成することが可能です (例えば、半導体ディスク (SSD) に第 1 レベルのスワップ空間を設定し、それよりも速度の遅いストレージ・デバイスに、サイズを大きくした第 2 レベルのスワップ空間を設定するなど)。SSD スワップに高い優先順位を付けることで、SSD のスワップ空間が使い果たされるまで使用された後に、それよりも低い優先順位の (速度の遅い) スワップ・パーティションにページが書き込まれるようにすることができます。

図 3. 仮想アドレスと物理アドレスのマッピングを構成するアドレス空間および要素
仮想アドレスと物理アドレスのマッピング

すべてのページがスワップ処理の候補となるわけではありません。割り込みに応答するカーネル・コードや、ページ・テーブルとスワップ・ロジックを管理するコードを考えてみてください。これらのコードは、決してスワップ・アウトしてはならないページであることは明らかなので、固定されているか、あるいは永続的にメモリー内に置かれます。カーネル・ページはスワップ処理の候補ではありませんが、ユーザー空間ページはスワップ処理の候補です。それでも、ページをロックするための mlock (または mlockall) 関数を使用すれば、ユーザー空間ページを固定することもできます。これが、ユーザー空間メモリー・アクセス関数の背後にある目的です。ユーザーから渡されたアドレスが有効であり、アクセス可能であるという前提に立ったカーネルは、最終的にカーネル・パニックが発生することになるでしょう (その原因としては、例えばユーザー・ページがスワップ・アウトされたことによる、カーネル内でのページ・フォルトなどが挙げられます)。次に紹介する API では、これらの特別なケースが適切に処理されるようにします。


カーネル API

ここからは、ユーザー・メモリーを操作するためのカーネル API の詳細を探ります。このセクションで取り上げるのはカーネルおよびユーザー空間インターフェースですが、他のメモリー API についても次のセクションでいくつか探ります。表 1 に、このセクションで説明するユーザー空間メモリー・アクセス関数を記載します。

表 1. ユーザー空間メモリー・アクセス API
関数説明
access_okユーザー空間メモリー・ポインターの有効性をチェック
get_userユーザー空間から単純な変数を取得
put_userユーザー空間に単純な変数を追加
clear_userユーザー空間のブロックをクリア (ゼロに設定する)
copy_to_userカーネルのデータ・ブロックをユーザー空間にコピー
copy_from_userユーザー空間のデータ・ブロックをカーネルにコピー
strnlen_userユーザー空間のストリング・バッファーのサイズを取得
strncpy_from_userストリングをユーザー空間からカーネルにコピー

ご想像の通り、この表に記載した関数の実装はアーキテクチャーによって異なります。x86 アーキテクチャーの場合、これらの関数とシンボルが定義されているのは ./linux/arch/x86/include/asm/uaccess.h で、ソースは ./linux/arch/x86/lib/usercopy_32.c および usercopy_64.c にあります。

図 4 に示すように、データ移動関数の役割は、コピーに関連付けられたタイプ (単純タイプと集約タイプ) に関係します。

図 4. ユーザー空間メモリー・アクセス API を使用したデータの移動
ユーザー空間メモリー・アクセス API を使用したデータの移動

access_ok 関数

access_ok 関数は、アクセス対象とするユーザー空間内のポインターが指すアドレスが有効であるかどうかをチェックするために使用します。呼び出し側が指定するのは、データ・ブロックの開始アドレスを参照するポインター、ブロックのサイズ、そしてアクセスのタイプ (その領域が読み取り用であるか、書き込み用であるか) です。関数のプロトタイプは以下のように定義されています。

access_ok( type, addr, size );

type 引数には、VERIFY_READ または VERIFY_WRITE を指定することができます。VERIFY_WRITE シンボリックは、メモリー領域が読み取り可能であるとともに書き込み可能であるかどうかも判別します。この関数は、領域がアクセス可能だと思われる場合 (アクセスが –EFAULT という結果になる可能性もあります) にはゼロ以外の値を返します。この関数は単に、アドレスがカーネル内ではなく、ユーザー空間内にある可能性をチェックするだけです。

get_user 関数

ユーザー空間から単純な変数を読み取るには、get_user 関数を使用します。charint などの単純な型にはこの関数を使用しますが、構造体のような大きなデータ型には copy_from_user 関数を使用する必要があります。プロトタイプでは、(データを保管する) 変数と、読み取り操作対象のユーザー空間内のアドレスを引数に取ります。

get_user( x, ptr );

get_user 関数は 2 つの内部関数のうちのいずれかにマッピングされます。内部では、この関数はアクセス対象の変数のサイズを (結果を保管するために指定された変数に基づき) 判断し、__get_user_x で内部呼び出しを行います。操作に成功すると、関数はゼロを返します。一般に、get_user 関数と put_user 関数は、ブロック・コピーを行う同等の関数よりも高速なので、サイズの小さいデータ型を移動する場合には、これらの関数を使用してください。

put_user 関数

put_user 関数は、カーネルからユーザー空間に単純な変数を書き込む場合に使用します。get_user と同じく、(書き込む値が含まれる) 変数と、書き込み対象のユーザー空間アドレスを引数に取ります。

put_user( x, ptr );

get_user と同様に、put_user 関数は内部で put_user_x 関数にマッピングされており、成功すると 0 を、エラーが発生すると -EFAULTを返します。

clear_user 関数

clear_user は、ユーザー空間のメモリー・ブロックにゼロを書き込むための関数です。この関数はユーザー空間内のポインターと、ゼロにするサイズ (バイト単位で定義) を引数に取ります。

clear_user( ptr, n );

内部では、clear_user 関数は最初にユーザー空間内のポインターが指すアドレスが書き込み可能であるかどうかをチェックした上で (access_ok を使用)、インライン・アセンブリーとしてコーディングされている内部関数を呼び出してクリア操作を実行します。この関数は、繰り返し回数のプレフィックス付きストリング型命令を使用した非常に厳密なループとして最適化されています。完全にクリアできなかった場合、関数はクリアできなかったバイト数を返し、操作が成功した場合にはゼロを返します。

copy_to_user 関数

copy_to_user 関数は、カーネルのデータ・ブロックをユーザー空間にコピーします。この関数が引数として取るのは、ユーザー空間バッファーへのポインター、カーネル・バッファーへのポインター、そしてバイト単位で定義された長さです。関数は操作に成功するとゼロを返します。ゼロ以外の値を返す場合、それは転送されなかったバイト数を示します。

copy_to_user( to, from, n );

ユーザー・バッファーが書き込み可能であるかどうかをチェックした後 (access_ok を使用)、内部関数 __copy_to_user を呼び出し、さらにその内部関数から __copy_from_user_inatomic (./linux/arch/x86/include/asm/uaccess_XX.h に定義された関数。ここで、XX はアーキテクチャーによって 32 または 64 になります) を呼び出します。するとこの関数が、実行するコピーが 1 バイト、2 バイト、または 4 バイトのどれであるかを判断した後、最終的に __copy_to_user_ll を呼び出します。この関数により、実際の作業が行われます。WP ビットを監視モードから使用できない、i486 よりも前のハードウェアの場合、ハードウェアが壊れていると、ページ・テーブルが常に変更される可能性があるため、必要なページをメモリーに固定して、アドレスが指定されるまでの間にスワップ・アウトされることがないようにしなければなりません。i486以降では、このプロセスは最適化されたコピーにすぎません。

copy_from_user 関数

copy_from_user 関数は、ユーザー空間のデータ・ブロックをカーネル・バッファーにコピーします。この関数が引数として取るのは、宛先バッファー (カーネル空間内)、ソース・バッファー (ユーザー空間内)、そしてバイト単位で定義された長さです。copy_to_user の場合と同じく、この関数も操作が成功するとゼロを返すか、コピーできなかった場合はバイト数を示すゼロ以外の値を返します。

copy_from_user( to, from, n );

この関数はまず、ユーザー空間内のソース・バッファーから読み取り可能であるかどうかをチェックし (access_ok を使用)、それから __copy_from_user、そして最後に __copy_from_user_ll を呼び出します。ここから先はアーキテクチャーによって異なりますが、呼び出しによってユーザー・バッファーからカーネル・バッファーにデータ・ブロックがコピーされ、使用不可能なバイトはゼロにされます。この最適化されたアセンブリー関数には、管理機能が組み込まれています。

strnlen_user 関数

strnlen_user 関数は strnlen と同じように使用されますが、この関数の場合、バッファーがユーザー空間内で使用可能であることを前提とします。strnlen_user 関数が取る引数は、ユーザー空間のバッファー・アドレスとチェック対象である最大長の 2 つです。

strnlen_user( src, n );

strnlen_user 関数は最初に access_ok を呼び出してユーザー・バッファーが読み取り可能であるかどうかを調べます。アクセス可能である場合、strlen 関数が呼び出され、max length 引数は無視されます。

strncpy_from_user 関数

strncpy_from_user 関数は、ユーザー空間のソース・アドレスと最大長に基づいて、ユーザー空間からカーネル・バッファーにストリングをコピーします。

strncpy_from_user( dest, src, n );

ユーザー空間からのコピー操作として、この関数は最初に access_ok を使用し、バッファーが読み取り可能であるかどうかをチェックします。この関数は copy_from_user と同じように、最適化されたアセンブリー関数として実装されています (./linux/arch/x86/lib/usercopy_XX.c 内)。


その他のメモリー・マッピング・スキーム

前のセクションでは、カーネルとユーザー空間の間で (カーネルによって操作を開始して) データを移動する手法を検討しました。Linux には他にもカーネル内およびユーザー空間内でデータを移動するために使用できる手法がいくつかあります。これらの手法は必ずしもユーザー空間メモリー・アクセス関数による機能とまったく同じように機能するわけではありませんが、アドレス空間の間でメモリーをマッピングできるという点では似ています。

ユーザー空間での注意点として、ユーザー・プロセスはそれぞれに異なるアドレス空間にあることから、アドレス空間の間でのデータ移動は、何らかの形のプロセス間通信メカニズムによって行われなければなりません。Linux にはさまざまなスキーム (メッセージ・キューなど) がありますが、最も注目に値すべきなのは POSIX 共有メモリー (shmem) です。このメカニズムでは、プロセスがメモリー領域を作成し、その領域を 1 つ以上のプロセスと共有することができます。各プロセスは、それぞれのアドレス空間にある異なるアドレスにメモリー領域をマッピングできることに注意してください。したがって、相対オフセット・アドレッシングが必要となります。

一方、mmap 関数を使用することで、ユーザー空間のアプリケーションは仮想アドレス空間にマッピングを作成することができます。この機能は、(パフォーマンスを向上させるために) 特定のクラスのデバイス・ドライバーによってよく使われ、物理デバイス・メモリーからプロセスの仮想アドレス空間へのマッピングが行われます。mmap 関数は remap_pfn_range カーネル関数によってドライバー内に実装され、デバイス・メモリーからユーザーのアドレス空間への線形マッピングが行われます。


さらに詳しく調べてください

この記事では、(ページングの背景知識として) Linux 内でのメモリー管理というトピックを取り上げた後、これらの概念を使用したユーザー空間メモリー・アクセス関数について説明しました。ユーザー空間とカーネルとの間でデータを移動するのは意外と面倒な作業ですが、Linux には、さまざまなプラットフォームでこの複雑な作業を行うための単純な API 一式が組み込まれています。

参考文献

学ぶために

  • Red Hat の Linux System Administration Primer で、仮想メモリーについてわかりやすく要約しています。
  • All about Linux swap space」では、スワップの目的、スワップの場所、そしてスワップ空間を管理するための各種コマンドについて説明しています。
  • キャッシングのパフォーマンスを改善するために考案されたのが、圧縮キャッシュ・スキームです。このスキーマで言うスワップ・ディスクとは、実際には高速 RAM ディスクのことで、ストレージの効率性を高めるためにページは圧縮してから格納されます。
  • Linux メモリー管理 (およびデバイス・ドライバーに関するすべて) の最も優れた情報源として挙げられるのは、デバイス・ドライバーのバイブルとも言える『Linuxデバイスドライバ 第3版』です。
  • カーネル・ページとユーザー空間ページの違いの 1 つは、カーネル・ページはメモリー内に存続する一方、ユーザー空間ページはストレージ・デバイスにスワップ・アウトできるという点です。プロセスの仮想アドレス空間の一部あるいは全体をメモリーにロックするには、mlock() および mlockall() システム・コールを使用します。
  • すべてのプロセッサーに MMU があるわけではありません。そのため、Linux では MMU を使用しないアーキテクチャーを uClinux ディストリビューションでサポートしています。uClinux は、マイクロコントローラーなどの MMU を使用しないアーキテクチャーを焦点とするプロジェクトです。
  • ウィキペディアに、仮想メモリーページング方式ページ・テーブル、およびページ置換アルゴリズムをはじめ、メモリー管理に関連するトピックの参考資料が用意されています。
  • developerWorks Linux ゾーンで、Linux 開発者および管理者向けのハウツー記事とチュートリアル、そしてダウンロード、ディスカッション、フォーラムなど、豊富に揃った資料を探してください。
  • さまざまな IBM 製品および IT 業界についての話題に絞った developerWorks の Technical events and webcasts で時代の流れをキャッチしてください。
  • 無料の developerWorks Live! briefing に参加して、IBM 製品およびツール、そして IT 業界の傾向を素早く学んでください。
  • developerWorks の on-demand demos で、初心者向けの製品のインストールおよびセットアップから熟練開発者向けの高度な機能に至るまで、さまざまに揃ったデモを見てください。
  • Twitter で developerWorks をフォローするか、developerWorks で Linux に関するツイートのフィードに登録してください。

製品や技術を入手するために

  • ご自分に最適な方法で IBM 製品を評価してください。評価の方法としては、製品の試用版をダウンロードすることも、オンラインで製品を試してみることも、クラウド環境で製品を使用することもできます。また、SOA Sandbox では、数時間でサービス指向アーキテクチャーの実装方法を効率的に学ぶことができます。

議論するために

  • My developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者が主導するブログ、フォーラム、グループ、ウィキを調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=521170
ArticleTitle=Linux カーネルによるユーザー空間のメモリー・アクセス
publish-date=08112010