レベル: 初級 Avinesh Kumar, System Software Engineer,
IBM
2008年 09月 30日 Linux® のカーネル 2.6.25 では、プロセスをスリープさせるための TASK_KILLABLE という新しいプロセスの状態が導入されています。TASK_KILLABLE は、効率的ではあっても kill できなくなる恐れのある TASK_UNINTERRUPTIBLE や、頻繁に起こされる代わりに安全な TASK_INTERRUPTIBLE に代わるものです。TASK_KILLABLE は、2002年に提起された問題、つまり OpenAFS ファイルシステム・ドライバーがすべてのシグナルをブロックした後、割り込み可能な状態でイベントを待ち続けるという問題に対応するために作られたものです。この新しいスリープ状態は TASK_UNINTERRUPTIBLE に重大なシグナルに応答できる機能を追加したものです。この記事ではこの領域に焦点を当て、2.6.26 や以前の2.6.18 の例を使いながら、TASK_KILLABLE に関連した Linux カーネルの変更と、こうした変更の結果による新しい API について説明します。
プロセスはファイルと同様、すべての UNIX® オペレーティング・システムにとって基本となるものです。プロセスは、実行可能ファイルの命令を実行する生きた実体で、命令を実行する以外に、開いたファイルやプロセッサー関連の情報、アドレス空間、そのプログラムに関連するデータなどの管理も行う場合があります。Linux カーネルはプロセスに関する完全な情報を、struct task_struct として定義されるプロセス記述子の中に保持します。Linux カーネルのソース・ファイル include/linux/sched.h を見ると、struct task_struct のさまざまなフィールドを調べることができます。
プロセスの状態に関して
プロセスはその存続期間に相互排他的な一連の状態の間を遷移します。カーネルはプロセスの状態の情報を struct task_struct の state フィールドに保持します。図 1 は、プロセスの状態間の遷移を示しています。
図 1. プロセスの状態遷移
では、さまざまなプロセスの状態を調べてみましょう。
TASK_RUNNING: プロセスは CPU 上で実行されているか、あるいはラン・キューの中でスケジューリングされるのを待っています。
TASK_INTERRUPTIBLE: プロセスは何らかのイベントの発生を待ってスリープしています。さまざまなシグナルによって、このプロセスに割り込みをかけることができます。シグナルを受信するか、あるいは明示的なウェイクアップ・コールによって起こされると、プロセスは TASK_RUNNING に遷移します。
TASK_UNINTERRUPTIBLE: このプロセスの状態は TASK_INTERRUPTIBLE と似ていますが、このプロセスの状態ではシグナルを処理しない点が異なります。この状態にあるときは何か重要なタスクを完了しようとしている最中の可能性があるため、割り込みをかけることすら望ましくないかもしれません。待ち望んでいたイベントが発生すると、プロセスは明示的なウェイクアップ・コールによって起こされます。
TASK_STOPPED: プロセスの実行は停止されるため、実行中ではなく、また実行することができません。SIGSTOP や SIGTSTP などのシグナルを受信すると、プロセスはこの状態になります。SIGCONT シグナルを受信すると、そのプロセスは実行可能になります。
TASK_TRACED: プロセスがデバッガーなど別のプロセスによって監視されていると、この状態になります。
EXIT_ZOMBIE: プロセスは終了しています。このプロセスは、このプロセスに関する統計情報を親プロセスが収集できるようにする目的のみのために残されています。
EXIT_DEAD: (文字どおり) 最終的な状態です。このプロセスの親プロセスがシステム・コール wait4() または waitpid() を発行してすべての統計情報の収集を終え、このプロセスがシステムから削除されているときに、この状態になります。
プロセスの状態遷移に関する詳細については、「参考文献」セクションに挙げた『Design of the UNIX Operating System』を参照してください。
上記のように、TASK_UNINTERRUPTIBLE と TASK_INTERRUPTIBLE というプロセスの状態はスリープ状態です。今度はプロセスをスリープさせるためにカーネルに用意されているメカニズムを調べてみましょう。
カーネルの居眠り
Linux カーネルには、プロセスをスリープさせる方法が 2 つ用意されています。
プロセスをスリープさせるための通常の方法は、そのプロセスの状態を TASK_INTERRUPTIBLE または TASK_UNINTERRUPTIBLE に設定し、そしてスケジューラーの schedule() 関数を呼び出す方法です。そうすることによって、そのプロセスは CPU のラン・キューから削除されます。プロセスが (プロセスの状態を TASK_INTERRUPTIBLE に設定することによって) 割り込み可能なモードでスリープしている場合には、明示的なウェイクアップ・コール (wakeup_process()) によって、または処理を必要とするシグナルによって、そのプロセスを起こすことができます。
しかし、プロセスが (プロセスの状態を TASK_UNINTERRUPTIBLE に設定することによって) 割り込み禁止のモードでスリープしている場合には、明示的なウェイクアップ・コール以外にそのプロセスを起こす方法がありません。絶対にどうしても必要な場合 (デバイス I/O の最中のようにシグナル処理が困難な場合など) 以外は、プロセスのスリープ・モードを割り込み禁止ではなく、割り込み可能にした方が賢明です。
割り込み可能でスリープ中のタスクがシグナルを受信すると、そのタスクは (そのシグナルが既にマスクされていない限り) そのシグナルを処理する必要があり、それまで行っていた作業をそのまま残し (このためにクリーンアップ・コードが必要です)、ユーザー空間に -EINTR を返します。この場合も、この戻りコードをチェックすること、そして適切なアクションを取ることは、プログラマーの責任です。そのため怠惰なプログラマーは、そのプロセスを割り込み禁止のスリープ・モードにしてしまうかもしれません (そうすればシグナルによってタスクが起こされることがなくなります)。しかし、割り込み禁止の状態でスリープしているプロセスに対して何らかの理由からウェイクアップ・コールが発生せず、そのプロセスを kill できなくなってしまうケースに注意する必要があります。そうした場合には苛立たしいことに、システムをリブートする以外に方法がありません。いくつかの詳細事項に注意しないとカーネルにもユーザー側にもバグが入り込む可能性がある一方、何もできない状態のまま残る (ブロックされたまま kill できない) プロセスになってしまう可能性があります。
しかし今や、スリープするための新しい方法がカーネルにはあるのです。
新しいスリープ状態: TASK_KILLABLE
Linux カーネルのバージョン 2.6.25 では、プロセスをスリープさせるための新しい状態である TASK_KILLABLE が導入されています。kill 可能という、この新しい状態でプロセスがスリープしている場合、そのプロセスは TASK_UNINTERRUPTIBLE の場合と同じように動作し、しかも重要なシグナルに応答することができます。(include/linux/sched.h で定義される) プロセスの状態をカーネル 2.6.18 と 2.6.26 で比較したリスト 1 を見てください。
リスト 1. 2.6.18 と 2.6.26 の間でのプロセスの状態の比較
Linux Kernel 2.6.18 Linux Kernel 2.6.26
================================= ===================================
#define TASK_RUNNING 0 #define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1 #define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2 #define TASK_UNINTERRUPTIBLE 2
#define TASK_STOPPED 4 #define __TASK_STOPPED 4
#define TASK_TRACED 8 #define __TASK_TRACED 8
/* in tsk->exit_state */ /* in tsk->exit_state */
#define EXIT_ZOMBIE 16 #define EXIT_ZOMBIE 16
#define EXIT_DEAD 32 #define EXIT_DEAD 32
/* in tsk->state again */ /* in tsk->state again */
#define TASK_NONINTERACTIVE 64 #define TASK_DEAD 64
#define TASK_WAKEKILL 128
|
TASK_INTERRUPTIBLE と TASK_UNINTERRUPTIBLE という状態は変更されていないことに注目してください。TASK_WAKEKILL は重要なシグナルを受信するとプロセスを起こすように設計されています。
リスト 2 は TASK_STOPPED と TASK_TRACEDという状態が、TASK_KILLABLE の定義とともにどのように変更されたかを示しています。
リスト 2. カーネル 2.6.26 での新しい状態定義
#define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)
#define TASK_STOPPED (TASK_WAKEKILL | __TASK_STOPPED)
#define TASK_TRACED (TASK_WAKEKILL | __TASK_TRACED)
|
言い換えれば、TASK_UNINTERRUPTIBLE + TASK_WAKEKILL = TASK_KILLABLE ということです。
TASK_KILLABLE を使った新しいカーネル API
 |
