[apue] 程序環境那些事兒

2023-08-29 12:01:32

main 函數與程序終止

眾所周知,main 函數為 unix like 系統上可執行檔案的"入口",然而這個入口並不是指連結器設定的程式起始地址,後者通常是一個啟動例程,它從核心取得命令列引數和環境變數值後,為呼叫 main 函數做好安排。main 函數原型為:

int main (int argc, char *argv[]);

這是 ISO C 和 POSIX.1 指義的,當然還存在下面幾種不太標準的 main 原型:

void main (int argc, char *argv[]);
void main (void); 
int main (void);

不帶 argc & argv 引數的表示不打算接受命令列引數;void 返回值的表示不打算返回一個結束狀態。

程序的結束狀態碼與 main 的返回值關係如下:

  • main 宣告為 int 型別返回值
    • main 結束前執行了 return x 語句:x
    • main 結束前執行了無引數 return 語句:未定義 (warning: ‘return’ with no value, in function returning non-void)
    • main 結束前執行了 exit(x) 函數:x
    • main 結束前未執行以上語句:未定義 (warning: control reaches end of non-void function)
    • main 結束前未執行以上語句 [-std=c99]:0
  • main 宣告為 void 型別返回值 (warning: return type of ‘main’ is not ‘int’)
    • main 結束前執行了 return x 語句:未定義 (warning: ‘return’ with a value, in function returning void)
    • main 結束前執行了無引數 return 語句:未定義 
    • main 結束前執行了 exit(x) 函數:x
    • main 結束前未執行以上語句:未定義

測試機為 CentOS 7.9,gcc 版本 4.8.5,每一項的 warning 資訊就是基於這兩個版本測得。未定義的場景中,均返回 25 這個魔數。

開了 -std=c99 後大部分場景沒有改善,僅 main 返回值被宣告為 int 型別且在結束前沒有呼叫任何 return 或 exit 時 (第 1 項第 4 小項) 發生了顯著變化:從未定義變為返回 0。

程序有 8 種終止方式,其中 5 種為正常終止:

  • 從 main 返回 (無論是否有返回值)
  • 呼叫 exit
  • 呼叫 _exit 或 _Exit
  • 最後一個執行緒從其啟動例程返回
  • 最後一個執行緒呼叫 pthread_exit

另有 3 種為異常終止:

  • 呼叫 abort
  • 接到一個訊號並終止
  • 最後一個執行緒對取消請求做出響應

下面重點看一下 3 個 exit 函數:

#include <unistd.h>
void _exit(int status);

#include <stdlib.h>
void exit(int status);
void _Exit(int status);

宣告差別不大,_exit 與 _Exit 分別是 POSIX.1 與 ISO C 的標準,不過可以將它們視為等價,都直接進入核心。exit 則在它們的基礎上做了一些清理工作,主要包含以下幾個方面:

  • 清理執行緒區域性儲存 (TLS) 資訊
  • 按順序呼叫註冊的終止處理程式
  • 為所有標準 I/O 庫開啟的流呼叫 fclose 函數,這會 flush 緩衝的輸出資料

關於標準 I/O 庫,請參考之前寫的這篇文章:《[apue] 標準 I/O 庫那些事兒 》。

有了上面的鋪墊,可以這樣理解可執行程式的啟動例程與 main 之間的關係:

...
exit (main (argc, argv));

 即 main 的返回值是直接傳遞給 exit 的 status 引數作為程序結束狀態的。

atexit

關於終止處理程式,一般通過 atexit 函數進行註冊:

#include <stdlib.h>
int atexit(void (*function)(void));

這裡的 function 引數就是希望在 exit 時被呼叫的清理程式,關於終止處理程式,有下面幾點需要注意:

  • 呼叫次數有上限,通過 sysconf (_SC_ATEXIT_MAX) 查詢 (實測為 2147483647, 即 INT_MAX)
  • FILO,先註冊的後被呼叫,類似於堆疊,而非佇列
  • 呼叫次數等於註冊次數,同一清理程式可多次註冊,註冊幾次呼叫幾次
  • 執行 exec 函數族執行另一個程式的時候,自動清空 atexit 註冊的清理程式
  • 在清理程式中呼叫 exit "無效",如果呼叫 _exit 或 _Exit,會導致程式直接退出,後續清理程式不再被呼叫
  • 程序異常終止時清理程式不會被呼叫

下面這個例子驗證了呼叫次數與 FILO 特性:

#include "../apue.h"

void do_dirty_work ()
{
  printf ("doing dirty works!\n");
}

void bye ()
{
  printf ("bye, forks~\n");
}

void times ()
{
  static int counter = 32;
  printf ("times %d\n", counter--);
}

int main ()
{
  int ret = 0;
  ret = atexit (do_dirty_work);
  if (ret != 0)
    err_sys ("atexit");

  ret = atexit (bye);
  if (ret != 0)
    err_sys ("bye1");

  ret = atexit (bye);
  if (ret != 0)
    err_sys ("bye2");

  for (int i=0; i<32; i++)
  {
    ret = atexit (times);
    if (ret != 0)
      err_sys ("times");
  }

  printf ("main is done!\n");
  return 0;
}

執行它會有如下輸出:

$ ./atexit
main is done!
times 32
times 31
times 30
times 29
times 28
times 27
times 26
times 25
times 24
times 23
times 22
times 21
times 20
times 19
times 18
times 17
times 16
times 15
times 14
times 13
times 12
times 11
times 10
times 9
times 8
times 7
times 6
times 5
times 4
times 3
times 2
times 1
bye, forks~
bye, forks~
doing dirty works!

在 bye 中增加一些 exit 呼叫,觀察是否會有變化:

void bye ()
{
  printf ("bye, forks~\n");
  exit (2);  // no effect
  printf ("after exit (2)\n");
}

結果與之前完全一致,不過程序結束狀態變為了 2:

$ ./atexit
main is done!
times 32
...
times 1
bye, forks~
bye, forks~
doing dirty works!
> echo $?
2

可見 exit 並非沒有生效,一個合理的推斷是:第二次進入 exit 後,繼續處理之前沒處理完的清理程式,使得輸出看起來就像"沒生效"一樣。真正的 _exit 是被第二次進入 bye 的那個 exit 所呼叫,對程式稍加改動來看個明白:

int exit_status = 10;
void bye ()
{
  printf ("bye, forks~\n");
  exit (exit_status++);  // no effect
  printf ("after exit (%d)\n", exit_status-1);
}

為了便於區別,這裡給的初始值為 10,每呼叫一次 bye,exit_status 遞增 1,如果最後程序結束狀態碼為 10 就證明是第一次 exit 結束了程序,否則就是第二次。

$ ./atexit
main is done!
times 32
...
times 1
bye, forks~
bye, forks~
doing dirty works!
> echo $?
11

結論已經非常明顯,之前的猜測成立!如此就可以合理的推斷 exit 呼叫清理程式後,會將其從 FILO 結構中移除,從而避免再次呼叫,進而引發無限迴圈。

下面試試 _exit 的效果:

void bye ()
{
  printf ("bye, forks~\n");
  _exit (3);  // quit and no other atexit function running anymore !
  printf ("after _exit (3)\n");
}

改為 _exit 後輸出發生了截斷:

$ ./atexit
main is done!
times 32
...
times 1
bye, forks~
$ echo $?
3

進入 bye 處理程式後進程就終止了,後續的處理程式不再呼叫。檢查程序結束狀態碼為 3,正好是 _exit  的 status 引數。

