レベル: 中級 M. Tim Jones, Consultant Engineer, Emulex
2007年 3月 21日
Linux
®
システム・コールは私たちが毎日使っているものですが、システム・コールはどのようにしてユーザー空間からカーネルに対して行われるかをご存知ですか。そこで、この記事では
Linux システム・コール・インターフェース (SCI) の詳細を説明します。新しいシステム・コールを追加する方法
(そしてその代わりとなる手段)、そして SCI 関連のユーティリティーについて学んでください。
システム・コール
は、ユーザー空間のアプリケーションとカーネルが提供するサービスとの間のインターフェースです。サービスはカーネルに提供されるため、直接呼び出すことはできません。その代わりとして必要となるのがユーザー空間とカーネルの境界をまたぐプロセスです。このプロセスはアーキテクチャーそれぞれによって異なるので、この記事ではもっとも一般的なアーキテクチャーとして
i386 に対象を絞ることにします。
この記事では、Linux SCI の詳細を掘り下げ、システム・コールを 2.6.20
カーネルに追加する方法を説明し、そして追加したシステム・コールをユーザー空間から使用してみます。さらに、システム・コールの開発に役立つ関数とシステム・コールの代わりとなる手段について取り上げ、最後にシステム・コール関連の補助機構
(特定プロセスでのシステム・コールの使用状況を追跡するための機構など) を紹介します。
SCI
Linux
でのシステム・コールの実装はアーキテクチャーによってさまざまですが、特定のアーキテクチャー内でも異なることがあります。例えば、古い
x86 プロセッサーでは割り込み機構を使用してユーザー空間からカーネル空間に移植していましたが、新しい
IA-32 プロセッサーにはこの移植を最適化する命令が用意されています (sysenter 命令と
sysexit
命令を使用)。そのようなオプションの豊富さと最終結果の複雑さから、この記事ではインターフェースの詳細は表面的なレベルで説明するだけにとどめます。複雑な詳細については、この記事の最後にある
「参考文献」
を参照してください。
SCI
を変更するのにその内部構造を完全に理解する必要はないので、ここでは単純なバージョンのシステム・コール・プロセスを取り上げることにします
(図 1
を参照)。このプロセスではまず、それぞれのシステム・コールが単一のエントリー・ポイントからカーネルに多重化されます。呼び出さなければならない特定のシステム・コールを識別するために使用されるのは、eax
レジスターです。このレジスターは、(ユーザー空間のアプリケーションからのシステム・コールごとに) C
ライブラリーに指定されます。C
ライブラリーがシステム・コールのインデックスと引数をロードすると、ソフトウェア割り込み (割り込み 0x80)
が呼び出され、その結果、(割り込みハンドラーによって) system_call 関数が実行されます。この
system_call は、eax
の内容で識別されるすべてのシステム・コールを処理する関数です。簡単なテストがいくつか行われた後、system_call_table
と eax
に含まれるインデックスを使用して実際のシステム・コールが呼び出されます。システム・コールから戻った時点で行き着くのは結局
syscall_exit で、resume_userspace 呼び出しによってユーザー空間に戻ります。C
ライブラリーで実行が再開され、それからユーザー・アプリケーションに実行が戻されます。
図 1. 割り込みメソッドを使用したシステム・コールの簡易フロー
SCI の中核となるのは、システム・コールの逆多重化テーブルです。このテーブル (図 2 を参照) は eax
に指定されたインデックスを使用して、テーブル (sys_call_table)
から呼び出すシステム・コールを識別します。図 2
には、このテーブルのコンテンツの例とそれぞれのエンティティーがある場所も示しています
(逆多重化についての詳細は、囲み記事
「システム・コールの逆多重化」
を参照してください)。
図 2. システム・コール・テーブルとさまざまなリンク
Linux システム・コールの追加方法
 |