completion に関する注意
タスクをスリープさせ、後で何らかのイベントが完了したらそのタスクを起こしたい場合、completion によるメカニズムが最適です。completion を利用すると競合状態にならずに簡単に同期をとることができます。wait_for_completion(struct completion *comp) というルーチンは、このルーチンを呼び出したタスクを completion の発生による割り込み以外の割り込み禁止で、スリープさせます。このタスクを起こすためには complete(struct completion *comp) 関数または complete_all(struct completion *comp) 関数を使う必要があります。
待ち状態にするためのルーチンには、wait_for_completion_killable() 以外に下記があります。
wait_for_completion_timeout()
wait_for_completion_interruptible()
wait_for_completion_interruptible_timeout()
completion の構造体の定義は include/linux/completion.h を参照してください。
|
|
この新しい状態の中にある新しい関数をいくつか調べてみましょう。
int wait_event_killable(wait_queue_t queue, condition);
この関数は include/linux/wait.h の中で定義され、condition を評価した結果が真になるまで、この関数を呼び出したプロセスを (kill 可能な状態で) queue の中でスリープさせます。
long schedule_timeout_killable(signed long timeout);
この関数は kernel/timer.c の中で定義され、基本的に現在のタスクの状態を TASK_KILLABLE に設定し、schedule_timeout() を呼び出します。schedule_timeout() は、このルーチンを呼び出すタスクを timeout 個の jiffy の間スリープさせます (UNIX システムでの jiffy は基本的に、連続する 2 つのクロック・ティック間の時間を指します)。
int wait_for_completion_killable(struct completion *comp);
この関数は kernel/sched.c の中で定義され、あるイベントの完了を (kill 可能な状態で) 待つために使われます。この関数は、処理待ちの重要なシグナルが何もなければ schedule_timeout() を呼び出し、(LONG_MAX に等価と定義される) MAX_SCHEDULE_TIMEOUT 個の jiffy の間、このルーチンを呼び出すタスクをスリープさせます。
int mutex_lock_killable(struct mutex *lock);
この関数は kernel/mutex.c の中で定義され、mutex ロックを取得するために使われます。しかしそのタスクが mutex ロックを取得できない上に、mutex ロックの取得を待つ間に重要なシグナルを受信した場合には、そのタスクは、そのシグナルを処理するために mutex ロックを待つタスクのリストから削除されます。
int down_killable(struct semaphore *sem);
この関数は kernel/semaphore.c の中で定義され、sem というセマフォーを取得するために使われます。セマフォーを取得できない場合には、そのタスクはスリープさせられます。そのタスクが重要なシグナルを受信すると、そのタスクはシグナル待ちのリストから削除されることになり、そのシグナルに応答しなければなりません。セマフォーを取得するための他の 2 つの方法として、down() または down_interruptible() を使う方法があります。down() 関数は現在では非推奨とされており、down_killable() または down_interruptible() を使う必要があります。

 |