將上面 exit 和 _exit 全都開啟後,_exit 反而不起作用了:

void bye ()
{
  printf ("bye, forks~\n");
  exit (2);  // no effect
  printf ("after exit (2)\n");
  _exit (3);  // no effect
  printf ("after _exit (3)\n");
}

經過上面的分析,想必讀者已經知道了答案,正確的做法是將 _exit 放在 exit 前面,這樣才能避免進入 exit 之後不再返回,從而被忽略。

最後再試一種場景,就是在處理器中繼續呼叫 atexit 註冊新的處理器,觀察新的處理器是否能被呼叫,參考下面這個例子:

#include "../apue.h"

void do_dirty_work ()
{
  printf ("doing dirty works!\n");
}

void bye ()
{
  printf ("bye, forks~\n");
  int ret = atexit (do_dirty_work);
  if (ret != 0)
      err_sys ("do_dirty_work");
}

int main ()
{
  int ret = 0;
  ret = atexit (bye);
  if (ret != 0)
    err_sys ("bye");

  printf ("main is done!\n");
  return 0;
}

先註冊處理器 bye,在其被回撥時再註冊處理器 do_dirty_work,結果是兩個處理器都能被回撥:

$ ./atexit_term
main is done!
bye, forks~
doing dirty works!

如果註冊的處理器形成迴圈會如何?參考下面的例子:

#include "../apue.h"

extern void bye ();
void do_dirty_work ()
{
  printf ("doing dirty works!\n");
  int ret = atexit (bye);
  if (ret != 0)
      err_sys ("bye2");
}

void bye ()
{
  printf ("bye, forks~\n");
  int ret = atexit (do_dirty_work);
  if (ret != 0)
      err_sys ("do_dirty_work");
}

int main ()
{
  int ret = 0;
  ret = atexit (bye);
  if (ret != 0)
    err_sys ("bye");

  printf ("main is done!\n");
  return 0;
}

在 do_dirty_work 中再次註冊 bye 作為處理器,重新編譯後執行,發現程式果然陷入了死迴圈:

$ ./atexit_term
main is done!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
...
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!
bye, forks~
doing dirty works!^C

直到輸入 Ctrl+C  才能退出,看起來 atexit 並不能檢測這種情況,需要程式設計師自己避免呼叫環的形成,好在這種場景並不多見。

命令列引數與環境變數

ISO C 與 POSIX.1 都要求 argv[argc] 引數為 NULL,因此下面兩種遍歷命令列引數的方式是等價的:

int i; 
for (i=0; i<argc; ++ i)
...
for (i=0; argv[i]!=NULL; ++i)
...

環境變數也有類似的約定。大多數 unix like 都支援以下的 main 宣告:

int main (int argc, char* argv[], char* envp[]);

將環境變數放在 main 第三個引數上,不過標準的 ISO C 和 POSIX.1 不支援,它們規定使用單獨的全域性變數存取環境變數:

extern char **environ; 

由於沒有類似 argc 的引數來說明參數列長度,環境變數的遍歷只能依賴結尾 NULL 的方式。

環境變數

環境變數的內容通常為以下形式:

name=value

name 通常大寫,不過這只是一種慣例,核心並不檢查環境變數內容,它的解釋完全取決於各個應用程式。例如 PATH 變數可以通過冒號指定多個路徑:

PATH=/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin

ISO C & POSIX.1 定義了一組處理環境變數的函數:

#include <stdlib.h>

[ISO C/POSIX.1] char *getenv(const char *name);
[POSIX.1]       int setenv(const char *name, const char *value, int overwrite);
[POSIX.1]       int unsetenv(const char *name);
[XSI]           int putenv(char *string);
[linux]         int clearenv(void);

它們屬於的標準在函數宣告前做了標識。其中:

  • getenv 根據 name 引數查詢變數並獲取 value 部分返回給使用者
  • setenv 根據 name 引數查詢變數
    • 變數不存在,直接設定新的變數 name=value
    • 變數存在
      • overwrite == 0:保留原有變數不變,返回 0
      • overwrite != 0:刪除原有變數,設定新的變數 name=value
  • unsetenv 刪除 name 的定義,不存在也返回 0
  • putenv 與 setenv 類似,不同的是
    • string 引數本身是 name=value 的組合體
    • 變數存在時刪除,沒有標誌位可以控制覆蓋行為
    • setenv 需要分配新的儲存區,因此不要求使用者為 name & value 引數分配儲存空間;putenv 則必需由使用者分配
  • clearenv 是 linux 平臺的專有擴充套件,用於清空環境變數

關於增刪改環境變數導致的空間變化問題,下一節詳細說明。

最後需要說明的是,對環境變數的更改只對當前程序及之後啟動的子程序生效,不對父程序及之前啟動的子程序產生影響。

儲存空間佈局

直接上圖:

 

 

 

 

 

 

 

 

 

 

 

 

 

 

上面是一種典型的記憶體排布,只是舉個例子,並不代表所有平臺和架構都以此這種方式安排其儲存空間,圖中的記憶體地址更是以 Linux x86 處理器為例的。

其中:

  • 程式碼段也稱正文段,儲存可執行程式的機器指令部分,一般是唯讀、共用的
  • 初始化資料段也稱為資料段,包含了程式中明確賦初值的全域性或靜態變數,以上兩段從程式檔案中讀入
  • 非初始化資料段也稱為 bss (block started by symbol),沒有初始化的全域性或靜態變數包含在這個段中。由於不需要儲存初始化值,程式檔案中甚至沒有這個段,它是由 exec 初始化為 0 的
  • 堆,動態儲存分配區域,由低地址向高地址增長
  • 棧,自動變數及函數呼叫所需的資訊存放在此段。一個函數呼叫範例中的變數不會影響另一個函數呼叫範例中的變數。由高地址向低地址增長
  • 命令列引數與環境變數存放在棧底以上的空間

其中除堆和棧外,其它段都變化很小或不變,所以設定堆和棧對向增長是非常聰明的做法。當向下增長的棧與向上增長的堆相遇時,程序的地址空間就用光了。

下面的程式驗證了 C 程式的記憶體佈局:

#include "../apue.h"

int data1 = 2;
int data2 = 3;
int data3;
int data4;

int main (int argc, char *argv[])
{
  char buf1[1024] = { 0 };
  char buf2[1024] = { 0 };
  char *buf3 = malloc(1024);
  char *buf4 = malloc(1024);
  printf ("onstack %p, %p\n",
    buf1,
    buf2);

  extern char ** environ;
  printf ("env %p\n", environ);
  printf ("arg %p\n", argv);

  printf ("onheap %p, %p\n",
    buf3,
    buf4);

  free (buf3);
  free (buf4);

  printf ("on bss %p, %p\n",
    &data3,
    &data4);

  printf ("on init %p, %p\n",
    &data1,
    &data2);

  printf ("on code %p\n", main);
  return 0;
}

在 linux 上編譯執行:

$ ./layout
onstack 0x7ffe31b752a0, 0x7ffe31b74ea0
env 0x7ffe31b757b8
arg 0x7ffe31b757a8
onheap 0x1984010, 0x1984420
on bss 0x6066b8, 0x6066bc
on init 0x606224, 0x606228
on code 0x40179d

雖然具體地址和書上講的有出入,但是總體佈局確實是 code -> init -> bss -> heap -> stack -> env / arg 的順序沒錯。

size

size 命令用於報告可執行檔案的 code/data/bss 段的長度:

