Linuxの仮想アドレス空間レイアウトおよびプロセスとスレッド・スタックの概要

転送:https://blog.csdn.net/freeelinux/article/details/53782986

これは、複数のブログを切り替え、そして最終的に私の結論はそこにあります。私は理由の概要は、人々は非常によく書かれていることである書き込みを最初から最後まで一人ではないよ、私は多くの時間がこのレベルに達していない費やすことはありません。

 

A:Linuxの仮想アドレス空間レイアウト

 

 マルチタスクオペレーティングシステムでは、各プロセスは、独自のメモリサンドボックス内で実行されます。サンドボックスは、32ビットモードで、それは4ギガバイトのメモリアドレスのブロックであり、仮想アドレス空間(仮想アドレス空間)です。Linuxシステムでは、仮想メモリのカーネルとユーザプロセスシェアの割合は1:3、およびWindowsシステムは、2:2(大アドレスアウェアを設定することにより、実行可能ファイルマークは1であってもよい:3)。これは、物理メモリにマップされるために必要なアドレス空間のこの部分を支配することができる唯一のことを言って、カーネルで使用されている多くの物理メモリを意味するものではありません。

     ページテーブル(ページテーブル)をマッピングすることにより、物理メモリへの仮想アドレスは、ページ・テーブルは、オペレーティングシステムとプロセッサ参照によって維持します。カーネル空間は、ページテーブル内の高い特権レベルを持っているので、ユーザーモードアプリケーションは、ページ・エラー(ページフォルト)時間が発生することができ、これらのページにアクセスしようとしました。Linuxでは、カーネル空間は永続的であり、すべてのプロセスで同じ物理メモリにマッピングされています。カーネルのコードとデータは、割り込みやシステムコールを処理する準備ができて、常にアドレス指定可能です。対照的に、プロセスの切り替えにマッピングされたユーザモードアドレス空間は常に変化が発生します。

     仮想メモリ・レイアウトのLinuxプロセスの標準的なメモリ・セグメントは、以下に示すように:

     異なるメモリ・セグメントに対応する物理メモリにマッピングされた青色帯域のユーザアドレス空間、灰色の領域は、非マッピングされた部分を表します。これらのセグメントは、セグメントのIntelプロセッサとは関係がなく、単にメモリアドレスの範囲です。

     およびその他のランダムランダムにmmapオフセット値以上のランダムスタックオフセットの図は、悪意のあるプログラムを防止するためです。スタック、メモリマッピングセグメント、ヒープの先頭アドレスに加えてランダムを通じてLinuxはアドレス計算・スタック、ライブラリ関数などにアクセスすることにより、悪意あるプログラムを避けるために、レイアウトを破壊するオフセット。(2)はexecveはコードとデータセグメントの実際の内容は、完全な欠落ページ例外ハンドラシステムの需要によってメモリに読み込まれマップコードとデータセグメントのプロセスを担当しています。また、はexecve(2)BSSセグメントもクリアされます。

     (アドレスの降順に)次の表に示すユーザプロセスに格納されたコンテンツセグメント:

名前

メモリ内容

スタック

ローカル変数、関数の引数、戻りアドレス

ヒープ

動的に割り当てられたメモリ

BSSセグメント

0または初期化されていないグローバル変数と静的ローカル変数の初期値

データセグメント

初期化され、初期値がゼロ以外のグローバル変数と静的ローカル変数であります

スニペット

実行可能なコード、文字列リテラル、変数の読み取り専用

     アプリケーションを実行するためにメモリ空間にロードされたときに、オペレーティング・システム・コード・セグメントは、データセグメントとBSSセグメントをロードし、メモリ内のこれらのセグメントのためのスペースを割り当てることに責任があります。また、オペレーティングシステムのスタック割り当てと管理により、スタックは、プログラマ、すなわち、アプリケーションによって管理され、明示的なスペースを解放しています。

     コンパイルされたときBSS、データ及びコードセグメントは、セグメント化された実行可能プログラムは、実行時には、スタックとヒープする必要があります。

 

     各セグメントの意味の以下の詳細な説明。

 

1つのカーネル空間

     カーネルは常に、メモリに常駐するオペレーティング・システムの一部です。カーネル用に予約カーネル空間は、アプリケーションがコア領域の内容を読み書きすることができないか、または機能コードは、直前の呼び出し元を定義します。

 

