Linux 環(huán)境變量與進(jìn)程地址空間:從命令查找到虛擬地址詳解
寫在前面
在終端里輸入 ls,系統(tǒng)為什么能直接找到程序;而運(yùn)行自己編譯的文件時,為什么常常要寫 ./code?程序又是怎樣拿到命令行選項(xiàng)、當(dāng)前用戶和工作目錄的?繼續(xù)向下追問,還會遇到一個更反直覺的現(xiàn)象:父子進(jìn)程打印出的變量地址相同,讀到的值卻可以不同。
這些問題看似分散,實(shí)際上由一條主線串在一起:shell 解析命令并維護(hù)運(yùn)行環(huán)境,進(jìn)程繼承這份環(huán)境,而程序中看到的地址屬于每個進(jìn)程自己的虛擬地址空間。
知識框架總覽

一、命令行參數(shù):一個程序怎樣支持多種用法
1.argc與argv是什么
最常見的 main 函數(shù)形式如下:
int main(int argc, char *argv[])
argv 是字符指針數(shù)組,每個元素指向一個參數(shù)字符串;argc 表示有效參數(shù)的數(shù)量。argv[0] 通常保存程序啟動時使用的名稱或路徑,最后還有一個不計(jì)入 argc 的空指針 argv[argc]。

為了先看清數(shù)據(jù),可以寫一個最小程序:
#include <stdio.h>
int main(int argc, char *argv[])
{
for (int i = 0; i < argc; ++i) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}編譯并運(yùn)行:
gcc -std=c99 -Wall -Wextra argv_demo.c -o argv_demo ./argv_demo -a file.txt
一個重要的簡化邊界是:可以把參數(shù)形成過程直觀理解為“shell 把命令行拆成多個字符串”,但 shell 并非機(jī)械地按空格切割。引號、轉(zhuǎn)義符、變量展開、通配符展開等語法都會參與解析。例如 "hello world" 會作為一個參數(shù)傳入。
2. 為什么命令能帶選項(xiàng)
ls -l、cp -f 等命令本質(zhì)上也是可執(zhí)行程序。程序讀取 argv 后進(jìn)行參數(shù)檢查,再把不同選項(xiàng)分派給不同功能,這便是“同一個二進(jìn)制文件,多種使用方式”的基礎(chǔ)。

下面先實(shí)現(xiàn)一個最小的選項(xiàng)分派程序:
#include <stdio.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "用法: %s [-a|-b|-c]\n", argv[0]);
return 1;
}
if (strcmp(argv[1], "-a") == 0) {
puts("功能一");
} else if (strcmp(argv[1], "-b") == 0) {
puts("功能二");
} else if (strcmp(argv[1], "-c") == 0) {
puts("功能三");
} else {
fprintf(stderr, "未知選項(xiàng): %s\n", argv[1]);
return 1;
}
return 0;
}它還不是成熟的參數(shù)解析器,但已經(jīng)具備三項(xiàng)關(guān)鍵能力:檢查參數(shù)個數(shù)、識別選項(xiàng)、對錯誤用法給出提示。選項(xiàng)較多時,可進(jìn)一步使用 getopt 等接口。
提示: 在 Linux 的典型本地程序中,進(jìn)程首先從運(yùn)行時入口(常見符號為
_start)開始,啟動代碼完成必要初始化后調(diào)用main。argc、argv等初始信息來自進(jìn)程啟動約定,并不是運(yùn)行時臨時猜測出來的。
二、PATH:shell 怎樣找到要執(zhí)行的程序
1. 為什么./code要帶路徑,而ls不需要
執(zhí)行程序之前必須先找到程序文件。./code 已經(jīng)明確給出相對路徑,shell 可以直接按該路徑查找;只輸入 ls 時沒有給出路徑,shell 便要查詢 PATH 環(huán)境變量。
PATH 的值由多個目錄組成,目錄之間使用冒號分隔:
echo "$PATH"
shell 依次在這些目錄中尋找名為 ls 的可執(zhí)行文件。找到后執(zhí)行;全部檢查完仍未找到,才報告 command not found。

