Linux內核分析(六)——進程簡析

禹曉博+ 原創作品轉載請註明出處 + 歡迎加入《Linux內核分析》MOOC網易雲課堂學習

一、Linux中的進程簡析

       進程是任何多道程序設計的操作系統額基本概念,就像我們經常看到的關於進程的定義就是程序執行的一個實例,也是系統資源調度的最小單位。如何同一個程序被多個用戶同時運行,那麼這個程序就有多個相對獨立的進程,與此同時他們又共享相同的執行代碼。在Linux系統中進程的概念類似於任務或者線程(task & threads)。

       實際上我們說進程是一個程序運行時候的一個實例實際上說的是它就是一個一個可以充分描述程序以達到了其可以運行狀態的的一個數據和代碼集合。一個進程會被產生並會複製出自己的子代,類似細胞分裂一樣。從系統的角度來看進程的任務實際上就是擔當承載系統資源的單位,系統在調度和分配資源的時候也會以他們作爲基本單位開始進行分配。(系統中的資源很多例如CPU的時間片、內存堆棧等等)

       當我們創建一個進程的時候,實際上系統就是在複製他的父進程,什麼是他的父進程?我們知道程序或者進程執行的時候有的時候就會需要創建新的實例這個時候A如果新創建了B那麼A就是B的父進程。實際上就是複製了幾乎所有父進程的信息包括代碼。子進程接收父進程地址空間的一個邏輯拷貝,(實際上就是可以理解爲面向對象中的類創建實例的過程或者繼承父類的這種關係,實際上他們看起來域屬性是一樣的但是又不會完全一樣,所以我們說這裏面是邏輯上的一個複製,後面會詳細分析~)然後,這個子進程會從創建進程那個系統調用服務代碼之後的下一條指令開始執行(ret_from_fork),執行代碼與父進程是相同的。但是我們要知道實際上雖然AB都是指向相同的代碼部分,但是正如我們知道的程序需要指令和數據,所以他們的數據拷貝是不同的,因此進程對一個內存單元的修改在AB之間是不可見的。以上是早期的時候情況,現代的系統實際上可能並不是這樣的。在支持多線程應用的系統中很多擁有相對獨立執行路徑的用戶程序共享應用程序的大部分數據結構。那麼這樣的話一個進程就是由幾個用戶線程組成,而且每一個執行線路就是一個線程。

       那麼進程在系統中的數據結構又是什麼樣子的呢?首先最應該知道就是系統如何管理這些進程,那麼系統一定要有相應的數據結構去標識每一個進程以及他們的擴展數據結構(這裏可以想象一下進程標識可能不會涵蓋具體的執行代碼和數據集合可能它僅僅是包含指向這些代碼段和數據段的入口地址或者說是指針,事實上也是這樣)大體上想想可以知道實際上這個結構就是我們在操作系統中所說的PCB(Process Control Block)在Linux中這個數據結構我們叫做task_struct,我們想想它實際上至少應該包括以下信息,比如優先級,它的運行狀態,他所在的內存空間,它的文件訪問權限等等,下面我們看一個教科書上的圖片:


       實際上我們看到他的結構還是很冗長的,不僅僅包含了很多進程信息的標識字段,同時又有很多的指針指向很多附件的數據結構。圖中列出來的包括進程的基本信息thread_info、內存區域描述mm_struct、與進程相關的tty tty_struct、當前的目錄fs_struct 、文件描述符files_struct、所接受的信號singal_struct等等。

 1.1進程的狀態

        進程執行時,它會根據具體情況改變狀態 。進程狀態是調度和對換的依據。Linux中的進程主要有如下狀態(上面圖中的那個state字段)


(1)可運行狀態
       基於linux簡潔優雅的性質,系統這裏面不區分執行和ready兩個狀態,只要進程處於資源充足狀態,就可以運行或者隨時可以準備執行。而準備運行的進程只要得到CPU就可以立即投入運行,CPU是這些進程唯一等待的系統資源。
(2)可中斷的等待狀態
       進程被掛起,直到等到一個可以喚醒他的東西,例如一個硬件中斷、某項系統資源、或一個信號量。當它等到這些喚醒條件的之後就會進入可運行狀態。
(3)不可中斷的等待狀態
       實際上我們要理解爲什麼是不可中斷的,一種常見的狀態就是這個進程正在訪問一個獨佔的臨界資源,這種時候處於一種不可搶佔的狀態。通常當進程接收到SIGSTOP、SIGTSTP、SIGTTIN或 SIGTTOU信號後就處於這種狀態。例如,正接受調試的進程就處於這種狀態。