2スタック(スタック)

     スタックもスタック割り当てとして知られているコンパイラによって自動的に解放され、それがスタックデータ構造(最終)のように振る舞います。三つの主要な目的をスタック:

  • 機能(「自動変数」と呼ばれるC言語)内で宣言非静的ローカル変数のためのストレージスペースを提供します。
  • レコード関数呼び出し関連保守情報は、スタック・フレーム(スタックフレーム)プロセスまたはアクティビティレコード(手順アクティベーションレコード)と呼ばれます。これは、関数の戻りアドレスが含まれ、関数は、パラメータレジスタに適合し、いくつかのレジスタの値を格納しません。再帰呼び出しに加えて、スタックは必要ありません。これは、ローカル変数、パラメータとして知られており、必要なスペースを返し、コンパイル時間をBSSセグメントアドレスに割り当てています。
  • 一時的に長いまたはallocaを演算式の結果を格納するための一時記憶部()内のスタックのためのメモリを割り当てるように機能します。

     継続的な再利用スタック領域は、このようにアクセスを高速化、メモリはCPUのキャッシュ内でアクティブなままスタックすることができます。各スレッドは独自のスタックを持つプロセス。スタックは、連続データに押された場合、その容量を超えた場合、それによってページフォールトをトリガー、スタックに対応するメモリ領域が不足します。この場合、スタックは、最大スタックサイズRLIMIT_STACK(通常8M)を下回っている場合、スタックは動的に成長し、プログラムの実行を継続します。必要なサイズ、無収縮に拡張スタック領域をマッピングします。

     プログラムが使用する場合、Linuxは、ulimit -sは、プログラムセグメントエラー(セグメンテーション違反)を受信する、スタックオーバーフローが(スタックオーバーフロー)が発生し、この値がスタックを超えて、スタックの最大値を表示および設定するためのコマンド。スタックサイズの増加は、メモリのオーバーヘッドと起動時間が長くなる場合があります。

     成長はまた、ダウンスタックは特定の実装に応じて、(より低いメモリアドレスに向かって)上方に成長することができます。スタックは下向きに、本明細書に成長します。

     実行時に動的にカーネルによってスタックのサイズを調整します。

 

メモリマッピング部3(MMAP)

     ここでは、メモリに直接マッピングされたハードコアファイルの内容は、任意のアプリケーションは、Linuxのmmap()システムコール、またはWindows CreateFileMapping関数()/のMapViewOfFile()をマッピングすることにより、これを要求することができます。メモリマッピングは、動的共有ライブラリをロードするために使用される便利で効率的なファイルI / Oモード、です。また、ユーザーは店舗プログラムデータに使用することができ、該当するファイルを持っていません匿名のメモリマッピングを作成することができます。()は、malloc関数でメモリのチャンクを要求した場合Linuxでは、Cランタイムはヒープメモリを使用せずに、匿名のメモリマッピングを作成します。「チャンク」は、閾値が128キロバイトにMMAP_THRESHOLDデフォルトよりも大きいことを意味し、()malloptによって調整することができます。

     面積は、使用ダイナミックリンクライブラリの実行ファイルをマッピングするために使用されます。Linuxの2.4のバージョンでは、依存共有ライブラリの実行可能ファイルならば、システムは、これらの動的ライブラリを開始するには0x40000000からから該当するアドレス空間を割り当て、プログラムがこの空間にロードするロードします。Linux 2.6カーネルでは、共有ライブラリの開始アドレスがスタック領域に近い位置まで上昇します。

     プロセスのアドレス空間のレイアウトから見ることができ、場合には、共有ライブラリが存在し、空き領域のヒープが左2があります:1は、1GBの空き領域よりも小さい、0x40000000からの.bssセクションからであり、もう一つはスタック間の空間に共有ライブラリから、2ギガバイト未満程度。これら二つのスペースには、スタックの大きさ、大きさ、共有ライブラリの数によって異なります。この視点、最大ヒープ領域のアプリケーションのみが2ギガバイトを適用することができますか?実際には、これはLinuxカーネルのバージョンに関連しています。実際にLinuxカーネル2.6版、バージョン2.6の前にケースである古典的なレイアウトビュー上に与えられたプロセスのアドレス空間、共有ライブラリのロードアドレス0x40000000から、では、共有ライブラリのロードアドレスは近いスタックに移動されました0xBFxxxxxx位置するメモリロケーションの最大理論値が、すなわち、従って、この場合のスタックの範囲は共有されないライブラリーは、2つの「チップ」、システム2.6のようにビットのLinuxカーネル32に分割され、malloc関数アプリケーションは2.9ギガバイト程度であります。

 

