目次


TASK_KILLABLE: Linux での新しいプロセスの状態

TASK_UNINTERRUPTIBLE に重大なシグナルに応答できる機能を追加した、この新しいスリープ状態

Comments

プロセスはファイルと同様、すべての 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_UNINTERRUPTIBLETASK_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_INTERRUPTIBLETASK_UNINTERRUPTIBLE という状態は変更されていないことに注目してください。TASK_WAKEKILL は重要なシグナルを受信するとプロセスを起こすように設計されています。

リスト 2 は TASK_STOPPEDTASK_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

この新しい状態の中にある新しい関数をいくつか調べてみましょう。

  • 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 を使うということです。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=348820
ArticleTitle=TASK_KILLABLE: Linux での新しいプロセスの状態
publish-date=09302008