国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

程序執(zhí)行的一剎那

2018-02-24 15:41 更新

程序執(zhí)行的一剎那

當(dāng)我們在 Linux 下的命令行輸入一個命令之后,這背后發(fā)生了什么?

什么是命令行接口

用戶使用計算機有兩種常見的方式,一種是圖形化的接口(GUI),另外一種則是命令行接口(CLI)。對于圖形化的接口,用戶點擊某個圖標就可啟動后臺的某個程序;對于命令行的接口,用戶鍵入某個程序的名字就可啟動某個程序。這兩者的基本過程是類似的,都需要查找程序文件在磁盤上的位置,加載到內(nèi)存并通過不同的解釋器進行解析和運行。下面以命令行為例來介紹程序執(zhí)行一剎那發(fā)生的一些事情。

首先來介紹什么是命令行?命令行就是 Command Line,很直觀的概念就是系統(tǒng)啟動后的那個黑屏幕:有一個提示符,并有光標在閃爍的那樣一個終端,一般情況下可以用 CTRL+ALT+F1-6 切換到不同的終端;在 GUI 界面下也會有一些偽終端,看上去和系統(tǒng)啟動時的那個終端沒有什么區(qū)別,也會有一個提示符,并有一個光標在閃爍。就提示符和響應(yīng)用戶的鍵盤輸入而言,它們兩者在功能上是一樣的,實際上它們就是同一個東西,用下面的命令就可以把它們打印出來。

$ echo $SHELL # 打印當(dāng)前SHELL,當(dāng)前運行的命令行接口程序
/bin/bash
$ echo $$     # 該程序?qū)?yīng)進程ID,$$是個特殊的環(huán)境變量,它存放了當(dāng)前進程ID
1481
$ ps -C bash   # 通過PS命令查看
  PID TTY          TIME CMD
 1481 pts/0    00:00:00 bash

從上面的操作結(jié)果可以看出,當(dāng)前命令行接口實際上是一個程序,那就是 /bin/bash,它是一個實實在在的程序,它打印提示符,接受用戶輸入的命令,分析命令序列并執(zhí)行然后返回結(jié)果。不過 /bin/bash 僅僅是當(dāng)前使用的命令行程序之一,還有很多具有類似功能的程序,比如 /bin/ash, /bin/dash 等。不過這里主要來討論 bash,討論它自己是怎么啟動的,它怎么樣處理用戶輸入的命令等后臺細節(jié)?

/bin/bash 是什么時候啟動的

/bin/login

先通過 CTRL+ALT+F1 切換到一個普通終端下面,一般情況下看到的是 "XXX login: " 提示輸入用戶名,接著是提示輸入密碼,然后呢?就直接登錄到了我們的命令行接口。實際上正是你輸入正確的密碼后,那個程序才把 /bin/bash 給啟動了。那是什么東西提示 "XXX login:" 的呢?正是 /bin/login 程序,那 /bin/login 程序怎么知道要啟動 /bin/bash,而不是其他的 /bin/dash 呢?

/bin/login 程序?qū)嶋H上會檢查我們的 /etc/passwd 文件,在這個文件里頭包含了用戶名、密碼和該用戶的登錄 Shell。密碼和用戶名匹配用戶的登錄,而登錄 Shell 則作為用戶登錄后的命令行程序??纯?/etc/passwd 中典型的這么一行:

$ cat /etc/passwd | grep falcon
falcon:x:1000:1000:falcon,,,:/home/falcon:/bin/bash

這個是我用的帳號的相關(guān)信息哦,看到最后一行沒?/bin/bash,這正是我登錄用的命令行解釋程序。至于密碼呢,看到那個 x 沒?這個 x 說明我的密碼被保存在另外一個文件里頭 /etc/shadow,而且密碼是經(jīng)過加密的。至于這兩個文件的更多細節(jié),看手冊吧。

我們怎么知道剛好是 /bin/login 打印了 "XXX login" 呢?現(xiàn)在回顧一下很早以前學(xué)習(xí)的那個 strace 命令。我們可以用 strace 命令來跟蹤 /bin/login 程序的執(zhí)行。

跟上面一樣,切換到一個普通終端,并切換到 Root 用戶,用下面的命令:

$ strace -f -o strace.out /bin/login

退出以后就可以打開 strace.out 文件,看看到底執(zhí)行了哪些文件,讀取了哪些文件。從中可以看到正是 /bin/login 程序用 execve 調(diào)用了 /bin/bash 命令。通過后面的演示,可以發(fā)現(xiàn) /bin/login 只是在子進程里頭用 execve 調(diào)用了 /bin/bash,因為在啟動 /bin/bash 后,可以看到 /bin/login 并沒有退出。

/bin/getty

/bin/login 又是怎么起來的呢?

下面再來看一個演示。先在一個可以登陸的終端下執(zhí)行下面的命令。

$ getty 38400 tty8 linux

getty 命令停留在那里,貌似等待用戶的什么操作,現(xiàn)在切回到第 8 個終端,是不是看到有 "XXX login:" 的提示了。輸入用戶名并登錄,之后退出,回到第一個終端,發(fā)現(xiàn) getty 命令已經(jīng)退出。