システム・コールの逆多重化
一部のシステム・コールは、カーネルによってさらに逆多重化されます。例えば、バークレー・ソフトウェア・ディストリビューション
(BSD) ソケット呼び出し (socket、bind、connect など)
は単一のシステム・コールのインデックス (__NR_socketcall)
に関連付けられますが、カーネルでは別の引数を使って適切な呼び出しに逆多重化されます。./linux/net/socket.c
の関数 sys_socketcall を参照してください。
|
|
システム・コールを新しく追加する方法はほとんど型にはまったものですが、注意しなければならない点がいくつかあります。このセクションではいくつかのシステム・コールの作成手順をとおして、その実装方法とユーザー空間のアプリケーションでの使用方法を説明します。
新しいシステム・コールをカーネルに追加するには、次の 3 つの基本ステップを実行します。
- 新規関数を追加する。
- ヘッダー・ファイルを更新する。
- システム・コール・テーブルを更新して新規関数を反映させる。
注:
このプロセスではユーザー空間側に必要なことを省いていますが、それについては後で対応します。
たいていの場合、新しい関数には新しいファイルを作成しますが、ここでは説明を簡潔にするために新規関数を既存のソース・ファイルに追加しています。リスト
1 に記載した最初の 2
つの関数は、システム・コールの単純な例です。ポインター引数を使用する多少複雑な関数は、リスト 2 に記載します。
リスト 1. システム・コール・サンプル用の単純なカーネル関数
asmlinkage long sys_getjiffies( void )
{
return (long)get_jiffies_64();
}
asmlinkage long sys_diffjiffies( long ujiffies )
{
return (long)get_jiffies_64() - ujiffies;
}
|
リスト 1 では、jiffies 監視のための 2 つの関数が用意されています (jiffies
についての詳細は、囲み記事
「カーネルの jiffies」
を参照してください)。最初の関数は現行の jiffies を返し、2 番目の関数は現行の jiffies
と呼び出し側が渡す値の差を返します。ここで注目してもらいたいのは、asmlinkage
修飾子の使い方です。このマクロ (linux/include/asm-i386/linkage.h に定義)
によって、コンパイラーにすべての関数の引数をスタックで渡すよう指示しています。
リスト 2. システム・コール・サンプル用の最後のカーネル関数
asmlinkage long sys_pdiffjiffies( long ujiffies,
long __user *presult )
{
long cur_jiffies = (long)get_jiffies_64();
long result;
int err = 0;
if (presult) {
result = cur_jiffies - ujiffies;
err = put_user( result, presult );
}
return err ? -EFAULT : 0;
}
|
 |
