Linux カーネル サブシステム -- プロセス管理分析

Linux は、コンピューティングのニーズが常に変化する非常に動的なシステムです。Linux コンピューティングのニーズの表現は、プロセスの共通の抽象化を中心としており、プロセスは短期間 (コマンド ラインから実行されるコマンド) または長期間 (ネットワーク サービス) にすることができます。したがって、プロセスとそのスケジュールの全体的な管理が非常に重要です。

ユーザー空間では、プロセスはプロセス識別子 (PID) によって表されます。ユーザーの観点から見ると、PID はプロセスを一意に識別する数値です。PID はプロセスの存続期間中は変更されませんが、PID はプロセスの終了後に再利用できるため、キャッシュすることが常に理想的であるとは限りません。

ユーザー空間では、いくつかの方法でプロセスを作成できます。プログラムを実行したり (これにより新しいプロセスが作成されます)、プログラム内で fork または exec システム コールを呼び出すこともできます。fork 呼び出しにより子プロセスが作成され、exec 呼び出しにより現在のプロセス コンテキストが新しいプログラムに置き換えられます。それぞれの方法について説明し、それらがどのように機能するかを確認します。

この投稿では、最初にプロセスのカーネル表現とカーネル内でプロセスがどのように管理されるかを示し、次に 1 つ以上のプロセッサ上でプロセスを作成およびスケジュールするさまざまな方法を確認し、最後にプロセスが停止した場合に何が起こるかについて説明して説明を構築します。プロセスの。

プロセス表現

Linux カーネルでは、プロセスは task_struct と呼ばれるかなり大きな構造によって表されます。この構造には、プロセスを表すために必要なすべてのデータと、アカウンティングおよび他のプロセス (親プロセスおよび子プロセス) との関係を維持するためのその他の多くのデータが含まれています。task_struct の完全な説明はこの記事の範囲を超えていますが、task_struct の一部をリスト 1 に示します。このコードには、この記事で説明した特定の要素が含まれています。task_struct は ./linux/include/linux/sched.h にあることに注意してください。