4ヒープ(ヒープ)

     動的に割り当てられたヒープメモリセグメントは、動的拡張または縮小処理が実行されて格納するために使用されます。ヒープ内容は匿名で、名前によって直接アクセスすることはできません、それだけでポインタを介して間接的にアクセスすることができます。プロセスは、メモリの割り当てなどのmalloc(C)/新しい(C ++)関数を呼び出すと、メモリが動的に新しい割り当ては、ヒープ(拡張)に追加されている、あなたは(C)無料呼び出すと/(C ++)を削除し、他の機能のメモリを解放し、解放されますヒープ除去(切断)からメモリ。

     割り当てられたヒープメモリはバイト整列アトミック操作に合わせて、空間的です。アプリケーションと解放ヒープに、メモリのリストを介してアプリケーションを管理するためのヒープマネージャが乱れ、最終的断片のメモリです。アプリケーションによって割り当てられた一般的なリリースのヒープメモリ、再使用のために利用可能なメモリの回復。プログラマが解放しない場合は、オペレーティング・システムが自動的にプログラムの終了時に回復することができます。

     スタックポインタの端部によってスタックマネージャは、より多くのメモリを必要とするとき、ポインタは一般に、システムによって自動的に呼び出され、システムを通ってスタックを拡張する)(ブレークコールBRK()とsbrkのを移動させることができ、ブレークを識別する。

     多くの場合、これらの問題のヒープの両方の場合:メモリ(「メモリ破損が」)1)使用中でリリースまたは書き換え; 2)使用しなくなっ)、「メモリリーク」(メモリを解放しません。アプリケーションの数よりも少ない解放すると、メモリリークによって引き起こされていてもよいです。メモリリークは、多くの場合、割り当てられたメモリは、通常2のパワーのアプリケーションの数よりも大きい次に丸められているため、大きなデータ構造よりも解放することを忘れて(例えば、アプリケーション212B、256Bに丸められます)。

     ヒープデータ構造は、リンクリストのように振る舞う「スタック」とは異なるので注意してください。

【さらにリーディング]スタックとヒープとの間の差

①管理:スタックは、コンパイラによって自動的に管理;プログラマ、使いやすく、製造が容易でなく、メモリリークによって制御スタック。

②成長方向が:低アドレス(すなわち、「ダウン成長」)にメモリの連続領域を拡張スタックと、高い拡張アドレス(すなわち、「成長」)、メモリの不連続領域にスタック。システムは、空きメモリアドレスのリンクリスト、不連続な自然、そして上位アドレスに下からリストトラバーサルを格納するために使用されるためです。

③空間:予めシステムによって指定されたスタックとスタックアドレス(通常はデフォルトまたは、10M 2M)の最大容量、ヒープのサイズを効果的コンピュータシステムの仮想メモリに制限され、2.9gの32ビットLinuxシステムヒープメモリアップスペース。

④コンテンツ記憶されている:関数呼び出しスタック、次の命令(関数呼び出し文物品実行文)アドレスと関数の引数、関数と呼ばれるローカル変数の関数を調整する第一の圧力に入りました。この呼び出しの後、ローカル変数の最初のアウトスタックは、その後、命令メモリの先頭アドレスへの最後のスタックポインタポイントのパラメータは、プログラムは、この時点の記事実行文から実行し続けます。典型的には、単一バイトのヘッダサイズ、独立した関数呼び出しの生存のための大容量データ記憶装置、プログラマにより配置の特定のコンテンツに格納されたスタック。

⑤割当:スタックは、静的に割り当てられたまたは動的に割り当てることができます。静的割り当ては、ローカル変数の割り当てとして、コンパイラによって行われます。スタック上のスペースを動的に割り当てるためのALLOCA機能は、使用後に自動的に解除されます。動的割り当てはヒープと手動解除することができます。

⑥割当効率:基礎となるコンピュータによって提供されるスタックのサポート:専用レジスタを割り当てることスタックのアドレスを保持し、スタックは、専用のプッシュ命令によって実行されるので、より高い効率です。ライブラリによって提供されるヒープ、複雑なメカニズム、効率は、スタックよりもはるかに低いです。VirtualAllocのWindowsシステムでは、直接、迅速かつ柔軟なプロセスのアドレス空間にメモリのブロックを割り当てることができます。

