(1)shell如何執行一個命令
Linux的命令分為兩類:一類是shell的內建命令;另一類則是獨立於shell的命令。別忘了,shell也只是系統中的一個程式而已,當它執行非內建命令時,本質上是在呼叫另一隻程式,比如ls。下面驗證一下:
m@meng:~/scripts$ which sh/bin/shm@meng:~/scripts$ file /bin/sh/bin/sh: symbolic link to `dash`m@meng:~/scripts$ file /bin/dash/bin/dash: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=1fff1896e5f8db5be4db7b7ebab6ee176129b399, strippedm@meng:~/scripts$ file /bin/ls/bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.24, BuildID[sha1]=25210dc46cdefbe29cf3d2b5ceef3eceb6e2a57e, stripped
這段代碼說明兩個問題: Ubuntu中sh預設連結的是dash;dash和ls是兩個獨立的ELF檔案,即可執行檔,這意味著二者的地位是等同的。從源碼的角度講,它們都有自己的main函數,編譯成二進位後都有自己的_start入口。所以,在shell中執行ls命令,shell需要另建一個進程,即調用fork();然後在新進程中調用exec()執行ls的代碼。
但是對於內建命令,shell執行時相當於調用自己的一個函數,即調用自己的一部分代碼,這顯然無需建立進程。Shell應該有辦法區分哪些命令是內建的,哪些不是。對人類來說,區分的辦法就是使用which命令,凡是找不到程式檔案所在的都是內建命令:
m@meng:~/scripts$ which cdm@meng:~/scripts$ which which /usr/bin/whichm@meng:~/scripts$ man cd沒有 cd 的手冊頁條目
cd竟然是內建命令。。。而且內建命令還沒有man手冊。查看內建命令的手冊應使用man builtins命令。
(2)shell如何執行一個指令碼
眾所周知,執行一個shell指令碼一般有兩種辦法:1、sh script.sh 2、chmod +x script.sh && ./script.sh
第一種辦法不需要指令碼具有可執行許可權,而且可以在sh後面加各種選項進行調試,其原理就是建立一個進程,在新進程中執行sh,同時把指令檔當做參數傳給sh,由sh逐行讀取指令碼中的命令,然後就像上面執行命令一樣。不過我也很好奇:為什麼不直接由當前這個shell來執行指令碼呢。幹嘛還要費事建立一個。下面我們就會知道。
第二種辦法需要給指令碼賦予可執行許可權(這很可能是兩種辦法的關鍵區別,可執行許可權到底意味著什麼。),然後似乎直接當ELF檔案給執行了。。。就像執行./a.out一樣,這是什麼情況。然後我們就要引入shebang的概念~
shell指令碼的第一行往往是這樣開頭的:
#!/bin/bash
“#!”被稱為shebang,可以說這是shell指令碼的標準起始行,第一行一般都這樣寫。它的作用是指明執行該指令碼所使用的程式,要注意的是,shebang後面的程式必須使用絕對路徑,而且不一定非要是sh、dash、csh等shell,也可以是任意一個可執行檔檔案,比如sed、rm這類命令;或者某個可執行檔指令檔。
有了shebang之後,直接執行指令碼的原理是這樣的(摘抄自《Linux C一站式編程》):
Shell會 fork 一個子進程並調用 exec 執行 ./script.sh 這個程式, exec 系統調用應該把子進程的程式碼片段替換成 ./script.sh 程式的程式碼片段,並從它的 _start 開始執行。然而 script.sh 是個文字檔,根本沒有程式碼片段和 _start 函數,怎麼辦呢?其實 exec 還有另外一種機制,如果要執行的是一個文字檔,並且第一行用Shebang指定瞭解釋器,則用解譯器程式的程式碼片段替換當前進程,並且從解譯器的 _start 開始執行,而這個文字檔被當作命令列參數傳給解譯器。
來個栗子:
m@meng:~/tmp$ lsonlyme tesh.shm@meng:~/tmp$ cat tesh.sh #!/bin/rm -f$0m@meng:~/tmp$ ./tesh.sh m@meng:~/tmp$ lsonlyme
test.sh的內容就是刪除它本身,它的shebang中的執行程式就是rm -f,而$0代表指令碼本身,所以指令碼執行之後,它消失了。。。
那麼,如果shebang中的執行程式不靠譜或者根本沒有shebang,還硬要用第二種辦法執行,會發生什麼呢。 解譯器沒有可執行許可權
m@meng:~/tmp$ ls -l onlyme -rw-rw-r-- 1 m m 6 6月 23 01:22 onlymem@meng:~/tmp$ cat test.sh #!/home/m/tmp/onlymeps -ef | grep $$ | grep -v grep | grep -v psm@meng:~/tmp$ ./test.sh bash: ./test.sh: /home/m/tmp/onlyme: 解譯器錯誤: 許可權不夠
結果報錯了。 解譯器有可執行許可權,但是內容不靠譜
m@meng:~/tmp$ ls onlyme -l-rwxrwxr-x 1 m m 6 6月 23 01:22 onlymem@meng:~/tmp$ cat onlyme hellom@meng:~/tmp$ cat test.sh #!/home/m/tmp/onlymeps -ef | grep $$ | grep -v grep | grep -v psm@meng:~/tmp$ ./test.sh m 31054 5944 0 02:59 pts/14 00:00:00 bash
預料中的報錯沒有出現,反而指令碼可以正常執行。這是因為,解譯器無法正常執行時(有執行許可權),父shell會接管指令碼。父shell就是輸入指令碼時所在的shell,我們可以驗證一下:
m@meng:~/tmp$ sh$ /home/m/tmp/test.shm 31093 31091 0 03:03 pts/14 00:00:00 /bin/sh /home/m/tmp/test.sh
輸入sh則進入了sh世界,當前的shell不再是bash了,然後執行指令碼(內容沒變,還是顯示當前進程使用的shell),發現做解譯器的shell已經變成了/Bin/sh了。 解譯器不存在
root@meng:/home/m/tmp# lsonlyme test.shroot@meng:/home/m/tmp# cat test.sh #!/home/m/tmp/onlyme2echo $SHELLroot@meng:/home/m/tmp# ./test.sh bash: ./test.sh: /home/m/tmp/onlyme2: 解譯器錯誤: 沒有那個檔案或目錄
沒有shebang這一行
m@meng:~/tmp$ cat test.sh ps -ef | grep $$ | grep -v grep | grep -v psm@meng:~/tmp$ ./test.sh m 30534 5944 0 02:14 pts/14 00:00:00 bashm@meng:~/tmp$ sh$ /home/m/tmp/test.shm 30551 30550 0 02:17 pts/14 00:00:00 /bin/sh /home/m/tmp/test.sh
跟第二種情況的結果一樣:父shell接管了。 執行指令碼時指定瞭解釋器
m@meng:~/tmp$ cat test.sh \#!/bin/bashps -ef | grep $$ | grep -v grep | grep -v psm@meng:~/tmp$ dash test.sh m 30610 5944 0 02:24 pts/14 00:00:00 dash test.sh
顯然,執行指令碼時指定的解譯器會覆蓋shebang的內容。
(3)命令列中臨時的指令碼
如果不想把一些命令組合寫進一個檔案中再執行(略麻煩),而且這個組合沒有長期儲存的必要,只是臨時的用一下,那麼可以使用分號把一堆命令擺在一行上然後執行:
m@meng:~/tmp$ lsonlyme test.shm@meng:~/tmp$ cd ..;lsblog new program test.sh tmp wine-git 公用的 視頻 文檔 音樂examples.desktop patches scripts timerecords VirtualBox VMs workspaces 模板 圖片 下載 案頭
這樣做的好處是,如果第一個命令花費時間很長,那麼使用分號將不必等待它執行完後再輸入第二條命令。可以提前輸入第二條命令,等第一條命令執行完後自動執行第二條。
但是,這樣做還不夠“指令碼化”,因為這些命令會改變父shell的環境,比如cd命令。而真正的指令碼是在一個新進程中執行的,執行完後返回到父shell中並不改變父shell的環境。為了達到這一點,需要將這些命令組合用圓括弧括起來:
m@meng:~/tmp$ (cd ..;ls)blog new program test.sh tmp wine-git 公用的 視頻 文檔 音樂examples.desktop patches scripts timerecords VirtualBox VMs workspaces 模板 圖片 下載 案頭m@meng:~/tmp$ lsonlyme test.sh
果然達到了指令碼的效果~
但是這種辦法有個缺點:長度有限。shell能接受的單行命令的長度是有限的,不過一般都是夠用的。