IBM®
本文へジャンプ
    Japan [変更]    ご利用条件
 
 
検索範囲検索:    
    ホーム    製品    サービス & ソリューション    サポート & ダウンロード    マイアカウント    
skip to main content

developerWorks Japan  >  Linux  >

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

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

developerWorks
ページオプション

JavaScript を要するドキュメントオプションは表示されません

原文はこちら

原文はこちら


レベル: 中級

M. Tim Jones, Consultant Engineer, Emulex Corp.

2008年 12月 20日

Linux® でのユーザー空間プロセスの作成と管理には、UNIX® と共通する原則が数多くありますが、それと同時に、Linux ならではの独特な最適化もいくつかあります。この記事では、Linux プロセスのライフサイクルに焦点を当て、ユーザー・プロセスの作成、メモリー管理、スケジューリング、そしてユーザー・プロセスの消滅といったカーネル内部での動作について詳しく説明します。

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

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

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

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

プロセスの表現

developerWorks の他の Tim Jones の記事も読んでください

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 内部で動的に割り当てられるものの、一定の最大数を超えることはありません。カーネルでは、最大数は max_threads というシンボルによって表されます (./linux/kernel/fork.c を参照)。ユーザー空間からこの値を変更するには、/proc/sys/kernel/threads-max の proc ファイル・システムを使用します。

このセクションでは、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)。




上に戻る


プロセスの作成

システム・コール関数

システム・コールのパターンはもうご存知のことでしょう。多くの場合、システム・コールには sys_* という名前が付けられ、呼び出しを実装するための初期機能の一部 (エラー・チェックやユーザー空間のアクティビティーなど) を提供します。実際の作業は、do_* という名前の別の関数に委譲されることがよくあります。

今度はユーザー空間からプロセスを作成する際の一連の動作について説明します。基礎となるメカニズムは、ユーザー空間とカーネル・タスクに共通で、どちらも最終的には 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
図 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

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 モデルより遙かに効率的なスレッド化が可能になっています。これらの革新技術と、今後見込まれる革新技術について詳しく学ぶには、「参考文献」を参照してください。



参考文献

学ぶために

製品や技術を入手するために
  • developerWorks から直接ダウンロードできる IBM ソフトウェアの試用版を使用して、Linux で次の開発プロジェクトを構築してください。


議論するために


著者について

M. Tim Jones

M. 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. の顧問エンジニアでもあります。




記事の評価


サイト改善のため、ご意見をお寄せください。こちらのフォームからお願いいたします。



 


 


不充分・不完全である大変素晴らしい
 


この記事を共有する

del.icio.us del.icio.us newsing newsing FC2ブックマーク FC2ブックマーク
Choix! Choix! ニフティクリップ ニフティクリップ Yahoo!ブックマーク Yahoo!ブックマーク
MM/memo MM/memo CZブックマーク CZブックマーク livedoorクリップ livedoorクリップ
はてなブックマーク はてなブックマーク Buzzurl(バザール) Buzzurl(バザール)




上に戻る


IBM、IBM ロゴ、ibm.com、DB2、developerWorks、Lotus、Rational、Tivoli、および WebSphere は、International Business Machines Corporation の米国およびその他の国における商標または登録商標です。これらおよび他の IBM 商標に、この情報の最初に現れる個所で商標表示 (® または ™) が付されている場合、これらの表示は、この情報が公開された時点で、米国において、IBM が所有する登録商標またはコモン・ロー上の商標であることを示しています。このような商標は、その他の国においても登録商標またはコモン・ロー上の商標である可能性があります。IBM が現在所有している米国における商標のリストをご参照ください。 Linux は、Linus Torvalds の米国およびその他の国における商標です。 UNIX は The Open Group の米国およびその他の国における登録商標です。 他の会社名、製品名およびサービス名等はそれぞれ各社の商標です。

    日本IBMについて プライバシー お問い合わせ