完全解讀Linux環境變數
一、概述
環境變數:bash shell用一個稱作“環境變數(environment variables)”的特性來儲存有關shell會話和工作環境的資訊,它允許你在記憶體中儲存資料,以便運行在shell上的程式和指令碼訪問,這些資料可以用來識別使用者、賬戶、系統、shell特性以及任何其他你需要儲存的資料。
shell中的環境變數有全域環境變數和局部環境變數,通過KV(variable=value)的形式聲明一個局部變數,export這個局部變數,則升級成為全域環境變數。既然shell分開來用,顯然兩者有區別,下面分開來討論。
1. 全域環境變數
特性:聲明一個全域環境變數,在當前shell進程以及子shell進程中可用,父shell進程中不可用,這裡的“可用”可以理解成父shell進程全域環境變數的一個copy,而不是繼承父類的全域環境變數兩者共用一份,因此子shell進程中對父shell進程的全域環境變數進行增、刪、該、查均無影響。
證明一:1)全域變數在當前shell可用、子shell可用,但是父shell進程不可用;2)子shell copy了父shell進程的全域環境變數的副本,因此子shell對變數的操作對父類透明
#!/bin/bash#父shell進程# 1. 聲明一個全域環境變數# 2. fork一個子shell進程# 在子shell進程中可以訪問父類定義的全域變數# 在子shell中修改或刪除(unset)父類定義的全域環境變數,對父進程沒有影響# 在子shell中增加一個全域環境變數,父類中是訪問不到的# 3. 父shell進程中訪問被子shell進程修改或刪除的全域環境變數,該變數值未改變# 4. 父shell進程訪問子類增加的子類全域環境變數:結果是訪問不到export testing="zhangsan"echo "father定義了一個全域變數,初始值為:testing=zhangsan"sh son.shecho "father訪問被子類修改的全域變數:$testing"echo "father訪問子類增加的全域變數:$sonTest"
#!/bin/bash#子shell進程echo "son訪問父類shell進程中定義的全域變數:$testing"testing="son zhangsan"echo "son修改了父類的全域變數:testing=$testing"export sonTest="son lizi"echo "son增加了子類全域變數:$sonTest"
[work@localhost global]$ sh father.sh father定義了一個全域變數,初始值為:testing=zhangsanson訪問父類shell進程中定義的全域變數:zhangsanson修改了父類的全域變數:testing=son zhangsanson增加了子類全域變數:son lizifather訪問被子類修改的全域變數:zhangsanfather訪問子類增加的全域變數:
證明二:登入shell fork一個子shell進程,該子shell進程export一個全域環境變數,執行完成後在登入shell中沒有該變數
#!/bin/bash#登入shell fork一個子shell進程# 子shell進程聲明一個全域環境變數# 子shell執行完成之後,退出到登入shell# 登入shell訪問不到該變數export userName="zhangsan"
執行該指令碼,執行完成後訪問變數,沒有值。
[work@localhost global]$ sh var.sh [work@localhost global]$ echo $userName[work@localhost global]$
2. 局部環境變數
特性:當前shell進程可用,父、子shell進程不可用
證明:父進程定義局部變數,子進程訪問不到;子進程定義局部變數,父進程訪問不到
#!/bin/bashfatherVar="father var"echo "father定義的局部變數:$fatherVar"sh son.shecho "son定義的局部變數:$sonVar"
#!/bin/bashecho "son訪問fanther定義的局部變數:$fatherVar"sonVar="son var"echo "son定義的局部變數:$sonVar"
[work@localhost local]$ sh father.sh father定義的局部變數:father varson訪問fanther定義的局部變數:son定義的局部變數:son varson定義的局部變數:
3. 總結
1. 全域環境變數:當前shell進程及子進程可用,父shell進程不可用,子進程對變數的修改對當前shell來說透明無影響;
2. 局部環境變數:當前shell進程可用,父進程、子進程均不可用;
3. 環境變數存在記憶體當中,斷電後消失,如果想要在下一次系統加電後還可以訪問:
a. 如果這個變數是系統層級的(所有使用者共用),則可以在/etc/profile中export;
b. 如果這個變數只對目前使用者有效,則可以在~/.bash_profile(或者~./bash_login,或者~./profile,或者~./bashrc,或者/etc/bashrc)中export;
補充:本節討論主要是fork進程方式,不包括source和exec,三種方式的差異在後面進行討論;
二、啟動shell
環境變數存放在記憶體當中,斷電後消失。但是你會發現類似USER、PATH、LOGNAME、HOSTNAME這些環境變數,只要系統一啟動就存在了。這是為什嗎?
作業系統因該具備至少5個功能:記憶體管理、檔案系統管理、進程管理、裝置管理(硬體)和使用者介面管理,這裡的使用者介面不能理解成init 5啟動介面功能或類似windows的介面,而是使用者和系統核心互動的介面,即核心提供給外界的一組提供者,所以在系統啟動的時候,啟動shell就是為此而生,他向使用者提供了一組和Linux kernel互動的介面。
啟動shell隨著系統的啟動而啟動,根據表現可以將其理解成後續登入shell、互動shell、非互動shell的頂級父shell環境,因此在啟動shell中聲明的全域環境變數,後續的所有shell都可以訪問到,毫無疑問,局部變數例外。
啟動shell啟動之後,他首先會到/etc/profile中讀取命令並執行,讀取執行該檔案,主要做了兩件事:
1. 定義並 聲明全域的環境變數,export PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL,所有的shell進程都可以訪問到這些變數;
2. 到/etc/profile.d目錄中執行該目錄下的應用開機檔案,這個目錄集中存放了一些應用的開機檔案,這些應用隨著系統啟動而啟動,譬如vim、less等,類似於windows中的啟動項中定義的一些開機自啟動並執行軟體;
/etc/profile.d目錄下的內容和/etc/profile的內容如下:
# /etc/profile# System wide environment and startup programs, for login setup# Functions and aliases go in /etc/bashrc# It's NOT a good idea to change this file unless you know what you# are doing. It's much better to create a custom.sh shell script in# /etc/profile.d/ to make custom changes to your environment, as this# will prevent the need for merging in future updates.pathmunge () { case ":${PATH}:" in *:"$1":*) ;; *) if [ "$2" = "after" ] ; then PATH=$PATH:$1 else PATH=$1:$PATH fi esac}if [ -x /usr/bin/id ]; then if [ -z "$EUID" ]; then # ksh workaround EUID=`id -u` UID=`id -ru` fi USER="`id -un`" LOGNAME=$USER MAIL="/var/spool/mail/$USER"fi# Path manipulationif [ "$EUID" = "0" ]; then pathmunge /sbin pathmunge /usr/sbin pathmunge /usr/local/sbinelse pathmunge /usr/local/sbin after pathmunge /usr/sbin after pathmunge /sbin afterfiHOSTNAME=`/bin/hostname 2>/dev/null`HISTSIZE=1000if [ "$HISTCONTROL" = "ignorespace" ] ; then export HISTCONTROL=ignorebothelse export HISTCONTROL=ignoredupsfiJAVA_HOME=/usr/java/jdk1.7.0_75CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jarPATH=$PATH:$JAVA_HOME/binexport PATH USER LOGNAME MAIL HOSTNAME HISTSIZE HISTCONTROL# By default, we want umask to get set. This sets it for login shell# Current threshold for system reserved uid/gids is 200# You could check uidgid reservation validity in# /usr/share/doc/setup-*/uidgid fileif [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then umask 002else umask 022fifor i in /etc/profile.d/*.sh ; do if [ -r "$i" ]; then if [ "${-#*i}" != "$-" ]; then . "$i" else . "$i" >/dev/null 2>&1 fi fidoneunset iunset -f pathmunge
三、登入shell
無論是哪一個使用者登入系統,printenv列印的全域環境變數都是一樣的,因為這些值在啟動shell中定義完成。但是你會發現,實際上存在不一樣,這會給人造成錯覺,進而無法理解登入shell的真正含義。
1. 區別啟動shell和登入shell
系統啟動完成之後,緊接著就是使用者登入訪問了。不同的使用者登入系統,printenv的結果是不一樣的,也就是說不同的使用者有不同的的shell進程環境,這也間接說明登入shell和啟動shell是不一樣,登入shell是頂級環境。先來驗證一下,再來進行說明。
驗證一:不同使用者登入,其全域環境變數不一樣,此處選擇root和work使用者,測試全域環境變數PATH
步驟一:work賬戶登入,echo $PATH > /home/work/var.path
步驟二:切換使用者以及環境到root,su - root,echo $PATH >> /home/work/var.path
步驟三:cat /home/work/var.path,比較兩行值,執行結果如下所示
[work@localhost ~]$ echo $PATH > /home/work/xx.path[work@localhost ~]$ su - root Password: [root@localhost ~]# echo $PATH >> /home/work/xx.path[root@localhost ~]# cat /home/work/xx.path /usr/local/bin:/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/sbin:/usr/java/jdk1.7.0_75/bin:/home/work/bin/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/usr/java/jdk1.7.0_75/bin:/root/bin
2. 登入shell詳解
登入shell在啟動shell之上又加入了個人化,不同使用者獲得的登入環境是不一樣的。除root使用者之外,Linux上其他使用者的主目錄是/home/xxx,要進行個人化,必然在此做文章。
登入shell的入口,~/.bash_profile檔案。
a. .bash_profile檔案
當你輸入使用者名稱和密碼登入系統的時候,啟動shell進程會fork一個登入shell進程,這個登入shell進程會到當前登入使用者的主目錄下順序讀取(如果存在)檔案~/.bash_profile、~/.bash_login、~/.profile 中的指令並執行,需要注意的是這三個檔案不一定都存在(我的系統centos 2.6.32中就只存在.bash_profile),如果存在多個,則按照列出順序進行讀取指令並執行。
讀取到.bash_profile主要做了兩件事情,一件是source(最後一節會有source的詳解)~/.bashrc這個檔案,另一件是重新export PATH,詳細內容如下。
# .bash_profile# Get the aliases and functionsif [ -f ~/.bashrc ]; then . ~/.bashrcfi# User specific environment and startup programsPATH=$PATH:$HOME/binexport PATH
b. .bashrc檔案由此,可知不同使用者的環境變數PATH不一致的原因。但是source ~/.bashrc這個檔案又做了什麼事情?
.bashrc檔案也做了兩件事,一件是定義一些別名(定義別名,你可以在當前shell中任意使用,類似定義局部環境變數),另一件事是source了/etc/bashrc檔案。
# .bashrc# User specific aliases and functionsalias rm='rm -i'alias cp='cp -i'alias mv='mv -i'# Source global definitionsif [ -f /etc/bashrc ]; then . /etc/bashrcfi
c. /etc/bashrc檔案
/etc/bashrc又TM的做了什麼事情?在/etc/bashrc檔案中也做了兩件事情,一件事情是定義了一些局部變數(沒有export,登入shell可用,其fork的子shell均不能訪問),還根據UID設定了umask的許可權;另一件事情是又讀取並執行了一次/etc/profile.d目錄下的檔案,這一步和啟動shell有重複,雖然看似重複但實際上是有必要的原因,這裡了需要和互動shell(下一節講解)結合。
至此,使用者登入環境初始化完成,所有使用者公用的全域變數有了,不同使用者個人化的全域變數也準備好了,和自己有關係的局部變數也準備好了,萬事俱備,使用者可以進行系統操作了。
# /etc/bashrc# System wide functions and aliases# Environment stuff goes in /etc/profile# It's NOT a good idea to change this file unless you know what you# are doing. It's much better to create a custom.sh shell script in# /etc/profile.d/ to make custom changes to your environment, as this# will prevent the need for merging in future updates.# are we an interactive shell?if [ "$PS1" ]; then if [ -z "$PROMPT_COMMAND" ]; then case $TERM in xterm*) if [ -e /etc/sysconfig/bash-prompt-xterm ]; then PROMPT_COMMAND=/etc/sysconfig/bash-prompt-xterm else PROMPT_COMMAND='printf "\033]0;%s@%s:%s\007" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"' fi ;; screen) if [ -e /etc/sysconfig/bash-prompt-screen ]; then PROMPT_COMMAND=/etc/sysconfig/bash-prompt-screen else PROMPT_COMMAND='printf "\033]0;%s@%s:%s\033\\" "${USER}" "${HOSTNAME%%.*}" "${PWD/#$HOME/~}"' fi ;; *) [ -e /etc/sysconfig/bash-prompt-default ] && PROMPT_COMMAND=/etc/sysconfig/bash-prompt-default ;; esac fi # Turn on checkwinsize shopt -s checkwinsize [ "$PS1" = "\\s-\\v\\\$ " ] && PS1="[\u@\h \W]\\$ " # You might want to have e.g. tty in prompt (e.g. more virtual machines) # and console windows # If you want to do so, just add e.g. # if [ "$PS1" ]; then # PS1="[\u@\h:\l \W]\\$ " # fi # to your custom modification shell script in /etc/profile.d/ directoryfiif ! shopt -q login_shell ; then # We're not a login shell # Need to redefine pathmunge, it get's undefined at the end of /etc/profile pathmunge () { case ":${PATH}:" in *:"$1":*) ;; *) if [ "$2" = "after" ] ; then PATH=$PATH:$1 else PATH=$1:$PATH fi esac } # By default, we want umask to get set. This sets it for non-login shell. # Current threshold for system reserved uid/gids is 200 # You could check uidgid reservation validity in # /usr/share/doc/setup-*/uidgid file if [ $UID -gt 199 ] && [ "`id -gn`" = "`id -un`" ]; then umask 002 else umask 022 fi # Only display echos from profile.d scripts if we are no login shell # and interactive - otherwise just process them to set envvars for i in /etc/profile.d/*.sh; do if [ -r "$i" ]; then if [ "$PS1" ]; then . "$i" else . "$i" >/dev/null 2>&1 fi fi done unset i unset pathmungefi四、互動shell
init 3登入系統之後,你在命令列介面輸入的指令實際上都是在登入shell的環境中執行,你可以訪問登入shell中聲明的任意局部變數。但是當你輸入bash顯式啟動一個互動shell的時候(命令列介面給你的感覺是敲了一下斷行符號,剩下的好像什麼也沒發生),你會發現粗大事了,還是在一模一樣的介面,但是你發現訪問不了剛才可以訪問到的局部變數了。這是因為登入shell fork了一個子shell的環境,而子shell環境是不能訪問父shell環境的局部變數的。
1. 區別登入shell和互動shell
證明一:互動shell不是登入shell,而是登入shell fork的一個子shell進程
步驟一:登入系統後,定義一個局部變數testing="zhangsan",命令列輸入echo $testing,測試可以訪問得到;
步驟二:命令列輸入bash啟動一個互動shell,訪問剛才定義的局部變數,echo $testing,測試不可以訪問;
步驟三:命令列輸入exit指令,退出互動式shell,再次訪問echo $testing,發現可以訪問;
[work@localhost ~]$ testing="zhangsan"[work@localhost ~]$ echo $testingzhangsan[work@localhost ~]$ bash[work@localhost ~]$ echo $testing[work@localhost ~]$ exitexit[work@localhost ~]$ echo $testingzhangsan[work@localhost ~]$
由此可見,互動shell是登入shell的子進程。
2. 互動shell詳解
當你在命令列輸入bash指令顯式進入互動式shell環境的時候,系統發生了什嗎?不像啟動shell和登入shell,互動式shell沒有發生在系統啟動的時候,也沒有發生在使用者登入的時候,這時候系統已經啟動,使用者已經登入,之前的讀取的檔案執行的指令和結果都已經在記憶體中存在了,那麼互動式shell做了那些事情呢?
互動shell是登入shell的子進程,因此他擁有了登入shell聲明的所有全域環境變數(當然也擁有啟動shell聲明的全域環境變數),但是他沒有登入shell聲明的局部變數,也沒有登入shell聲明的一些個人化例如umask、alias等,那麼目前使用者沒有任何理由開啟一個互動式shell,個人化丟失,每次開啟都要重新搞一遍個人化,太TM麻煩了,這種功能你用嗎?有毛用啊?
請不要激動,存在即合理。
互動式shell啟動第一步,就是檢查目前使用者的主目錄下是否有.bashrc檔案(這個檔案上一節有詳細的描述),存在則讀取並執行檔案中的指令。這個檔案的作用不就是聲明局部變數和個人化配置的的嗎?他第一步就是定義別名等,然後source /etc/bashrc檔案,/etc/bashrc檔案一開始定義了使用者的局部變數,接著又載入並執行了/etc/profile.d目錄下的應用開機檔案。
你會驚奇的發現,開啟一個互動shell,和一個剛剛登入的登入shell環境一模一樣!乾淨、純粹、原始,就像少女一樣啊!激動吧?!還有更激動的事情,不要忘了,子shell可以訪問父shell的全域環境變數,但是父shell不能訪問子shell的環境,也就意味著你在互動式shell中所做的壞事永遠不會影響到父shell環境(當然你不要手賤去改設定檔,改了設定檔就是持久化了操作內容,而不是更改記憶體內容,系統斷電後再啟動負載檔案還是會還原出來的),當然如果想父shell環境也能感受到,則需要修改檔案了。
實際上你也能感受到,登入shell其實就是第一個互動shell,一問一答,使用者問系統答,很友好啊,有求必應!
五、非互動shell
非互動shell就沒有那麼友好了,這也正是非互動shell存在的核心價值,不需要使用者來幹預,自動完成指令碼中規定的指令。
因此,簡單來說,非互動shell,即是執行shell指令碼。
當然,非互動式shell作為互動shell fork出來的子進程,擁有父進程所有的全域環境變數(再次申明:不擁有父進程的局部環境變數,你在指令碼中是訪問不到的),他不會再像互動shell那樣到使用者主目錄下去讀取執行.bashrc檔案給自己做個人化,最簡單直接的例子就是ls的alias指令ll,你在指令碼中使用該指令,就會收到”run.sh: line 3: ll: command not found“的錯誤提示,如以下程式碼範例。
[work@localhost env]$ alias -p |grep llalias ll='ls -l --color=auto'[work@localhost env]$ cat run.sh #!/bin/bashll[work@localhost env]$ sh run.sh run.sh: line 3: ll: command not found[work@localhost env]$ echo $?127[work@localhost env]$ lltotal 16drwxrwxr-x. 2 work work 4096 Apr 23 06:56 aliasdrwxrwxr-x. 2 work work 4096 Apr 23 05:43 globaldrwxrwxr-x. 2 work work 4096 Apr 23 05:59 local-rw-rw-r--. 1 work work 16 Apr 23 07:59 run.sh
要想對非互動是shell進行個人化,系統也提供了”介面“,就是環境變數BASH_ENV,當啟動一個非互動式shell的時候,系統會檢查這個環境變數來查看要載入執行的開機檔案,因此你可以自訂一個檔案路徑然後export BASH_ENV,從而達到你想要的目的和結果,這也是傳說中的BASH_ENV漏洞,但是這個漏洞在2014.09月被修複了,在此就不再進行贅述了。
1)BASH_ENV漏洞簡單利用:點擊開啟連結
2)BASH_ENV漏洞修複介紹:點擊開啟連結
六、fork、source和exec
shell編程的時候,往往不會把所有功能都寫在一個指令碼中,這樣太不好維護了,需要多個指令檔協同工作。那麼問題來了,在一個指令碼中怎麼調用其他的指令碼呢?有三種方式,分別是fork、source和exec。
1. fork
fork其實最麻煩,他是從當前shell進程中fork一個子shell進程來完成調用,上文所述的所有情況都是fork調用形式,簡單總結,就是父shell進程的全域環境變數copy一份給子shell進程,因為是拷貝,所以子shell進程對這一份變數的所有操對父shell進程無影響,父進程的局部變數不會被子shell進程訪問到;子shell的全域環境變數自對自己和自己的子shell進程有用,對父shell進程屏蔽,子shell的局部變數也只對當前shell進程有效。
另外,fork調用其實就是在一個指令碼中調用另一個指令碼,被呼叫指令碼執行完成之後返回給父shell進程,父shell進程繼續執行剩下的指令,其中所涉及的慶幸上文已經基本全部覆蓋,此處示範一下fork調用的範例程式碼。
#!/bin/bashecho "父shell進程開始執行"sh son.sh #父shell fork子shell環境執行另一個指令碼echo "父shell進程執行完畢"
#!/bin/bashecho "子shell被調用"
[work@localhost fork]$ sh father.sh 父shell進程開始執行子shell被調用父shell進程執行完畢
2. source
source調用,是把被呼叫指令碼載入到當前的shell環境中來執行,就好像是在一個指令碼裡面運行一樣,他們的定義的局部變數共用,在同一個進程中,如以下樣本。
#!/bin/bash. ./son.sh #通過source方式將son.sh載入到當前shell環境中echo "father訪問son中定義的局部變數:$sonVar"
#!/bin/bashsonVar="son var"echo "son定義了一個變數:sonVar=$sonVar"
[work@localhost source]$ sh father.sh son定義了一個變數:sonVar=son varfather訪問son中定義的局部變數:son var
3. exec
exec調用,也是fork一個子shell環境來執行被呼叫指令碼,但是父shell環境的執行權會被剝奪,也就是執行權被交給了被呼叫指令碼,父shell環境不再擁有執行權,無論父shell指令碼中的指令是否執行完成,都不在被執行,隨著子shell進程的結束而結束。
#!/bin/bashecho "父shell開始執行"exec sh son.shecho "父shell完成執行,但是這句話不會被執行"
#!/bin/bashecho "子shell被父shell exec調用,執行權已經被搶佔過來了,不會在交回給父shell進程"
[work@localhost exec]$ sh father.sh 父shell開始執行子shell被父shell exec調用,執行權已經被搶佔過來了,不會在交回給父shell進程
關於fork、source和exec網上已經有很多介紹了,要是這裡沒有看明白,可以搜尋“fork、source和exec”。
七、環境變數PATH
什麼是環境變數PATH?這個環境變數,在調用shell指令碼的時候很有用,麻煩和問題也主要集中在這裡,PATH環境變數定義了各種shell環境搜尋執行指令的路徑,就像windows上的path環境變數一樣。執行自己寫的指令碼,如果你不想進入到指令碼目錄或者輸入全路徑,那麼你直接把你的指令碼拷貝到PATH下面的任意一個目錄即可,但是這樣太侵入了,不優雅,下面簡單介紹一種比較優雅的開發技巧。
開發技巧:便捷執行指令
通過軟串連和別名alias來讓自訂指令碼可以在當前shell環境中任意被調用。
第一步:查看PATH環境變數,隨便選一個目前使用者有操作許可權的一個出來,本處選擇/home/work/bin
第二步:寫一個測試指令碼run.sh,放在/home/work/shell/myCodes目錄下,裡面只有一句指令”echo $0"列印當前執行指令碼名稱,然後做一個軟串連ln -s /home/work/shell/myCodes/run.sh /home/work/bin/run
第三步:定義別名,先alias -p看一下,不要定義重複了,然後定義別名alias run="sh run",完成
結果:你不用在進入到指令碼所在的目錄執行指令碼,在任意目錄執行run指令就可以調用/home/work/shell/myCodes/run.sh指令碼,而且支援tab鍵補全,是不是很NB!
[work@localhost myCodes]$ lsrun.sh[work@localhost myCodes]$ cat run.sh #!/bin/bashecho $0[work@localhost myCodes]$ pwd/home/work/shell/myCodes[work@localhost myCodes]$ cat run.sh #!/bin/bashecho $0[work@localhost myCodes]$ ll /home/work/bin/run lrwxrwxrwx. 1 work work 31 Apr 23 09:57 /home/work/bin/run -> /home/work/shell/myCodes/run.sh[work@localhost myCodes]$ alias -p |grep runalias run='run'[work@localhost myCodes]$ cd /home/work/[work@localhost ~]$ runi'm bash_env/home/work/bin/run