国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

[apue] 進(jìn)程環(huán)境那些事兒

這篇具有很好參考價(jià)值的文章主要介紹了[apue] 進(jìn)程環(huán)境那些事兒。希望對大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

main 函數(shù)與進(jìn)程終止

眾所周知,main 函數(shù)為 unix like 系統(tǒng)上可執(zhí)行文件的"入口",然而這個(gè)入口并不是指鏈接器設(shè)置的程序起始地址,后者通常是一個(gè)啟動例程,它從內(nèi)核取得命令行參數(shù)和環(huán)境變量值后,為調(diào)用 main 函數(shù)做好安排。main 函數(shù)原型為:

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

這是 ISO C 和 POSIX.1 指義的,當(dāng)然還存在下面幾種不太標(biāo)準(zhǔn)的 main 原型:

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

不帶 argc & argv 參數(shù)的表示不打算接受命令行參數(shù);void 返回值的表示不打算返回一個(gè)結(jié)束狀態(tài)。

進(jìn)程的結(jié)束狀態(tài)碼與 main 的返回值關(guān)系如下:

  • main 聲明為 int 類型返回值
    • main 結(jié)束前執(zhí)行了 return x 語句:x
    • main 結(jié)束前執(zhí)行了無參數(shù) return 語句:未定義 (warning: ‘return’ with no value, in function returning non-void)
    • main 結(jié)束前執(zhí)行了 exit(x) 函數(shù):x
    • main 結(jié)束前未執(zhí)行以上語句:未定義 (warning: control reaches end of non-void function)
    • main 結(jié)束前未執(zhí)行以上語句 [-std=c99]:0
  • main 聲明為 void 類型返回值 (warning: return type of ‘main’ is not ‘int’)
    • main 結(jié)束前執(zhí)行了 return x 語句:未定義 (warning: ‘return’ with a value, in function returning void)
    • main 結(jié)束前執(zhí)行了無參數(shù) return 語句:未定義?
    • main 結(jié)束前執(zhí)行了 exit(x) 函數(shù):x
    • main 結(jié)束前未執(zhí)行以上語句:未定義

測試機(jī)為 CentOS 7.9,gcc 版本 4.8.5,每一項(xiàng)的 warning 信息就是基于這兩個(gè)版本測得。未定義的場景中,均返回 25 這個(gè)魔數(shù)。

開了 -std=c99 后大部分場景沒有改善,僅 main 返回值被聲明為 int 類型且在結(jié)束前沒有調(diào)用任何 return 或 exit 時(shí) (第 1 項(xiàng)第 4 小項(xiàng)) 發(fā)生了顯著變化:從未定義變?yōu)榉祷?0。

進(jìn)程有 8 種終止方式,其中 5 種為正常終止:

  • 從 main 返回 (無論是否有返回值)
  • 調(diào)用 exit
  • 調(diào)用 _exit 或 _Exit
  • 最后一個(gè)線程從其啟動例程返回
  • 最后一個(gè)線程調(diào)用 pthread_exit

另有 3 種為異常終止:

  • 調(diào)用 abort
  • 接到一個(gè)信號并終止
  • 最后一個(gè)線程對取消請求做出響應(yīng)

下面重點(diǎn)看一下 3 個(gè) exit 函數(shù):

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

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

聲明差別不大,_exit 與 _Exit 分別是 POSIX.1 與 ISO C 的標(biāo)準(zhǔn),不過可以將它們視為等價(jià),都直接進(jìn)入內(nèi)核。exit 則在它們的基礎(chǔ)上做了一些清理工作,主要包含以下幾個(gè)方面:

  • 清理線程局部存儲 (TLS) 信息
  • 按順序調(diào)用注冊的終止處理程序
  • 為所有標(biāo)準(zhǔn) I/O 庫打開的流調(diào)用 fclose 函數(shù),這會 flush 緩沖的輸出數(shù)據(jù)

關(guān)于標(biāo)準(zhǔn) I/O 庫,請參考之前寫的這篇文章:《[apue] 標(biāo)準(zhǔn) I/O 庫那些事兒 》。

有了上面的鋪墊,可以這樣理解可執(zhí)行程序的啟動例程與 main 之間的關(guān)系:

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

?即 main 的返回值是直接傳遞給 exit 的 status 參數(shù)作為進(jìn)程結(jié)束狀態(tài)的。

atexit

關(guān)于終止處理程序,一般通過 atexit 函數(shù)進(jìn)行注冊:

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

這里的 function 參數(shù)就是希望在 exit 時(shí)被調(diào)用的清理程序,關(guān)于終止處理程序,有下面幾點(diǎn)需要注意:

  • 調(diào)用次數(shù)有上限,通過 sysconf (_SC_ATEXIT_MAX) 查詢 (實(shí)測為 2147483647, 即 INT_MAX)
  • FILO,先注冊的后被調(diào)用,類似于堆棧,而非隊(duì)列
  • 調(diào)用次數(shù)等于注冊次數(shù),同一清理程序可多次注冊,注冊幾次調(diào)用幾次
  • 執(zhí)行 exec 函數(shù)族執(zhí)行另一個(gè)程序的時(shí)候,自動清空 atexit 注冊的清理程序
  • 在清理程序中調(diào)用 exit "無效",如果調(diào)用 _exit 或 _Exit,會導(dǎo)致程序直接退出,后續(xù)清理程序不再被調(diào)用
  • 進(jìn)程異常終止時(shí)清理程序不會被調(diào)用