這也說明自己編譯的程序與系統(tǒng)命令沒有本質(zhì)上的“物種差異”。把可執(zhí)行文件放入 PATH 包含的目錄,或者把它所在目錄加入 PATH,就可以不寫路徑直接運(yùn)行。
2. 修改PATH時不要覆蓋原值
下面的寫法會把原有內(nèi)容整體替換:
PATH=/my/bin
此后 shell 只能在 /my/bin 中搜索,許多外部命令可能突然“消失”。追加目錄時應(yīng)保留舊值:
PATH="$PATH:/my/bin"

這種賦值默認(rèn)只影響當(dāng)前 shell 及其后續(xù)子進(jìn)程。關(guān)閉當(dāng)前 shell 后,內(nèi)存中的修改隨之消失。若要長期生效,需要把導(dǎo)出命令寫入合適的啟動配置文件。
易錯點(diǎn): 不建議為了省略
./,隨意把未經(jīng)驗(yàn)證的個人程序復(fù)制到/usr/bin等系統(tǒng)目錄。把自己的工具目錄加入PATH更容易管理,也不會污染系統(tǒng)文件。
三、環(huán)境變量從哪里來,又存在哪里
1. 本質(zhì):一組名稱=值字符串
環(huán)境變量可直觀理解為進(jìn)程運(yùn)行環(huán)境中的參數(shù),例如:
PATH=/usr/local/bin:/usr/bin HOME=/home/user SHELL=/bin/bash
在 C 接口中,環(huán)境常被表示為一個以空指針結(jié)束的字符指針數(shù)組。每個指針指向一條以 '\0' 結(jié)束的 名稱=值 字符串。
2. 配置文件負(fù)責(zé)持久化,shell 負(fù)責(zé)構(gòu)建運(yùn)行時環(huán)境
環(huán)境變量不是憑空出現(xiàn)的。登錄或啟動 shell 時,shell 會按照自身類型和系統(tǒng)配置讀取若干文件,執(zhí)行其中的設(shè)置,再形成當(dāng)前進(jìn)程的環(huán)境。

常見文件包括:
/etc/profile:常見的系統(tǒng)級登錄 shell 配置入口。~/.bash_profile:常見的 Bash 用戶級登錄配置。~/.bashrc:常見的交互式非登錄 Bash 配置。/etc/bashrc或/etc/bash.bashrc:名稱和加載方式因發(fā)行版而異。
很多系統(tǒng)會在 ~/.bash_profile 中主動加載 ~/.bashrc,再由 ~/.bashrc 間接加載系統(tǒng)級配置,但這是一種常見組織方式,并非所有系統(tǒng)都完全相同。
運(yùn)行時環(huán)境保存在進(jìn)程上下文中;配置文件則負(fù)責(zé)在下一次啟動 shell 時重建它。因此,臨時執(zhí)行 export 與寫入配置文件解決的是兩個不同時間尺度的問題。
四、常見環(huán)境變量與基本操作
1. 常見變量

PATH:可執(zhí)行程序搜索目錄。HOME:當(dāng)前用戶的家目錄,cd ~會利用相關(guān)展開規(guī)則回到這里。SHELL:用戶登錄時配置的 shell 路徑;它不一定等于當(dāng)前正在執(zhí)行的 shell。USER:當(dāng)前環(huán)境記錄的用戶名。LOGNAME:登錄名,切換身份時它與USER是否變化取決于使用方式和工具行為。HISTSIZE:當(dāng)前 shell 內(nèi)存中保留的歷史命令數(shù)量上限之一。PWD:當(dāng)前工作目錄。OLDPWD:上一次工作目錄,cd -會利用它完成往返切換。
此外還常見 HOSTNAME、TERM、LANG、SSH_CLIENT、SSH_TTY、LD_LIBRARY_PATH 等變量。它們分別服務(wù)于主機(jī)標(biāo)識、終端類型、語言編碼、遠(yuǎn)程會話和動態(tài)庫搜索等場景。
2. 四個基本操作

# 創(chuàng)建并導(dǎo)出環(huán)境變量 export MYENV=1234 # 查看全部環(huán)境變量 env # 查看一個變量;雙引號可避免不必要的再次分詞 echo "$MYENV" # 刪除變量 unset MYENV
也可以先創(chuàng)建本地變量,再把它導(dǎo)出:
A=10 export A
五、C 程序獲取環(huán)境變量的三種方式