類似地,也可以用 strace 命令來跟蹤 getty 的執(zhí)行過程。在第一個終端下切換到 Root 用戶。執(zhí)行如下命令:

$ strace -f -o strace.out getty 38400 tty8 linux

同樣在 strace.out 命令中可以找到該命令的相關(guān)啟動細節(jié)。比如,可以看到正是 getty 程序用 execve 系統(tǒng)調(diào)用執(zhí)行了 /bin/login 程序。這個地方,getty 是在自己的主進程里頭直接執(zhí)行了 /bin/login,這樣 /bin/login 將把 getty 的進程空間替換掉。

/sbin/init

這里涉及到一個非常重要的東西:/sbin/init,通過 man init 命令可以查看到該命令的作用,它可是“萬物之王”(init is the parent of all processes on the system)哦。它是 Linux 系統(tǒng)默認啟動的第一個程序,負責(zé)進行 Linux 系統(tǒng)的一些初始化工作,而這些初始化工作的配置則是通過 /etc/inittab 來做的。那么來看看 /etc/inittab 的一個簡單的例子吧,可以通過 man inittab 查看相關(guān)幫助。

需要注意的是,在較新版本的 Ubuntu 和 Fedora 等發(fā)行版中,一些新的 init 程序,比如 upstartsystemd 被開發(fā)出來用于取代 System V init,它們可能放棄了對 /etc/inittab 的使用,例如 upstart 會讀取 /etc/init/ 下的配置,比如 /etc/init/tty1.conf,但是,基本的配置思路還是類似 /etc/inittab,對于 upstartinit 配置,這里不做介紹,請通過 man 5 init 查看幫助。

配置文件 /etc/inittab 的語法非常簡單,就是下面一行的重復(fù),

id:runlevels:action:process
  • id 就是一個唯一的編號,不用管它,一個名字而言,無關(guān)緊要。

  • runlevels 是運行級別,這個還是比較重要的,理解運行級別的概念很有必要,它可以有如下的取值:

0 is halt.
1 is single-user.
2-5 are multi-user.
6 is reboot.

不過,真正在配置文件里頭用的是 1-5 了,而 06 非常特別,除了用它作為 init 命令的參數(shù)關(guān)機和重啟外,似乎沒有哪個“傻瓜”把它寫在系統(tǒng)的配置文件里頭,讓系統(tǒng)啟動以后就關(guān)機或者重啟。1 代表單用戶,而 2-5 則代表多用戶。對于 2-5 可能有不同的解釋,比如在 Slackware 12.0 上,2,3,5 被用來作為多用戶模式,但是默認不啟動 X windows (GUI接口),而 4 則作為啟動 X windows 的運行級別。

  • action 是動作,它也有很多選擇,我們關(guān)心幾個常用的

  • initdefault:用來指定系統(tǒng)啟動后進入的運行級別,通常在 /etc/inittab 的第一條配置,如:

id:3:initdefault:

這個說明默認運行級別是 3,即多用戶模式,但是不啟動 X window 的那種。

  • sysinit:指定那些在系統(tǒng)啟動時將被執(zhí)行的程序,例如:

si:S:sysinit:/etc/rc.d/rc.S

man inittab 中提到,對于 sysinit,boot 等動作,runlevels 選項是不用管的,所以可以很容易解讀這條配置:它的意思是系統(tǒng)啟動時將默認執(zhí)行 /etc/rc.d/rc.S 文件,在這個文件里可直接或者間接地執(zhí)行想讓系統(tǒng)啟動時執(zhí)行的任何程序,完成系統(tǒng)的初始化。

  • wait:當(dāng)進入某個特別的運行級別時,指定的程序?qū)⒈粓?zhí)行一次,init 將等到它執(zhí)行完成,例如:

rc:2345:wait:/etc/rc.d/rc.M

這個說明無論是進入運行級別 2,3,4,5 中哪一個,/etc/rc.d/rc.M 將被執(zhí)行一次,并且有 init 等待它執(zhí)行完成。

  • ctrlaltdel,當(dāng) init 程序接收到 SIGINT 信號時,某個指定的程序?qū)⒈粓?zhí)行,我們通常通過按下 CTRL+ALT+DEL,這個默認情況下將給 init 發(fā)送一個 SIGINT 信號。

如果我們想在按下這幾個鍵時,系統(tǒng)重啟,那么可以在 /etc/inittab 中寫入:

ca::ctrlaltdel:/sbin/shutdown -t5 -r now
  • respawn:這個指定的進程將被重啟,任何時候當(dāng)它退出時。這意味著沒有辦法結(jié)束它,除非 init 自己結(jié)束了。例如:

c1:1235:respawn:/sbin/agetty 38400 tty1 linux