(4)跟蹤狀態

        沒什麼說的,就是一個進程正在被另一個進程監視,比如我們在調試的時候。
(5)僵死狀態
        進程雖然已經終止,但由於某種原因,父進程還沒有執行wait()系統調用,終止進程的信息也還沒有回收。顧名思義,處於該狀態的進程就是死進程,這種進程實際上是系統中的垃圾,必須進行相應處理以釋放其佔用的資源。

        我們在設置這些狀態的時候是可以直接用語句進行的比如:p—>state = TASK_RUNNING。同時內核也會使用set_task_state和set_current_state。

 1.2關於thread_info

       進程是一個動態的東西,所以系統也是希望很有效率的進行管理。從這個方面去看thread_info結構與內核的堆棧放在一起(8k的一個頁中)可以有一個很方便的好處就是系統很容易從esp寄存器找到這寫CPU中正在執行的進程的thread_info的首地址。實際上就像我們猜到的一樣他們既然總是在一個8k的頁中,所以基於Linux簡潔優雅的特性這些信息的地址分配肯定也是基於8K對齊的(什麼叫對齊,嗯就是他們的起始地址都是8k整數倍)我們來再看一張教科書上的圖(尷尬哎呀怎麼這麼多教科書上圖~恩恩筆者自己畫的圖都是會打碼~不,啃啃打水印的喲得意~後面就會有的呢)


       那麼我們一看上面的圖,嗯怎麼都是0x015什麼的開頭的呢?沒有錯,對齊也是這種意思的呢,實際上我們知道如果地址只有2位的話就只能表示4個內存單元,如果有10位就是1024個內存單元就是1k的空間。那麼8k呢就是13位地址空間。那麼重點來了如何通過esp快速找到這些info呢?既然是8k的整數倍那麼後13位就是一樣的呢。我們可以先屏蔽低13位然後加上thread_info的頁偏移量就可以了。嗯linux簡潔優雅的性質又來了(額,這個詞出現這麼多!還整成紅色簡直是“幹sang得xin漂bing亮kuang”,接下來不用了嗯),沒有偏移量~有木有。所以我們只要屏蔽esp低13位就可以了。比如我們在源碼中可以看到這樣的一段指令:

movl $0xffffe000, %ecx

andl %esp, %ecx

movl %ecx, p

 1.3進程的創建

        說了這麼多大體上應該知道了一些關於進程的東西,接下來我們看看進程是如何創建的呢?我們實際上使用的是fork這個命令。╮(╯▽╰)╭爲什麼fork呢?因爲進程的穿件就是想細菌複製一樣,畫一個一個分成兩個,然後用複製自己創建新的,這樣看起來就像個叉子一樣,估計所以就叫fork了。我們首先來看一下從整體上看它的過程是這樣的。


       看着就眼熟吧,實際上我們看到了他就是一個系統調用的過程,參見我們上兩個文章我們看到實際上他們的過程都差不多。具體代碼的的執行過程示意圖是這樣的。


       我們都知道,對於父進程 fork 返回子進程號,對於子進程 fork 返回 0 ,這也是執行路徑如此的原因所在。但是, fork 的返回不同值的原因又是什麼,這就得看 fork 的實現了。fork 先是調用 find_empty_process 爲子進程找到一個空閒的任務號,然後調用 copy_process 複製進程, fork 返回 copy_process 的返回值 last_pid ,也就是子進程號。從上面的實現看來, fork 的返回值不會是 0 , last_pid 從 1 開始,父進程執行 if 外面的部分,上面的邏輯正是父進程的執行邏輯。

       對於子進程,先看子進程的初始狀態, copy_process 中創造了子進程的上下文執行環境,這個上下文環境正是父進程 fork 系統調用時的環境,其中, p->tss.eip 正是被賦值爲 fork 之後下一條指令地址,這就是子進程和父進程都返回到 fork 下一條指令處的原因。同時,需要注意的是, p->tss.eax 被賦了值 0 ,當調度到子進程開始執行時,首先加載其上下文環境, eip 被加載爲 fork 之後下一條指令, eax 就被加載爲 0 ,所以,對於子進程來說,和父進程唯一的區別就是返回值( eax )爲 0 ,子進程執行 if 裏面的部分。

