目次


Linux プロセス管理の徹底調査

作成、管理、スケジューリング、そして破棄

Comments

Linux は、コンピューティング・リソース利用のニーズが絶えず変化する極めて動的なシステムです。Linux でのコンピューティング・リソース利用のニーズは、共通の形に抽象化されたプロセスによって表現されます。プロセスには短時間しか存続しないもの (コマンドラインから実行されるコマンドなど) もあれば、長期間実行されるもの (ネットワーク・サービスなど) もあります。そのため、さまざまなプロセスとそのスケジューリングを全般的に管理することが非常に重要となります。

ユーザー空間からは、プロセスはプロセス識別子 (PID) によって表現されます。ユーザーの観点で見ると、PID はプロセスを一意に識別する数値です。PID はプロセスが存続している間は変わりませんが、プロセスが終了した後は PID を再度使用できるため、常に PID をキャッシュに入れたほうがよいとは限りません。

ユーザー空間でプロセスを作成するには、何通りかの方法があります。例えば、プログラムを実行するという方法 (プログラムの実行結果として新規プロセスが作成されます) もあれば、プログラム内で fork または exec システム・コールを呼び出すこともできます。fork を呼び出すと子プロセスが作成される一方、exec を呼び出すと現行プロセスのコンテキストが新規のプログラムで置き換えられます。これらのメソッドについては、それぞれがどのような動作をするか理解できるようにこの記事で説明します。

さらにこの記事では、プロセスについて説明します。まず始めにカーネルでのプロセスの表現を取り上げ、カーネル内でプロセスがどのように管理されるかを説明します。続いてプロセスの作成および 1 つ以上のプロセッサーでのプロセスのスケジューリングを行う際に使用するさまざまな手段を検討し、最後にプロセスが消滅する際の動作を見ていきます。

プロセスの表現

Linux カーネル内部では、プロセスは task_struct と呼ばれるかなり大きな構造体で表現されます。この構造体には、プロセスを表現するために必要なすべてのデータに加え、アカウンティング用の大量のデータ、他のプロセス (親および子) との関係を維持するためのデータも含まれます。task_struct についての詳しい説明はこの記事ではしませんが、task_struct の一部をリスト 1 に記載します。このコードには、記事で具体的に取り上げる要素が含まれています。task_struct は ./linux/include/linux/sched.h に含まれていることに注意してください。

リスト 1. task_struct のごく一部
struct task_struct {

	volatile long state;
	void *stack;
	unsigned int flags;

	int prio, static_prio;

	struct list_head tasks;

	struct mm_struct *mm, *active_mm;

	pid_t pid;
	pid_t tgid;

	struct task_struct *real_parent;

	char comm[TASK_COMM_LEN];

	struct thread_struct thread;

	struct files_struct *files;

	...

};

リスト 1 を見ると、実行状態やスタック、そして一連のフラグ、親プロセス、実行のスレッド (多数のスレッドがある場合もあります)、オープン・ファイルなど、一般的に要求される項目があります。これらの項目については後で説明しますが、ここで、そのうちのいくつかを紹介しておきます。まず、state 変数はタスクの状態を示す一連のビットです。最も一般的な状態では、プロセスが実行中またはラン・キューのなかにあって実行されるのを待っている (TASK_RUNNING) か、スリープ状態である (TASK_INTERRUPTIBLE) か、スリープ状態になっているけれどもウェイクアップできない (TASK_UNINTERRUPTIBLE) か、停止している (TASK_STOPPED) か、などが示されます。これらのフラグすべてのリストを見るには、./linux/include/linux/sched.h を参照してください。

ワード・サイズの flags には、プロセスを作成中 (PF_STARTING) または終了中 (PF_EXITING) であるかどうか、さらにはプロセスが現在メモリーを割り当てているかどうか (PF_MEMALLOC) など、あらゆる状態を示すさまざまな標識が定義されています。実行可能ファイルの名前 (パスを除く) は、comm (コマンド) フィールドのなかに含まれます。

それぞれのプロセスには優先順位 (static_prio) も割り当てられますが、プロセスの実際の優先順位は、負荷やその他の要素に基づいて動的に決定されます。優先順位の値が低いほど、実際の優先順位は高くなります。

tasks フィールドは、リンク・リストの機能を提供します。このフィールドには前のタスクを指す prev ポインターと、次のタスクを指す next ポインターが含まれます。

プロセスのアドレス空間を表すのは、mm フィールドと active_mm フィールドです。mm はプロセスのメモリー記述子を表す一方、active_mm は前のプロセスのメモリー記述子です (コンテキスト・スイッチの時間を短縮するための最適化)。