カーネルの jiffies
Linux カーネルが保持している jiffies
というグローバル変数は、マシン起動後のタイマー・ティック数を表します。この変数はゼロに初期化され、タイマー割り込みのたびにインクリメントされます。jiffies
を読み出すには、get_jiffies_64
関数を使用します。その上で、jiffies_to_msecs または jiffies_to_usecs
を使うと、jiffies の値をそれぞれミリ秒 (msec) またはマイクロ秒 (usec)
に変換できます。jiffies
のグローバル関数および関連する関数は、./linux/include/linux/jiffies.h
にあります。
|
|
リスト 2 に、3 つ目の関数を記載します。この関数は、2 つの引数、つまり long 型の引数と __user
として定義される long 型へのポインターを取ります。__user マクロは (noderef を使用して)
コンパイラーにポインターを逆参照しないように指示しているにすぎません
(現行のアドレス空間では意味がないため)。この関数は 2 つの jiffies
値の差を計算し、ユーザー空間ポインターを使用して結果をユーザーに提供します。この結果は put_user
関数によって、ユーザー空間の presult が指定する場所に配置されます。
この操作中にエラーが発生するとエラーが返されるので、ユーザー空間の呼び出し側にも同じく通知されることになります。
ステップ 2
では、ヘッダー・ファイルを更新してシステム・コール・テーブル内に新しい関数のための場所を割り当てることになっています。そこで、ヘッダー・ファイル
linux/include/asm/unistd.h を新規システム・コールの番号で更新しました。リスト 3
に、この更新を太字で示します。
リスト 3. unistd.h の更新による新規システム・コールの場所の割り当て
#define __NR_getcpu 318
#define __NR_epoll_pwait 319
#define __NR_getjiffies 320
#define __NR_diffjiffies 321
#define __NR_pdiffjiffies 322
#define NR_syscalls 323
|
これで、カーネル・システム・コール、そしてそれぞれを表す番号が用意できました。後は、これらの番号
(テーブル・インデックス) と関数自体を対応させるだけです。それが、ステップ 3
のシステム・コール・テーブルを更新するという手順です。そのためリスト 4
のとおり、linux/arch/i386/kernel/syscall_table.S
ファイルに新規関数を追加して更新することで、これらの関数にリスト 3 の個々のインデックスを対応させました。
リスト 4. 新規関数によるシステム・コール・テーブルの更新
.long sys_getcpu
.long sys_epoll_pwait
.long sys_getjiffies /* 320 */
.long sys_diffjiffies
.long sys_pdiffjiffies
|
注:
このテーブルのサイズはシンボリック定数 NR_syscalls で定義されます。
カーネルはこの時点で更新されます。そこで必要となるのは、ユーザー空間アプリケーションをテストする前に、カーネルを再コンパイルして新規イメージをブートに使用できるようにすることです。
ユーザー・メモリーの読み取りと書き込み
Linux
カーネルには、システム・コールの引数をユーザー空間に対して出し入れするために使用できる複数の関数が用意されています。オプションには、基本型に対応した単純な関数
(get_user や put_user など) や、構造体や配列などのデータ・ブロックを移動させるための関数
(copy_from_user と copy_to_user)
が含まれます。ヌル終了文字列を移動させる固有の呼び出し (strncpy_from_user と
strlen_from_user) もあります。また、access_ok
を呼び出すことによってユーザー空間ポインターが有効かどうかをテストすることもできます。これらの関数は、linux/include/asm/uaccess.h
に定義されています。
access_ok
マクロは、特定の操作に対するユーザー空間ポインターが有効であることを検証するために使用します。以下に示すように、この関数で使用するのはアクセスのタイプ
(VERIFY_READ または
VERIFY_WRITE)、ユーザー空間のメモリー・ブロックへのポインター、そしてブロックのサイズ
(バイト単位) です。検証に成功すると、関数はゼロを返します。
int access_ok( type, address, size );
|
get_user および put_user では、単純な型 (int や long など)
をカーネル空間とユーザー空間の間で簡単に移動させることができます。これらのマクロが使用するのは値と変数へのポインターです。get_user
関数の場合は、ユーザー空間アドレス (ptr) が指定する値を、指定されたカーネル変数 (var)
に移動させます。一方 put_user 関数は、カーネル変数 (var)
が指定する値を、指定されたユーザー空間アドレス (ptr)
に移動させます。いずれの関数も成功するとゼロを返します。
int get_user( var, ptr );
int put_user( var, ptr );
|
構造体や配列などのサイズの大きいオブジェクトを移動させるには、copy_from_user 関数と
copy_to_user 関数を使用します。この 2
つは、データ・ブロック全体をユーザー空間とカーネル空間の間で移動させる関数です。copy_from_user
関数はデータ・ブロックをユーザー空間からカーネル空間に移動させ、copy_to_user はその逆を行います。
unsigned long copy_from_user( void *to, const void __user *from, unsigned long n );
unsigned long copy_to_user( void *to, const void __user *from, unsigned long n );
|
また strncpy_from_user
関数を使用すると、ヌル終了文字列をユーザー空間からカーネルにコピーできます。ユーザー空間ストリングのサイズを取得するには、以下のように、この関数を呼び出す前に
strlen_user マクロを呼び出します。
long strncpy_from_user( char *dst, const char __user *src, long count );
strlen_user( str );
|
カーネル空間とユーザー空間の間でのメモリーの基本的な移動は、上記の関数によって行いますが、関数は他にもあります
(実行するチェックの量を減らす関数など)。これらの関数は、uaccess.h に含まれています。
システム・コールの使用方法
カーネルはいくつかの新規システム・コールで更新されているので、今度はユーザー空間アプリケーションからこれらのシステム・コールを使用するのに必要な手順を説明します。新しいカーネル・システム・コールは
2 通りの方法で使用できます。一方は手軽な方法で
(実動コードではおそらく使用しないようなもの)、もう一方は多少の作業が必要となる従来からの方法です。
手軽なほうの方法では、インデックスで識別した新規関数を syscall 関数で呼び出します。syscall
関数を使うと、対応するインデックスと一連の引数を指定してシステム・コールを呼び出すことができます。例えばリスト
5 の簡単なアプリケーションでは、sys_getjiffies をそのインデックスを使って呼び出しています。
リスト 5. syscall によるシステム・コールの呼び出し
#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
int main()
{
long jiffies;
jiffies = syscall( __NR_getjiffies );
printf( "Current jiffies is %lx\n", jiffies );
return 0;
}
|
ご覧のように、syscall
関数に組み込まれる最初の引数は、使用するシステム・コール・テーブルのインデックスです。他にも渡す引数があったとしたら、それらの引数はこのインデックスの後に指定されることになります。ほとんどのシステム・コールには、__NR_
インデックスへのマッピングを指定する SYS_ シンボリック定数が組み込まれます。以下は、syscall
でインデックス __NR_getpid を呼び出す例です。
syscall
はアーキテクチャーに固有の関数ですが、カーネルに制御を移すための機構を使用します。引数は、/usr/include/bits/syscall.h
に指定された __NR インデックスから SYS_ シンボルへのマッピング (libc のビルド時に定義)
に基づきます。このファイルは直接参照できません。代わりに /usr/include/sys/syscall.h
を使用してください。
従来の方法では、カーネル内の関数とシステム・コールのインデックスが一致する関数コールを作成すること
(正しいカーネル・サービスを呼び出せるようにするため)、そして引数が一致することが必要となります。Linux
には、この必要を満たすための一連のマクロ、_syscallN が用意されています。_syscallN
マクロが定義されている場所は /usr/include/linux/unistd.h
で、そのフォーマットは以下のとおりです。
_syscall0( ret-type, func-name )
_syscall1( ret-type, func-name, arg1-type, arg1-name )
_syscall2( ret-type, func-name, arg1-type, arg1-name, arg2-type, arg2-name )
|
 |