NFS クライアント・コードの変更
NFS クライアント・コードが何カ所か変更され、この新しいプロセスの状態が使われています。リスト 3 は Linux カーネル 2.6.18 と 2.6.26 の間での nfs_wait_event マクロの違いを示しています。
リスト 3. TASK_KILLABLE の導入による nfs_wait_event の変更
Linux Kernel 2.6.18 Linux Kernel 2.6.26
========================================== =============================================
#define nfs_wait_event(clnt, wq, condition) #define nfs_wait_event(clnt, wq, condition)
({ ({
int __retval = 0; int __retval =
wait_event_killable(wq, condition);
if (clnt->cl_intr) { __retval;
sigset_t oldmask; })
rpc_clnt_sigmask(clnt, &oldmask);
__retval =
wait_event_interruptible(wq, condition);
rpc_clnt_sigunmask(clnt, &oldmask);
} else
wait_event(wq, condition);
__retval;
})
|
リスト 4 は Linux カーネル 2.6.18 と 2.6.26 との間での nfs_direct_wait() 関数の定義の違いを示しています。
リスト 4. TASK_KILLABLE の導入による nfs_direct_wait() の変更
Linux Kernel 2.6.18
=================================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED;
/* Async requests don't wait here */
if (dreq->iocb)
goto out;
result = wait_for_completion_interruptible(&dreq->completion);
if (!result)
result = dreq->error;
if (!result)
result = dreq->count;
out:
kref_put(&dreq->kref, nfs_direct_req_release);
return (ssize_t) result;
}
Linux Kernel 2.6.26
=====================
static ssize_t nfs_direct_wait(struct nfs_direct_req *dreq)
{
ssize_t result = -EIOCBQUEUED;
/* Async requests don't wait here */
if (dreq->iocb)
goto out;
result = wait_for_completion_killable(&dreq->completion);
if (!result)
result = dreq->error;
if (!result)
result = dreq->count;
out:
return (ssize_t) result;
}
|
この新機能を利用するための NFS クライアントの変更の詳細を知るためには、「参考文献」に挙げた Linux Kernel Mailing List のエントリーを見てください。
これまでは NFS マウント・オプション intr を指定することで、何らかのイベントを待っている NFS クライアント・プロセスに割り込みをかけられましたが、その場合、(TASK_KILLABLE のように) kill を目的とする 1 つのシグナルのみではなく、すべての割り込みが許可されてしまいました。
まとめ
TASK_KILLABLE 機能は、大まかに言えば既存のオプションよりも改善されていますが、結局のところ、TASK_KILLABLE はプロセスが死んだ状態に陥らないように防ぐための別の方法にすぎません。TASK_KILLABLE が一般的に使われるようになるまでには、しばらく時間がかかるでしょうが、次のことだけは覚えておいてください。それは、明示的なウェイクアップ・コール以外のいかなる種類の割り込みも (従来の TASK_UNINTERRUPTIBLE によって) 禁止することが非常に重要でない限りは、新しい TASK_KILLABLE を使うということです。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Avinesh Kumar は、インドの Pune にある IBM Software Labs のAndrew File System Team で働くシステム・ソフトウェア・エンジニアです。Avinesh は、ダンプやクラッシュのほか、Linux、AIX、および Solaris プラットフォームで報告されたバグをカーネル・レベルおよびユーザー・レベルでデバッグしています。彼は、University of Pune のコンピューター・サイエンス学部で MCA を取得しています。Linux の熱狂的な支持者である Avinesh は、自分の Fedora Core 6 コンピューターに入っている Linux カーネルを研究しながら余暇を過ごしています。 |
記事の評価
|