⑦分配システムの応答の後:限り、アプリケーション・スタック領域が残っているスペースよりも大きいように、システムは、そうでない場合、スタックオーバーフローで例外をプログラムメモリを提供します。

     操作系统为堆维护一个记录空闲内存地址的链表。当系统收到程序的内存分配申请时,会遍历该链表寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点空间分配给程序。若无足够大小的空间(可能由于内存碎片太多),有可能调用系统功能去增加程序数据段的内存空间,以便有机会分到足够大小的内存,然后进行返回。,大多数系统会在该内存空间首地址处记录本次分配的内存大小,供后续的释放函数(如free/delete)正确释放本内存空间。

     此外,由于找到的堆结点大小不一定正好等于申请的大小,系统会自动将多余的部分重新放入空闲链表中。

⑧碎片问题:栈不会存在碎片问题,因为栈是先进后出的队列,内存块弹出栈之前,在其上面的后进的栈内容已弹出。而频繁申请释放操作会造成堆内存空间的不连续,从而造成大量碎片,使程序效率降低。

     可见,堆容易造成内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和内核态切换,内存申请的代价更为昂贵。所以栈在程序中应用最广泛,函数调用也利用栈来完成,调用过程中的参数、返回地址、栈基指针和局部变量等都采用栈的方式存放。所以,建议尽量使用栈,仅在分配大量或大块内存空间时使用堆。

     使用栈和堆时应避免越界发生,否则可能程序崩溃或破坏程序堆、栈结构,产生意想不到的后果。

 

5 BSS段

     BSS(Block Started by Symbol)段中通常存放程序中以下符号:

  • 未初始化的全局变量和静态局部变量
  • 初始值为0的全局变量和静态局部变量(依赖于编译器实现)
  • 未定义且初值不为0的符号(该初值即common block的大小)

     C语言中,未显式初始化的静态分配变量被初始化为0(算术类型)或空指针(指针类型)。由于程序加载时,BSS会被操作系统清零,所以未赋初值或初值为0的全局变量都在BSS中。BSS段仅为未初始化的静态分配变量预留位置,在目标文件中并不占据空间,这样可减少目标文件体积。但程序运行时需为变量分配内存空间,故目标文件必须记录所有未初始化的静态分配变量大小总和(通过start_bss和end_bss地址写入机器代码)。当加载器(loader)加载程序时,将为BSS段分配的内存初始化为0。在嵌入式软件中,进入main()函数之前BSS段被C运行时系统映射到初始化为全零的内存(效率较高)。

     注意,尽管均放置于BSS段,但初值为0的全局变量是强符号,而未初始化的全局变量是弱符号。若其他地方已定义同名的强符号(初值可能非0),则弱符号与之链接时不会引起重定义错误,但运行时的初值可能并非期望值(会被强符号覆盖)。因此,定义全局变量时,若只有本文件使用,则尽量使用static关键字修饰;否则需要为全局变量定义赋初值(哪怕0值),保证该变量为强符号,以便链接时发现变量名冲突,而不是被未知值覆盖。

     某些编译器将未初始化的全局变量保存在common段,链接时再将其放入BSS段。在编译阶段可通过-fno-common选项来禁止将未初始化的全局变量放入common段。

     此外,由于目标文件不含BSS段,故程序烧入存储器(Flash)后BSS段地址空间内容未知。U-Boot启动过程中将U-Boot的Stage2代码(通常位于lib_xxxx/board.c文件)搬迁(拷贝)到SDRAM空间后必须人为添加清零BSS段的代码,而不可依赖于Stage2代码中变量定义时赋0值。

【扩展阅读】BSS历史

     BSS(Block Started by Symbol,以符号开始的块)一词最初是UA-SAP汇编器(United Aircraft Symbolic Assembly Program)中的伪指令,用于为符号预留一块内存空间。该汇编器由美国联合航空公司于20世纪50年代中期为IBM 704大型机所开发。

     后来该词被作为关键字引入到了IBM 709和7090/94机型上的标准汇编器FAP(Fortran Assembly Program),用于定义符号并且为该符号预留指定字数的未初始化空间块。

     在采用段式内存管理的架构中(如Intel 80x86系统),BSS段通常指用来存放程序中未初始化全局变量的一块内存区域,该段变量只有名称和大小却没有值。程序开始时由系统初始化清零。

     BSS段不包含数据,仅维护开始和结束地址,以便内存能在运行时被有效地清零。BSS所需的运行时空间由目标文件记录,但BSS并不占用目标文件内的实际空间,即BSS节段应用程序的二进制映象文件中并不存在。

 