ユーザー空間と __NR 定数
リスト 6 では、__NR
シンボリック定数を指定していることに注目してください。これらの定数は、/usr/include/asm/unistd.h
にあります (標準システム・コールの場合)。
|
|
_syscall マクロは、最大 6 つの引数までが定義されています (ただし、以下では 3
つしか示していません)。
それではここで、_syscall
マクロを使用して新しいシステム・コールをユーザー空間に見えるようにする方法を紹介しましょう。リスト 6
に示すのは、それぞれのシステム・コールを _syscall マクロの定義に従って使用するアプリケーションです。
リスト 6. _syscall マクロによるユーザー空間のアプリケーションのデプロイメント
#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
#define __NR_getjiffies 320
#define __NR_diffjiffies 321
#define __NR_pdiffjiffies 322
_syscall0( long, getjiffies );
_syscall1( long, diffjiffies, long, ujiffies );
_syscall2( long, pdiffjiffies, long, ujiffies, long*, presult );
int main()
{
long jifs, result;
int err;
jifs = getjiffies();
printf( "difference is %lx\n", diffjiffies(jifs) );
err = pdiffjiffies( jifs, &result );
if (!err) {
printf( "difference is %lx\n", result );
} else {
printf( "error\n" );
}
return 0;
}
|
このアプリケーションでは、__NR インデックスが必要になります。これは、_syscall マクロが
func-name を使用して __NR インデックスを構成しているためです (getjiffies ->
__NR_getjiffies)。ただし結果的には、他のシステム・コールとまったく同じように、カーネル関数をそれぞれの名前で呼び出せるようになっています。
ユーザーとカーネルを相互作用させるための代わりの手段
システム・コールはカーネルのサービスを要求するには効率的な方法ですが、その最大の問題はインターフェースが標準化されていることです。標準化されたインターフェースでは新しいシステム・コールをカーネルに追加しにくいため、別の追加手段が採られがちです。ただし、独自のシステム・コールを
Linux
のパブリック・カーネルの主流に加えるという意図がないのであれば、システム・コールはカーネル・サービスをユーザー空間で使用できるようにする便利で効率的な方法です。
新しいサービスをユーザー空間に対して見えるようにするもう 1 つの方法は、/proc
ファイル・システムを使うことです。/proc
ファイル・システムは仮想ファイル・システムなので、ディレクトリーとファイルをユーザーに公開し、ファイル・システムのインターフェース
(read、write など) を使ってカーネル内のインターフェースを新しいサービスに提供することができます。
strace によるシステム・コールの追跡方法
Linux カーネルには、プロセスが呼び出したシステム・コール (ならびに、プロセスが受信した信号)
を追跡するのに便利な方法があります。これは strace
というユーティリティーで、追跡対象のアプリケーションを引数として指定してコマンド・ラインから実行します。例えば、date
コマンドのコンテキストでどのシステム・コールが呼び出されたかを調べるには、以下のコマンドを入力します。
上記のコマンドを実行すると、date
コマンド呼び出しのコンテキストで実行された各種システム・コールを示すかなり大きなダンプになります。このダンプで確認できるのは、共有ライブラリーのロード、メモリーのマッピング、そして追跡の終了時に標準出力される日付情報です。
...
write(1, "Fri Feb 9 23:06:41 MST 2007\n", 29Fri Feb 9 23:06:41 MST 2007) = 29
munmap(0xb747a000, 4096) = 0
exit_group(0) = ?
$
|
現行のシステム・コール要求に、do_syscall_trace 関数を呼び出す syscall_trace
という特殊フィールド・セットある場合、追跡はカーネル内で行われます。./linux/arch/i386/kernel/entry.S
でも、追跡の呼び出しがシステム・コール要求の一部になっていることがわかります
(syscall_trace_entry を参照)。
さらに詳しく調べてください
システム・コールは、カーネル空間内のサービスを要求するためにユーザー空間とカーネル空間を結ぶ効率的なインターフェースですが、同時にシステム・コールは厳重にコントロールされています。そのため、ユーザー空間とカーネル空間のやりとりをする方法としては、新しい
/proc
ファイル・システム・エントリーを単純に追加するだけのほうがはるかに簡単です。ただし処理速度が重要な場合には、システム・コールがアプリケーションのパフォーマンスを最大限引き出す理想的な方法となります。
「参考文献」
を参照して、SCI をさらに詳しく調べてみてください。
参考文献 学ぶために
製品や技術を入手するために
-
SEK for Linux を注文してください。
この 2 枚組 DVD セットには、Linux 対応の DB2
®
、Lotus
®
、Rational
®
、Tivoli
®
、そして WebSphere
®
の最新 IBM トライアル・ソフトウェアが収録されています。
-
developerWorks から直接ダウンロードできる
IBM トライアル・ソフトウェア
を使用して、Linux で次の開発プロジェクトを構築してください。
議論するために
著者について  | 
|  | M. Tim Jones は組み込みソフトウェアのエンジニアであり、『GNU/Linux Application Programming』や『AI Application Programming』(現在、第 2 版)、それに『BSD Sockets Programming from a Multilanguage Perspective』などの著者でもあります。技術的な経歴は静止軌道衛星用のカーネル開発から、組み込みシステム・アーキテクチャーやネットワーク・プロトコル開発まで、広範にわたっています。また、コロラド州ロングモン所在のEmulex Corp. の顧問エンジニアでもあります。 |
記事の評価
|