Linux 應用開發之進程和程序
1. 進程和程序
進程是一個可執行程序的實例,程序包含了一系列信息文件,這些信息描述瞭如何在運行期間創建一個進程:
-
二進制格式標識 :包含用於描述可執行文件格式的元信息,內核根據此信息來解釋文件中的其他信息:
-
a.out(彙編程序輸出)
-
COFF(通用文件格式)
-
ELF(可執行連接格式),目前大多數 linux 實現採用此種方式,具有更多優點
-
機器語言指令 :對程序算法進行編碼
-
程序入口地址 :表示程序開始執行時的起始指令位置
-
數據 :程序文件包含的變量初始值和程序使用的字面量值 (比如字符串)
-
符號表及重定位表 :描述程序中函數和變量的位置及名稱,包括調試和運行時的符號解析 (動態鏈接)
-
共享庫和動態鏈接信息 :程序文件所包含的一些字段,列出程序運行時需要使用的共享庫,以及加載共享庫的動態鏈接器的路徑名
-
其他信息 :程序文件還包含了很多其他信息,用以描述如何創建進程
可以用一個程序來創建很多進程。
從內核角度來看:
-
進程是內核定義的抽象的實體,併爲該實體分配用以執行程序的各項系統資源。
-
進程由用戶內存空間和一系列內核數據結構組成,用戶內存空間包含了程序代碼及代碼所使用的變量,內核數據結構則用於維護進程狀態信息,其中包括:
-
進程相關的標識號
-
虛擬內存表
-
打開文件的描述表
-
信號傳遞和處理的相關信息
-
進程資源使用及限制
-
當前工作目錄
-
一些其他信息
2. 進程號和父進程號
每個進程都有一個進程號 PID,是一個正數,唯一標識系統中的某個進程。
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
-
getpid
返回值的數據類型是pid_t
,專用於存儲進程號 -
Linux 內核限制進程號需要小於 32767(內核常量
PID_MAX
定義),新進程創建時,內核會按順序將一個可用的進程號分配給其使用,進程號到達 32767 的限制時,內核將重置進程號計數器爲 300(不是 1)
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
-
getppid
獲取父進程的進程號 -
看
/proc/PID/status
文件提供的PPid
字段可以獲知每個進程的父進程 -
pstree
命令可以查看進程家族樹
3. 進程內存佈局
每個進程所分配的內存都由很多虛擬內存邏輯劃分的部分,稱之爲 "段":
-
文本段 :包含了進程運行的程序機器語言指令,文本段是隻讀的,也是可共享的
-
初始化數據段 :包含顯式初始化的全局變量和靜態變量
-
未初始化數據段 :包含了未顯示初始化的全局變量和靜態變量,程序啓動之前,系統將本段內所有內存初始化爲 0,這個段又稱爲 BSS(block started by symbol) 段。未初始化的全局變量和靜態變量與初始化的全局變量和靜態變量分開放的原因:
-
程序在磁盤存儲時,沒有必要爲未經初始化的變量分配存儲空間
-
可執行文件只需要記錄未初始化數據段的位置及大小,直到運行時再有程序加載器來分配這一空間
-
棧:動態增長和收縮的段,由棧幀組成,系統會爲每個當前調用的函數分配一個棧幀,棧幀中存儲了函數的局部變量,實參和返回值
-
堆 :在運行時爲變量動態進行分配的一塊區域,堆頂端稱爲 program break
初始化數據段和未初始化數據段又常被稱爲用戶初始化數據段和零初始化數據段。
size
命令可以顯示二進制文件的文本段,初始化數據段,未初始化數據段的大小。
-
argv
和environ
用來存儲程序的命令行實參和環境列表 -
十六進制的地址會因爲內核配置和程序鏈接選項的差異而有所不同
-
灰色區域表示這些範圍在進程虛擬地址空間中不可用,也就是說沒有爲這些區域創建頁表
4. 虛擬內存管理
linux 內核採用虛擬內存管理技術,該技術利用了大多數程序的一個典型特徵,即訪問局部性:要求高效使用 CPU 和 RAM(物理內存) 資源:
-
空間局部性 :程序傾向於訪問在最近訪問過的內存地址附近的內存 (指令是順序執行的)
-
時間局部性 :程序傾向於在不久將來再次訪問最近剛訪問的內存地址 (由於循環)
正是因爲局部性特徵,使得程序即便在僅有部分地址空間存在於 RAM 中,依然可以獲得執行。
虛擬內存的規劃之一就是將每個程序使用的內存切割成小型的,固定大小的 "頁" 單元。相應的,將 RAM 劃分成一系列與虛擬頁尺寸相同的頁幀:
-
任意時刻,每個程序僅有部分頁駐留在物理內存頁幀中,這些頁構成了所謂的駐留集
-
程序未使用的頁拷貝保存在交換區,交換區是磁盤空間中的保留區域,是作爲計算機 RAM 補充,僅在需要時纔會載入物理內存
-
若進程欲訪問的頁面並未駐留在物理內存中,將會發生頁面錯誤 (page fault),內核即刻掛起進程的執行,同時從磁盤中將該頁面載入內存
頁面的大小通常爲 4096 個字節,但也有的實現使用更大的頁面。
內核中維護了一張頁表,該頁表描述了每頁在進程虛擬地址空間中的位置:頁表中的每個條目要麼指出這個虛擬頁面在 RAM 中的具體位置,要麼表明其駐留在磁盤上。
進程虛擬地址空間中,並非所有的地址範圍都需要頁表條目:
-
由於可能存在大段的虛擬地址空間並未投入使用,因此也不需要爲其維護頁表條目
-
進程試圖訪問的地址無頁表條目與之對應,那麼進程將收到
SIGSEGV
信號
由於內核能夠爲進程分配和釋放頁和頁表條目,所以進程的有效地址範圍在其聲明週期內可以發生變化:
-
棧向下增長超出了之前未達到的位置
-
堆中分配或者釋放內存時引起 program break 位置的變化
-
調用
shmat()
連接 System V 共享內存區或者調用shmdt()
脫離共享內存區時 -
調用
mmap()
創建內存映射時,或者調用munmap()
解除內存映射時
虛擬內存的實現需要硬件中分頁內存管理單元 (PMMU) 的支持,PMMU 把要訪問的每個虛擬地址轉換成相應的物理內存地址,當特定虛擬內存地址所對應的頁沒有駐留在 RAM 中時,將以頁面錯誤通知內核。
虛擬內存管理使虛擬地址空間與 RAM 物理地址隔離,帶來的優點:
-
進程與進程,進程與內核相互隔離,一個進程不能讀寫另一個進程或內核中的數據,因爲每個進程的頁表條目指向 RAM 或交換分區中不同的物理頁面集合
-
適當情況下,兩個或者更多的進程能夠共享內存,因爲內核可以使不同進程的頁表條目指向相同的 RAM 頁,經常發生在下面的情形:
-
多個程序執行相同的程序文件或者加載相同的共享庫時,會共享一份代碼副本
-
可以使用
shmget()
和mmap()
等系統調用顯式的請求與其它進程共享內存區,這麼做是出於通信的目的 -
便於實現內存保護機制,可以對頁表條目進行標記,以表示相關頁面內容是可讀、可寫、可執行,多個進程共享 RAM 頁面時,允許每個進程對內存採取不同的保護措施,例如一個進程可能以只讀方式訪問某頁面,而另一進程則以讀寫方式訪問同一頁面
-
程序員和編譯器、鏈接器之類的工具無需關注在 RAM 中的物理佈局
-
因爲需要駐留在內存中的僅是程序的一部分,所以程序在加載和運行時都很快,而且,一個進程所佔用的虛擬內存大小能夠超出 RAM 容量
-
每個進程使用的 RAM 減少了,RAM 中同時可以容納的進程數量就增多了,從而任意時刻,CPU 都可執行至少一個進程,CPU 的利用率也會提高
5. 棧和棧幀
棧駐留在內存的高端,並向下增長,專用寄存器——棧指針 (stack pointer),用於跟蹤當前棧頂。
每次函數調用時,會在棧上新分配一幀,每當函數返回,再從棧上將此幀移去。
通常將這裏的棧稱爲用戶棧,以便與內核棧加以區分,內核棧是每個進程駐留在內核內存中的內存區域,在執行系統調用的過程中供內核內部函數調用使用。
每個用戶棧包括的信息:
-
函數實參和局部變量
-
函數調用鏈接信息
6. 命令行參數
-
argc
表示命令行參數的個數 -
argv
是指向命令行參數的指針數組,每一個參數都是以空字符null
結尾的字符串,第一個參數,即argv[0]
通常是程序名,argv[argc]
是NULL
要想從程序內的任一位置訪問部分或者全部內容,還有兩種方法:
-
/proc/PID/cmdline
文件可以讀取任一進程的命令行參數,每個參數都以null
字節終止,程序可以通過/proc/self/cmdline
文件訪問自己的命令行參數 -
GNC C 語言庫提供兩個全局變量,可以在程序中內獲取調用該程序時的程序名稱,即命令行的第一個參數,定義
_GNU_SOURCE
後,從<errno.h>
中獲取: -
program_invocation_name
:提供了用於調用該程序的完整路徑名 -
program_invocation_short_name
:提供了程序基本名稱
argv
駐留於進程棧之上的一個內存區域,此區域可存儲的字節數有上限要求, <limits.h>
中的 ARG_MAX
常量規定了其大小,一般要求下限是 4096 個字節,調用 sysconf(_SC_ARB_MAX)
確定此值。
7. 環境列表
每個進程都有一個與其相關的稱之爲環境列表的字符串數組,或者簡稱爲環境:
-
環境是 "名稱 - 值" 的成對集合
-
常將列表中的名稱稱爲環境變量
-
新進程在創建時會繼承其父進程的環境副本,這種傳遞是單向的,一次性的,子進程創建後,父、子進程均可各自修改自己的環境變量,且這些變更對對方是不可見的
-
環境變量常用於 shell 中,通過在自身環境變量中存放變量值,shell 可以確保將這些值傳遞給其所創建的進程
-
可以通過設置環境變量來改變一些庫函數的行爲
大多數 shell 使用 export
添加環境變量:
export SHELL=/bin/bash
printenv
命令可以顯示當前的環境列表,export
命令不加任何參數時也可以達到此目的。
通過 /proc/PID/environ
文件可以檢查任一進程的環境列表。
8. 從程序中訪問環境
C 語言使用全局變量 char** environ
訪問列表。
#include <stdlib.h>
char *getenv(const char *name);
- 根據換了變量的名稱,返回相應的字符串指針,如果該環境變量不存在則返回
NULL
9. 修改環境
#include <stdlib.h>
int putenv(char *string);
-
向調用進程的環境添加一個新的變量或者修改一個已經存在的變量值
-
string
是指向name=value
形式的字符串 -
調用函數後,該字符串就稱爲環境變量的一部分,
environ
變量的某一元素的指向與string
參數指向相同,而非string
參數指向字符串的副本,因此: -
後續修改
string
將會影響進程的環境 -
string
參數不能是棧空間變量 -
調用失敗返回非 0 值,而不是 -1
-
glibc 中將
putenv
做了非標準擴展,如果string
中不包含=
,環境列表將會移除以string
命名的環境變量
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
-
會爲
name=value
的字符串分配內存緩衝區,並將name
和value
所指的字符串複製到緩衝區,創建一個新的環境變量 -
name
結尾處 和value
開始處不要加=
,因爲setenv()
會自動添加 -
name
的環境變量在環境中已經存在,且參數overwrite
的值是 0,則setenv()
不改變環境,如果overwrite
的值非 0,則setenv()
將總是改變環境 -
之後修改
name
和value
以及使用棧上變量都不會有影響
#include <stdlib.h>
int unsetenv(const char *name);
- 將
name
指定的環境變量從環境列表中移除
#include <stdlib.h>
int clearenv(void);
-
清除環境變量,即設置
environ=NULL
-
需要預定義
_SVID_SOURCE
或者_BSD_SOURCE
使用 setenv
和 clearenv()
可能導致內存泄漏:
-
調用
setenv
分配一塊內存緩衝區,隨即稱爲進程環境的一部分,調用clearenv
並不會釋放上述的緩衝區 -
但是一般這不會稱爲一個問題,因爲程序一般在開始處調用
clearenv()
以清除繼承自父進程的環境
10. 執行非局部跳轉
#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
-
setjmp()
和longjmp()
可執行非局部跳轉,"非局部" 指的是跳轉目標爲當前執行函數之外的某個位置 -
setjmp()
調用爲後續longjmp()
調用執行的跳轉確立了跳轉目標,該目標就是程序發起setjmp()
調用的位置 -
通過查看
setjmp()
返回的整數值,可以區分setjmp()
是初始返回還是第二次返回: -
初始調用返回 0
-
後續 "僞返回" 的返回值爲
longjmp()
調用中val
參數指定的任意值,如果val
指定爲 0,則longjmp()
實際返回時用 1 替換 -
setjmp()
將環境變量env
參數中,調用longjmp()
時必須指定相同的env
變量,因爲跳轉發生在不同的函數,所以: -
env
定義爲全局變量 -
env
作爲函數的參數傳遞,此種方法比較少見 -
調用
setjmp()
時,env
除了保存當前進程的其他信息外,還保存了程序計數器 (指向當前正在執行的機器語言指令) 和棧指針寄存器(標記棧頂) 的副本,這些信息保證後續longjmp()
調用完成後執行兩個關鍵的操作: -
將發起
longjmp()
調用的函數與之前調用setjmp()
的函數之間的函數棧從棧上剝離,稱爲棧解開,這是通過將棧指針寄存器重置爲env
參數的保存值 -
重置程序計數寄存器,使得程序得以從初始的
setjmp()
調用位置繼續執行,這是通過env
參數中的程序計數寄存器實現的
編譯器優化會重組程序的指令執行順序,並在 CPU 寄存器中而非 RAM 中存儲某些變量,這些優化不會將 setjmp()
和 longjmp()
考慮在內,因而當有優化時,可能出錯。
程序中應該儘可能避免使用非局部跳轉。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/cnvcMBY8Ypv4SAMnuzSAUQ