1. 通過main的第三個參數(shù)
許多 Unix-like 系統(tǒng)支持下面的形式:
#include <stdio.h>
int main(int argc, char *argv[], char *envp[])
{
(void)argc;
(void)argv;
for (int i = 0; envp[i] != NULL; ++i) {
puts(envp[i]);
}
return 0;
}envp 適合遍歷整個環(huán)境。不過,三個參數(shù)的 main 是廣泛使用的系統(tǒng)擴(kuò)展,不屬于 ISO C 只規(guī)定的兩種標(biāo)準(zhǔn)形式。
2. 使用getenv
只需要一個變量時,getenv 更直接:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
const char *path = getenv("PATH");
if (path == NULL) {
fputs("PATH 不存在\n", stderr);
return 1;
}
printf("PATH=%s\n", path);
return 0;
}getenv 成功時返回變量值字符串的指針,失敗時返回 NULL。調(diào)用方不應(yīng)直接釋放該指針;后續(xù)對環(huán)境的修改還可能使舊指針失效。
3. 使用全局變量environ
#include <stdio.h>
extern char **environ;
int main(void)
{
for (char **p = environ; *p != NULL; ++p) {
puts(*p);
}
return 0;
}environ 可以訪問完整環(huán)境表。實(shí)際編程中,需要單個值時優(yōu)先用 getenv;確實(shí)要枚舉全部內(nèi)容時再使用 envp 或 environ。
六、環(huán)境變量為什么具有繼承性
shell 啟動程序時,會把當(dāng)前環(huán)境傳給新程序。新程序若再創(chuàng)建后代進(jìn)程,這份環(huán)境還可以繼續(xù)向下傳遞,因此環(huán)境變量表現(xiàn)出“全局可見”的效果。

這里的“全局”不是所有進(jìn)程共享同一塊可隨意互改的內(nèi)存。更準(zhǔn)確地說:
- 環(huán)境沿父進(jìn)程到子進(jìn)程方向傳遞。
- 子進(jìn)程可以讀取繼承到的值。
- 子進(jìn)程修改自己的環(huán)境,不會反向修改父進(jìn)程。
- 不同用戶、不同終端中的 shell 可以擁有不同環(huán)境。
環(huán)境變量適合傳遞啟動參數(shù)和運(yùn)行配置。例如程序可以讀取功能開關(guān)、日志級別或資源路徑。但是環(huán)境變量來自進(jìn)程環(huán)境,通??杀徽{(diào)用者修改,不應(yīng)該獨(dú)自承擔(dān)安全認(rèn)證。
易錯點(diǎn): 用
getenv("USER")判斷“只有某個用戶可以執(zhí)行”只能作為演示,不能構(gòu)成可靠權(quán)限控制。真實(shí)身份應(yīng)通過getuid、geteuid等系統(tǒng)接口判斷,并配合文件權(quán)限或其他安全機(jī)制。
七、本地變量、環(huán)境變量與內(nèi)建命令
1. 本地變量與環(huán)境變量
A=10
此時 A 是當(dāng)前 shell 的本地變量。set 能看到它,但子進(jìn)程默認(rèn)拿不到它。執(zhí)行 export A 后,它才進(jìn)入導(dǎo)出環(huán)境,后續(xù)子進(jìn)程可以繼承。

本地變量的重要用途之一,是支持 shell 自己的腳本語言和交互行為。例如提示符格式由 PS1 等變量控制。
2. 為什么cd和export必須是內(nèi)建命令
一般外部命令會由 shell 找到可執(zhí)行文件,再創(chuàng)建子進(jìn)程執(zhí)行。如果 cd 在子進(jìn)程中改變目錄,退出后父 shell 的工作目錄完全不會變化;如果 export 只修改子進(jìn)程環(huán)境,也無法反向改變父 shell。
因此,cd、export、unset 等需要修改當(dāng)前 shell 狀態(tài)的操作通常由 shell 自己執(zhí)行,稱為內(nèi)建命令(built-in command)。

可以使用下面的命令判斷一個名稱如何被解析:
type cd type export type ls
八、從變量地址走進(jìn)進(jìn)程地址空間
把代碼、全局變量、動態(tài)申請的內(nèi)存、局部變量、命令行參數(shù)和環(huán)境變量地址依次打印,會發(fā)現(xiàn)它們大體落在不同區(qū)域。這些區(qū)域共同組成進(jìn)程看到的虛擬地址空間。

從低地址到高地址,常見的簡化布局包括:
- 代碼段:機(jī)器指令,通常不可寫。
- 只讀常量區(qū):字符串字面量等只讀數(shù)據(jù)。
- 已初始化數(shù)據(jù)區(qū):具有非零初值的全局變量、靜態(tài)變量。
- 未初始化數(shù)據(jù)區(qū)(BSS):未顯式初始化或零初始化的全局變量、靜態(tài)變量。
- 堆:動態(tài)內(nèi)存分配常用區(qū)域,通常向高地址方向擴(kuò)展。
- 共享映射區(qū):共享庫、
mmap映射等常見于此。 - 棧:函數(shù)調(diào)用、局部變量等常用區(qū)域,通常向低地址方向擴(kuò)展。
- 命令行參數(shù)和環(huán)境變量:進(jìn)程啟動時放入初始棧附近。

這張圖是理解模型,不是要求每一臺機(jī)器都顯示同樣的數(shù)值。體系結(jié)構(gòu)、鏈接方式、內(nèi)核策略、地址空間布局隨機(jī)化(ASLR)以及位置無關(guān)可執(zhí)行文件(PIE)都會影響實(shí)際地址。
靜態(tài)局部變量也能用布局解釋:它的作用域仍受函數(shù)限制,但存儲期貫穿整個程序運(yùn)行過程,因?yàn)樗ǔN挥跀?shù)據(jù)區(qū)或 BSS,而不是隨函數(shù)調(diào)用創(chuàng)建和銷毀的棧幀中。
九、父子進(jìn)程地址相同,值為什么不同
下面的實(shí)驗(yàn)讓父子進(jìn)程都打印全局變量的地址和數(shù)值,只有子進(jìn)程修改變量:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int g_val = 100;
int main(void)
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return 1;
}
for (;;)
{
if (id == 0)
{
++g_val;
printf("子進(jìn)程: value=%d, addr=%p, pid=%ld, ppid=%ld\n",
g_val, (void *)&g_val,
(long)getpid(), (long)getppid());
} else
{
printf("父進(jìn)程: value=%d, addr=%p, pid=%ld, child=%ld\n",
g_val, (void *)&g_val,
(long)getpid(), (long)id);
}
sleep(1);
}
}編譯:
gcc -Wall -Wextra -O0 fork_addr.c -o fork_addr ./fork_addr
典型現(xiàn)象是:父子進(jìn)程打印的 &g_val 數(shù)值相同,父進(jìn)程卻持續(xù)讀到 100,子進(jìn)程則讀到遞增值。

