Linuxカーネルのプロセスについて知りたかったことすべて【Linuxカーネルを読む準備】

Linuxカーネルのプロセス

ユーザープロセスについて

ユーザープロセスとは文字通り、ユーザーの操作が元となり発生するプロセスのことです。

カーネルスレッド

ユーザープロセス以外にも、カーネルスレッドと呼ばれる特別なプロセスがあります。

  • システムの起動時に生成され、シャットダウンされるまで動作する
  • ユーザとのやり取りはないため、端末デバイスがいらない
  • カーネルアドレス空間にて、カーネルモードで動作する
    • ex) プロセスIDが1のinitプロセスなど

Linuxカーネルのそのほかの役割

  • プロセスによるシステムコール
  • 例外信号の発生。(命令が無効な場合など、何かしら異常な状態の処理)
  • 周辺デバイスからCPUへの割り込み信号の発行。CPUの状態を注意するものや状態の変更などの事象を知らせるものであり、各割り込み信号あh、割り込みハンドラと呼ばれるカーネルプログラムによって処理されます。
  • カーネルスレッドの実行。

プロセスの切り替え

カーネルはプロセスの実行を停止する際、その時点でのプロセッサレジスタの内容をプロセスディスクリプタの中に退避します。 この機能により、すべてのUNIXカーネルは、再入可能(リエントラント)です。 これによりCPUは各プロセスを高速で切り替えながら処理を進めるので、実質的に同時に複数のプロセスをカーネルモードで実行することができます。

例えば

プロセスに代わってディスクからの読み取り要求を発行した後、カーネルはその仕事をディスクコントローラーに任せ、ほかのプロセスの実行を再開します。 デバイスが読み取りを完了すると、割り込みによってカーネルに通知します。

再入可能性(理エントラント)なシステムコールの実現方法

  • ローカル変数だけを変更し、グローバル変数を変更しないような関数を書くこと
  • カーネルグローバル変数に対してロック機能を使い、その関数を実行するプロセスがその時点で一つだけであることを保証します。

どんな時に複数のカーネル実行パスを切り替えるのか?

カーネル実行パスとは、システムコール、例外、割り込みを処理するためにカーネルによって実行される命令シーケンスのことです。

最も単純な場合、CPUは一つのカーネル実行パスを最初から最後まで逐次的に実行しますが、以下の事象が発生した場合はCPUは複数のカーネル実行パスを切り替えながら実行します。

  • A:ユーザーモードで実行中のプロセスによってシステムコールが発行されたが、カーネル実行パスではその要求を即座に完了できない場合。 この場合、新しい実行プロセスを選ぶためにスケジューラを起動します。
  • B:カーネル実行パスを実行中、CPUが例外を発生した場合。CPUは実行中のプロセスを一時停止し、対応する新たなプロセスを開始する。この処理が終了すると、最初の実行パスを再開できる。
  • C:割り込み許可されたカーネル実行パスを実行中に、ハードウェア割り込みが発生した場合。ハードウェアの処理を優先し、処理が完了すれば最初の処理に戻る。

参考書籍

メモリ内部の動き(プロセスアドレス空間

プロセスは基本的にはそれぞれの固有なアドレス空間において実行されます。

  • ユーザーモードプロセスは、固有のスタック、データ、コード領域を参照します。
  • カーネルモードプロセスは、カーネルのデータとコード領域を参照し、もう一つの固有スタックを使用します。

しかし、各プロセスが固有のアドレスう空間にアクセスしているかのように見えていても、固有のアドレス空間の一部が複数のプロセスによって共有されている場合もあります。

同時に複数のユーザーによって同じプログラム(エディタなど)が必要とされている場合、そのプログラムがメモリに読み込まれるのは一度だけです。

この種のアドレス空間の共有は、メモリの節約のためにカーネルが自動的に行います。

シグナル

UNIXシグナルは、プロセスに対する通知です。 シグナルは20種類ほど存在し、通常はSIGTERMのようなグローバル変数、マクロ名によって参照されます。

非同期と同期

シグナルは大きく分けて2つに大別できます。

  • 非同期通知

    ex) ユーザーは割り込みキーコード(Ctrl+C, Ctrl+Z)を入力することにより、割り込み信号SIGINTをフォアグラウンドプロセスに送ることができます。

  • 同期通知

    プロセスが無効なアドレスのメモリ位置にアクセスしようとした場合、カーネルはプロセスに対し、シグナルSIGSERGを送ります。

