在 Linux 上有哪些執行程式的方式?
Have you ever thought ?
前段時間,我在編寫一個 Go
程式,這個程式要做的一件事是在作業系統上執行一個命令(可執行檔案或者可執行指令碼),程式大概像下面這樣子:
|
|
當我讓程式去執行一個 shell 指令碼的時候,收到了 fork/exec: exec format error
的錯誤,然而我在 shell 下執行這個指令碼卻是正常的,這讓我很迷惑。
當我弄清楚原因是我沒有在腳本里加 Shebang
(#!
) 的時候,我更加疑惑了:為什麼作業系統會容忍我的過錯呢?
對此,我會在稍後的章節中進行解釋。當我搞清楚問題的始末的之後,我突然對作業系統執行程式的方式產生了極大的興趣,並試圖去搞清楚它。這就是我寫這篇文章的初衷。
你是否想過,除了在 shell
下啟動一個程式,是否還有其它的方式? 我們是不是永遠無法擺脫 shell
? 你是否曾經對 shell
“source xxx.sh”
、“. xxx.sh”
、“./xxx.sh”
、“. ./xxx.sh”
、“sh ./xxx.sh”
。 沒關係,這篇文章會帶你走出迷霧。
本文會涉及到一點 Sysvinit
和 Systemd
的內容, 但我不會過多的去介紹他們,只是簡單說明,這是一種能讓你的程式執行起來的方式,我最後的重點會放在 shell
上面。
讓程式跑起來有多少種可能的方法?
當我產生這個疑問之後,我努力的思考,並去尋找答案,最後總結了如下幾種:
- 傳統的 Sysvinit 方式
- Systemd
-
crontab
或者Systemd Timer
- shell(無論是終端還是 sshd )
- GUI
其中 1, 2 兩種方式可以算作同一類,雖然他們的工作方式有所不同,但都屬於系統管理層面。如果你的程式是一個隨系統啟動,並託付給系統管理的 Daemon
,那麼最好的方式就是通過 Sysvinit
或者 Systemd
來管理,他們都是 Linux 的 init 系統。相似的 init 系統還有 Upstart
,但我並不熟悉它,所以不準備做介紹,當然這並不影響,因為他們屬於一類系統。
定時任務
是另一個可能會拉起一個程式的方式,相信很多朋友都有在 Linux 上使用 crontab
的經歷,而它的替代品 Systemd Timer
可能就沒那麼多人熟悉了。
shell
是最常見的啟動程式的方式。事實上 shell
的主要作用就是去執行其它的程式,即便是前面 3 種方式,很多時候也是使用 shell 來啟動需要執行的程式的,只不過不是我們手動在 shell 裡執行而已。
還有一種方式就是在桌面環境下,使用 GUI 來啟動一個前臺程式,你可能通過點選一個 .desktop
的快捷方式來啟動一個桌面應用,在我的 Manjaro
下桌面應用全部是由 plasmashell
這個程序 fork
出來的子程序。
承前啟後的 SysV init
Linux 作業系統的啟動首先從 BIOS 開始,然後由 Boot Loader 載入核心,並初始化核心。核心初始化的最後一步就是啟動 init 程序。這個程序是系統的第一個程序,PID 為 1,又叫超級程序,也叫根程序。它負責產生其他所有使用者程序。所有的程序都會被掛在這個程序下,如果這個程序退出了,那麼所有的程序都被 kill 。如果一個子程序的父程序退了,那麼這個子程序會被掛到 PID 1 下面。
因為大多數 Linux 發行版的 init 系統是和 Unix System V 是相互相容的,因此 Linux 上的 init 系統也被成為 Sysvinit
。
在 sysvinit 下有幾個 runlevel
,並且有 0~6 七個執行級別,比如:常見的 3 級別指定啟動到多使用者的字元命令列介面,5 級別指定啟動到圖形介面,0 表示關機,6 表示重啟。其配置在 /etc/inittab 檔案中。
與此配套的還有 /etc/init.d/ 和 /etc/rc[X].d,前者存放各種程序的啟停指令碼(需要按照規範支援 start,stop子命令),後者的 X 表示不同的 runlevel 下相應的後臺程序服務,如:/etc/rc3.d 是 runlevel=3 的。 裡面的檔案主要是 link 到 /etc/init.d/ 裡的啟停指令碼。其中也有一定的命名規範:S 或 K 打頭的,後面跟一個數字,然後再跟一個自定義的名字,如:S01rsyslog,S02ssh。S 表示啟動,K表示停止,數字表示執行的順序。
為了將作業系統帶入可操作狀態,init 系統通過讀取 /etc/inittab
獲得 runlevel,然後依次順序執行對應 level 下的指令碼。rc[X].d 下都是些 link, 連結到 rc.d 中的 shell 指令碼, 可見系統初始化過程中依然是使用的 shell
來啟動相應程式的。
然而這些指令碼中需要使用 awk, sed, grep, find, xargs 等等這些作業系統的命令,這些命令需要生成程序(這涉及到 shell 的工作方式,我稍後在 shell 小節詳細介紹),生成程序的開銷很大,關鍵是生成完這些程序後,這個程序就幹了點屁大的事就退了。這完全是殺雞用牛刀啊,作業系統廢了九牛二虎之力拉起來一個程序,結果這個程序就幹了個把字串轉為小寫的活,然後丟下一臉懵逼的作業系統就瀟灑的退出了。
可以想見,當rc.d中有大量的指令碼,且指令碼中又有成百上千個類似於 awk、sed、grep 這樣的命令,系統的啟動過程就會慢的要死。當然對於啟停不那麼頻繁的伺服器來說,這依然可以接受,而且這樣的系統設計也很符合 Unix 設計哲學:Do one thing and Do it well
,所以 sysvinit 可以一統江湖幾十年。直到 2006年 Linux 核心進入 2.6 時代,Linux 開始進入桌面系統,而桌面系統和伺服器系統不一樣的是,桌面系統面臨頻繁重啟,而且,使用者會非常頻繁的使用硬體的熱插拔技術。於是,這些新的場景,讓 sysvint 受到了很多挑戰。
更詳細的 sysvint 介紹可以參考 淺析 Linux 初始化 init 系統-sysvinit
步行奪猛馬的 Systemd
歷史上總是會有人站出來對現狀說不,2010 年 Lennart Poettering和他的小夥伴們開源併發布了一個新的 init 系統——systemd
。
Systemd
是 Linux 系統中最新的初始化系統(init),它主要的設計目標是克服 sysvinit 固有的缺點,提高系統的啟動速度。systemd 和 ubuntu 的 upstart 是競爭對手,而 ubuntu 在15.04及後續版本中已將 systemd 設定為預設 init 程式,redhat 和 centos 也從 7.0 之後開始使用 systemd,截止目前 systemd 已經執行在大部分的 Linux發行版中。
在系統啟動上 systemd 擁有絕對的優勢,有張三方對比圖可見分曉:
如今 systemd 成為 1 號程序,後續所有的程序都是由它 fork 出來的:
|
|
深入瞭解請參考:LINUX PID 1 和 SYSTEMD
定時任務
簡單說一下定時任務,當我們使用 crontab 配置了一個定時執行任務之後,Cron每分鐘做一次檢查,看看哪個命令可執行,當 Cron 檢查到有命令需要執行時則 fork 子程序,再由此子程序 fork-execve 執行真正的命令:
|
|
這篇 Linux cron執行原理 有更詳細的介紹。
對於 systemd 我測試一個 timer,其程序是掛在 systemd 程序之下的,我猜測也是 systemd 程序去 fork 執行 timer 中的任務。
|
|
偉大的造物主——shell
通過前面的分析,會發現 Linux 上的程式絕大多數情況下是通過 shell 來執行的,所以我們接下來將重點放在 shell 上。
你可能會問: shell 有什麼好講的,它不就是個與核心互動的外殼
程式麼?
沒錯,它的功能就是如此純粹——a shell is a user interface for access to an operating system's services
,但它卻無處不在。
傳統的 Sysvinit 系統下絕大部分的系統服務都是通過 shell 拉起來的,雖然到了 Systemd 時代,很多工作由 C 語言重新實現了(具體見LINUX PID 1 和 SYSTEMD),但是你依然可以使用 systemd 來管理你的啟停指令碼,這些指令碼用來啟停你的程式。而對於非 Daemon 方式的程式。你仍然需要用 shell 來啟動它們到前臺,或者使用 nohup、setsid等方式啟動到後臺。
可見,我們無法逃離 shell
,它就像是一個造物主,系統中幾乎所有的程序都是或曾是它的子民。
要講清楚shell是一個十分艱鉅的任務,對於只查過幾天資料的我來說自然無法勝任,但是擇其一兩點來講,以多少理清一些 Linux 下程式啟動與執行的原理為目的,或可一試。
文中涉及到關於 shell 的實驗或者結論皆以 Bash 作為參考依據。
What is a shell?
Bash 主頁上有關於 shell 的定義:
At its base, a shell is simply a macro processor that executes commands. The term macro processor means functionality where text and symbols are expanded to create larger expressions.
這段話真不太好翻譯,勉強翻譯一下為:從根本上說,shell 只是執行命令的巨集處理器。術語巨集處理器是指將文字和符號擴充套件以建立更大的表示式的功能。
對於 Unix shell 來說,它既是一個命令列直譯器也是一個程式語言。shell 作為命令列直譯器為豐富的 GNU 工具集提供了使用者介面,而作為程式語言它成功的將這些工具集結合在一起,之後就可以將命令編寫進檔案,去完成各種各樣的任務。
很多人可能傻傻分不清 terminal
、tty
、console
和 shell
,這裡第一個高票回答對這些概念做了詳細的解釋:What is the exact difference between a 'terminal', a 'shell', a 'tty' and a 'console'?。如果英文閱讀不暢,知乎上有人將其翻譯了一下:終端、Shell、tty 和控制檯(console)有什麼區別?,我不再做額外的闡述了,接下來只需要記住 shell 是一個命令列直譯器就好,它可以執行在互動模式和非互動模式。
shell 是如何查詢命令的
當我們在互動式 shell 下敲下一個命令時,shell 查詢命令檔案的規則大概如下:
-
執行命令前 shell 會先檢查是否有 alias,如果有就會使用 alias 中的內容。
-
如果 command 名字不包含
"/"
,shell 將嘗試尋找它。如果存在同名的函式,則會呼叫函式。 -
如果沒有匹配到函式,則從 shell 內建命令(
builtins
)中尋找,如果找到則呼叫該命令。 -
如果都沒有找到則從
$PATH
中尋找,為了避免每次遍歷$PATH
,shell 維護了一張HASH
表,記錄了每個命令對應的絕對路徑,如果 HASH 表中沒有再去$PATH
中的目錄遍歷,如果 PATH 中未找到就執行一個預定義的函式command_not_found_handle
。如果函式存在,則在子 shell
中呼叫,如果不存在則列印錯誤資訊並返回 127 狀態碼。 -
如果尋找成功或者 command 中含有
“/”
, shell 將在新環境中執行它( fork 一個新程序 )。 -
如果 command 不是非同步啟動的,shell 將等待其完成並收集退出狀態碼。
如上所述就是 shell 在執行命令式的查詢規則。也是時候破解一下我們在文章開頭留下的謎題了,先從 ./
開始吧。
./
在類 Unix 系統中表示相對路徑指向某個檔案或者目錄,因為在 Unix 系統中 PATH 不包含當前路徑,也無法包含當前路徑。如 ./test
、touch ./a
.
是 BASH 的一個內建命令,它繼承自 Bourne shell (sh)
,並且是 source
同義詞,跟 source
功能相同。
. filename [arguments]
的功能是在當前 shell 上下文中( 不會 fork )讀取並執行 filename
中的命令,如果 filename
不包含 “/”
, shell 將從 $PATH
中尋找該檔案,如果當前 shell 不是 POSIX
模式,則在 PATH
中尋找失敗後,繼續從當前目錄中尋找。
對於 sh ./test.sh
這種模式,在 bash 的文件中可以找到對應的描述 Invoked with name sh
(大多數 Linux 發行版會把 sh 設定成 bash 的軟連線,所以這裡只針對此種情況):
When invoked as sh, Bash enters POSIX mode after the startup files are read.
POSIX mode 我會在後面展開介紹,這裡暫且略去,開始進入 shell 如何執行一個 command 吧。
shell 是如何執行命令的
我在介紹 Systemd 和 cron 的時候用了 fork
這個詞,而在描述 shell 的時候僅僅說“shell 啟動相應程式”
。其實,shell 執行一個程式的方式也一樣使用了 fork
,我只是為了能在本章節重點作介紹才故意沒有使用 fork
這個詞。
我們知道, Linux 下的可執行檔案可以分為 ELF 檔案
和指令碼檔案
,當我們在 bash 下輸入一個命令執行某個 ELF 程式時,Linux 系統是如何裝載並執行它的呢?
首先,在使用者層面,bash 程序會呼叫 fork()
系統呼叫建立一個新的程序。其次,新的程序通過呼叫 execve()
系統呼叫來執行指定的 ELF 檔案。原先的 bash 程序繼續返回並等待剛才啟動的新程序結束,之後繼續等待使用者輸入命令。
當進入 execve()
系統呼叫之後,Linux 核心就開始進行真正的裝載工作。在核心中,execve()
系統呼叫相應的入口是 sys_execve()
。sys_execve()
進行一些引數的檢查複製之後,呼叫 do_execve()
。do_execve()
會首先查詢被執行的檔案,如果找到檔案,則讀取檔案的前 128 個位元組。
為什麼要先讀取檔案的前 128 個位元組?這是因為 Linux 支援的可執行檔案不止 ELF 一種,還包括 a.out
、Java 程式
、以 #!
開頭的指令碼程式。do_execve()
通過讀取前 128 個位元組來判斷檔案的格式。每種可執行檔案格式的開頭幾個位元組都是很特殊的,尤其是前4個位元組,被稱為 魔數
(Magic Number)。
我們用一段 C 程式來讀取一些各種檔案的前 4 個位元組:
|
|
-
ELF
我們編譯這段程式,並讀取程式自身
1 2 3
{15:32}~/Learing/c/src:master ✗ ➭ gcc -o read4bytes read4bytes.c {15:33}~/Learing/c/src:master ✗ ➭ ./read4bytes read4bytes 464C457F
可以看到輸出為
464C457F
,我們檢視ASCII 表,得出如下的對應關係:我的作業系統位元組序是小端法排序,因此,
ELF的可執行檔案格式的頭 4 個位元組為 0x7F、E、L、F
。 -
shell 指令碼
1 2
{15:33}~/Learing/c/src:master ✗ ➭ ./read4bytes ~/meta 622F2123
前 4 個位元組為
622F2123
,我們再查一下 ASCII 表的對應關係:翻轉一下就是
#!/b
,可以猜測如果我們多讀 7 個位元組,結果肯定是#!/bin/bash
.對於
python
、perl
、php
指令碼處理方式相同。 -
java class
1 2
{15:51}~/Learing/c/src:master ✗ ➭ ./read4bytes ~/java/HelloWorld.class BEBAFECA
《程式設計師的自我修養》
一書 6.5 章節介紹 Linux 裝載可執行檔案時,依次介紹了ELF
、java 可執行檔案
和#!
三種情況。ELF 的前4個位元組將 16 進位制轉換為 ASCII 字元是E
、L
、F
;但是 java 的 class 檔案則不同,由上可知讀出的前4個位元組的 16 進製表示為BEBAFECA
。因為是小端,所以 16 進位制的表示法剛好是CAFEBABE
,並不需要轉化成具體的字元,而書中介紹說“Java的可執行檔案格式的頭 4 個位元組為 c、a、f、e”
,我猜可能書中存在前後邏輯不一致的問題,除非真的存在所謂的java 可執行檔案
,如果有朋友瞭解,歡迎聯絡我,給我批評指正。關於 CAFEBABE的來源可以參見 wiki 上 James Gosling 的自白。
當 do_execve()
讀取了 128 個位元組的檔案頭部之後,呼叫 search_binary_handle()
去搜索和匹配合適的可執行檔案裝載處理過程。Linux 中所有被支援的可執行檔案格式都有相應的裝載處理過程,search_binary_handler()
會通過判斷頭部的魔數確定檔案的格式,並且呼叫相應的裝載處理過程。常見的可執行程式及其裝載處理過程的對應關係如下所示.
- ELF 可執行檔案:load_elf_binary()
- a.out 可執行檔案:load_aout_binary()
- 可執行指令碼程式:load_script()
有必要提一下 a.out, a.out 本身要追溯到更早的 Unix 時代,並且伴隨 Linux 的誕生至今在 Linux 中有將近 ~28 年的歷史。從 核心 5.1 開始, Linux 移除 a.out 格式的訊息,因為ELF 自 1994 年進入 Linux 1.0 以來,已經 ~25 年了,a.out 早已年久失修,而且現在基本上找不到能產生 a.out 格式的編譯器了。
大家可能會說,gcc 預設編譯生成的不就是 a.out 麼?非也,此 a.out 非彼 a.out。gcc 預設生成的 a.out 的實際格式也是 ELF,如果你按照剛才的方式讀取 a.out 的前四個位元組,你會發現同樣是 464C457F
,a.out 這個名字很大意義上屬於計算機歷史文化的沿襲,想了解更多可以參考為 a.out 舉行一個特殊的告別儀式。
我在這裡省去 load_elf_binary()
的過程,只需提一下其中一步會修改系統呼叫的返回地址為 ELF 檔案的入口地址,細節可以去參考《程式設計師的自我修養》
6.5 節。我們說當 load_elf_binary()
執行完畢之後,返回至 do_execve()
再返回至 sys_execve()
時,因為 load_elf_binary()
已經修改了返回地址,所以當 sys_execve()
系統呼叫從核心態返回到使用者態時,EIP 暫存器直接跳轉到了 ELF 程式的入口地址,於是開始執行新程式的程式碼指令, ELF 可執行檔案裝載完成。
是時候去破解我在文章開頭留下的問題了,我用 Go 程式通過 fork
和 exec
去執行指令碼的時候收穫到 fork/exec: exec format error
的錯誤。現在來看是 search_binary_handle()
的過程出了問題,核心並沒有識別到指令碼檔案格式,經查確認是我指令碼中沒有加入 Shebang,當我在首行增加了 #!/bin/bash
之後,程式便可以正確運行了。
我還沒有解釋為什麼在互動式的 shell 下執行不帶 Shebang
的指令碼不會觸發錯誤,因為說起我尋找答案的過程總讓我喟嘆不已,我是從一篇幾近 30 年前的文章中找到答案的。這讓我想到了 5 年前我學習 Oracle 調優時從一本 10 年前出版的書中獲益的經歷。我難以想象,我今天寫就的一篇博文,有可能會在 30 年後幫助到另一個人,這會讓我永葆寫作的熱情......
就是這篇 (Why do some scripts start with #! ... ?)寫於 1992 年的文章幫助我找到事實的真相。
簡單概括一下就是早在 Unix 時代,為了不讓核心什麼東西都拿來執行,程式設計師們發明了 “magic number”
,通過 magic number 核心可以辨別出哪些是可執行程式,在檔案不可執行時丟擲 ENOEXEC
錯誤,但是 shell 程式碼擴充了這項功能,在收到 ENOEXEC
失敗後會去使用 “/bin/sh”
嘗試將其作為 shell 指令碼去執行,所以指令碼執行是由 shell 來完成的,而不是核心,程式碼邏輯大概像這樣:
|
|
後來,伯克利的一些 guys 擴充了核心的功能,使其可以識別魔數 “#!”
,如果核心讀到 #!
則將繼續讀取該行的剩餘部分,並將其作為命令去執行檔案中的內容。
試想,當你執行一個沒有正確填寫 Shebang
的指令碼檔案的時候,shell 很可能會給你報一個沒有執行許可權的錯誤,當你依照錯誤提示給予 +x
許可權的時候,你很可能收到更多的錯誤,原因很可能是你正在編寫一個 python 指令碼。
乖乖的寫 Shebang
吧 !
事實上,後來我在 Bash 的文件中也找到了相關描述:
this execution fails because the file is not in executable format, and the file is not a directory, it is assumed to be a shell script and the shell executes it as described in Shell Scripts.
也許這一節描述沒有燃起你的興奮點,因為我假設你對 fork
和 exec
函式族以及虛擬記憶體
有所瞭解,如果你不瞭解的話可以參考下面我給出的連結:
- Unix/Linux fork前傳
- Linux fork那些隱藏的開銷
- Fork三部曲之clone的誕生
- 深入理解計算機系統 第9章 Virtual Memory
login、non-login、interactive 、non-interactive 與 Startup Files
因為 shell 可以執行在互動模式和非互動模式下,並且有 login 和 non-login 的情況,所以每一種組合他們讀取並執行的 Startup Files 都有所不同,下面我給出一幅圖來展示各種不同的情況:
所謂的 login & interactive
模式我舉兩個例子,一個是我們登入 Linux 字元介面的時候,輸入使用者名稱密碼進入的那個 shell 就是登入互動式的,另一個就是我們使用 sshd
服務遠端登入,在輸入使用者名稱密碼後獲得的 shell 也是登入互動式的。
對於非互動式的 shell 典型的情況就是執行指令碼啦,而在執行指令碼的時候可以通過新增 --login
或者 -l
的選項來使這個 shell 去讀取 Startup Files
,因為它沒有輸入口令的登入動作,只有讀取和執行 Startup Files 。
另外,你在 X Windows 下執行 terminal
軟體開啟的 shell 是 non-login & interactive
模式的。如果你曾有在視窗下開啟 shell 卻無法獲取 ~/bash_profile
中定義的變數的疑惑的話,現在你可以釋然了。
來做個實驗吧,我事先在 /etc/profile
、/etc/bashrc
、~/.bash_profile
、~/.bashrc
中增加了 echo “Hello from xxxx”
的語句,讓我們來看看各種情況下我們得到的 shell 到底執行了哪些檔案:
-
sshd
1 2 3 4 5 6
{11:07}~ ➭ ssh [email protected] Last login: Wed Dec 4 10:44:06 2019 from 192.168.1.183 Hello from /etc/profile Hello from /etc/bashrc Hello from ~/.bashrc Hello from ~/.bash_profile
~/.bash_profile 呼叫了 ~/.bashrc, ~/.bashrc 呼叫了 /etc/bashrc,所以 shell 呼叫的是/etc/profile 和 ~/.bash_profile
-
GUI Terminal
GUI 下開啟 shell 只運行了
~/.bashrc
。 -
執行 bash
1 2 3 4 5 6 7 8 9 10
[root@afis-db ~]# bash Hello from /etc/bashrc Hello from ~/.bashrc [root@afis-db ~]# exit exit [root@afis-db ~]# bash -l Hello from /etc/profile Hello from /etc/bashrc Hello from ~/.bashrc Hello from ~/.bash_profile
預設情況下,bash 命令進入的是一個非登入的互動式子 shell,當使用
-l
或--login
選項後進入的是登入的互動式子 shell 。 -
su
su
的功能是切換使用者,其中-
選項表示登入,一個登入的 shell 在 ps 中顯示為-bash
,非登入的顯示為bash
。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
[root@afis-db ~]# su - oracle Hello from /etc/profile Hello from /etc/bashrc Hello from ~/.bashrc Hello from ~/.bash_profile [oracle@afis-db ~]$ echo $$ 11935 [oracle@afis-db ~]$ ps -ef|grep 11935 oracle 11935 11934 0 13:22 pts/1 00:00:00 -bash oracle 11960 11935 0 13:22 pts/1 00:00:00 ps -ef oracle 11961 11935 0 13:22 pts/1 00:00:00 grep 11935 [oracle@afis-db ~]$ exit logout [root@afis-db ~]# su oracle Hello from /etc/bashrc Hello from ~/.bashrc [oracle@afis-db root]$ echo $$ 11965 [oracle@afis-db root]$ ps -ef|grep 11965 oracle 11965 11964 0 13:22 pts/1 00:00:00 bash oracle 11982 11965 4 13:22 pts/1 00:00:00 ps -ef oracle 11983 11965 0 13:22 pts/1 00:00:00 grep 11965
-
執行指令碼—非互動模式
1 2 3 4 5 6 7 8 9 10
[root@afis-db ~]# ./script.sh I am script! [root@afis-db ~]# bash script.sh I am script! [root@afis-db ~]# bash -l script.sh Hello from /etc/profile Hello from /etc/bashrc Hello from ~/.bashrc Hello from ~/.bash_profile I am script!
可見在非互動模式下不會讀取任何檔案,增加了登入選項則會依次讀取 Startup Files。
我們很少以 bash script.sh
這種方式執行指令碼,更多的是以 ./script.sh
執行,當以後一種方式執行時,真正執行指令碼的直譯器依賴於具體的 Shebang
。而我們經常看到使用 sh script.sh
這樣的方式執行,那 sh
究竟是什麼呢?
在大多數 Linux 發行版中,sh 通常是 bash 的軟連線
,但是 bash 文章中有如下描述:
If Bash is invoked with the name sh, it tries to mimic the startup behavior of historical versions of sh as closely as possible, while conforming to the POSIX standard as well.
When invoked as sh, Bash enters POSIX mode after the startup files are read.
意思是當你用 sh 來啟動 shell 的時候,bash 會以 posix 標準模式執行
,就如同呼叫了 bash --posix
。需要注意的是:sh
並不是一個具體的 shell 實現,而是一種規格標準,bash 在這種模式下執行的時候,將遵循 posix 的標準去讀取執行檔案。如下圖所示:
來實地驗證一下:
-
執行 sh
1 2 3 4 5 6
[root@afis-db ~]# sh sh-4.1# exit exit [root@afis-db ~]# sh -l Hello from /etc/profile Hello from ~/.profile.(this file is touched by me)
-
執行指令碼
1 2 3 4 5 6
[root@afis-db ~]# sh script.sh I am script! [root@afis-db ~]# sh -l script.sh Hello from /etc/profile Hello from ~/.profile.(this file is touched by me) I am script!
因為生產伺服器上使用 Bash 居多,而線上服務多少都依託於 shell 去呼叫,因為不同的呼叫方式下 shell 讀取執行檔案的規則不同,這樣就可能對應用造成一定程度的困擾。我曾經維護一個線上的 java 專案,這個專案有幾百個 java 服務需要每天定時重啟,專案上線時反覆檢查驗證了 cron 服務的配置以確保萬無一失,而沒有想到啟停指令碼對環境變數 JAVA_HOME
的依賴會在 cron 呼叫的時候失效。如今看來,只要使用登入非互動模式即可。
總結
本片文章細節太過零散,去驗證以及查閱資料花了不少時間,其實寫作的最初興奮點是想從 fork & exec
的角度去理解 Linux 上各種執行程式的方式,但是回頭一看,關於 fork 和 exec 的介紹只有寥寥幾筆,剩下的都是關於細節的追求與驗證,但是 Done is better than perfect
~
因能力有限,行文或有疏漏與錯誤之處,望閱讀本文的朋友給予斧正,也希望瞭解其它啟動方式的朋友不吝賜教。
參考文章:
- Why do some scripts start with #! ... ?
- Bash Reference Manual
- 淺析 Linux 初始化 init 系統——sysvinit
- 淺析 Linux 初始化 init 系統——Systemd
- Linux 的啟動流程
- LINUX PID 1 和 SYSTEMD
- Difference between a 'terminal', a 'shell', a 'tty' and a 'console'?
- 為什麼執行自己的程式時需要加上點斜槓
- Shebang
- Java class file
- 為 a.out 舉行一個特殊的告別儀式
- Linux cron執行原理
《程式設計師的自我修養》