6 数据段(Data)

     数据段通常用于存放程序中已初始化且初值不为0的全局变量和静态局部变量。数据段属于静态内存分配(静态存储区),可读可写。

     数据段保存在目标文件中(在嵌入式系统里一般固化在镜像文件中),其内容由程序初始化。例如,对于全局变量int gVar = 10,必须在目标文件数据段中保存10这个数据,然后在程序加载时复制到相应的内存。

     数据段与BSS段的区别如下: 

     1) BSS段不占用物理文件尺寸,但占用内存空间;数据段占用物理文件,也占用内存空间。

     对于大型数组如int ar0[10000] = {1, 2, 3, ...}和int ar1[10000],ar1放在BSS段,只记录共有10000*4个字节需要初始化为0,而不是像ar0那样记录每个数据1、2、3...,此时BSS为目标文件所节省的磁盘空间相当可观。

     2) 当程序读取数据段的数据时,系统会出发缺页故障,从而分配相应的物理内存;当程序读取BSS段的数据时,内核会将其转到一个全零页面,不会发生缺页故障,也不会为其分配相应的物理内存。

     运行时数据段和BSS段的整个区段通常称为数据区。某些资料中“数据段”指代数据段 + BSS段 + 堆。

 

7 代码段(text)

     代码段也称正文段或文本段,通常用于存放程序执行代码(即CPU执行的机器指令)。一般C语言执行语句都编译成机器代码保存在代码段。通常代码段是可共享的,因此频繁执行的程序只需要在内存中拥有一份拷贝即可。代码段通常属于只读,以防止其他程序意外地修改其指令(对该段的写操作将导致段错误)。某些架构也允许代码段为可写,即允许修改程序。

     代码段指令根据程序设计流程依次执行,对于顺序指令,只会执行一次(每个进程);若有反复,则需使用跳转指令;若进行递归,则需要借助栈来实现。

     代码段指令中包括操作码和操作对象(或对象地址引用)。若操作对象是立即数(具体数值),将直接包含在代码中;若是局部数据,将在栈区分配空间,然后引用该数据地址;若位于BSS段和数据段,同样引用该数据地址。

     代码段最容易受优化措施影响。

 

8 保留区

     位于虚拟地址空间的最低部分,未赋予物理地址。任何对它的引用都是非法的,用于捕捉使用空指针和小整型值指针引用内存的异常情况。

     它并不是一个单一的内存区域,而是对地址空间中受到操作系统保护而禁止用户进程访问的地址区域的总称。大多数操作系统中,极小的地址通常都是不允许访问的,如NULL。C语言将无效指针赋值为0也是出于这种考虑,因为0地址上正常情况下不会存放有效的可访问数据。

     在32位X86架构的Linux系统中,用户进程可执行程序一般从虚拟地址空间0x08048000开始加载。该加载地址由ELF文件头决定,可通过自定义链接器脚本覆盖链接器默认配置,进而修改加载地址。0x08048000以下的地址空间通常由C动态链接库、动态加载器ld.so和内核VDSO(内核提供的虚拟共享库)等占用。通过使用mmap系统调用,可访问0x08048000以下的地址空间。

     通过cat /proc/self/maps命令查看加载表如下:

 

【扩展阅读】分段的好处

     进程运行过程中,代码指令根据流程依次执行,只需访问一次(当然跳转和递归可能使代码执行多次);而数据(数据段和BSS段)通常需要访问多次,因此单独开辟空间以方便访问和节约空间。具体解释如下:

     当程序被装载后,数据和指令分别映射到两个虚存区域。数据区对于进程而言可读写,而指令区对于进程只读。两区的权限可分别设置为可读写和只读。以防止程序指令被有意或无意地改写。

     现代CPU具有极为强大的缓存(Cache)体系,程序必须尽量提高缓存命中率。指令区和数据区的分离有利于提高程序的局部性。现代CPU一般数据缓存和指令缓存分离,故程序的指令和数据分开存放有利于提高CPU缓存命中率。

     当系统中运行多个该程序的副本时,其指令相同,故内存中只须保存一份该程序的指令部分。若系统中运行数百进程,通过共享指令将节省大量空间(尤其对于有动态链接的系统)。其他只读数据如程序里的图标、图片、文本等资源也可共享。而每个副本进程的数据区域不同,它们是进程私有的。

     此外,临时数据及需要再次使用的代码在运行时放入栈区中,生命周期短。全局数据和静态数据可能在整个程序执行过程中都需要访问,因此单独存储管理。堆区由用户自由分配,以便管理。



二:Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈

(转自:Linux 中的各种栈:进程栈 线程栈 内核栈 中断栈,不过我只转了他的部分内容,感兴趣可以去看)

 

Linux 中有几种栈?各种栈的内存位置?

 

介绍完栈的工作原理和用途作用后,我们回归到 Linux 内核上来。内核将栈分成四种:

  • 进程栈
  • 线程栈
  • 内核栈
  • 中断栈

一、进程栈

进程栈是属于用户态栈,和进程 虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

Linux 内核将这 4G 字节的空间分为两部分,将最高的 1G 字节(0xC0000000-0xFFFFFFFF)供内核使用,称为内核空间。而将较低的3G字节(0x00000000-0xBFFFFFFF)供各个进程使用,称为 用户空间。每个进程可以通过系统调用陷入内核态,因此内核空间是由所有进程共享的。虽然说内核和用户态进程占用了这么大地址空间,但是并不意味它们使用了这么多物理内存,仅表示它可以支配这么大的地址空间。它们是根据需要,将物理内存映射到虚拟地址空间中使用。

Linux虚拟地址空间

Linux 对进程地址空间有个标准布局,地址空间中由各个不同的内存段组成 (Memory Segment),主要的内存段如下: 
- 程序段 (Text Segment):可执行文件代码的内存映射 
- 数据段 (Data Segment):可执行文件的已初始化全局变量的内存映射 
- BSS段 (BSS Segment):未初始化的全局变量或者静态变量(用零页初始化) 
- 堆区 (Heap) : 存储动态内存分配,匿名的内存映射 
- 栈区 (Stack) : 进程用户空间栈,由编译器自动分配释放,存放函数的参数值、局部变量的值等 
- 映射段(Memory Mapping Segment):任何内存映射文件

Linux标准进程内存段布局

而上面进程虚拟地址空间中的栈区,正指的是我们所说的进程栈。进程栈的初始化大小是由编译器和链接器计算出来的,但是栈的实时大小并不是固定的,Linux 内核会根据入栈情况对栈区进行动态增长(其实也就是添加新的页表)。但是并不是说栈区可以无限增长,它也有最大限制 RLIMIT_STACK (一般为 8M),我们可以通过 ulimit 来查看或更改 RLIMIT_STACK 的值。

【扩展阅读】:如何确认进程栈的大小

我们要知道栈的大小,那必须得知道栈的起始地址和结束地址。栈起始地址 获取很简单,只需要嵌入汇编指令获取栈指针 esp 地址即可。栈结束地址 的获取有点麻烦,我们需要先利用递归函数把栈搞溢出了,然后再 GDB 中把栈溢出的时候把栈指针 esp 打印出来即可。代码如下:

 
/* file name: stacksize.c */
 
void *orig_stack_pointer;
 
void blow_stack() {
    blow_stack();
}
 
int main() {
    __asm__("movl %esp, orig_stack_pointer");
 
    blow_stack();
    return 0;
}
$ g++ -g stacksize.c -o ./stacksize
$ gdb ./stacksize
(gdb) r
Starting program: /home/home/misc-code/setrlimit
 
Program received signal SIGSEGV, Segmentation fault.
blow_stack () at setrlimit.c:4
4       blow_stack();
(gdb) print (void *)$esp
$1 = (void *) 0xffffffffff7ff000
(gdb) print (void *)orig_stack_pointer
$2 = (void *) 0xffffc800
(gdb) print 0xffffc800-0xff7ff000
$3 = 8378368    // Current Process Stack Size is 8M

 

上面对进程的地址空间有个比较全局的介绍,那我们看下 Linux 内核中是怎么体现上面内存布局的。内核使用内存描述符来表示进程的地址空间,该描述符表示着进程所有地址空间的信息。内存描述符由 mm_struct 结构体表示,下面给出内存描述符结构中各个域的描述,请大家结合前面的 进程内存段布局 图一起看:

struct mm_struct {
    struct vm_area_struct *mmap;           /* 内存区域链表 */
    struct rb_root mm_rb;                  /* VMA 形成的红黑树 */
    ...
    struct list_head mmlist;               /* 所有 mm_struct 形成的链表 */
    ...
    unsigned long total_vm;                /* 全部页面数目 */
    unsigned long locked_vm;               /* 上锁的页面数据 */
    unsigned long pinned_vm;               /* Refcount permanently increased */
    unsigned long shared_vm;               /* 共享页面数目 Shared pages (files) */
    unsigned long exec_vm;                 /* 可执行页面数目 VM_EXEC & ~VM_WRITE */
    unsigned long stack_vm;                /* 栈区页面数目 VM_GROWSUP/DOWN */
    unsigned long def_flags;
    unsigned long start_code, end_code, start_data, end_data;    /* 代码段、数据段 起始地址和结束地址 */
    unsigned long start_brk, brk, start_stack;                   /* 栈区 的起始地址,堆区 起始地址和结束地址 */
    unsigned long arg_start, arg_end, env_start, env_end;        /* 命令行参数 和 环境变量的 起始地址和结束地址 */
    ...
    /* Architecture-specific MM context */
    mm_context_t context;                  /* 体系结构特殊数据 */
 
    /* Must use atomic bitops to access the bits */
    unsigned long flags;                   /* 状态标志位 */
    ...
    /* Coredumping and NUMA and HugePage 相关结构体 */
};

 

mm_struct 内存段

【扩展阅读】:进程栈的动态增长实现

进程在运行的过程中,通过不断向栈区压入数据,当超出栈区容量时,就会耗尽栈所对应的内存区域,这将触发一个 缺页异常 (page fault)。通过异常陷入内核态后,异常会被内核的 expand_stack() 函数处理,进而调用 acct_stack_growth() 来检查是否还有合适的地方用于栈的增长。

如果栈的大小低于 RLIMIT_STACK(通常为8MB),那么一般情况下栈会被加长,程序继续执行,感觉不到发生了什么事情,这是一种将栈扩展到所需大小的常规机制。然而,如果达到了最大栈空间的大小,就会发生 栈溢出(stack overflow),进程将会收到内核发出的 段错误(segmentation fault) 信号。

动态栈增长是唯一一种访问未映射内存区域而被允许的情形,其他任何对未映射内存区域的访问都会触发页错误,从而导致段错误。一些被映射的区域是只读的,因此企图写这些区域也会导致段错误。

二、线程栈

从 Linux 内核的角度来说,其实它并没有线程的概念。Linux 把所有线程都当做进程来实现,它将线程和进程不加区分的统一到了 task_struct 中。线程仅仅被视为一个与其他进程共享某些资源的进程,而是否共享地址空间几乎是进程和 Linux 中所谓线程的唯一区别。线程创建的时候,加上了 CLONE_VM 标记,这样 线程的内存描述符 将直接指向 父进程的内存描述符。

  if (clone_flags & CLONE_VM) {
    /*
     * current 是父进程而 tsk 在 fork() 执行期间是共享子进程
     */
    atomic_inc(&current->mm->mm_users);
    tsk->mm = current->mm;
  }

 

虽然线程的地址空间和进程一样,但是对待其地址空间的 stack 还是有些区别的。对于 Linux 进程或者说主线程,其 stack 是在 fork 的时候生成的,实际上就是复制了父亲的 stack 空间地址,然后写时拷贝 (cow) 以及动态增长。然而对于主线程生成的子线程而言,其 stack 将不再是这样的了,而是事先固定下来的,使用 mmap 系统调用,它不带有 VM_STACK_FLAGS 标记。这个可以从 glibc 的nptl/allocatestack.c 中的 allocate_stack() 函数中看到:

mem = mmap (NULL, size, prot,
            MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);

 

由于线程的 mm->start_stack 栈地址和所属进程相同,所以线程栈的起始地址并没有存放在 task_struct 中,应该是使用 pthread_attr_t 中的 stackaddr 来初始化 task_struct->thread->sp(sp 指向 struct pt_regs 对象,该结构体用于保存用户进程或者线程的寄存器现场)。这些都不重要,重要的是,线程栈不能动态增长,一旦用尽就没了,这是和生成进程的 fork 不同的地方。由于线程栈是从进程的地址空间中 map 出来的一块内存区域,原则上是线程私有的。但是同一个进程的所有线程生成的时候浅拷贝生成者的 task_struct 的很多字段,其中包括所有的vma,如果愿意,其它线程也还是可以访问到的,于是一定要注意。