下面這個(gè)例子驗(yàn)證了調(diào)用次數(shù)與 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;
}

執(zhí)行它會有如下輸出:

$ ./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 調(diào)用,觀察是否會有變化:

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

結(jié)果與之前完全一致,不過進(jìn)程結(jié)束狀態(tài)變?yōu)榱?2:

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

可見 exit 并非沒有生效,一個(gè)合理的推斷是:第二次進(jìn)入 exit 后,繼續(xù)處理之前沒處理完的清理程序,使得輸出看起來就像"沒生效"一樣。真正的 _exit 是被第二次進(jìn)入 bye 的那個(gè) exit 所調(diào)用,對程序稍加改動來看個(gè)明白:

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

為了便于區(qū)別,這里給的初始值為 10,每調(diào)用一次 bye,exit_status 遞增 1,如果最后進(jìn)程結(jié)束狀態(tài)碼為 10 就證明是第一次 exit 結(jié)束了進(jìn)程,否則就是第二次。

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

結(jié)論已經(jīng)非常明顯,之前的猜測成立!如此就可以合理的推斷 exit 調(diào)用清理程序后,會將其從 FILO 結(jié)構(gòu)中移除,從而避免再次調(diào)用,進(jìn)而引發(fā)無限循環(huán)。

下面試試 _exit 的效果:

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

改為 _exit 后輸出發(fā)生了截?cái)啵?/p>

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

進(jìn)入 bye 處理程序后進(jìn)程就終止了,后續(xù)的處理程序不再調(diào)用。檢查進(jìn)程結(jié)束狀態(tài)碼為 3,正好是 _exit? 的 status 參數(shù)。

將上面 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");
}

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

最后再試一種場景,就是在處理器中繼續(xù)調(diào)用 atexit 注冊新的處理器,觀察新的處理器是否能被調(diào)用,參考下面這個(gè)例子:

#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,在其被回調(diào)時(shí)再注冊處理器 do_dirty_work,結(jié)果是兩個(gè)處理器都能被回調(diào):

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

如果注冊的處理器形成循環(huán)會如何?參考下面的例子:

#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 作為處理器,重新編譯后運(yùn)行,發(fā)現(xiàn)程序果然陷入了死循環(huán):

$ ./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 并不能檢測這種情況,需要程序員自己避免調(diào)用環(huán)的形成,好在這種場景并不多見。

命令行參數(shù)與環(huán)境變量

ISO C 與 POSIX.1 都要求 argv[argc] 參數(shù)為 NULL,因此下面兩種遍歷命令行參數(shù)的方式是等價(jià)的:

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

環(huán)境變量也有類似的約定。大多數(shù) unix like 都支持以下的 main 聲明:

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

將環(huán)境變量放在 main 第三個(gè)參數(shù)上,不過標(biāo)準(zhǔn)的 ISO C 和 POSIX.1 不支持,它們規(guī)定使用單獨(dú)的全局變量訪問環(huán)境變量:

extern char **environ; 

由于沒有類似 argc 的參數(shù)來說明參數(shù)表長度,環(huán)境變量的遍歷只能依賴結(jié)尾 NULL 的方式。

環(huán)境變量

環(huán)境變量的內(nèi)容通常為以下形式:

name=value

name 通常大寫,不過這只是一種慣例,內(nèi)核并不檢查環(huán)境變量內(nèi)容,它的解釋完全取決于各個(gè)應(yīng)用程序。例如 PATH 變量可以通過冒號指定多個(gè)路徑:

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 定義了一組處理環(huán)境變量的函數(shù):

#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);

它們屬于的標(biāo)準(zhǔn)在函數(shù)聲明前做了標(biāo)識。其中:

  • getenv 根據(jù) name 參數(shù)查找變量并獲取 value 部分返回給用戶
  • setenv 根據(jù) name 參數(shù)查找變量
    • 變量不存在,直接設(shè)置新的變量 name=value
    • 變量存在
      • overwrite == 0:保留原有變量不變,返回 0
      • overwrite != 0:刪除原有變量,設(shè)置新的變量 name=value
  • unsetenv 刪除 name 的定義,不存在也返回 0
  • putenv 與 setenv 類似,不同的是
    • string 參數(shù)本身是 name=value 的組合體
    • 變量存在時(shí)刪除,沒有標(biāo)志位可以控制覆蓋行為
    • setenv 需要分配新的存儲區(qū),因此不要求用戶為 name & value 參數(shù)分配存儲空間;putenv 則必需由用戶分配
  • clearenv 是 linux 平臺的專有擴(kuò)展,用于清空環(huán)境變量

關(guān)于增刪改環(huán)境變量導(dǎo)致的空間變化問題,下一節(jié)詳細(xì)說明。

最后需要說明的是,對環(huán)境變量的更改只對當(dāng)前進(jìn)程及之后啟動的子進(jìn)程生效,不對父進(jìn)程及之前啟動的子進(jìn)程產(chǎn)生影響。

存儲空間布局

直接上圖:

[apue] 進(jìn)程環(huán)境那些事兒

?

?

?

?

?

?

?

?

?

?

?

?

?

?

上面是一種典型的內(nèi)存排布,只是舉個(gè)例子,并不代表所有平臺和架構(gòu)都以此這種方式安排其存儲空間,圖中的內(nèi)存地址更是以 Linux x86 處理器為例的。