這一行的意思非常簡單,就是系統(tǒng)運行在級別 1,2,3,5 時,將默認執(zhí)行 /sbin/agetty 程序(這個類似于上面提到的 getty 程序),這個程序非常有意思,就是無論什么時候它退出,init 將再次啟動它。這個有幾個比較有意思的問題:

  • 在 Slackware 12.0 下,當(dāng)默認運行級別為 4 時,只有第 6 個終端可以用。原因是什么呢?因為類似上面的配置,因為那里只有 1235,而沒有 4,這意味著當(dāng)系統(tǒng)運行在第 4 級別時,其他終端下的 /sbin/agetty 沒有啟動。所以,如果想讓其他終端都可以用,把 1235 修改為 12345 即可。

  • 另外一個有趣的問題就是:正是 init 程序在讀取這個配置行以后啟動了 /sbin/agetty,這就是 /sbin/agetty 的秘密。
  • 還有一個問題:無論退出哪個終端,那個 "XXX login:" 總是會被打印,原因是 respawn 動作有趣的性質(zhì),因為它告訴 init,無論 /sbin/agetty 什么時候退出,重新把它啟動起來,那跟 "XXX login:" 有什么關(guān)系呢?從前面的內(nèi)容,我們發(fā)現(xiàn)正是 /sbin/getty (同 agetty)啟動了 /bin/login,而 /bin/login 又啟動了 /bin/bash,即我們的命令行程序。

命令啟動過程追本溯源

init 程序作為“萬物之王”,它是所有進程的“父”(也可能是祖父……)進程,那意味著其他進程最多只能是它的兒子進程。而這個子進程是怎么創(chuàng)建的,fork 調(diào)用,而不是之前提到的 execve 調(diào)用。前者創(chuàng)建一個子進程,后者則會覆蓋當(dāng)前進程。因為我們發(fā)現(xiàn) /sbin/getty 運行時,init 并沒有退出,因此可以判斷是 fork 調(diào)用創(chuàng)建一個子進程后,才通過 execve 執(zhí)行了 /sbin/getty。

因此,可以總結(jié)出這么一個調(diào)用過程:

     fork     execve         execve         fork           execve
init --> init --> /sbin/getty --> /bin/login --> /bin/login --> /bin/bash

這里的 execve 調(diào)用以后,后者將直接替換前者,因此當(dāng)鍵入 exit 退出 /bin/bash 以后,也就相當(dāng)于 /sbin/getty 都已經(jīng)結(jié)束了,因此最前面的 init 程序判斷 /sbin/getty 退出了,又會創(chuàng)建一個子進程把 /sbin/getty 啟動,進而又啟動了 /bin/login,又看到了那個 "XXX login:"。

通過 pspstree 命令看看實際情況是不是這樣,前者打印出進程的信息,后者則打印出調(diào)用關(guān)系。

$ ps -ef | egrep "/sbin/init|/sbin/getty|bash|/bin/login"
root         1     0  0 21:43 ?        00:00:01 /sbin/init
root      3957     1  0 21:43 tty4     00:00:00 /sbin/getty 38400 tty4
root      3958     1  0 21:43 tty5     00:00:00 /sbin/getty 38400 tty5
root      3963     1  0 21:43 tty3     00:00:00 /sbin/getty 38400 tty3
root      3965     1  0 21:43 tty6     00:00:00 /sbin/getty 38400 tty6
root      7023     1  0 22:48 tty1     00:00:00 /sbin/getty 38400 tty1
root      7081     1  0 22:51 tty2     00:00:00 /bin/login --
falcon    7092  7081  0 22:52 tty2     00:00:00 -bash

上面的結(jié)果已經(jīng)過濾了一些不相干的數(shù)據(jù)。從上面的結(jié)果可以看到,除了 tty2 被替換成 /bin/login 外,其他終端都運行著 /sbin/getty,說明終端 2 上的進程是 /bin/login,它已經(jīng)把 /sbin/getty 替換掉,另外,我們看到 -bash 進程的父進程是 7081 剛好是 /bin/login 程序,這說明 /bin/login 啟動了 -bash,但是它并沒有替換掉 /bin/login,而是成為了 /bin/login 的子進程,這說明 /bin/login 通過 fork 創(chuàng)建了一個子進程并通過 execve 執(zhí)行了 -bash(后者通過 strace跟蹤到)。而 init 呢,其進程 ID 是 1,是 /sbin/getty/bin/login 的父進程,說明 init 啟動或者間接啟動了它們。下面通過 pstree 來查看調(diào)用樹,可以更清晰地看出上述關(guān)系。

$ pstree | egrep "init|getty|\-bash|login"
init-+-5*[getty]
     |-login---bash
     |-xfce4-terminal-+-bash-+-grep

結(jié)果顯示 init 是 5 個 getty 程序,login 程序和 xfce4-terminal 的父進程,而后兩者則是 bash 的父進程,另外我們執(zhí)行的 grep 命令則在 bash 上運行,是 bash 的子進程,這個將是我們后面關(guān)心的問題。

從上面的結(jié)果發(fā)現(xiàn),init 作為所有進程的父進程,它的父進程 ID 饒有興趣的是 0,它是怎么被啟動的呢?誰才是真正的“造物主”?

誰啟動了 /sbin/init

如果用過 Lilo 或者 Grub 這些操作系統(tǒng)引導(dǎo)程序,可能會用到 Linux 內(nèi)核的一個啟動參數(shù) init,當(dāng)忘記密碼時,可能會把這個參數(shù)設(shè)置成 /bin/bash,讓系統(tǒng)直接進入命令行,而無須輸入帳號和密碼,這樣就可以方便地把登錄密碼修改掉。