最後に、thread_struct はプロセスが保管されているときの状態を特定する要素です。この要素は、どういうアーキテクチャーの上で Linux が実行されているかによって変わりますが、その一例を ./linux/include/asm-i386/processor.h で参照することができます。この構造体を見れば、実行中のコンテキストからプロセスが切り替えられたときに保管されているプロセスの状態 (ハードウェア・レジスター、プログラム・カウンターなど) がわかります。

プロセスの管理

このセクションでは、Linux 内部でのプロセスの管理方法を探ります。大抵の場合、プロセスは動的に割り当てられる task_struct によって動的に作成され、表現されますが、init プロセスだけは例外です。このプロセスは常に存在し、静的に割り当てられた task_struct によって表されます。その一例は、./linux/arch/i386/kernel/init_task.c で確認することができます。

Linux 内のすべてのプロセスは、2 種類の手段で収集されます。1 つは PID 値によってハッシュ化されたハッシュ・テーブル、そしてもう 1 つは、循環式の二重リンク・リストです。この循環式リストは、タスク・リストを繰り返し処理するには理想的な手段となります。循環式リストには先頭も末尾もありませんが、init_task は常に存在するので、これをアンカー・ポイントとして使用すれば繰り返し処理を実行することができます。この仕組みを、現行のタスクのセットをウォークスルーする例で説明します。

タスク・リストにはユーザー空間からはアクセスできませんが、この問題はコードをモジュールという形でカーネルに挿入することによって簡単に解決することができます。リスト 2 に記載する至って単純なプログラムは、タスク・リストを繰り返し処理し、各タスクに関する限られた量の情報 (namepid、および parent 名) を出力するというものです。このモジュールは、printk を使用して出力を行うことに注目してください。出力を表示するには、cat ユーティリティー (または、リアルタイムで表示するには tail -f /var/log/messages) によって /var/log/messages ファイルを表示する必要があります。next_task 関数は、タスク・リストの繰り返し処理を単純化する sched.h 内のマクロです (次のタスクの task_struct 参照を返します)。

リスト 2. タスク情報を出力する単純なカーネル・モジュール (procsview.c)
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/sched.h>

int init_module( void )
{
  /* Set up the anchor point */
  struct task_struct *task = &init_task;

  /* Walk through the task list, until we hit the init_task again */
  do {

    printk( KERN_INFO "*** %s [%d] parent %s\n",
task->comm, task->pid, task->parent->comm );

  } while ( (task = next_task(task)) != &init_task );

  return 0;

}

void cleanup_module( void )
{
  return;
}

このモジュールは、リスト 3 に記載する Makefile を使ってコンパイルすることができます。コンパイルが完了すると、カーネル・オブジェクトを insmod procsview.ko を実行することで挿入し、rmmod procsview を実行することで削除することが可能になります。

リスト 3. カーネル・モジュールをビルドする Makefile
obj-m += procsview.o

KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

default:
	$(MAKE) -C $(KDIR) SUBDIRS=$(PWD) modules

挿入後に、/var/log/messages には以下の出力が表示されます。この出力から、アイドル・タスク (swapper という名前のタスク) と init タスク (pid 1) を確認することができます。

Nov 12 22:19:51 mtj-desktop kernel: [8503.873310] *** swapper [0] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904182] *** init [1] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904215] *** kthreadd [2] parent swapper
Nov 12 22:19:51 mtj-desktop kernel: [8503.904233] *** migration/0 [3] parent kthreadd
...

現在実行中のタスクを識別することも可能です。Linux が管理する current というシンボルが、現在実行中の (task_struct タイプの) プロセスを示します。以下の行を init_module の最後に追加してください。

