Linuxのシステムコールとライブラリ関数・APIに関する世界観2(プロセス・メモリ)です。
X11やGTK+/QtなどについてはLinux(X11周辺)を参照のこと。またLinuxカーネルの世界観(プロセス)も参照のこと。
自分の書いたブログ「神々とともに生きる詩人」2021/01/27より。
UNIXではプロセスは、fork()とそれに準ずるシステムコールによって作成する。
また、デーモンを作る時は、fork()した後でsetsid()で端末を切り離す。
マルチタスクにはプロセスとスレッドがあり、プロセスはそれぞれ個別にメモリ領域が与えられるが、スレッドでは共有データとなる。
一応、プロセス間通信などの例外もある。
fork(2)を実行すると、プロセスが親プロセスと子プロセスに分かれる。fork()を呼び出した瞬間、2つのプロセスは同じコードのfork()を呼び出した直後の状態になる。両方のプロセスで、その後のコードが実行される。
fork()のエラー処理を行ったり、子プロセスと親プロセスで違った処理をさせるためには、fork()の返り値(通常はpidなどの変数に格納する)をif文で-1や0と比較する。
fork()は、失敗した時は-1、成功した時は子プロセスの場合は0、親プロセスの場合は子プロセスのPIDを返す。
これをif文などで比較することで、親プロセスと子プロセスのfork()後の処理を分岐できる。
fork()した後で、exec(3)を実行することで、新しい別のプログラムを実行できる。exec()は、新しいプログラムをロードして、自らのプロセスに上書きして実行する。
exec()にはexec*という名前で始まるさまざまなAPIがあり、多くがライブラリ関数で、execve(2)のみがシステムコールとなる。
fork()した後で、親プロセスが子プロセスの終了を待つためにはwait(2)を実行する。
wait()にはwaitpid()やwaitid()などの亜種もあり、より細かい制御が可能。
あるいはそれぞれのプロセスを個別に終了するためには_exit(2)あるいはexit(3)を実行する。子プロセスの場合は_exit()、親プロセスの場合はexit()を使う。
また、これ以外に、パイプでプロセスを連結するAPIであるpipe(2)、dup(2)、dup2(2)がある。pipe()はfork()などとともに使う。また、stdioにはpopen(3)、pclose(3)というAPIがある。
fork()によって作成されたプロセスは、親子関係を持っており、これを繋ぐと1つのツリー構造となる。pstreeコマンドにより、このツリー構造の親子関係を表示できる。
また、親子関係以外に、セッション(同じ端末から起動したプロセスがまとめられたもの)やプロセスグループ(パイプで連結された複数のプロセスをCtrl+Cで強制終了できる)という概念がある。ps jコマンドを実行すると、セッションやプロセスグループを表示できる。
このほか、セッションリーダーやプロセスグループリーダーと呼ばれる概念がある。セッションやプロセスグループのプロセスのうち、セッションを最初に作ったのがセッションリーダー、プロセスグループを最初に作ったのがプロセスグループリーダーとなる。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道を参考に執筆しました。)
モニタリングも参照のこと。
2023.01.24編集
fork()の単純な例は以下。
pid_t pid; pid = fork(); if (pid < 0) { fprintf(stderr, "forkができません\n"); exit(1); } else if (pid == 0) { /* 子プロセスの処理 */ } else { /* 親プロセスの処理 */ }
実際は親プロセスの処理の中で、wait()を使って子プロセスの終了を待つ必要がある。
(ふつうのLinuxプログラミング Linuxの仕組みから学べるgccプログラミングの王道とプロセスの作成 fork - C言語入門を参考に執筆しました。)
2023.01.18
setsid(2)は、セッションを新しく作成する、デーモンを作る時にお世話になるAPI。デーモンを作る時にはfork()した後でsetsid()を実行して制御端末を持たないプロセスを作る。
後日注記:Linuxにはdaemon(3)というデーモンのための専用のAPIもある。
initとデーモンも参照のこと。
exit(3)はプログラムを終了する関数。引数については、exit(0)は正常終了、exit(1)は異常終了を意味する。また、EXIT_SUCCESSやEXIT_FAILUREなどが使われることも多い。
たとえば、以下のrealloc()のエラーチェックの例では、exit(EXIT_FAILURE)が使われている。
2023.01.18
詳しくは以下の書籍が参考になります。
スレッドを用いたプログラミングをCとLinuxでするのであれば、NPTL(Native POSIX Thread Library)のpthread_create()という関数を使えば良い。
後日注記:C/C++には長らく標準のライブラリにスレッドと呼ばれる概念がなかった。Pスレッドは、UNIXの標準規格であるPOSIXに存在する、プラットフォーム依存のマルチスレッドのAPI。これを使うことで、Linuxでマルチスレッドプログラミングができる。しかしながら、プラットフォーム依存であるためWindowsやDOSでは動作しない。最近はC++にも標準のマルチスレッドAPIができた(C++11のstd::thread)ので、そちらを使う手もある。MachにはCスレッドと呼ばれるものもある。
詳しくは以下の書籍が参考になります。
以下はLinuxでスレッドプログラミングをする際に参考になるページ。
C++(STL・ライブラリ)やJava(マルチスレッド)も参照のこと。
Linuxでスレッドを作るためには、pthread_create(3)関数を利用する。UNIXではこのpthreads(Pスレッド、POSIXスレッド)が標準的なスレッドAPIとして用いられる。LinuxではNPTL (Native POSIX Threads Library)で実装されている。
pthreads関係の各関数は複雑だが、以下にmanpageがある。
2023.08.19編集
一般に、複数のプロセスを作成・破棄するよりも、単一のプロセスの中で複数のスレッドを作成・破棄するほうが、軽量かつ効率的でパフォーマンスがいい。このため、スレッドは「軽量プロセス」などと呼ばれる。
また、プロセス同士がデータを共有するには、共有メモリのようなIPC(プロセス間通信)の機構を使う必要があるのに比べて、単一プロセスの中でそれぞれのスレッドからデータを共有するのは容易である。
マルチスレッドの欠点として言えるのは、設計や実装が複雑になること。高度なマルチスレッドアプリケーションは、複雑な設計と実装が必要であるため、開発が困難になり、バグを生み出す温床となってしまう。
マルチスレッドでプログラミングを行う場合、プログラムに同期や排他制御・ロック、割り込みのような処理をきちんと記述し、マルチスレッド環境で実行しても問題のない「スレッドセーフ」なアプリケーションを実装する必要がある。
(一部の記述で放送大学「コンピュータの動作と管理 ('17)」を参考に執筆しました。)
2023.03.28
通常のC言語の配列は要素数が固定されており、動的に要素数を決めたり、後になってからサイズを変更することができない。
これに対してmalloc(3)で動的に確保された配列は、要素数を動的に決められるし、realloc(3)によって後になってからサイズを変更できる。
malloc()の使い方は、
int *mem; mem = (int *)malloc(sizeof(int) * 20);
のように、配列のサイズを指定したmalloc()関数を実行し、その返り値を型キャストして配列のポインタに代入する。
(配列の使い方 - 徳島大学大学院ソシオテクノサイエンス研究部 - 三輪昌史より引用。)
2023.01.18編集
malloc()を使って動的に生成したメモリ領域は、使わなくなった段階で必ずfree(3)で解放すること。そうでなければメモリリークが起きてしまう。
malloc()を使う時には、エラーチェックをきちんとしましょう。もし、システムのメモリが使い果たされた場合など、正常にメモリを確保できなかった場合、malloc()はNULLを返します。
(エキスパートCプログラミング―知られざるCの深層 (Ascii books)を参考に執筆しました。)
UNIXには、ヒープ領域と呼ばれるメモリ領域がある。これはmalloc()やcalloc()で管理されるメモリ領域のことで、プログラム実行中に領域の大きさが変わることがある。
calloc()は、領域を確保する時にゼロクリアする。また、free()は領域を解放する。
malloc()とfree()を繰り返していると、ヒープのメモリ領域がフラグメンテーションする原因となることがある。
また、brk(2)はデータセグメントのサイズを変更するシステムコール。malloc()の内部において、brk()とmmap()システムコールが呼ばれるようになっている。注意点として、同じプログラムでmalloc()とbrk(), sbrk()を同時に使うことはできない。malloc()は自分自身以外がbrk()やsbrk()を使わないことを前提として実装されているためである。
malloc()やbrk()とそれに属するAPIについては以下にmanpageがある。
動的なメモリ割り当てについては、C言語(配列とポインタ)も参照のこと。
Linuxカーネルの世界観(プロセス)を参照のこと。
並列処理を参照のこと。
Linuxカーネル(シグナル)やLinuxカーネル(IPC)を参照のこと。