其中:

  • 代碼段也稱正文段,存儲可執(zhí)行程序的機(jī)器指令部分,一般是只讀、共享的
  • 初始化數(shù)據(jù)段也稱為數(shù)據(jù)段,包含了程序中明確賦初值的全局或靜態(tài)變量,以上兩段從程序文件中讀入
  • 非初始化數(shù)據(jù)段也稱為 bss (block started by symbol),沒有初始化的全局或靜態(tài)變量包含在這個(gè)段中。由于不需要保存初始化值,程序文件中甚至沒有這個(gè)段,它是由 exec 初始化為 0 的
  • 堆,動態(tài)存儲分配區(qū)域,由低地址向高地址增長
  • 棧,自動變量及函數(shù)調(diào)用所需的信息存放在此段。一個(gè)函數(shù)調(diào)用實(shí)例中的變量不會影響另一個(gè)函數(shù)調(diào)用實(shí)例中的變量。由高地址向低地址增長
  • 命令行參數(shù)與環(huán)境變量存放在棧底以上的空間

其中除堆和棧外,其它段都變化很小或不變,所以設(shè)置堆和棧對向增長是非常聰明的做法。當(dāng)向下增長的棧與向上增長的堆相遇時(shí),進(jìn)程的地址空間就用光了。

下面的程序驗(yàn)證了 C 程序的內(nèi)存布局:

#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 上編譯運(yùn)行:

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

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

size

size 命令用于報(bào)告可執(zhí)行文件的 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 列分別是三者加總后的十進(jìn)制與十六進(jìn)制長度。示例中 layout_s 是靜態(tài)鏈接版本,可見使用共享庫的動態(tài)鏈接在各個(gè)段的尺寸上都有明顯縮減。

堆分配

棧的增長主要依賴函數(shù)調(diào)用層次的增加;堆的增長主要依賴以下存儲器分配函數(shù):

#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 長度的存儲區(qū)
  • calloc 分配 nmemb*size 長度的存儲區(qū)
  • realloc 可更改以前分配區(qū)到 size 長度 (增加或減小)

對于新增的存儲區(qū)

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

對于 realloc,新舊地址之間的關(guān)系:

  • 當(dāng)存儲區(qū)減小時(shí),新舊地址保持一致
  • 當(dāng)存儲區(qū)增加時(shí)
    • 原存儲區(qū)后有足夠的空間時(shí),新舊地址保持一致
    • 原存儲區(qū)后沒有足夠的空間,新舊地址不同,會先分配足夠大的空間,復(fù)制數(shù)據(jù),再釋放原存儲區(qū)

realloc(NULL, size) 等價(jià)于 malloc(size)。

sbrk

這些分配例程通常用 sbrk 系統(tǒng)調(diào)用來擴(kuò)充進(jìn)程的堆:

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

這通常是通過調(diào)用 program break 的位置來實(shí)現(xiàn)的,參考 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 段的結(jié)尾,參考上圖應(yīng)該就是堆底。

sbrk 也可以減小堆大小,不過大多數(shù) malloc 和 free 的實(shí)現(xiàn)都不減小進(jìn)程的存儲空間,釋放的空間可供以后再分配,但通常將它們保持在 malloc 池中而不返回給內(nèi)核。

環(huán)境變量空間的變更

有上面內(nèi)容的鋪墊,就可以回顧下上一節(jié)中增刪改環(huán)境變量對存儲空間的影響了:

  • 刪除環(huán)境變量,之后的變量前移填補(bǔ)刪除后的空位
  • 修改環(huán)境變量
    • 新值長度小于等于舊值,在原字符串空間中寫入新值
    • 新值長度大于舊值,在堆上分配新字符串空間并賦值,更新環(huán)境變量表中的指針使之指向新分配的字符串
  • 新增環(huán)境變量
    • 第一次新增環(huán)境變量,在堆上分配新的環(huán)境變量表,將原來的環(huán)境變量"復(fù)制"到新分配的環(huán)境變量表中,然后把新增的環(huán)境變量字符串放在表尾,再新增一個(gè)空指針放在最后,最后使用 environ 變量指向新分配的環(huán)境變量表,基本上就是將環(huán)境變量從棧頂搬到了堆中,不過大多數(shù)環(huán)境變量仍指向棧頂中分配的字符串而已
    • 非第一次新增,使用 realloc 重新分配 environ 變量,以容納新增加的環(huán)境變量

環(huán)境變量空間改變?nèi)绱藦?fù)雜,主要是因?yàn)樗拇笮”粭m斚拗扑懒耍瑳]有辦法擴(kuò)容,當(dāng)增加環(huán)境變量數(shù)目時(shí),只能從棧頂搬到堆中。

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

#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;
}

程序比較簡單,依次執(zhí)行以下操作:add HOME -> add LOGNAME -> remove PATH -> add DISAPPEAR -> add ADDISION,每次操作后都打印整個(gè)環(huán)境變量表,以觀察 environ 和各個(gè)環(huán)境變量的變化:

$ ./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

啟動后先打印整個(gè)環(huán)境變量表,大概有 30 個(gè)環(huán)境變量。

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

設(shè)置 HOME 變量,雖然新值長度小于舊值,這里仍然為新值在堆上分配了空間,看起來 linux 上的實(shí)現(xiàn)偷懶了。

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

設(shè)置 LOGNAME 變量,新值長度大于舊值,這里沒有懸念的在堆上進(jìn)行了分配。

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 變量,這一步主要驗(yàn)證再次新增環(huán)境變量時(shí),會不會重復(fù)利用已刪除的空位,到目前為止 environ 指針地址 (0x7fff15e16468) 沒有發(fā)生變化,仍位于棧頂之上。

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) 沒有發(fā)生變化,看起來仍有足夠的空間讓 realloc 分配。

