目錄
進程創建
Linux 中我們可以說一個進程就是一個PCB, 即 一個task_struct, 那麼創建進程也就是創建PCB, 即是創建task_struct
Linux 中說到進程創建, 就不得不提到 fork()函數. fork()在Lnux下是非常重要的一個函數 .
fork()函數
從已存在進程中創建一個新進程。新進程爲子進程,而原進程爲父進程
fork()在函數內部會調用clone這個系統調用接口
pid_t fork ()
頭文件: unistd.h
fork返回值
fork函數返回值 : (返回值類型爲pid_t, 實際等同於int)
- 子進程在執行fork()時返回 0
- 父進程在執行fork()時, fork()創建子進程, 返回子進程的PID (PID是一個大於0的整數)
- 父進程在用fork()創建子進程失敗時返回 -1
因爲fork運行有多種結果, 所以往往fork之後要根據fork的返回值進行分流(例如用 if 寫多個分支), 來看個例子 .
testfork.c 如下
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
}
else if(pid == 0){
printf("子進程\n");
}
else{
printf("父進程\n");
}
return 0;
}
編譯執行如下, 可以看到, 當父進程用fork() 創建子進程成功後, 返回了其子進程的pid, 然後繼續執行, 直到執行打印語句後子進程才執行, 如下 :
但並不是父進程創建了子進程, 父進程就一定會先執行完,才執行子進程, 也可能是父進程執行到一半, 甚至剛調用fork()創建完子進程後, 就立即轉而執行子進程. 這取決於CPU的調度. 比如說下面這段代碼 .
#include<stdio.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
if(pid == -1){
perror("fork error");
}
else if(pid == 0){
printf("子進程執行\n子進程pid:%d\n", getpid());
}
else{
printf("父進程開始執行\n");
sleep(5);
printf("父進程執行\n父進程pid:%d\n", getpid());
printf("父進程運行結束\n");
}
return 0;
}
可以看到, 父進程執行到一半開始執行子進程了, 就此次運行結果分析, 由於父進程中sleep()函數, 致使父進程進入睡眠狀態
(sleeping)(這種睡眠是可中斷的, 當sleep()執行完, 就會中斷睡眠, 進入就緒狀態(或者說進入運行隊列), 等待分配時間片), 子進程
當被創建後, 一直處於就緒狀態(一直處於運行隊列中), 等待分配時間片, 當父進程睡眠時, 子進程拿到了時間片, 子進程執行 . 當子
進程執行完, 父進程拿到時間片後, 父進程繼續運行 .
所以就有, fork創建子進程之前, 父進程獨立運行, 創建子進程之後, 誰先運行取決於調度器的調度
進程調用fork, 內核會做出以下操作
- 分配新的內存塊和內核數據結構給子進程
- 將父進程部分數據結構內容拷貝至子進程 ( 此時已經創建了子進程的PCB即Linux下的task_struct )
- 添加子進程到系統進程列表當中 ( 即添加子進程PCB)
- fork返回,調度器開始調度
fork寫時拷貝
fork 創建子進程採取分時拷貝的策略 .
即, 父進程創建子進程時,只創建了子進程的task_struct(PCB), 並沒有直接給子進程開闢內存來拷貝數據,而是跟父進程
一樣映射到同一位置,但是如果父進程或子進程有一方想要修改內存中的數據時,那麼對於改變的這塊內存,需要重新給
子進程開闢內存,並且更新子進程頁表信息. 這樣做, 提高創建子進程的性能, 並且能節省內存 . 如下圖 :
圖1. 父子進程共享代碼與數據 圖2. 父子進程代碼共享, 數據獨立
圖裏涉及到了虛擬內存與分頁式內存管理的內容, 簡單說一下, 分頁式內存管理可以將一段程序加載到不連續的物理空間上,但是
從虛擬地址空間來看依舊是連續的, 用以解決內存使用率低的問題 .
mm_struct結構體也叫內存描述符,其中記錄虛擬內存各個段的起始地址, 結束地址, 通過這種方式描述了進程的虛擬地址空間,
每一個進程都會有唯一的mm_struct結構體, mm_struct記錄在task_struct中.
頁表: 頁表中存儲的是虛擬地址和物理地址的映射關係, 即頁號到物理塊的地址映射. 通過虛擬地址得到頁號與頁內地址(或者叫頁內偏移), 在頁表中通過頁號找到物理塊號. 然後, 物理地址 = 物理塊號 x 頁面大小 + 頁內偏移 就得到了物理地址
來段代碼感受一下
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
pid_t pid = fork();
int data = 0;
if(pid == -1){
perror("fork erro");
}
else if(pid == 0){
printf("子進程執行\n");
data = 10;
printf("data = %d\n", data);
printf("data地址: %p\n", &data);
}
else{
sleep(2);
printf("父進程執行\n");
printf("data = %d\n", data);
printf("data地址: %p\n", &data);
}
return 0;
}
代碼中, 先讓父進程睡上2秒, 這時會執行子進程, 子進程修改了data的值爲10, 但子進程結束後, 父進程繼續執行打印出的data
還是0, 兩個進程所打印的值不同, 但父子進程中data的地址都是一樣的. 我們知道數據不同, 則數據一定存儲在不同的物理地址
上, 打印的的變量地址依舊相同, 這是因爲取地址&得到的並不是物理地址(在所有有關地址的操作中, 我們只能接觸到虛擬地址),
而是虛擬地址, 虛擬地址雖然相同, 但是父子進程有着不同的mm_struct, 即有着不同的頁表, 這父子進程的data相同的(虛擬)地址
通過不同的頁表映射到不同的物理地址上.
運行結果如下圖:
fork失敗原因
- 系統進程數達到太多, 達到上限(系統會有一個進程數的限定, 可以修改)
- 內存不足 (fork創建子進程需要創建新的PCB, 寫時拷貝可能還會分配新的數據空間)
fork用法
fork()函數當然不是爲了創建子進程而創建子進程, 創建子進程目的有兩種:
- 一個父進程希望複製自己,使父子進程同時執行不同的代碼段.
例如: 父進程等待客戶端請求,生成子進程來處理請求。
- 一個進程要執行一個不同的程序 . 例如: 子進程從fork返回後,調用exec函數(用來做進程替換的函數, 下面說)替換子進程. 比
如, 我們用的Shell就是一個程序, 一般爲默認爲bash, 我們執行一些非Shell內建命令時, 實際就是一個Shell創建子進程, 再進
行進程替換的過程