1. 概要

カーネルスタックとユーザースタックは、スタックデータ構造を使用して実装され、まとめて呼び出しスタックとして機能します。 この記事では、ユーザーおよびカーネル空間でのコールスタックの使用法について説明します。 また、仮想メモリとそのプロセスへのマッピングについても簡単に説明します。

2. スタック

スタックはLIFOデータ構造です。 プッシュを実行してアイテムをスタックに追加し、ポップしてアイテムを削除できます。 スタックデータ構造は、式の評価/変換、構文チェック、順序の反転、バックトレース、および関数呼び出しに役立ちます。

プログラムは通常、他の関数を呼び出すことができる多くの関数で構成されています。 再帰的(リエントラント)関数はそれ自体を呼び出すこともできます。 どちらの場合も、呼び出し元と呼び出し先のコールグラフを使用してプログラム実行フローを記述できます。

プログラムの実行を追跡するとき、関数間で制御を移すために状態を保存する必要があります。 この目的のために、スタックデータ構造に基づくコールスタックを使用します。 特定のアーキテクチャ依存の規則は、実行中に呼び出し元と呼び出し先によって保存される状態を定義します。

呼び出しスタック内で、一連のスタックフレームを見つけることができます。スタックフレームは、特定の機能に関連するデータで構成されます。 通常、関数引数、差出人住所、およびローカルデータが含まれます。 コールグラフを深く掘り下げると、より多くのフレームが割り当てられ、コールスタックのサイズが大きくなります。

3. カーネルスペースとユーザースペース

最新のシステムのメモリには直接アクセスしません。 物理メモリに支えられた仮想アドレス空間が使用されます。 概念的には、仮想メモリと物理メモリはページと呼ばれるチャンクに分割されます。 通常のページサイズは4096バイトです。

それで、これは私たちに何を買うのですか? まあ、それが判明したように、かなりたくさん。 1つは、プロセス間でメモリを共有する際のメモリ管理の手間が少なくなることです。 このモデルは、各プロセスに独自の仮想メモリスペース(メモリ分離)があるため、より安全です。 また、物理ページによるバッキングはオンデマンド(デマンドページング)であるため、実質的に無制限のメモリが生成されます。 さらに、システムは非アクティブなページをハードドライブにスワップできます。 詳細については、swap-spaceの管理に関する記事を参照してください。

仮想メモリ空間は、ユーザー空間とカーネル空間に分離されています。 カーネル空間は仮想メモリアドレス空間の上位部分です。たとえば、 x86_64 アーキテクチャでは、このマッピング0xffff800000000000から始まります。 。

メモリ領域に加えて、ハードウェアアーキテクチャはI/OポートとCPU命令にも制限を提供します。 たとえば、x86では、0〜3の番号が付けられた4つの保護リングがありますが、Linuxでは、ring-0(カーネルモード)とring-3(ユーザーモード)のみを使用します。

ユーザープロセスが制限されたサービスを必要とする場合、システムコール(syscalls)を使用できます。これらのsyscallは、ユーザーアプリケーションがカーネルリソースにアクセスするためのインターフェイスを形成します。

4. ユーザースタックとカーネルスタック

ユーザースペースでは、動的割り当て(ヒープ)が上位アドレスに向かって増加するのに対し、ユーザースタックは下位アドレスに向かって減少することがわかります。 ユーザースタックは、プロセスがユーザーモードで実行されているときにのみ使用されます。

カーネルスタックはカーネル空間の一部です。 したがって、ユーザープロセスから直接アクセスすることはできません。 ユーザープロセスがsyscallを使用するときはいつでも、CPUモードはカーネルモードに切り替わります。 システムコール中は、実行中のプロセスのカーネルスタックが使用されます。

カーネルスタックのサイズはコンパイル中に構成され、固定されたままです。 これは通常、スレッドごとに2ページ(8KB)です。 さらに、追加のCPUごとの割り込みスタックは、外部割り込みを処理するために使用されます。 プロセスはユーザーモードで実行されますが、これらの特別なスタックには有用なデータがありません。

カーネルスタックとは異なり、ユーザースタックを変更できます。

# ulimit -s
8192
# ulimit -s 32768
# ulimit -s
32768
# ulimit -s unlimited
# ulimit -s
unlimited
#

ulimitコマンドを使用して、現在の制限を確認できます。 -s オプションを使用して、ユーザースタックサイズ(キロバイト単位)を設定または表示できます。 値unlimitedは、スタックサイズに制限がないことを意味します。

次に、大きなスタックスペースを必要とするカスタムプログラムを実行してみましょう。

# ulimit -s 
8192
# ./ackermann.x 3 12
Ackermann(3,12) = 32765
# ./ackermann.x 3 15
Segmentation fault (core dumped)
# ulimit -s 1048576
# ulimit -s 
1048576
# ./ackermann.x 3 15
Ackermann(3,15) = 262141
#

カスタムプログラムは、アッカーマン関数を実装します。これは、一部の入力に多くのスタックスペースを使用できます。 たとえば、この例では、入力3と15のスタック制限を1GBに増やしました。

5. 結論

プロセスまたはスレッドごとに個別のユーザースタックとカーネルスタックを使用することで、分離が向上します。 ユーザースタックの問題によってカーネルがクラッシュすることはありません。この分離により、カーネルは制御下にあるスタック領域のみを信頼するため、カーネルの安全性が高まります。

ただし、スタックは深くネストされた呼び出しで増大するため、アルゴリズムのスペースの複雑さに注意する必要があります。 カーネルスタックサイズを簡単に変更できないため、これはカーネルコードにとってより重要になります。