$ size ./layout ./layout_s /bin/sh
   text	   data	    bss	    dec	    hex	filename
  20073	   2152	     80	  22305	   5721	./layout
 802535	   7292	  11120	 820947	  c86d3	./layout_s
 905942	  36000	  22920	 964862	  eb8fe	/bin/sh

dec/hex 列分別是三者加總後的十進位制與十六進位制長度。範例中 layout_s 是靜態連結版本,可見使用共用庫的動態連結在各個段的尺寸上都有明顯縮減。

堆分配

棧的增長主要依賴函數呼叫層次的增加;堆的增長主要依賴以下記憶體分配函數:

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);

void free(void *ptr);

其中:

  • malloc 分配 size 長度的儲存區
  • calloc 分配 nmemb*size 長度的儲存區
  • realloc 可更改以前分配區到 size 長度 (增加或減小)

對於新增的儲存區

  • calloc 初始值為 0
  • malloc 和 realloc 初始值不確定

對於 realloc,新舊地址之間的關係:

  • 當儲存區減小時,新舊地址保持一致
  • 當儲存區增加時
    • 原儲存區後有足夠的空間時,新舊地址保持一致
    • 原儲存區後沒有足夠的空間,新舊地址不同,會先分配足夠大的空間,複製資料,再釋放原儲存區

realloc(NULL, size) 等價於 malloc(size)。

sbrk

這些分配例程通常用 sbrk 系統呼叫來擴充程序的堆:

#include <unistd.h>
void *sbrk(intptr_t increment);

這通常是通過呼叫 program break 的位置來實現的,參考 man 這段說明:

DESCRIPTION
       brk()  and  sbrk() change the location of the program break, which defines the end of the process's data segment (i.e., the pro‐
       gram break is the first location after the end of the uninitialized data segment).  Increasing the program break has the  effect
       of allocating memory to the process; decreasing the break deallocates memory.

program break 就是 bss 段的結尾,參考上圖應該就是堆底。

sbrk 也可以減小堆大小,不過大多數 malloc 和 free 的實現都不減小程序的儲存空間,釋放的空間可供以後再分配,但通常將它們保持在 malloc 池中而不返回給核心。

環境變數空間的變更

有上面內容的鋪墊,就可以回顧下上一節中增刪改環境變數對儲存空間的影響了:

  • 刪除環境變數,之後的變數前移填補刪除後的空位
  • 修改環境變數
    • 新值長度小於等於舊值,在原字串空間中寫入新值
    • 新值長度大於舊值,在堆上分配新字串空間並賦值,更新環境變數表中的指標使之指向新分配的字串
  • 新增環境變數
    • 第一次新增環境變數,在堆上分配新的環境變數表,將原來的環境變數"複製"到新分配的環境變數表中,然後把新增的環境變數字串放在表尾,再新增一個空指標放在最後,最後使用 environ 變數指向新分配的環境變數表,基本上就是將環境變數從棧頂搬到了堆中,不過大多數環境變數仍指向棧頂中分配的字串而已
    • 非第一次新增,使用 realloc 重新分配 environ 變數,以容納新增加的環境變數

環境變數空間改變如此複雜,主要是因為它的大小被棧頂限制死了,沒有辦法擴容,當增加環境變數數目時,只能從棧頂搬到堆中。

下面的程式演示了這一過程:

#include "../apue.h"

void print_envs()
{
  extern char **environ;
  printf ("base %p\n", environ);
  for (int i=0; environ && environ[i] != 0; ++ i)
  {
    printf ("[%p]  %s\n", environ[i], environ[i]);
  }
}

int
main (int argc, char *argv[])
{
  print_envs ();

  setenv ("HOME", "ME", 1);
  printf ("\nafter set HOME:\n");
  print_envs ();

  setenv ("LOGNAME", "this is a very very long user name", 1);
  printf ("\nafter set LOGNAME:\n");
  print_envs ();

  unsetenv ("PATH");
  printf ("\nafter unset PATH:\n");
  print_envs ();

  setenv ("DISAPPEAR", "not exist before", 0);
  printf ("\nafter set DISAPPEAR:\n");
  print_envs ();

  setenv ("ADDISION", "addision adding", 0);
  printf ("\nafter set ADDISION:\n");
  print_envs ();

  return 0;
}

程式比較簡單,依次執行以下操作:add HOME -> add LOGNAME -> remove PATH -> add DISAPPEAR -> add ADDISION,每次操作後都列印整個環境變數表,以觀察 environ 和各個環境變數的變化:

$ ./envpos
base 0x7fff15e16468
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17c5f]  PATH=/home/yunh/.BCloud/bin:/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/home/users/yunhai01/tools/bin:/home/users/yunhai01/project/android-ndk-r20:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x7fff15e17e58]  HOME=/home/users/yunhai01
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x7fff15e17e8a]  LOGNAME=yunhai01
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos

啟動後先列印整個環境變數表,大概有 30 個環境變數。

after set HOME:
base 0x7fff15e16468
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17c5f]  PATH=/home/yunh/.BCloud/bin:/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/home/users/yunhai01/tools/bin:/home/users/yunhai01/project/android-ndk-r20:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x16fc010]  HOME=ME
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x7fff15e17e8a]  LOGNAME=yunhai01
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos

設定 HOME 變數,雖然新值長度小於舊值,這裡仍然為新值在堆上分配了空間,看起來 linux 上的實現偷懶了。

after set LOGNAME:
base 0x7fff15e16468
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17c5f]  PATH=/home/yunh/.BCloud/bin:/home/users/yunhai01/.local/bin:/home/users/yunhai01/bin:/home/users/yunhai01/tools/bin:/home/users/yunhai01/project/android-ndk-r20:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/bin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin:/usr/local/sbin:/usr/sbin:/opt/bin:/home/opt/bin:/home/users/yunhai01/tools/node-v14.17.0-linux-x64/bin
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x16fc010]  HOME=ME
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x16fc060]  LOGNAME=this is a very very long user name
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos

設定 LOGNAME 變數,新值長度大於舊值,這裡沒有懸念的在堆上進行了分配。

after unset PATH:
base 0x7fff15e16468
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x16fc010]  HOME=ME
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x16fc060]  LOGNAME=this is a very very long user name
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos

刪除 PATH 變數,這一步主要驗證再次新增環境變數時,會不會重複利用已刪除的空位,到目前為止 environ 指標地址 (0x7fff15e16468) 沒有發生變化,仍位於棧頂之上。

after set DISAPPEAR:
base 0x16fc0d0
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x16fc010]  HOME=ME
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x16fc060]  LOGNAME=this is a very very long user name
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos
[0x16fc1e0]  DISAPPEAR=not exist before

增加 DISAPPEAR 變數,沒有懸念的在堆上分配了空間,最大的變化在於 environ 指標變了!從棧頂之上移動了到了堆中 (0x16fc0d0),看起來之前刪除 PATH 變數騰空的位置沒有利用上。