指令跳轉(zhuǎn) (setjmp & longjmp)

說到指令跳轉(zhuǎn),第一印象就是 goto。由于程序的執(zhí)行本質(zhì)是一條條機(jī)器代碼的執(zhí)行,有些指令本身自帶跳轉(zhuǎn)屬性,像函數(shù)調(diào)用 (call)、函數(shù)返回 (return) 、switch-case 都是某種形式的指令跳轉(zhuǎn),goto 則將這種能力公布給了開發(fā)者,然而下面的兩個(gè)限制導(dǎo)致它在實(shí)際應(yīng)用上的推廣受阻:

  • 只能在函數(shù)內(nèi)部跳轉(zhuǎn),無法跨越函數(shù)棧
  • 濫用 goto 導(dǎo)致代碼邏輯不清晰、后期維護(hù)困難

setjmp & longjmp 完美的解決了上述 goto 的缺點(diǎn),支持跨函數(shù)棧的跳轉(zhuǎn)、且使用上更不易被濫用,也被稱為非局部 goto。

它的跳轉(zhuǎn)邏輯和現(xiàn)代 C++ 的異常機(jī)制已經(jīng)非常相似了,區(qū)別是后者加入了對棧上對象析構(gòu)函數(shù)的自動調(diào)用等更多的內(nèi)容。

先來看函數(shù)原型:

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

書上給的例子就不錯(cuò),這里找到另外一個(gè)更簡單的例子:

#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;
}

編譯運(yùn)行這個(gè) demo,輸出如下:

throw_exception_jmp start.
catch exception via setimp-longjmp.

對于沒有接觸過非局部 goto 的人來說還是比較直觀的。

compiler explorer

這里推薦一個(gè)在線的 c++ 編譯器 compiler explorer,對于沒有 Linux 環(huán)境的人來說非常友好,下面是編譯運(yùn)行上述 demo 的過程:

[apue] 進(jìn)程環(huán)境那些事兒

可以看到這個(gè)工具非常強(qiáng)大,可以:

  • 選擇編譯語言
  • 選擇編譯器
  • 選擇編譯模式 (是否開啟 Vim)
  • 修改編譯鏈接選項(xiàng)
  • 查看反匯編
  • 查看預(yù)處理結(jié)果
  • 查看運(yùn)行輸出
  • 更改窗口布局

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

回歸代碼,注意 longjmp 第二個(gè)參數(shù),這個(gè)不是隨便給的,它將作為跳轉(zhuǎn)后 setjmp 的返回值,要與初始化時(shí)返回的 0 有一些區(qū)別,另外允許任意多個(gè) longjmp 跳向同一個(gè) jmp_buf 實(shí)例,這種情況下,通過指定不同的 val 參數(shù)也能區(qū)別出跳轉(zhuǎn)源,是不是想的很周到?

longjmp 跳轉(zhuǎn)時(shí),當(dāng)前所在的函數(shù)棧到 setjmp 之間的棧將被回收,依附之上的自動變量將不復(fù)存在,但是跳轉(zhuǎn)目的地所在的棧幀還是存在的,此外還有不在當(dāng)前棧上的全局變量、靜態(tài)變量等等也是存在的。

變量值回退

雖然沒讀過 setjmp & longjmp 的源碼,但原理應(yīng)該就是存儲和恢復(fù)函數(shù)棧 (各種寄存器),那這些未被撤銷的變量,是恢復(fù)到 setjmp 時(shí)的狀態(tài),還是保留最后的狀態(tài)呢?對上面的例子稍加修改來進(jìn)行一番考察:

#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;
}

在原來的基礎(chǔ)上添加了幾種類型的變量:

  • globaval:全局變量
  • autoval:main 棧上自動變量
  • regival:main 棧上寄存器變量
  • valaval:main 棧上易失變量
  • statval:main 棧上靜態(tài)變量

并分別在 call_jmp 內(nèi)部和 longjmp 后 (第二次從 setjmp 返回) 時(shí)打印它們的值:

$  ./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

在沒開優(yōu)化的情況下,各個(gè)變量都最新的狀態(tài),沒有發(fā)生值回退現(xiàn)象,添加 -O 編譯選項(xiàng):?

...
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
...

再次運(yùn)行:

$ ./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 的值發(fā)生了回退。加優(yōu)化選項(xiàng)后為提高程序運(yùn)行效率,這些變量的值從內(nèi)存提升到了寄存器,從而導(dǎo)致恢復(fù) main 堆棧時(shí)被一并恢復(fù)了。這里有幾個(gè)值得注意的點(diǎn):

  • 聲明為 register 的 regival 在未開啟優(yōu)化前編譯器并沒有遵循指令將其放置在寄存器,再一次證實(shí)了 register 關(guān)鍵字只是建議而非強(qiáng)制
  • 開優(yōu)化后,棧上的自動變量也被放置在了寄存器中
  • 即使開優(yōu)化,volatile 關(guān)鍵字聲明的變量也不存在于寄存器中

所以最終的結(jié)論是:如果不想棧上的變量受 setjmp & longjmp 影響發(fā)生值回退,最好將它們聲明為 volatile

這里出于好奇,也使用 compiler explorer 運(yùn)行了一把,結(jié)果沒加優(yōu)化的第一次運(yùn)行輸出就不一樣:

?[apue] 進(jìn)程環(huán)境那些事兒

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

