1. 程式人生 > >fork之後子程序到底複製了父程序什麼

fork之後子程序到底複製了父程序什麼

原文地址:http://blog.csdn.net/xy010902100449/article/details/44851453

  1. #include<stdio.h>  
  2. #include<string.h>  
  3. #include<stdlib.h>  
  4. #include<unistd.h>  
  5. void main()  
  6. {  
  7.     char str[6]="hello";  
  8.     pid_t pid=fork();  
  9.     if(pid==0)  
  10.     {  
  11.         str[0]='b';  
  12.         printf("子程序中str=%s\n",str);  
  13.         printf("子程序中str指向的首地址:%x\n",(unsigned int)str);  
  14.     }  
  15.     else  
  16.     {  
  17.         sleep(1);  
  18.         printf("父程序中str=%s\n",str);  
  19.         printf("父程序中str指向的首地址:%x\n",(unsigned int)str);  
  20.     }  
  21. }  

子程序中str=bello 子程序中str指向的首地址:bfdbfc06 父程序中str=hello

父程序中str指向的首地址:bfdbfc06

這裡就涉及到實體地址和邏輯地址(或稱虛擬地址)的概念。

從邏輯地址到實體地址的對映稱為地址重定向。分為:

靜態重定向--在程式裝入主存時已經完成了邏輯地址到實體地址和變換,在程式執行期間不會再發生改變。

動態重定向--程式執行期間完成,其實現依賴於硬體地址變換機構,如基址暫存器。

邏輯地址:CPU所生成的地址。CPU產生的邏輯地址被分為 :p (頁號) 它包含每個頁在實體記憶體中的基址,用來作為頁表的索引;d (頁偏移),同基址相結合,用來確定送入記憶體裝置的實體記憶體地址。

實體地址:記憶體單元所看到的地址。

使用者程式看不見真正的實體地址。使用者只生成邏輯地址,且認為程序的地址空間為0到max。實體地址範圍從R+0到R+max,R為基地址,地址對映-將程式地址空間中使用的邏輯地址變換成記憶體中的實體地址的過程。由記憶體管理單元(MMU)來完成。

fork()會產生一個和父程序完全相同的子程序,但子程序在此後多會exec系統呼叫,出於效率考慮,linux中引入了“寫時複製“技術,也就是隻有程序空間的各段的內容要發生變化時,才會將父程序的內容複製一份給子程序。在fork之後exec之前兩個程序用的是相同的物理空間(記憶體區),子程序的程式碼段、資料段、堆疊都是指向父程序的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子程序中有更改相應段的行為發生時,再為子程序相應的段分配物理空間,如果不是因為exec,核心會給子程序的資料段、堆疊段分配相應的物理空間(至此兩者有各自的程序空間,互不影響),而程式碼段繼續共享父程序的物理空間(兩者的程式碼完全相同)。而如果是因為exec,由於兩者執行的程式碼不同,子程序的程式碼段也會分配單獨的物理空間。

fork時子程序獲得父程序資料空間、堆和棧的複製,所以變數的地址(當然是虛擬地址)也是一樣的。

每個程序都有自己的虛擬地址空間,不同程序的相同的虛擬地址顯然可以對應不同的實體地址。因此地址相同(虛擬地址)而值不同沒什麼奇怪。 具體過程是這樣的: fork子程序完全複製父程序的棧空間,也複製了頁表,但沒有複製物理頁面,所以這時虛擬地址相同,實體地址也相同,但是會把父子共享的頁面標記為“只讀”(類似mmap的private的方式),如果父子程序一直對這個頁面是同一個頁面,知道其中任何一個程序要對共享的頁面“寫操作”,這時核心會複製一個物理頁面給這個程序使用,同時修改頁表。而把原來的只讀頁面標記為“可寫”,留給另外一個程序使用。 這就是所謂的“寫時複製”。正因為fork採用了這種寫時複製的機制,所以fork出來子程序之後,父子程序哪個先排程呢?核心一般會先排程子程序,因為很多情況下子程序是要馬上執行exec,會清空棧、堆。。這些和父程序共享的空間,載入新的程式碼段。。。,這就避免了“寫時複製”拷貝共享頁面的機會。如果父程序先排程很可能寫共享頁面,會產生“寫時複製”的無用功。所以,一般是子程序先排程滴。

假定父程序malloc的指標指向0x12345678, fork 後,子程序中的指標也是指向0x12345678,但是這兩個地址都是虛擬記憶體地址 (virtual memory),經過記憶體地址轉換後所對應的 實體地址是不一樣的。所以兩個進城中的這兩個地址相互之間沒有任何關係。

(注1:在理解時,你可以認為fork後,這兩個相同的虛擬地址指向的是不同的實體地址,這樣方便理解父子程序之間的獨立性) (注2:但實際上,linux為了提高 fork 的效率,採用了 copy-on-write 技術,fork後,這兩個虛擬地址實際上指向相同的實體地址(記憶體頁),只有任何一個程序試圖修改這個虛擬地址裡的內容前,兩個虛擬地址才會指向不同的實體地址(新的實體地址的內容從原實體地址中複製得到))

2、exec家族

exec家族一共有六個函式,分別是:

  1. <span style="font-family:Microsoft YaHei;font-size:12px;">(1)int execl(const char *path, const char *arg, ......);  
  2. (2)int execle(const char *path, const char *arg, ...... , char * const envp[]);  
  3. (3)int execv(const char *path, char *const argv[]);  
  4. (4)int execve(const char *filename, char *const argv[], char *const envp[]);  
  5. (5)int execvp(const char *file, char * const argv[]);  
  6. (6)int execlp(const char *file, const char *arg, ......);</span>  