after set ADDISION:
base 0x16fc0d0
[0x7fff15e17488]  XDG_SESSION_ID=318004
[0x7fff15e1749e]  HOSTNAME=yunhai.bcc-bdbl.baidu.com
[0x7fff15e174c1]  SHELL=/bin/bash
[0x7fff15e174d1]  TERM=xterm-256color
[0x7fff15e174e5]  HISTSIZE=1000
[0x7fff15e174f3]  SSH_CLIENT=172.31.23.41 52661 22
[0x7fff15e17514]  ANDROID_SDK_ROOT=/home/users/yunhai01/android_sdk
[0x7fff15e17546]  QTDIR=/usr/lib64/qt-3.3
[0x7fff15e1755e]  QTINC=/usr/lib64/qt-3.3/include
[0x7fff15e1757e]  SSH_TTY=/dev/pts/6
[0x7fff15e17591]  USER=yunhai01
[0x7fff15e17c57]  TMOUT=0
[0x7fff15e17de4]  MAIL=/var/spool/mail/yunhai01
[0x7fff15e17e02]  PWD=/home/users/yunhai01/code/apue/07.chapter
[0x7fff15e17e30]  LANG=en_US.UTF-8
[0x7fff15e17e41]  HISTCONTROL=ignoredups
[0x16fc010]  HOME=ME
[0x7fff15e17e72]  SHLVL=2
[0x7fff15e17e7a]  GTAGSFORCECPP=1
[0x16fc060]  LOGNAME=this is a very very long user name
[0x7fff15e17e9b]  QTLIB=/usr/lib64/qt-3.3/lib
[0x7fff15e17eb7]  SSH_CONNECTION=172.31.23.41 52661 10.138.62.136 22
[0x7fff15e17eea]  LESSOPEN=||/usr/bin/lesspipe.sh %s
[0x7fff15e17f0d]  ANDROID_NDK_HOME=/home/users/yunhai01/project/android-ndk-r20
[0x7fff15e17f4b]  XDG_RUNTIME_DIR=/run/user/383278
[0x7fff15e17f6c]  LLVM_HOME=/home/users/yunhai01/project/ndk-llvm
[0x7fff15e17f9c]  HISTTIMEFORMAT=%Y-%m-%d %H:%M:%S
[0x7fff15e17fbe]  OLDPWD=/home/users/yunhai01/code/apue
[0x7fff15e17fe4]  _=./envpos
[0x16fc1e0]  DISAPPEAR=not exist before
[0x16fc240]  ADDISION=addision adding

增加 ADDISION 變數,仍然在堆上分配空間,而且 environ 指標地址 (0x16fc0d0) 沒有發生變化,看起來仍有足夠的空間讓 realloc 分配。

指令跳轉 (setjmp & longjmp)

說到指令跳轉,第一印象就是 goto。由於程式的執行本質是一條條機器程式碼的執行,有些指令本身自帶跳轉屬性,像函數呼叫 (call)、函數返回 (return) 、switch-case 都是某種形式的指令跳轉,goto 則將這種能力公佈給了開發者,然而下面的兩個限制導致它在實際應用上的推廣受阻:

  • 只能在函數內部跳轉,無法跨越函數棧
  • 濫用 goto 導致程式碼邏輯不清晰、後期維護困難

setjmp & longjmp 完美的解決了上述 goto 的缺點,支援跨函數棧的跳轉、且使用上更不易被濫用,也被稱為非區域性 goto。

它的跳轉邏輯和現代 C++ 的異常機制已經非常相似了,區別是後者加入了對棧上物件解構函式的自動呼叫等更多的內容。

先來看函數原型:

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

書上給的例子就不錯,這裡找到另外一個更簡單的例子:

#include <setjmp.h>
#include <stdio.h>
 
static jmp_buf g_jmpbuf;
 
void exception_jmp()
{
    printf ("throw_exception_jmp start.\n");
    longjmp(g_jmpbuf, 1);
    printf ("throw_exception_jmp end.\n");
}
 
void call_jmp()
{
    exception_jmp();
}
 
int main(int argc, char *argv[])
{
    /* using setjmp and longjmp */
    if (setjmp(g_jmpbuf) == 0)
    {
        call_jmp();
    }
    else
    {
        printf ("catch exception via setimp-longjmp.\n");
    }
 
    return 0;
}

編譯執行這個 demo,輸出如下:

throw_exception_jmp start.
catch exception via setimp-longjmp.

對於沒有接觸過非區域性 goto 的人來說還是比較直觀的。

compiler explorer

這裡推薦一個線上的 c++ 編譯器 compiler explorer,對於沒有 Linux 環境的人來說非常友好,下面是編譯執行上述 demo 的過程:

可以看到這個工具非常強大,可以:

  • 選擇編譯語言
  • 選擇編譯器
  • 選擇編譯模式 (是否開啟 Vim)
  • 修改編譯連結選項
  • 檢視反組合
  • 檢視預處理結果
  • 檢視執行輸出
  • 更改視窗佈局

有興趣的讀者可以自行探索。

迴歸程式碼,注意 longjmp 第二個引數,這個不是隨便給的,它將作為跳轉後 setjmp 的返回值,要與初始化時返回的 0 有一些區別,另外允許任意多個 longjmp 跳向同一個 jmp_buf 範例,這種情況下,通過指定不同的 val 引數也能區別出跳轉源,是不是想的很周到?

longjmp 跳轉時,當前所在的函數棧到 setjmp 之間的棧將被回收,依附之上的自動變數將不復存在,但是跳轉目的地所在的棧幀還是存在的,此外還有不在當前棧上的全域性變數、靜態變數等等也是存在的。

變數值回退

雖然沒讀過 setjmp & longjmp 的原始碼,但原理應該就是儲存和恢復函數棧 (各種暫存器),那這些未被複原的變數,是恢復到 setjmp 時的狀態,還是保留最後的狀態呢?對上面的例子稍加修改來進行一番考察:

#include <setjmp.h>
#include <stdio.h>
 
static jmp_buf g_jmpbuf;
static int globval; 
 
void exception_jmp()
{
    printf ("throw_exception_jmp start.\n");
    longjmp(g_jmpbuf, 1);
    printf ("throw_exception_jmp end.\n");
}
 
void call_jmp(int i, int j, int k, int l)
{
    printf ("in call_jmp (): \n"
        "globval = %d,\n"
        "autoval = %d,\n"
        "regival = %d,\n"
        "volaval = %d,\n"
        "statval = %d\n\n", 
        globval, 
        i, 
        j, 
        k, 
        l); 
    exception_jmp();
}
 
int main(int argc, char *argv[])
{
    int autoval; 
    register int regival; 
    volatile int volaval; 
    static int statval; 
    globval = 1; 
    autoval = 2; 
    regival = 3; 
    volaval = 4; 
    statval = 5; 
    /* using setjmp and longjmp */
    if (setjmp(g_jmpbuf) == 0)
    {
        /*
        * Change variables after setjmp, but before longjmp
        */
        globval = 95;  
        autoval = 96; 
        regival = 97; 
        volaval = 98; 
        statval = 99; 
        call_jmp(autoval, regival, volaval, statval);
    }
    else
    {
        printf ("catch exception via setimp-longjmp.\n");
        printf ("in main (): \n"
        "globval = %d,\n"
        "autoval = %d,\n"
        "regival = %d,\n"
        "volaval = %d,\n"
        "statval = %d\n\n", 
        globval, 
        autoval, 
        regival, 
        volaval, 
        statval); 
    }
 
    return 0;
}

在原來的基礎上新增了幾種型別的變數:

  • globaval:全域性變數
  • autoval:main 棧上自動變數
  • regival:main 棧上暫存器變數
  • valaval:main 棧上易失變數
  • statval:main 棧上靜態變數

並分別在 call_jmp 內部和 longjmp 後 (第二次從 setjmp 返回) 時列印它們的值:

$  ./jumpvar
in call_jmp ():
globval = 95,
autoval = 96,
regival = 97,
volaval = 98,
statval = 99