受信時の反応

一般的に、シグナル受信時のプロセスの対応は次のどちらかの選択肢が考えられます。

  • シグナルを無視する。(プログラムは通常は何もしないという選択をする)
  • 非同期的に特定の手続きをする(プログラムはシグナル受信時に事前に決めた処理をする)

プロセスがこの二つをどちらも指定していない場合。次の5つの対応が設定されます。

  • プロセスを終了する
  • 実行コンテキストとアドレス空間の内容をファイルに書き込み、プロセスを終了する(コアダンプともいう)
  • シグナルを無視する
  • プロセスを停止する
  • プロセスが停止していた場合、再開する

IPC資源(プロセス間通信)

IPCとは、プロセス間通信という意味である。

POSIXが定めるプロセス間通信は三つの種類があり、それぞれメリットデメリットがある。

  • メッセージキュー
  • メモリ共有
  • フォマ

メッセージキュー

いわゆるメッセージボックスのような動作を行う IPC です。 プロセス間で一意に識別できるメッセージキューに対してメッセージを書き込み、受け取りができます。

  • メッセージキューの作成
msgkey = ftok("msgq.key",'X');
  • メッセージキューのid取得
msqid = msgget(msgkey,IPC_CREAT|0666);
  • メッセージ受け取り
msgrcv(msqid,p,sizeof(p->mdata),0,IPC_NOWAIT)
  • メッセージの送信
msgsnd(msqid,p,sizeof(p->mdata),0);

from https://lightning-brains.blogspot.com/2019/08/unix-linux.html

サンプルコード(メッセージ受信)

#include <sys/types.h>
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
#include <unistd.h>
#define MSGBUFSIZ 256

int main(int argc, char** argv) {
    int msqid;
    key_t msgkey;
    struct msgbuf{
      long mtype;
      char mdata[MSGBUFSIZ];
    };
    struct msgbuf msgdata,*p;
    p = &msgdata;
    // メッセージキューの作成
    msgkey = ftok("msgq.key",'X');
    // メッセージキューの情報取得
    msqid = msgget(msgkey,IPC_CREAT|0666);
    // "Quit"メッセージを受け取るまでループさせる
    while(1) {
        // メッセージ受け取り
        if(msgrcv(msqid,p,sizeof(p->mdata),0,IPC_NOWAIT)<=0) {
          printf("No message\n");
          sleep(3);
          continue;
        }
        printf("message received from %ld\n%s\n",p->mtype,p->mdata);
        if(strncmp((char*)p->mdata, "Quit", (size_t)3)==0)
            break;
    }
    // Remove Message Queue
    msgctl(msqid, IPC_RMID, 0);
}

from https://software.fujitsu.com/jp/manual/manualfiles/M060041/B1WN7071/02Z200/mqdml07/mqdml163.htm

サンプルコード(メッセージ送信)

#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <unistd.h>
#define MSGBUFSIZ 256

int main(int argc, char** argv) {
    int msqid;
    key_t msgkey;
    struct msgbuf{
        long mtype;
        char mdata[MSGBUFSIZ];
    };
    struct msgbuf msgdata,*p;
    // メッセージキューの作成
    msgkey = ftok(argv[1], 'X');
    // メッセージキューの取得 (Only get existing message queue.)
    msqid = msgget(msgkey, 0666);
    printf("QID = %d\n", msqid);
    if(msqid<0 -1="" fflush="" fgets="" nter="" p-="" p="&msgdata;" printf="" return="" stdin="">mdata,MSGBUFSIZ,stdin);
    p->mtype = getpid();
    // メッセージ送信
    msgsnd(msqid,p,sizeof(p->mdata),0);
}

