Linux操作系統學習_用戶進程之由新進程創建到可執行程序的加載

 fork()函數大家應該都不陌生,一個現有進程可以調用fork函數來創建一個新進程,由fork()創建的新進程通常被稱爲子進程。fork()函數被調用一次,但返回兩次,兩次返回的區別在於,子進程的返回值爲0,父進程返回值爲子進程的PID值。但是,就是這大家都非常熟悉的一個函數,在你調用fork進行新進程創建的過程中,操作系統到底做了哪些工作,具體的工作過程又是什麼,不知道又有多少人清楚的知道。下面在這裏,將這一過程大體走一遍。

      當用戶編寫一個程序或一段代碼後,通過編譯鏈接生成可執行文件(這裏這一具體過程在前一篇博文中已經具體介紹過了,這裏不再贅述),這裏假設這一可執行文件的名字爲str1,當用戶在操作系統的shell終端界面中輸入一條指令./str1,shell程序便會響應並解析這條指令,經解析得知,用戶現在要執行str1這個程序,於是shell將調用fork函數開始創建進程,接下來會產生int 0x80軟中斷,軟中斷產生後,system_call函數會對此響應,並最終執行call sys_call_table(,%eax,4)這行代碼,這一過程爲查詢系統調用表,其中%eax寄存器中存儲的爲系統調用號。通過查詢系統調用表之後,最終映射到函數sys_fork中。之後,調用do_fork函數,分別爲str1進程申請一個可用的進程號和在進程槽task[64]中爲該進程申請一個空閒的位置。

      然後通過調用copy_process函數,準備將當前shell進程的管理結構複製給str1。首先,在主內存中爲用戶進程str1申請一個頁面的空間,然後將這個申請到的空閒頁面與之前申請到的進程槽task[64]中的相應的空閒項相掛接。這裏需要指出的是,這個內存頁面除了用來存儲str1自身的管理結構之外,還用來存儲str1進程的“內核棧”數據。

      這裏涉及到系統的兩個狀態——“用戶態”和“內核態”。

       當進程運行在內核態時,可以執行指令集中的任何指令,並且可以訪問系統中任何存儲器。

       當進程運行在用戶態時,不允許執行特權指令,也不允許直接引用地址空間中內核區內的代碼和數據。用戶程序必須通過系統調用接口間接地訪問內核代碼和數據。

      之前申請到的空閒頁面被用來存儲進程管理結構的數據和內核棧的數據,它們被放在同一個頁面內,分別處於同一頁面的兩端。當用戶程序執行時,通常進程處於用戶態,這時候用戶程序會致使多少數據需要壓棧,操作系統的設計者是不可能預知的。所以就根據程序執行時的具體需要,在主內存中分配頁面,以此來承載棧數據。但是,一旦內核程序開始執行,即處於內核態,那麼內核程序運行時會有多少數據需要壓棧,系統設計者是可以預知的,尤其可以預知最多會有多少數據需要壓棧。所以,設計者敢於將內核棧的數據與進程管理結構的數據放在同一個頁面內,將來用戶進程str1進入內核態的時候,壓棧的數據將向着str1管理結構所在的位置不斷地積累,但絕對不可能覆蓋掉進程管理結構的數據。

      copy_process函數還有一項非常重要的任務,即將當前運行進程shell的進程管理結構task_struct,複製到剛纔新申請的空閒頁面的起始端。這就是str1的task_struct的雛形。這裏有人會問,如果將str1的task_struct設置成與shell的一樣,那新建的用戶進程怎麼去執行用戶的程序?那豈不是和shell執行一樣的程序了?這裏此處先埋下一個伏筆,這個問題留到後面介紹完頁目錄項和頁表項之後進行解釋。

      下面調用copy_mm函數爲新建的用戶進程str1在線性地址空間中指定一個位置。這裏提一下,對普通用戶程序而言,能夠接觸到的只有邏輯地址,即在所有用戶程序看來,它們的地址範圍都是0~64MB這個空間內;而內核根據不同的用戶程序所在的進程將其對應成線性地址,並將此線性地址解析成“頁目錄項”、“頁表項”、“頁內偏移”,最終映射到物理頁面內。這裏對str1進程設置的線性地址範圍是專供內核使用的。

      下面調用copy_page_tables函數把shell進程對應頁表的全部表項複製給str1進程,併爲此新建一套頁目錄項。正因爲str1進程的頁表信息是由其父進程shell複製過來的,所以str1進程現在暫時先與其父進程shell共享相同的內存頁面,將來str1有了屬於自己的程序後,再另行調整。這裏的調整指的是將這些建立好的內存映射、頁表項關係全部解除。那麼會有人有疑問,既然後面子進程有了自己的程序之後這種關係要解除,爲什麼之前還要這樣將父進程的頁表項內容不加修改的複製到子進程的頁表中呢?

      這個意義是重大的,因爲有一個對str1進程獨立運行來說非常關鍵的步驟需要基於上述複製頁表和設置頁目錄項的操作來解決。我們知道,在str1創建並執行之初,該進程並沒有對應的執行程序,這就需要加載程序,於是就要調用execve函數。然而,調用execve函數的這行代碼又在哪裏呢?很顯然,只能在它的父進程,即shell程序中,這就意味着,str1進程一旦開始執行,必須有能力執行shell程序裏面的代碼,這樣才能開展以後的加載工作,直到它自己的子程序加載完畢。要想具備這種能力,只有一個辦法,就是讓str1進程在創建之初就能夠共享shell程序所佔用的頁面,一旦執行,共享能力馬上生效。而這裏的複製頁表和設置頁目錄項的工作就是爲str1能夠具備這個能力所做的準備工作。str1和shell的線性地址肯定不同,但是通過複製頁表和設置頁目錄項,就可以使不同的線性地址映射到相同的物理地址上,以此來實現共享。

      在調整完str1進程中與文件相關的結構之後,就要建立str1進程與全局描述符表GDT的關聯。將用戶進程str1的任務狀態描述符表TSS,以及局部數據描述符表LDT掛接在全局描述符表的指定位置。所有進程都要把這兩套描述符表掛接在全局描述符表中,這樣在進程切換時,系統就可以通過全局描述符表找到當前進程的LDT和TSS,並把相關的信息保存在當前進程的LDT和TSS中。同時,還可以找到即將切換到的進程的TSS和LDT,並用它們裏面存儲的數據信息來設置局部數據寄存器和任務狀態寄存器。可見,現在所做的掛接工作是系統能夠進行進程間切換的最根本的保障。

      然後將str1進程的狀態設置爲“就緒態”,至此,這個用戶進程的管理結構就創建完畢了。

      shell創建完str1進程的管理結構之後,通過shell自身的代碼繼續執行,最終會切換到str1進程去執行,由str1進程來加載自己對應的程序。str1進程開始執行後,會調用execve函數,爲了支持str1程序加載的準備工作更好的完成,系統將一些數據進行了壓棧保存。首先execve函數將文件的路徑名、參數、環境變量壓棧,以此來支持將來參數和環境變量的加載。創建str1用戶進程時,參數和環境變量的具體信息是由shell程序自身的代碼來提供的,任何一個用戶進程所對應的參數和環境變量都是由創建它的父進程來提供的。隨後,產生軟中斷,映射到一個系統調用函數sys_execve中執行,硬件將EIP的值壓棧,這個值決定中斷程序結束後執行哪一條指令。sys_execve中要做的最重要的事情,就是把EIP值“所在棧空間的地址值”壓棧。這裏注意,Intel CPU是不能通過指令直接修改EIP寄存器的值,有了前面的壓棧,操作系統就可以通過這個值找到棧中的EIP值,修改棧中的EIP的值。

      然後,進入do_execve函數,先做外圍準備工作,即爲管理str1進程參數和環境變量所佔用的頁面做準備。在此過程中有一個小插曲,就是需要在中間時刻暫停外圍準備工作,轉而將str1進程的可執行文件的i節點由硬盤中讀入到內存中,讀入這個i節點的目的就是爲了對這個str1文件進行檢測,看其是否具備載入條件。之後再繼續回去做外圍準備工作,把參數和環境變量的個數統計出來。具體過程如下:

                                            

 

      然後,對讀取的i節點進行分析,通過對i節點中“文件屬性”的分析,可以得知這個str1文件是不是一個“常規文件”。因爲只有是常規文件,即,除了塊設備文件、字符設備文件、目錄文件、管道文件等特殊文件之外的文件,纔有被載入的可能。同時還要檢測當前進程是否有執行權限,並對用戶ID等信息進行設置。

      接下來要對可執行文件的文件頭進行檢測,文件頭中記載了這個可執行程序的代碼段長度和數據段長度等關鍵的屬性信息,這些信息直接影響到可執行程序將來被加載入內存後是否能夠正常執行,而且若這個可執行程序確實能夠執行,那麼這個文件頭中的信息將引導系統對可執行程序的加載。因爲可執行程序也是以數據塊的形式存儲在虛擬盤上的,所以它的文件頭,正常情況下在它的第一個邏輯塊內。系統將這個文件頭從硬盤上讀入到緩衝塊內,先對這個文件頭進行備份,然後根據文件頭和i節點提供的信息,對可執行文件進行檢測,判斷可執行程序是否能被加載。

      當經過前面的文件檢測,系統可以確定,shell程序確實具備加載的能力,接下來要把前面統計的參數和環境變量及其相關輔助信息拷貝到內存指定的頁面中去。到這裏,加載str1程序的外圍準備工作和對str1這個可執行文件的檢測工作就全部結束了。接下來,要根據實際情況,對str1進程的管理結構進行調整。先要更改該文件的executable字段,這個字段就是str1進程對應的可執行文件的i節點,目前str1該字段對應的是創建其的shell進程的可執行文件。str1進程創建時,由於shell管理機構中的信息全部都複製給了str1進程,所以這裏面也一定包括shell進程與其可執行文件的關係,現在str1進程就要擁有自己的程序了,也一定會對應一個屬於自己的可執行文件的i節點,所以原來這個關係已經沒有必要繼續維護下去了,這就要先把它與shell可執行文件的關係解除掉,這裏具體表現爲釋放這個文件的i節點,並把str1這個可執行文件的i節點與str1進程掛接上。

