• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            興海北路

            ---男兒仗劍自橫行
            <2014年9月>
            31123456
            78910111213
            14151617181920
            21222324252627
            2829301234
            567891011

            統計

            • 隨筆 - 85
            • 文章 - 0
            • 評論 - 17
            • 引用 - 0

            常用鏈接

            留言簿(6)

            隨筆分類

            隨筆檔案

            收藏夾

            全是知識啊

            搜索

            •  

            最新評論

            閱讀排行榜

            評論排行榜

            Linux命令行上程序執行的那一剎那!
            by falcon<zhangjinw@gmail.com>
            2008-02-15

                (這一小節應該是作為《shell編程范例之進程操作》的一些補充性質的內容。)

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

            1、什么是命令行接口

                用戶使用計算機有兩種常見的方式,一種是圖形化的接口(GUI),另外一種則是命令行接口(CLI)。對于圖形化的接口,用戶點擊某個圖標就可啟動后 臺的某個程序;對于命令行的接口,用戶鍵入某個程序的名字就可啟動某個程序。這兩者的基本過程是類似的,都需要查找程序文件在磁盤上的位置,加載到內存并 通過不同的解釋器進行解析和運行。下面以命令行為例來介紹程序執行那一剎那發生的一些事情。
                首先來介紹什么是命令行?命令行就是command line,很直觀的概念就是系統啟動后的那個黑屏幕:有一個提示符,并有光標在閃爍的那樣一個終端,一般情況下可以用CTRL+ALT+F1-6切換到不同的終端;在GUI界 面下也會有一些偽終端,看上去和系統啟動時的那個終端沒有什么區別,也會有一個提示符,并有一個光標在閃爍。就提示符和響應用戶的鍵盤輸入而言,它們兩者 在功能上是一樣的,實際上它們就是同一個東西,你用下面的命令就可以把它們打印出來。
            Quote:

            $ echo $SHELL   #打印當前SHELL,當前運行的命令行接口程序
            /bin/bash
            $ echo $$   #該程序對應的進程ID,$$是一個比較特殊的環境變量,它存放了當前進程ID
            1481
            $ ps -C bash   #通過PS命令查看
              PID TTY          TIME CMD
             1481 pts/0    00:00:00 bash


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

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

            1.2.1 /bin/bash

               先通過CTRL+ALT+F1切換到一個普通終端下面,一般情況下看到的是XXX login: 提示輸入用戶名,接著是提示輸入密碼,然后呢?就直接登錄到了我們的命令 行接口。實際上正是你輸入正確的密碼后,那個程序把/bin/bash給啟動了。那是什么東西提示"XXX login:"的呢?正是/bin/login程序,那/bin/login程序怎么知道要啟動/bin/bash,而不是其他的/bin/tcsh呢?
                /bin/login程序實際上會檢查我們的/etc/passwd文件,在這個文件里頭包含了用戶名、密碼和該用戶的登錄shell。密碼和用戶名匹配用戶的登錄,而登錄shell則作為用戶登錄后的命令行程序。看看/etc/passwd中典型的這么一行:
            Quote:

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


                這個是我用的帳號的相關信息哦,看到最后一行沒?/bin/bash,這正是我登錄用的命令行解釋程序。至于密碼呢,看到那個x沒?這個x說明我的密碼被 保存在另外一個文件里頭/etc/shadow,而且密碼是經過加密的。至于這兩個文件的更多細節,看manual吧。
                我們怎么知道剛好是/bin/login打印了"XXX login"呢?現在回顧一下很早以前學習的那個strace命令。我們可以用strace命令來跟蹤/bin/login程序的執行。
                跟上面一樣,切換到一個普通終端,并切換到root用戶,用下面的命令:
            Quote:

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


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

            1.2.2 /bin/login

                那/bin/login又是怎么起來的呢?
                下面再來看一個演示。先在一個可以登陸的終端下執行下面的命令。
            Quote:

            $ getty 38400 tty8 linux


                getty命令停留在那里,貌似等待用戶的什么操作,現在切回到第8個終端,是不是看到有"XXX login:"的提示了。輸入用戶名并登錄,之后退出,回到第一個終端,發現getty命令已經退出。
                類似地,我們也可以用strace命令來跟蹤getty的執行過程。在第一個終端下切換到root用戶。執行如下命令,
            Quote:

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


                同樣在strace.out命令中可以找到該命令的相關啟動細節。比如,我們可以看到正是getty程序用execve系統調用執行了 /bin/login程序。這個地方,getty是在自己的主進程里頭直接執行了/bin/login,這樣/bin/login將把getty的進程空 間替換掉。

            1.2.3 /sbin/getty

                這里涉及到一個非常重要的東西了:/sbin/init,通過man init你可以查看到該命令的作用,它可是“萬物之王”(init  is  the  parent of all processes on the system)哦。它是Linux系統默認啟動的第一個程序,負責進行Linux系統的一些初始化工作,而這些初始化工作的配置則是通過 /etc/inittab來做的。那么來看看/etc/inittab的一個簡單的example吧,可以通過man inittab查看相關幫助。
                整個配置文件的語法非常簡單,就是下面一行的重復,
            Quote:

            id:runlevels:action:process


               
          1. id就是一個唯一的編號,不用管它,一個名字而言,無關緊要。
               
          2. runlevels是運行級別,整個還是比較重要的,理解運行級別的概念很有必要,它可以有如下的取值,
            Quote:

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


                不過,真正在配置文件里頭用的是1-5了,而0和6非常特別,除了用它作為init命令的參數關機和重啟外,似乎沒有哪個“傻瓜”把它寫在系統的配置文件 里頭,讓系統啟動以后就關機或者重啟。1代表單用戶,而2-5則代表多用戶。對于2-5可能有不同的解釋,比如在slackware 12.0上,2,3,5被用來作為多用戶模式,但是默認不啟動X windows(GUI接口),而4則作為啟動X windows的運行級別。
               
          3. action是動作,它也有很多選擇,我們關心幾個常用的
                initdefault 用來指定系統啟動后進入的運行級別,通常在/etc/inittab的第一條配置,如
            Quote:

                    id:3:initdefault:


                這個說明默認運行級別是3,即多用戶模式,但是不啟動X window的那種。
                sysinit 指定那些在系統啟動時將被執行的程序,例如
            Quote:

                si:S:sysinit:/etc/rc.d/rc.S
            [quote]
                在man inittab中提到,對于sysinit,boot等動作,runlevels選項是不用管的,所以我們可以很容易解讀這條配置:它的意思是系統啟動時 將默認執行/etc/rc.d/rc.S文件,在這個文件里你可直接或者間接的執行你想讓系統啟動時執行的任何程序,完成系統的初始化。
                wait,當進入某個特別的運行級別時,指定的程序將被執行一次,init將等到它執行完成,例如
            [quote]
            rc:2345:wait:/etc/rc.d/rc.M


                這個說明無論是進入運行級別2,3,4,5中哪一個,/etc/rc.d/rc.M將被執行一次,并且有init等待它執行完成。
                ctrlaltdel,當init程序接收到SIGINT信號時,某個指定的程序將被執行,我們通常通過按下CTRL+ALT+DEL,這個默認情況下將 給init發送一個SIGINT信號。如果我們想在按下這幾個鍵時,系統重啟,那么可以在/etc/inittab中寫入,
            Quote:

            ca::ctrlaltdel:/sbin/shutdown -t5 -r now


                respawn,這個指定的進程將被重啟,任何時候當它退出時。這意味著你沒有辦法結束它,除非init自己結束了。例如,
            Quote:

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


                這一行的意思非常簡單,就是系統運行在級別1,2,3,5時,將默認執行/sbin/agetty程序(這個類似于上面提到的getty程序),這個程序非常有意思,就是無論什么時候它退出,init將再次啟動它。這個有幾個比較有意思的問題:
                在slackware 12.0下,當你把默認運行級別修改為4的時候,只有第6個終端可以用。原因是什么呢?因為類似上面的配置,因為那里只有1235,而沒有4,這意味著當 系統運行在第4級別時,其他終端下的/sbin/agetty沒有啟動。所以,如果想讓其他終端都可以用,把1235修改為12345即可。
                另外一個有趣的問題就是:正是init程序在讀取這個配置行以后啟動了/sbin/agetty,這就是我們的/sbin/agetty的秘密。
                還有一個問題:無論我們退出哪個終端,那個"XXX login:"總是會被打印,原因是respawn動作有趣的性質,因為它告訴init,無論/sbin/agetty什么時候退出,重新把它啟動起來, 那跟"XXX login:"有什么關系呢?從前面的內容,我們發現正是/sbin/getty(同agetty)啟動了/bin/login,而/bin/login 有啟動了/bin/bash,即我們的命令行程序。
                而init程序作為“萬物之王”,它是所有進程的“父”(也可能是祖父……)進程,那意味著其他進程最多只能是它的兒子進程。而這個子進程是怎么創建的, fork調用,而不是之前提到的execve調用。前者創建一個子進程,后者則會覆蓋當前進程。因為我們發現/sbin/getty運行時,init并沒 有退出,因此可以判斷是fork調用創建一個子進程后,才通過execve執行了/sbin/getty。
                因此,我們可以總結出這么一個調用過程:
            Quote:

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


                這里的execve調用以后,后者將直接替換前者,因此當我們鍵入exit退出/bin/bash以后,也就相當于/sbin/getty都已經結束了, 因此最前面的init程序判斷/sbin/getty退出了,又會創建一個子進程把/sbin/getty啟動,進而又啟動了/bin/login,又看 到了那個"XXX login:"。
                通過ps和pstree命令看看實際情況是不是這樣,前者打印出進程的信息,后者則打印出調用關系。
            Quote:

            $ 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


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

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


                結果顯示init是5個getty程序,login程序和xfce4-terminal的父進程,而后兩者則是bash的父進程,另外我們執行的grep命令則在bash上運行,是bash的子進程,這個將是我們后面關心的問題。
                從上面的結果發現,init作為所有進程的父進程,它的父進程ID饒有興趣的是0,它是怎么被啟動的呢?誰才是真正的“造物主”?

            1.2.4 誰啟動了/sbin/init

                如果用過Lilo或者Grub這兩個操作系統引導程序,你可能會用到Linux內核的一個啟動參數init,當你忘記密碼時,可能會把這個參數設置成/bin/bash,讓系統直接進入命令行,而無須輸入帳號和密碼,這樣就可以方便地登錄密碼修改掉。
                這個init參數是個什么東西呢?通過man bootparam會發現它的秘密,init參數正好指定了內核啟動后要啟動的第一個程序,而如果沒有指定該參數,內核將依次查找/sbin/init, /etc/init, /bin/init, /bin/sh,如果找不到這幾個文件中的任何一個,內核就要恐慌(panic)了,并呆在那里一動不動了。
                因此/sbin/init就是Linux內核啟動的。而Linux內核呢?是通過Lilo或者Grub等引導程序啟動的,Lilo和Grub都有相應的配 置文件,一般對應/etc/lilo.conf和/boot/grub/menu.lst,通過這些配置文件可以指定內核映像文件、系統根目錄所在分區、 啟動選項標簽等信息,從而能夠讓它們順利把內核啟動起來。
                那Lilo和Grub本身又是怎么被運行起來的呢?還記得以前介紹的MBR不?MBR就是主引導扇區,一般情況下這里存放著Lilo和Grub的代碼,而 誰知道正好是這里存放了它們呢?BIOS,如果你用光盤安裝過操作系統的話,那么應該修改過BIOS的默認啟動設置,通過設置你可以讓系統從光盤、硬盤甚 至軟盤啟動。正是這里的設置讓BIOS知道了MBR處的代碼需要被執行。
                那BIOS又是什么時候被起來的呢?加電自檢就執行到了這里。
                更多系統啟動的細節,看看 "man boot-scripts" 吧。

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

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

            1.3.0 預備知識

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

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

            1.3.1 哪種命令先被執行

                在C語言里頭要寫一段輸入字符串的命令很簡單,調用scanf或者fgets就可以。這個在bash里頭應該是類似的。但是它獲取用戶的命令以后,如何分析命令,如何響應不同的命令呢?
                首先來看看bash下所謂的命令,用最常見的test來作測試。
            Quote:

            $ test1   #隨便鍵入一個字符串test1,bash發出響應,告訴我們找不到這個程序
            bash: test1: command not found
            $ test    #當我們鍵入test的時候,看不到任何輸出,唯一的響應是,新的命令提示符被打印了
            $ type test
            test is a shell builtin
            #查看test這個命令的類型,即查看test將被如何解釋,type告訴我們test是一個內置命令,如果沒有理解錯,test應該是利用諸如case "test": do something;break;這樣的機制實現的。
            $ which test   #通過which查到/usr/bin下有一個test命令文件,在鍵入test時,到底哪一個被執行了呢?
            /usr/bin/test
            $ /usr/bin/test   #執行這個呢?也沒什么反應,到底誰先被執行了?


                從上面的演示中發現一個問題?如果輸入一個命令,這個命令要么就不存在,要么可能同時是shell的內置命令、也有可能是磁盤上環境變量PATH所指定的目錄下的某個程序文件。
                考慮到test內置命令和/usr/bin/test命令的響應結果一樣,我們無法知道哪一個先被執行了,怎么辦呢?把/usr/bin/test替換成 一個我們自己的命令,并讓它打印一些信息(比如hello,world!),這樣我們就知道到底誰被執行了。寫完程序,編譯好,命名為test放到 /usr/bin下(記得備份原來那個)。開始測試:
            Quote:

            $ test #鍵入test,還是沒有效果
            $ /usr/bin/test   #而鍵入絕對路徑呢,則打印了hello, world!誒,那默認情況下肯定是內置命令先被執行了
            hello, world!


                總結:內置命令比磁盤文件中的程序優先被bash執行

                下面看看更多有趣的東西,鍵盤鍵入的命令還有可能是什么呢?因為bash支持別名和函數,所以還有可能是別名和函數,另外,如果PATH環境變量指定的不同目錄下有相同名字的程序文件,那到底哪個被優先找到呢?
                下面再作一些實驗,
            Quote:

            $ alias test="ls -l"   #把test命名為ls -l的別名
            $ test                 #再執行test,竟然執行了ls -l,而不是什么也沒有,說明alias比內置命令更優先
            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
            $ function test { echo "hi, I'm a function"; }   #定義一個名叫test的函數
            $ test   #執行一下,發現,還是執行了ls -l,說明function沒有alias優先級高
            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 test   #把別名給去掉
            $ test         #現在執行的是函數,說明函數的優先級比內置命令也要高
            hi, I'm a function
            $ builtin test #如果在命令之前跟上builtin,那么將直接執行內置命令
            $ unset test   #要去掉某個函數的定義,這樣就可以


                通過這個實驗我們得到一個命令的別名(alias)、函數(function),內置命令(builtin)和程序(program)的執行優先次序:
            Quote:

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


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

            $ 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!"的區別,放到PATH指定的另外一個目錄/bin下,為了保證測試的說服力,再寫一個放到另外一個叫/usr/local/sbin的目錄 下。
                先看看PATH環境變量,確保它有/usr/bin,/bin和/usr/local/sbin這幾個目錄,然后通過type -P(-P參數強制到PATH下查找,而不管是別名還是內置命令等,可以通過help type查看該參數的含義)查看,到底哪個先被執行。

            Quote:

            $ echo $PATH
            /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games
            $ type -P test   #可以看到/usr/local/sbin下的先被找到
            /usr/local/sbin/test
            $ rm /usr/local/sbin/test #把/usr/local/sbin/test下的給刪除掉
            $ type -P test   #現在/usr/bin下的先被找到
            /usr/bin/test
            $ type -a test #type -a也顯示類似的結果
            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


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

            補充:對于shell的內置命令,可以通過help command的方式獲得幫助,對于程序文件,可以查看用戶手冊(當然,這個需要安裝,一般叫做xxx-doc),man command。關于用戶手冊安裝辦法見在Linux下學會查看Man文檔

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

                在命令行上,除了輸入各種命令以及一些參數外,比如上面type命令的各種參數-a, -P等,對于這些參數,是傳遞給程序本身的,非常好處理,比如if,else條件分之或者switch, case都可以處理。當然,在bash里頭可能使用專門的參數處理函數getopt和getopt_long來處理它們。
                而|,>,<,&等字符,則比較特別,shell是怎么處理它們的呢?它們也被傳遞給程序本身嗎?可我們的程序內部一般都不處理這些字符的,所以應該是shell程序自己解析了它們。
                先來看看這幾個字符在命令行的常見用法,
            Quote:

            $ cat < ./test.c  #<字符表示:把test.c文件重定向為標準輸入,作為cat命令的輸入,而cat默認把內容輸出到標準輸出。
            #include <stdio.h>

            int main(void)
            {
                    printf("hi, myself!\n");
                    return 0;
            }
            $ cat < ./test.c > test_new.c #>表示把標準輸出重定向為文件test_new.c,結果內容輸出到test_new.c


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

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

            Quote:

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



                在這個例子中,cat讀出了test.c文件中的內容,并輸出到標準輸出上,但是實際上輸出的內容卻只有一行,原因是這個標準輸出被“接到”了grep命令的標準輸入上,而grep命令只打印了包含“hi”字符串的一行。
                這是怎么被“接”上的。cat和grep作為兩個單獨的命令,它們本身沒有辦法把兩者的輸入和輸出“接”起來。這正是shell自己的“杰作”,它通過C 語言里頭的pipe函數創建了一個管道(一個包含兩個文件描述符的整形數組,一個描述符用于寫入數據,一個描述符用于讀入數據),并且通過 dup/fcntl把cat的輸出復制到了管道的輸入,而把管道的輸出則復制到了grep的輸入。這真是一個奇妙的想法。
               
                那&呢?當你在程序的最后跟上這個奇妙的字符以后就可以接著做其他事情了,看看效果,
            Quote:

            $ sleep 50 &   #讓程序在后臺運行
            [1] 8261
            $ fg %1      #提示符被打印出來,可以輸入東西,讓程序到前臺運行,無法輸入東西了,按下CTRL+Z,再讓程序到后臺運行
            sleep 50

            [1]+  Stopped                 sleep 50
            $ fg %1   #再調到前臺
            sleep 50


                實際上&正是shell支持作業控制的表征,通過作業控制,用戶在命令行上可以同時作幾個事情(把當前不做的放到后臺,用&或者CTRL +Z或者bg)并且可以自由的選擇當前需要執行哪一個(用fg調到前臺)。這在實現時應該涉及到很多東西,包括終端會話(session)、終端信號、前 臺進程、后臺進程等。而在命令的后面加上&后,該命令將被作為后臺進程執行,后臺進程是什么呢?這類進程無法接收用戶發送給終端的信號(如 SIGHUP,SIGQUIT,SIGINT),無法響應鍵盤輸入(被前臺進程占用著),不過可以通過fg切換到前臺而享受作為前臺進程具有的特權。
                因此,當一個命令被加上&執行后,shell必須讓它具有后臺進程的特征,讓它無法響應鍵盤的輸入,無法響應終端的信號(意味忽略這些信號),并 且比較重要的是新的命令提示符得打印出來,并且讓命令行接口可以繼續執行其他命令,這些就是shell對&的執行動作。

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

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

                當我們從鍵盤鍵入一串命令,shell奇妙的響應了,對于內置命令和函數,shell自身就可以解析了(通過switch case之類的C語言語句)。但是,如果這個命令是磁盤上的一個文件呢。它找到該文件以后,怎么執行它的呢?
                還是用strace來跟蹤一個命令的執行過程看看。
            Quote:

            $ 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


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

            Quote:

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



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

            Quote:

            exec /usr/bin/test


                應該可以看到當前shell“嘩”(聽不到,突然沒了而已)的一下就沒有了。
                下面來模擬一下shell執行普通程序。multiprocess相當于當前shell,而/usr/bin/test則相當于通過命令行傳遞給shell的一個程序。這里是代碼:



            Code:

            [Ctrl+A Select All]


                運行看看,
            Quote:

            $ 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!


                從執行結果可以看出,/usr/bin/test在multiprocess的子進程中運行并不干擾父進程,因為父進程一直等到了/usr/bin/test執行完成。
                再回頭看看代碼,你會發現execlp并沒有傳遞任何環境變量信息給/usr/bin/test,到底是怎么把環境變量傳送過去的呢?通過man exec我們可以看到一組exec的調用,在里頭并沒有發現execve,但是通過man execve可以看到該系統調用。實際上exec的那一組調用都只是libc庫提供的,而execve才是真正的系統調用,也就是說無論使用exec調用 中的哪一個,最終調用的都是execve,如果使用execlp,那么execlp將通過一定的處理把參數轉換為execve的參數。因此,雖然我們沒有 傳遞任何環境變量給execlp,但是默認情況下,execlp把父進程的環境變量復制給了子進程,而這個動作是在execlp函數內部完成的。
                現在,總結一下execve,它有有三個參數,
                第一個是程序本身的絕對路徑,對于剛才使用的execlp,我們沒有指定路徑,這意味著它會設法到PATH環境變量指定的路徑下去尋找程序的全路徑。
                第二個參數是一個將傳遞給被它執行的程序的參數數組指針。正是這個參數把我們從命令行上輸入的那些參數,諸如grep命令的-v等傳遞給了新程序,可以通過main函數的第二個參數char *argv[]獲得這些內容。
                第三個參數是一個將傳遞給被它執行的程序的環境變量,這些環境變量也可以通過main函數的第三個變量獲取,只要定義一個char *env[]就可以了,只是通常不直接用它罷了,而是通過另外的方式,通過extern char **environ全局變量(環境變量表的指針)或者getenv函數來獲取某個環境變量的值。
                當然,實際上,當程序被execve執行后,它被加載到了內存里,包括程序的各種指令、數據以及傳遞給它的各種參數、環境變量等都被存放在系統分配給該程序的內存空間中。
                我們可以通過/proc/<pid>/maps把一個程序對應的進程的內存映象看個大概。
            Quote:

            $ cat /proc/self/maps   #查看cat程序自身加載后對應進程的內存映像
            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]


                關于程序加載和進程內存映像的更多細節請參考《C語言程序緩沖區注入分析》。

                到這里,關于命令行的秘密都被“曝光”了,可以開始寫自己的命令行解釋程序了。
                關于進程的相關操作請參考《shell編程范例之進程操作》。

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

          4. 文件屬主具有可執行權限時才可以執行某個程序
            Quote:

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


               
            參考的資料:

            [1] man boot-scripts
            Linux啟動過程
            [2] man bootparam
            Linux內核啟動參數
            [3] man 5 passwd
            [4] man shadow
            [5] "UNIX環境高級編程",進程關系一章
            [6] 2006,2007 Summer School hold by DSLab
            http://dslab.lzu.edu.cn/docs/2007summerschool/index.html
            http://dslab.lzu.edu.cn/docs/2006summerschool/index.html
          5. posted on 2008-03-14 15:27 隨意門 閱讀(3597) 評論(1)  編輯 收藏 引用

            評論

            # re: Linux命令行上程序執行的那一剎那! 2014-09-02 01:08 chaosink

            寫的太好了,贊!
              回復  更多評論    
            中文字幕无码精品亚洲资源网久久 | 四虎国产精品免费久久| 久久久久99精品成人片三人毛片| 欧美午夜A∨大片久久 | 国产高潮国产高潮久久久91| 久久最新免费视频| 精品综合久久久久久97超人 | 久久精品国产WWW456C0M| 久久国产欧美日韩精品免费| 97久久精品无码一区二区天美| 精品久久久无码中文字幕| 亚洲精品白浆高清久久久久久 | 亚洲AV无码久久寂寞少妇| 久久国产福利免费| 精品久久久久久亚洲| 综合人妻久久一区二区精品| 久久精品无码一区二区三区日韩| 精品久久久久久无码专区不卡| 日韩久久无码免费毛片软件| 久久久久一区二区三区| 性高湖久久久久久久久| 怡红院日本一道日本久久 | 久久青草国产精品一区| 伊人久久无码中文字幕| 久久99精品免费一区二区| 精品久久久久香蕉网| 狠狠色狠狠色综合久久 | 久久青青草原国产精品免费| 日韩AV无码久久一区二区 | 国产精品成人无码久久久久久| 久久久久99精品成人片直播| 久久久黄色大片| 久久久久久久精品妇女99| 亚洲国产精品一区二区三区久久| 久久精品国产精品亚洲人人| 天天久久狠狠色综合| 狠狠色婷婷久久一区二区三区| 久久久久久久人妻无码中文字幕爆| 亚洲国产另类久久久精品| 亚洲中文久久精品无码| 国内精品久久久久久久久电影网 |