throw_exception_jmp start.
catch exception via setimp-longjmp.
in main ():
globval = 95,
autoval = 96,
regival = 97,
volaval = 98,
statval = 99

在沒開優化的情況下,各個變數都最新的狀態,沒有發生值回退現象,新增 -O 編譯選項: 

...
jumpvar: jumpvar.o apue.o
	gcc -Wall -g $^ -o $@

jumpvar.o: jumpvar.c ../apue.h
	gcc -Wall -g -c $< -o $@ -std=c99
    
jumpvar_opt: jumpvar_opt.o apue.o
	gcc -Wall -g $^ -o $@

jumpvar_opt.o: jumpvar.c ../apue.h
	gcc -Wall -g -c $< -o $@ -std=c99 -O
...

再次執行:

$ ./jumpvar
in call_jmp ():
globval = 95,
autoval = 96,
regival = 97,
volaval = 98,
statval = 99

throw_exception_jmp start.
catch exception via setimp-longjmp.
in main ():
globval = 95,
autoval = 2,
regival = 3,
volaval = 98,
statval = 99

這次 autoval 和 regival 的值發生了回退。加優化選項後為提高程式執行效率,這些變數的值從記憶體提升到了暫存器,從而導致恢復 main 堆疊時被一併恢復了。這裡有幾個值得注意的點:

  • 宣告為 register 的 regival 在未開啟優化前編譯器並沒有遵循指令將其放置在暫存器,再一次證實了 register 關鍵字只是建議而非強制
  • 開優化後,棧上的自動變數也被放置在了暫存器中
  • 即使開優化,volatile 關鍵字宣告的變數也不存在於暫存器中

所以最終的結論是:如果不想棧上的變數受 setjmp & longjmp 影響發生值回退,最好將它們宣告為 volatile

這裡出於好奇,也使用 compiler explorer 執行了一把,結果沒加優化的第一次執行輸出就不一樣:

 

主要區別在於 regival 會回退,將 compiler explorer 中的 gcc 版本降到和我本地一樣的 4.8.5 後輸出就一致了,因此主要區別在於編譯器版本。

這一方面展示了 compiler explorer 強大的切換編譯器版本的能力,另一方面也顯示高版本 gcc 版本器傾向於"相信"使用者提供的 register 關鍵字。

最後在 compiler explorer 中增加 -O 編譯器引數,會得到和之前一樣的結果:

資源限制 (getrlimit & setrlimit)

程序對系統資源的請求並不是沒有上限的,使用 getrlimit 和 setrlimit 查詢或更改它們:

#include <sys/resource.h>

// struct rlimit {
//     rlim_t rlim_cur;  /* Soft limit */
//     rlim_t rlim_max;  /* Hard limit (ceiling for rlim_cur) */
// };

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

resource 指定了限制的型別,rlim 則包含了資源限制的資訊,主要包含兩個成員:

  • rlim_cur:軟限制值,當前生效的限制值
  • rlim_max:硬限制值,大於等於軟限制值,軟限制值的提升上限
    • 任何使用者可以降低硬限制值,只有超級使用者可以提升硬限制值
    • 每次降低的硬限制值必需大於等於軟限制值

RLIM_INFINITY 表示無限量:

# define RLIM_INFINITY ((__rlim_t) -1)

可以指定的資源限制型別及在本地環境上的軟硬限制值列表如下:

resource  含義 軟限制 硬限制
RLIMIT_AS 程序可用儲存區的最大位元組長度,會影響 sbrk & mmap 函數,非 Linux 平臺也命名為 RLIMIT_VMEM infinite infinite
RLIMIT_CORE 崩潰轉儲檔案的最大位元組數,0 表示阻止建立,生成的 core 檔案大於限制值時會被截斷 0 infinite
RLIMIT_CPU CPU 的最大量值,單位秒,超過軟限制時,向程序傳送 SIGXCPU 訊號;超過硬限制時,向程序傳送 SIGKILL 訊號 infinite infinite
RLIMIT_DATA 資料段的最大位元組長度,是 init + bss + heap 的總長度,即除棧、環境變數、命令列引數外的記憶體總長度 infinite infinite
RLIMIT_FSIZE 可以建立的檔案的最大位元組長度,當超過軟限制時,向程序傳送 SIGXFSZ 訊號,若訊號被捕獲,則 write 返回 EBIG 錯誤 infinite infinite
RLIMIT_LOCKS 一個程序可持有的檔案鎖的最大數量 (僅 Linux 支援) infinite infinite
RLMIT_MEMLOCK 一個程序使用 mlock 能夠鎖定在記憶體中的最大位元組長度,當超過軟限制時,mlock 返回 ENOMEM 錯誤 65536 65536
RLIMIT_NOFILE 每個程序能開啟的最大檔案數,當超過軟限制時,open 返回 EMFILE 錯誤,更改軟限制會影響 sysconf (_SC_OPEN_MAX) 返回的值 1024 4096
RLIMIT_NPROC 每個實際使用者 ID 可擁有的最大程序數,當超過軟限制時,fork 返回 EAGAGIN 錯誤,更改軟限制會影響 sysconf (_SC_CHILD_MAX) 返回的值 4096 63459
RLIMIT_RSS 最大駐記憶體集的位元組長度 (resident set size in bytes),如果實體記憶體不足,核心將從程序處取回超過 RSS 的部分 infinite infinite
RLMIT_SBSIZE 使用者任意給定時刻可以佔用的通訊端緩衝區的最大位元組長度 (僅 FreeBSD 支援) n/a n/a
RLMIT_STACK 棧的最大位元組長度 8388608 infinite

限制值獲取的 demo 就直接用書上提供的,感興趣的讀者可以檢視原書,這裡就不再列出了。

程序的資源限制通常是在系統初始化時由程序 0 建立的,然後由每個後續程序繼承,對於其中非 RLIM_INFINITY 限制值的,程序終其一生無法提升限制值 (超級使用者程序除外)。

shell 也提供相應的內建命令 (一般為 ulimit) 來修改預設的限制值,在啟動命令前設定各種限制值才能在新程序中生效,在 CentOS 上使用 -a 選項可以檢視所有的限制值:

$ ulimit -a
core file size          (blocks, -c) 0
data seg size           (kbytes, -d) unlimited
scheduling priority             (-e) 0
file size               (blocks, -f) unlimited
pending signals                 (-i) 63459
max locked memory       (kbytes, -l) 64
max memory size         (kbytes, -m) unlimited
open files                      (-n) 1024
pipe size            (512 bytes, -p) 8
POSIX message queues     (bytes, -q) 819200
real-time priority              (-r) 0
stack size              (kbytes, -s) 8192
cpu time               (seconds, -t) unlimited
max user processes              (-u) 4096
virtual memory          (kbytes, -v) unlimited
file locks                      (-x) unlimited

大部分限制值與呼叫介面的 demo 列印的一致,但是單位可能和介面不同,使用時需要注意。

下面大體按上表的順序對各個限制型別分別施加資源限制,觀察程式的行為是否和預期一致。

RLIMIT_AS (RLIMIT_VMEM)

#include "../apue.h"
#include <sys/resource.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1024 * 1024;
  lmt.rlim_max = RLIM_INFINITY;
  ret = setrlimit (RLIMIT_AS, &lmt);
  if (ret == -1)
    err_sys ("set rlimit as failed");
    
  char *ptr = malloc (1024 * 1024);
  if (ptr == NULL)
    err_sys ("malloc failed");
    
  printf ("alloc 1 MB success!\n");
  free (ptr);
}