メモリ共有

データをやり取りしたり共有する際に最も高速な方法。 プロセスはshmget()システムコールを発行することで、要求された大きさの新しい共有メモリの作成から始めます。 IPC資源の識別子を獲得した後は、プロセスはshmat()システムコールを呼び出します。 このシステムコールは、プロセスアドレス空間内の新しい領域の先頭アドレスを返します。 最後に、アドレス空間から共有メモリを開放したい場合、ぷろっすはshmdt()システムコールを呼び出します。

ちなみに、shmshare memoryの略

フォマ

英語の「semaphore」の本来の語義は「視覚による通信・信号」全般を指し、腕木通信や、それから派生した鉄道の腕木信号(や自動車の方向指示器)、手旗信号などが含まれる。

複数プロセスの同時アクセスを制御する機構を持っていますが、整数型のデータしか扱えないというのが難点。主な用途としては共有メモリの相互排他に使われます。

図書館での例

ある図書室に学習室が10部屋あり、それぞれの学習室は一度に1人の学生が使用するとする。争奪戦が始まらないよう、学生は受付カウンターで学習室の使用を申し込むことになっている。学習室を使用し終えたら、学生は受付所に立ち寄ってその学習室が空いたことを知らせなければならない。全ての学習室が埋まっている場合、学生は受付所で学習室が空くのを待つ。

受付カウンターの図書係はどの学習室が空いているかは把握しておらず、単に空いている部屋数のみを知っている。学生が申し込んできたとき、図書係は空き部屋数から1を引く。学生が学習室の利用を終えたとき、図書係は空き部屋数に1を加える。学習室の使用許可が与えられたとき、その部屋は必要なだけ使い続けることができる。学習室は事前に予約することはできない。

このシナリオでは、受付カウンターがセマフォ、学習室が資源、学生が実行単位に相当する。また、セマフォの初期値は10になっている。学生が学習室の使用を申し込んで許可されたとき、セマフォ値から1が引かれて9になる。次の学生のときには8、さらに次は7となる。1を引くとセマフォ値が負になるという状態で学生が申し込んだ場合、その学生は待たされる[2]。複数人の学生が待っているとき、彼らは待ち行列を形成するか、いずれかの学生が学習室の使用を終えて受付カウンターにやってきたときに空いた部屋を割り当てる。割り当て方式には、ラウンドロビン・スケジューリング方式等がある(セマフォが正しく制御されるよう実装されていれば、割り当て方式は妥当なものであればよい)。

親子プロセス

プロセスの作成と終了

新しいプロセスの生成と終了のために、それぞれfork(),_exec()システムコールを使用し、新しいプログラムを読み込むためにはexec()系のシステムコールを呼び出します。 プロセスは読み込まれたプログラムを含む、まったく新しいアドレス空間を使用して実行を再開します。

  • fork()を呼び出したプロセスを親プロセス、呼び出されたプロセスを子プロセスと呼びます。
  • _exit()システムコールはプロセスを終了させ、資源を解消し、親プロセスにSIGCHLDシグナルを送ります。
  • wait4()システムコールは自分の子プロセスの一つが終了するまで待つことができます。

ゾンビプロセス

ゾンビプロセスとは親プロセスが子プロセスをほっといてしまい、いつまで経っても終了できない子プロセスのことです。 そして厳密にいえば、親プロセスがfork()を実行した後はゾンビの状態であり、wait4()システムコールを行うまでゾンビのままです。wait4()を実行した後はゾンビプロセスの状態を解消することができます。ゾンビプロセスとは、親プロセスが子プロセスの状態を管理できていない状態といえます。