這個 init 參數(shù)是個什么東西呢?通過 man bootparam 會發(fā)現(xiàn)它的秘密,init 參數(shù)正好指定了內(nèi)核啟動后要啟動的第一個程序,而如果沒有指定該參數(shù),內(nèi)核將依次查找 /sbin/init/etc/init,/bin/init,/bin/sh,如果找不到這幾個文件中的任何一個,內(nèi)核就要恐慌(panic)了,并掛(hang)在那里一動不動了(注:如果 panic=timeout 被傳遞給內(nèi)核并且 timeout 大于 0,那么就不會掛住而是重啟)。

因此 /sbin/init 就是 Linux 內(nèi)核啟動的。而 Linux 內(nèi)核呢?是通過 Lilo 或者 Grub 等引導(dǎo)程序啟動的,LiloGrub 都有相應(yīng)的配置文件,一般對應(yīng) /etc/lilo.conf/boot/grub/menu.lst,通過這些配置文件可以指定內(nèi)核映像文件、系統(tǒng)根目錄所在分區(qū)、啟動選項標簽等信息,從而能夠讓它們順利把內(nèi)核啟動起來。

LiloGrub 本身又是怎么被運行起來的呢?有了解 MBR 不?MBR 就是主引導(dǎo)扇區(qū),一般情況下這里存放著 LiloGrub 的代碼,而誰知道正好是這里存放了它們呢?BIOS,如果你用光盤安裝過操作系統(tǒng)的話,那么應(yīng)該修改過 BIOS 的默認啟動設(shè)置,通過設(shè)置可以讓系統(tǒng)從光盤、硬盤、U 盤甚至軟盤啟動。正是這里的設(shè)置讓 BIOS 知道了 MBR 處的代碼需要被執(zhí)行。

那 BIOS 又是什么時候被起來的呢?處理器加電后有一個默認的起始地址,一上電就執(zhí)行到了這里,再之前就是開機鍵按鍵后的上電時序。

更多系統(tǒng)啟動的細節(jié),看看 man boot-scripts 吧。

到這里,/bin/bash 的神秘面紗就被揭開了,它只是系統(tǒng)啟動后運行的一個程序而已,只不過這個程序可以響應(yīng)用戶的請求,那它到底是如何響應(yīng)用戶請求的呢?

/bin/bash 如何處理用戶鍵入的命令

預(yù)備知識

在執(zhí)行磁盤上某個程序時,通常不會指定這個程序文件的絕對路徑,比如要執(zhí)行 echo 命令時,一般不會輸入 /bin/echo,而僅僅是輸入 echo。那為什么這樣 bash 也能夠找到 /bin/echo 呢?原因是 Linux 操作系統(tǒng)支持這樣一種策略:Shell 的一個環(huán)境變量 PATH 里頭存放了程序的一些路徑,當(dāng) Shell 執(zhí)行程序時有可能去這些目錄下查找。which 作為 Shell(這里特指 bash)的一個內(nèi)置命令,如果用戶輸入的命令是磁盤上的某個程序,它會返回這個文件的全路徑。

有三個東西和終端的關(guān)系很大,那就是標準輸入、標準輸出和標準錯誤,它們是三個文件描述符,一般對應(yīng)描述符 0,1,2。在 C 語言程序里,我們可以把它們當(dāng)作文件描述符一樣進行操作。在命令行下,則可以使用重定向字符>,<等對它們進行操作。對于標準輸出和標準錯誤,都默認輸出到終端,對于標準輸入,也同樣默認從終端輸入。

哪種命令先被執(zhí)行

在 C 語言里頭要寫一段輸入字符串的命令很簡單,調(diào)用 scanf 或者 fgets 就可以。這個在 bash 里頭應(yīng)該是類似的。但是它獲取用戶的命令以后,如何分析命令,如何響應(yīng)不同的命令呢?

首先來看看 bash 下所謂的命令,用最常見的 test 來作測試。

  • 字符串被解析成命令

    隨便鍵入一個字符串 test1, bash 發(fā)出響應(yīng),告知找不到這個程序:

  $ test1
  bash: test1: command not found
  • 內(nèi)置命令

    而當(dāng)鍵入 test 時,看不到任何輸出,唯一響應(yīng)是,新命令提示符被打印了:

  $ test
  $

查看 test 這個命令的類型,即查看 test 將被如何解釋, type 告訴我們 test 是一個內(nèi)置命令,如果沒有理解錯, test 應(yīng)該是利用諸如 case "test": do something;break; 這樣的機制實現(xiàn)的,具體如何實現(xiàn)可以查看 bash 源代碼。

  $ type test
  test is a shell builtin
  • 外部命令

    這里通過 which 查到 /usr/bin 下有一個 test 命令文件,在鍵入 test 時,到底哪一個被執(zhí)行了呢?

  $ which test
  /usr/bin/test

執(zhí)行這個呢?也沒什么反應(yīng),到底誰先被執(zhí)行了?

  $ /usr/bin/test

從上述演示中發(fā)現(xiàn)一個問題?如果輸入一個命令,這個命令要么就不存在,要么可能同時是 Shell 的內(nèi)置命令、也有可能是磁盤上環(huán)境變量 PATH 所指定的目錄下的某個程序文件。