三、进程内核栈

在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈。进程内核栈在进程创建的时候,通过 slab 分配器从 thread_info_cache 缓存池中分配出来,其大小为 THREAD_SIZE,一般来说是一个页大小 4K;

union thread_union {                                   
        struct thread_info thread_info;                
        unsigned long stack[THREAD_SIZE/sizeof(long)];
};                                                     

 

thread_union 进程内核栈 和 task_struct 进程描述符有着紧密的联系。由于内核经常要访问 task_struct,高效获取当前进程的描述符是一件非常重要的事情。因此内核将进程内核栈的头部一段空间,用于存放 thread_info 结构体,而此结构体中则记录了对应进程的描述符,两者关系如下图(对应内核函数为 dup_task_struct()):

进程内核栈与进程描述符

有了上述关联结构后,内核可以先获取到栈顶指针 esp,然后通过 esp 来获取 thread_info。这里有一个小技巧,直接将 esp 的地址与上 ~(THREAD_SIZE - 1) 后即可直接获得 thread_info 的地址。由于 thread_union 结构体是从thread_info_cache 的 Slab 缓存池中申请出来的,而 thread_info_cache 在 kmem_cache_create 创建的时候,保证了地址是 THREAD_SIZE 对齐的。因此只需要对栈指针进行 THREAD_SIZE 对齐,即可获得 thread_union 的地址,也就获得了 thread_union 的地址。成功获取到 thread_info 后,直接取出它的 task 成员就成功得到了task_struct。其实上面这段描述,也就是 current 宏的实现方法:

register unsigned long current_stack_pointer asm ("sp");
 
static inline struct thread_info *current_thread_info(void)  
{                                                            
        return (struct thread_info *)                        
                (current_stack_pointer & ~(THREAD_SIZE - 1));
}                                                            
 
#define get_current() (current_thread_info()->task)
 
#define current get_current()                       

 

四、中断栈

进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。

X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在 arch/x86/kernel/irq_32.c 的irq_ctx_init() 函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用 __alloc_pages在低端内存区分配 2个物理页面,也就是8KB大小的空间。有趣的是,这个函数还会为 softirq 分配一个同样大小的独立堆栈。如此说来,softirq 将不会在 hardirq 的中断栈上执行,而是在自己的上下文中执行。

中断栈

而 ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。


 

Linux 为什么需要区分这些栈?

为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:

  1. 为什么需要单独的进程内核栈?

    • 所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;
  2. 为什么需要单独的线程栈?

    • Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1: 
      • 此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。
      • 如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
  3. 进程和线程是否共享一个内核栈?

    • No,线程和进程创建的时候都调用 dup_task_struct 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。
  4. 为什么需要单独中断栈?

    • 这个问题其实不对,ARM 架构就没有独立的中断栈。

 

三:自己的总结

 

上面的图都很好,但我觉得这张图更形象,32位进程栈大小是8M,理论上堆区最大大小约为2.9G,所以还是蛮大的。

从上面两篇文章,我知道的线程栈是使用mmap系统调用分配的空间,但是mmap分配的系统空间是什么呢?也就是上图中的mmap区域或者说共享的内存映射区域是什么呢?它的方向是向上生长还是向下生长的?

下面两幅图给出了答案:

 

图一:

图二:

所以,mmap其实和堆一样,实际上可以说他们都是动态内存分配,但是严格来说mmap区域并不属于堆区,反而和堆区会争用虚拟地址空间。

这里要提到一个很重要的概念,内存的延迟分配,只有在真正访问一个地址的时候才建立这个地址的物理映射,这是Linux内存管理的基本思想。Linux内核在用户申请内存的时候,只是给它分配了一个线性区(也就是虚拟内存),并没有分配实际物理内存;只有当用户使用这块内存的时候,内核才会分配具体的物理页面给用户,这时候才占用宝贵的物理内存。内核释放物理页面是通过释放先行区,找到其对应的物理页面,将其全部释放的过程。

这篇文章关于mmap生长方向说的也挺详细的: 进程地址空间的布局(整理)

 

最后还有一个mmap机制的源代码分析博客,我水平暂时不够,只能看懂意思,待日后阅读内核源码再来回顾一遍:Linux用户空间线程管理介绍之二:创建线程堆栈

おすすめ

転載: www.cnblogs.com/dongzhiquan/p/11415722.html