如果你對代碼段、數據段、棧、堆存放哪些數據還不是很清楚,請先看我寫和Linux 內存管理。
有時會出現父子進程變量的地址一樣,但值不一樣。看下面代碼:
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<unistd.h> main(){ char str[4]="asd"; pid_t pid=fork(); if(pid==0){ str[0]='b'; printf("子進程中str=%s\n",str); printf("子進程中str指向的首地址:%x\n",(unsigned int)str); } else{ sleep(1); printf("父進程中str=%s\n",str); printf("父進程中str指向的首地址:%x\n",(unsigned int)str); } }
輸出:
子進程中str=bsd
子進程中str指向的首地址:bfc224dc
父進程中str=asd
父進程中str指向的首地址:bfc224dc
這里就涉及到物理地址和邏輯地址(或稱虛擬地址)的概念。
從邏輯地址到物理地址的映射稱為地址重定向。分為:
靜態重定向--在程序裝入主存時已經完成了邏輯地址到物理地址和變換,在程序執行期間不會再發生改變。
動態重定向--程序執行期間完成,其實現依賴於硬件地址變換機構,如基址寄存器。
邏輯地址:CPU所生成的地址。CPU產生的邏輯地址被分為 :p (頁號) 它包含每個頁在物理內存中的基址,用來作為頁表的索引;d (頁偏移),同基址相結合,用來確定送入內存設備的物理內存地址。
用戶程序看不見真正的物理地址。用戶只生成邏輯地址,且認為進程的地址空間為0到max。物理地址范圍從R+0到R+max,R為基地址,地址映射-將程序地址空間中使用的邏輯地址變換成內存中的物理地址的過程。由內存管理單元(MMU)來完成。
fork()會產生一個和父進程完全相同的子進程,但子進程在此后多會exec系統調用,出於效率考慮,linux中引入了“寫時復制“技術,也就是只有進程空間的各段的內容要發生變化時,才會將父進程的內容復制一份給子進程。在fork之后exec之前兩個進程用的是相同的物理空間(內存區),子進程的代碼段、數據段、堆棧都是指向父進程的物理空間,也就是說,兩者的虛擬空間不同,但其對應的物理空間是同一個。當父子進程中有更改相應段的行為發生時,再為子進程相應的段分配物理空間,如果不是因為exec,內核會給子進程的數據段、堆棧段分配相應的物理空間(至此兩者有各自的進程空間,互不影響),而代碼段繼續共享父進程的物理空間(兩者的代碼完全相同)。而如果是因為exec,由於兩者執行的代碼不同,子進程的代碼段也會分配單獨的物理空間。
fork之后內核會通過將子進程放在隊列的前面,以讓子進程先執行,以免父進程執行導致寫時復制,而后子進程執行exec系統調用,因無意義的復制而造成效率的下降。
fork時子進程獲得父進程數據空間、堆和棧的復制,所以變量的地址(當然是虛擬地址)也是一樣的。
每個進程都有自己的虛擬地址空間,不同進程的相同的虛擬地址顯然可以對應不同的物理地址。因此地址相同(虛擬地址)而值不同沒什么奇怪。
具體過程是這樣的:
fork子進程完全復制父進程的棧空間,也復制了頁表,但沒有復制物理頁面,所以這時虛擬地址相同,物理地址也相同,但是會把父子共享的頁面標記為“只讀”(類似mmap的private的方式),如果父子進程一直對這個頁面是同一個頁面,知道其中任何一個進程要對共享的頁面“寫操作”,這時內核會復制一個物理頁面給這個進程使用,同時修改頁表。而把原來的只讀頁面標記為“可寫”,留給另外一個進程使用。
這就是所謂的“寫時復制”。正因為fork采用了這種寫時復制的機制,所以fork出來子進程之后,父子進程哪個先調度呢?內核一般會先調度子進程,因為很多情況下子進程是要馬上執行exec,會清空棧、堆。。這些和父進程共享的空間,加載新的代碼段。。。,這就避免了“寫時復制”拷貝共享頁面的機會。如果父進程先調度很可能寫共享頁面,會產生“寫時復制”的無用功。所以,一般是子進程先調度滴。
子進程復制了父進程的行緩沖
#include <stdio.h> #include <unistd.h> int main(void) { printf("one"); fork(); printf("two/n"); return 0; }
輸出
onetwo
onetwo
#include <stdio.h> #include <unistd.h> int main(void) { printf("one"); fflush(stdout); fork(); printf("two/n"); return 0; }
輸出
onetwo
two
想消除行緩沖所帶來的困擾,方法如下
1、輸出數據后加換行符
2、使用fflush之類的函數強制刷新
3、使用setbuf,setvbuf函數設置緩沖區大小
4、使用非緩沖的的流,如stderr.