這一方面展示了 compiler explorer 強(qiáng)大的切換編譯器版本的能力,另一方面也顯示高版本 gcc 版本器傾向于"相信"用戶提供的 register 關(guān)鍵字。

最后在 compiler explorer 中增加 -O 編譯器參數(shù),會得到和之前一樣的結(jié)果:

[apue] 進(jìn)程環(huán)境那些事兒

資源限制 (getrlimit & setrlimit)

進(jìn)程對系統(tǒng)資源的請求并不是沒有上限的,使用 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 則包含了資源限制的信息,主要包含兩個(gè)成員:

  • rlim_cur:軟限制值,當(dāng)前生效的限制值
  • rlim_max:硬限制值,大于等于軟限制值,軟限制值的提升上限
    • 任何用戶可以降低硬限制值,只有超級用戶可以提升硬限制值
    • 每次降低的硬限制值必需大于等于軟限制值

RLIM_INFINITY 表示無限量:

# define RLIM_INFINITY ((__rlim_t) -1)

可以指定的資源限制類型及在本地環(huán)境上的軟硬限制值列表如下:

resource ?含義 軟限制 硬限制
RLIMIT_AS 進(jìn)程可用存儲區(qū)的最大字節(jié)長度,會影響 sbrk & mmap 函數(shù),非 Linux 平臺也命名為 RLIMIT_VMEM infinite infinite
RLIMIT_CORE 崩潰轉(zhuǎn)儲文件的最大字節(jié)數(shù),0 表示阻止創(chuàng)建,生成的 core 文件大于限制值時(shí)會被截?cái)?/td> 0 infinite
RLIMIT_CPU CPU 的最大量值,單位秒,超過軟限制時(shí),向進(jìn)程發(fā)送 SIGXCPU 信號;超過硬限制時(shí),向進(jìn)程發(fā)送 SIGKILL 信號 infinite infinite
RLIMIT_DATA 數(shù)據(jù)段的最大字節(jié)長度,是 init + bss + heap 的總長度,即除棧、環(huán)境變量、命令行參數(shù)外的內(nèi)存總長度 infinite infinite
RLIMIT_FSIZE 可以創(chuàng)建的文件的最大字節(jié)長度,當(dāng)超過軟限制時(shí),向進(jìn)程發(fā)送 SIGXFSZ 信號,若信號被捕獲,則 write 返回 EBIG 錯(cuò)誤 infinite infinite
RLIMIT_LOCKS 一個(gè)進(jìn)程可持有的文件鎖的最大數(shù)量 (僅 Linux 支持) infinite infinite
RLMIT_MEMLOCK 一個(gè)進(jìn)程使用 mlock 能夠鎖定在存儲器中的最大字節(jié)長度,當(dāng)超過軟限制時(shí),mlock 返回 ENOMEM 錯(cuò)誤 65536 65536
RLIMIT_NOFILE 每個(gè)進(jìn)程能打開的最大文件數(shù),當(dāng)超過軟限制時(shí),open 返回 EMFILE 錯(cuò)誤,更改軟限制會影響 sysconf (_SC_OPEN_MAX) 返回的值 1024 4096
RLIMIT_NPROC 每個(gè)實(shí)際用戶 ID 可擁有的最大進(jìn)程數(shù),當(dāng)超過軟限制時(shí),fork 返回 EAGAGIN 錯(cuò)誤,更改軟限制會影響 sysconf (_SC_CHILD_MAX) 返回的值 4096 63459
RLIMIT_RSS 最大駐內(nèi)存集的字節(jié)長度 (resident set size in bytes),如果物理內(nèi)存不足,內(nèi)核將從進(jìn)程處取回超過 RSS 的部分 infinite infinite
RLMIT_SBSIZE 用戶任意給定時(shí)刻可以占用的套接字緩沖區(qū)的最大字節(jié)長度 (僅 FreeBSD 支持) n/a n/a
RLMIT_STACK 棧的最大字節(jié)長度 8388608 infinite

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

進(jìn)程的資源限制通常是在系統(tǒng)初始化時(shí)由進(jìn)程 0 建立的,然后由每個(gè)后續(xù)進(jìn)程繼承,對于其中非 RLIM_INFINITY 限制值的,進(jìn)程終其一生無法提升限制值 (超級用戶進(jìn)程除外)。

shell 也提供相應(yīng)的內(nèi)置命令 (一般為 ulimit) 來修改默認(rèn)的限制值,在啟動命令前設(shè)置各種限制值才能在新進(jìn)程中生效,在 CentOS 上使用 -a 選項(xiàng)可以查看所有的限制值:

$ 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

大部分限制值與調(diào)用接口的 demo 打印的一致,但是單位可能和接口不同,使用時(shí)需要注意。

下面大體按上表的順序?qū)Ω鱾€(gè)限制類型分別施加資源限制,觀察程序的行為是否和預(yù)期一致。

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);
}

設(shè)置進(jìn)程內(nèi)存軟限制 1M ,然后分配 1M 的堆內(nèi)存:

$ ./lmt_as
malloc failed: Cannot allocate memory

果然內(nèi)存超限失敗了。

RLIMIT_DATA

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

畢竟 RLIMIT_DATA 所包含的三個(gè)段 (init / bss / heap) 中有堆內(nèi)存,通過分配堆內(nèi)存肯定是會擠占這部分限制的。

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;
}

設(shè)置崩潰轉(zhuǎn)儲文件軟限制為 1K,在遭遇空指針崩潰后,能正常生成 core 文件:

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