設定程序記憶體軟限制 1M ,然後分配 1M 的堆記憶體:

$ ./lmt_as
malloc failed: Cannot allocate memory

果然記憶體超限失敗了。

RLIMIT_DATA

例子同上,只需將 RLIMIT_AS 修改為 RLIMIT_DATA 即可,輸出也一致。

畢竟 RLIMIT_DATA 所包含的三個段 (init / bss / heap) 中有堆記憶體,通過分配堆記憶體肯定是會擠佔這部分限制的。

RLIMIT_CORE

#include "../apue.h"
#include <sys/resource.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1024;
  lmt.rlim_max = 102400;
  ret = setrlimit (RLIMIT_CORE, &lmt);
  if (ret == -1)
    err_sys ("set rlimit core failed");
    
   char *ptr = 0;
   *ptr = 1;
   return 0;
}

設定崩潰轉儲檔案軟限制為 1K,在遭遇空指標崩潰後,能正常生成 core 檔案:

$ ./lmt_core
Segmentation fault (core dumped)
$ ls -l core.22482
-rw------- 1 yunhai01 DOORGOD 1024 Aug 27 21:59 core.22482

檔案大小未超過 1K。當然前提是需要通過 ulimit -c 指定一個大於 1K 的數值 (非 root 使用者),否則在 setrlimit 時會報錯:

$ ./lmt_core
set rlimit core failed: Operation not permitted

另外生成的 core 檔案應該是被截斷了,通過 gdb 載入過程紀錄檔可以判斷:

$ gdb --core=./core.22482
GNU gdb (GDB) Red Hat Enterprise Linux 7.6.1-120.el7
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
"/home/users/yunhai01/code/apue/07.chapter/./core.22482" is not a core dump: File truncated
(gdb)

因此也是不能用的。最後補充一點,設定 core 檔案的最小尺寸必需大於 1,否則不會生成任何 core 檔案。

RLIMIT_CPU

#include "../apue.h"
#include <sys/resource.h>

void sigxcpu_handler (int sig)
{
  printf ("ate SIGXCPU...\n");
  signal (SIGXCPU, sigxcpu_handler);
}

int main (int argc, char *argv[])
{
  int ret = 0;
  signal (SIGXCPU, sigxcpu_handler);
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1;
  lmt.rlim_max = 5;
  ret = setrlimit (RLIMIT_CPU, &lmt);
  if (ret == -1)
    err_sys ("set rlimit cpu failed");

  int i = 1, j = 1;
  while (1)
  {
    i *= j++;
  }
  return 0;
}

設定了 CPU 軟限制為 1 秒,硬限制為 5 秒,且捕獲 SIGXCPU 訊號,之後進入一個計算死迴圈,不停消耗 CPU 時間:

$ ./lmt_cpu
ate SIGXCPU...
ate SIGXCPU...
ate SIGXCPU...
ate SIGXCPU...
Killed

紀錄檔幾乎是一秒輸出一行,第 5 秒時達到 CPU 硬限制,程序被強制殺死。

RLIMIT_FSIZE

#include "../apue.h"
#include <sys/resource.h>
#include <sys/file.h>

void sigxfsz_handler (int sig)
{
  printf ("ate SIGXFSZ...\n");
  signal (SIGXFSZ, sigxfsz_handler);
}

int main (int argc, char *argv[])
{
  int ret = 0;
  signal (SIGXFSZ, sigxfsz_handler);

  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1024;
  lmt.rlim_max = RLIM_INFINITY;
  ret = setrlimit (RLIMIT_FSIZE, &lmt);
  if (ret == -1)
    err_sys ("set rlimit fsize failed");

  int fd = open ("core.tmp", O_RDWR | O_CREAT, 0644);
  if (fd == -1)
    err_sys ("open file failed");

  char buf[16];
  int total = 0;
  while (1)
  {
    ret = write (fd, buf, 16);
    if (ret == -1)
      err_sys ("write failed");

    total += ret;
    printf ("write %d, total %d\n", ret, total);
  }

  close (fd);
  return 0;
}

設定最大寫入檔案位元組數軟限制 1K,捕獲 SIGXFZE 訊號後開啟 core.tmp 檔案不停寫入,每次寫入 32 位元組直到失敗:

$ ./lmt_fsize
write 32, total 32
write 32, total 64
write 32, total 96
write 32, total 128
write 32, total 160
write 32, total 192
write 32, total 224
write 32, total 256
write 32, total 288
write 32, total 320
write 32, total 352
write 32, total 384
write 32, total 416
write 32, total 448
write 32, total 480
write 32, total 512
write 32, total 544
write 32, total 576
write 32, total 608
write 32, total 640
write 32, total 672
write 32, total 704
write 32, total 736
write 32, total 768
write 32, total 800
write 32, total 832
write 32, total 864
write 32, total 896
write 32, total 928
write 32, total 960
write 32, total 992
write 32, total 1024
ate SIGXFSZ...
write failed: File too large

寫滿 1K 後收到了 SIGXFSZ 訊號,捕獲訊號避免了程序 abort,不過 write 返回了 EBIG 錯誤。

這裡需要注意不應使用 fopen/fclose/fwrite 來進行測試,因標準 I/O 庫的快取機制,導致寫入的位元組數大於實際落盤的位元組數,從而得不到準確的限制值。

RLMIT_MEMLOCK

#include "../apue.h"
#include <errno.h>
#include <sys/resource.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 32 * 1024;
  lmt.rlim_max = 64 * 1024;
  ret = setrlimit (RLIMIT_MEMLOCK, &lmt);
  if (ret == -1)
    err_sys ("set rlimit memlock failed");
  
  char *ptr = malloc (32 * 1024);
  if (ptr == NULL)
    err_sys ("malloc failed");

  printf ("alloc 32K success!\n");
#define BLOCK_NUM 32
  for (int i=0; i<BLOCK_NUM; ++ i)
  {
      ret = mlock (ptr + 1024 * i, 1024);
      if (ret == -1)
          err_sys ("mlock failed, %d", errno);

      printf ("lock 1 KB success!\n");
  }
  for (int i=0; i<BLOCK_NUM; ++ i)
  {
      ret = munlock (ptr + 1024 * i, 1024);
      if (ret == -1)
          err_sys ("munlock failed, %d", errno);

      printf ("unlock 1 KB success!\n");
  }
  free (ptr);
  return 0;
}

程式設定記憶體鎖總長度軟限制為 32K,硬限制 64K,分配 32K 記憶體後,在該記憶體上施加 32 個 1K 的範圍鎖:

$ ./lmt_memlock
alloc 32K success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
mlock failed, 12: Cannot allocate memory

在達到 27K 左右時 mlock 報錯了,沒有達到 32K 的上限可能和 glibc 內部也有一些 mlock 呼叫有關。

如果將 1K 的塊調整為 16 個,總的鎖長度調整為 16K,再次執行 demo:

$ ./lmt_memlock
alloc 32K success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
lock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!
unlock 1 KB success!

這回能成功。最後需要注意的是,預設 memlock 的上限是 64K,如果需要測試大於 64K 的場景,需要提前使用 ulimit 提升該限制。

RLIMIT_NOFILE

#include "../apue.h"
#include <sys/resource.h>
#include <sys/file.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 5;
  lmt.rlim_max = 10;
  ret = setrlimit (RLIMIT_NOFILE, &lmt);
  if (ret == -1)
    err_sys ("set rlimit nofile failed");

  ret = sysconf (_SC_OPEN_MAX);
  printf ("sysconf (_SC_OPEN_MAX) = %d\n", ret);