考慮到 test 內(nèi)置命令和 /usr/bin/test 命令的響應(yīng)結(jié)果一樣,我們無法知道哪一個先被執(zhí)行了,怎么辦呢?把 /usr/bin/test 替換成一個我們自己的命令,并讓它打印一些信息(比如 hello, world! ),這樣我們就知道到底誰被執(zhí)行了。寫完程序,編譯好,命名為 test 放到 /usr/bin 下(記得備份原來那個)。開始測試:

鍵入 test ,還是沒有效果:

  $ test
  $

而鍵入絕對路徑呢,則打印了 hello, world! 誒,那默認情況下肯定是內(nèi)置命令先被執(zhí)行了:

  $ /usr/bin/test
  hello, world!

由上述實驗結(jié)果可見,內(nèi)置命令比磁盤文件中的程序優(yōu)先被 bash 執(zhí)行。原因應(yīng)該是內(nèi)置命令避免了不必要的 fork/execve 調(diào)用,對于采用類似算法實現(xiàn)的功能,內(nèi)置命令理論上有更高運行效率。

下面看看更多有趣的內(nèi)容,鍵盤鍵入的命令還有可能是什么呢?因為 bash 支持別名(alias)和函數(shù)(function),所以還有可能是別名和函數(shù),另外,如果 PATH 環(huán)境變量指定的不同目錄下有相同名字的程序文件,那到底哪個被優(yōu)先找到呢?

下面再作一些實驗,

  • 別名

    test 命名為 ls -l 的別名,再執(zhí)行 test ,竟然執(zhí)行了 ls -l ,說明別名(alias)比內(nèi)置命令(builtin)更優(yōu)先:

  $ alias test="ls -l"
  $ test
  total 9488
  drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
  -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz
  • 函數(shù)

    定義一個名叫 test 的函數(shù),執(zhí)行一下,發(fā)現(xiàn),還是執(zhí)行了 ls -l ,說明 function 沒有 alias 優(yōu)先級高:

  $ function test { echo "hi, I'm a function"; }
  $ test
  total 9488
  drwxr-xr-x 12 falcon falcon    4096 2008-02-21 23:43 bash-3.2
  -rw-r--r--  1 falcon falcon 2529838 2008-02-21 23:30 bash-3.2.tar.gz

把別名給去掉(unalias),現(xiàn)在執(zhí)行的是函數(shù),說明函數(shù)的優(yōu)先級比內(nèi)置命令也要高:

  $ unalias test
  $ test
  hi, I'm a function

如果在命令之前跟上 builtin ,那么將直接執(zhí)行內(nèi)置命令:

  $ builtin test

要去掉某個函數(shù)的定義,這樣就可以:

  $ unset test

通過這個實驗我們得到一個命令的別名(alias)、函數(shù)(function),內(nèi)置命令(builtin)和程序(program)的執(zhí)行優(yōu)先次序:

    先    alias --> function --> builtin --> program   后

實際上, type 命令會告訴我們這些細節(jié), type -a 會按照 bash 解析的順序依次打印該命令的類型,而 type -t 則會給出第一個將被解析的命令的類型,之所以要做上面的實驗,是為了讓大家加印象。

$ type -a test
test is a shell builtin
test is /usr/bin/test
$ alias test="ls -l"
$ function test { echo "I'm a function"; }
$ type -a test
test is aliased to `ls -l'
test is a function
test ()
{
    echo "I'm a function"
}
test is a shell builtin
test is /usr/bin/test
$ type -t test
alias

下面再看看 PATH 指定的多個目錄下有同名程序的情況。再寫一個程序,打印 hi, world!,以示和 hello, world! 的區(qū)別,放到 PATH 指定的另外一個目錄 /bin 下,為了保證測試的說服力,再寫一個放到另外一個叫 /usr/local/sbin 的目錄下。

先看看 PATH 環(huán)境變量,確保它有 /usr/bin,/bin/usr/local/sbin 這幾個目錄,然后通過 type -P-P 參數(shù)強制到 PATH 下查找,而不管是別名還是內(nèi)置命令等,可以通過 help type 查看該參數(shù)的含義)查看,到底哪個先被執(zhí)行。

$ echo $PATH/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games$ type -P test/usr/local/sbin/test

如上可以看到 /usr/local/sbin 下的先被找到。

/usr/local/sbin/test 下的給刪除掉,現(xiàn)在 /usr/bin 下的先被找到:

$ rm /usr/local/sbin/test
$ type -P test
/usr/bin/test

type -a 也顯示類似的結(jié)果:

$ type -a test
test is aliased to `ls -l'
test is a function
test ()
{
    echo "I'm a function"
}
test is a shell builtin
test is /usr/bin/test
test is /bin/test

因此,可以找出這么一個規(guī)律: Shell 從 PATH 列出的路徑中依次查找用戶輸入的命令。考慮到程序的優(yōu)先級最低,如果想優(yōu)先執(zhí)行磁盤上的程序文件 test 呢?那么就可以用 test -P 找出這個文件并執(zhí)行就可以了。

