Docker原理解讀
737
2025-04-02
GitHub: https://github.com/storagezhang
Emai: debugzhang@163.com
本文為《UNIX 環境高級編程》第 7 章學習筆記
理解 UNIX 系統環境中 C 程序的環境是理解 UNIX 系統進程控制特性的先決條件。
本章說明了一個進程是如何啟動和終止的,如何向其傳遞參數表和環境。
雖然參數表和環境都不是由內核進行解釋的,但內核起到了從 exec 的調用者將這兩者傳遞給新進程的作用。
本章也說明了 C 程序的典型存儲空間布局,以及一個進程如何動態地分配和釋放存儲空間。
詳細地了解用于維護環境的一些函數是有意義的,因為它們涉及存儲空間分配。
本章也介紹了 setjmp 和 longjmp 函數,它們提供了一種在進程內非局部轉移的方法。
最后介紹了各種實現提供的資源限制功能。
7.2 main 函數
C 程序總是從 main 函數開始執行的。main 函數的原型是:
int main(int argc, char *argv[]);
參數:
argc:命令行參數的數目
argv:指向各命令行參數的指針所構成的數組。
ISOC 和 POSIX 都要求 argv[argc] 是一個空指針。
當內核執行 C 程序時:
通過使用一個 exec 函數實現。
在調用 main 之前先調用一個特殊的啟動例程。
可執行程序文件將此啟動例程指定為程序的起始地址——這是由鏈接器設置的,而鏈接器由 C 編譯器調用。
啟動例程從內核取得命令行參數和環境變量值,然后為按上述方式調用 main 函數做好安排。
7.3 進程終止
有 8 種方式使進程終止,其中 5 種為正常終止,3 種為異常終止:
正常終止:
從 main 返回;
調用 exit;
調用 _exit 或 _Exit;
最后一個線程從其啟動例程返回;
從最后一個線程調用 pthread_exit 函數。
異常終止:
調用 abort;
接到一個信號;
最后一個線程對取消請求作出響應。
啟動例程:
從 main 返回后立即調用 exit 函數;
常常用匯編語言編寫
如果用 C 代碼形式表示,它調用 main 函數的形式為
exit(main(grgc, argv));
退出函數
3 個函數用于正常終止一個程序:
#include
區別:
使用不同頭文件的原因是 exit 和 _Exit 是由 ISO C 說明的, 而 _exit 是由 POSIX.1 說明的。
_exit 和 _Exit 立即進入內核
exit 先執行一些清理處理,然后進入內核
由于歷史原因,exit 函數總是執行一個標準 I/O 庫的清理關閉操作:對于所有打開流調用 fclose 函數,這造成輸出緩沖中的所有數據都被沖洗(寫到文件上)。
參數:
status:終止狀態(或退出狀態)
大多數 UNIX 系統 shell 都提供檢查進程終止狀態的方法。
如果:
調用這些函數式不帶終止狀態;
main 執行了一個無返回值的 return 語句;
main 沒有聲明返回類型為整型;
則該進程的終止狀態是未定義的。
如果 main 的返回類型為整型,并且 main 執行到最后一條語句時返回(隱式返回),那么該進程的終止狀態是 0。
main 函數返回一個整型值與用該值調用 exit 是等價的。
于是在 main 函數中,exit(0); 等價于 return (0);。
函數 atexit
按照 ISO C 的規定,一個進程可以登記多至 32 個函數,這些函數將由 exit 自動調用,我們稱這些函數為終止處理程序,并調用 atexit 函數來登記這些函數:
#include
參數:
func:一個函數指針
當調用此函數時無需向它傳遞任何參數,也不期望它返回一個值。
exit 調用這些函數的順序與它們登記時候的順序相反。
同一函數如若登記多次,也會被調用多次。
下圖顯示了一個 C 程序是如何啟動的,以及它終止的各種方式:
注意:
內核使程序執行的唯一方法是調用一個 exec 函數。
進程自愿終止的唯一方法是顯式或隱式地(通過調用 exit)調用 _exit 或 _Exit。
進程也可非自愿地由一個信號使其終止。
7.5 環境表
每個進程都接收到一張環境表:
與參數表一樣,環境表也是一個字符指針數組
其中每個指針包含一個以 null 結束的 C 字符串的地址
這些字符串稱之為環境字符串
全局變量 environ 則包含了該指針數組的地址:
extern char **environ;
我們稱 environ 為環境指針,它位于
按照慣例,環境字符串由 name=value 這種格式的字符串組成。
通常用 getenv 和 putenv 函數來訪問特定的環境變量,而不是用 environ 變量,但是如果要查看整個環境,則必須使用 environ 指針。
7.6 C 程序的存儲空間布局
C 程序一直由下列幾部分組成:
正文段:這是由 CPU 執行的機器指令部分。
通常,正文段是可共享的,所以即使是頻繁執行的程序在存儲器中也只需有一個副本。
正文段常常是只讀的,以防止程序由于意外而修改其指令。
初始化數據段:通常將此段稱為數據段。
包含了程序中需明確地賦初值的變量:
函數外的賦初值的聲明
函數內的賦初值的聲明
以其初值存放在初始化數據段中。
未初始化數據段:通常將此段稱為 bss 段。
在程序開始執行之前,內核將此段中的數據初始化為 0 或者空指針。
包含了程序未明確地賦初值的變量:
函數外的未賦初值的聲明
函數內的未賦初值的聲明
棧:臨時變量以及每次函數調用時所需要保存的信息都存放在此段中。
每次函數調用時,函數返回地址以及調用者的環境信息(如某些機器寄存器的值)都存放在棧中。
最近被調用的函數在棧上為其臨時變量分配存儲空間。
通過這種方式使用棧,C 遞歸函數可以工作。
遞歸函數每次調用自身時,就創建一個新的棧幀。
因此一次函數調用中的變量集不會影響另一次函數調用實例中的變量。
堆:通常在堆中進行動態存儲分配。
由于歷史習慣,堆位于未初始化數據段和棧之間。
size(1) 命令報告正文段、數據段和 bss 段的長度(以字節為單位)。
7.7 共享庫
共享庫使得可執行文件中不再需要包含公用的庫函數,而只需在所有進程都可引用的存儲區中保存這種庫例程的一個副本。
程序第一次執行或者調用某個庫函數時,用動態鏈接方法將程序與共享庫函數相鏈接。
這減少了每個可執行文件的長度,但增加了一些運行時間開銷。這種時間開銷發生在該程序第一次被執行時,或者每個共享庫函數第一次被調用時。
共享庫的另一個優點是可以用庫函數的新版本代替老版本,而無需對使用該庫的程序重新連接編輯(嘉定參數的數目和類型都沒有發生改變)。
7.8 存儲空間分配
ISO C 說明了 3 個用于存儲空間動態分配的函數:
malloc,分類指定字節數的存儲區。
此存儲區中的初始值不確定。
calloc,為指定數量指定長度的對象分配存儲空間。
該空間中的每一位(bit)都初始化為 0。
realloc,增加或減少以前分配區的長度。
當增加長度時,可能需要將以前分配區的內容移到另一個足夠大的區域,以便在尾端提供增加的存儲區,而新增區域內的初始值不確定。
#include
參數:
realloc - ptr:
如果 ptr 是 NULL,則 realloc 與 malloc 功能相同,是分配一個指定長度為 newsize 字節的動態存儲空間。
注意:
這 3 個分配函數所返回的指針一定是適當對齊的,使其可以用于任何數據對象。
因為這 3 個 alloc 函數都返回通用指針 void *,所以如果在程序中包括了 #include
函數 free 釋放 ptr 指向的存儲空間。被釋放的空間通常被送入可用存儲區池,以后可在調用上述 3 個分配函數時再分配。
realloc可以增加、減少之前分配的動態存儲區長度。
對于增加動態存儲區的情況:
如果在原來動態存儲區位置后面有足夠的空間可以擴充,則可以在原存儲區位置上向高地址擴充,無需移動任何原先的內容,并返回與傳給它相同的指針值。
如果在原來動態存儲區位置后面沒有足夠的空間可以擴充,則 realloc 分配另一個足夠大的動態存儲區,然后將原先的內容復制到新的存儲區。然后釋放原存儲區,返回新分配存儲區的指針。
這些分配函數通常使用 sbrk 系統調用實現。該系統調用擴充或縮小進程的堆。
大多數 malloc 和 free 的實現都不減小進程的存儲空間。釋放的空間可供以后再分配,但將它們保持在 malloc 池中而不返回給內核。
大多數實現所分配的存儲空間要比所要求的稍大一些,額外的空間用來記錄管理信息,例如分配塊的長度、指向下一個分配塊的指針等。
如果超過一個已分配區的尾端或者在已分配區起始位置之前進行寫操作,則會改寫另一塊的管理記錄信息。這種類型的錯誤是災難性的,但是因為這種錯誤不會很快就暴露出來,所以很難發現。
在動態分配的緩沖區前或后進行寫操作,破壞的可能不僅僅是該區的管理記錄信息。
在動態分配的緩沖區前后的存儲空間很可能用于其他動態分配的對象。
這些對象與破壞它們的代碼可能無關,這造成尋求信息破壞的源頭更加困難。
如果一個進程調用 malloc 函數,但是沒有調用 free 函數,則該進程占用的存儲空間就會連續增加,這被稱為內存泄漏。
進程地址空間長度慢慢增加,直至不再有空閑空間。
此時,由于過度的換頁開銷,會造成性能下降。
替代的存儲空間分配程序
libmalloc
vmalloc
quick-fit
jemalloc
TCMalloc
alloca
7.9 環境變量
環境字符串的形式是:name=value
UNIX 內核并不查看這些字符串,它們的解釋完全取決于各個應用程序。
ISO C 定義了一個函數 getenv,可以用其取環境變量值,但是該標準又稱環境的內容是由實現定義的:
#include
此函數返回一個指針,它指向 name=value 字符串中的 value。我們應當使用 getenv 從環境中取一個指定環境變量的值,而不是直接訪問 environ。
下圖列出了由 Single UNIX Specification 定義的環境變量:
注意:ISO C 沒有定義任何環境變量。
除了獲取環境變量值,有時也需要設置環境變量:
#include
區別:
putenv 取形式為 name=value 的字符串,將其放到環境表中。
如果 name 已經存在,則先刪除其原來的定義。
setenv 將 name 設置為 value。
如果 name 已經存在
若 rewrite 非 0,則首先刪除現有的定義;
若 rewrite 為 0,則不刪除現有的定義;
unsetenv 刪除 name 的定義。
即使不存在這種定義也不算出錯。
setenv 必須分配存儲空間,以便依據其參數創建 name=value 字符串。putenv 可以自由地將傳遞給它的參數字符串直接放到環境中,因此將存放在棧中的字符串作為參數傳遞給 putenv 就會發生錯誤,因為從當前函數返回時,其棧幀占用的存儲區可能將被重用。
這些函數內部操作環境表非常復雜:
刪除一個name:
先在環境表中找到該指針,然后將所有的后續指針都向環境表的首部順次移動一個位置。
修改一個現有的 name:
如果新 value 的長度少于或等于現有 value 的長度,則只需要將新字符串復制到原字符串所用的空間中;
如果新 value 的長度大于現有 value 的長度,則必須調用 malloc 為新字符串分配空間,然后將新字符串復制到該空間,接著使環境表中針對 name 的指針指向新分配區。
增加一個新的 name:
調用 malloc 為新的指針表分配空間。
如果這是第一次增加一個新 name
調用 malloc 為新的指針表分配空間
將原來的環境表復制到新分配區
將新的 name=value 字符串的指針存放到該指針表的表尾
將一個空指針存放在其后
使 environ 指向新指針表
如果這不是第一次增加一個新的 name
調用 realloc,以分配比原空間多存放一個指針的空間
將指向新 name=value 字符串的指針存放在該表表尾
將一個空指針存放在其后
7.10 函數 setjmp 和 longjmp
在 C 中,goto 語句是不能跨越函數的,而執行這種類型跳轉功能的是函數 setjmp 和 longjmp。這兩個函數對于處理發生在很深層嵌套函數調用中的出錯情況是非常有用的。它們不是由普通的 C 語言 goto 語句在一個函數內實施的跳轉,而是在棧上跳過若干調用幀,返回到當前函數調用路徑上的某一個函數中。
#include
參數:
對于setjmp函數:
env:是一個特殊類型 jmp_buf
是某種形式的數組,其中存放在調用 longjmp 時能用來恢復棧狀態的所有信息。
因為需在 longjmp 中引用 env 變量,所以通常將 env 變量定義為全局變量。
val:將成為從 setjmp 處返回的值。
對于一個 setjmp,可以有多個 longjmp。
7.11 函數 getrlimit 和 setrlimit
每個進程都有一組資源限制,其中一些可以用 getrlimit 和 setrlimit 函數查詢和更改:
#include
參數:
resource:指定的資源,取下列值之一:
RLIMIT_AS:進程總的可用存儲空間的最大長度(字節)。
這會影響到 sbrk 函數和 mmap 函數。
RLIMIT_CORE:core 文件的最大字節數。
如果為 0,則阻止創建 core 文件
RLIMIT_CPU:CPU時間的最大量值(秒)。
當超過此軟限制時,向該進程發送 SIGXCPU 信號。
RLIMIT_DATA:數據段的最大字節長度。
是初始化、非初始以及堆的總和。
RLIMIT_FSIZE:可以創建的文件的最大字節長度。
當超過此軟限制時,向該進程發送 SIGXFSX 信號。
RLIMIT_MEMLOCK:一個進程使用 mlock 能夠鎖定在存儲空間中的最大字節長度。
RLIMIT_MSGQUEUE:進程為 POSIX 消息隊列可分配的最大存儲字節數。
RLIMIT_NICE:為了影響進程的調度優先級,友好值可設置的最大限制。
RLIMIT_NOFILE:每個進程能打開的最多文件數。
更改此限制將影響到 sysconf 函數在參數 _SC_OPEN_MAX 中返回的值。
RLIMIT_NPROC:每個實際用戶 ID 可以擁有的最大子進程數。
更改此限制將影響到 sysconf 函數在參數 _SC_CHILD_MAX 中返回的值。
RLIMIT_RSS:最大駐內存集字節長度。
如果可用的物理存儲器非常少,則內核將從進程處取回超過 RSS 的部分。
RLIMIT_SIGPENDING:一個進程可排隊的信號的最大數量。
這個限制是 sigqueue 函數實施的。
RLIMIT_STACK:棧的最大字節長度。
RLIMIT_SWAP:用戶可消耗的交換空間的最大字節數。
RLIMIT_VMEM:RLIMIT_AS 的同義詞。
資源限制影響到調用進程并由其子進程繼承。
rlptr:指向下列結構的指針
struct rlimit { rlim_t rlim_cur; /* soft limit: current limit */ rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */ }
在 getrlimit 中,它返回資源限制值。
在 setrlimit中,它存放待設置的資源限制值。
常量 RLIM_INFINITY 指定了一個無限量的限制。
在更改資源限制時,必須遵循下列 3 條規則:
任何一個進程都可將一個軟限制值更改為小于或等于其硬限制值。
任何一個進程都可降低其硬限制值,但它必須大于或等于其軟限制值。
這種降低,對普通用戶而言是不可逆的。
只有超級用戶進程可以提高硬限制值。
習題
是否有方法不適應(a)參數傳遞、(b)全局變量這兩種方法,將 main 中的參數 argc 和 argv 傳遞給它所調用的其他函數?
由于 argc 和 argv 的副本不像 environ 一樣保存在全局變量中,所以在大多數 UNIX 系統中沒有其他辦法。
在有些 UNIX 系統實現中執行程序時訪問不到其數據段的 0 單元,這是一種有意的安排,為什么?
當 C 程序解引用一個空指針出錯時,執行該程序的進程將終止。
可以利用這種方法終止進程。
如果用 calloc 分配一個 long 型的數組,數組的初始值是否為 0?如果用 calloc 分配一個指針數組,數組的初始值是否為空指針?
calloc 將分配的內存空間初始化為 0。
但是 ISO C 并不保證 0 值與浮點 0 或空指針的值相同。
Linux Unix
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。