#define FD_SIZE 10
  char filename[256] = { 0 };
  int fds[FD_SIZE] = { 0 };
  for (int i=0; i<FD_SIZE; ++ i)
  {
    sprintf (filename, "core.%02d.lck", i+1);
    fds[i] = open (filename, O_RDWR | O_CREAT, 0644);
    if (fds[i] == -1)
      err_sys ("open file failed");

    printf ("open file %s\n", filename);
  }

  for (int i=0; i<FD_SIZE; ++ i)
  {
    if (fds[i] != 0)
    {
      close (fds[i]);
    }
  }
  return 0;
}

設定開啟檔案數軟限制為 5,硬限制為 10,之後建立 10 個檔案 (core.xx.lck):

$ ./lmt_nofile
sysconf (_SC_OPEN_MAX) = 5
open file core.01.lck
open file core.02.lck
open file failed: Too many open files

在開啟第 3 個檔案時失敗,open 返回 EMFILE,這是由於程式本身的 stdin/stdout/stderr 會佔用 3 個檔案控制程式碼,導致只剩下 2 個指標了。

值得注意的是在設定軟限制後,sysconf 對應的返回值也變為 5 了。

RLIMIT_LOCKS

#include "../apue.h"
#include <sys/resource.h>
#include <sys/file.h>

void sigxfsz_handler (int sig)
{
  printf ("ate SIGXFSZ...\n");
  signal (SIGXFSZ, sigxfsz_handler);
}

int main (int argc, char *argv[])
{
  int ret = 0;
  signal (SIGXFSZ, sigxfsz_handler);

  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1;
  lmt.rlim_max = 1;
  ret = setrlimit (RLIMIT_LOCKS, &lmt);
  if (ret == -1)
    err_sys ("set rlimit locks failed");

#define FD_SIZE 10
  char filename[256] = { 0 };
  int fds[FD_SIZE] = { 0 };
  for (int i=0; i<FD_SIZE; ++ i)
  {
    sprintf (filename, "core.%02d.lck", i+1);
    fds[i] = open (filename, O_RDWR | O_CREAT, 0644);
    if (fds[i] == -1)
      err_sys ("open file failed");

    ret = flock (fds[i], LOCK_EX /*| LOCK_NB | LOCK_SH*/);
    if (ret == -1)
      err_sys ("lock file failed");

    printf ("establish lock %2d OK\n", i+1);
  }

  for (int i=0; i<FD_SIZE; ++ i)
  {
    if (fds[i] != 0)
    {
      //flock (fds[i], LOCK_UN);
      close (fds[i]);
    }
  }
  return 0;
}

在上一小節例子的基礎上修改:設定檔案鎖數量軟硬限制均為 1,在建立檔案後為每個檔案施加一個檔案鎖:

$ ./lmt_locks
establish lock  1 OK
establish lock  2 OK
establish lock  3 OK
establish lock  4 OK
establish lock  5 OK
establish lock  6 OK
establish lock  7 OK
establish lock  8 OK
establish lock  9 OK
establish lock 10 OK
$ ls -lh core.*
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.01.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.02.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.03.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.04.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.05.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.06.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.07.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.08.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.09.lck
-rw-r--r-- 1 yunhai01 DOORGOD    0 Aug 27 22:26 core.10.lck

看起來沒有生效,不清楚是否和檔案長度為零有關,但是 flock 介面確實返回了成功,有功夫再研究。

RLIMIT_NPROC

#include "../apue.h"
#include <sys/resource.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 10;
  lmt.rlim_max = 10;
  ret = setrlimit (RLIMIT_NPROC, &lmt);
  if (ret == -1)
    err_sys ("set rlimit nproc failed");

#define PROC_SIZE 10
  int pids[PROC_SIZE] = { 0 };
  for (int i=0; i<PROC_SIZE; ++ i)
  {
    pids[i] = fork ();
    if (pids[i] == -1)
      err_sys ("fork failed");
    if (pids[i] == 0)
    {
      printf ("child %d running\n", getpid ());
      sleep (1);
      exit (0);
    }

    printf ("create child %d\n", pids[i]);
  }

  sleep (1);
  return 0;
}

設定程序數軟硬限制均為 10,啟動 10 個子程序,如果算上本身已達到 11 個,所以肯定會有程序 fork 失敗:

$ ./lmt_nproc
fork failed: Resource temporarily unavailable

但沒想到第一個子程序就建立失敗了,又研究了一下 RLIMIT_NPROC 的含義——"每個實際 UID 使用者擁有的最大程序數"——原來是使用者維度的,並不是子程序維度的,所以還得看目前系統中存在的程序數:

$ ps -aux | grep yunhai01 | wc -l
259

參考這個設定為 265,留 6 個餘量,結果還是一樣。直接調大到 512 ,這回倒是成功了,但是沒法驗證邊界情況了,於是有了下面探索邊界的程式碼:

#include "../apue.h"
#include <sys/resource.h>

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  int base = 10;
  int success_cnt = 0;
#define PROC_SIZE 50
  ret = sysconf (_SC_CHILD_MAX);
  printf ("sysconf (_SC_CHILD_MAX) = %d\n", ret);
  while (base < 1024 && base + PROC_SIZE < 1024)
  {
      printf ("============================\n");
      printf ("detect with limit based %d\n", base);
      lmt.rlim_cur = base;
      lmt.rlim_max = 1024;
      ret = setrlimit (RLIMIT_NPROC, &lmt);
      if (ret == -1)
          err_sys ("set rlimit nproc failed");

      ret = sysconf (_SC_CHILD_MAX);
      printf ("sysconf (_SC_CHILD_MAX) = %d\n", ret);
      success_cnt = 0;
      int pids[PROC_SIZE] = { 0 };
      for (int i=0; i<PROC_SIZE; ++ i)
      {
          pids[i] = fork ();
          if (pids[i] == -1)
          {
              if (success_cnt > 0)
                  err_sys ("fork failed");
              else
              {
                  err_msg ("fork failed");
                  break;
              }
          }
          else if (pids[i] == 0)
          {
              printf ("child %d running\n", getpid ());
              sleep (1);
              exit (0);
          }

          printf ("create child %d\n", pids[i]);
          success_cnt ++;
      }

      sleep (1);
      if (base > PROC_SIZE)
          base += PROC_SIZE;
      else
          base *= 2;
  }
  return 0;
}

與之前相比,在外側增加了一個迴圈,用於不停提升探索 RLIMIT_NPROC 的基數,初始時設定為 10,之後以指數方式遞增,直到超過探查子程序數量 (PROC_SIZE),這之後每次增加的數量固定為 PROC_SIZE。

這樣做的上的是在儘快定位邊界的同時,保證一次探查能完全覆蓋失敗的情況,為此也將 PROC_SIZE 從 10 提升到了 50。

設定 RLIMIT_NPROC 時需注意保持硬限制不變 (1024),如果硬限制同軟限制一同降低,後面就再也無法提升軟限制。

最後增加了 sysconf(_SC_CHILD_MAX) 的呼叫,驗證與 RLMIT_NPROC 的軟限制設定是否同步:

