差分

移動先: 案内検索

プロセス管理

3,397 バイト追加, 2022年8月18日 (木) 18:55
/* プライオリティ */
プログラムが動作する実行実体のことをプロセスと呼びます。プログラムが動作する実行実体のことをプロセスと呼びます。単純化して説明すると、プログラムの実行のことです。単純化して説明すると、プログラムの実行のことです。それで説明が終わるのはあんまりなので、もうちょっと概念的にどのような位置づけなのか考えてみます。プロセスはプロセスごとにプロセスIDを付与され、その数字で管理されています。このプロセスIDの数字は[[プロセスIDを擬似乱数生成関数の初期化パラメータに使う是非 | 一定の数字で循環]]します。Linuxカーネルが扱えるプロセスIDの最大値は/proc/sys/kernel/pid_maxを参照すればわかります。カーネルの[https://github.com/torvalds/linux/blob/master/include/linux/threads.h ソースコード]内ではプロセスIDの最大値は次のようにして設定しています。最少構成でカーネルを作った場合は4096、32ビットCPUだと32768、64ビットもしくはそれ以上のCPUだと4 * 1024 * 1024つまり4194304ということになります。  <pre class="C">/* * This controls the default maximum pid allocated to a process */#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000) /* * A maximum of 4 million PIDs should be enough for a while. * [NOTE: PID/TIDs are limited to 2^30 ~= 1 billion, see FUTEX_TID_MASK.] */#define PID_MAX_LIMIT (CONFIG_BASE_SMALL ? PAGE_SIZE * 8 : \ (sizeof(long) > 4 ? 4 * 1024 * 1024 : PID_MAX_DEFAULT)) </pre>  概念的にどのような位置づけなのかを確認してみます。
==== カーネル側から見る ====
[[File:Process-resource.png|thumb|leftright|300px350px|プロセスは色々なリソースを持っている]] 
今度はカーネル側から見てみましょう。物理的なCPUが1個しかありません。
また、その実行のためにCPUリソース以外にも必要なリソースも割り当てられています。
実行中プロセスの持つリソースを分類すると次のようになります。
 
 
* CPUリソース (タスク系リソース)
* プロセス間通信系リソース
* ファイル管理系リソース
* ユーザ管理系リソース
* ほか
;調べてみよう:個々のプロセスのデータ構造は[httphttps://uc2github.h2np.netcom/misctorvalds/codeslinux/sched_hblob/master/include/linux/sched.html h sched.h]に定義されている[httphttps://uc2github.h2np.netcom/misctorvalds/codeslinux/sched_hblob/master/include/linux/sched.htmlh#N1228 L723 struct task_struct]です。個々のプロセスが保持する情報(とリソース)にはどのようなものがあるかチェックしてみよう。
スレッドやタスクの考え方(と、実装)は80年代にUNIXに入って来た、UNIXの歴史から見ると、わりと新しい技術です。
80年代に入ってから色々な組織がスレッドの実装を試みました。
Brown Univ米ブラウン大学の [https://cs.のブラウンスレッド、brown.edu/research/pubs/techreports/reports/CS-96-18.html Brown Thread Package (BTP)]、サンマイクロシステムズのLWP (Lightweiht ProcessLight-weight process)、DECのCMA スレッド、、DECのDECthreads、[https://kilthub.cmu.edu/articles/journal_contribution/C_threads/6603980/1 CMUのCスレッドパッケージ]がありました。各々互換性はありません。CMUのCスレッドパッケージがありました。各々互換性はありません。紆余曲折あり、現在では[https://hpc-tutorials.llnl.gov/posix/ POSIX仕様のスレッド]である紆余曲折あり、現在ではPOSIX仕様のスレッドである[http://man7.org/linux/man-pages/man7/pthreads.7.html pthreads(7)]が主流になっています。
ですから、単純にプロセスと呼んでも構わないですし、
また同時に(ここまでの説明の範囲では)「プロセス=タスクのこと」と考えてもかまいません。
タスクは複数のスレッドを持つことができます。複数のスレッドが動き出すまでは、プロセスといってもいいわけですが、スレッドを複数持つ状態の場合は、プロセスと呼ぶよりタスクと呼ぶ方が適切でしょう。タスクは複数のスレッドを持つことができます。複数のスレッドが動き出すまでは、プロセスといってもいいわけですが、スレッドを複数持つ状態の場合は、プロセスと呼ぶよりタスクと呼ぶ方が適切でしょう。
このあたりは新旧の考え方が入り乱れて呼び方が混乱しているのは確かなようです。
UNIXが生まれた当時の処理の粒度と今時のUNIXでの処理の粒度は違うので、
そこでここでは説明は理解しやすいようにLinuxそのままではなく
<ref>
LinuxカーネルではTASK_RUNNING、TASK_INTERRUPTIBLE、 TASK_UNINTERRUPTIBLE、 TASK_STOPPEDなどの状態をもちますが、詳しくはヘッダファイルの TASK_STOPPEDなどの状態をもちます。詳しくはヘッダファイルの [https://uc2github.h2np.netcom/torvalds/linux/blob/master/include/srclinux/sched_hsched.htmlh#N197 L82 sched.h] を参照してください。
</ref>
ある程度単純化したプロセス状態の基本モデルを考えて説明したいと思います。
プロセスの生成は[http://man7.org/linux/man-pages/man2/fork.2.html fork(2)]を使い親プロセスが子プロセスを生み出します。下のコードを見て下さい。
 <syntaxhighlight langpre class='"C' line="1" >
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <stdio.h>
void int main(void){
pid_t pid, wait_pid;
int wait_stat;
exit(EXIT_SUCCESS);
}
else { if ( pid == -1 ){ exit(EXIT_FAILURE); } fprintf(stderr,"pid in parent: %d\n",pid); wait_pid=wait(&wait_stat); fprintf(stderr,"wait returns : %d(%o)\n",wait_pid,wait_stat); exit(EXIT_SUCCESS);
}
fprintfreturn(stderr,"pid in parent: %d\n",pid); wait_pid=wait(&wait_stat); fprintf(stderr,"wait returns : %d(%o)\n",wait_pid,wait_stat); exit(EXIT_SUCCESS0);
}
 </syntaxhighlight pre >
システムコールfork(2)はフォーク後、親プロセスでは子プロセスのプロセスIDを返却し、子プロセスでは0を返却します。
;補足: プログラム中ではシステムコールexecve(2)よりもライブラリ関数execlよりも下のサンプルコードのようにライブラリ関数execl(3)などを使う場合の方が多いでしょうが、ここでは説明としてexecveなどを使う場合の方が多いでしょう。下のコードはライブラリ関数execl(23) を取り上げています。下のコードはを使って/bin/dateを実行している例です。
<syntaxhighlight langpre class='"C' line="1" >
#include <unistd.h>
#include <sys/types.h>#include <sys/wait.h>void main(void) { int wstat;
if ( fork() == 0 ) {
execl("/bin/date","/bin/date",NULL);
}
wait(&wstat);
}
</syntaxhighlightpre>
;調べてみよう: カーネルの中でforkをするのはkernelカーネルの中でforkをするのは[https://github.com/torvalds/linux/blob/master/kernel/fork.cの中の関数do_fork です。関数do_forkは関数copy_processを呼び出してプロセスをコピーするのがメインの役割です。関数copy_processは、必要なものを親プロセスからコピーし子プロセス独自に必要な情報を初期化しています。c#L2541 kernel_clone()]です。それまでは_do_fork()でしたが2020年にリリースされたLinux Kernel 5.9以降kernel_clone()になりました。[https://patchwork.kernel.org/project/linux-kselftest/cover/20200818173411.404104-1-christian.brauner@ubuntu.com/#23554325 参考資料]
==== プロセスを生成するシステムコール ====
プロセスにCPU資源をどのように与えるかの順番を決めるスケジュール (schedule)ことを行うことがスケジューリング(scheduling)です。それを司るのがスケジューラ(scheduler)<ref>Linux Schedulerhttps://www.kernel.org/doc/html/latest/scheduler/index.html</ref>です。
==== 基本的なスケジューリング方式 ====
スケジューリングの方式にはポリシーと呼ばれる、いくつかの方式があります。スケジューリングの方式にはポリシーと呼ばれる、いくつかの方式<ref>Linux カーネル version 3.14 以降ではデッドラインスケジューリングポリシー (SCHED_DEADLINE) が追加されています。</ref>があります。
ラウンドロビンは一定時間CPU時間を消化したプロセスを待ち状態に戻し、待っていた次のプロセスに切替えて順繰りにプロセスを実行していく方法です。この一定時間のことをタイムスライス(timesliceTime Slice)といいます。公平にプロセスにCPUを割り当てることになります。タイムスライスを小さくすれば小さくするのど、細かくプロセスをCPUへ割り当てることができますが、しかし割り当て処理の回数が増えるにつれカーネル自体が消費するCPU時間も多くなっていきます。ですから、リーズナブルな値にしなければなりません。あるいはタイムクアンタム (Time Quantum)と呼びます。公平にプロセスにCPUを割り当てることになります。タイムスライスを小さくすれば小さくするのど、細かくプロセスをCPUへ割り当てることができますが、しかし割り当て処理の回数が増えるにつれカーネル自体が消費するCPU時間も多くなっていきます。ですから、リーズナブルな値にしなければなりません。
Linux 2.6系におけるスケジューラは以前2.4系で使われていたスケジューラからみると画期的に性能がいいO(1)スケジューラ(Order one scheduler)に変更されました。さらにLinux 2.6.23からO(1)スケジューラからCFS(Completely Fair Scheduler)に変更されました。
 その後 Linux 3.14 (2014/3/20リリース) からリアルタイム指向の処理に必要なデットラインを処理するための [https://core.ac.uk/download/pdf/14699805.pdf SCHED_DEADLINE] がスケジューラ <ref> デフォルトで使えるかどうかはディストリビューションによります。</ref> に加わっています。Linux 3.14ではデットライン・スケジューラのアルゴリズムはEarliest Deadline First (EDF)を採用していましたが、Linux 4.13 (2017/9/5リリース)からはEDFを改良しさらに Constant Bandwidth Server ( CBS ) 加えた [https://uc2.h2np.net/src/deadline_c.html#N1 ( と、カーネルのコメントでは表現している )] アルゴリズムを採用しています。==== 使えるスケジューリングを調べてみる ====
SCHED_IDLE min/max priority : 0/0
</pre>
 
またchrtはプロセスのスケジューリング・ポリシーを変更することが出来ます。詳しくはマニュアル [https://man7.org/linux/man-pages/man1/chrt.1.html chrt] を確認してください。
 
 
;追記: その後 Linux 3.14 (2014/3/20リリース) からリアルタイム指向の処理に必要なデットラインを処理するための [https://core.ac.uk/download/pdf/14699805.pdf SCHED_DEADLINE] がスケジューラ <ref> デフォルトで使えるかどうかはディストリビューションによります。</ref> に加わっています。Linux 3.14ではデットライン・スケジューラのアルゴリズムはEarliest Deadline First (EDF)を採用していましたが、Linux 4.13 (2017/9/5リリース)からはEDFを改良しさらに Constant Bandwidth Server ( CBS ) 加えた( と、 [https://github.com/torvalds/linux/blob/master/kernel/sched/deadline.c カーネルのコメント] では表現している )アルゴリズムを採用しています。
 
 
 
タイムスライスの値はスケジューリングのポリシーによっても変化しますし、スケジューリング・ポリシーによっては動的にも変化します。
指定されたプロセスの SCHED_RR 間隔を取得するシステムコール[http://man7.org/linux/man-pages/man2/sched_rr_get_interval.2.html sched_rr_get_interval(2)]を使うと実際の[[プロセスのタイムスライスの値を計測する]]ことが可能です。
 
 
これらのスケジューリングもカーネルのパラメータを変えることでチューニングができます。ただし、それに関しての説明は本サイトの主旨を越えてしまいますし、当然ながらカーネル内のパラメータを変更するわけですから、不適切な値の場合、システムに大きな損傷を与える可能性があります。ここでは[https://doc.opensuse.org/documentation/leap/archive/42.1/tuning/html/book.sle.tuning/cha.tuning.taskscheduler.html openSUSEのタスクスケジュラーのチューニング]のページを紹介するに留めます。
==== CFS ====
Linux 2.6.23からCFS(Completely Fair Scheduler)
<ref>
Inside the Linux 2.6 Completely Fair Scheduler
https://developer.ibm.com/tutorials/l-completely-fair-scheduler/
(リンク切れ)
Linux カーネル 2.6 Completely Fair Scheduler の内側
http://www.ibm.com/developerworks/jp/linux/library/l-completely-fair-scheduler/
</ref>
<ref>Completely Fair Scheduler によるマルチプロセッシングhttp://www.ibm.com/developerworks/jp/linux/library/l-cfs/index.html</ref>は、Oは、O(1) スケジューラがヒューリスティック(発見的)にスケジューリングしていたのに対し、CFS のスケジューリングは(数学的な意味で)数値を計算してスケジューリングを決めます。
ヒューリスティックな方法では、ある種の特定の条件に嵌まってしまいスケジューリングが公平に処理できなくなったり、あるいは偶然に有利な条件になったり不利な条件になったりとする可能性があります。
それに比べCFSは公平にスケジューリングされます。
=== プライオリティ ===
UNIX系のオペレーティングシステムではプロセスの持つ動的に変化するプライオリティ値によって、CPUの割当てが変化します。プライオリティ値が小さいものが実行優先順位が高く、大きいものが実行優先順位が低くなります。UNIX系のオペレーティングシステムではプロセスの持つ動的に変化するプライオリティ値によって、CPUの割当てが変化します。プライオリティ値が小さいものが実行優先順位が低く、大きいものが実行優先順位が高くなります。
プロセス待機キューの並べ変え、プロセスのタイムスライスの値を設定する時に、プライオリティが高い程、よりCPUが割り当てられるような計算がされます。
</pre>
:;補足| コマンドniceやreniceで与えるナイス値と、表示されるナイス値、カーネル内部で使うプライオリティ値、表示されるプライオリティ値ののプラス/マイナスは間違え安いので注意しましょう。上の例では: コマンドniceやreniceで与えるナイス値と、表示されるナイス値、カーネル内部で使うプライオリティ値、表示されるプライオリティ値のプラス/マイナスは間違え安いので注意しましょう。上の例では-19の値を与えているが、topなどのNIの欄の値は19となります。
=== スレッド ===
スレッドは計算資源を共有しCPU資源だけは別の状態であるような処理の流れをいいます。スレッドにはclone(2)のようなカーネルレベルで実行するスレッドとglibcなどユーザライブラリに含まれているユーザレベルで実行するスレッドの2つがあります。ですからスレッドが必ずしもカーネルモードで動作しているわけではなく、それはプログラムの書かれ方によります。ここの説明ではカーネルスレッドとユーザスレッドとの両方取り上げてみます。
 
 
既に説明した通りfork(2)が親と子のプロセスで内容をコピーしているのですが、とはいえ fork(2) 後は各々の実体は独立しています。しかし、スレッドは内容が一緒ですから、つまり処理の流れを別に持つCPU資源だけは複数個持つのですから、新しいプロセスが生まれていないので親も子もありません。CPU資源以外の計算資源のコピーが必要ない場合、あるいは同じ計算資源でCPU 資源を複数持ちたい時、計算資源を共有したい時などにスレッドは便利です。
 
 
CPUリソースだけを同時に複数個持つことができることで、たとえば実行しているタスク中でネットワーク入出力からの処理を行いつつ同時に他の計算を繰り返すような場合に非常に有効に働きができます。
この利点は、資源コピーが発生しないまま処理スレッドだけを高速に立ち上げることができると説明しましたが実際にfork(2)とLinuxのpthreadライブラリ(カーネルスレッドではなくユーザスレッド)を使ってどれぐらい違うか試してみました。
1000 回プロセスを生成するのとスレッドを生成するプログラムを書いてみます。結果はAthlon 1GHzマシンでスレッド生成側がトータル0.157秒、プロセス生成側が0.360秒と2倍以上の差をつけました。このようにCPUリソースのみだけで処理が出来るようなものを大量に生成するような場合はスレッドが有利に働きます。
 
 
生成可能な最大スレッド数は利用しているシステムのメモリ量に依存します。/proc/sys/kernel/threads-max を参照することでわかります。
* fork版
<syntaxhighlight langpre class='"C' line="1" >
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <sys/wait.h>int main(void) { int i,stat;
for(i=0;i<1000;i++) {
if ( fork() == 0 ) {
_exit(0);
}
wait(&stat);
}
printf("%d\n",i);
}
</syntaxhighlightpre
<pre class="bash">
% cc -O fork.c -o f
* thread版
<syntaxhighlight langpre class='"C' line="1" > 
#include <pthread.h>
#include <stdlib.h>
printf("%d\n",i);
}
</syntaxhighlightpre>
<pre class="bash">
% cc -O thread.c -lpthread -o t