補充:對于 Shell 的內(nèi)置命令,可以通過 help command 的方式獲得幫助,對于程序文件,可以查看用戶手冊(當(dāng)然,這個需要安裝,一般叫做 xxx-doc), man command 。

這些特殊字符是如何解析的:|, >, <, &

在命令行上,除了輸入各種命令以及一些參數(shù)外,比如上面 type 命令的各種參數(shù) -a-P 等,對于這些參數(shù),是傳遞給程序本身的,非常好處理,比如 if , else 條件分支或者 switch,case 都可以處理。當(dāng)然,在 bash 里頭可能使用專門的參數(shù)處理函數(shù) getoptgetopt_long 來處理它們。

|> , < , & 等字符,則比較特別, Shell 是怎么處理它們的呢?它們也被傳遞給程序本身嗎?可我們的程序內(nèi)部一般都不處理這些字符的,所以應(yīng)該是 Shell 程序自己解析了它們。

先來看看這幾個字符在命令行的常見用法,

< 字符表示:把 test.c 文件重定向為標準輸入,作為 cat 命令輸入,而 cat 默認輸出到標準輸出:

$ cat < ./test.c
#include <stdio.h>

int main(void)
{
        printf("hi, myself!\n");
        return 0;
}

> 表示把標準輸出重定向為文件 test_new.c ,結(jié)果內(nèi)容輸出到 test_new.c

$ cat < ./test.c > test_new.c

對于 >< , >><< , <> 我們都稱之為重定向(redirect), Shell 到底是怎么進行所謂的“重定向”的呢?

這主要歸功于 dup/fcntl 等函數(shù),它們可以實現(xiàn):復(fù)制文件描述符,讓多個文件描述符共享同一個文件表項。比如,當(dāng)把文件 test.c 重定向為標準輸入時。假設(shè)之前用以打開 test.c 的文件描述符是 5 ,現(xiàn)在就把 5 復(fù)制為了 0 ,這樣當(dāng) cat 試圖從標準輸入讀出內(nèi)容時,也就訪問了文件描述符 5 指向的文件表項,接著讀出了文件內(nèi)容。輸出重定向與此類似。其他的重定向,諸如 >><< , <> 等雖然和 >< 的具體實現(xiàn)功能不太一樣,但本質(zhì)是一樣的,都是文件描述符的復(fù)制,只不過可能對文件操作有一些附加的限制,比如 >> 在輸出時追加到文件末尾,而 > 則會從頭開始寫入文件,前者意味著文件的大小會增長,而后者則意味文件被重寫。

那么 | 呢? | 被形象地稱為“管道”,實際上它就是通過 C 語言里頭的無名管道來實現(xiàn)的。先看一個例子,

$ cat < ./test.c  | grep hi
        printf("hi, myself!\n");

在這個例子中, cat 讀出了 test.c 文件中的內(nèi)容,并輸出到標準輸出上,但是實際上輸出的內(nèi)容卻只有一行,原因是這個標準輸出被“接到”了 grep 命令的標準輸入上,而 grep 命令只打印了包含 “hi” 字符串的一行。

這是怎么被“接”上的。 catgrep 作為兩個單獨的命令,它們本身沒有辦法把兩者的輸入和輸出“接”起來。這正是 Shell 自己的“杰作”,它通過 C 語言里頭的 pipe 函數(shù)創(chuàng)建了一個管道(一個包含兩個文件描述符的整形數(shù)組,一個描述符用于寫入數(shù)據(jù),一個描述符用于讀入數(shù)據(jù)),并且通過 dup/fcntlcat 的輸出復(fù)制到了管道的輸入,而把管道的輸出則復(fù)制到了 grep 的輸入。這真是一個奇妙的想法。

& 呢?當(dāng)你在程序的最后跟上這個奇妙的字符以后就可以接著做其他事情了,看看效果:

$ sleep 50 & #讓程序在后臺運行
[1] 8261

提示符被打印出來,可以輸入東西,讓程序到前臺運行,無法輸入東西了,按下 CTRL+Z ,再讓程序到后臺運行:

$ fg %1
sleep 50

[1]+  Stopped                 sleep 50

實際上 & 正是 Shell 支持作業(yè)控制的表征,通過作業(yè)控制,用戶在命令行上可以同時作幾個事情(把當(dāng)前不做的放到后臺,用 & 或者 CTRL+Z 或者 bg)并且可以自由地選擇當(dāng)前需要執(zhí)行哪一個(用 fg 調(diào)到前臺)。這在實現(xiàn)時應(yīng)該涉及到很多東西,包括終端會話(session)、終端信號、前臺進程、后臺進程等。而在命令的后面加上 & 后,該命令將被作為后臺進程執(zhí)行,后臺進程是什么呢?這類進程無法接收用戶發(fā)送給終端的信號(如 SIGHUPSIGQUIT ,SIGINT),無法響應(yīng)鍵盤輸入(被前臺進程占用著),不過可以通過 fg 切換到前臺而享受作為前臺進程具有的特權(quán)。