$ ./lmt_nproc
sysconf (_SC_CHILD_MAX) = 4096
============================
detect with limit based 10
sysconf (_SC_CHILD_MAX) = 10
fork failed
============================
detect with limit based 20
sysconf (_SC_CHILD_MAX) = 20
fork failed
============================
detect with limit based 40
sysconf (_SC_CHILD_MAX) = 40
fork failed
============================
detect with limit based 80
sysconf (_SC_CHILD_MAX) = 80
fork failed
============================
detect with limit based 130
sysconf (_SC_CHILD_MAX) = 130
fork failed
============================
detect with limit based 180
sysconf (_SC_CHILD_MAX) = 180
fork failed
============================
detect with limit based 230
sysconf (_SC_CHILD_MAX) = 230
fork failed
============================
detect with limit based 280
sysconf (_SC_CHILD_MAX) = 280
fork failed
============================
detect with limit based 330
sysconf (_SC_CHILD_MAX) = 330
fork failed
============================
detect with limit based 380
sysconf (_SC_CHILD_MAX) = 380
create child 8623
create child 8624
create child 8625
child 8624 running
create child 8626
child 8625 running
create child 8627
child 8627 running
create child 8628
child 8628 running
create child 8629
child 8629 running
create child 8630
child 8630 running
create child 8631
child 8631 running
create child 8632
child 8632 running
create child 8633
child 8633 running
create child 8634
child 8634 running
create child 8635
child 8635 running
create child 8636
child 8623 running
create child 8637
child 8626 running
create child 8638
child 8637 running
create child 8639
child 8638 running
create child 8640
child 8639 running
create child 8641
child 8640 running
create child 8642
child 8641 running
create child 8643
child 8642 running
create child 8644
child 8643 running
create child 8645
fork failed: Resource temporarily unavailable
child 8644 running
child 8645 running
child 8636 running

在限制值為 380 且建立了 22 個程序時出現 fork 失敗,可以推算之前使用者的程序數不到 360,這和 ps 輸出的 259 差距不小,可能是 ps 選項沒設定對的緣故。

另外 sysconf (_SC_CHILD_MAX) 的輸出與軟限制的設定基本同步,只是第一次呼叫它返回的 1024 看起來並不實用,因為在已有 360 個使用者程序的情況下,只能再新建不到 700 個程序,與 sysconf 返回的 1024 差距還是比較大的。

可以推斷這個返回值只是簡單的將系統軟限制返回,並沒有參考當前的系統負載,使用時需謹慎。

最後補充一點,在 compiler explorer 中執行上面的程式,第一輪就可以覆蓋到失敗的場景:1

看起來使用的系統非常乾淨 ~

RLMIT_STACK

#include "../apue.h"
#include <sys/resource.h>

int stack_depth = 0;
void call_stack_recur ()
{
  char buf[1024] = { 0 };
  printf ("call stack %d\n", stack_depth++);
  call_stack_recur ();
}

int main (int argc, char *argv[])
{
  int ret = 0;
  struct rlimit lmt = { 0 };
  lmt.rlim_cur = 1024 * 10;
  lmt.rlim_max = 1024 * 10;
  ret = setrlimit (RLIMIT_STACK, &lmt);
  if (ret == -1)
    err_sys ("set rlimit stack failed");

  call_stack_recur ();
  return 0;
}

設定軟硬限制均為 10K,然後遞迴呼叫 call_stack_recur,後者棧上有一個 1K 大小的陣列,理論上只能遞迴不到 10 次,demo 執行結果如下:

檢視程式碼
 $ ./lmt_stack
call stack 0
call stack 1
call stack 2
call stack 3
call stack 4
call stack 5
call stack 6
call stack 7
call stack 8
call stack 9
call stack 10
call stack 11
call stack 12
call stack 13
call stack 14
call stack 15
call stack 16
call stack 17
call stack 18
call stack 19
call stack 20
call stack 21
call stack 22
call stack 23
call stack 24
call stack 25
call stack 26
call stack 27
call stack 28
call stack 29
call stack 30
call stack 31
call stack 32
call stack 33
call stack 34
call stack 35
call stack 36
call stack 37
call stack 38
call stack 39
call stack 40
call stack 41
call stack 42
call stack 43
call stack 44
call stack 45
call stack 46
call stack 47
call stack 48
call stack 49
call stack 50
call stack 51
call stack 52
call stack 53
call stack 54
call stack 55
call stack 56
call stack 57
call stack 58
call stack 59
call stack 60
call stack 61
call stack 62
call stack 63
call stack 64
call stack 65
call stack 66
call stack 67
call stack 68
call stack 69
call stack 70
call stack 71
call stack 72
call stack 73
call stack 74
call stack 75
call stack 76
call stack 77
call stack 78
call stack 79
call stack 80
call stack 81
call stack 82
call stack 83
call stack 84
call stack 85
call stack 86
call stack 87
call stack 88
call stack 89
call stack 90
call stack 91
call stack 92
call stack 93
call stack 94
call stack 95
call stack 96
call stack 97
call stack 98
call stack 99
call stack 100
call stack 101
call stack 102
call stack 103
call stack 104
call stack 105
call stack 106
call stack 107
call stack 108
call stack 109
call stack 110
call stack 111
call stack 112
call stack 113
call stack 114
call stack 115
call stack 116
call stack 117
call stack 118
call stack 119
call stack 120
call stack 121
call stack 122
Segmentation fault (core dumped)

卻執行了 122 次之多,難道是 buf 陣列被編譯器優化了?調整 buf 尺寸為 5120,再次執行:

$ ./lmt_stack
call stack 0
call stack 1
call stack 2
call stack 3
call stack 4
call stack 5
call stack 6
call stack 7
call stack 8
call stack 9
call stack 10
call stack 11
call stack 12
call stack 13
call stack 14
call stack 15
call stack 16
call stack 17
call stack 18
call stack 19
call stack 20
call stack 21
call stack 22
call stack 23
call stack 24
Segmentation fault (core dumped)

尺寸變大 5 倍,遞迴次數減少為 1/5 左右,應該是生效的。最終的結果是限制值的 12 倍之多,沒有限制住。

使用 compiler explorer 執行上面的 demo,事情有些不同:

首先需要的起始記憶體比較大,500K,其次遞迴的數量沒那麼多,不到 300 次。可以對 compiler explorer 使用的系統做如下合理推測:

  • 系統需要的棧空間更大,小於 500K 無法執行 demo
  • 對 RLIMIT_STACK 的限制控制更精準了,且其它地方有消耗棧空間,導致實際可遞迴的次數大大下降

其它

現代 linux 除了書上列的這些,還有其它許多方面的限制 (例如限制訊息佇列的 RLIMIT_MSGQUEUE),這裡就不一一列舉了,感興趣的可以參考 setrlimit 的 man 手冊頁。

另外表中的 RLIMIT_RSS 並沒有驗證,因為這需要一種極端記憶體緊張的系統環境,不太好搭建。

RLMIT_SBSIZE 僅在 FreeBSD 上有效,Linux 是通過通訊端介面設定底層 buffer 大小的,而系統層級的緩衝大小限制是通過 proc 檔案系統來檢視和修改的。

結語

程序環境的核心還是這張記憶體佈局圖,有必要再複習一下:

從這裡可以看到程式的各個段是如何排布的,就可以:

  • 瞭解棧與堆的對向生長
  • 瞭解環境變數新增的難點
  • 瞭解 setjmp & longjmp 跳轉時棧上自動變數的回退
  • 瞭解記憶體資源限制的三個方面
    • 總記憶體:RLIMIT_AS (RLIMIT_VMEM)
    • 棧空間:RLIMIT_STACK
    • 堆+bss+init:RLIMIT_DATA

參考

[1]. C/C++ 例外處理之 01:setjmp 和 longjmp

[2]. compiler explorer

[3].