/* 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;
    ...
};

上記のコード スニペットでは、実行状態、スタック、フラグのセット、親プロセス、実行スレッド (複数の可能性あり)、開いているファイルなど、予想されるいくつかの項目が表示されます。これらについてはこの記事の後半で説明しますが、ここではその一部を簡単に説明します。状態変数は、タスクの状態を示すビットのセットです。最も一般的な状態は、プロセスが実行キュー内で実行中または実行直前である (TASK_RUNNING)、スリープ中 (TASK_INTERRUPTIBLE)、スリープ中だが復帰できない (TASK_UNINTERRUPTIBLE)、停止中 (TASK_STOPPED)、またはその他の状態を示します。これらのフラグの完全なリストは、./linux/include/linux/sched.h にあります。

flags ワードは、プロセスが作成中 (PF_STARTING) か終了中 (PF_EXITING) であるか、さらにはプロセスが現在メモリを割り当てている (PF_MEMALLOC) かどうかに至るまで、すべてを示す多数のインジケーターを定義します。実行可能ファイルの名前 (パスは含まれません) が comm (コマンド) フィールドを占めます。

各プロセスには優先順位 (static_prio と呼ばれる) も与えられますが、プロセスの実際の優先順位は負荷やその他の要因に基づいて動的に決定されます。優先順位の値が低いほど、実際の優先順位は高くなります。

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

プロセスのアドレス空間は、mm フィールドと active_mm フィールドで表されます。mm はプロセスのメモリ記述子を表し、active_mm は前のプロセスのメモリ記述子を表します (コンテキスト切り替え時間を改善するための最適化)。

最後に、thread_struct はプロセスの保存された状態を識別します。この要素は Linux が実行されている特定のアーキテクチャに依存し、その例は ./linux/include/asm-i386/processor.h で確認できます。この構造では、プロセスが実行コンテキストから切り替わるときのプロセスのストレージ (ハードウェア レジスタ、プログラム カウンターなど) が見つかります。

プロセス管理

ここで、Linux でプロセスを管理する方法を見てみましょう。ほとんどの場合、プロセスは動的に作成され、動的に割り当てられた task_struct によって表されます。1 つの例外は init プロセス自体であり、常に存在し、静的に割り当てられた task_struct によって表されます。この例は ./linux/arch/i386/kernel/init_task.c で見ることができます。

Linux のすべてのプロセスは 2 つの異なる方法で収集されます。1 つ目は PID 値によってハッシュ化されるハッシュ テーブルで、2 つ目は循環二重リンク リストです。循環リンク リストは、タスクのリストを反復処理するのに最適です。リンクされたリストは循環しているため、先頭や末尾はありませんが、init_task は常に存在するため、それを以降の反復のアンカーとして使用できます。現在の一連のタスクを説明する例を見てみましょう。

タスク リストにはユーザー空間からアクセスできませんが、コードをモジュールとしてカーネルに挿入することで簡単に回避できます。以下のコード スニペットは、タスクのリストを反復処理し、各タスクに関する少量の情報 (名前、pid、親名) を提供する非常に単純なプログラムを示しています。このモジュールは printk を使用してコンテンツを出力することに注意してください。出力を確認するには、cat ユーティリティを使用して /var/log/messages ファイル (または live tail -f /var/log/messages) を表示する必要があります。next_task 関数は、タスク リストの反復処理を簡素化する sched.h 内のマクロです (次のタスクへの task_struct 参照を返します)。

/* 用于发出任务信息的简单内核模块(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;
}

このモジュールは、以下のコードに示す Makefile を使用してコンパイルできます。コンパイル後、insmod procsview.ko コマンドを使用してカーネル オブジェクトを挿入し、rmmod procsview コマンドを使用してカーネル オブジェクトを削除できます。


obj‑m += procsview.o

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

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

プロセスの作成

次に、ユーザー空間からプロセスを作成するプロセスを見てみましょう。ユーザー空間タスクとカーネルタスクの基礎となるメカニズムは同じであり、どちらも最終的に do_fork と呼ばれる関数に依存して新しいプロセスを作成します。カーネル スレッドを作成する場合、カーネルは kernel_thread という関数を呼び出します (./linux/arch/i386/kernel/process.c を参照)。この関数は初期化を実行してから do_fork を呼び出します。

ユーザー空間プロセスの作成でも同様の操作が行われます。ユーザー空間では、プログラムが fork を呼び出し、その結果、sys_fork という名前のカーネル関数へのシステム コールが発生します (./linux/arch/i386/kernel/process.c を参照)。機能的な関係を図 1 に示します。

                                          图1 用于进程创建的函数层次结构

図 1 から、do_fork がプロセス作成の基礎を提供していることがわかります。do_fork 関数 (および付随関数 copy_process) は ./linux/kernel/fork.c にあります。
do_fork 関数は、最初に alloc_pidmap を呼び出して、新しい PID を割り当てます。次に、do_fork は、デバッガーが親プロセスをトレースしているかどうかを確認します。その場合は、フォークの準備として clone_flags に CLONE_PTRACE フラグを設定します。その後、 do_fork 関数は copy_process の呼び出しに進み、フラグ、スタック、レジスタ、親プロセス、および新しく割り当てられた PID を渡します。

copy_process 関数は、新しいプロセスが親プロセスのコピーとして作成される場所です。この関数は、プロセスの開始以外のすべてを実行しますが、プロセスの開始は後で処理されます。copy_process の最初のステップは、CLONE フラグを検証して一貫性があることを確認することです。そうでない場合は、EINVAL エラーが返されます。次に、Linux セキュリティ モジュール (LSM) が調べられ、現在のタスクが新しいタスクを作成できるかどうかが確認されます。Security-Enhanced Linux (SELinux) 環境における LSM の詳細については、「リソース」セクションを確認してください。

次に、dup_task_struct 関数 (./linux/kernel/fork.c にあります) が呼び出され、新しい task_struct が割り当てられ、現在のプロセスの記述子がそこにコピーされます。新しいスレッド スタックを構築した後、一部の状態情報が初期化され、制御が copy_process に戻ります。copy_process に戻ると、他の制限や安全性チェックの中でも特に、新しい task_struct のさまざまな初期化を含むいくつかのハウスキーピングが実行されます。次に、オープン ファイル記述子のコピー (copy_files)、シグナル情報のコピー (copy_sighand および copy_signal)、プロセス メモリのコピー (copy_mm)、最後にスレッドのコピー (copy_thread) に至るまで、プロセスのあらゆる側面をコピーするために一連のコピー関数が呼び出されます。

次に、新しいタスクがプロセッサに割り当てられ、プロセスの実行が許可されているプロセッサ (cpus_allowed) に基づいて追加のチェックが行われます。新しいプロセスの優先順位が親プロセスの優先順位を継承した後、少量の追加のハウスキーピングが実行され、制御が do_fork に返されます。この時点では、新しいプロセスは存在しますが、まだ実行されていません。do_fork 関数は wake_up_new_task を呼び出すことでこれを修正します。./linux/kernel/sched.c にあるこの関数は、スケジューラのハウスキーピングを初期化し、新しいプロセスを実行キューに入れて、実行のためにウェイクアップします。最後に、do_fork が戻ると、PID 値が呼び出し元に返され、プロセスは完了します。

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

プロセスは Linux に存在しますが、Linux スケジューラを通じてスケジュールできます。この記事の範囲を超えていますが、Linux スケジューラは、task_struct 参照を含む優先度ごとのリストのセットを維持します。タスクは、スケジューリング機能 (./linux/kernel/sched.c で提供) を通じて呼び出され、ロードと以前のプロセスの実行履歴に基づいて、実行する最適なプロセスが決定されます。Linux バージョン 2.6 スケジューラの詳細については、右側のリソース セクションを参照してください。

プロセスが破壊されました

プロセスの破棄は、通常のプロセスの終了、シグナル経由、または終了関数の呼び出しなど、いくつかのイベントによって引き起こされます。ただし、プロセス終了は駆動され、カーネル関数 do_exit (./linux/kernel/exit.c で利用可能) を呼び出すことによってプロセスが終了します。このプロセスを図 2 に示します。
![図 2
プロセス破棄の関数階層] 代替
do_exit の背後にある目的は、現在のプロセス (すべての非共有リソース) へのすべての参照をオペレーティング システムから削除することです。プロセスを破棄すると、PF_EXITING フラグが設定されてプロセスが終了していることが最初に示されます。カーネルの他の側面では、このディレクティブを使用して、プロセスの削除時の操作を回避します。プロセスを、その存続期間中に取得するさまざまなリソースから分離するループは、exit_mm (メモリ ページの削除用) から exit_keys (スレッドごとのセッションおよびプロセス セキュリティ キーの処理用) までの一連の呼び出しを通じて実行されます。do_exit 関数は、プロセスの処理に関するさまざまな統計を実行し、exit_notify を呼び出して一連の通知 (たとえば、子プロセスが終了していることを親プロセスに通知する) を実行します。最後に、プロセスのステータスは PF_DEAD になり、スケジュール関数が呼び出されて、実行する新しいプロセスが選択されます。シグナルを親に送信する必要がある場合 (またはプロセスが追跡されている場合)、タスクは完全には消えないことに注意してください。release_task を呼び出すと、シグナルを送信する必要がない場合、プロセスによって使用されていたメモリが実際に再利用されます。

おすすめ

転載: blog.csdn.net/hhhlizhao/article/details/131873226