因此,當(dāng)一個命令被加上 & 執(zhí)行后,Shell 必須讓它具有后臺進程的特征,讓它無法響應(yīng)鍵盤的輸入,無法響應(yīng)終端的信號(意味忽略這些信號),并且比較重要的是新的命令提示符得打印出來,并且讓命令行接口可以繼續(xù)執(zhí)行其他命令,這些就是 Shell 對 & 的執(zhí)行動作。

還有什么神秘的呢?你也可以寫自己的 Shell 了,并且可以讓內(nèi)核啟動后就執(zhí)行它 l ,在 lilo 或者 grub 的啟動參數(shù)上設(shè)置 init=/path/to/your/own/shell/program 就可以。當(dāng)然,也可以把它作為自己的登錄 Shell ,只需要放到 /etc/passwd 文件中相應(yīng)用戶名所在行的最后就可以。不過貌似到現(xiàn)在還沒介紹 Shell 是怎么執(zhí)行程序,是怎樣讓程序變成進程的,所以繼續(xù)。

/bin/bash 用什么魔法讓一個普通程序變成了進程

當(dāng)我們從鍵盤鍵入一串命令,Shell 奇妙地響應(yīng)了,對于內(nèi)置命令和函數(shù),Shell 自身就可以解析了(通過 switch ,case 之類的 C 語言語句)。但是,如果這個命令是磁盤上的一個文件呢。它找到該文件以后,怎么執(zhí)行它的呢?

還是用 strace 來跟蹤一個命令的執(zhí)行過程看看。

$ strace -f -o strace.log /usr/bin/test
hello, world!
$ cat strace.log | sed -ne "1p"   #我們對第一行很感興趣
8445  execve("/usr/bin/test", ["/usr/bin/test"], [/* 33 vars */]) = 0

從跟蹤到的結(jié)果的第一行可以看到 bash 通過 execve 調(diào)用了 /usr/bin/test ,并且給它傳了 33 個參數(shù)。這 33 個 vars 是什么呢?看看 declare -x 的結(jié)果(這個結(jié)果只有 32 個,原因是 vars 的最后一個變量需要是一個結(jié)束標志,即 NULL)。

$ declare -x | wc -l   #declare -x聲明的環(huán)境變量將被導(dǎo)出到子進程中
32
$ export TEST="just a test"   #為了認證declare -x和之前的vars的個數(shù)的關(guān)系,再加一個
$ declare -x | wc -l
33
$ strace -f -o strace.log /usr/bin/test   #再次跟蹤,看看這個關(guān)系
hello, world!
$ cat strace.log | sed -ne "1p"
8523  execve("/usr/bin/test", ["/usr/bin/test"], [/* 34 vars */]) = 0

通過這個演示發(fā)現(xiàn),當(dāng)前 Shell 的環(huán)境變量中被設(shè)置為 export 的變量被復(fù)制到了新的程序里頭。不過雖然我們認為 Shell 執(zhí)行新程序時是在一個新的進程里頭執(zhí)行的,但是 strace 并沒有跟蹤到諸如 fork 的系統(tǒng)調(diào)用(可能是 strace 自己設(shè)計的時候并沒有跟蹤 fork ,或者是在 fork 之后才跟蹤)。但是有一個事實我們不得不承認:當(dāng)前 Shell 并沒有被新程序的進程替換,所以說 Shell 肯定是先調(diào)用 fork (也有可能是 vfork)創(chuàng)建了一個子進程,然后再調(diào)用 execve 執(zhí)行新程序的。如果你還不相信,那么直接通過 exec 執(zhí)行新程序看看,這個可是直接把當(dāng)前 Shell 的進程替換掉的。

exec /usr/bin/test

該可以看到當(dāng)前 Shell “嘩”(聽不到,突然沒了而已)的一下就沒有了。

下面來模擬一下 Shell 執(zhí)行普通程序。 multiprocess 相當(dāng)于當(dāng)前 Shell ,而 /usr/bin/test 則相當(dāng)于通過命令行傳遞給 Shell 的一個程序。這里是代碼:

/* multiprocess.c */
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>     /* sleep, fork, _exit */

int main()
{
    int child;
    int status;

    if( (child = fork()) == 0) {    /* child */
        printf("child: my pid is %d\n", getpid());
        printf("child: my parent's pid is %d\n", getppid());
        execlp("/usr/bin/test","/usr/bin/test",(char *)NULL);;
    } else if(child < 0){       /* error */
            printf("create child process error!\n");
            _exit(0);
    }                                                   /* parent */
    printf("parent: my pid is %d\n", getpid());
    if ( wait(&status) == child ) {
        printf("parent: wait for my child exit successfully!\n");
    }
}

運行看看,

$ make multiprocess
$ ./multiprocess
child: my pid is 2251
child: my parent's pid is 2250
hello, world!
parent: my pid is 2250
parent: wait for my child exit successfully!

從執(zhí)行結(jié)果可以看出,/usr/bin/testmultiprocess 的子進程中運行并不干擾父進程,因為父進程一直等到了 /usr/bin/test 執(zhí)行完成。