文件大小未超過 1K。當(dāng)然前提是需要通過 ulimit -c 指定一個(gè)大于 1K 的數(shù)值 (非 root 用戶),否則在 setrlimit 時(shí)會報(bào)錯(cuò):

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

另外生成的 core 文件應(yīng)該是被截?cái)嗔?,通過 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)

因此也是不能用的。最后補(bǔ)充一點(diǎn),設(shè)置 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;
}

設(shè)置了 CPU 軟限制為 1 秒,硬限制為 5 秒,且捕獲 SIGXCPU 信號,之后進(jìn)入一個(gè)計(jì)算死循環(huán),不停消耗 CPU 時(shí)間:

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

日志幾乎是一秒輸出一行,第 5 秒時(shí)達(dá)到 CPU 硬限制,進(jìn)程被強(qiáng)制殺死。

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;
}

設(shè)置最大寫入文件字節(jié)數(shù)軟限制 1K,捕獲 SIGXFZE 信號后打開 core.tmp 文件不停寫入,每次寫入 32 字節(jié)直到失?。?/p>

$ ./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 信號,捕獲信號避免了進(jìn)程 abort,不過 write 返回了 EBIG 錯(cuò)誤。

這里需要注意不應(yīng)使用 fopen/fclose/fwrite 來進(jìn)行測試,因標(biāo)準(zhǔn) I/O 庫的緩存機(jī)制,導(dǎo)致寫入的字節(jié)數(shù)大于實(shí)際落盤的字節(jié)數(shù),從而得不到準(zhǔn)確的限制值。

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;
}

程序設(shè)置內(nèi)存鎖總長度軟限制為 32K,硬限制 64K,分配 32K 內(nèi)存后,在該內(nèi)存上施加 32 個(gè) 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

在達(dá)到 27K 左右時(shí) mlock 報(bào)錯(cuò)了,沒有達(dá)到 32K 的上限可能和 glibc 內(nèi)部也有一些 mlock 調(diào)用有關(guān)。

如果將 1K 的塊調(diào)整為 16 個(gè),總的鎖長度調(diào)整為 16K,再次運(yùn)行 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!

這回能成功。最后需要注意的是,默認(rèn) 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;
}

設(shè)置打開文件數(shù)軟限制為 5,硬限制為 10,之后創(chuàng)建 10 個(gè)文件 (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 個(gè)文件時(shí)失敗,open 返回 EMFILE,這是由于程序本身的 stdin/stdout/stderr 會占用 3 個(gè)文件句柄,導(dǎo)致只剩下 2 個(gè)指標(biāo)了。

值得注意的是在設(shè)置軟限制后,sysconf 對應(yīng)的返回值也變?yōu)?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;
}

在上一小節(jié)例子的基礎(chǔ)上修改:設(shè)置文件鎖數(shù)量軟硬限制均為 1,在創(chuàng)建文件后為每個(gè)文件施加一個(gè)文件鎖:

$ ./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

看起來沒有生效,不清楚是否和文件長度為零有關(guān),但是 flock 接口確實(shí)返回了成功,有功夫再研究。

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;
}

設(shè)置進(jìn)程數(shù)軟硬限制均為 10,啟動 10 個(gè)子進(jìn)程,如果算上本身已達(dá)到 11 個(gè),所以肯定會有進(jìn)程 fork 失?。?/p>

$ ./lmt_nproc
fork failed: Resource temporarily unavailable

但沒想到第一個(gè)子進(jìn)程就創(chuàng)建失敗了,又研究了一下 RLIMIT_NPROC 的含義——"每個(gè)實(shí)際 UID 用戶擁有的最大進(jìn)程數(shù)"——原來是用戶維度的,并不是子進(jìn)程維度的,所以還得看目前系統(tǒng)中存在的進(jìn)程數(shù):

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

參考這個(gè)設(shè)置為 265,留 6 個(gè)余量,結(jié)果還是一樣。直接調(diào)大到 512 ,這回倒是成功了,但是沒法驗(yàn)證邊界情況了,于是有了下面探索邊界的代碼:

#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;
}

與之前相比,在外側(cè)增加了一個(gè)循環(huán),用于不停提升探索 RLIMIT_NPROC 的基數(shù),初始時(shí)設(shè)置為 10,之后以指數(shù)方式遞增,直到超過探查子進(jìn)程數(shù)量 (PROC_SIZE),這之后每次增加的數(shù)量固定為 PROC_SIZE。

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

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

最后增加了 sysconf(_SC_CHILD_MAX) 的調(diào)用,驗(yàn)證與 RLMIT_NPROC 的軟限制設(shè)置是否同步:

$ ./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 且創(chuàng)建了 22 個(gè)進(jìn)程時(shí)出現(xiàn) fork 失敗,可以推算之前用戶的進(jìn)程數(shù)不到 360,這和 ps 輸出的 259 差距不小,可能是 ps 選項(xiàng)沒設(shè)置對的緣故。

另外 sysconf (_SC_CHILD_MAX) 的輸出與軟限制的設(shè)置基本同步,只是第一次調(diào)用它返回的 1024 看起來并不實(shí)用,因?yàn)樵谝延?360 個(gè)用戶進(jìn)程的情況下,只能再新建不到 700 個(gè)進(jìn)程,與 sysconf 返回的 1024 差距還是比較大的。

可以推斷這個(gè)返回值只是簡單的將系統(tǒng)軟限制返回,并沒有參考當(dāng)前的系統(tǒng)負(fù)載,使用時(shí)需謹(jǐn)慎。

