- 學習交流加(可免費幫忙下載CSDN資源):
- 個人微信: liu1126137994
- 學習交流資源分享qq羣1(已滿): 962535112
- 學習交流資源分享qq羣2(已滿): 780902027
- 學習交流資源分享qq羣3:893215882
上一篇文章 點擊鏈接【Linux進程、線程、任務調度】一
講了
- Linux進程生命週期(就緒、運行、睡眠、停止、殭屍)
- 殭屍的含義
- 停止狀態與作業控制, cpulimit
- 內存泄漏的真實含義
- task_struct以及task_struct之間的關係
- 初見fork和殭屍
本篇接着上一篇文章主要記錄以下學習內容:
- fork vfork clone 的含義
- 寫時拷貝技術
- Linux線程的實現本質
- 進程0 和 進程1
- 進程的睡眠和等待隊列
- 孤兒進程的託孤 ,SUBREAPER
1、fork
fork(),創建子進程,實際上就是將父進程的task_struct結構進行一份拷貝(注意拷貝的都是指針),假設有p1進程,fork後產生p2子進程:
上面的mm ,fs,files,signal等都是task_struct結構體裏的指針,分別指向進程的內存資源,文件系統資源,文件資源,信號資源等,當父進程p1 fork後,內核把p1的task_struct拷貝一份,作爲子進程p2的描述符。此時p1和p2所指向的資源實際上是一樣的,這並不與進程是佔有獨立的空間矛盾,因爲後面對資源進型任何的修改將導致資源分裂,比如當p1(或p2)對fs,files,signal等資源執行操作,將導致fs,files,signal各分裂一份作爲p1(或p2)的資源。
其中對於mm(內存)的情況,就比較複雜,有一種技術:寫時拷貝(copy on write)
2、寫時拷貝(Copy on write)
看下圖:
最開始的時候進程p1,假設某一塊的虛擬內存爲virt1,virt1所映射的物理內存爲phy1,原則上virt1與phy1是可讀可寫的。當p1調用fork()後,產生了新的虛存和物理內存表示子進程p2的某一塊地址,實際上此時p1和p2的是指向同樣的物理內存地址,並且這塊內存變得只讀了 。假設p2(p1)要對這塊內存進行寫操作,就會產生page fault,此時就會重新開闢一塊物理內存空間,讓p2(p1)的virt1映射到新的物理內存phy2,然後重新對phy2的內存進行寫操作。
我們注意到,這個過程中,需要有MMU進行地址翻譯,所以寫時拷貝技術必須要有MMU才能實現。
無MMU,不能寫時拷貝,不能fork
- 實驗
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int data = 10;
int child_process()
{
printf("Child process %d, data %d\n",getpid(),data);
data = 20;
printf("Child process %d, data %d\n",getpid(),data);
sleep(15);
printf("Child process %d exit\n",getpid());
_exit(0);
}
int main(int argc, char* argv[])
{
int pid;
pid = fork();
if(pid==0) {
child_process();
}
else{
sleep(1);
printf("Parent process %d, data %d\n",getpid(), data);
sleep(20);
printf("Parent process %d exit\n",getpid());
exit(0);
}
}
編譯運行結果:
- 結果分析
以上程序父進程fork後,子進程對全局變量進行寫,物理內存部分進行分裂,使得子進程與父進程data變量對應的物理內存部分分離(寫時拷貝)。從此以後父子進程各自讀寫data將不會影響彼此,且父子進程的運行是獨立的,可以同時運行。
3、vfork
那麼如果沒有MMU,該如何呢?vfork在無MMU的場合下使用。
無MMU時只能使用vfork
vfork在以下情況下使用:
父進程阻塞直到子進程:
- exit 或 _exit
- exec
vfork實際上內部是對mm(內存資源)進行一個clone,而不是copy,其他資源還是copy(因爲其他資源不受MMU影響),見下圖:
- 實驗
#include <sched.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int data = 10;
int child_process()
{
printf("Child process %d, data %d\n",getpid(),data);
data = 20;
printf("Child process %d, data %d\n",getpid(),data);
sleep(15);
printf("Child process %d exit\n",getpid());
_exit(0);
}
int main(int argc, char* argv[])
{
int pid;
pid = vfork();
if(pid==0) {
child_process();
}
else{
sleep(1);
printf("Parent process %d, data %d\n",getpid(), data);
sleep(20);
printf("Parent process %d exit\n",getpid());
exit(0);
}
}
- 運行結果:
- 結果分析
由結果可以看出vfork與fork的區別:vfork的mm部分,是clone的而非copy的。父進程在子進程exit或者exec之前一直都處於阻塞狀態(可以自己運行下看看sleep效果)。
4、Linux線程的實現本質
線程,共享進程的所有資源!那麼內部是如何實現的呢?
實際上pthread_create內部就是調用clone,當進程(線程)p1調用pthread_create,內部就是調用clone,新生成的線程p2與原來的線程p1共享所有資源。
其實,此時可以看成是p1和p2的task_struct結構體內的指向資源的指針是一樣的。多線程是共享資源的。
我們可以看到,線程p1和p2都是有task_struct的,而且裏面的資源是一樣的,內核的調度器只看task_struct,所以進程,線程,都是可以被調度的對象。線程也被叫做輕量級進程。
5、PID與TGID
POSIX規定,進程中的多個線程getpid()後應該獲得同一個id(主線程id(TGID)),但是實際上每一個線程都有一個task_struct結構體,這個結構體中存有各個線程的id(PID)。
爲了解決有兩個id的情況,內核搞出了一個TGID,每一個線程的TGID都是相等的,等於主線程的id。
假設現在有進程進程p1,它創建了三個子進程:
其中:
1、top 查看的是進程的視角,查看的id實際上是各個進程(線程)的TGID
2、top -H是線程視角,查看的是各個線程的自己獨有的id即PID
看以下程序:
#include <stdio.h>
#include <pthread.h>
#include <stdio.h>
#include <linux/unistd.h>
#include <sys/syscall.h>
static pid_t gettid( void )
{
return syscall(__NR_gettid);
}
static void *thread_fun(void *param)
{
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
while(1);
return NULL;
}
int main(void)
{
pthread_t tid1, tid2;
int ret;
printf("thread pid:%d, tid:%d pthread_self:%lu\n", getpid(), gettid(),pthread_self());
ret = pthread_create(&tid1, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return -1;
}
ret = pthread_create(&tid2, NULL, thread_fun, NULL);
if (ret == -1) {
perror("cannot create new thread");
return -1;
}
if (pthread_join(tid1, NULL) != 0) {
perror("call pthread_join function fail");
return -1;
}
if (pthread_join(tid2, NULL) != 0) {
perror("call pthread_join function fail");
return -1;
}
return 0;
}
- 運行結果:
可以看出此時程序處於死循環,getpid獲得的id是一樣的,gettid獲得的是線程結構體中的id。所以是不一樣的,而pthread_self 並不是任何id,這裏我們不關心pthread_slef獲得id,我們只關心PID與TGID。
另開一個終端執行命令:
$ top
可得知,只能看到一個thread,實際上就是我們的進程(主線程),它的id也是進程的id。top命令只能看到進程的視角,看到的都是進程與進程的id,看不到線程與線程id
$ top -H
可看到,兩個被創建出來的線程thread,且它們的id都是各自的task_struct裏面的id(PID),而不是進程的id(TGID)。top -H 看到的是線程視角,顯示的id是線程的獨有的id(PID)。這裏id名詞較多,容易弄混,知道原理即可。
6、SUBREAPER與託孤
- 孤兒進程
當父進程死掉,子進程就被稱爲孤兒進程
對孤兒進程,有一個問題,就是父進程掛掉了,孤兒進程最後怎門辦,因爲沒有人來回收它了。
在Linux中,當父進程掛掉,子進程會在進程樹上向上找subreaper進程,當找到subreaper進程,孤兒進程就會掛到subreaper進程下面成爲subreaper進程的子進程,後面就由subreaper進程對該孤兒進程進行回收。如果沒有subreaper進程,那麼最終該孤兒進程會掛到init 1號進程下,由init進程回收。如下圖:
此過程,稱爲託孤!
Linux內核中,有一種方法可以將某一進程變爲subreaper進程:
prctl函數可以使當前調用它的進程變爲subreaper進程。
PR_SET_CHILD_SUBREAPER是Linux 3.4引入的新特性,將它設置爲非零值,就可以使當前進程變爲像1號進程那樣的subreaper進程,可以對孤兒進程進行收養了。
- 實驗
life_period.c
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <unistd.h>
int main(void)
{
pid_t pid,wait_pid;
int status;
pid = fork();
if (pid==-1) {
perror("Cannot create new process");
exit(1);
} else if (pid==0) {
printf("child process id: %ld\n", (long) getpid());
pause();
_exit(0);
} else {
printf("parent process id: %ld\n", (long) getpid());
wait_pid=waitpid(pid, &status, WUNTRACED | WCONTINUED);
if (wait_pid == -1) {
perror("cannot using waitpid function");
exit(1);
}
if(WIFSIGNALED(status))
printf("child process is killed by signal %d\n", WTERMSIG(status));
exit(0);
}
}
編譯程序運行結果如下:
此時,子進程處於停止狀態,父進程也處於阻塞狀態(waitpid等待子進程結束)。
輸入以下命令查看當前進程樹,可以看到我們的life_period進程與其子進程在進程樹中的位置:
$ pstree
然後,殺死父進程
$ kill -9 3532
再看進程樹:
- 結論
可以看到,當我們殺死父進程後,子進程被init進程託孤,以後如果該進程退出了,由init進程回收它的task_struct結構。
7、進程0和進程1
這是一個雞生蛋蛋生雞的故事,我們知道Linux系統中所有的進程都是init進程fork而來,init進程是內核中跑的所有進程的祖先,那麼問題來了,init進程哪裏來的?答案是,init進程是由編譯器編譯而來的?那麼編譯器又是哪裏來的?答案是編譯器是由編譯器編譯而來。那麼編譯編譯器的編譯器又是哪裏來的?
可見,這是一個死循環。實際上,最開始,是有一些大牛用0 1寫的編譯器,寫完直接可以在cpu上跑的,然後用最開始的編譯器編譯後面的編譯器。這個不是重點。今天我們的重點是0號進程。0號進程是1號進程父進程。
0號進程也叫IDLE進程。0號進程在什麼時候跑呢?
當所有其他進程,包括init進程都停止運行了,0號進程就會運行。此時0號進程會把CPU置爲低功耗,非常省電。
此時內核被置爲wait_for_interrupt狀態,除非有一箇中斷過來,纔會喚醒其他進程。
8、進程的睡眠和等待隊列
上一篇文章點擊鏈接 簡單講了深度睡眠和淺睡眠。那麼什麼情況下需要將進程置爲深度睡眠呢?
假設有進程p,它正在運行,但是整個程序代碼並沒有完全進入內存,假如p調用一個函數fun(),這個函數代碼也沒有進入到內存,那麼現在就會出現缺頁(page fault),從而內核需要去執行缺頁處理函數,這個階段進程p就會進入到睡眠狀態,假設進入淺睡眠,那麼有可能會來一個信號signal1,假設signal1的信號處理函數也沒有進入到內存,這個時候又會出現缺頁錯誤(page fault) 。。。。這樣的話,就有可能導致程序崩潰。
下面看一段代碼來理解進程的睡眠與調度:
…
…
上面程序註解非常的清晰明瞭,我們只需要注意兩點即可:
進程在阻塞讀(或者其他類似於讀的狀態如sleep)時,那個讀的函數內部一定會調用內核調度函數schedule(),讓CPU去執行其他的進程。
當有信號來(或者有資源來)的時候,進程被喚醒,這裏的喚醒,實際上是給等待隊列一個信號,然後讓隊列自己去喚醒隊列中的進程,並不是去直接喚醒進程的,此時等待隊列可以看做一箇中間機構代替我們去做複雜的喚醒工作。具體是如何實現的,在以後的學習中,還會繼續分析。
9、總結
掌握以下內容
- fork vfork clone的關係
- 寫時拷貝技術與fork,MMU的關係
- Linux線程的實現本質,內部是調用clone
- 0號進程與1號進程
- 進程的託孤與subreaper進程
- 進程的睡眠與等待隊列