二、實驗過程

       首先我們要將新的menuOS放入我們的實驗樓系統中,如下圖所示:


       然後我們看到我們把新的文件test_fork替換了之前的test.c文件因爲裏面我們加入了新的命令就是fork用於我們之後的運行調試,然後我們 make rootfs 製作很文件系統。然後我們就可以調試了。


       然後們開始設置一些主要的斷點如下圖:


       這裏面我們看到我們設置了do_fork copy_process這樣的斷點,n是next的意思我們可以一條條執行這些代碼。


       繼續執行我們看到這裏面會用一些條件判斷和賦值的東西,比如將當先任務的thread_info是專爲防止多處理器併發而引入的一種鎖指向運行的那個模塊之後將P指向的進程標識設爲相應狀態之後會設置一個自旋鎖,這裏面爲什麼要設置自旋鎖呢?是專爲防止多處理器併發而引入的一種鎖,這裏面涉及了IO操作我們看到所以就必須設置這種鎖,之後的文章應該會講到恩恩這裏面不細細說。


       文章前面說過了穿件一個進程實際上就是複製的過程,在ret_from_kernel_thread之前我們開袋實際上紫禁城的數據端被複製成用戶數據段(實際上Linux只有四個段,分別是內核和用戶態下的數據段和代碼段)上圖中從哪個內核態返回就是我們之前第三幅圖中看到的它從內核返回到用戶了。


       然後繼續調試我們可以看到就進入了彙編指令部分,包括它需要得到thread_info,重新設置標示符之類的。然後我們看到了之後還會執行調度函數將現在的執行的task賦值到task_struct中。

三、總結

        可以看出,fork()中,內核並不立刻爲新進程分配代碼和數據物理內存頁,新進程與父進程共同使用父進程已有的代碼和數據物理內存頁面。只有當以後執行過程中由一個進程一寫方式訪問內存時候被訪問的內存頁面纔會在寫操作之前被複制到新申請的內存頁面中。另外在fork的最後是將任務設置成了就緒狀態,由於fork()是一個系統調用,在系統調用部分system_call.s,可以看到在系統函數返回後,會調用調度函數schedule(),在schedule()中,就會檢測到新進程的就緒狀態,並用switch_to()切換到新進程進行執行。

        具體過程是:首先在內存中申請一頁內存存放進程控制塊task_struct,並返回進程號nr,並在task數組的nr處存放task_struct的指針,還要將task的當前指針current指到nr處;然後將父進程的task_struct的內容複製到新進程的task_struct中作爲模版;之後我們對task_struct中的信息進行修改,主要進行一下工作:設置父進程、清除信號位圖、時間片、運行時間、根據當前環境設置tss(內核態指針esp0指向task_struct所在頁的頂端)、設置LDT的選擇子等(根據nr指向GDT中相應的ldt描述符)。 在之後我們要設置新進程的代碼段、數據段的基地址和段長:更新task_struct中的代碼開始地址:更新task_struct中局部描述符表中的代碼段和數據段描述符。然後再複製父進程的頁表目錄項和頁表:在頁目錄表中,複製父進程的頁表目錄項,目的地址由新進程的線性地址計算出來;對每個對應的頁表目錄項申請一個空閒頁,並用頁表地址更新頁表目錄項,最後將父進程頁表中各項複製到新進程對應的頁表中,也就是說,這個時候,子進程與父進程共享物理內存。並且更新task_struct中的文件信息:文件打開次數加1,父進程的當前目錄引用數加1。還要設置TSS和LDT描述符項:在全局描述符表(GDT)中設置新任務的TSS描述符項和LDT段的描述符項,使TSS描述符項和LDT描述符項分別指向task_struct的TSS結構和LDT結構。最後我們將任務設置爲就緒狀態,向當前進程(父進程)返回新進程號。

       最後總結一下,Linux進程的創建過程就是內存中進程相關資源產生的過程,就是clone的過程。例如task _struct、內核態堆棧、線型地址到物理地址的映射表、全局描述表(GDT)中的任務狀態段,局部描述符表、代碼段、堆、棧、參數全局變量等數據區什麼的。上面提到的這幾類資源中,很多都與task_struct有關,所以我想說一下task_struct。它是Linux的進程控制塊,駐留在內存中,描述進程的基本信息,所以它是進程操作依據的重要數據結構。Linux中一般進程都是由現有的一個進程創建的,也就是我們所說的父進程,子進程。具體的創建是通過fork()實現的。



 借鑑參考:

http://soft.chinabyte.com/os/51/12324551.shtml

DLUBRUCE ZHANG's Blog http://blog.csdn.net/dlutbrucezhang


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