最后補(bǔ)充一點(diǎn),在 compiler explorer 中運(yùn)行上面的程序,第一輪就可以覆蓋到失敗的場景:1

[apue] 進(jìn)程環(huán)境那些事兒

看起來使用的系統(tǒng)非常干凈 ~

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;
}

設(shè)置軟硬限制均為 10K,然后遞歸調(diào)用 call_stack_recur,后者棧上有一個(gè) 1K 大小的數(shù)組,理論上只能遞歸不到 10 次,demo 運(yùn)行結(jié)果如下:

查看代碼
?$ ./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)

卻運(yùn)行了 122 次之多,難道是 buf 數(shù)組被編譯器優(yōu)化了?調(diào)整 buf 尺寸為 5120,再次運(yùn)行:

$ ./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 倍,遞歸次數(shù)減少為 1/5 左右,應(yīng)該是生效的。最終的結(jié)果是限制值的 12 倍之多,沒有限制住。

使用 compiler explorer 運(yùn)行上面的 demo,事情有些不同:

[apue] 進(jìn)程環(huán)境那些事兒

首先需要的起始內(nèi)存比較大,500K,其次遞歸的數(shù)量沒那么多,不到 300 次??梢詫?compiler explorer 使用的系統(tǒng)做如下合理推測:

  • 系統(tǒng)需要的??臻g更大,小于 500K 無法運(yùn)行 demo
  • 對 RLIMIT_STACK 的限制控制更精準(zhǔn)了,且其它地方有消耗??臻g,導(dǎo)致實(shí)際可遞歸的次數(shù)大大下降

其它

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

另外表中的 RLIMIT_RSS 并沒有驗(yàn)證,因?yàn)檫@需要一種極端內(nèi)存緊張的系統(tǒng)環(huán)境,不太好搭建。

RLMIT_SBSIZE 僅在 FreeBSD 上有效,Linux 是通過套接字接口設(shè)置底層 buffer 大小的,而系統(tǒng)層級的緩沖大小限制是通過 proc 文件系統(tǒng)來查看和修改的。

結(jié)語

進(jìn)程環(huán)境的核心還是這張內(nèi)存布局圖,有必要再復(fù)習(xí)一下:

[apue] 進(jìn)程環(huán)境那些事兒

從這里可以看到程序的各個(gè)段是如何排布的,就可以:

  • 了解棧與堆的對向生長
  • 了解環(huán)境變量新增的難點(diǎn)
  • 了解 setjmp & longjmp 跳轉(zhuǎn)時(shí)棧上自動變量的回退
  • 了解內(nèi)存資源限制的三個(gè)方面
    • 總內(nèi)存:RLIMIT_AS (RLIMIT_VMEM)
    • 棧空間:RLIMIT_STACK
    • 堆+bss+init:RLIMIT_DATA

參考

[1]. C/C++ 異常處理之 01:setjmp 和 longjmp

[2]. compiler explorer

[3].文章來源地址http://www.zghlxwxcb.cn/news/detail-679141.html