其中只有execve是真正意義上的系統呼叫,其它都是在此基礎上經過包裝的庫函式。

    exec函式族的作用是根據指定的檔名找到可執行檔案,並用它來取代呼叫程序的內容,換句話說,就是在呼叫程序內部執行一個可執行檔案。這裡的可執行檔案既可以是二進位制檔案,也可以是任何Linux下可執行的指令碼檔案。

與一般情況不同,exec函式族的函式執行成功後不會返回,因為呼叫程序的實體,包括程式碼段,資料段和堆疊等都已經被新的內容取代,只留下程序ID等一些表面上的資訊仍保持原樣,頗有些神似"三十六計"中的"金蟬脫殼"。看上去還是舊的軀殼,卻已經注入了新的靈魂。只有呼叫失敗了,它們才會返回一個-1,從原程式的呼叫點接著往下執行。

程序是什麼?

程式並不能單獨執行,只有將程式裝載到記憶體中,系統為它分配資源才能執行,而這種執行的程式就稱之為程序。程式和程序的區別就在於:程式是指令的集合,它是程序執行的靜態描述文字;程序是程式的一次執行活動,屬於動態概念。程序是資源分配的基本單位。

在多道程式設計中,我們允許多個程式同時載入到記憶體中,在作業系統的排程下,可以實現併發地執行。這是這樣的設計,大大提高了CPU的利用率。程序的出現讓每個使用者感覺到自己獨享CPU,因此,程序就是為了在CPU上實現多道程式設計而提出的。

 有了程序為什麼還要執行緒?

執行緒是CPU執行排程的基本單位,執行緒必須被包含在程序中,一個程序可以有很多執行緒(至少有一個),這些執行緒有自己的資源(如棧,暫存器)也共享程序的許多資源。

程序有很多優點,它提供了多道程式設計,讓我們感覺我們每個人都擁有自己的CPU和其他資源,可以提高計算機的利用率。很多人就不理解了,既然程序這麼優秀,為什麼還要執行緒呢?其實,仔細觀察就會發現程序還是有很多缺陷的,主要體現在兩點上:

  • 程序只能在一個時間幹一件事,如果想同時幹兩件事或多件事,程序就無能為力了。

  • 程序在執行的過程中如果阻塞,例如等待輸入,整個程序就會掛起,即使程序中有些工作不依賴於輸入的資料,也將無法執行。

如果這兩個缺點理解比較困難的話,舉個現實的例子也許你就清楚了:如果把我們上課的過程看成一個程序的話,那麼我們要做的是耳朵聽老師講課,手上還要記筆記,腦子還要思考問題,這樣才能高效的完成聽課的任務。而如果只提供程序這個機制的話,上面這三件事將不能同時執行,同一時間只能做一件事,聽的時候就不能記筆記,也不能用腦子思考,這是其一;如果老師在黑板上寫演算過程,我們開始記筆記,而老師突然有一步推不下去了,阻塞住了,他在那邊思考著,而我們呢,也不能幹其他事,即使你想趁此時思考一下剛才沒聽懂的一個問題都不行,這是其二。

現在你應該明白了程序的缺陷了,而解決的辦法很簡單,我們完全可以讓聽、寫、思三個獨立的過程,並行起來,這樣很明顯可以提高聽課的效率。而實際的作業系統中,也同樣引入了這種類似的機制——執行緒。

執行緒的優點

因為要併發,我們發明了程序,又進一步發明了執行緒。只不過程序和執行緒的併發層次不同:程序屬於在處理器這一層上提供的抽象;執行緒則屬於在程序這個層次上再提供了一層併發的抽象。如果我們進入計算機體系結構裡,就會發現,流水線提供的也是一種併發,不過是指令級的併發。這樣,流水線、執行緒、程序就從低到高在三個層次上提供我們所迫切需要的併發!

除了提高程序的併發度,執行緒還有個好處,就是可以有效地利用多處理器和多核計算機。現在的處理器有個趨勢就是朝著多核方向發展,在沒有執行緒之前,多核並不能讓一個程序的執行速度提高,原因還是上面所有的兩點限制。但如果講一個程序分解為若干個執行緒,則可以讓不同的執行緒執行在不同的核上,從而提高了程序的執行速度。

例如:我們經常使用微軟的Word進行文字排版,實際上就打開了多個執行緒。這些執行緒一個負責顯示,一個接受鍵盤的輸入,一個進行存檔等等。這些執行緒一起執行,讓我們感覺到我們輸入和螢幕顯示同時發生,而不是輸入一些字元,過一段時間才能看到顯示出來。在我們不經意間,還進行了自動存檔操作。這就是執行緒給我們帶來的方便之處。

程序與執行緒的區別

  • 程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。

  • 執行緒是程序的一個實體, 是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。

  • 一個執行緒可以建立和撤銷另一個執行緒,同一個程序中的多個執行緒之間可以併發執行。

程序和執行緒的主要差別在於它們是不同的作業系統資源管理方式。程序有獨立的地址空間,一個程序崩潰後,在保護模式下不會對其它程序產生影響,而執行緒只是一個程序中的不同執行路徑。執行緒有自己的堆疊和區域性變數,但執行緒之間沒有單獨的地址空間,一個執行緒死掉就等於整個程序死掉,所以多程序的程式要比多執行緒的程式 健壯,但在程序切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變數的併發操作,只能用執行緒,不能用程序。