再回頭看看代碼,你會發(fā)現(xiàn) execlp 并沒有傳遞任何環(huán)境變量信息給 /usr/bin/test ,到底是怎么把環(huán)境變量傳送過去的呢?通過 man exec 我們可以看到一組 exec 的調(diào)用,在里頭并沒有發(fā)現(xiàn) execve ,但是通過 man execve 可以看到該系統(tǒng)調(diào)用。實際上 exec 的那一組調(diào)用都只是 libc 庫提供的,而 execve 才是真正的系統(tǒng)調(diào)用,也就是說無論使用 exec 調(diào)用中的哪一個,最終調(diào)用的都是 execve ,如果使用 execlp ,那么 execlp 將通過一定的處理把參數(shù)轉(zhuǎn)換為 execve 的參數(shù)。因此,雖然我們沒有傳遞任何環(huán)境變量給 execlp ,但是默認情況下,execlp 把父進程的環(huán)境變量復(fù)制給了子進程,而這個動作是在 execlp 函數(shù)內(nèi)部完成的。

現(xiàn)在,總結(jié)一下 execve ,它有有三個參數(shù),

- 第一個是程序本身的絕對路徑,對于剛才使用的 execlp ,我們沒有指定路徑,這意味著它會設(shè)法到 PATH 環(huán)境變量指定的路徑下去尋找程序的全路徑。 - 第二個參數(shù)是一個將傳遞給被它執(zhí)行的程序的參數(shù)數(shù)組指針。正是這個參數(shù)把我們從命令行上輸入的那些參數(shù),諸如 grep 命令的 -v 等傳遞給了新程序,可以通過 main 函數(shù)的第二個參數(shù) charargv[] 獲得這些內(nèi)容。 - 第三個參數(shù)是一個將傳遞給被它執(zhí)行的程序的環(huán)境變量,這些環(huán)境變量也可以通過 main 函數(shù)的第三個變量獲取,只要定義一個 charenv[] 就可以了,只是通常不直接用它罷了,而是通過另外的方式,通過 extern char ** environ 全局變量(環(huán)境變量表的指針)或者 getenv 函數(shù)來獲取某個環(huán)境變量的值。

當(dāng)然,實際上,當(dāng)程序被 execve 執(zhí)行后,它被加載到了內(nèi)存里,包括程序的各種指令、數(shù)據(jù)以及傳遞給它的各種參數(shù)、環(huán)境變量等都被存放在系統(tǒng)分配給該程序的內(nèi)存空間中。

我們可以通過 /proc/<pid>/maps 把一個程序?qū)?yīng)的進程的內(nèi)存映象看個大概。

$ cat /proc/self/maps   #查看cat程序自身加載后對應(yīng)進程的內(nèi)存映像
08048000-0804c000 r-xp 00000000 03:01 273716     /bin/cat
0804c000-0804d000 rw-p 00003000 03:01 273716     /bin/cat
0804d000-0806e000 rw-p 0804d000 00:00 0          [heap]
b7c46000-b7e46000 r--p 00000000 03:01 87528      /usr/lib/locale/locale-archive
b7e46000-b7e47000 rw-p b7e46000 00:00 0
b7e47000-b7f83000 r-xp 00000000 03:01 466875     /lib/libc-2.5.so
b7f83000-b7f84000 r--p 0013c000 03:01 466875     /lib/libc-2.5.so
b7f84000-b7f86000 rw-p 0013d000 03:01 466875     /lib/libc-2.5.so
b7f86000-b7f8a000 rw-p b7f86000 00:00 0
b7fa1000-b7fbc000 r-xp 00000000 03:01 402817     /lib/ld-2.5.so
b7fbc000-b7fbe000 rw-p 0001b000 03:01 402817     /lib/ld-2.5.so
bfcdf000-bfcf4000 rw-p bfcdf000 00:00 0          [stack]
ffffe000-fffff000 r-xp 00000000 00:00 0          [vdso]

關(guān)于程序加載和進程內(nèi)存映像的更多細節(jié)請參考《C 語言程序緩沖區(qū)注入分析》。

到這里,關(guān)于命令行的秘密都被“曝光”了,可以開始寫自己的命令行解釋程序了。

關(guān)于進程的相關(guān)操作請參考《進程與進程的基本操作》

補充:上面沒有討論到一個比較重要的內(nèi)容,那就是即使 execve 找到了某個可執(zhí)行文件,如果該文件屬主沒有運行該程序的權(quán)限,那么也沒有辦法運行程序。可通過 ls -l 查看程序的權(quán)限,通過 chmod 添加或者去掉可執(zhí)行權(quán)限。

文件屬主具有可執(zhí)行權(quán)限時才可以執(zhí)行某個程序:

$ whoami
falcon
$ ls -l hello  #查看用戶權(quán)限(第一個x表示屬主對該程序具有可執(zhí)行權(quán)限
-rwxr-xr-x 1 falcon users 6383 2000-01-23 07:59 hello*
$ ./hello
Hello World
$ chmod -x hello  #去掉屬主的可執(zhí)行權(quán)限
$ ls -l hello
-rw-r--r-- 1 falcon users 6383 2000-01-23 07:59 hello
$ ./hello
-bash: ./hello: Permission denied

參考資料

  • Linux 啟動過程:man boot-scripts
  • Linux 內(nèi)核啟動參數(shù):man bootparam
  • man 5 passwd
  • man shadow
  • 《UNIX 環(huán)境高級編程》,進程關(guān)系一章
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號