printk( KERN_INFO, "Current task is %s [%d], current->comm, current->pid );

すると、以下の内容が表示されます。

Nov 12 22:48:45 mtj-desktop kernel: [10233.323662] Current task is insmod [6538]

init_module 関数は insmod コマンドの実行コンテキスト内で実行されることから、カレント・タスクは insmod であることに注意してください。current シンボルは、実際には関数 (get_current) を参照します。このシンボルは、Arch 固有のヘッダーにあります (例えば、./linux/include/asm-i386/current.h)。

プロセスの作成

今度はユーザー空間からプロセスを作成する際の一連の動作について説明します。基礎となるメカニズムは、ユーザー空間とカーネル・タスクに共通で、どちらも最終的には do_fork という関数に依存して新しいプロセスを作成します。カーネル・スレッドを作成する場合、カーネルは kernel_thread という関数を呼び出します (./linux/arch/i386/kernel/process.c を参照)。この関数は何らかの初期化を行ってから、do_fork を呼び出します。

ユーザー空間のプロセスを作成する場合にも、同じような動作が行われます。ユーザー空間では、プログラムが fork を呼び出すと、カーネル関数 sys_fork に対するシステム・コールが行われます (./linux/arch/i386/kernel/process.c を参照)。図 1 に、関数の関係を図解します。

図 1. プロセスを作成する場合の関数階層
Function hierarchy for process creation
Function hierarchy for process creation

図 1 から、do_fork がプロセス作成の基礎となっていることがわかります。do_fork 関数は、./linux/kernel/fork.c の中で定義されています (do_fork 関数と連携する copy_process 関数も同ファイルの中で定義されています)。

do_fork 関数はまず最初に、新しい PID を割り当てるための alloc_pidmap を呼び出します。次に、do_fork 関数ではデバッガーが親プロセスをトレースしているかどうかをチェックし、トレースしている場合には、fork に備えて clone_flagsCLONE_PTRACE フラグをセットします。続いて do_fork 関数は copy_process を呼び出して、フラグ、スタック、レジスター、親プロセス、そして新しく割り当てられた PID を渡します。

copy_process 関数では、新しいプロセスが親のコピーとして作成されます。この関数は、プロセスの開始を除くすべてのアクションを実行します (プロセスの開始は後で処理されます)。copy_process での最初のステップは、CLONE フラグを検証してフラグの一貫性を確実にすることです。フラグが一貫していない場合には、EINVAL エラーを返します。続いて LSM (Linux Security Module) を調べ、カレント・タスクが新しいタスクを作成できるかどうかを確認します。SELinux (Security-Enhanced Linux) のコンテキストでの LSM についての詳細は、「参考文献」セクションを参照してください。

続いて呼び出されるのは、dup_task_struct 関数 (./linux/kernel/fork.c を参照) です。この関数は新規の task_struct を割り当て、その構造体のなかにカレント・プロセスの記述子をコピーします。新しいスレッド・スタックがセットアップされた後は、一部の状態情報が初期化されて制御が copy_process に戻ります。制御が返された copy_process では、新規 task_struct で追加の制限およびセキュリティー・チェックが行われる他、さまざまな初期化を含めたハウスキーピングも行われます。その上で、プロセスの個別の側面をコピーする一連のコピー関数が呼び出され、オープン・ファイル記述子 (copy_files)、シグナル情報 (copy_sighandcopy_signal)、プロセス・メモリー (copy_mm)、そして最後にスレッド (copy_thread) がコピーされます。

続いて、新しいタスクがプロセッサーに割り当てられ、プロセスの実行が許可されるプロセッサーに基づく追加のチェックも行われます (cpus_allowed)。新規プロセスの優先順位が親の優先順位を継承した後、多少のハウスキーピングが追加で行われ、制御が do_fork に戻ります。この時点で、新規プロセスは存在していますが、まだ実行中にはなっていません。do_fork 関数はこれを wake_up_new_task を呼び出すことで解決します。この wake_up_new_task 関数 (./linux/kernel/sched.c を参照) は、スケジューラー・ハウスキーピング情報の一部を初期化して新規プロセスをラン・キューに入れた後、そのプロセスをウェイクアップして実行させます。そして最後に do_fork に戻った時点で、PID 値が呼び出し側に返されます。これで、プロセスは完了することになります。

プロセスのスケジューリング

プロセスが Linux に存在する間は、Linux スケジューラーによってプロセスがスケジューリングされる可能性があります。Linux スケジューラーについてはこの記事では説明しませんが、このスケジューラーは、task_struct 参照が置かれているそれぞれの優先レベルごとに一連のリストを保持します。タスクを呼び出す手段となる schedule 関数 (./linux/kernel/sched.c を参照) は、負荷および以前のプロセス実行履歴に基づいて、実行するのに最適なプロセスを判断します。Linux バージョン 2.6 のスケジューラーについての詳細を学ぶには、「参考文献」を参照してください。

プロセスの破棄

プロセスの破棄は、複数のイベントによって行われます。プロセスを通常の方法で終了することもできれば、シグナルを使用することも、exit 関数を呼び出すこともできます。どのようにプロセスを終了する場合であれ、プロセスの終了は最終的にカーネル関数 do_exit (./linux/kernel/exit.c を参照) の呼び出しによって行われます。このプロセスを図 2 に図解します。

図 2. プロセスを破棄する場合の関数階層
Function hierarchy for process destruction
Function hierarchy for process destruction

do_exit の背後にある目的は、カレント・プロセスへのすべての参照をオペレーティング・システムから削除することです (共有されていないすべてのリソースが対象)。破棄のプロセスではまず、PF_EXITING フラグをセットすることで、プロセスを終了していることを示します。カーネルのその他の処理では、この標識を参照することで、このプロセスを削除している最中にこのプロセスを操作しないようにします。プロセスをその存続中に使用したさまざまなリソースから切り離すサイクルは、exit_mm (メモリー・ページを削除) から exit_keys (スレッドごとのセッションおよびプロセスのセキュリティー・キーを廃棄) までの一連の呼び出しによって行われます。do_exit 関数はプロセスの廃棄に関するさまざまなアカウンティングを行った後、exit_notify を呼び出すことによって一連の通知が行われます (例えば、親に子が終了していることを通知するなど)。最後に、プロセス状態が PF_DEAD に変更されて、次に実行するプロセスを選択するために schedule 関数が呼び出されます。親に対するシグナル送出が必要な場合 (またはプロセスがトレースされている場合) には、タスクは完全には消滅しないことに注意してください。シグナル送出が不要であれば、release_task を呼び出すことで、プロセスが使用していたメモリーが解放されます。

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

Linux は進化し続けています。そんな Linux でさらなる革新と最適化が見込まれる領域の 1 つが、プロセス管理です。UNIX の原則に従いながらも、Linux はその境界を押し広げつつあります。カーネルのこの領域での新たな前進の要因となっているのは、新しいプロセッサー・アーキテクチャーや対称型マルチプロセッシング (SMP)、そして仮想化です。その一例としては、Linux バージョン 2.6 で導入された新しい O(1) スケジューラーが挙げられます。このスケジューラーは、多数のタスクを使用するシステムにスケーラビリティーをもたらします。別の例としては、NPTL (Native POSIX Thread Library) を使用して更新したスレッド化モデルもあります。このモデルによって、以前の LinuxThreads モデルより遙かに効率的なスレッド化が可能になっています。これらの革新技術と、今後見込まれる革新技術について詳しく学ぶには、「参考文献」を参照してください。


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


関連トピック

  • 2.6 カーネルでとりわけ革新的な側面の 1 つとなっているのは、その O(1) スケジューラーです。このスケジューラーにより、Linux は通常のオーバーヘッドをもたらすことなく、極めて多くのプロセスに拡張することができます。「Linux スケジューラーの内側」(developerWorks、2006年6月) を読んで、2.6 カーネルのスケジューラーについて詳しく学んでください。
  • Linux でのメモリー管理について詳しく説明している Mel Gorman 『Understanding the Linux Virtual Memory Manager』(Prentice Hall、2004年) を読んでください (PDF 形式も用意されています)。プロセス・アドレス空間についての章をはじめ、この優れた著書では Linux でのメモリー管理について、詳細ながらもわかりやすく説明しています。
  • プロセス管理の優れた入門書である『Performance Tuning for Linux: An Introduction to Kernels』(Prentice Hall、2005年) を読んでください。IBM Press には、章のサンプルが公開されています。
  • Linux は、ユーザー空間とカーネルとの間の遷移を使用した興味深いシステム・コール手法を提供します (アドレス空間の分離)。「Linux システム・コールを使用したカーネル・コマンド」(developerWorks、2007年3月) では、この手法について詳しく説明しています。
  • この記事では、カーネルが呼び出し側のセキュリティー機能をチェックする例を取り上げました。カーネルとセキュリティー・フレームワーク間の基本インターフェースは、Linux Security Module と呼ばれています。このモジュールについて SELinux のコンテキストで詳しく探っている記事、「Anatomy of Security-Enhanced Linux (SELinux)」(developerWorks、2008年4月) を読んでください。
  • スレッドに関する POSIX (Portable Operating System Interface) 標準では、スレッドを作成および管理する標準アプリケーション・プログラミング・インターフェース (API) を定義しています。POSIX 対応の実装は、Linux、Sun Solaris、さらには非 UNIX ベースのオペレーティング・システムにも見られます。
  • The Native POSIX Thread Library」は、POSIX スレッドを効率的に実行するための Linux カーネルでのスレッド化実装です。この技術が導入されたのは 2.6 カーネルで、以前の実装は LinuxThreads と呼ばれていました。
  • TASK_UNINTERRUPTIBLE および TASK_INTERRUPTIBLE プロセス状態に代わる便利な TASK_KILLABLE について紹介している「TASK_KILLABLE: Linux での新しいプロセスの状態」(developerWorks、2008年9月) を読んでください。
  • developerWorks Linux ゾーンに豊富に揃った Linux 開発者向け (Linux を始めたばかりの開発者も含まれます) の資料を調べてください。記事とチュートリアルの人気ランキングも要チェックです。
  • developerWorks に掲載されているすべての Linux のヒントLinux チュートリアルを参照してください。

コメント

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

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=368184
ArticleTitle=Linux プロセス管理の徹底調査
publish-date=12202008