如果打印出的地址是唯一的物理內(nèi)存地址,同一個地址就不該同時對應(yīng)兩個不同值。合理解釋是:
- 程序打印的是虛擬地址。
- 每個進(jìn)程擁有獨(dú)立的虛擬地址空間。
- 相同虛擬地址可以通過各自的頁表映射到不同物理頁。
fork后父子進(jìn)程最初可以共享只讀物理頁;一方寫入時,內(nèi)核通過寫時拷貝(copy-on-write)為寫入方準(zhǔn)備獨(dú)立物理頁。
因此,“地址數(shù)值相同”不等于“正在使用同一份可寫變量”。虛擬地址到物理地址的轉(zhuǎn)換由操作系統(tǒng)和硬件協(xié)作完成,對普通 C/C++ 指針使用保持透明。
十、邊界與常見誤區(qū)
- 誤區(qū):命令行永遠(yuǎn)按空格切分。
- 空格分隔只是入門模型;引號、轉(zhuǎn)義、展開規(guī)則都會改變最終的
argv。 - 誤區(qū):
PATH負(fù)責(zé)查找所有文件。 PATH主要參與無路徑命令名的可執(zhí)行程序查找,不是通用文件搜索器。- 誤區(qū):修改配置文件后當(dāng)前 shell 一定立刻變化。
- 配置通常在 shell 啟動或被顯式
source時讀取。修改文件與修改當(dāng)前進(jìn)程環(huán)境是兩件事。 - 誤區(qū):環(huán)境變量是全系統(tǒng)共享的一張可寫表。
- 每個進(jìn)程擁有自己的環(huán)境;繼承方向通常是父到子,子進(jìn)程不能靠普通賦值反向修改父進(jìn)程。
- 誤區(qū):
USER等環(huán)境變量可以作為可信身份。 - 調(diào)用者可以偽造環(huán)境變量,安全判斷必須使用系統(tǒng)提供的身份與權(quán)限機(jī)制。
- 誤區(qū):地址空間圖就是物理內(nèi)存排布圖。
- 它描述的是進(jìn)程虛擬地址空間。不同進(jìn)程可以擁有相同虛擬地址數(shù)值,卻映射到不同物理內(nèi)存。
- 誤區(qū):每次運(yùn)行的地址都應(yīng)完全相同。
- ASLR、PIE、體系結(jié)構(gòu)和鏈接方式都會造成地址變化;應(yīng)觀察區(qū)域關(guān)系和行為,而不是死記某個十六進(jìn)制數(shù)值。
總結(jié)
命令行參數(shù)解決了“同一個程序怎樣接受不同指令”的問題,PATH 解決了“shell 去哪里找到程序”的問題,環(huán)境變量則把用戶、目錄、搜索路徑等運(yùn)行信息交給后續(xù)進(jìn)程。繼續(xù)觀察程序中的地址,會發(fā)現(xiàn)代碼、數(shù)據(jù)、堆、棧、參數(shù)和環(huán)境都位于進(jìn)程自己的虛擬地址空間中。fork 實(shí)驗(yàn)進(jìn)一步證明:指針打印出的地址是虛擬地址,相同虛擬地址并不保證指向同一份可寫物理數(shù)據(jù)。
這條主線可以濃縮為一句話:shell 組織程序的啟動環(huán)境,進(jìn)程繼承環(huán)境并運(yùn)行,而操作系統(tǒng)用虛擬地址空間為每個進(jìn)程提供獨(dú)立、連續(xù)且統(tǒng)一的地址視圖。
到此這篇關(guān)于Linux 環(huán)境變量與進(jìn)程地址空間:從命令查找到虛擬地址詳解的文章就介紹到這了,更多相關(guān)linux 環(huán)境變量與進(jìn)程地址空間內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Linux環(huán)境變量&&進(jìn)程地址空間詳解
- Linux環(huán)境變量和進(jìn)程地址空間介紹
- Linux命令行參數(shù)和環(huán)境變量使用詳解
- Linux查看、設(shè)置與取消環(huán)境變量詳解
- 詳解Linux中常見環(huán)境變量的特點(diǎn)與設(shè)置
- Linux系統(tǒng)中環(huán)境變量的配置方法詳解
- Linux上設(shè)置Ollama服務(wù)配置(常用環(huán)境變量)
- 在Linux/Windows系統(tǒng)中設(shè)置PYTHONPATH環(huán)境變量的操作步驟
- Linux切換用戶環(huán)境變量消失問題及解決
- 解決Linux中修改/etc/profile文件寫錯環(huán)境變量路徑導(dǎo)致系統(tǒng)命令找不到問題
相關(guān)文章
Linux下文件剪切的shell腳本實(shí)現(xiàn)代碼
本篇文章主要介紹了Linux下文件剪切的sh腳本實(shí)現(xiàn)代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-10-10
Linux shell數(shù)組與關(guān)聯(lián)數(shù)組的用法實(shí)例
今天小編就為大家分享一篇關(guān)于Linux shell數(shù)組與關(guān)聯(lián)數(shù)組的用法實(shí)例,小編覺得內(nèi)容挺不錯的,現(xiàn)在分享給大家,具有很好的參考價值,需要的朋友一起跟隨小編來看看吧2019-02-02
linux執(zhí)行sh腳本報錯:$’\r’: 未找到命令的完美解決方法
本文給大家介紹linux執(zhí)行sh腳本報錯:$’\r’: 未找到命令的完美解決方法,原因分析及解決辦法給大家講解的非常明白,感興趣的朋友跟隨小編一起看看吧2023-02-02
Linux中多命令執(zhí)行'';''和''&&''的區(qū)別解釋
大家有沒有發(fā)現(xiàn)在 Linux 中經(jīng)常使用到一個命令,如 make && make install,這里也可以使用 make ; make install,那么在 Linux 中執(zhí)行命令 ; 和 && 有何區(qū)別?下面通過這篇文章來給大家詳細(xì)的介紹下面,有需要的朋友們可以參考借鑒。2016-12-12