PS:這裏注意,並不是當str1要加載新程序時就會把所有之前與文件的關係都解除掉。str1加載子程序後也會用到的關係是不會被解除的。

      接着調整str1進程管理結構,將與文件和信號相關的數據信息進行調整,與信號相關的信息全部清0,這是因爲每個進程都會接受各自的信號,這些都是進程私有的信息,彼此之間不能交叉使用,所以str1把shell進程的信號繼承下來毫無意義,而且由於信號還會影響到進程的執行狀態,繼承下來有可能造成混亂,因此全部清空。

      下面釋放str1進程原先的代碼段和數據段所佔用的物理頁面。這裏主要有兩個原因:1、str1將要對應的程序與shell程序肯定有所不同,但是str1進程現在與shell進程正在共享着相同的頁面和管理頁表,所以要把這個共享關係解除掉。2、str1與shell進程共享頁面,共享就意味着這些頁面必須是“只讀”的,現在str1由於加載新程序不再需要這些頁面中的程序了,如果還共享着原來shell程序的頁面,一來毫無意義,二來也會影響到shell程序對這些頁面的應用,所以關係也要解除。

      然後,根據之前拷貝進內存的str1可執行文件的文件頭所提供的信息重新設置str1程序代碼段的描述符以及重新設置數據段描述符。具體表現爲,代碼段限長被設置爲與str1.c文件的代碼段長度相同。數據段長度設置爲64MB,之後再將參數與環境變量所在的物理頁面映射到str1這個進程的線性地址空間內。

      接着創建環境變量和參數的指針表,並將其地址存放到環境變量和參數所在的主內存區的頁面中。

      然後對str1用戶進程管理結構中的brk、start_stack等信息進行設置,這些信息都是直接或者間接從str1可執行文件的文件頭中採集出來的,這與從shell進程對應的可執行文件中採集出來的程序肯定是不一樣的,所以這裏必須重新設置,以支持str1進程在加載子程序後運行。到此爲止,爲str1程序加載所做的工作中,針對str1進程管理結構自身的屬性進行調整的工作就全部做完了。

      系統現在開始爲str1程序的加載做最後的準備工作,即對EIP的值進行調整,使之指向str1程序的第一條指令處,這將導致軟中斷服務程序執行完畢後,系統會從str1程序起始位置開始執行,並會發生缺頁中斷。另外,爲了保證str1執行後能夠使用棧空間,也要對棧頂指針進行設置,即對esp進行設置,使其指向str1的棧底。前面已經介紹過,系統將EIP值“所在棧空間的地址值”壓棧,這個地址值將成爲這裏所進行設置的基礎,系統將分別用str1程序起始地址值和當前進程棧頂位置值,來對EIP和棧頂指針ESP進行設置,設置完畢後,通過sys_execve函數中的ret指令,將重新設置的EIP值載入eip寄存器,並指引代碼的執行。(此處原理參見這篇文章

      到此,爲str1程序的加載所需要做的準備工作就全部完成了,接下來,str1程序就可以加載了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章