到了這里,關(guān)于[apue] 進(jìn)程環(huán)境那些事兒的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 【C++那些事兒】內(nèi)聯(lián)函數(shù),auto,以及C++中的空指針nullptr

    【C++那些事兒】內(nèi)聯(lián)函數(shù),auto,以及C++中的空指針nullptr

    君兮_的個(gè)人主頁 即使走的再遠(yuǎn),也勿忘啟程時(shí)的初心 C/C++ 游戲開發(fā) Hello,米娜桑們,這里是君兮_,我之前看過一套書叫做《明朝那些事兒》,把本來枯燥的歷史講的生動有趣。而C++作為一門接近底層的語言,無疑是抽象且難度頗深的。我希望能努力把抽象繁多的知識講的生

    2024年02月08日
    瀏覽(27)
  • 【C++那些事兒】初識C++ 命名空間,C++中的輸入輸出以及缺省函數(shù)

    【C++那些事兒】初識C++ 命名空間,C++中的輸入輸出以及缺省函數(shù)

    君兮_的個(gè)人主頁 即使走的再遠(yuǎn),也勿忘啟程時(shí)的初心 C/C++ 游戲開發(fā) Hello,米娜桑們,這里是君兮_,數(shù)據(jù)結(jié)構(gòu)和算法初階更新完畢,我們繼續(xù)來擴(kuò)充我們的知識面,今天我們從認(rèn)識C++開始來帶大家學(xué)習(xí)C++,我之前看過一套書叫做《明朝那些事兒》,把本來枯燥的歷史講的生動

    2024年02月08日
    瀏覽(35)
  • 零基礎(chǔ)Linux_10(進(jìn)程)進(jìn)程終止(main函數(shù)的返回值)+進(jìn)程等待

    零基礎(chǔ)Linux_10(進(jìn)程)進(jìn)程終止(main函數(shù)的返回值)+進(jìn)程等待

    目錄 1. 進(jìn)程終止 1.1 main函數(shù)的返回值 1.2?進(jìn)程退出碼和錯(cuò)誤碼 1.3 進(jìn)程終止的常見方法 2. 進(jìn)程等待 2.1 進(jìn)程等待的原因 2.2?wait?函數(shù) 2.3 waitpid 函數(shù) 2.4?int* status參數(shù) 2.5?int options非阻塞等待 本篇完。 進(jìn)程終止指的就是程序執(zhí)行結(jié)束了,進(jìn)程終止退出的場景有三種: 代碼運(yùn)行

    2024年02月07日
    瀏覽(20)
  • Linux之進(jìn)程控制&&進(jìn)程終止&&進(jìn)程等待&&進(jìn)程的程序替換&&替換函數(shù)&&實(shí)現(xiàn)簡易shell

    Linux之進(jìn)程控制&&進(jìn)程終止&&進(jìn)程等待&&進(jìn)程的程序替換&&替換函數(shù)&&實(shí)現(xiàn)簡易shell

    1.1 fork的使用 我們可以使用man指令來查看一下 子進(jìn)程會復(fù)制父進(jìn)程的PCB,之間代碼共享,數(shù)據(jù)獨(dú)有,擁有各自的進(jìn)程虛擬地址空間。 這里就有一個(gè)代碼共享,并且子進(jìn)程是拷貝了父進(jìn)程的PCB,雖然他們各自擁有自己的進(jìn)程虛擬地址空間,數(shù)據(jù)是拷貝過來的,通過頁表映射到

    2024年04月17日
    瀏覽(26)
  • fork函數(shù)如何創(chuàng)建進(jìn)程,exit/_exit函數(shù)如何使進(jìn)程終止的詳細(xì)分析與代碼實(shí)現(xiàn)

    fork函數(shù)如何創(chuàng)建進(jìn)程,exit/_exit函數(shù)如何使進(jìn)程終止的詳細(xì)分析與代碼實(shí)現(xiàn)

    ??【進(jìn)程通信與并發(fā)】專題正在持續(xù)更新中,進(jìn)程,線程,IPC,線程池等的創(chuàng)建原理與運(yùn)用?,歡迎大家前往訂閱本專題,獲取更多詳細(xì)信息哦?????? ??本系列專欄 - ???????并發(fā)與進(jìn)程通信 ??歡迎大家 ??? ?點(diǎn)贊?? ?評論?? ?收藏?? ??個(gè)人主頁 - 勾欄聽曲

    2024年02月05日
    瀏覽(27)
  • 【C++那些事兒】深入理解C++類與對象:從概念到實(shí)踐(下)| 再談構(gòu)造函數(shù)(初始化列表)| explicit關(guān)鍵字 | static成員 | 友元

    【C++那些事兒】深入理解C++類與對象:從概念到實(shí)踐(下)| 再談構(gòu)造函數(shù)(初始化列表)| explicit關(guān)鍵字 | static成員 | 友元

    ?? 江池?。簜€(gè)人主頁 ?? 個(gè)人專欄:?C++那些事兒 ?Linux技術(shù)寶典 ?? 此去關(guān)山萬里,定不負(fù)云起之望 1.1 構(gòu)造函數(shù)體賦值 在創(chuàng)建對象時(shí),編譯器通過調(diào)用構(gòu)造函數(shù),給對象中各個(gè)成員變量一個(gè)合適的初始值。 雖然上述構(gòu)造函數(shù)調(diào)用之后,對象中已經(jīng)有了一個(gè)初始值,但是

    2024年03月21日
    瀏覽(24)
  • 【C++11那些事兒(一)】

    【C++11那些事兒(一)】

    在2003年C++標(biāo)準(zhǔn)委員會曾經(jīng)提交了一份技術(shù)勘誤表(簡稱TC1),使得C++03這個(gè)名字已經(jīng)取代了C++98稱為C++11之前的最新C++標(biāo)準(zhǔn)名稱。不過由于TC1主要是對C++98標(biāo)準(zhǔn)中的漏洞進(jìn)行修復(fù),語言的核心部分則沒有改動,因此人們習(xí)慣性的把兩個(gè)標(biāo)準(zhǔn)合并稱為C++98/03標(biāo)準(zhǔn)。從C++0x到C++11,C++標(biāo)

    2023年04月14日
    瀏覽(25)
  • 面試的那些事兒

    假如你是網(wǎng)申,你的簡歷必然會經(jīng)過HR的篩選,一張簡歷HR可能也就花費(fèi)10秒鐘看一下,然后HR 就會決定你這一關(guān)是Fail還是Pass。 假如你是內(nèi)推,如果你的簡歷沒有什么優(yōu)勢的話,就算是內(nèi)推你的人再用心,也無能為力。 另外,就算你通過了篩選,后面的面試中,面試官也會根

    2024年01月18日
    瀏覽(22)
  • 賬號安全那些事兒

    賬號安全那些事兒

    隨著《網(wǎng)絡(luò)安全法》正式成為法律法規(guī),等級保護(hù)系列政策更新,“安全” 對于大部分企業(yè)來說已成為“強(qiáng)制項(xiàng)”。然而,網(wǎng)絡(luò)空間安全形勢日趨復(fù)雜和嚴(yán)峻。賬號安全,也在不斷的威脅著企業(yè)核心數(shù)據(jù)安全。 根據(jù)最新的 IBM 全球威脅調(diào)查報(bào)告《X-Force威脅情報(bào)指數(shù)2020》,受

    2024年01月21日
    瀏覽(27)
  • HTTP的那些事兒

    超文本傳輸協(xié)議(Hyper Text Transfer Protocol,HTTP),它是 在計(jì)算機(jī)世界中的兩個(gè)點(diǎn)之間傳遞文本,圖片,多媒體等超文本文件的協(xié)議 。HTTP處在 數(shù)據(jù)鏈路層,網(wǎng)絡(luò)層,傳輸層,應(yīng)用層 中的應(yīng)用層,基于TCP之上。 應(yīng)用廣泛,各大網(wǎng)站,APP都離不開HTTP的身影 無狀態(tài),和TCP不同,

    2023年04月15日
    瀏覽(37)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包